343 ImGui::TextDisabled(
"This project has no custom rail assets.");
347 ImGui::GetContentRegionAvail().x, 64.0f, ImGui::GetStyle().ItemSpacing.x);
349 ImGui::PushID(
"MinecartRailPalette");
352 static constexpr std::array<std::array<int, 7>, 2> kFamilies = {
353 {{7, 6, 8, 9, 10, 11, 12}, {0, 1, 2, 3, 4, 5, 14}}};
354 for (
size_t family = 0; family < kFamilies.size(); ++family) {
355 ImGui::TextDisabled(
"%s",
356 family == 0 ?
"Floor-style rails" :
"Pit-style rails");
358 for (
int subtype : kFamilies[family]) {
363 const auto label = absl::StrFormat(
"##Rail%d", subtype);
364 ImGui::BeginDisabled(!status.ok());
365 if (ImGui::Selectable(label.c_str(),
368 0, ImVec2(layout.item_size, layout.item_size))) {
373 absl::StrFormat(
"Rail%d", subtype));
377 ImGui::EndDisabled();
378 const auto top = ImGui::GetItemRectMin();
381 const bool rendered =
382 status.ok() && ImGui::IsItemVisible() &&
384 ImVec2(layout.item_size - 6, layout.item_size - 6));
386 ImGui::GetWindowDrawList()->AddText(
387 top, ImGui::GetColorU32(ImGuiCol_TextDisabled),
388 status.ok() ?
"Preview..." :
"Missing");
390 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
391 ImGui::BeginTooltip();
392 ImGui::TextUnformatted(name.c_str());
393 ImGui::TextUnformatted(status.ok()
394 ?
"Click, then place on the room canvas."
395 : std::string(status.message()).c_str());
396 if (status.ok() && family == 0) {
397 ImGui::TextUnformatted(
398 "Uses fixed floor tiles; check the room's graphics and palette.");
402 column = (column + 1) % layout.columns;
412 constexpr const char* categories[] = {
413 "All objects",
"Walls",
"Floors",
"Decorations",
"Interactive",
414 "Stairs",
"Doors",
"Chests",
"Special",
"Custom assets"};
418 ImGui::TextDisabled(
"Custom objects are disabled");
426 std::vector<Asset> assets;
430 kPersistedCustomSubtypeSlots);
431 for (
int slot = 0; slot < count; ++slot) {
433 const std::string key = absl::StrFormat(
"%02X:%02X",
id, slot);
438 assets.push_back({id, slot, name});
442 std::stable_sort(assets.begin(), assets.end(),
443 [](
const Asset& a,
const Asset& b) {
444 return workbench::DungeonBrowserFold(a.name) <
445 workbench::DungeonBrowserFold(b.name);
447 if (ImGui::BeginChild(
"##CustomLibrary", ImVec2(0, 0),
false)) {
448 for (
const auto& asset : assets) {
449 ImGui::PushID(asset.id * 1000 + asset.slot);
451 ImGui::BeginDisabled(!status.ok());
453 asset.id, asset.name,
456 [&](ImVec2 pos, ImVec2 size) {
457 auto object = MakePreviewObject(asset.id);
458 object.size_ = zelda3::CanonicalRoomObjectSize(
459 asset.id, static_cast<uint8_t>(asset.slot));
460 return DrawObjectPreview(object, pos, size);
462 absl::StrFormat(
"%02X:%02X", asset.id, asset.slot));
463 ImGui::EndDisabled();
465 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
466 ImGui::SetTooltip(
"%s", std::string(status.message()).c_str());
467 if (picked && status.ok())
474 asset.id,
static_cast<uint8_t
>(asset.slot)));
478 ImGui::TextDisabled(
"No matches");
483 std::vector<std::pair<int, std::string>> entries;
484 for (
const auto range : {std::pair{0x000, 0x0F7}, std::pair{0x100, 0x13F},
485 std::pair{0xF80, 0xFFF}}) {
486 for (
int id = range.first;
id <= range.second; ++id) {
497 entries.emplace_back(
id, std::move(name));
501 std::stable_sort(entries.begin(), entries.end(),
502 [](
const auto& a,
const auto& b) {
503 return workbench::DungeonBrowserFold(a.second) <
504 workbench::DungeonBrowserFold(b.second);
506 if (ImGui::BeginChild(
"##ObjectLibrary", ImVec2(0, 0),
false)) {
507 for (
const auto& [
id, name] : entries) {
511 [&,
id =
id](ImVec2 pos, ImVec2 size) {
516 absl::StrFormat(
"place_object_%03X",
id));
524 ImGui::TextDisabled(
"No matches");
542 static const ObjectRange ranges[] = {
543 {0x00, 0xF7,
"Type 1"},
544 {0x100, 0x13F,
"Type 2"},
545 {0xF80, 0xFFF,
"Type 3"},
550 int custom_count = 0;
552 custom_count += std::min(obj_manager.GetSubtypeCount(object_id),
553 kPersistedCustomSubtypeSlots);
556 const std::string custom_mode_label =
557 absl::StrFormat(
"Custom Assets (%d)", custom_count);
558 if (ImGui::BeginTable(
559 "##ObjectBrowserMode", 2,
560 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
561 ImGui::TableNextColumn();
563 0, ImVec2(ImGui::GetContentRegionAvail().x, 0.0f))) {
566 ImGui::TableNextColumn();
567 if (ImGui::Selectable(custom_mode_label.c_str(),
569 ImVec2(ImGui::GetContentRegionAvail().x, 0.0f))) {
581 const ImGuiStyle& style = ImGui::GetStyle();
582 const float control_spacing = std::max(2.0f, style.ItemSpacing.x * 0.5f);
585 ImGuiStyleVar_ItemSpacing,
586 ImVec2(control_spacing, std::max(2.0f, style.ItemSpacing.y * 0.5f)));
588 const float toolbar_width =
589 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
590 const float button_h = ImGui::GetFrameHeight();
592 const float size_btn_w = button_h;
593 const float more_btn_w = button_h;
594 const float clear_btn_w = has_search ? button_h : 0.0f;
595 const float search_w =
596 std::max(60.0f, toolbar_width - size_btn_w - more_btn_w - clear_btn_w -
597 control_spacing * (has_search ? 3.0f : 2.0f));
599 ImGui::SetNextItemWidth(search_w);
600 ImGui::InputTextWithHint(
"##ObjectSearch",
"Search by name or hex ID...",
604 ImGui::SameLine(0.0f, control_spacing);
606 ImVec2(clear_btn_w, 0.0f))) {
609 if (ImGui::IsItemHovered()) {
610 ImGui::SetTooltip(tr(
"Clear search"));
613 ImGui::SameLine(0.0f, control_spacing);
617 if (ImGui::Button(size_icon, ImVec2(size_btn_w, 0.0f))) {
620 if (ImGui::IsItemHovered()) {
621 ImGui::SetTooltip(tr(
"Card size: %s (click to cycle)"),
624 ImGui::SameLine(0.0f, control_spacing);
626 ImVec2(more_btn_w, 0.0f))) {
627 ImGui::OpenPopup(
"##ObjectSelectorOptionsPopup");
629 if (ImGui::IsItemHovered()) {
630 ImGui::SetTooltip(tr(
"Object selector options"));
633 static const char* kFilterLabels[] = {
634 "All categories",
"Walls",
"Floors",
"Chests",
635 "Doorways",
"Decor",
"Stairs"};
636 static const char* kStreamLabels[] = {
"All streams",
"Type 1",
"Type 2",
639 const float used_width = ImGui::GetFrameHeight() +
640 ImGui::CalcTextSize(
"Used here").x +
641 style.ItemInnerSpacing.x;
642 const float category_width =
643 std::max(1.0f, toolbar_width - used_width - control_spacing);
645 ImGui::SetNextItemWidth(category_width);
647 IM_ARRAYSIZE(kFilterLabels));
650 tr(
"Doorways are tile objects. Use the Door Editor "
651 "to place or change room doors."));
653 ImGui::SameLine(0.0f, control_spacing);
655 if (ImGui::BeginPopup(
"##ObjectSelectorOptionsPopup")) {
659 if (ImGui::MenuItem(tr(
"Clear filters"),
nullptr,
false, has_filters)) {
666 ImGui::SetNextItemWidth(130.0f);
668 IM_ARRAYSIZE(kStreamLabels));
670 ImGui::SeparatorText(tr(
"Display"));
672 if (ImGui::IsItemHovered()) {
674 tr(
"Show rendered object thumbnails in the selector.\n"
675 "Requires a room to be loaded and may cost some performance."));
677 ImGui::SeparatorText(tr(
"Card size"));
678 for (
int density = 0; density < 3; ++density) {
679 if (ImGui::MenuItem(kObjectGridDensityLabels[density],
nullptr,
689 const auto name_strip = ReserveBrowserNameStrip();
690 std::string browser_name =
694 : tr(
"Point to an object; right-click for details");
698 const float child_height = std::max(ImGui::GetContentRegionAvail().y,
699 std::max(minimum_grid_height, 1.0f));
700 if (ImGui::BeginChild(
"##ObjectGrid", ImVec2(0, child_height),
false)) {
701 const float item_spacing = control_spacing;
706 ImGui::GetContentRegionAvail().x,
708 const float item_size = grid_layout.item_size;
709 const int columns = grid_layout.columns;
711 ImGuiStyleVar_ItemSpacing,
712 ImVec2(item_spacing, std::max(item_spacing, style.ItemSpacing.y)));
715 for (
const auto& range : ranges) {
722 ImGui::TextDisabled(
"%s 0x%03X-0x%03X", range.label, range.start,
726 int current_column = 0;
728 for (
int obj_id = range.start; obj_id <= range.end; ++obj_id) {
750 if (current_column > 0) {
751 ImGui::SameLine(0.0f, item_spacing);
753 ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
754 grid_layout.leading_inset);
757 ImGui::PushID(obj_id);
761 ImVec2 button_size(item_size, item_size);
763 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
766 const bool item_visible = ImGui::IsItemVisible();
773 ImVec2 button_pos = ImGui::GetItemRectMin();
778 ImDrawList* draw_list = ImGui::GetWindowDrawList();
779 const bool show_id = item_size >= 32.0f;
780 const float footer_height =
781 show_id ? std::min(item_size * 0.32f, ImGui::GetFontSize() + 3.0f)
783 const float card_padding = std::min(3.0f, item_size * 0.08f);
784 const ImVec2 preview_pos(button_pos.x + card_padding,
785 button_pos.y + card_padding);
786 const ImVec2 preview_box(
787 std::max(1.0f, item_size - card_padding * 2.0f),
788 std::max(1.0f, item_size - footer_height - card_padding * 2.0f));
791 bool rendered =
false;
797 if (item_visible && !rendered) {
799 DrawFallbackPreviewTile(
800 draw_list, preview_pos, preview_box,
805 const bool item_hovered = ImGui::IsItemHovered();
806 if (item_visible && (is_selected || item_hovered)) {
807 const ImU32 border_color =
808 ImGui::GetColorU32(is_selected ? theme.dungeon_selection_primary
809 : theme.panel_border_color);
812 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
813 border_color, 2.0f, is_selected ? 2.0f : 1.0f);
820 if (item_visible && !sym.
badge.empty() && sym.
badge !=
"?") {
821 const ImVec2 badge_text_size =
822 ImGui::CalcTextSize(sym.
badge.c_str(),
nullptr,
true);
823 const float badge_pad = 2.0f;
824 const float badge_w = badge_text_size.x + badge_pad * 2.0f;
825 const float badge_h = badge_text_size.y + badge_pad;
826 const ImVec2 badge_tl(button_pos.x + item_size - badge_w,
828 const ImVec2 badge_br(button_pos.x + item_size,
829 button_pos.y + badge_h);
830 draw_list->AddRectFilled(
832 ImGui::GetColorU32(WithAlpha(theme.panel_bg_darker, 0.82f)),
837 ? theme.dungeon_object_wall
838 : theme.text_secondary_gray;
840 ImVec2(badge_tl.x + badge_pad, badge_tl.y + badge_pad * 0.5f),
841 ImGui::GetColorU32(badge_color), sym.
badge.c_str());
844 if (item_visible && show_id) {
845 const ImVec2 card_bottom(button_pos.x + item_size,
846 button_pos.y + item_size);
847 const float footer_y = card_bottom.y - footer_height;
848 draw_list->AddRectFilled(
849 ImVec2(button_pos.x, footer_y), card_bottom,
850 ImGui::GetColorU32(WithAlpha(theme.panel_bg_darker, 0.88f)),
852 std::string id_text = absl::StrFormat(
"%03X", obj_id);
853 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
854 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
855 footer_y + (footer_height - id_size.y) * 0.5f);
856 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
861 browser_name = absl::StrFormat(
"0x%03X %s", obj_id, full_name);
866 current_column = (current_column + 1) % columns;
872 DrawBrowserNameStrip(name_strip, browser_name);
912 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 260.0f);
915 const auto binding = manager.ResolveSlotBinding(object_id, subtype);
916 ImGui::TextColored(theme.selection_primary,
"Custom 0x%02X:%02X", object_id,
921 ImGui::TextWrapped(
"File: %s", binding.ok() && !binding->filename.empty()
922 ? binding->filename.c_str()
925 ImGui::TextWrapped(
"Mapping: %s",
926 CustomObjectMappingOriginLabel(binding->origin));
929 ImGui::TextColored(status.ok() ? theme.status_success : theme.status_error,
931 status.ok() ?
"Asset decoded and ready"
932 : std::string(status.message()).c_str());
934 ImGui::TextColored(theme.selection_primary,
"Object 0x%03X", object_id);
939 if (!sym.family.empty()) {
940 ImGui::TextWrapped(
"Routine: %s", sym.family.c_str());
942 if (sym.dual_layer) {
943 ImGui::TextColored(theme.dungeon_object_wall,
944 "BothBG: writes to BG1 and BG2");
956 layout =
layout_cache_.emplace(key, std::move(*captured)).first;
960 const auto& value = layout->second;
961 ImGui::Text(
"Tiles: %zu", value.cells.size());
962 if (value.bounds_width > 0 && value.bounds_height > 0) {
964 const float scale = std::min({4.0f, 160.0f / value.bounds_width,
965 120.0f / value.bounds_height});
966 const ImVec2 origin = ImGui::GetCursorScreenPos();
967 const ImVec2 size(value.bounds_width * scale,
968 value.bounds_height * scale);
969 auto* draw = ImGui::GetWindowDrawList();
971 origin, ImVec2(origin.x + size.x, origin.y + size.y),
true);
972 for (
const auto& cell : value.cells) {
973 const ImVec2 start(origin.x + cell.rel_x * scale,
974 origin.y + cell.rel_y * scale);
975 const ImVec2 end(start.x + scale, start.y + scale);
976 draw->AddRectFilled(start, end,
977 ThemeColor(theme.dungeon_grid_cell_highlight));
978 draw->AddRect(start, end, ThemeColor(theme.panel_border_color));
986 ImGui::PopTextWrapPos();
1416 const std::string custom_base_path = obj_manager.GetBasePath();
1418 ImGui::TextColored(theme.text_info,
1420 ImGui::PushTextWrapPos(0.0f);
1422 theme.text_secondary_gray,
1423 tr(
"Manage the 21 fixed runtime assets used by Oracle custom objects."));
1424 ImGui::TextDisabled(
1425 "%s", tr(
"New subtypes require an ASM dispatch-table change; this "
1426 "browser edits or places existing slots only."));
1427 ImGui::PopTextWrapPos();
1430 if (ImGui::BeginTable(
1431 "##CustomObjectToolbar", 2,
1432 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
1433 ImGui::TableSetupColumn(
"Status", ImGuiTableColumnFlags_WidthStretch, 2.0f);
1434 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
1436 ImGui::TableNextRow();
1438 ImGui::TableNextColumn();
1439 if (custom_base_path.empty()) {
1441 " Custom object folder is not configured.");
1443 ImGui::TextColored(theme.text_secondary_gray,
1445 if (ImGui::IsItemHovered()) {
1446 ImGui::SetTooltip(
"%s", custom_base_path.c_str());
1450 ImGui::TableNextColumn();
1452 obj_manager.ReloadAll();
1456 if (ImGui::IsItemHovered()) {
1458 tr(
"Refresh custom object and external sprite previews from disk. "
1459 "Unsaved room and tile edits are kept."));
1467 " Custom Objects is disabled for this project. Enable the feature "
1468 "before editing or placing runtime assets.");
1472 ImGui::TextDisabled(
"%s", tr(
"Card size"));
1474 ImGui::SetNextItemWidth(-1.0f);
1476 kObjectGridDensityLabels,
1477 IM_ARRAYSIZE(kObjectGridDensityLabels));
1479 ImGui::PushTextWrapPos(0.0f);
1482 " Track tile layouts are assets here; routes and collision are managed "
1483 "in Minecart Tracks.");
1484 ImGui::PopTextWrapPos();
1487 struct CustomAssetFamily {
1490 const char* description;
1492 static constexpr std::array<CustomAssetFamily, 3> kFamilies = {{
1493 {0x31,
"Tracks + Props (16)",
1494 "Minecart tile layouts, the Sword House wall object, and a small "
1496 {0x32,
"Ice + Minish Props (5)",
1497 "Furnace, firewood, chair, Minish stump, and Minish wall hole assets"},
1498 {0x54,
"Boss Bodies (2)",
"Kydreeok and Manhandla body tilemaps"},
1500 const CustomAssetFamily* selected_family = &kFamilies.front();
1501 for (
const auto& family : kFamilies) {
1503 selected_family = &family;
1507 ImGui::TextDisabled(
"%s", tr(
"Asset family"));
1508 ImGui::SetNextItemWidth(-1.0f);
1509 if (ImGui::BeginCombo(
"##CustomAssetFamily", selected_family->label)) {
1510 for (
const auto& family : kFamilies) {
1512 if (ImGui::Selectable(family.label, selected)) {
1515 selected_family = &family;
1519 ImGui::SetItemDefaultFocus();
1524 ImGui::PushTextWrapPos(0.0f);
1525 ImGui::TextDisabled(
"%s", tr(selected_family->description));
1526 ImGui::PopTextWrapPos();
1528 const auto name_strip = ReserveBrowserNameStrip();
1529 std::string browser_name = absl::StrFormat(
1534 const ImGuiStyle& style = ImGui::GetStyle();
1535 const float item_spacing = std::max(2.0f, style.ItemSpacing.x * 0.5f);
1537 ImGui::GetContentRegionAvail().x,
1539 const int columns = grid_layout.columns;
1540 const float item_size = grid_layout.item_size;
1542 ImGuiStyleVar_ItemSpacing,
1543 ImVec2(item_spacing, std::max(item_spacing, style.ItemSpacing.y)));
1549 const int subtype_count = std::min(obj_manager.GetSubtypeCount(obj_id),
1550 kPersistedCustomSubtypeSlots);
1551 for (
int subtype = 0; subtype < subtype_count; ++subtype) {
1552 const std::string subtype_name =
1555 if (custom_col > 0) {
1556 ImGui::SameLine(0.0f, item_spacing);
1558 ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
1559 grid_layout.leading_inset);
1562 ImGui::PushID(obj_id * 1000 + subtype);
1564 const absl::Status asset_status =
1566 const bool asset_ready = asset_status.ok();
1570 ImVec2 button_size(item_size, item_size);
1572 if (ImGui::Selectable(
"", is_selected, 0, button_size)) {
1577 const bool item_visible = ImGui::IsItemVisible();
1578 ImVec2 button_pos = ImGui::GetItemRectMin();
1580 if (asset_ready &&
rooms_ !=
nullptr &&
1585 static_cast<uint8_t
>(subtype)));
1587 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1588 const bool show_id = item_size >= 44.0f;
1589 const float footer_height =
1590 show_id ? std::min(item_size * 0.32f, ImGui::GetFontSize() + 3.0f)
1592 const float card_padding = std::min(3.0f, item_size * 0.08f);
1593 const ImVec2 preview_pos(button_pos.x + card_padding,
1594 button_pos.y + card_padding);
1595 const ImVec2 preview_box(
1596 std::max(1.0f, item_size - card_padding * 2.0f),
1597 std::max(1.0f, item_size - footer_height - card_padding * 2.0f));
1599 bool rendered =
false;
1603 obj_id,
static_cast<uint8_t
>(subtype));
1607 if (item_visible && !rendered) {
1608 std::string sub_text = absl::StrFormat(
"%02X", subtype);
1609 DrawFallbackPreviewTile(
1610 draw_list, preview_pos, preview_box,
1611 asset_ready ? theme.status_success : theme.status_error,
1615 const bool item_hovered = ImGui::IsItemHovered();
1616 if (item_visible && (is_selected || item_hovered)) {
1617 const ImU32 border_color =
1618 ImGui::GetColorU32(is_selected ? theme.dungeon_selection_primary
1619 : theme.panel_border_color);
1622 ImVec2(button_pos.x + item_size, button_pos.y + item_size),
1623 border_color, 2.0f, is_selected ? 2.0f : 1.0f);
1626 if (item_visible && show_id) {
1627 const ImVec2 card_bottom(button_pos.x + item_size,
1628 button_pos.y + item_size);
1629 const float footer_y = card_bottom.y - footer_height;
1630 draw_list->AddRectFilled(
1631 ImVec2(button_pos.x, footer_y), card_bottom,
1632 ImGui::GetColorU32(WithAlpha(theme.panel_bg_darker, 0.88f)),
1634 std::string id_text = absl::StrFormat(
"%02X:%02X", obj_id, subtype);
1635 ImVec2 id_size = ImGui::CalcTextSize(id_text.c_str());
1636 ImVec2 id_pos = ImVec2(button_pos.x + (item_size - id_size.x) / 2,
1637 footer_y + (footer_height - id_size.y) * 0.5f);
1638 draw_list->AddText(id_pos, ImGui::GetColorU32(theme.text_primary),
1644 absl::StrFormat(
"0x%02X:%02X %s", obj_id, subtype, subtype_name);
1648 custom_col = (custom_col + 1) % columns;
1652 DrawBrowserNameStrip(name_strip, browser_name);
1656 const auto selected_binding_or = obj_manager.ResolveSlotBinding(
1658 const std::string selected_filename =
1659 selected_binding_or.ok() ? selected_binding_or->filename :
"";
1662 const auto selected_path_or =
1664 const bool selected_asset_ready = selected_asset_status.ok();
1665 ImGui::TextColored(theme.selection_primary,
"0x%02X:%02X %s",
1667 selected_name.c_str());
1669 ImGui::TextDisabled(
"%s", selected_filename.empty()
1671 : selected_filename.c_str());
1672 if (selected_binding_or.ok()) {
1673 const ImVec4 mapping_color =
1674 selected_binding_or->origin ==
1676 ? theme.status_active
1677 : (selected_binding_or->origin ==
1678 zelda3::CustomObjectMappingOrigin::
1679 kConfiguredSlotUnmapped
1680 ? theme.status_error
1681 : theme.text_secondary_gray);
1683 mapping_color, tr(
"Mapping: %s"),
1684 tr(CustomObjectMappingOriginLabel(selected_binding_or->origin)));
1686 if (selected_asset_ready) {
1687 ImGui::TextColored(theme.status_success,
1689 if (selected_path_or.ok() && ImGui::IsItemHovered()) {
1690 ImGui::SetTooltip(
"%s", selected_path_or->string().c_str());
1694 selected_asset_status.message().data());
1696 ImGui::PushTextWrapPos(0.0f);
1698 ImGui::TextDisabled(
1699 "%s", tr(
"Custom 0x31 wall object; separate from vanilla wall and "
1700 "corner tile tables"));
1703 ImGui::TextDisabled(
"%s", tr(
"Tile layout only; behavior is separate"));
1707 " Tilemap editing is available; external boss pixels "
1708 "are not loaded into the room preview yet.");
1709 if (ImGui::IsItemHovered()) {
1712 tr(
"Oracle forces nonzero body tiles into graphics page 0x300 and "
1713 "DMA-loads separate Kydreeok or Manhandla graphics at runtime. "
1714 "Yaze currently preserves the correct tilemap geometry and raw "
1715 "source words, but the displayed pixels are not visual-parity "
1719 ImGui::PopTextWrapPos();
1725 const bool has_room_context =
1727#if defined(__EMSCRIPTEN__)
1728 constexpr bool kCanPublishCustomAssets =
false;
1730 constexpr bool kCanPublishCustomAssets =
true;
1732 const bool can_edit = selected_asset_ready && has_room_context &&
1734 kCanPublishCustomAssets;
1735 const bool can_use_in_room = selected_asset_ready && has_room_context;
1736 if (ImGui::BeginTable(
1737 "##CustomAssetActions", 2,
1738 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX)) {
1739 ImGui::TableNextColumn();
1741 ImGui::BeginDisabled();
1743 if (ImGui::Button(
ICON_MD_EDIT " Edit Tile Layout", ImVec2(-1.0f, 0.0f))) {
1754 ImGui::EndDisabled();
1756 if (!kCanPublishCustomAssets &&
1757 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1759 "%s", tr(
"Custom asset publishing is desktop-only until browser "
1760 "persistence can be verified."));
1763 ImGui::TableNextColumn();
1764 if (!can_use_in_room) {
1765 ImGui::BeginDisabled();
1767 if (ImGui::Button(
ICON_MD_ADD " Place in Room", ImVec2(-1.0f, 0.0f))) {
1771 if (!can_use_in_room) {
1772 ImGui::EndDisabled();
1779 const bool can_open_minecart =
1781 if (!can_open_minecart) {
1782 ImGui::BeginDisabled();
1784 if (ImGui::Button(
ICON_MD_TRAIN " Minecart Routes & Collision",
1785 ImVec2(-1.0f, 0.0f))) {
1790 "Minecart Tracks window is unavailable in this session";
1793 if (!can_open_minecart) {
1794 ImGui::EndDisabled();