329 static const ObjectRange ranges[] = {
330 {0x00, 0xF7,
"Type 1"},
331 {0x100, 0x13F,
"Type 2"},
332 {0xF80, 0xFFF,
"Type 3"},
337 int custom_count = 0;
339 custom_count += std::min(obj_manager.GetSubtypeCount(object_id),
340 kPersistedCustomSubtypeSlots);
343 const std::string custom_mode_label =
344 absl::StrFormat(
"Custom Assets (%d)", custom_count);
345 if (ImGui::BeginTable(
346 "##ObjectBrowserMode", 2,
347 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
348 ImGui::TableNextColumn();
350 0, ImVec2(ImGui::GetContentRegionAvail().x, 0.0f))) {
353 ImGui::TableNextColumn();
354 if (ImGui::Selectable(custom_mode_label.c_str(),
356 ImVec2(ImGui::GetContentRegionAvail().x, 0.0f))) {
368 const ImGuiStyle& style = ImGui::GetStyle();
369 const float control_spacing = std::max(2.0f, style.ItemSpacing.x * 0.5f);
372 ImGuiStyleVar_ItemSpacing,
373 ImVec2(control_spacing, std::max(2.0f, style.ItemSpacing.y * 0.5f)));
375 const float toolbar_width =
376 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
377 ImGui::SetNextItemWidth(toolbar_width);
378 ImGui::InputTextWithHint(
"##ObjectSearch",
"Search by name or hex ID...",
382 static const char* kFilterLabels[] = {
383 "All categories",
"Walls",
"Floors",
"Chests",
384 "Doorways",
"Decor",
"Stairs"};
385 static const char* kStreamLabels[] = {
"All streams",
"Type 1",
"Type 2",
387 constexpr const char* kMoreLabel =
"More##ObjectSelectorMore";
388 const float more_width = ImGui::CalcTextSize(kMoreLabel,
nullptr,
true).x +
389 style.FramePadding.x * 2.0f;
390 const bool stack_filters = toolbar_width < 230.0f;
391 const float filter_row_width =
392 std::max(1.0f, toolbar_width - more_width - control_spacing);
393 const float category_width =
394 stack_filters ? toolbar_width : filter_row_width * 0.56f;
395 const float stream_width =
396 stack_filters ? filter_row_width
397 : std::max(1.0f, filter_row_width - category_width -
400 ImGui::SetNextItemWidth(category_width);
402 IM_ARRAYSIZE(kFilterLabels));
405 tr(
"Doorways are tile objects. Use the Door Editor "
406 "to place or change room doors."));
408 if (!stack_filters) {
409 ImGui::SameLine(0.0f, control_spacing);
411 ImGui::SetNextItemWidth(stream_width);
413 IM_ARRAYSIZE(kStreamLabels));
414 ImGui::SameLine(0.0f, control_spacing);
415 if (ImGui::Button(kMoreLabel, ImVec2(more_width, 0.0f))) {
416 ImGui::OpenPopup(
"##ObjectSelectorOptionsPopup");
418 if (ImGui::BeginPopup(
"##ObjectSelectorOptionsPopup")) {
422 if (ImGui::MenuItem(tr(
"Clear filters"),
nullptr,
false, has_filters)) {
428 ImGui::SeparatorText(tr(
"Display"));
430 if (ImGui::IsItemHovered()) {
432 tr(
"Show rendered object thumbnails in the selector.\n"
433 "Requires a room to be loaded and may cost some performance."));
435 ImGui::SeparatorText(tr(
"Card size"));
436 for (
int density = 0; density < 3; ++density) {
437 if (ImGui::MenuItem(kObjectGridDensityLabels[density],
nullptr,
449 const float child_height = std::max(ImGui::GetContentRegionAvail().y, 1.0f);
450 if (ImGui::BeginChild(
"##ObjectGrid", ImVec2(0, child_height),
false)) {
451 const float item_spacing = control_spacing;
456 ImGui::GetContentRegionAvail().x,
458 const float item_size = grid_layout.item_size;
459 const int columns = grid_layout.columns;
461 ImGuiStyleVar_ItemSpacing,
462 ImVec2(item_spacing, std::max(item_spacing, style.ItemSpacing.y)));
465 for (
const auto& range : ranges) {
472 ImGui::TextDisabled(
"%s 0x%03X-0x%03X", range.label, range.start,
476 int current_column = 0;
478 for (
int obj_id = range.start; obj_id <= range.end; ++obj_id) {
498 if (current_column > 0) {
499 ImGui::SameLine(0.0f, item_spacing);
501 ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
502 grid_layout.leading_inset);
505 ImGui::PushID(obj_id);
509 ImVec2 button_size(item_size, item_size);
511 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
514 const bool item_visible = ImGui::IsItemVisible();
521 ImVec2 button_pos = ImGui::GetItemRectMin();
525 ImDrawList* draw_list = ImGui::GetWindowDrawList();
526 const bool show_id = item_size >= 32.0f;
527 const float footer_height =
528 show_id ? std::min(item_size * 0.32f, ImGui::GetFontSize() + 3.0f)
530 const float card_padding = std::min(3.0f, item_size * 0.08f);
531 const ImVec2 preview_pos(button_pos.x + card_padding,
532 button_pos.y + card_padding);
533 const ImVec2 preview_box(
534 std::max(1.0f, item_size - card_padding * 2.0f),
535 std::max(1.0f, item_size - footer_height - card_padding * 2.0f));
538 bool rendered =
false;
544 if (item_visible && !rendered) {
546 DrawFallbackPreviewTile(
547 draw_list, preview_pos, preview_box,
552 const bool item_hovered = ImGui::IsItemHovered();
553 if (item_visible && (is_selected || item_hovered)) {
554 const ImU32 border_color =
555 ImGui::GetColorU32(is_selected ? theme.dungeon_selection_primary
556 : theme.panel_border_color);
559 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
560 border_color, 2.0f, 0, is_selected ? 2.0f : 1.0f);
567 if (item_visible && !sym.
badge.empty() && sym.
badge !=
"?") {
568 const ImVec2 badge_text_size =
569 ImGui::CalcTextSize(sym.
badge.c_str(),
nullptr,
true);
570 const float badge_pad = 2.0f;
571 const float badge_w = badge_text_size.x + badge_pad * 2.0f;
572 const float badge_h = badge_text_size.y + badge_pad;
573 const ImVec2 badge_tl(button_pos.x + item_size - badge_w,
575 const ImVec2 badge_br(button_pos.x + item_size,
576 button_pos.y + badge_h);
577 draw_list->AddRectFilled(
579 ImGui::GetColorU32(WithAlpha(theme.panel_bg_darker, 0.82f)),
584 ? theme.dungeon_object_wall
585 : theme.text_secondary_gray;
587 ImVec2(badge_tl.x + badge_pad, badge_tl.y + badge_pad * 0.5f),
588 ImGui::GetColorU32(badge_color), sym.
badge.c_str());
591 if (item_visible && show_id) {
592 const ImVec2 card_bottom(button_pos.x + item_size,
593 button_pos.y + item_size);
594 const float footer_y = card_bottom.y - footer_height;
595 draw_list->AddRectFilled(
596 ImVec2(button_pos.x, footer_y), card_bottom,
597 ImGui::GetColorU32(WithAlpha(theme.panel_bg_darker, 0.88f)),
599 std::string id_text = absl::StrFormat(
"%03X", obj_id);
600 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
601 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
602 footer_y + (footer_height - id_size.y) * 0.5f);
603 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
610 {{ImGuiCol_PopupBg, theme.panel_bg_color},
611 {ImGuiCol_Border, theme.panel_border_color}});
613 if (ImGui::BeginTooltip()) {
614 ImGui::TextColored(theme.selection_primary, tr(
"Object 0x%03X"),
616 ImGui::Text(
"%s", full_name.c_str());
618 ImGui::TextColored(theme.text_secondary_gray, tr(
"Subtype %d"),
621 rendered ? theme.status_success : theme.status_warning,
623 rendered ?
"rendered tile layout"
625 :
"thumbnails off"));
628 const uint8_t preview_size =
630 const bool can_capture_layout =
635 const uint32_t layout_key =
637 if (can_capture_layout &&
640 auto& room_ref = *layout_room;
643 if (layout_or.ok()) {
650 ImGui::TextColored(theme.status_success, tr(
"Tiles: %zu"),
651 layout.cells.size());
653 if (can_capture_layout) {
656 room_ref.get_gfx_buffer().
data());
658 ImGui::TextColored(theme.status_active, tr(
"Draw Routine: %d"),
662 ImGui::Text(tr(
"Layout:"));
663 ImDrawList* tooltip_draw_list = ImGui::GetWindowDrawList();
664 ImVec2 grid_start = ImGui::GetCursorScreenPos();
665 float cell_size = 4.0f;
666 for (
const auto& cell : layout.cells) {
667 ImVec2 p1(grid_start.x + cell.rel_x * cell_size,
668 grid_start.y + cell.rel_y * cell_size);
669 ImVec2 p2(p1.x + cell_size, p1.y + cell_size);
670 tooltip_draw_list->AddRectFilled(
671 p1, p2, ThemeColor(theme.dungeon_grid_cell_highlight));
672 tooltip_draw_list->AddRect(
673 p1, p2, ThemeColor(theme.panel_border_color));
675 ImGui::Dummy(ImVec2(layout.bounds_width * cell_size,
676 layout.bounds_height * cell_size));
683 if (!sym.
family.empty()) {
687 const bool is_named_special =
690 if (is_named_special) {
691 ImGui::TextColored(theme.item_color, tr(
"Routine: %s"),
694 ImGui::TextColored(theme.text_secondary_gray, tr(
"Routine: %s"),
698 ImGui::TextColored(theme.dungeon_object_wall,
699 tr(
" BothBG: writes to BG1 and BG2"));
704 ImGui::TextColored(theme.text_secondary_gray,
705 tr(
"Click to select for placement"));
712 current_column = (current_column + 1) % columns;
1122 const std::string custom_base_path = obj_manager.GetBasePath();
1124 ImGui::TextColored(theme.text_info,
1126 ImGui::PushTextWrapPos(0.0f);
1128 theme.text_secondary_gray,
1129 tr(
"Manage the 21 fixed runtime assets used by Oracle custom objects."));
1130 ImGui::TextDisabled(
1131 "%s", tr(
"New subtypes require an ASM dispatch-table change; this "
1132 "browser edits or places existing slots only."));
1133 ImGui::PopTextWrapPos();
1136 if (ImGui::BeginTable(
1137 "##CustomObjectToolbar", 2,
1138 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
1139 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 2.0f);
1140 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
1142 ImGui::TableNextRow();
1144 ImGui::TableNextColumn();
1145 if (custom_base_path.empty()) {
1147 " Custom object folder is not configured.");
1149 ImGui::TextColored(theme.text_secondary_gray,
1151 if (ImGui::IsItemHovered()) {
1152 ImGui::SetTooltip(
"%s", custom_base_path.c_str());
1156 ImGui::TableNextColumn();
1158 obj_manager.ReloadAll();
1162 if (ImGui::IsItemHovered()) {
1164 tr(
"Refresh custom object and external sprite previews from disk. "
1165 "Unsaved room and tile edits are kept."));
1173 " Custom Objects is disabled for this project. Enable the feature "
1174 "before editing or placing runtime assets.");
1178 ImGui::TextDisabled(
"%s", tr(
"Card size"));
1180 ImGui::SetNextItemWidth(-1.0f);
1182 kObjectGridDensityLabels,
1183 IM_ARRAYSIZE(kObjectGridDensityLabels));
1185 ImGui::PushTextWrapPos(0.0f);
1188 " Track tile layouts are assets here; routes and collision are managed "
1189 "in Minecart Tracks.");
1190 ImGui::PopTextWrapPos();
1193 struct CustomAssetFamily {
1196 const char* description;
1198 static constexpr std::array<CustomAssetFamily, 3> kFamilies = {{
1199 {0x31,
"Tracks + Props (16)",
1200 "Minecart tile layouts, the Sword House wall object, and a small "
1202 {0x32,
"Ice Props (3)",
"Furnace, firewood, and chair assets"},
1203 {0x54,
"Boss Bodies (2)",
"Kydreeok and Manhandla body tilemaps"},
1205 const CustomAssetFamily* selected_family = &kFamilies.front();
1206 for (
const auto& family : kFamilies) {
1208 selected_family = &family;
1212 ImGui::TextDisabled(
"%s", tr(
"Asset family"));
1213 ImGui::SetNextItemWidth(-1.0f);
1214 if (ImGui::BeginCombo(
"##CustomAssetFamily", selected_family->label)) {
1215 for (
const auto& family : kFamilies) {
1217 if (ImGui::Selectable(family.label, selected)) {
1220 selected_family = &family;
1224 ImGui::SetItemDefaultFocus();
1229 ImGui::PushTextWrapPos(0.0f);
1230 ImGui::TextDisabled(
"%s", tr(selected_family->description));
1231 ImGui::PopTextWrapPos();
1234 const ImGuiStyle& style = ImGui::GetStyle();
1235 const float item_spacing = std::max(2.0f, style.ItemSpacing.x * 0.5f);
1237 ImGui::GetContentRegionAvail().x,
1239 const int columns = grid_layout.columns;
1240 const float item_size = grid_layout.item_size;
1242 ImGuiStyleVar_ItemSpacing,
1243 ImVec2(item_spacing, std::max(item_spacing, style.ItemSpacing.y)));
1249 const int subtype_count = std::min(obj_manager.GetSubtypeCount(obj_id),
1250 kPersistedCustomSubtypeSlots);
1251 for (
int subtype = 0; subtype < subtype_count; ++subtype) {
1252 const std::string subtype_name =
1255 if (custom_col > 0) {
1256 ImGui::SameLine(0.0f, item_spacing);
1258 ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
1259 grid_layout.leading_inset);
1262 ImGui::PushID(obj_id * 1000 + subtype);
1264 const absl::Status asset_status =
1266 const bool asset_ready = asset_status.ok();
1270 ImVec2 button_size(item_size, item_size);
1272 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
1277 const bool item_visible = ImGui::IsItemVisible();
1278 ImVec2 button_pos = ImGui::GetItemRectMin();
1279 if (asset_ready &&
rooms_ !=
nullptr &&
1284 static_cast<uint8_t
>(subtype)));
1286 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1287 const bool show_id = item_size >= 44.0f;
1288 const float footer_height =
1289 show_id ? std::min(item_size * 0.32f, ImGui::GetFontSize() + 3.0f)
1291 const float card_padding = std::min(3.0f, item_size * 0.08f);
1292 const ImVec2 preview_pos(button_pos.x + card_padding,
1293 button_pos.y + card_padding);
1294 const ImVec2 preview_box(
1295 std::max(1.0f, item_size - card_padding * 2.0f),
1296 std::max(1.0f, item_size - footer_height - card_padding * 2.0f));
1298 bool rendered =
false;
1302 obj_id,
static_cast<uint8_t
>(subtype));
1306 if (item_visible && !rendered) {
1307 std::string sub_text = absl::StrFormat(
"%02X", subtype);
1308 DrawFallbackPreviewTile(
1309 draw_list, preview_pos, preview_box,
1310 asset_ready ? theme.status_success : theme.status_error,
1314 const bool item_hovered = ImGui::IsItemHovered();
1315 if (item_visible && (is_selected || item_hovered)) {
1316 const ImU32 border_color =
1317 ImGui::GetColorU32(is_selected ? theme.dungeon_selection_primary
1318 : theme.panel_border_color);
1321 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
1322 border_color, 2.0f, 0, is_selected ? 2.0f : 1.0f);
1325 if (item_visible && show_id) {
1326 const ImVec2 card_bottom(button_pos.x + item_size,
1327 button_pos.y + item_size);
1328 const float footer_y = card_bottom.y - footer_height;
1329 draw_list->AddRectFilled(
1330 ImVec2(button_pos.x, footer_y), card_bottom,
1331 ImGui::GetColorU32(WithAlpha(theme.panel_bg_darker, 0.88f)),
1333 std::string id_text = absl::StrFormat(
"%02X:%02X", obj_id, subtype);
1334 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
1335 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
1336 footer_y + (footer_height - id_size.y) * 0.5f);
1337 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
1343 {{ImGuiCol_PopupBg, theme.panel_bg_color},
1344 {ImGuiCol_Border, theme.panel_border_color}});
1345 if (ImGui::BeginTooltip()) {
1346 const auto binding_or =
1347 obj_manager.ResolveSlotBinding(obj_id, subtype);
1348 const std::string filename =
1349 binding_or.ok() ? binding_or->filename :
"";
1351 ImGui::TextColored(theme.selection_primary,
1352 tr(
"Custom 0x%02X:%02X"), obj_id, subtype);
1353 ImGui::Text(
"%s", subtype_name.c_str());
1355 rendered ? theme.status_success : theme.status_warning,
1357 rendered ?
"rendered custom layout"
1359 :
"thumbnails off"));
1361 ImGui::Text(tr(
"File: %s"),
1362 filename.empty() ?
"(unmapped)" : filename.c_str());
1363 if (binding_or.ok()) {
1364 const ImVec4 mapping_color =
1365 binding_or->origin ==
1367 ? theme.status_active
1368 : (binding_or->origin ==
1369 zelda3::CustomObjectMappingOrigin::
1370 kConfiguredSlotUnmapped
1371 ? theme.status_error
1372 : theme.text_secondary_gray);
1374 mapping_color, tr(
"Mapping: %s"),
1375 tr(CustomObjectMappingOriginLabel(binding_or->origin)));
1378 ImGui::TextColored(theme.status_success,
1379 tr(
"Asset decoded and ready"));
1381 ImGui::TextColored(theme.status_error,
"%s",
1382 asset_status.message().data());
1385 ImGui::EndTooltip();
1390 custom_col = (custom_col + 1) % columns;
1397 const auto selected_binding_or = obj_manager.ResolveSlotBinding(
1399 const std::string selected_filename =
1400 selected_binding_or.ok() ? selected_binding_or->filename :
"";
1403 const auto selected_path_or =
1405 const bool selected_asset_ready = selected_asset_status.ok();
1406 ImGui::TextColored(theme.selection_primary,
"0x%02X:%02X %s",
1408 selected_name.c_str());
1410 ImGui::TextDisabled(
"%s", selected_filename.empty()
1412 : selected_filename.c_str());
1413 if (selected_binding_or.ok()) {
1414 const ImVec4 mapping_color =
1415 selected_binding_or->origin ==
1417 ? theme.status_active
1418 : (selected_binding_or->origin ==
1419 zelda3::CustomObjectMappingOrigin::
1420 kConfiguredSlotUnmapped
1421 ? theme.status_error
1422 : theme.text_secondary_gray);
1424 mapping_color, tr(
"Mapping: %s"),
1425 tr(CustomObjectMappingOriginLabel(selected_binding_or->origin)));
1427 if (selected_asset_ready) {
1428 ImGui::TextColored(theme.status_success,
1430 if (selected_path_or.ok() && ImGui::IsItemHovered()) {
1431 ImGui::SetTooltip(
"%s", selected_path_or->string().c_str());
1435 selected_asset_status.message().data());
1437 ImGui::PushTextWrapPos(0.0f);
1439 ImGui::TextDisabled(
1440 "%s", tr(
"Custom 0x31 wall object; separate from vanilla wall and "
1441 "corner tile tables"));
1444 ImGui::TextDisabled(
"%s", tr(
"Tile layout only; behavior is separate"));
1448 " Tilemap editing is available; external boss pixels "
1449 "are not loaded into the room preview yet.");
1450 if (ImGui::IsItemHovered()) {
1453 tr(
"Oracle forces nonzero body tiles into graphics page 0x300 and "
1454 "DMA-loads separate Kydreeok or Manhandla graphics at runtime. "
1455 "Yaze currently preserves the correct tilemap geometry and raw "
1456 "source words, but the displayed pixels are not visual-parity "
1460 ImGui::PopTextWrapPos();
1466 const bool has_room_context =
1468#if defined(__EMSCRIPTEN__)
1469 constexpr bool kCanPublishCustomAssets =
false;
1471 constexpr bool kCanPublishCustomAssets =
true;
1473 const bool can_edit = selected_asset_ready && has_room_context &&
1475 kCanPublishCustomAssets;
1476 const bool can_use_in_room = selected_asset_ready && has_room_context;
1477 if (ImGui::BeginTable(
1478 "##CustomAssetActions", 2,
1479 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
1480 ImGui::TableNextColumn();
1482 ImGui::BeginDisabled();
1484 if (ImGui::Button(
ICON_MD_EDIT " Edit Tile Layout", ImVec2(-1.0f, 0.0f))) {
1495 ImGui::EndDisabled();
1497 if (!kCanPublishCustomAssets &&
1498 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1500 "%s", tr(
"Custom asset publishing is desktop-only until browser "
1501 "persistence can be verified."));
1504 ImGui::TableNextColumn();
1505 if (!can_use_in_room) {
1506 ImGui::BeginDisabled();
1508 if (ImGui::Button(
ICON_MD_ADD " Place in Room", ImVec2(-1.0f, 0.0f))) {
1512 if (!can_use_in_room) {
1513 ImGui::EndDisabled();
1520 const bool can_open_minecart =
1522 if (!can_open_minecart) {
1523 ImGui::BeginDisabled();
1525 if (ImGui::Button(
ICON_MD_TRAIN " Minecart Routes & Collision",
1526 ImVec2(-1.0f, 0.0f))) {
1531 "Minecart Tracks window is unavailable in this session";
1534 if (!can_open_minecart) {
1535 ImGui::EndDisabled();