yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2.cc
Go to the documentation of this file.
1// Related header
2#include "dungeon_editor_v2.h"
3#include "util/i18n/tr.h"
4
5// C system headers
6#include <cctype>
7#include <cstdio>
8
9// C++ standard library headers
10#include <algorithm>
11#include <array>
12#include <iterator>
13#include <memory>
14#include <optional>
15#include <string>
16#include <tuple>
17#include <utility>
18#include <vector>
19
20// Third-party library headers
21#include "absl/status/status.h"
22#include "absl/strings/str_format.h"
23#include "absl/types/span.h"
24#include "imgui/imgui.h"
25
26// Project headers
66#include "app/gui/core/icons.h"
68#include "core/features.h"
69#include "core/project.h"
70#include "rom/snes.h"
71#include "util/log.h"
72#include "util/macro.h"
79#include "zelda3/dungeon/room.h"
82
83namespace yaze::editor {
84
85namespace {
86
88 return std::make_tuple(object.id_, object.x_, object.y_, object.size_,
89 object.GetLayerValue(), object.all_bgs_, object.lit_,
90 object.options(), object.block_behavior_layer(),
91 object.torch_reserved_bit());
92}
93
101
103 return project != nullptr && project->hack_manifest.loaded() &&
104 project->hack_manifest.minecart_track_layout().source.has_value();
105}
106
107// Dungeon tools are hosted by the Workbench inspector's tool drawer. They also
108// open standalone from shortcuts and canvas actions, and from the command
109// palette.
110WindowPresentationPolicy WorkbenchTool(std::string capability = std::string()) {
111 return WindowPresentationPolicy::EmbeddedTool(std::move(capability))
112 .WithHost("Dungeon Workbench inspector");
113}
114
115} // namespace
116
120 room_viewers_.ForEach([](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
121 if (viewer)
122 viewer->CancelMinecartGesture();
123 });
124 for (auto* viewer :
126 if (viewer)
127 viewer->CancelMinecartGesture();
128 }
135 // Dependency rebinding is the project-reload boundary even when the project
136 // object is reused in place with a newly loaded manifest.
139 const absl::Status status =
141 if (!status.ok()) {
142 LOG_WARN("DungeonEditorV2", "Minecart project rebind failed: %s",
143 status.message());
144 }
146 }
147}
148
150 if (minecart_track_editor_panel_ == nullptr) {
151 return;
152 }
153
154 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
155 const size_t session_id = dependencies_.session_id;
158 [this, editor_manager, session_id](const MinecartTrack& before,
159 const MinecartTrack& after,
160 bool move_cart) {
161 if (editor_manager &&
162 !editor_manager->IsCurrentProjectContextOwnedBySession(session_id))
163 return absl::FailedPreconditionError(
164 "Activate this dungeon project before editing tracks");
165 return ApplyMinecartTrackEdit(before, after, move_cart);
166 });
168 return object_selector_panel_ &&
170 });
172 [this, editor_manager, session_id](
173 int room, int x, int y,
174 const MinecartNodeBrush& brush) -> absl::Status {
175 if (editor_manager &&
176 !editor_manager->IsCurrentProjectContextOwnedBySession(
177 session_id)) {
178 return absl::FailedPreconditionError(
179 "Activate this dungeon project before placing track nodes");
180 }
181 return PlaceMinecartNode(room, x, y, brush);
182 });
184 [this, editor_manager, session_id](
185 int room, const zelda3::CustomCollisionMap& expected,
186 const MinecartCollisionStamp& stamp) -> absl::Status {
187 if (editor_manager &&
188 !editor_manager->IsCurrentProjectContextOwnedBySession(
189 session_id)) {
190 return absl::FailedPreconditionError(
191 "Activate this dungeon project before editing rails");
192 }
193 return ApplyMinecartCollisionStamp(room, expected, stamp);
194 });
196 [this, editor_manager, session_id](
197 const std::vector<zelda3::TrackCollisionResult>& preview,
198 const zelda3::GeneratorOptions& options) -> absl::Status {
199 if (editor_manager != nullptr &&
200 !editor_manager->IsCurrentProjectContextOwnedBySession(
201 session_id)) {
202 return absl::FailedPreconditionError(
203 "Minecart collision apply requires its project context to be "
204 "active");
205 }
206 return ApplyMinecartCollisionBatch(preview, options);
207 });
208 if (editor_manager == nullptr) {
212 return;
213 }
214
216 [editor_manager, session_id](
217 const project::DungeonOverlaySettings& overlay) -> absl::Status {
218 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
219 session_id)) {
220 return absl::FailedPreconditionError(
221 "Minecart overlay edit requires its project context to be "
222 "active");
223 }
224 editor_manager->GetCurrentProject()->dungeon_overlay = overlay;
225 editor_manager->MarkCurrentProjectDirty();
226 return absl::OkStatus();
227 });
229 [editor_manager, session_id]() -> absl::Status {
230 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
231 session_id)) {
232 return absl::FailedPreconditionError(
233 "Minecart overlay draft requires its project context to be "
234 "active");
235 }
236 editor_manager->MarkCurrentProjectDirty();
237 return absl::OkStatus();
238 });
240 [editor_manager, session_id]() -> absl::Status {
241 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
242 session_id)) {
243 return absl::FailedPreconditionError(
244 "Minecart project save requires its project context to be "
245 "active");
246 }
247 return editor_manager->SaveProject();
248 });
249}
250
252 if (minecart_track_editor_panel_ != nullptr) {
253 return absl::OkStatus();
254 }
255 if (!ProjectAdvertisesMinecartTracks(dependencies_.project)) {
256 return absl::FailedPreconditionError(
257 "Minecart Tracks requires hack_manifest.minecart_tracks.source");
258 }
259 if (!core::FeatureFlags::get().kEnableCustomObjects &&
260 dependencies_.toast_manager != nullptr) {
262 "Custom Objects is disabled. Minecart Tracks remains available, but "
263 "custom-object ROM writes stay protected until the flag is enabled.",
265 }
266 if (dependencies_.window_manager != nullptr &&
269 return absl::FailedPreconditionError(
270 "Minecart Tracks can only be registered for the active project "
271 "session");
272 }
273
274 auto minecart_panel = std::make_unique<MinecartTrackEditorPanel>();
275 RETURN_IF_ERROR(minecart_panel->SetProject(dependencies_.project));
276 minecart_panel->SetRooms(&rooms_);
277 minecart_panel->SetRoomNavigationCallback(
278 [this](int room_id) { OnRoomSelected(room_id); });
279
280 minecart_track_editor_panel_ = minecart_panel.get();
282 if (dependencies_.window_manager != nullptr) {
284 std::move(minecart_panel), WorkbenchTool("minecart_tracks"));
285 } else {
286 owned_minecart_track_editor_panel_ = std::move(minecart_panel);
287 }
288
289 room_viewers_.ForEach(
290 [this](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
291 if (viewer != nullptr) {
292 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
293 }
294 });
295 if (workbench_viewer_ != nullptr) {
296 workbench_viewer_->SetMinecartTrackPanel(minecart_track_editor_panel_);
297 }
298 if (workbench_compare_viewer_ != nullptr) {
299 workbench_compare_viewer_->SetMinecartTrackPanel(
301 }
302 if (workbench_panel_ != nullptr) {
307 }
308
309 return absl::OkStatus();
310}
311
313 const bool custom_objects_enabled =
315 const bool custom_objects_state_changed =
316 custom_objects_enabled != observed_custom_objects_enabled_;
317 const uint64_t generation =
319 if (generation == observed_custom_object_generation_ &&
320 !custom_objects_state_changed) {
321 return;
322 }
323
325 if (custom_objects_state_changed) {
327 }
328
330 room.MarkObjectsDirty();
331 room.MarkLayoutDirty();
332 });
333 // Reload Assets also refreshes external sprite art in every session-owned
334 // viewer, including hidden room windows and the comparison canvas.
335 const auto invalidate_sprite_resources = [](DungeonCanvasViewer* viewer) {
336 if (viewer != nullptr) {
337 viewer->InvalidateExternalSpriteResources();
338 }
339 };
340 room_viewers_.ForEach(
341 [&](int, auto& viewer) { invalidate_sprite_resources(viewer.get()); });
342 invalidate_sprite_resources(workbench_viewer_.get());
343 invalidate_sprite_resources(workbench_compare_viewer_.get());
344 if (object_selector_panel_ != nullptr) {
347 }
349 observed_custom_objects_enabled_ = custom_objects_enabled;
350}
351
352namespace {
353
354bool IsTransientDungeonRoomWindowId(const std::string& card_id) {
355 constexpr const char* kPrefix = "dungeon.room_";
356 constexpr size_t kPrefixLength = 13;
357 if (card_id.rfind(kPrefix, 0) != 0 || card_id.size() <= kPrefixLength) {
358 return false;
359 }
360 return std::all_of(card_id.begin() + kPrefixLength, card_id.end(),
361 [](unsigned char ch) { return std::isdigit(ch) != 0; });
362}
363
364bool IsWorkbenchNavigationWindowId(const std::string& card_id) {
365 constexpr std::array<const char*, 3> kWorkbenchNavigationPanelIds = {
367 "dungeon.entrance_properties",
369 };
370
371 return IsTransientDungeonRoomWindowId(card_id) ||
372 std::any_of(kWorkbenchNavigationPanelIds.begin(),
373 kWorkbenchNavigationPanelIds.end(),
374 [&card_id](const char* navigation_id) {
375 return card_id == navigation_id;
376 });
377}
378
379} // namespace
380
382 : rom_(rom), room_loader_(rom), room_selector_(rom), rooms_(rom) {
384 if (rom) {
386 }
387}
388
397
401 if (room_matrix_panel_) {
403 room_matrix_panel_ = nullptr;
404 }
405 // Clear runtime references in panels BEFORE editor-owned rooms and viewers are
406 // destroyed. WorkspaceWindowManager panels may outlive this editor session.
409 room_graphics_panel_ = nullptr;
410 }
416 }
419 object_selector_panel_ = nullptr;
420 }
421 if (door_editor_panel_) {
423 door_editor_panel_ = nullptr;
424 }
427 sprite_editor_panel_ = nullptr;
428 }
429 if (item_editor_panel_) {
431 item_editor_panel_ = nullptr;
432 }
436 custom_collision_panel_ = nullptr;
437 }
438 if (water_fill_panel_) {
441 water_fill_panel_ = nullptr;
442 }
446 }
447}
448
450 if (!rom_ || !rom_->is_loaded()) {
451 return absl::FailedPreconditionError("ROM not loaded");
452 }
453
456
457 std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>
458 refreshed_entrances;
459 std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>
460 refreshed_spawn_points;
461 RETURN_IF_ERROR(room_loader_.LoadRoomEntrances(refreshed_entrances));
462 RETURN_IF_ERROR(room_loader_.LoadDungeonSpawnPoints(refreshed_spawn_points));
463
464 struct RefreshedRoom {
465 int room_id;
466 zelda3::Room room;
467 };
468 std::vector<std::pair<int, bool>> materialized_rooms;
470 [&materialized_rooms](int room_id, const zelda3::Room& room) {
471 materialized_rooms.emplace_back(room_id, room.IsLoaded());
472 });
473
474 std::vector<RefreshedRoom> refreshed_rooms;
475 refreshed_rooms.reserve(materialized_rooms.size());
476 for (const auto& [room_id, was_loaded] : materialized_rooms) {
477 zelda3::Room refreshed_room;
478 if (was_loaded) {
479 RETURN_IF_ERROR(room_loader_.LoadRoom(room_id, refreshed_room));
480 } else {
481 refreshed_room = zelda3::LoadRoomHeaderFromRom(rom_, room_id);
482 refreshed_room.SetGameData(game_data_);
483 }
484 refreshed_rooms.push_back({room_id, std::move(refreshed_room)});
485 }
486
487 // All ROM reads succeeded. Clear every interaction that can retain indices
488 // into Room vectors before replacing any Room contents.
492 }
495 }
496 if (room_matrix_panel_) {
498 }
499 auto prepare_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
500 if (viewer) {
501 viewer->RefreshRomBackedState(rom_, game_data_, &rooms_, room_id);
502 }
503 };
504 room_viewers_.ForEach(
505 [&prepare_viewer](int room_id,
506 std::unique_ptr<DungeonCanvasViewer>& viewer) {
507 prepare_viewer(viewer.get(), room_id);
508 });
509 prepare_viewer(workbench_viewer_.get(), current_room_id_);
510 const int compare_room_id =
512 IsValidRoomId(workbench_compare_viewer_->current_room_id())
513 ? workbench_compare_viewer_->current_room_id()
515 prepare_viewer(workbench_compare_viewer_.get(), compare_room_id);
516
518 dungeon_editor_system_->ClearHistory();
519 if (auto object_editor = dungeon_editor_system_->GetObjectEditor()) {
520 (void)object_editor->ClearSelection();
521 object_editor->ClearHistory();
522 }
523 }
526 pending_undo_ = {};
529 has_pending_undo_ = false;
534 pending_swap_ = {};
538
542 for (auto& refreshed : refreshed_rooms) {
543 auto* room = rooms_.GetIfMaterialized(refreshed.room_id);
544 if (room) {
545 *room = std::move(refreshed.room);
546 }
547 }
548 entrances_ = std::move(refreshed_entrances);
549 spawn_points_ = std::move(refreshed_spawn_points);
552
557
560 auto* current_room = IsValidRoomId(current_room_id_)
562 : nullptr;
563 RETURN_IF_ERROR(dungeon_editor_system_->RefreshRomBackedState(
564 current_room, current_room_id_));
565 }
566
571 if (auto* current_room = rooms_.GetIfMaterialized(current_room_id_);
572 current_room && game_data_ &&
574 const int resolved_palette_id = current_room->ResolveDungeonPaletteId();
576 resolved_palette_id >= 0 &&
577 resolved_palette_id <
578 static_cast<int>(
580 ? static_cast<uint64_t>(resolved_palette_id)
581 : 0;
587 }
588
589 if (is_loaded_) {
593 }
594
595 auto bind_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
596 if (!viewer) {
597 return;
598 }
599 RefreshViewerRuntimeContext(viewer, room_id, /*rebind_project=*/true);
601 };
602 room_viewers_.ForEach(
603 [&bind_viewer](int room_id,
604 std::unique_ptr<DungeonCanvasViewer>& viewer) {
605 bind_viewer(viewer.get(), room_id);
606 });
607 bind_viewer(workbench_viewer_.get(), current_room_id_);
608 bind_viewer(workbench_compare_viewer_.get(), compare_room_id);
609
615 }
618 }
621 }
624 }
628 }
631 }
632 if (workbench_panel_) {
634 }
635
636 DungeonCanvasViewer* current_viewer = nullptr;
638 current_viewer = workbench_viewer_.get();
639 } else if (auto* viewer = room_viewers_.Get(current_room_id_)) {
640 current_viewer = viewer->get();
641 }
642 if (current_viewer) {
644 }
645
646 // This editor-level refresh is used for same-address ROM replacement and
647 // revert. Ordinary room navigation refreshes only its canvas viewer.
649 return absl::OkStatus();
650}
651
654 room.ClearWaterFillZone();
655 room.ClearWaterFillDirty();
656 });
657
658 bool legacy_imported = false;
659 const bool water_fill_save_enabled =
661 std::vector<zelda3::WaterFillZoneEntry> zones;
662
663 auto zones_or = zelda3::LoadWaterFillTable(rom_);
664 if (zones_or.ok()) {
665 zones = std::move(zones_or.value());
666 } else {
667 LOG_WARN("DungeonEditorV2", "WaterFillTable parse failed: %s",
668 zones_or.status().message().data());
671 absl::StrFormat("WaterFill table parse failed: %s",
672 zones_or.status().message()),
674 }
675 }
676
677 if (zones.empty()) {
678 std::string sym_path;
683 }
684 auto legacy_or = zelda3::LoadLegacyWaterGateZones(rom_, sym_path);
685 if (legacy_or.ok()) {
686 zones = std::move(legacy_or.value());
687 legacy_imported = !zones.empty();
688 } else {
689 LOG_WARN("DungeonEditorV2", "Legacy water gate import failed: %s",
690 legacy_or.status().message().data());
691 }
692 }
693
694 for (const auto& z : zones) {
695 if (z.room_id < 0 || z.room_id >= static_cast<int>(rooms_.size())) {
696 continue;
697 }
698 auto& room = rooms_[z.room_id];
699 room.set_water_fill_sram_bit_mask(z.sram_bit_mask);
700 for (uint16_t off : z.fill_offsets) {
701 const int x = static_cast<int>(off % 64);
702 const int y = static_cast<int>(off / 64);
703 room.SetWaterFillTile(x, y, true);
704 }
705
706 // A passive legacy import is pending migration only when this project is
707 // allowed to write the WaterFill table. Otherwise retain the overlay as
708 // read-only reference data without creating unsavable room dirtiness.
709 if (!legacy_imported || !water_fill_save_enabled) {
710 room.ClearWaterFillDirty();
711 }
712 }
713
714 if (legacy_imported && dependencies_.toast_manager) {
716 water_fill_save_enabled
717 ? "Imported legacy water gate zones (save to write new table)"
718 : "Loaded legacy water gate zones read-only (WaterFill saving is "
719 "disabled)",
721 }
722}
723
727 }
729 const bool rom_changed = dependencies_.rom && dependencies_.rom != rom_;
730 if (rom_changed) {
732 // The system owns ROM-backed views; ensure it matches the current ROM.
734 }
737 if (game_data_) {
739 }
740 }
741
742 // Setup docking class for room windows
743 room_window_class_.DockingAllowUnclassed = true;
744 room_window_class_.DockingAlwaysTabBar = true;
745
747 return;
748 auto* window_manager = dependencies_.window_manager;
749 const auto shortcut_hint =
750 [this](const char* id,
751 std::vector<ImGuiKey> default_keys) -> std::string {
752 if (dependencies_.shortcut_manager != nullptr) {
753 const std::string live =
755 if (!live.empty()) {
756 return live;
757 }
758 }
759 return PrintShortcut(default_keys);
760 };
761
762 // Legacy panel IDs persisted in older layouts/settings.
763 window_manager->RegisterPanelAlias("dungeon.object_tools", kObjectSelectorId);
764 window_manager->RegisterPanelAlias("dungeon.entrances",
765 "dungeon.entrance_properties");
766 window_manager->RegisterPanelAlias(kEntranceListId,
767 "dungeon.entrance_properties");
768
769 // Register panels with WorkspaceWindowManager (no boolean flags - visibility is
770 // managed entirely by WorkspaceWindowManager::ShowPanel/HidePanel/IsPanelVisible)
771 // No default chord: Ctrl+Shift+W is reserved for Close Session. Toggle via
772 // View / Window Browser / command palette instead.
773 window_manager->RegisterPanel(
774 {.card_id = "dungeon.workbench",
775 .display_name = "Dungeon Workbench",
776 .window_title = " Dungeon Workbench",
777 .icon = ICON_MD_WORKSPACES,
778 .category = "Dungeon",
779 .workflow_group = "Core",
781 .shortcut_hint = "",
782 .visibility_flag = nullptr,
783 .priority = 5,
784 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
785 .disabled_tooltip = "Load a ROM to edit dungeon rooms"});
786
787 window_manager->RegisterPanel(
788 {.card_id = kRoomSelectorId,
789 .display_name = "Room List",
790 .window_title = " Room List",
791 .icon = ICON_MD_LIST,
792 .category = "Dungeon",
793 .workflow_group = "Core",
794 .presentation = {.role = WindowPresentationRole::kOptionalPopOut,
795 .default_host = WindowDefaultHost::kWorkspace,
796 .list_in_window_browser = false,
797 .allow_popout = true,
798 .declared = true},
799 .shortcut_hint = shortcut_hint(
800 "dungeon.open_room_list", {ImGuiMod_Ctrl, ImGuiMod_Alt, ImGuiKey_L}),
802 .visibility_flag = nullptr,
803 .priority = 20,
804 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
805 .disabled_tooltip = "Load a ROM to browse dungeon rooms"});
806
807 window_manager->RegisterPanel(
808 {.card_id = "dungeon.entrance_properties",
809 .display_name = "Entrances",
810 .window_title = " Entrances",
811 .icon = ICON_MD_DOOR_FRONT,
812 .category = "Dungeon",
813 .workflow_group = "Core",
815 .shortcut_hint =
816 shortcut_hint("dungeon.open_entrances",
817 {ImGuiMod_Ctrl, ImGuiMod_Shift, ImGuiKey_E}),
819 .visibility_flag = nullptr,
820 .priority = 25,
821 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
822 .disabled_tooltip = "Load a ROM to browse and edit dungeon entrances"});
823
824 window_manager->RegisterPanel(
825 {.card_id = kRoomMatrixId,
826 .display_name = "Room Matrix",
827 .window_title = " Room Matrix",
828 .icon = ICON_MD_GRID_VIEW,
829 .category = "Dungeon",
830 .workflow_group = "Core",
832 .shortcut_hint = shortcut_hint(
833 "dungeon.open_matrix", {ImGuiMod_Ctrl, ImGuiMod_Shift, ImGuiKey_M}),
835 .visibility_flag = nullptr,
836 .priority = 30,
837 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
838 .disabled_tooltip = "Load a ROM to view the room matrix"});
839
840 window_manager->RegisterPanel(
841 {.card_id = kRoomGraphicsId,
842 .display_name = "Room Graphics",
843 .window_title = " Room Graphics",
844 .icon = ICON_MD_IMAGE,
845 .category = "Dungeon",
846 .workflow_group = "Editors",
847 .presentation = WorkbenchTool(),
848 .shortcut_hint =
849 shortcut_hint("dungeon.open_room_graphics",
850 {ImGuiMod_Ctrl, ImGuiMod_Shift, ImGuiKey_G}),
852 .visibility_flag = nullptr,
853 .priority = 50,
854 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
855 .disabled_tooltip = "Load a ROM to view room graphics"});
856
857 window_manager->RegisterPanel(
858 {.card_id = kObjectSelectorId,
859 .display_name = "Object Selector",
860 .window_title = " Object Selector",
861 .icon = ICON_MD_CATEGORY,
862 .category = "Dungeon",
863 .workflow_group = "Editors",
864 .presentation = WorkbenchTool(),
865 .shortcut_hint = "",
866 .visibility_flag = nullptr,
867 .priority = 60,
868 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
869 .disabled_tooltip = "Load a ROM to browse dungeon objects"});
870
871 window_manager->RegisterPanel(
872 {.card_id = kDoorEditorId,
873 .display_name = "Door Editor",
874 .window_title = " Door Editor",
875 .icon = ICON_MD_DOOR_FRONT,
876 .category = "Dungeon",
877 .workflow_group = "Editors",
878 .presentation = WorkbenchTool(),
879 .shortcut_hint = "",
880 .visibility_flag = nullptr,
881 .priority = 69,
882 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
883 .disabled_tooltip = "Load a ROM to edit dungeon doors"});
884
885 window_manager->RegisterPanel(
886 {.card_id = kPaletteEditorId,
887 .display_name = "Palette Editor",
888 .window_title = " Palette Editor",
889 .icon = ICON_MD_PALETTE,
890 .category = "Dungeon",
891 .workflow_group = "Editors",
892 .presentation = WorkbenchTool(),
893 // Avoid conflicting with the global Command Palette (Ctrl/Cmd+Shift+P).
894 .shortcut_hint = shortcut_hint(
895 "dungeon.open_palette",
896 {ImGuiMod_Ctrl, ImGuiMod_Shift, ImGuiMod_Alt, ImGuiKey_P}),
898 .visibility_flag = nullptr,
899 .priority = 70,
900 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
901 .disabled_tooltip = "Load a ROM to edit dungeon palettes"});
902
903 // Show default panels on startup.
904 // Workbench mode intentionally suppresses parallel standalone defaults to
905 // avoid two competing workflows being open at once.
907 /*show_toast=*/false);
908
909 // Wire intent-aware callback for double-click / context menu
911 [this](int room_id, RoomSelectionIntent intent) {
912 OnRoomSelected(room_id, intent);
913 });
914
915 // Register WindowContent instances
916 window_manager->RegisterWindowContent(std::make_unique<RoomBrowserContent>(
917 &room_selector_, [this](int room_id) { OnRoomSelected(room_id); }));
918
919 {
920 auto matrix_panel = std::make_unique<RoomMatrixContent>(
922 [this](int room_id) { OnRoomSelected(room_id); },
923 [this](int old_room, int new_room) {
924 SwapRoomInPanel(old_room, new_room);
925 },
926 &rooms_);
927 matrix_panel->SetRoomIntentCallback(
928 [this](int room_id, RoomSelectionIntent intent) {
929 OnRoomSelected(room_id, intent);
930 });
931 matrix_panel->SetCensusManifestProvider(
932 [this]() -> const core::HackManifest* {
934 : nullptr;
935 });
936 room_matrix_panel_ = matrix_panel.get();
937 window_manager->RegisterWindowContent(std::move(matrix_panel));
938 }
939
940 {
941 auto workbench = std::make_unique<DungeonWorkbenchContent>(
943 [this](int room_id) { OnRoomSelected(room_id); },
944 [this](int room_id, RoomSelectionIntent intent) {
945 OnRoomSelected(room_id, intent);
946 },
947 [this](int room_id) {
948 auto status = SaveRoom(room_id);
949 if (!status.ok()) {
950 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
951 status.message().data());
954 absl::StrFormat("Apply Room failed: %s", status.message()),
956 }
957 return;
958 }
960 dependencies_.toast_manager->Show("Applied room to ROM buffer",
962 }
963 },
964 [this]() { SaveAllRooms(); }, [this]() { return GetWorkbenchViewer(); },
965 [this](int room_id) { return GetWorkbenchCompareViewer(room_id); },
966 [this]() -> const std::deque<int>& { return recent_rooms_; },
967 [this](int room_id) {
968 recent_rooms_.erase(
969 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
970 recent_rooms_.end());
971 },
972 [this](bool enabled) { QueueWorkbenchWorkflowMode(enabled); }, rom_);
973 workbench_panel_ = workbench.get();
975 [this](DungeonCanvasViewer& viewer) {
977 });
979 [this]() { return undo_manager_.CanUndo(); },
980 [this]() { return undo_manager_.CanRedo(); },
981 [this]() { Undo().IgnoreError(); }, [this]() { Redo().IgnoreError(); },
982 [this]() { return undo_manager_.GetUndoDescription(); },
983 [this]() { return undo_manager_.GetRedoDescription(); },
984 [this]() { return static_cast<int>(undo_manager_.UndoStackSize()); });
986 [window_manager](const std::string& window_id) {
987 const size_t session_id = window_manager->GetActiveSessionId();
988 return window_manager->IsWindowOpen(session_id, window_id);
989 },
990 [this, window_manager](const std::string& window_id) {
991 if (window_id.empty()) {
992 return false;
993 }
995 window_manager->GetActiveSessionId();
998 return true;
999 });
1001 [window_manager]() { window_manager->TriggerShowShortcuts(); });
1003 [this]() {
1004 return game_data_ ? &game_data_->pit_damage_table : nullptr;
1005 },
1006 [this]() { InvalidateConnectedRoomGraphs(); });
1007 window_manager->RegisterWindowContent(std::move(workbench));
1008 }
1009
1010 {
1011 auto entrances_panel = std::make_unique<DungeonEntrancesPanel>(
1013 [this](int entrance_id) { OnEntranceSelected(entrance_id); });
1014 entrances_panel->SetProject(dependencies_.project);
1015 entrances_panel->SetGraphicsPreview(
1016 [this]() {
1018 },
1019 [this]() { return automatic_entrance_preview_; },
1020 [this](int slot) { SetGraphicsPreviewEntrance(slot); });
1021 ConfigureEntrancePropertyEditCallbacks(entrances_panel.get());
1022 entrance_navigator_panel_ = entrances_panel.get();
1023 window_manager->RegisterWindowContent(std::move(entrances_panel));
1024 }
1025 if (workbench_panel_) {
1029 [this](int room_id) { ApplyEntranceRenderContext(room_id); });
1031 [this](int room_id, std::optional<std::string> label) {
1032 return ApplyRoomLabelEdit(room_id, std::move(label));
1033 });
1034 }
1035 if (room_matrix_panel_) {
1038 });
1039 }
1040
1041 // Note: RoomGraphicsContent and PaletteEditorContent are registered
1042 // in Load() after their dependencies (renderer_, palette_editor_) are initialized
1043}
1044
1046 if (!rom_ || !rom_->is_loaded()) {
1047 return absl::FailedPreconditionError("ROM not loaded");
1048 }
1049
1050 // Initialize ObjectDimensionTable so DimensionService's fallback path works.
1051 // DimensionService::Get() queries this table internally but has no Load API.
1052 auto& dim_table = zelda3::ObjectDimensionTable::Get();
1053 if (!dim_table.IsLoaded()) {
1054 RETURN_IF_ERROR(dim_table.LoadFromRom(rom_));
1055 }
1056
1060
1061 if (!game_data()) {
1062 return absl::FailedPreconditionError("GameData not available");
1063 }
1064 auto dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
1065 current_palette_ = dungeon_main_pal_group[current_palette_group_id_];
1068
1075 [this](int room_id) { OnRoomSelected(room_id); });
1077 [this](int room_id, RoomSelectionIntent intent) {
1078 OnRoomSelected(room_id, intent);
1079 });
1081 [this](int entrance_id) { OnEntranceSelected(entrance_id); });
1082
1083 // Canvas viewers are lazily created in GetViewerForRoom
1084
1085 if (game_data()) {
1087 } else {
1089 }
1090
1093
1094 // Register panels that depend on initialized state (renderer, palette_editor_)
1096 auto graphics_panel = std::make_unique<RoomGraphicsContent>(
1098 graphics_panel->SetAssignmentCallback(
1099 [this](int room, int slot, int sheet) {
1100 return EditRoomGraphicsSheet(room, slot, sheet);
1101 });
1102 graphics_panel->SetUsageCallback(
1103 [this](int roomset) { return GetRoomGraphicsUsage(roomset); });
1104 room_graphics_panel_ = graphics_panel.get();
1106 std::move(graphics_panel), WorkbenchTool());
1107 auto palette_panel =
1108 std::make_unique<PaletteEditorContent>(&palette_editor_);
1109 palette_editor_panel_ = palette_panel.get();
1111 std::move(palette_panel), WorkbenchTool());
1112 }
1113
1114 dungeon_editor_system_ = std::make_unique<zelda3::DungeonEditorSystem>(rom_);
1115 (void)dungeon_editor_system_->Initialize();
1117
1118 // Initialize browse-only object selector and the dedicated object editor.
1119 auto object_selector = std::make_unique<ObjectSelectorContent>(
1120 rom_, nullptr, dungeon_editor_system_->GetObjectEditor(),
1122
1123 // Wire up object change callback to trigger room re-rendering
1124 dungeon_editor_system_->GetObjectEditor()->SetObjectChangedCallback(
1125 [this](size_t /*object_index*/, const zelda3::RoomObject& /*object*/) {
1126 if (current_room_id_ >= 0 && current_room_id_ < (int)rooms_.size()) {
1127 rooms_[current_room_id_].RenderRoomGraphics();
1128 }
1129 });
1130
1131 // Set rooms and initial palette group for correct preview rendering
1132 object_selector->SetRooms(&rooms_);
1133 object_selector->SetCurrentPaletteGroup(current_palette_group_);
1134
1135 auto open_workbench_selection_inspector = [this]() {
1136 if (workbench_panel_) {
1138 }
1139 OpenWindow("dungeon.workbench");
1140 };
1141
1142 object_selector->SetOpenObjectEditorCallback(
1143 open_workbench_selection_inspector);
1144
1145 // Keep raw pointers for later access
1146 object_selector_panel_ = object_selector.get();
1147
1148 auto jump_to_reciprocal_door = [this](int neighbor_room_id,
1149 size_t door_index) {
1150 if (neighbor_room_id < 0 ||
1151 neighbor_room_id >= static_cast<int>(rooms_.size())) {
1152 return;
1153 }
1154 OnRoomSelected(neighbor_room_id, true);
1155 if (auto* viewer = GetViewerForRoom(neighbor_room_id)) {
1156 // Bind before selecting; see NavigateToPlacedObject.
1157 viewer->object_interaction().SetCurrentRoom(&rooms_, neighbor_room_id);
1158 viewer->object_interaction()
1159 .entity_coordinator()
1160 .door_handler()
1161 .SelectDoor(door_index);
1162 }
1163 };
1164 auto door_editor = std::make_unique<DoorEditorContent>();
1165 door_editor->SetRooms(&rooms_);
1166 door_editor->SetOpenSelectionInspectorCallback(
1167 open_workbench_selection_inspector);
1168 door_editor->SetJumpToReciprocalDoorCallback(jump_to_reciprocal_door);
1169 door_editor_panel_ = door_editor.get();
1170
1171 // Propagate game_data to the object editor panel if available
1172 if (game_data()) {
1174 }
1175
1176 // Room-scoped utility tools use the Workbench inspector by default, while the
1177 // same WindowContent instances remain available to users who prefer floating
1178 // windows. OpenTool keeps one presentation owner at a time so one content
1179 // instance is never drawn in both places during the same frame.
1180 auto custom_collision_panel =
1181 std::make_unique<CustomCollisionPanel>(nullptr, nullptr);
1182 custom_collision_panel_ = custom_collision_panel.get();
1183
1184 auto water_fill_panel = std::make_unique<WaterFillPanel>(nullptr, nullptr);
1185 water_fill_panel_ = water_fill_panel.get();
1186
1187 auto room_tag_panel = std::make_unique<RoomTagEditorPanel>();
1188 room_tag_panel->SetProject(dependencies_.project);
1189 room_tag_panel->SetRooms(&rooms_);
1190 room_tag_panel->SetCurrentRoomId(current_room_id_);
1191 room_tag_panel->SetMetadataEditCallback(
1192 [this](int room_id, const RoomMetadataEdit& edit) {
1193 return EditRoomMetadata(room_id, edit);
1194 });
1195 room_tag_editor_panel_ = room_tag_panel.get();
1196
1197 auto object_coverage_panel = CreateObjectCoveragePanel();
1198 object_coverage_panel_ = object_coverage_panel.get();
1199
1200 // Register the ObjectSelectorContent directly (it inherits from WindowContent)
1201 // Panel manager takes ownership
1204 std::move(object_selector), WorkbenchTool());
1205 dependencies_.window_manager->RegisterWindowContent(std::move(door_editor),
1206 WorkbenchTool());
1207
1208 // Register sprite and item editor panels with canvas viewer = nullptr
1209 // They will get the viewer reference in OnRoomSelected when a room is selected
1210 auto sprite_panel = std::make_unique<SpriteEditorPanel>(&current_room_id_,
1211 &rooms_, nullptr);
1212 sprite_panel->SetOpenSelectionInspectorCallback(
1213 open_workbench_selection_inspector);
1214 sprite_editor_panel_ = sprite_panel.get();
1215 dependencies_.window_manager->RegisterWindowContent(std::move(sprite_panel),
1216 WorkbenchTool());
1217
1218 auto item_panel =
1219 std::make_unique<ItemEditorPanel>(&current_room_id_, &rooms_, nullptr);
1220 item_panel->SetOpenSelectionInspectorCallback(
1221 open_workbench_selection_inspector);
1222 item_editor_panel_ = item_panel.get();
1223 dependencies_.window_manager->RegisterWindowContent(std::move(item_panel),
1224 WorkbenchTool());
1225
1227 std::move(custom_collision_panel), WorkbenchTool());
1229 std::move(water_fill_panel), WorkbenchTool());
1231 std::move(room_tag_panel), WorkbenchTool());
1233 std::move(object_coverage_panel),
1235 "Canvas context menu > Check in Object Coverage"));
1236 // Object Tile Editor Panel
1237 {
1238 auto tile_editor_panel =
1239 std::make_unique<ObjectTileEditorPanel>(renderer_, rom_);
1240 tile_editor_panel->SetCurrentPaletteGroup(current_palette_group_);
1241 tile_editor_panel->SetStandardWritePreflightCallback(
1242 [this](const std::vector<std::pair<uint32_t, uint32_t>>& ranges) {
1243 if (ranges.empty() || dependencies_.project == nullptr ||
1245 return absl::OkStatus();
1246 }
1250 "dungeon object tile data", "ObjectTileEditorPanel",
1252 });
1253 tile_editor_panel->SetTilesAppliedCallback([this]() {
1256 }
1257 });
1258
1259 object_tile_editor_panel_ = tile_editor_panel.get();
1261 std::move(tile_editor_panel),
1262 WorkbenchTool().WithHost(
1263 "Object navigator or canvas selection menu > Edit tiles"));
1264 }
1265
1266 // Wire fixed-slot custom-object management actions to their workspace
1267 // windows.
1270 [this](int room_id, const zelda3::RoomObject& object) {
1271 return OpenObjectTileEditorForObject(room_id, object);
1272 });
1276 [this]() {
1277 return dependencies_.window_manager != nullptr &&
1278 object_tile_editor_panel_ != nullptr &&
1281 });
1284 return dependencies_.window_manager != nullptr &&
1285 minecart_track_editor_panel_ != nullptr &&
1288 });
1289 }
1290
1291 // Overlay Manager Panel
1292 {
1293 auto overlay_panel = std::make_unique<OverlayManagerPanel>();
1294 overlay_manager_panel_ = overlay_panel.get();
1296 std::move(overlay_panel),
1297 WorkbenchTool().WithHost("Command palette"));
1298 }
1299
1300 } else {
1301 owned_object_selector_panel_ = std::move(object_selector);
1302 owned_door_editor_panel_ = std::move(door_editor);
1303 owned_custom_collision_panel_ = std::move(custom_collision_panel);
1304 owned_water_fill_panel_ = std::move(water_fill_panel);
1305 owned_room_tag_editor_panel_ = std::move(room_tag_panel);
1306 owned_object_coverage_panel_ = std::move(object_coverage_panel);
1307 }
1308
1309 if (ProjectAdvertisesMinecartTracks(dependencies_.project)) {
1311 }
1312
1314 [this](gui::DungeonPaletteChange change) {
1316 });
1318
1319 // Oracle of Secrets: load editor-authored water fill zones (best-effort).
1321
1322 if (workbench_panel_) {
1330 }
1331
1332 is_loaded_ = true;
1333 return absl::OkStatus();
1334}
1335
1337 if (palette_listener_id_ >= 0) {
1338 return;
1339 }
1340
1342 [this](const std::string& group_name, int palette_index) {
1343 HandleArenaPaletteChanged(group_name, palette_index);
1344 });
1345}
1346
1347void DungeonEditorV2::HandleArenaPaletteChanged(const std::string& group_name,
1348 int palette_index) {
1349 if (!gfx::PaletteManager::Get().IsManaging(game_data_)) {
1350 return;
1351 }
1352
1353 if (group_name == "hud") {
1356 } else if (group_name == "dungeon_main") {
1359 }
1360}
1361
1365
1366 std::array<bool, zelda3::kNumberOfRooms> rendered_rooms{};
1367 auto render_once = [this, &rendered_rooms](int room_id) {
1368 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()) ||
1369 room_id >= static_cast<int>(rendered_rooms.size()) ||
1370 rendered_rooms[room_id]) {
1371 return;
1372 }
1373 rooms_[room_id].RenderRoomGraphics();
1374 rendered_rooms[room_id] = true;
1375 };
1376
1377 for (int i = 0; i < active_rooms_.Size; i++) {
1378 render_once(active_rooms_[i]);
1379 }
1380
1381 render_once(current_room_id_);
1382
1383 auto apply_palette = [](DungeonCanvasViewer* viewer, uint64_t palette_id,
1384 const gfx::PaletteGroup& palette_group,
1385 bool force_shared_palette_refresh) {
1386 if (!viewer) {
1387 return;
1388 }
1389 viewer->SetCurrentPaletteId(palette_id);
1390 viewer->SetCurrentPaletteGroup(
1391 palette_group,
1392 /*force_refresh=*/force_shared_palette_refresh &&
1394 };
1395
1396 if (current_room_id_ >= 0 &&
1397 current_room_id_ < static_cast<int>(rooms_.size()) && game_data()) {
1398 const bool force_shared_palette_refresh =
1400 auto& dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
1402 static_cast<uint64_t>(dungeon_main_pal_group.size())) {
1404 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1407 game_data());
1408 apply_palette(workbench_viewer_.get(), current_palette_id_,
1409 current_palette_group_, force_shared_palette_refresh);
1411 current_palette_group_, force_shared_palette_refresh);
1412 room_viewers_.ForEach([this, change, &apply_palette,
1413 &dungeon_main_pal_group, &render_once,
1414 force_shared_palette_refresh](
1415 int room_id,
1416 std::unique_ptr<DungeonCanvasViewer>& viewer) {
1417 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1418 return;
1419 }
1420 const int palette_id = rooms_[room_id].ResolveDungeonPaletteId();
1421 if (palette_id < 0 ||
1422 palette_id >= static_cast<int>(dungeon_main_pal_group.size())) {
1423 return;
1424 }
1426 change.palette_id >= 0 && palette_id != change.palette_id) {
1427 return;
1428 }
1429 const auto room_palette_group =
1431 dungeon_main_pal_group.palette_ref(palette_id), game_data());
1432 if (viewer && viewer->object_interaction().IsObjectLoaded()) {
1433 render_once(room_id);
1434 }
1435 apply_palette(viewer.get(), palette_id, room_palette_group,
1436 force_shared_palette_refresh);
1437 });
1440 }
1443 }
1444 }
1445 }
1446
1447 // Tile edits remain bound to their original room even when the active room
1448 // changes or closes. Refresh that room's palette without reopening its session.
1450 !game_data()) {
1451 return;
1452 }
1453 const int tile_room_id = object_tile_editor_panel_->current_room_id();
1454 const auto* tile_room = rooms_.GetIfLoaded(tile_room_id);
1455 if (!tile_room) {
1456 return;
1457 }
1458 const int tile_palette_id = tile_room->ResolveDungeonPaletteId();
1459 const auto& dungeon_palettes = game_data()->palette_groups.dungeon_main;
1460 if (tile_palette_id < 0 ||
1461 tile_palette_id >= static_cast<int>(dungeon_palettes.size()) ||
1463 change.palette_id >= 0 && tile_palette_id != change.palette_id)) {
1464 return;
1465 }
1467 tile_room_id,
1469 dungeon_palettes.palette_ref(tile_palette_id), game_data()));
1470}
1471
1475 change.palette_id < 0) {
1477 [](int, zelda3::Room& room) { room.MarkGraphicsDirty(); });
1478 return;
1479 }
1480
1481 // Editing palette bytes does not change a room header, so the room's normal
1482 // property cache cannot detect this dependency change. Mark every cached
1483 // room that resolves to the edited concrete palette; inactive rooms will
1484 // redraw lazily the next time they are shown.
1485 rooms_.ForEachMaterialized([change](int, zelda3::Room& room) {
1486 if (room.ResolveDungeonPaletteId() == change.palette_id) {
1487 room.MarkGraphicsDirty();
1488 }
1489 });
1490}
1491
1497
1498 const auto& theme = AgentUI::GetTheme();
1499 if (room_window_class_.ClassId == 0) {
1500 room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
1501 }
1502
1503 if (!is_loaded_) {
1504 gui::PanelWindow loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);
1505 loading_card.SetDefaultSize(400, 200);
1506 if (loading_card.Begin()) {
1507 ImGui::TextColored(theme.text_secondary_gray,
1508 tr("Loading dungeon data..."));
1509 ImGui::TextWrapped(
1510 tr("Independent editor cards will appear once ROM data is loaded."));
1511 }
1512 loading_card.End();
1513 return absl::OkStatus();
1514 }
1515
1516 // Poll the async ownership census and propagate a newly resolved entrance
1517 // context before any room or graphics surface draws this frame.
1519
1520 if (!IsWorkbenchWorkflowEnabled() || active_rooms_.Size > 0) {
1522 }
1523
1524 // Process any pending room swaps after all drawing is complete
1525 // This prevents ImGui state corruption from modifying collections mid-frame
1527
1528 return absl::OkStatus();
1529}
1530
1532 if (ImGui::GetCurrentContext() == nullptr) {
1533 return false;
1534 }
1535 bool canvas_owns_focus = false;
1536 room_viewers_.ForEach(
1537 [&canvas_owns_focus](int,
1538 const std::unique_ptr<DungeonCanvasViewer>& viewer) {
1539 canvas_owns_focus =
1540 canvas_owns_focus ||
1541 (viewer != nullptr && viewer->OwnsRoomNavigationShortcutFocus());
1542 });
1543 canvas_owns_focus =
1544 canvas_owns_focus ||
1545 (workbench_viewer_ != nullptr &&
1546 workbench_viewer_->OwnsRoomNavigationShortcutFocus()) ||
1547 (workbench_compare_viewer_ != nullptr &&
1548 workbench_compare_viewer_->OwnsRoomNavigationShortcutFocus());
1549 const bool navigator_owns_focus =
1550 (room_matrix_panel_ != nullptr &&
1552 (entrance_navigator_panel_ != nullptr &&
1554 return canvas_owns_focus || navigator_owns_focus;
1555}
1556
1559 constexpr int kColumns = 16;
1560 int next_room = -1;
1561 switch (direction) {
1563 if (current_room_id_ >= kColumns) {
1564 next_room = current_room_id_ - kColumns;
1565 }
1566 break;
1568 if (IsValidRoomId(current_room_id_ + kColumns)) {
1569 next_room = current_room_id_ + kColumns;
1570 }
1571 break;
1573 if (current_room_id_ % kColumns > 0) {
1574 next_room = current_room_id_ - 1;
1575 }
1576 break;
1578 if (current_room_id_ % kColumns < kColumns - 1 &&
1580 next_room = current_room_id_ + 1;
1581 }
1582 break;
1583 }
1584 if (next_room < 0) {
1585 return false;
1586 }
1588 OnRoomSelected(next_room, /*request_focus=*/false);
1589 } else {
1591 }
1592 return true;
1593}
1594
1596 if (direction == 0) {
1597 return false;
1598 }
1600 if (recent_rooms_.size() <= 1) {
1601 return false;
1602 }
1603 const auto current =
1604 std::find(recent_rooms_.begin(), recent_rooms_.end(), current_room_id_);
1605 if (current == recent_rooms_.end()) {
1606 return false;
1607 }
1608 const int current_index =
1609 static_cast<int>(std::distance(recent_rooms_.begin(), current));
1610 const int count = static_cast<int>(recent_rooms_.size());
1611 const int next_index =
1612 (current_index + (direction > 0 ? 1 : -1) + count) % count;
1613 OnRoomSelected(recent_rooms_[next_index]);
1614 return true;
1615 }
1616
1617 if (active_rooms_.size() <= 1) {
1618 return false;
1619 }
1620 int current_index = -1;
1621 for (int index = 0; index < active_rooms_.size(); ++index) {
1622 if (active_rooms_[index] == current_room_id_) {
1623 current_index = index;
1624 break;
1625 }
1626 }
1627 if (current_index < 0) {
1628 return false;
1629 }
1630 const int count = active_rooms_.size();
1631 const int next_index =
1632 (current_index + (direction > 0 ? 1 : -1) + count) % count;
1633 OnRoomSelected(active_rooms_[next_index]);
1634 return true;
1635}
1636
1638 if (direction == 0 || !IsValidRoomId(current_room_id_)) {
1639 return false;
1640 }
1641 const auto* room = rooms_.GetIfLoaded(current_room_id_);
1642 if (!room || room->GetTileObjects().empty()) {
1643 return false;
1644 }
1645 auto* viewer = GetViewerForRoom(current_room_id_);
1646 if (!viewer || viewer->rooms() != &rooms_ ||
1647 viewer->current_room_id() != current_room_id_ ||
1648 !viewer->IsObjectInteractionEnabled() || viewer->header_read_only()) {
1649 return false;
1650 }
1651 auto& interaction = viewer->object_interaction();
1652 const auto* context =
1653 interaction.entity_coordinator().tile_handler().context();
1654 if (!context || context->current_room_id != current_room_id_) {
1655 return false;
1656 }
1657 const auto selected = interaction.GetSelectedObjectIndices();
1658 const auto& objects = room->GetTileObjects();
1659 const size_t count = objects.size();
1660 const size_t current = selected.empty() ? 0 : selected.front();
1661 const size_t next =
1662 ((current < count ? current : 0) + (direction > 0 ? 1 : count - 1)) %
1663 count;
1664 interaction.SetSelectedObjects({next});
1665 viewer->ScrollToTile(objects[next].x_, objects[next].y_);
1666 return true;
1667}
1668
1670 return rooms_.LoadedCount();
1671}
1672
1674 int count = 0;
1675 rooms_.ForEachMaterialized([&count](int, const zelda3::Room& room) {
1676 if (room.HasUnsavedChanges()) {
1677 ++count;
1678 }
1679 });
1680 return count;
1681}
1682
1684 return PendingRoomCount() > 0;
1685}
1686
1691
1694 return true;
1695 }
1696 if (object_tile_editor_panel_ != nullptr &&
1698 return true;
1699 }
1700 if (HasPendingRoomChanges()) {
1701 return true;
1702 }
1703 if (std::any_of(entrances_.begin(), entrances_.end(),
1704 [](const zelda3::RoomEntrance& entrance) {
1705 return entrance.dirty();
1706 })) {
1707 return true;
1708 }
1709 if (std::any_of(spawn_points_.begin(), spawn_points_.end(),
1710 [](const zelda3::DungeonSpawnPoint& spawn) {
1711 return spawn.dirty();
1712 })) {
1713 return true;
1714 }
1715 return game_data_ != nullptr && game_data_->pit_damage_table.dirty();
1716}
1717
1722
1724 return minecart_track_editor_panel_ != nullptr
1726 : absl::OkStatus();
1727}
1728
1730 if (current_room_id_ < 0 ||
1731 current_room_id_ >= static_cast<int>(rooms_.size())) {
1732 return false;
1733 }
1734 if (const auto* room = rooms_.GetIfMaterialized(current_room_id_)) {
1735 return room->HasUnsavedChanges();
1736 }
1737 return false;
1738}
1739
1741 if (!core::FeatureFlags::get().dungeon.kUseWorkbench) {
1742 return false;
1743 }
1745 return true;
1746 }
1747 return dependencies_.window_manager->IsWindowOpen("dungeon.workbench");
1748}
1749
1750void DungeonEditorV2::SetWorkbenchWorkflowMode(bool enabled, bool show_toast) {
1751 auto* window_manager = dependencies_.window_manager;
1752 if (!window_manager) {
1753 return;
1754 }
1755
1756 const size_t session_id = window_manager->GetActiveSessionId();
1757 const bool was_enabled = IsWorkbenchWorkflowEnabled();
1758
1759 if (enabled) {
1760 window_manager->OpenWindow(session_id, "dungeon.workbench");
1761
1762 if (!was_enabled) {
1764
1765 // The Workbench replaces room navigation, but not standalone editing
1766 // tools. Keep those tool windows open so user-owned HM/ZS-style layouts
1767 // survive workflow switches; the embedded inspector yields ownership to
1768 // any standalone tool that is already visible.
1769 for (const auto& descriptor :
1770 window_manager->GetWindowsInCategory(session_id, "Dungeon")) {
1771 const std::string& card_id = descriptor.card_id;
1772 if (!IsWorkbenchNavigationWindowId(card_id) ||
1773 !window_manager->IsWindowOpen(session_id, card_id) ||
1774 window_manager->IsWindowPinned(session_id, card_id)) {
1775 continue;
1776 }
1778 window_manager->CloseWindow(session_id, card_id);
1779 }
1780 }
1781 } else {
1782 window_manager->CloseWindow(session_id, "dungeon.workbench");
1783 window_manager->OpenWindow(session_id, kRoomMatrixId);
1784 window_manager->OpenWindow(session_id, "dungeon.entrance_properties");
1785 if (current_room_id_ >= 0) {
1787 }
1788 for (const std::string& card_id :
1790 window_manager->OpenWindow(session_id, card_id);
1791 }
1793 }
1794
1795 const bool now_enabled = IsWorkbenchWorkflowEnabled();
1796 if (is_loaded_ && was_enabled != now_enabled &&
1798 // One room model and the same tools serve either canvas host. Rebind all
1799 // room-scoped panel pointers before closed standalone viewers are retired
1800 // by DrawRoomPanels on the next frame.
1802 }
1803
1804 if (show_toast && dependencies_.toast_manager && was_enabled != now_enabled) {
1806 now_enabled ? "Dungeon workflow: Workbench"
1807 : "Dungeon workflow: Standalone Panels",
1809 }
1810}
1811
1813 bool show_toast) {
1817}
1818
1822
1824 for (int i = 0; i < active_rooms_.Size; i++) {
1825 int room_id = active_rooms_[i];
1826 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1827 bool panel_visible = true;
1829 panel_visible = dependencies_.window_manager->IsWindowOpen(card_id);
1830 }
1831
1832 if (!panel_visible) {
1834 room_cards_.erase(room_id);
1835 active_rooms_.erase(active_rooms_.Data + i);
1836 ReleaseRoomPanelSlotId(room_id);
1837 // Clean up viewer
1838 room_viewers_.Erase(room_id);
1839 i--;
1840 continue;
1841 }
1842
1843 bool is_pinned = dependencies_.window_manager &&
1845 std::string active_category =
1848 : "";
1849
1850 if (active_category != "Dungeon" && !is_pinned) {
1851 continue;
1852 }
1853
1854 // Ensure room card exists (should have been created by ShowRoomPanel/ShowPanel)
1855 if (room_cards_.find(room_id) == room_cards_.end()) {
1856 ShowRoomPanel(room_id);
1857 }
1858
1859 auto& room_card = room_cards_[room_id];
1860 bool open = true;
1861
1862 ImGui::SetNextWindowClass(&room_window_class_);
1863 if (room_dock_id_ == 0) {
1864 room_dock_id_ = ImGui::GetID("DungeonRoomDock");
1865 }
1866 ImGui::SetNextWindowDockID(room_dock_id_, ImGuiCond_FirstUseEver);
1867
1868 if (room_card->Begin(&open)) {
1869 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) {
1870 OnRoomSelected(room_id, /*request_focus=*/false);
1871 }
1872 DrawRoomTab(room_id);
1873 }
1874 room_card->End();
1875
1876 if (!open) {
1879 }
1880
1881 room_cards_.erase(room_id);
1882 active_rooms_.erase(active_rooms_.Data + i);
1883 room_viewers_.Erase(room_id);
1884 ReleaseRoomPanelSlotId(room_id);
1885 i--;
1886 }
1887 }
1888}
1889
1891 if (auto it = room_panel_slot_ids_.find(room_id);
1892 it != room_panel_slot_ids_.end()) {
1893 return it->second;
1894 }
1895 const int slot_id = next_room_panel_slot_id_++;
1896 room_panel_slot_ids_[room_id] = slot_id;
1897 return slot_id;
1898}
1899
1901 room_panel_slot_ids_.erase(room_id);
1902}
1903
1905 const auto& theme = AgentUI::GetTheme();
1906 if (room_id < 0 || room_id >= 0x128) {
1907 ImGui::Text(tr("Invalid room ID: %d"), room_id);
1908 return;
1909 }
1910
1911 auto& room = rooms_[room_id];
1912
1913 if (!room.IsLoaded()) {
1914 auto status = room_loader_.LoadRoom(room_id, room);
1915 if (!status.ok()) {
1916 ImGui::TextColored(theme.text_error_red, tr("Failed to load room: %s"),
1917 status.message().data());
1918 return;
1919 }
1920
1922 auto sys_status = dungeon_editor_system_->ReloadRoom(room_id);
1923 if (!sys_status.ok()) {
1924 LOG_ERROR("DungeonEditorV2", "Failed to load system data: %s",
1925 sys_status.message().data());
1926 }
1927 }
1928 }
1929
1930 if (room.IsLoaded()) {
1931 bool needs_render = false;
1932
1933 if (room.blocks().empty()) {
1935 room.LoadRoomGraphics();
1936 needs_render = true;
1937 }
1938
1939 if (!room.AreObjectsLoaded()) {
1940 room.LoadObjects();
1941 needs_render = true;
1942 }
1943
1944 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1945 if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
1946 room.RenderRoomGraphics();
1947 }
1948 }
1949
1950 if (room.IsLoaded()) {
1951 ImGui::TextColored(theme.text_success_green, ICON_MD_CHECK " Loaded");
1952 } else {
1953 ImGui::TextColored(theme.text_error_red, ICON_MD_PENDING " Not Loaded");
1954 }
1955 ImGui::SameLine();
1956 ImGui::TextDisabled(tr("Objects: %zu"), room.GetTileObjects().size());
1957
1958 if (core::FeatureFlags::get().dungeon.kUseWorkbench &&
1960 ImGui::SameLine();
1961 const std::string return_label = absl::StrFormat(
1962 ICON_MD_WORKSPACES " Workbench##return_workbench_%03X", room_id);
1963 if (ImGui::SmallButton(return_label.c_str())) {
1965 }
1966 if (ImGui::IsItemHovered()) {
1967 ImGui::SetTooltip(
1968 tr("Switch back to the integrated Dungeon Workbench workflow."));
1969 }
1970 }
1971
1972 // Warp to Room button — sends room ID to running Mesen2 emulator
1973 ImGui::SameLine();
1974 {
1976 bool connected = client && client->IsConnected();
1977 if (!connected) {
1978 ImGui::BeginDisabled();
1979 }
1980 std::string warp_label =
1981 absl::StrFormat(ICON_MD_ROCKET_LAUNCH " Warp##warp_%03X", room_id);
1982 if (ImGui::SmallButton(warp_label.c_str())) {
1983 auto status = client->WriteWord(0x7E00A0, static_cast<uint16_t>(room_id));
1984 if (status.ok()) {
1987 absl::StrFormat("Warped to room 0x%03X", room_id),
1989 }
1990 } else {
1993 absl::StrFormat("Warp failed: %s", status.message()),
1995 }
1996 }
1997 }
1998 if (!connected) {
1999 ImGui::EndDisabled();
2000 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
2001 ImGui::SetTooltip(tr("Connect to Mesen2 first"));
2002 }
2003 } else {
2004 if (ImGui::IsItemHovered()) {
2005 ImGui::SetTooltip(tr("Warp to room 0x%03X in Mesen2"), room_id);
2006 }
2007 }
2008 }
2009
2010 ImGui::Separator();
2011
2012 // Use per-room viewer; reserve a bottom strip for the shared dungeon
2013 // status bar so this Windows-mode standalone window mirrors the Workbench
2014 // canvas pane's affordances.
2015 if (auto* viewer = GetStandaloneViewerForRoom(room_id)) {
2016 const float status_bar_reserved =
2017 std::max(ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
2019 ImGui::GetStyle().ItemSpacing.y;
2020 if (ImGui::BeginChild("##StandaloneRoomBody",
2021 ImVec2(0.0f, -status_bar_reserved), false)) {
2022 viewer->DrawDungeonCanvas(room_id);
2024 }
2025 ImGui::EndChild();
2026
2027 bool room_dirty = false;
2028 if (room_id >= 0 && room_id < static_cast<int>(rooms_.size())) {
2029 room_dirty = rooms_[room_id].HasUnsavedChanges();
2030 }
2031 const char* tool_mode =
2032 viewer->object_interaction().mode_manager().GetModeName();
2033 auto status = DungeonStatusBar::BuildState(*viewer, tool_mode, room_dirty);
2034 status.workflow_mode = workflow_mode_names::kStandalone;
2035 status.workflow_primary = false;
2036 status.can_undo = undo_manager_.CanUndo();
2037 status.can_redo = undo_manager_.CanRedo();
2038 status.undo_depth = static_cast<int>(undo_manager_.UndoStackSize());
2039 status.on_undo = [this]() {
2040 Undo().IgnoreError();
2041 };
2042 status.on_redo = [this]() {
2043 Redo().IgnoreError();
2044 };
2045 DungeonStatusBar::Draw(status);
2046 }
2047}
2048
2050 if (ImGui::GetCurrentContext() == nullptr) {
2052 return;
2053 }
2054 room_canvas_delete_shortcut_frame_ = ImGui::GetFrameCount();
2055}
2056
2058 if (!room_canvas_delete_shortcut_frame_.has_value()) {
2059 return;
2060 }
2061 if (ImGui::GetCurrentContext() == nullptr ||
2062 *room_canvas_delete_shortcut_frame_ != ImGui::GetFrameCount()) {
2064 }
2065}
2066
2068 DungeonCanvasViewer& viewer) {
2070 if (!room_canvas_delete_shortcut_frame_.has_value() ||
2071 ImGui::GetIO().WantTextInput || ImGui::IsAnyItemActive() ||
2072 !viewer.CanHandleRoomCanvasShortcut()) {
2073 return false;
2074 }
2075
2077 viewer.DeleteSelectedObjects();
2078 return true;
2079}
2080
2082 switch (intent) {
2084 OnRoomSelected(room_id, /*request_focus=*/true);
2085 break;
2087 // Explicitly pivot to panel workflow when user asks for standalone room.
2088 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2089 return;
2090 }
2093 }
2094 // Update shared state (same as OnRoomSelected with request_focus=true)
2095 OnRoomSelected(room_id, /*request_focus=*/false);
2096 // Now force-open a standalone panel for this room
2097 ShowRoomPanel(room_id);
2098 break;
2099 }
2101 OnRoomSelected(room_id, /*request_focus=*/false);
2102 break;
2103 }
2104}
2105
2106void DungeonEditorV2::OnRoomSelected(int room_id, bool request_focus) {
2107 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2108 LOG_WARN("DungeonEditorV2", "Ignoring invalid room selection: %d", room_id);
2109 return;
2110 }
2111 if (room_id != current_room_id_ && workbench_panel_) {
2113 }
2114 current_room_id_ = room_id;
2116 const auto context = ResolveDungeonRenderContextForRoom(room_id);
2117 if (context.uses_entrance()) {
2118 current_entrance_id_ = context.entrance_slot;
2120 }
2121 }
2122 room_selector_.set_current_room_id(static_cast<uint16_t>(room_id));
2123
2124 // Track recent rooms (remove if already present, add to front)
2125 recent_rooms_.erase(
2126 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
2127 recent_rooms_.end());
2128 recent_rooms_.push_front(room_id);
2129 if (recent_rooms_.size() > kMaxRecentRooms) {
2130 recent_rooms_.pop_back();
2131 }
2132
2134 dungeon_editor_system_->SetExternalRoom(&rooms_[room_id]);
2135 }
2136
2137 // Update all sub-panels (Object Editor, Sprite Editor, etc.)
2138 SyncPanelsToRoom(room_id);
2139
2140 // Sync palette with current room (must happen before early return for focus changes)
2141
2142 // Sync palette with current room (must happen before early return for focus changes)
2143 if (room_id >= 0 && room_id < (int)rooms_.size()) {
2144 auto& room = rooms_[room_id];
2145 if (!room.IsLoaded()) {
2146 room_loader_.LoadRoom(room_id, room);
2147 }
2148
2149 if (room.IsLoaded()) {
2151 current_palette_id_ = room.ResolveDungeonPaletteId();
2154
2155 // Update viewer and object editor palette
2156 if (auto* viewer = GetViewerForRoom(room_id)) {
2157 ConfigureViewerRenderContext(viewer, room_id);
2158 viewer->SetCurrentPaletteId(current_palette_id_);
2159
2160 if (game_data()) {
2161 auto dungeon_main_pal_group =
2163 if (current_palette_id_ < (int)dungeon_main_pal_group.size()) {
2164 current_palette_ = dungeon_main_pal_group[current_palette_id_];
2168 viewer->SetCurrentPaletteGroup(current_palette_group_);
2172 }
2173 // Sync palette to graphics panel for proper sheet coloring
2177 }
2180 room_id, current_palette_group_);
2181 }
2182 }
2183 }
2184 }
2185 }
2186 }
2187
2188 // Workbench mode uses a single stable window and does not spawn per-room
2189 // panels. Keep selection + panels in sync and return.
2191 if (dependencies_.window_manager && request_focus) {
2192 // Only force-show if it's already visible or if it's the first initialization
2193 // This avoids obtrusive behavior when the user explicitly closed it.
2194 if (dependencies_.window_manager->IsWindowOpen("dungeon.workbench")) {
2195 dependencies_.window_manager->OpenWindow("dungeon.workbench");
2196 }
2197 }
2198 return;
2199 }
2200
2201 // Check if room is already open
2202 for (int i = 0; i < active_rooms_.Size; i++) {
2203 if (active_rooms_[i] == room_id) {
2204 // Always ensure panel is visible, even if already in active_rooms_
2206 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2208 }
2209 if (request_focus) {
2210 FocusRoom(room_id);
2211 }
2212 return;
2213 }
2214 }
2215
2216 active_rooms_.push_back(room_id);
2218
2220 // Use unified ResourceLabelProvider for room names
2221 std::string room_name = absl::StrFormat(
2222 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2223
2224 std::string base_card_id = absl::StrFormat("dungeon.room_%d", room_id);
2225
2227 {.card_id = base_card_id,
2228 .display_name = room_name,
2229 .window_title = ICON_MD_GRID_ON " " + room_name,
2230 .icon = ICON_MD_GRID_ON,
2231 .category = "Dungeon",
2232 .workflow_group = "Rooms",
2234 .shortcut_hint = "",
2235 .visibility_flag = nullptr,
2236 .priority = 200 + room_id});
2237
2239 }
2240}
2241
2243 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
2244 return;
2245 }
2246 current_entrance_id_ = entrance_id;
2250
2251 const int room_id = ResolveEntranceRoomId(entrance_id);
2252 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2253 return;
2254 }
2255 OnRoomSelected(room_id);
2256}
2257
2259 if (entrance_id < -1 || entrance_id >= static_cast<int>(entrances_.size()))
2260 return;
2261 automatic_entrance_preview_ = entrance_id == -1;
2263 current_entrance_id_ = entrance_id;
2265 OnRoomSelected(current_room_id_, /*request_focus=*/false);
2266}
2267
2268int DungeonEditorV2::ResolveEntranceRoomId(int entrance_id) const {
2269 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
2270 return -1;
2271 }
2272 if (entrance_id < zelda3::kNumDungeonSpawnPoints) {
2273 const auto& spawn = spawn_points_[entrance_id];
2274 return spawn.spawn_id() == entrance_id ? spawn.room_id : -1;
2275 }
2276 return entrances_[entrance_id].room_;
2277}
2278
2280 int room_id) const {
2283 return context.effective_blockset();
2284}
2285
2287 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2288 return;
2289 }
2290 auto& room = rooms_[room_id];
2291 if (!room.IsLoaded()) {
2292 return;
2293 }
2294 room.SetRenderEntranceBlockset(
2296}
2297
2299 int room_id) {
2300 if (viewer == nullptr) {
2301 return;
2302 }
2305 viewer->SetEntranceRenderContext(context.entrance_slot,
2306 context.effective_blockset());
2307 viewer->SetEntranceBlocksetResolver([this](int target_room) {
2308 return ResolveSelectedEntranceBlocksetForRoom(target_room);
2309 });
2310 const auto camera = GetEntranceCameraState(context.entrance_slot);
2311 if (camera.has_value() && camera->room_id == room_id) {
2312 viewer->SetEntranceCameraOverlay(context.entrance_slot, *camera);
2313 } else {
2315 }
2316}
2317
2318void DungeonEditorV2::add_room(int room_id) {
2319 OnRoomSelected(room_id);
2320}
2321
2323 if (entrance_id < 0 || entrance_id >= zelda3::kNumRegularDungeonEntrances)
2324 return;
2326}
2327
2329 auto it = room_cards_.find(room_id);
2330 if (it != room_cards_.end()) {
2331 it->second->Focus();
2332 }
2333}
2334
2338
2339absl::Status DungeonEditorV2::LoadProposalForReview(const std::string& path) {
2340 if (!workbench_panel_) {
2341 return absl::FailedPreconditionError("Dungeon Workbench is not loaded");
2342 }
2344 return absl::InvalidArgumentError(workbench_panel_->proposal_error());
2345 }
2346 OpenWindow("dungeon.workbench");
2348 return absl::OkStatus();
2349}
2350
2356 OpenWindow("dungeon.workbench");
2357 } else {
2359 }
2360 }
2361}
2362
2364 if (enabled && dependencies_.window_manager) {
2366 OpenWindow("dungeon.workbench");
2367 if (workbench_panel_) {
2369 }
2370 } else {
2374 }
2377 }
2378 }
2379}
2380
2384
2387 LOG_ERROR("DungeonEditorV2", "Cannot place object: Invalid room ID %d",
2389 const std::string message = absl::StrFormat(
2390 "Cannot place 0x%02X: invalid room %d", obj.id_, current_room_id_);
2393 } else if (dependencies_.toast_manager) {
2395 }
2396 return;
2397 }
2398
2399 auto& room = rooms_[current_room_id_];
2400
2401 LOG_INFO("DungeonEditorV2",
2402 "Placing object ID=0x%02X at position (%d,%d) in room %03X", obj.id_,
2403 obj.x_, obj.y_, current_room_id_);
2404
2405 room.RenderRoomGraphics();
2406 LOG_DEBUG("DungeonEditorV2",
2407 "Object placed and room re-rendered successfully");
2408 // Brief success feedback so the user knows the placement was accepted.
2409 // Kept minimal (no inline panel update) — success is non-spammy by design.
2412 absl::StrFormat("Placed 0x%02X in room %03X", obj.id_,
2415 }
2416}
2417
2419 const std::function<void(int, zelda3::Room&, const std::string&, bool,
2420 int)>& visitor) {
2421 rooms_.ForEachLoaded([&](int id, zelda3::Room& room) {
2423 const auto previous_blocks = room.blocks();
2424 const auto previous_main = room.resolved_main_blockset();
2425 room.LoadRoomGraphics();
2426 if (room.blocks() != previous_blocks ||
2427 room.resolved_main_blockset() != previous_main) {
2428 // Membership resolution must not consume a group change without leaving
2429 // the room dirty for its eventual on-demand render.
2430 room.MarkGraphicsDirty();
2431 }
2433 visitor(id, room, DungeonRenderContextSourceName(context),
2434 id == current_room_id_, context.entrance_slot);
2435 });
2436}
2437
2439 int room_id, const zelda3::RoomObject& object) {
2440 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2441 LOG_WARN("DungeonEditorV2", "Edit Graphics ignored (invalid room id %d)",
2442 room_id);
2443 return;
2444 }
2445
2446 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
2447 if (!editor_manager) {
2448 LOG_WARN("DungeonEditorV2",
2449 "Edit Graphics ignored (editor manager unavailable)");
2450 return;
2451 }
2452
2453 auto& room = rooms_[room_id];
2455 room.PrepareForRender();
2456 const auto object_stamp = GraphicsObjectStamp(object);
2457 std::optional<size_t> placed_index;
2458 bool ambiguous_placement = false;
2459 // The canvas callback supplies an object copy, without its room index.
2460 // Infer a placement only when its scalar identity has one match. An
2461 // unmatched copy is a catalog pattern; duplicates may be previewed, but
2462 // their mutation source is ambiguous until the callback carries an index.
2463 const auto& placed_objects = room.GetTileObjects();
2464 for (size_t i = 0; i < placed_objects.size(); ++i) {
2465 if (GraphicsObjectStamp(placed_objects[i]) != object_stamp)
2466 continue;
2467 if (placed_index) {
2468 ambiguous_placement = true;
2469 break;
2470 }
2471 placed_index = i;
2472 }
2474 if (!context.ok()) {
2475 LOG_WARN("DungeonEditorV2", "Edit Graphics: %s",
2476 std::string(context.status().message()).c_str());
2478 dependencies_.toast_manager->Show(std::string(context.status().message()),
2480 }
2481 return;
2482 }
2483 const auto entrance = ResolveDungeonRenderContextForRoom(room_id);
2484 context->label +=
2485 absl::StrFormat(" / %s", DungeonRenderContextSourceName(entrance));
2486 auto resolve_live_source =
2487 [this, room_id, object, object_stamp, placed_index, ambiguous_placement,
2488 entrance, owner = game_data_,
2489 source_rom = rom_](GraphicsEditContext& fresh) -> absl::Status {
2490 if (game_data_ != owner || rom_ != source_rom || !rom_ ||
2491 !rom_->is_loaded()) {
2492 return absl::FailedPreconditionError("Dungeon source session changed");
2493 }
2494 if (ambiguous_placement) {
2495 return absl::FailedPreconditionError(
2496 "Dungeon object placement is ambiguous; reopen Graphics");
2497 }
2498 auto* live_room = rooms_.GetIfMaterialized(room_id);
2499 if (!live_room || !live_room->IsLoaded()) {
2500 return absl::FailedPreconditionError("Dungeon source room changed");
2501 }
2502 if (!SameGraphicsEntrance(entrance,
2504 return absl::FailedPreconditionError(
2505 "Dungeon entrance or blockset source changed; reopen Graphics");
2506 }
2507 const zelda3::RoomObject* live_object = &object;
2508 if (placed_index) {
2509 const auto& objects = live_room->GetTileObjects();
2510 if (*placed_index >= objects.size() ||
2511 GraphicsObjectStamp(objects[*placed_index]) != object_stamp) {
2512 return absl::FailedPreconditionError(
2513 "Dungeon object placement changed; reopen Graphics");
2514 }
2515 live_object = &objects[*placed_index];
2516 }
2518 // Prepare resolves changed assignments and committed sheet revisions. A
2519 // read-only source check must not invalidate an unchanged staged preview.
2520 live_room->PrepareForRender();
2521 auto refreshed =
2522 MakeDungeonGraphicsEditContext(*live_room, *live_object, owner);
2523 if (!refreshed.ok())
2524 return refreshed.status();
2525 fresh = std::move(*refreshed);
2526 return absl::OkStatus();
2527 };
2528 context->resolve_live_source = resolve_live_source;
2529 ConfigureStagedGraphicsPreview(*context, room_id, resolve_live_source);
2530 // Display refresh retains its earlier captured-source behavior. The strict
2531 // live resolver runs only at the palette/pixel mutation boundary.
2532 context->refresh_palettes = [this, room_id, object, owner = game_data_](
2533 GraphicsEditContext& source) -> absl::Status {
2534 if (game_data_ != owner) {
2535 return absl::FailedPreconditionError("Dungeon source session changed");
2536 }
2537 auto& live_room = rooms_[room_id];
2539 live_room.MarkGraphicsDirty();
2540 live_room.PrepareForRender();
2541 auto refreshed = MakeDungeonGraphicsEditContext(live_room, object, owner);
2542 if (!refreshed.ok())
2543 return refreshed.status();
2544 return UpdateGraphicsContextPalettes(source, std::move(*refreshed));
2545 };
2546 context->render_preview =
2547 [this, room_id]() -> absl::StatusOr<usage_preview::PreviewImage> {
2548 auto& source = rooms_[room_id];
2550 // A stroke updates store pixels before committing its revision. Usage
2551 // Preview already throttles on pixel changes; render those live pixels too.
2552 source.MarkGraphicsDirty();
2553 source.PrepareForRender();
2555 layers.ApplyLayerMerging(source.layer_merging());
2556 layers.ApplyRoomEffect(source.effect());
2557 layers.ApplyGameLayerRegisters(source.GameLayerRegisters());
2558 gfx::Bitmap composite;
2559 source.RenderComposite(layers, composite);
2560 return usage_preview::SnapshotBitmap(composite);
2561 };
2562 context->return_to_source = [this, editor_manager, room_id]() {
2563 editor_manager->SwitchToEditor(EditorType::kDungeon, true);
2564 OnRoomSelected(room_id);
2565 };
2566
2567 if (auto* editor_set = editor_manager->GetCurrentEditorSet()) {
2568 if (auto* graphics = editor_set->GetGraphicsEditor()) {
2569 const auto loaded =
2570 editor_manager->EnsureEditorAssetsLoaded(EditorType::kGraphics);
2571 if (!loaded.ok()) {
2573 dependencies_.toast_manager->Show(std::string(loaded.message()),
2575 }
2576 return;
2577 }
2578 const auto status = graphics->OpenInContext(std::move(*context));
2579 if (!status.ok()) {
2580 LOG_WARN("DungeonEditorV2", "Edit Graphics: %s",
2581 std::string(status.message()).c_str());
2583 dependencies_.toast_manager->Show(std::string(status.message()),
2585 }
2586 return;
2587 }
2588 editor_manager->SwitchToEditor(EditorType::kGraphics, true);
2589 }
2590 }
2591}
2592
2594 int room_id, const zelda3::RoomObject& object) {
2595 if (object_tile_editor_panel_ == nullptr) {
2596 return absl::FailedPreconditionError(
2597 "Object Tile Editor panel is unavailable");
2598 }
2599 if (dependencies_.window_manager == nullptr) {
2600 return absl::FailedPreconditionError(
2601 "Workspace window manager is unavailable");
2602 }
2603 if (rom_ == nullptr || !rom_->is_loaded()) {
2604 return absl::FailedPreconditionError("ROM not loaded");
2605 }
2606 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2607 return absl::OutOfRangeError("Dungeon room id is outside the room store");
2608 }
2610 return absl::UnimplementedError(
2611 "Tile editing is not yet supported for this object layout");
2612 }
2613 if (game_data_ == nullptr ||
2615 return absl::FailedPreconditionError("Dungeon palette data is unavailable");
2616 }
2617
2618 auto& room = rooms_[room_id];
2619 if (!room.IsLoaded()) {
2620 RETURN_IF_ERROR(room_loader_.LoadRoom(room_id, room));
2621 }
2623 room.PrepareForRender();
2624
2625 const int palette_id = room.ResolveDungeonPaletteId();
2626 const auto& dungeon_palettes = game_data_->palette_groups.dungeon_main;
2627 if (palette_id < 0 ||
2628 palette_id >= static_cast<int>(dungeon_palettes.size())) {
2629 return absl::OutOfRangeError(
2630 "Room resolves to an unavailable dungeon palette");
2631 }
2632 auto room_palette = dungeon_palettes[palette_id];
2633 const auto palette_group = zelda3::BuildDungeonRenderPaletteGroupFromGameData(
2634 room_palette, game_data_);
2635
2636 const absl::Status open_status = object_tile_editor_panel_->OpenForObject(
2637 object.id_, room_id, &rooms_, palette_group);
2638 if (!open_status.ok()) {
2639 // A modified hidden session rejects replacement to prevent data loss.
2640 // Surface that existing session so the user can apply, revert, or use the
2641 // explicit discard action described by the returned status.
2645 }
2646 return open_status;
2647 }
2651 return absl::NotFoundError(
2652 "Object Tile Editor window is not registered in this session");
2653 }
2654 return absl::OkStatus();
2655}
2656
2657void DungeonEditorV2::SwapRoomInPanel(int old_room_id, int new_room_id) {
2658 // Defer the swap until after the current frame's draw phase completes
2659 // This prevents modifying data structures while ImGui is still using them
2660 if (new_room_id < 0 || new_room_id >= static_cast<int>(rooms_.size())) {
2661 return;
2662 }
2663 pending_swap_.old_room_id = old_room_id;
2664 pending_swap_.new_room_id = new_room_id;
2665 pending_swap_.pending = true;
2666}
2667
2669 if (!pending_swap_.pending) {
2670 return;
2671 }
2672
2673 int old_room_id = pending_swap_.old_room_id;
2674 int new_room_id = pending_swap_.new_room_id;
2675 pending_swap_.pending = false;
2676
2677 // Find the position of old_room in active_rooms_
2678 int swap_index = -1;
2679 for (int i = 0; i < active_rooms_.Size; i++) {
2680 if (active_rooms_[i] == old_room_id) {
2681 swap_index = i;
2682 break;
2683 }
2684 }
2685
2686 if (swap_index < 0) {
2687 // Old room not found in active rooms, just select the new one
2688 OnRoomSelected(new_room_id);
2689 return;
2690 }
2691
2692 // Avoid swapping into an already-open room panel (the per-room maps assume
2693 // room IDs are unique). In this case, just focus/select the existing room.
2694 for (int i = 0; i < active_rooms_.Size; i++) {
2695 if (i != swap_index && active_rooms_[i] == new_room_id) {
2696 OnRoomSelected(new_room_id);
2697 return;
2698 }
2699 }
2700
2701 // Preserve the old panel's stable ImGui window identity by transferring its
2702 // slot id to the new room id.
2703 int slot_id = -1;
2704 if (auto it = room_panel_slot_ids_.find(old_room_id);
2705 it != room_panel_slot_ids_.end()) {
2706 slot_id = it->second;
2707 room_panel_slot_ids_.erase(it);
2708 } else {
2709 slot_id = next_room_panel_slot_id_++;
2710 }
2711 room_panel_slot_ids_[new_room_id] = slot_id;
2712
2713 // Preserve the viewer instance so canvas pan/zoom and UI state don't reset
2714 // when navigating with arrows (swap-in-panel).
2715 room_viewers_.Rename(old_room_id, new_room_id);
2716
2717 // Replace old room with new room in active_rooms_
2718 active_rooms_[swap_index] = new_room_id;
2720
2721 // Unregister old panel
2723 std::string old_card_id = absl::StrFormat("dungeon.room_%d", old_room_id);
2724 const bool old_pinned =
2727
2728 // Register new panel
2729 // Use unified ResourceLabelProvider for room names
2730 std::string new_room_name = absl::StrFormat(
2731 "[%03X] %s", new_room_id, zelda3::GetRoomLabel(new_room_id).c_str());
2732
2733 std::string new_card_id = absl::StrFormat("dungeon.room_%d", new_room_id);
2734
2736 {.card_id = new_card_id,
2737 .display_name = new_room_name,
2738 .window_title = ICON_MD_GRID_ON " " + new_room_name,
2739 .icon = ICON_MD_GRID_ON,
2740 .category = "Dungeon",
2741 .workflow_group = "Rooms",
2743 .shortcut_hint = "",
2744 .visibility_flag = nullptr,
2745 .priority = 200 + new_room_id});
2746
2747 if (old_pinned) {
2748 dependencies_.window_manager->SetWindowPinned(new_card_id, true);
2749 }
2751 }
2752
2753 // Clean up old room's card and viewer
2754 room_cards_.erase(old_room_id);
2755
2756 // Update current selection
2757 OnRoomSelected(new_room_id, /*request_focus=*/false);
2758}
2759
2762 return;
2763 }
2764
2765 const bool enabled = pending_workflow_mode_.enabled;
2766 const bool show_toast = pending_workflow_mode_.show_toast;
2768 SetWorkbenchWorkflowMode(enabled, show_toast);
2769}
2770
2773 return;
2774 }
2775
2779
2780 auto* window_manager = dependencies_.window_manager;
2781 if (!window_manager) {
2782 return;
2783 }
2784
2785 if (!window_manager->OpenWindow(request.session_id, request.window_id)) {
2786 return;
2787 }
2788
2789 if (ImGui::GetCurrentContext() != nullptr) {
2790 const std::string window_name = window_manager->GetWorkspaceWindowName(
2791 request.session_id, request.window_id);
2792 if (!window_name.empty()) {
2793 ImGui::SetWindowFocus(window_name.c_str());
2794 }
2795 }
2796}
2797
2799 room_viewers_.Touch(room_id);
2800}
2801
2803 // No-op: LruCache handles removal internally via Erase()
2804 (void)room_id;
2805}
2806
2808 if (!viewer) {
2809 return;
2810 }
2811
2812 viewer->SetShowObjectPanelCallback([this]() {
2815 OpenWindow("dungeon.workbench");
2816 return;
2817 }
2819 });
2820 viewer->SetShowSpritePanelCallback([this]() {
2823 OpenWindow("dungeon.workbench");
2824 return;
2825 }
2826 OpenWindow("dungeon.sprite_editor");
2827 });
2828 viewer->SetShowItemPanelCallback([this]() {
2831 OpenWindow("dungeon.workbench");
2832 return;
2833 }
2834 OpenWindow("dungeon.item_editor");
2835 });
2836 viewer->SetShowRoomListCallback([this]() {
2839 OpenWindow("dungeon.workbench");
2840 return;
2841 }
2843 });
2844 viewer->SetShowRoomMatrixCallback([this]() {
2847 OpenWindow("dungeon.workbench");
2848 return;
2849 }
2851 });
2852 viewer->SetShowEntranceListCallback([this]() {
2855 OpenWindow("dungeon.workbench");
2856 return;
2857 }
2858 OpenWindow("dungeon.entrance_properties");
2859 });
2860 viewer->SetShowRoomGraphicsCallback([this]() {
2863 OpenWindow("dungeon.workbench");
2864 return;
2865 }
2867 });
2869 [this](int room_id, const zelda3::RoomObject& object) {
2870 FocusObjectCoverage(room_id, object);
2871 });
2872 viewer->SetShowDoorEditorCallback([this]() {
2875 OpenWindow("dungeon.workbench");
2876 return;
2877 }
2878 OpenWindow("dungeon.door_editor");
2879 });
2880 viewer->SetShowDungeonSettingsCallback([this]() {
2881 if (workbench_panel_) {
2883 }
2884 OpenWindow("dungeon.workbench");
2885 });
2887 [this](int target_room_id, const zelda3::RoomObject& object) {
2888 OpenGraphicsEditorForObject(target_room_id, object);
2889 });
2891 [this](int target_room_id, const zelda3::RoomObject& object) {
2892 const absl::Status status =
2893 OpenObjectTileEditorForObject(target_room_id, object);
2894 if (status.ok()) {
2895 return;
2896 }
2897 LOG_ERROR("DungeonEditorV2", "Edit Object Tiles failed: %s",
2898 status.message().data());
2901 absl::StrFormat("Edit Object Tiles failed: %s", status.message()),
2903 }
2904 });
2905 viewer->SetSaveRoomCallback([this](int target_room_id) {
2906 auto status = SaveRoom(target_room_id);
2907 if (!status.ok()) {
2908 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
2909 status.message().data());
2912 absl::StrFormat("Apply Room failed: %s", status.message()),
2914 }
2915 return;
2916 }
2918 dependencies_.toast_manager->Show("Applied room to ROM buffer",
2920 }
2921 });
2922}
2923
2926 (void)room_id;
2927 return GetWorkbenchViewer();
2928 }
2929
2930 return GetStandaloneViewerForRoom(room_id);
2931}
2932
2934 // Set eviction predicate to protect active rooms from eviction
2935 room_viewers_.SetEvictionPredicate([this](const int& candidate) {
2936 for (int i = 0; i < active_rooms_.size(); ++i) {
2937 if (active_rooms_[i] == candidate)
2938 return false;
2939 }
2940 return true;
2941 });
2942
2943 if (auto* existing = room_viewers_.Get(room_id)) {
2944 // Viewer already exists - Get() already touched LRU
2945 auto* viewer_ptr = existing->get();
2946 RefreshViewerRuntimeContext(viewer_ptr, room_id);
2948
2949 // Update pinned state from manager
2951 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2952 viewer_ptr->SetPinned(
2954 viewer_ptr->SetPinCallback([this, card_id, room_id](bool pinned) {
2956 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2957 if (auto* existing_viewer = room_viewers_.Get(room_id)) {
2958 (*existing_viewer)->SetPinned(pinned);
2959 }
2960 }
2961 });
2962 }
2963
2964 return viewer_ptr;
2965 }
2966
2967 // Creating a new viewer - Insert will handle LRU and eviction
2968 {
2969 auto viewer = std::make_unique<DungeonCanvasViewer>(rom_);
2970 viewer->SetCompactHeaderMode(false);
2971 viewer->SetRoomDetailsExpanded(true);
2972 DungeonCanvasViewer* viewer_ptr = viewer.get();
2973 ConfigureEditableViewer(viewer_ptr);
2974 viewer->SetRoomNavigationCallback([this](int target_room) {
2975 if (target_room >= 0 && target_room < static_cast<int>(rooms_.size())) {
2976 OnRoomSelected(target_room);
2977 }
2978 });
2979 // Swap callback swaps the room in the current panel instead of opening new
2980 viewer->SetRoomSwapCallback([this](int old_room, int new_room) {
2981 SwapRoomInPanel(old_room, new_room);
2982 });
2983 // Wire up pinning for room panels.
2984 if (dependencies_.window_manager) {
2985 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2986 viewer->SetPinned(dependencies_.window_manager->IsWindowPinned(card_id));
2987 viewer->SetPinCallback([this, card_id, room_id](bool pinned) {
2988 if (dependencies_.window_manager) {
2989 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2990 // Sync state back to viewer in all panels showing this room
2991 if (auto* existing_viewer = room_viewers_.Get(room_id)) {
2992 (*existing_viewer)->SetPinned(pinned);
2993 }
2994 }
2995 });
2996 }
2997
2998 auto* stored = room_viewers_.Insert(room_id, std::move(viewer));
2999 RefreshViewerRuntimeContext(stored->get(), room_id);
3000 ApplyEntranceRenderContext(room_id);
3001 return stored->get();
3002 }
3003}
3004
3005void DungeonEditorV2::ConfigureEditableViewer(DungeonCanvasViewer* viewer) {
3006 ConfigureViewerUndoHooks(viewer);
3008 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
3009 WireViewerPanelCallbacks(viewer);
3010}
3011
3012void DungeonEditorV2::RefreshViewerRuntimeContext(DungeonCanvasViewer* viewer,
3013 int room_id,
3014 bool rebind_project) {
3015 if (!viewer) {
3016 return;
3017 }
3018
3019 if (viewer->rom() != rom_) {
3020 viewer->SetRom(rom_);
3021 }
3022 if (viewer->rooms() != &rooms_) {
3023 viewer->SetRooms(&rooms_);
3024 }
3025 if (viewer->game_data() != game_data_) {
3026 viewer->SetGameData(game_data_);
3027 }
3028 viewer->SetRenderer(renderer_);
3029 viewer->SetCurrentPaletteGroup(current_palette_group_);
3030 viewer->SetCurrentPaletteId(current_palette_id_);
3031 ConfigureViewerRenderContext(viewer, room_id);
3032 viewer->SetEditorSystem(dungeon_editor_system_.get());
3033 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
3034 if (rebind_project || viewer->project() != dependencies_.project) {
3035 viewer->SetProject(dependencies_.project);
3036 }
3037}
3038
3039DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchViewer() {
3040 if (!workbench_viewer_) {
3041 workbench_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
3042 auto* viewer = workbench_viewer_.get();
3043 viewer->SetCompactHeaderMode(true);
3044 viewer->SetRoomDetailsExpanded(false);
3045 viewer->SetHeaderVisible(false);
3046 viewer->SetHeaderHiddenMetadataHudVisible(false);
3047 ConfigureEditableViewer(viewer);
3048
3049 // In workbench mode, arrow navigation swaps the current room without
3050 // changing window identities.
3051 viewer->SetRoomSwapCallback([this](int /*old_room*/, int new_room) {
3052 OnRoomSelected(new_room, /*request_focus=*/false);
3053 });
3054 viewer->SetRoomNavigationCallback([this](int target_room) {
3055 OnRoomSelected(target_room, /*request_focus=*/false);
3056 });
3057 }
3058
3059 auto* viewer = workbench_viewer_.get();
3060 RefreshViewerRuntimeContext(viewer, current_room_id_);
3061 return viewer;
3062}
3063
3064DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchCompareViewer(int room_id) {
3065 if (!workbench_compare_viewer_) {
3066 workbench_compare_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
3067 auto* viewer = workbench_compare_viewer_.get();
3068 viewer->SetCompactHeaderMode(true);
3069 viewer->SetRoomDetailsExpanded(false);
3070 viewer->SetHeaderHiddenMetadataHudVisible(false);
3071 // Compare viewer is read-only by default: no object selection/mutation, but
3072 // still allows canvas pan/zoom.
3073 viewer->SetObjectInteractionEnabled(false);
3074 viewer->SetHeaderReadOnly(true);
3075 viewer->SetHeaderVisible(false);
3076 }
3077
3078 auto* viewer = workbench_compare_viewer_.get();
3079 RefreshViewerRuntimeContext(viewer, room_id);
3080 viewer->SetObjectInteractionEnabled(false);
3081 viewer->SetHeaderReadOnly(true);
3082 viewer->SetHeaderVisible(false);
3083 return viewer;
3084}
3085
3086void DungeonEditorV2::SyncPanelsToRoom(int room_id) {
3087 if (minecart_track_editor_panel_) {
3088 minecart_track_editor_panel_->SetActiveRoom(room_id);
3089 }
3090 // Update object editor card with current viewer
3091 if (object_selector_panel_) {
3092 object_selector_panel_->SetCanvasViewerProvider(
3093 [this]() { return GetViewerForRoom(current_room_id_); });
3094 object_selector_panel_->SetCurrentRoom(room_id);
3095 object_selector_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
3096 }
3097 if (door_editor_panel_) {
3098 door_editor_panel_->SetCanvasViewerProvider(
3099 [this]() { return GetViewerForRoom(current_room_id_); });
3100 door_editor_panel_->SetCurrentRoom(room_id);
3101 door_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
3102 }
3103
3104 // Update sprite and item editor panels with current viewer
3105 if (sprite_editor_panel_) {
3106 sprite_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
3107 }
3108 if (item_editor_panel_) {
3109 item_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
3110 }
3111 if (custom_collision_panel_) {
3112 auto* viewer = GetViewerForRoom(room_id);
3113 custom_collision_panel_->SetCanvasViewer(viewer);
3114 if (viewer) {
3115 custom_collision_panel_->SetInteraction(&viewer->object_interaction());
3116 }
3117 }
3118 if (water_fill_panel_) {
3119 auto* viewer = GetViewerForRoom(room_id);
3120 water_fill_panel_->SetCanvasViewer(viewer);
3121 if (viewer) {
3122 water_fill_panel_->SetInteraction(&viewer->object_interaction());
3123 }
3124 }
3125
3126 if (room_tag_editor_panel_) {
3127 room_tag_editor_panel_->SetCurrentRoomId(room_id);
3128 }
3129
3130 if (overlay_manager_panel_) {
3131 auto* viewer = GetViewerForRoom(room_id);
3132 if (viewer) {
3134 overlay_state.show_grid = viewer->mutable_show_grid();
3135 overlay_state.show_object_bounds = viewer->mutable_show_object_bounds();
3136 overlay_state.show_coordinate_overlay =
3137 viewer->mutable_show_coordinate_overlay();
3138 overlay_state.show_room_debug_info =
3139 viewer->mutable_show_room_debug_info();
3140 overlay_state.show_texture_debug = viewer->mutable_show_texture_debug();
3141 overlay_state.show_layer_info = viewer->mutable_show_layer_info();
3142 overlay_state.show_minecart_tracks =
3143 viewer->mutable_show_minecart_tracks();
3144 overlay_state.show_custom_collision =
3145 viewer->mutable_show_custom_collision_overlay();
3146 overlay_state.show_track_collision =
3147 viewer->mutable_show_track_collision_overlay();
3148 overlay_state.show_camera_quadrants =
3149 viewer->mutable_show_camera_quadrant_overlay();
3150 overlay_state.show_minecart_sprites =
3151 viewer->mutable_show_minecart_sprite_overlay();
3152 overlay_state.show_collision_legend =
3153 viewer->mutable_show_track_collision_legend();
3154 overlay_manager_panel_->SetState(overlay_state);
3155 }
3156 }
3157}
3158
3159void DungeonEditorV2::ShowRoomPanel(int room_id) {
3160 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
3161 return;
3162 }
3163
3164 bool already_active = false;
3165 for (int i = 0; i < active_rooms_.Size; ++i) {
3166 if (active_rooms_[i] == room_id) {
3167 already_active = true;
3168 break;
3169 }
3170 }
3171 if (!already_active) {
3172 active_rooms_.push_back(room_id);
3173 room_selector_.set_active_rooms(active_rooms_);
3174 }
3175
3176 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
3177
3178 if (dependencies_.window_manager) {
3179 if (!dependencies_.window_manager->GetWindowDescriptor(
3180 dependencies_.window_manager->GetActiveSessionId(), card_id)) {
3181 std::string room_name = absl::StrFormat(
3182 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
3183 dependencies_.window_manager->RegisterWindow(
3184 {.card_id = card_id,
3185 .display_name = room_name,
3186 .window_title = ICON_MD_GRID_ON " " + room_name,
3187 .icon = ICON_MD_GRID_ON,
3188 .category = "Dungeon",
3189 .workflow_group = "Rooms",
3190 .presentation = WindowPresentationPolicy::OptionalPopOut(),
3191 .shortcut_hint = "",
3192 .visibility_flag = nullptr,
3193 .priority = 200 + room_id});
3194 }
3195 dependencies_.window_manager->OpenWindow(card_id);
3196 }
3197
3198 // Create or update the PanelWindow for this room
3199 if (room_cards_.find(room_id) == room_cards_.end()) {
3200 std::string base_name = absl::StrFormat(
3201 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
3202 const int slot_id = GetOrCreateRoomPanelSlotId(room_id);
3203 std::string card_name_str = absl::StrFormat(
3204 "%s###RoomPanelSlot%d", MakePanelTitle(base_name).c_str(), slot_id);
3205
3206 auto card = std::make_shared<gui::PanelWindow>(card_name_str.c_str(),
3208 card->SetDefaultSize(620, 700);
3209 room_cards_[room_id] = card;
3210 }
3211}
3212
3213} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
Loads and queries the hack manifest JSON for yaze-ASM integration.
const MinecartTrackLayout & minecart_track_layout() const
bool loaded() const
Check if the manifest has been loaded.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetCheckObjectCoverageCallback(std::function< void(int, const zelda3::RoomObject &)> callback)
void SetShowEntranceListCallback(std::function< void()> callback)
void SetEditorSystem(zelda3::DungeonEditorSystem *system)
void SetMinecartTrackPanel(MinecartTrackEditorPanel *panel)
void SetShowItemPanelCallback(std::function< void()> callback)
void SetShowRoomGraphicsCallback(std::function< void()> callback)
void SetProject(const project::YazeProject *project)
void SetRenderer(gfx::IRenderer *renderer)
void SetEditGraphicsCallback(std::function< void(int, const zelda3::RoomObject &)> callback)
void SetShowRoomListCallback(std::function< void()> callback)
void SetEntranceBlocksetResolver(std::function< uint8_t(int)> resolver)
void RefreshRomBackedState(Rom *rom, zelda3::GameData *game_data, DungeonRoomStore *rooms, int room_id)
DungeonObjectInteraction & object_interaction()
void SetShowRoomMatrixCallback(std::function< void()> callback)
void SetShowSpritePanelCallback(std::function< void()> callback)
void SetShowObjectPanelCallback(std::function< void()> callback)
void SetEntranceRenderContext(int entrance_id, uint8_t entrance_blockset)
const project::YazeProject * project() const
void SetEditObjectTilesCallback(std::function< void(int, const zelda3::RoomObject &)> callback)
void SetSaveRoomCallback(std::function< void(int)> callback)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group, bool force_refresh=false)
void SetShowDungeonSettingsCallback(std::function< void()> callback)
void SetEntranceCameraOverlay(int entrance_slot, const DungeonEntranceCameraState &state)
void SetRooms(DungeonRoomStore *rooms)
void SetGameData(zelda3::GameData *game_data)
void SetShowDoorEditorCallback(std::function< void()> callback)
void SetPinCallback(std::function< void(bool)> callback)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
std::string proposal_review_path() const
DungeonRenderContext ResolveDungeonRenderContextForRoom(int room_id) const
absl::Status EditRoomGraphicsSheet(int room_id, int slot, int sheet_id)
std::unique_ptr< CustomCollisionPanel > owned_custom_collision_panel_
class CustomCollisionPanel * custom_collision_panel_
std::optional< int > room_canvas_delete_shortcut_frame_
void RefreshViewerRuntimeContext(DungeonCanvasViewer *viewer, int room_id, bool rebind_project=false)
void ToggleWorkbenchWorkflowMode(bool show_toast=true)
void OpenGraphicsEditorForObject(int room_id, const zelda3::RoomObject &object)
absl::Status OpenObjectTileEditorForObject(int room_id, const zelda3::RoomObject &object)
class ItemEditorPanel * item_editor_panel_
std::unique_ptr< RoomTagEditorPanel > owned_room_tag_editor_panel_
class ObjectCoveragePanel * object_coverage_panel_
std::unique_ptr< ObjectCoveragePanel > CreateObjectCoveragePanel()
absl::StatusOr< zelda3::RoomsetUsageReport > GetRoomGraphicsUsage(int roomset) const
absl::Status ApplyRoomLabelEdit(int room_id, std::optional< std::string > label)
void SetDependencies(const EditorDependencies &deps) override
std::unique_ptr< ObjectCoveragePanel > owned_object_coverage_panel_
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
gfx::PaletteGroup current_palette_group_
void OnEntranceSelected(int entrance_id)
static constexpr const char * kEntranceListId
PendingStandaloneToolWindow pending_standalone_tool_window_
class SpriteEditorPanel * sprite_editor_panel_
void HandleObjectPlaced(const zelda3::RoomObject &obj)
std::unique_ptr< zelda3::DungeonEditorSystem > dungeon_editor_system_
std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > entrances_
void OpenWindow(const std::string &window_id)
DoorEditorContent * door_editor_panel_
absl::Status LoadProposalForReview(const std::string &path)
std::unordered_map< int, int > room_panel_slot_ids_
void OpenRegularEntrance(int entrance_id)
class DungeonWorkbenchContent * workbench_panel_
int ResolveEntranceRoomId(int entrance_id) const
void OnRoomSelected(int room_id, bool request_focus=true)
absl::Status ApplyMinecartCollisionStamp(int room_id, const zelda3::CustomCollisionMap &expected, const MinecartCollisionStamp &stamp)
std::optional< DungeonEntranceCameraState > GetEntranceCameraState(int slot_index) const
PendingCollisionUndo pending_collision_undo_
void FocusObjectCoverage(int room_id, const zelda3::RoomObject &object)
void ConfigureEntrancePropertyEditCallbacks(DungeonEntrancesPanel *panel)
ObjectTileEditorPanel * object_tile_editor_panel_
ObjectSelectorContent * object_selector_panel_
OverlayManagerPanel * overlay_manager_panel_
uint8_t ResolveSelectedEntranceBlocksetForRoom(int room_id) const
DungeonCanvasViewer * GetWorkbenchCompareViewer(int room_id)
void ConfigureStagedGraphicsPreview(GraphicsEditContext &context, int room_id, std::function< absl::Status(GraphicsEditContext &)> resolve_live_source)
bool CycleObjectSelection(int direction)
void WireViewerPanelCallbacks(DungeonCanvasViewer *viewer)
absl::Status EditRoomMetadata(int room_id, const RoomMetadataEdit &edit)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
PendingWaterFillUndo pending_water_fill_undo_
RoomMatrixContent * room_matrix_panel_
std::unique_ptr< MinecartTrackEditorPanel > owned_minecart_track_editor_panel_
absl::Status ApplyMinecartTrackEdit(const MinecartTrack &before, const MinecartTrack &after, bool move_cart)
void SetGraphicsPreviewEntrance(int entrance_id)
std::array< PendingEntityUndo, 3 > pending_entity_undo_
gui::PaletteEditorWidget palette_editor_
std::unique_ptr< DoorEditorContent > owned_door_editor_panel_
bool ConsumeRoomCanvasDeleteShortcut(DungeonCanvasViewer &viewer)
static bool IsValidRoomId(int room_id)
void VisitGraphicsUsageRooms(const std::function< void(int, zelda3::Room &, const std::string &, bool, int)> &visitor)
void SwapRoomInPanel(int old_room_id, int new_room_id)
RoomGraphicsContent * room_graphics_panel_
static constexpr size_t kMaxRecentRooms
std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > spawn_points_
void SetWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
class RoomTagEditorPanel * room_tag_editor_panel_
std::optional< SaveTransactionSnapshot > save_transaction_snapshot_
std::unique_ptr< ObjectSelectorContent > owned_object_selector_panel_
static constexpr const char * kDoorEditorId
DungeonCanvasViewer * GetViewerForRoom(int room_id)
PendingSelectionUndo pending_selection_undo_
class WaterFillPanel * water_fill_panel_
bool NavigateToAdjacentRoom(DungeonRoomNavigationDirection direction)
DungeonCanvasViewer * GetWorkbenchViewer()
std::unordered_map< int, std::shared_ptr< gui::PanelWindow > > room_cards_
void ApplyEntranceRenderContext(int room_id)
static constexpr const char * kObjectSelectorId
void ConfigureViewerRenderContext(DungeonCanvasViewer *viewer, int room_id)
static constexpr const char * kRoomGraphicsId
void QueueWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
static constexpr const char * kRoomMatrixId
void HandleDungeonPaletteChanged(gui::DungeonPaletteChange change)
static constexpr const char * kRoomSelectorId
PaletteEditorContent * palette_editor_panel_
DungeonSelectionClipboard selection_clipboard_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
absl::Status PlaceMinecartNode(int room_id, int tile_x, int tile_y, const MinecartNodeBrush &brush)
DungeonCanvasViewer * GetStandaloneViewerForRoom(int room_id)
std::unordered_map< int, zelda3::MinecartTrackCollisionOwnership > minecart_collision_ownership_
std::unique_ptr< WaterFillPanel > owned_water_fill_panel_
absl::Status ApplyMinecartCollisionBatch(const std::vector< zelda3::TrackCollisionResult > &preview, const zelda3::GeneratorOptions &options)
DungeonEntrancesPanel * entrance_navigator_panel_
static constexpr const char * kPaletteEditorId
PendingWorkflowMode pending_workflow_mode_
void InvalidateDungeonPaletteUsers(gui::DungeonPaletteChange change)
void HandleArenaPaletteChanged(const std::string &group_name, int palette_index)
std::vector< std::string > workbench_suspended_navigation_window_ids_
void SetProject(const project::YazeProject *project)
void SetObjectPlacedCallback(std::function< void(const zelda3::RoomObject &)> callback)
void SetOpenMinecartEditorWindowCallback(std::function< bool()> callback)
void SetOpenTileEditorWindowCallback(std::function< bool()> callback)
void SetTileEditorPanel(ObjectTileEditorPanel *panel)
void SetEditObjectTilesCallback(std::function< absl::Status(int, const zelda3::RoomObject &)> callback)
absl::Status LoadRoom(int room_id, zelda3::Room &room)
absl::Status LoadRoomEntrances(std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > &entrances)
absl::Status LoadDungeonSpawnPoints(std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > &spawn_points)
void SetGameData(zelda3::GameData *game_data)
void SetRoomSelectedWithIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
void SetEntranceSelectedCallback(std::function< void(int)> callback)
void set_entrances(std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > *entrances)
void set_current_entrance_id(int entrance_id)
void set_rooms(DungeonRoomStore *rooms)
void SetRoomSelectedCallback(std::function< void(int)> callback)
void set_spawn_points(std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > *spawn_points)
void SetProject(const project::YazeProject *project)
void set_active_rooms(const ImVector< int > &rooms)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
void SetGameData(zelda3::GameData *game_data)
static void Draw(const DungeonStatusBarState &state)
static DungeonStatusBarState BuildState(const DungeonCanvasViewer &viewer, const char *tool_mode, bool room_dirty)
void SetOpenKeyboardShortcutsCallback(std::function< void()> callback)
void SetRoomLabelEditCallback(RoomLabelEditCallback callback)
void SetUndoRedoProvider(std::function< bool()> can_undo, std::function< bool()> can_redo, std::function< void()> on_undo, std::function< void()> on_redo, std::function< std::string()> undo_desc, std::function< std::string()> redo_desc, std::function< int()> undo_depth)
void SetPrimaryCanvasDrawnCallback(std::function< void(DungeonCanvasViewer &)> cb)
void SetRoomRenderContextCallback(std::function< void(int)> callback)
void SetObjectCoverageContent(WindowContent *object_coverage)
void SetPitDamageTableProvider(std::function< zelda3::PitDamageTable *()> provider, std::function< void()> on_change={})
void SetStandaloneToolCallbacks(std::function< bool(const std::string &)> is_window_open, std::function< bool(const std::string &)> open_and_focus_window)
void SetEmbeddedEditorPanels(WindowContent *object_selector, WindowContent *door_editor, WindowContent *sprite_editor, WindowContent *item_editor, WindowContent *room_graphics, WindowContent *palette_editor)
void SetEmbeddedToolPanels(RoomTagEditorPanel *room_tags, CustomCollisionPanel *custom_collision, WaterFillPanel *water_fill, MinecartTrackEditorPanel *minecart_tracks)
void NotifyRoomChanged(int previous_room_id)
Called by the editor when the current room changes.
bool LoadProposalOverlay(const std::string &path)
void SetNavigationPanels(RoomMatrixContent *room_matrix, DungeonEntrancesPanel *entrances)
The EditorManager controls the main editor window and manages the various editor classes.
EditorContext context() const
Definition editor.h:376
UndoManager undo_manager_
Definition editor.h:383
virtual void SetDependencies(const EditorDependencies &deps)
Definition editor.h:257
zelda3::GameData * game_data() const
Definition editor.h:369
EditorDependencies dependencies_
Definition editor.h:382
EditorType type_
Definition editor.h:381
void SetCanvasViewer(DungeonCanvasViewer *viewer)
std::string GetId() const override
Unique identifier for this panel.
void SetNodePlacementCallback(NodePlacementCallback callback)
void SetProjectChangedCallback(ProjectChangedCallback callback)
void SetRailPaletteCallback(std::function< bool()> draw)
absl::Status RebindProjectContext(project::YazeProject *project)
void SetTrackEditCallback(TrackEditCallback callback)
void SetCollisionBatchApplyCallback(CollisionBatchApplyCallback callback)
void SetProjectDraftChangedCallback(ProjectDraftChangedCallback callback)
void SetProjectSaveCallback(ProjectSaveCallback callback)
void SetCollisionStampCallback(MinecartCollisionApply apply)
void SetRooms(DungeonRoomStore *rooms)
void SetProject(project::YazeProject *project)
void SetPlacementError(const std::string &message)
void SetRooms(DungeonRoomStore *rooms)
void SetGameData(zelda3::GameData *game_data)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetStandardWritePreflightCallback(StandardWritePreflightCallback callback)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetCurrentPaletteGroupForRoom(int room_id, const gfx::PaletteGroup &group)
void SetTilesAppliedCallback(TilesAppliedCallback callback)
absl::Status OpenForObject(int16_t object_id, int room_id, DungeonRoomStore *rooms)
std::string GetId() const override
Unique identifier for this panel.
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
Set the current palette group for graphics rendering.
void SetPreviewEntranceBlocksetResolver(std::function< uint8_t(int)> resolver)
void SetRooms(DungeonRoomStore *rooms)
std::string GetDisplayString(const std::string &name) const
Live, platform-formatted binding for a shortcut name.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
std::string GetRedoDescription() const
Description of the action that would be redone (for UI)
std::string GetUndoDescription() const
Description of the action that would be undone (for UI)
size_t UndoStackSize() const
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
std::string GetWorkspaceWindowName(size_t session_id, const std::string &base_window_id) const
Resolve the exact ImGui window name for a panel by base ID.
void SetWindowPinned(size_t session_id, const std::string &base_window_id, bool pinned)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
void RegisterWindow(size_t session_id, const WindowDescriptor &descriptor)
bool IsWindowOpen(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
bool IsWindowPinned(size_t session_id, const std::string &base_window_id) const
void UnregisterWindow(size_t session_id, const std::string &base_window_id)
static std::shared_ptr< MesenSocketClient > & GetClient()
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:580
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:587
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
void Initialize(zelda3::GameData *game_data)
Initialize the palette manager with GameData.
static PaletteManager & Get()
Get the singleton instance.
void SetOnDungeonPaletteChanged(std::function< void(DungeonPaletteChange)> callback)
void SetDungeonRenderPaletteMode(bool enabled)
void Initialize(zelda3::GameData *game_data)
Draggable, dockable panel for editor sub-windows.
bool Begin(bool *p_open=nullptr)
void SetDefaultSize(float width, float height)
static CustomObjectManager & Get()
static DrawRoutineRegistry & Get()
static ObjectDimensionTable & Get()
static bool IsEditableStandardObject(int16_t object_id)
Dungeon Room Entrance or Spawn Point.
RoomLayerManager - Manages layer visibility and compositing.
void ApplyGameLayerRegisters(const RoomLayerRegisters &registers)
void ApplyLayerMerging(const LayerMergeType &merge_type)
void ApplyRoomEffect(EffectKey effect)
void MarkLayoutDirty()
Definition room.h:517
void MarkObjectsDirty()
Definition room.h:471
void set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:647
bool HasUnsavedChanges() const
Definition room.h:662
void MarkGraphicsDirty()
Definition room.h:512
bool IsLoaded() const
Definition room.h:970
void ClearWaterFillZone()
Definition room.h:634
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:953
void ClearWaterFillDirty()
Definition room.h:655
auto blocks() const
Definition room.h:1080
int ResolveDungeonPaletteId() const
Definition room.cc:921
void SetGameData(GameData *data)
Definition room.h:1088
uint8_t resolved_main_blockset() const
Definition room.h:1017
void SetWaterFillTile(int x, int y, bool filled)
Definition room.h:611
#define ICON_MD_ROCKET_LAUNCH
Definition icons.h:1612
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_PENDING
Definition icons.h:1398
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_CATEGORY
Definition icons.h:382
#define ICON_MD_WORKSPACES
Definition icons.h:2186
#define LOG_DEBUG(category, format,...)
Definition log.h:133
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
const AgentUITheme & GetTheme()
bool ProjectAdvertisesMinecartTracks(const project::YazeProject *project)
WindowPresentationPolicy WorkbenchTool(std::string capability=std::string())
auto GraphicsObjectStamp(const zelda3::RoomObject &object)
bool IsTransientDungeonRoomWindowId(const std::string &card_id)
bool SameGraphicsEntrance(const DungeonRenderContext &a, const DungeonRenderContext &b)
PreviewImage SnapshotBitmap(const gfx::Bitmap &bitmap)
Copies a palettized Bitmap (for example a room composite).
constexpr const char * kStandalone
Editors are the view controllers for the application.
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
absl::Status UpdateGraphicsContextPalettes(GraphicsEditContext &context, GraphicsEditContext refreshed)
const char * DungeonRenderContextSourceName(DungeonRenderContextSource source)
std::string PrintShortcut(const std::vector< ImGuiKey > &keys)
absl::StatusOr< GraphicsEditContext > MakeDungeonGraphicsEditContext(const zelda3::Room &room, const zelda3::RoomObject &object, const zelda3::GameData *owner)
RoomSelectionIntent
Intent for room selection in the dungeon editor.
std::string MakePanelTitle(const std::string &title)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:680
std::string GetRoomLabel(int id)
Convenience function to get a room label.
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadLegacyWaterGateZones(Rom *rom, const std::string &symbol_path)
gfx::PaletteGroup BuildDungeonRenderPaletteGroupFromGameData(const gfx::SnesPalette &dungeon_palette, const GameData *game_data)
Definition room.cc:191
constexpr int kNumRegularDungeonEntrances
constexpr int kNumDungeonSpawnPoints
std::unique_ptr< DungeonEditorSystem > CreateDungeonEditorSystem(Rom *rom, GameData *game_data)
Factory function to create dungeon editor system.
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::optional< Source > source
Unified dependency container for all editor types.
Definition editor.h:170
project::YazeProject * project
Definition editor.h:174
gfx::IRenderer * renderer
Definition editor.h:192
ShortcutManager * shortcut_manager
Definition editor.h:186
WorkspaceWindowManager * window_manager
Definition editor.h:182
static WindowPresentationPolicy Diagnostic()
static WindowPresentationPolicy CoreWorkspace()
static WindowPresentationPolicy EmbeddedTool(std::string capability=std::string())
WindowPresentationPolicy WithHost(std::string host_name) const
Returns a copy that names the hosting surface.
static WindowPresentationPolicy OptionalPopOut()
Represents a group of palettes.
DungeonRenderPaletteSource source
static constexpr float kStatusBarHeight
Definition ui_config.h:21
Dungeon overlay configuration (per-project).
Definition project.h:97
RomWritePolicy write_policy
Definition project.h:229
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
core::HackManifest hack_manifest
Definition project.h:376
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
std::string symbols_filename
Definition project.h:318
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
PitDamageTable pit_damage_table
Definition game_data.h:114