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 <string>
15#include <utility>
16#include <vector>
17
18// Third-party library headers
19#include "absl/status/status.h"
20#include "absl/strings/str_format.h"
21#include "absl/types/span.h"
22#include "imgui/imgui.h"
23
24// Project headers
63#include "app/gui/core/icons.h"
65#include "core/features.h"
66#include "core/project.h"
67#include "rom/snes.h"
68#include "util/log.h"
69#include "util/macro.h"
76#include "zelda3/dungeon/room.h"
79
80namespace yaze::editor {
81
85 const absl::Status status =
87 if (!status.ok()) {
88 LOG_WARN("DungeonEditorV2", "Minecart project rebind failed: %s",
89 status.message());
90 }
92 }
93}
94
96 if (minecart_track_editor_panel_ == nullptr) {
97 return;
98 }
99
100 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
101 const size_t session_id = dependencies_.session_id;
103 [this, editor_manager, session_id](
104 const std::vector<zelda3::TrackCollisionResult>& preview,
105 const zelda3::GeneratorOptions& options) -> absl::Status {
106 if (editor_manager != nullptr &&
107 !editor_manager->IsCurrentProjectContextOwnedBySession(
108 session_id)) {
109 return absl::FailedPreconditionError(
110 "Minecart collision apply requires its project context to be "
111 "active");
112 }
113 return ApplyMinecartCollisionBatch(preview, options);
114 });
115 if (editor_manager == nullptr) {
119 return;
120 }
121
123 [editor_manager, session_id](
124 const project::DungeonOverlaySettings& overlay) -> absl::Status {
125 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
126 session_id)) {
127 return absl::FailedPreconditionError(
128 "Minecart overlay edit requires its project context to be "
129 "active");
130 }
131 editor_manager->GetCurrentProject()->dungeon_overlay = overlay;
132 editor_manager->MarkCurrentProjectDirty();
133 return absl::OkStatus();
134 });
136 [editor_manager, session_id]() -> absl::Status {
137 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
138 session_id)) {
139 return absl::FailedPreconditionError(
140 "Minecart overlay draft requires its project context to be "
141 "active");
142 }
143 editor_manager->MarkCurrentProjectDirty();
144 return absl::OkStatus();
145 });
147 [editor_manager, session_id]() -> absl::Status {
148 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
149 session_id)) {
150 return absl::FailedPreconditionError(
151 "Minecart project save requires its project context to be "
152 "active");
153 }
154 return editor_manager->SaveProject();
155 });
156}
157
159 if (minecart_track_editor_panel_ != nullptr) {
160 return absl::OkStatus();
161 }
162 if (!core::FeatureFlags::get().kEnableCustomObjects) {
163 return absl::FailedPreconditionError(
164 "Enable Custom Dungeon Objects before opening Minecart Tracks");
165 }
166 if (dependencies_.window_manager != nullptr &&
169 return absl::FailedPreconditionError(
170 "Minecart Tracks can only be registered for the active project "
171 "session");
172 }
173
174 auto minecart_panel = std::make_unique<MinecartTrackEditorPanel>();
175 RETURN_IF_ERROR(minecart_panel->SetProject(dependencies_.project));
176 minecart_panel->SetRooms(&rooms_);
177 minecart_panel->SetRoomNavigationCallback(
178 [this](int room_id) { OnRoomSelected(room_id); });
179
180 minecart_track_editor_panel_ = minecart_panel.get();
182 if (dependencies_.window_manager != nullptr) {
184 std::move(minecart_panel));
185 } else {
186 owned_minecart_track_editor_panel_ = std::move(minecart_panel);
187 }
188
189 room_viewers_.ForEach(
190 [this](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
191 if (viewer != nullptr) {
192 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
193 }
194 });
195 if (workbench_viewer_ != nullptr) {
196 workbench_viewer_->SetMinecartTrackPanel(minecart_track_editor_panel_);
197 }
198 if (workbench_compare_viewer_ != nullptr) {
199 workbench_compare_viewer_->SetMinecartTrackPanel(
201 }
202 if (workbench_panel_ != nullptr) {
206 }
207
208 return absl::OkStatus();
209}
210
212 const bool custom_objects_enabled =
214 const bool custom_objects_state_changed =
215 custom_objects_enabled != observed_custom_objects_enabled_;
216 const uint64_t generation =
218 if (generation == observed_custom_object_generation_ &&
219 !custom_objects_state_changed) {
220 return;
221 }
222
223 if (custom_objects_state_changed) {
225 }
226
228 [](int, zelda3::Room& room) { room.MarkObjectsDirty(); });
229 // Reload Assets also refreshes external sprite art in every session-owned
230 // viewer, including hidden room windows and the comparison canvas.
231 const auto invalidate_sprite_resources = [](DungeonCanvasViewer* viewer) {
232 if (viewer != nullptr) {
233 viewer->InvalidateExternalSpriteResources();
234 }
235 };
236 room_viewers_.ForEach(
237 [&](int, auto& viewer) { invalidate_sprite_resources(viewer.get()); });
238 invalidate_sprite_resources(workbench_viewer_.get());
239 invalidate_sprite_resources(workbench_compare_viewer_.get());
240 if (object_selector_panel_ != nullptr) {
243 }
245 observed_custom_objects_enabled_ = custom_objects_enabled;
246}
247
248namespace {
249
250bool IsTransientDungeonRoomWindowId(const std::string& card_id) {
251 constexpr const char* kPrefix = "dungeon.room_";
252 constexpr size_t kPrefixLength = 13;
253 if (card_id.rfind(kPrefix, 0) != 0 || card_id.size() <= kPrefixLength) {
254 return false;
255 }
256 return std::all_of(card_id.begin() + kPrefixLength, card_id.end(),
257 [](unsigned char ch) { return std::isdigit(ch) != 0; });
258}
259
260bool IsWorkbenchNavigationWindowId(const std::string& card_id) {
261 constexpr std::array<const char*, 3> kWorkbenchNavigationPanelIds = {
265 };
266
267 return IsTransientDungeonRoomWindowId(card_id) ||
268 std::any_of(kWorkbenchNavigationPanelIds.begin(),
269 kWorkbenchNavigationPanelIds.end(),
270 [&card_id](const char* navigation_id) {
271 return card_id == navigation_id;
272 });
273}
274
275} // namespace
276
278 : rom_(rom), room_loader_(rom), room_selector_(rom), rooms_(rom) {
280 if (rom) {
282 }
283}
284
293
295 // Clear runtime references in panels BEFORE editor-owned rooms and viewers are
296 // destroyed. WorkspaceWindowManager panels may outlive this editor session.
302 }
305 object_selector_panel_ = nullptr;
306 }
309 object_editor_content_ = nullptr;
310 }
311 if (door_editor_panel_) {
313 door_editor_panel_ = nullptr;
314 }
317 sprite_editor_panel_ = nullptr;
318 }
319 if (item_editor_panel_) {
321 item_editor_panel_ = nullptr;
322 }
326 custom_collision_panel_ = nullptr;
327 }
328 if (water_fill_panel_) {
331 water_fill_panel_ = nullptr;
332 }
336 }
337}
338
340 if (!rom_ || !rom_->is_loaded()) {
341 return absl::FailedPreconditionError("ROM not loaded");
342 }
343
346
347 std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>
348 refreshed_entrances;
349 std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>
350 refreshed_spawn_points;
351 RETURN_IF_ERROR(room_loader_.LoadRoomEntrances(refreshed_entrances));
352 RETURN_IF_ERROR(room_loader_.LoadDungeonSpawnPoints(refreshed_spawn_points));
353
354 struct RefreshedRoom {
355 int room_id;
356 zelda3::Room room;
357 };
358 std::vector<std::pair<int, bool>> materialized_rooms;
360 [&materialized_rooms](int room_id, const zelda3::Room& room) {
361 materialized_rooms.emplace_back(room_id, room.IsLoaded());
362 });
363
364 std::vector<RefreshedRoom> refreshed_rooms;
365 refreshed_rooms.reserve(materialized_rooms.size());
366 for (const auto& [room_id, was_loaded] : materialized_rooms) {
367 zelda3::Room refreshed_room;
368 if (was_loaded) {
369 RETURN_IF_ERROR(room_loader_.LoadRoom(room_id, refreshed_room));
370 } else {
371 refreshed_room = zelda3::LoadRoomHeaderFromRom(rom_, room_id);
372 refreshed_room.SetGameData(game_data_);
373 }
374 refreshed_rooms.push_back({room_id, std::move(refreshed_room)});
375 }
376
377 // All ROM reads succeeded. Clear every interaction that can retain indices
378 // into Room vectors before replacing any Room contents.
382 }
385 }
386 auto prepare_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
387 if (viewer) {
388 viewer->RefreshRomBackedState(rom_, game_data_, &rooms_, room_id);
389 }
390 };
391 room_viewers_.ForEach(
392 [&prepare_viewer](int room_id,
393 std::unique_ptr<DungeonCanvasViewer>& viewer) {
394 prepare_viewer(viewer.get(), room_id);
395 });
396 prepare_viewer(workbench_viewer_.get(), current_room_id_);
397 const int compare_room_id =
399 IsValidRoomId(workbench_compare_viewer_->current_room_id())
400 ? workbench_compare_viewer_->current_room_id()
402 prepare_viewer(workbench_compare_viewer_.get(), compare_room_id);
403
405 dungeon_editor_system_->ClearHistory();
406 if (auto object_editor = dungeon_editor_system_->GetObjectEditor()) {
407 (void)object_editor->ClearSelection();
408 object_editor->ClearHistory();
409 }
410 }
413 pending_undo_ = {};
414 has_pending_undo_ = false;
417 pending_swap_ = {};
421
425 for (auto& refreshed : refreshed_rooms) {
426 auto* room = rooms_.GetIfMaterialized(refreshed.room_id);
427 if (room) {
428 *room = std::move(refreshed.room);
429 }
430 }
431 entrances_ = std::move(refreshed_entrances);
432 spawn_points_ = std::move(refreshed_spawn_points);
434
438
441 auto* current_room = IsValidRoomId(current_room_id_)
443 : nullptr;
444 RETURN_IF_ERROR(dungeon_editor_system_->RefreshRomBackedState(
445 current_room, current_room_id_));
446 }
447
452 if (auto* current_room = rooms_.GetIfMaterialized(current_room_id_);
453 current_room && game_data_ &&
455 const int resolved_palette_id = current_room->ResolveDungeonPaletteId();
457 resolved_palette_id >= 0 &&
458 resolved_palette_id <
459 static_cast<int>(
461 ? static_cast<uint64_t>(resolved_palette_id)
462 : 0;
468 }
469
470 if (is_loaded_) {
474 }
475
476 auto bind_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
477 if (!viewer) {
478 return;
479 }
480 viewer->SetRenderer(renderer_);
486 ConfigureViewerRenderContext(viewer, room_id);
488 };
489 room_viewers_.ForEach(
490 [&bind_viewer](int room_id,
491 std::unique_ptr<DungeonCanvasViewer>& viewer) {
492 bind_viewer(viewer.get(), room_id);
493 });
494 bind_viewer(workbench_viewer_.get(), current_room_id_);
495 bind_viewer(workbench_compare_viewer_.get(), compare_room_id);
496
502 }
505 }
508 }
511 }
514 }
515 if (workbench_panel_) {
517 }
518
519 DungeonCanvasViewer* current_viewer = nullptr;
521 current_viewer = workbench_viewer_.get();
522 } else if (auto* viewer = room_viewers_.Get(current_room_id_)) {
523 current_viewer = viewer->get();
524 }
525 if (current_viewer) {
527 }
528
529 return absl::OkStatus();
530}
531
534 room.ClearWaterFillZone();
535 room.ClearWaterFillDirty();
536 });
537
538 bool legacy_imported = false;
539 const bool water_fill_save_enabled =
541 std::vector<zelda3::WaterFillZoneEntry> zones;
542
543 auto zones_or = zelda3::LoadWaterFillTable(rom_);
544 if (zones_or.ok()) {
545 zones = std::move(zones_or.value());
546 } else {
547 LOG_WARN("DungeonEditorV2", "WaterFillTable parse failed: %s",
548 zones_or.status().message().data());
551 absl::StrFormat("WaterFill table parse failed: %s",
552 zones_or.status().message()),
554 }
555 }
556
557 if (zones.empty()) {
558 std::string sym_path;
563 }
564 auto legacy_or = zelda3::LoadLegacyWaterGateZones(rom_, sym_path);
565 if (legacy_or.ok()) {
566 zones = std::move(legacy_or.value());
567 legacy_imported = !zones.empty();
568 } else {
569 LOG_WARN("DungeonEditorV2", "Legacy water gate import failed: %s",
570 legacy_or.status().message().data());
571 }
572 }
573
574 for (const auto& z : zones) {
575 if (z.room_id < 0 || z.room_id >= static_cast<int>(rooms_.size())) {
576 continue;
577 }
578 auto& room = rooms_[z.room_id];
579 room.set_water_fill_sram_bit_mask(z.sram_bit_mask);
580 for (uint16_t off : z.fill_offsets) {
581 const int x = static_cast<int>(off % 64);
582 const int y = static_cast<int>(off / 64);
583 room.SetWaterFillTile(x, y, true);
584 }
585
586 // A passive legacy import is pending migration only when this project is
587 // allowed to write the WaterFill table. Otherwise retain the overlay as
588 // read-only reference data without creating unsavable room dirtiness.
589 if (!legacy_imported || !water_fill_save_enabled) {
590 room.ClearWaterFillDirty();
591 }
592 }
593
594 if (legacy_imported && dependencies_.toast_manager) {
596 water_fill_save_enabled
597 ? "Imported legacy water gate zones (save to write new table)"
598 : "Loaded legacy water gate zones read-only (WaterFill saving is "
599 "disabled)",
601 }
602}
603
607 }
609 const bool rom_changed = dependencies_.rom && dependencies_.rom != rom_;
610 if (rom_changed) {
612 // The system owns ROM-backed views; ensure it matches the current ROM.
614 }
617 if (game_data_) {
619 }
620 }
621
622 // Setup docking class for room windows
623 room_window_class_.DockingAllowUnclassed = true;
624 room_window_class_.DockingAlwaysTabBar = true;
625
627 return;
628 auto* window_manager = dependencies_.window_manager;
629
630 // Legacy panel IDs persisted in older layouts/settings.
631 window_manager->RegisterPanelAlias("dungeon.object_tools", kObjectSelectorId);
632 window_manager->RegisterPanelAlias("dungeon.entrances",
633 "dungeon.entrance_properties");
634
635 // Register panels with WorkspaceWindowManager (no boolean flags - visibility is
636 // managed entirely by WorkspaceWindowManager::ShowPanel/HidePanel/IsPanelVisible)
637 // No default chord: Ctrl+Shift+W is reserved for Close Session. Toggle via
638 // View / Window Browser / command palette instead.
639 window_manager->RegisterPanel(
640 {.card_id = "dungeon.workbench",
641 .display_name = "Dungeon Workbench",
642 .window_title = " Dungeon Workbench",
643 .icon = ICON_MD_WORKSPACES,
644 .category = "Dungeon",
645 .workflow_group = "Core",
646 .shortcut_hint = "",
647 .visibility_flag = nullptr,
648 .priority = 5,
649 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
650 .disabled_tooltip = "Load a ROM to edit dungeon rooms"});
651
652 window_manager->RegisterPanel(
653 {.card_id = kRoomSelectorId,
654 .display_name = "Room List",
655 .window_title = " Room List",
656 .icon = ICON_MD_LIST,
657 .category = "Dungeon",
658 .workflow_group = "Core",
659 .shortcut_hint = "Ctrl+Shift+R",
660 .visibility_flag = nullptr,
661 .priority = 20,
662 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
663 .disabled_tooltip = "Load a ROM to browse dungeon rooms"});
664
665 window_manager->RegisterPanel(
666 {.card_id = kEntranceListId,
667 .display_name = "Entrance List",
668 .window_title = " Entrance List",
669 .icon = ICON_MD_DOOR_FRONT,
670 .category = "Dungeon",
671 .workflow_group = "Core",
672 .shortcut_hint = "Ctrl+Shift+E",
673 .visibility_flag = nullptr,
674 .priority = 25,
675 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
676 .disabled_tooltip = "Load a ROM to browse dungeon entrances"});
677
678 window_manager->RegisterPanel(
679 {.card_id = "dungeon.entrance_properties",
680 .display_name = "Entrance Properties",
681 .window_title = " Entrance Properties",
682 .icon = ICON_MD_TUNE,
683 .category = "Dungeon",
684 .workflow_group = "Core",
685 .shortcut_hint = "",
686 .visibility_flag = nullptr,
687 .priority = 26,
688 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
689 .disabled_tooltip = "Load a ROM to edit entrance properties"});
690
691 window_manager->RegisterPanel(
692 {.card_id = kRoomMatrixId,
693 .display_name = "Room Matrix",
694 .window_title = " Room Matrix",
695 .icon = ICON_MD_GRID_VIEW,
696 .category = "Dungeon",
697 .workflow_group = "Core",
698 .shortcut_hint = "Ctrl+Shift+M",
699 .visibility_flag = nullptr,
700 .priority = 30,
701 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
702 .disabled_tooltip = "Load a ROM to view the room matrix"});
703
704 window_manager->RegisterPanel(
705 {.card_id = kRoomGraphicsId,
706 .display_name = "Room Graphics",
707 .window_title = " Room Graphics",
708 .icon = ICON_MD_IMAGE,
709 .category = "Dungeon",
710 .workflow_group = "Editors",
711 .shortcut_hint = "Ctrl+Shift+G",
712 .visibility_flag = nullptr,
713 .priority = 50,
714 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
715 .disabled_tooltip = "Load a ROM to view room graphics"});
716
717 window_manager->RegisterPanel(
718 {.card_id = kObjectSelectorId,
719 .display_name = "Object Selector",
720 .window_title = " Object Selector",
721 .icon = ICON_MD_CATEGORY,
722 .category = "Dungeon",
723 .workflow_group = "Editors",
724 .shortcut_hint = "",
725 .visibility_flag = nullptr,
726 .priority = 60,
727 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
728 .disabled_tooltip = "Load a ROM to browse dungeon objects"});
729
730 window_manager->RegisterPanel(
731 {.card_id = kDoorEditorId,
732 .display_name = "Door Editor",
733 .window_title = " Door Editor",
734 .icon = ICON_MD_DOOR_FRONT,
735 .category = "Dungeon",
736 .workflow_group = "Editors",
737 .shortcut_hint = "",
738 .visibility_flag = nullptr,
739 .priority = 69,
740 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
741 .disabled_tooltip = "Load a ROM to edit dungeon doors"});
742
743 window_manager->RegisterPanel(
744 {.card_id = kPaletteEditorId,
745 .display_name = "Palette Editor",
746 .window_title = " Palette Editor",
747 .icon = ICON_MD_PALETTE,
748 .category = "Dungeon",
749 .workflow_group = "Editors",
750 // Avoid conflicting with the global Command Palette (Ctrl/Cmd+Shift+P).
751 .shortcut_hint = "Ctrl+Shift+Alt+P",
752 .visibility_flag = nullptr,
753 .priority = 70,
754 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
755 .disabled_tooltip = "Load a ROM to edit dungeon palettes"});
756
757 // Show default panels on startup.
758 // Workbench mode intentionally suppresses parallel standalone defaults to
759 // avoid two competing workflows being open at once.
761 /*show_toast=*/false);
762
763 // Wire intent-aware callback for double-click / context menu
765 [this](int room_id, RoomSelectionIntent intent) {
766 OnRoomSelected(room_id, intent);
767 });
768
769 // Register WindowContent instances
770 window_manager->RegisterWindowContent(std::make_unique<RoomBrowserContent>(
771 &room_selector_, [this](int room_id) { OnRoomSelected(room_id); }));
772
773 window_manager->RegisterWindowContent(
774 std::make_unique<DungeonEntranceListPanel>(
776 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
777
778 {
779 auto matrix_panel = std::make_unique<RoomMatrixContent>(
781 [this](int room_id) { OnRoomSelected(room_id); },
782 [this](int old_room, int new_room) {
783 SwapRoomInPanel(old_room, new_room);
784 },
785 &rooms_);
786 matrix_panel->SetRoomIntentCallback(
787 [this](int room_id, RoomSelectionIntent intent) {
788 OnRoomSelected(room_id, intent);
789 });
790 window_manager->RegisterWindowContent(std::move(matrix_panel));
791 }
792
793 {
794 auto workbench = std::make_unique<DungeonWorkbenchContent>(
796 [this](int room_id) { OnRoomSelected(room_id); },
797 [this](int room_id, RoomSelectionIntent intent) {
798 OnRoomSelected(room_id, intent);
799 },
800 [this](int room_id) {
801 auto status = SaveRoom(room_id);
802 if (!status.ok()) {
803 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
804 status.message().data());
807 absl::StrFormat("Apply Room failed: %s", status.message()),
809 }
810 return;
811 }
813 dependencies_.toast_manager->Show("Applied room to ROM buffer",
815 }
816 },
817 [this]() { SaveAllRooms(); }, [this]() { return GetWorkbenchViewer(); },
818 [this](int room_id) { return GetWorkbenchCompareViewer(room_id); },
819 [this]() -> const std::deque<int>& { return recent_rooms_; },
820 [this](int room_id) {
821 recent_rooms_.erase(
822 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
823 recent_rooms_.end());
824 },
825 [this](bool enabled) { QueueWorkbenchWorkflowMode(enabled); }, rom_);
826 workbench_panel_ = workbench.get();
828 [this](DungeonCanvasViewer& viewer) {
830 });
832 [this]() { return undo_manager_.CanUndo(); },
833 [this]() { return undo_manager_.CanRedo(); },
834 [this]() { Undo().IgnoreError(); }, [this]() { Redo().IgnoreError(); },
835 [this]() { return undo_manager_.GetUndoDescription(); },
836 [this]() { return undo_manager_.GetRedoDescription(); },
837 [this]() { return static_cast<int>(undo_manager_.UndoStackSize()); });
839 [window_manager](const std::string& window_id) {
840 const size_t session_id = window_manager->GetActiveSessionId();
841 return window_manager->IsWindowOpen(session_id, window_id);
842 },
843 [this, window_manager](const std::string& window_id) {
844 if (window_id.empty()) {
845 return false;
846 }
848 window_manager->GetActiveSessionId();
851 return true;
852 });
854 [window_manager]() { window_manager->TriggerShowShortcuts(); });
855 workbench_panel_->SetOnInspectorSideChanged([this](bool on_left) {
858 : "right");
859 dependencies_.user_settings->Save().IgnoreError();
860 }
861 });
863 return game_data_ ? &game_data_->pit_damage_table : nullptr;
864 });
865 window_manager->RegisterWindowContent(std::move(workbench));
866 }
867
868 window_manager->RegisterWindowContent(std::make_unique<DungeonEntrancesPanel>(
870 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
871
872 // Note: RoomGraphicsContent and PaletteEditorContent are registered
873 // in Load() after their dependencies (renderer_, palette_editor_) are initialized
874}
875
876absl::Status DungeonEditorV2::Load() {
877 if (!rom_ || !rom_->is_loaded()) {
878 return absl::FailedPreconditionError("ROM not loaded");
879 }
880
881 // Initialize ObjectDimensionTable so DimensionService's fallback path works.
882 // DimensionService::Get() queries this table internally but has no Load API.
883 auto& dim_table = zelda3::ObjectDimensionTable::Get();
884 if (!dim_table.IsLoaded()) {
885 RETURN_IF_ERROR(dim_table.LoadFromRom(rom_));
886 }
887
890
891 if (!game_data()) {
892 return absl::FailedPreconditionError("GameData not available");
893 }
894 auto dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
895 current_palette_ = dungeon_main_pal_group[current_palette_group_id_];
898
904 [this](int room_id) { OnRoomSelected(room_id); });
906 [this](int room_id, RoomSelectionIntent intent) {
907 OnRoomSelected(room_id, intent);
908 });
910 [this](int entrance_id) { OnEntranceSelected(entrance_id); });
911
912 // Canvas viewers are lazily created in GetViewerForRoom
913
914 if (game_data()) {
916 } else {
918 }
919
922
923 // Register panels that depend on initialized state (renderer, palette_editor_)
925 auto graphics_panel = std::make_unique<RoomGraphicsContent>(
927 room_graphics_panel_ = graphics_panel.get();
929 std::move(graphics_panel));
930 auto palette_panel =
931 std::make_unique<PaletteEditorContent>(&palette_editor_);
932 palette_editor_panel_ = palette_panel.get();
934 std::move(palette_panel));
935 }
936
937 dungeon_editor_system_ = std::make_unique<zelda3::DungeonEditorSystem>(rom_);
938 (void)dungeon_editor_system_->Initialize();
940
941 // Initialize browse-only object selector and the dedicated object editor.
942 auto object_selector = std::make_unique<ObjectSelectorContent>(
943 rom_, nullptr, dungeon_editor_system_->GetObjectEditor(),
945 auto object_editor = std::make_unique<ObjectEditorContent>(
946 dungeon_editor_system_->GetObjectEditor());
947
948 // Wire up object change callback to trigger room re-rendering
949 dungeon_editor_system_->GetObjectEditor()->SetObjectChangedCallback(
950 [this](size_t /*object_index*/, const zelda3::RoomObject& /*object*/) {
951 if (current_room_id_ >= 0 && current_room_id_ < (int)rooms_.size()) {
952 rooms_[current_room_id_].RenderRoomGraphics();
953 }
954 });
955
956 // Set rooms and initial palette group for correct preview rendering
957 object_selector->SetRooms(&rooms_);
958 object_selector->SetCurrentPaletteGroup(current_palette_group_);
959
960 auto open_workbench_selection_inspector = [this]() {
961 if (workbench_panel_) {
963 }
964 OpenWindow("dungeon.workbench");
965 };
966
967 object_selector->SetOpenObjectEditorCallback(
968 open_workbench_selection_inspector);
969
970 // Keep raw pointers for later access
971 object_selector_panel_ = object_selector.get();
972 object_editor_content_ = object_editor.get();
973
974 auto jump_to_reciprocal_door = [this](int neighbor_room_id,
975 size_t door_index) {
976 if (neighbor_room_id < 0 ||
977 neighbor_room_id >= static_cast<int>(rooms_.size())) {
978 return;
979 }
980 OnRoomSelected(neighbor_room_id, true);
981 if (auto* viewer = GetViewerForRoom(neighbor_room_id)) {
982 viewer->object_interaction()
983 .entity_coordinator()
984 .door_handler()
985 .SelectDoor(door_index);
986 }
987 };
988 object_editor->SetJumpToReciprocalDoorCallback(jump_to_reciprocal_door);
989 owned_object_editor_content_ = std::move(object_editor);
990
991 auto door_editor = std::make_unique<DoorEditorContent>();
992 door_editor->SetRooms(&rooms_);
993 door_editor->SetOpenSelectionInspectorCallback(
994 open_workbench_selection_inspector);
995 door_editor->SetJumpToReciprocalDoorCallback(jump_to_reciprocal_door);
996 door_editor_panel_ = door_editor.get();
997
998 // Propagate game_data to the object editor panel if available
999 if (game_data()) {
1001 }
1002
1003 // Room-scoped utility tools use the Workbench inspector by default, while the
1004 // same WindowContent instances remain available to users who prefer floating
1005 // windows. OpenTool keeps one presentation owner at a time so one content
1006 // instance is never drawn in both places during the same frame.
1007 auto custom_collision_panel =
1008 std::make_unique<CustomCollisionPanel>(nullptr, nullptr);
1009 custom_collision_panel_ = custom_collision_panel.get();
1010
1011 auto water_fill_panel = std::make_unique<WaterFillPanel>(nullptr, nullptr);
1012 water_fill_panel_ = water_fill_panel.get();
1013
1014 auto room_tag_panel = std::make_unique<RoomTagEditorPanel>();
1015 room_tag_panel->SetProject(dependencies_.project);
1016 room_tag_panel->SetRooms(&rooms_);
1017 room_tag_panel->SetCurrentRoomId(current_room_id_);
1018 room_tag_editor_panel_ = room_tag_panel.get();
1019
1020 // Register the ObjectSelectorContent directly (it inherits from WindowContent)
1021 // Panel manager takes ownership
1024 std::move(object_selector));
1025 dependencies_.window_manager->RegisterWindowContent(std::move(door_editor));
1026
1027 // Register sprite and item editor panels with canvas viewer = nullptr
1028 // They will get the viewer reference in OnRoomSelected when a room is selected
1029 auto sprite_panel = std::make_unique<SpriteEditorPanel>(&current_room_id_,
1030 &rooms_, nullptr);
1031 sprite_panel->SetOpenSelectionInspectorCallback(
1032 open_workbench_selection_inspector);
1033 sprite_editor_panel_ = sprite_panel.get();
1035 std::move(sprite_panel));
1036
1037 auto item_panel =
1038 std::make_unique<ItemEditorPanel>(&current_room_id_, &rooms_, nullptr);
1039 item_panel->SetOpenSelectionInspectorCallback(
1040 open_workbench_selection_inspector);
1041 item_editor_panel_ = item_panel.get();
1042 dependencies_.window_manager->RegisterWindowContent(std::move(item_panel));
1043
1045 std::move(custom_collision_panel));
1047 std::move(water_fill_panel));
1049 std::move(room_tag_panel));
1050 // Object Tile Editor Panel
1051 {
1052 auto tile_editor_panel =
1053 std::make_unique<ObjectTileEditorPanel>(renderer_, rom_);
1054 tile_editor_panel->SetCurrentPaletteGroup(current_palette_group_);
1055 tile_editor_panel->SetStandardWritePreflightCallback(
1056 [this](const std::vector<std::pair<uint32_t, uint32_t>>& ranges) {
1057 if (ranges.empty() || dependencies_.project == nullptr ||
1059 return absl::OkStatus();
1060 }
1064 "dungeon object tile data", "ObjectTileEditorPanel",
1066 });
1067 tile_editor_panel->SetTilesAppliedCallback([this]() {
1070 }
1071 });
1072
1073 object_tile_editor_panel_ = tile_editor_panel.get();
1075 std::move(tile_editor_panel));
1076 }
1077
1078 // Wire fixed-slot custom-object management actions to their workspace
1079 // windows.
1084 [this]() {
1085 return dependencies_.window_manager != nullptr &&
1086 object_tile_editor_panel_ != nullptr &&
1089 });
1092 return dependencies_.window_manager != nullptr &&
1093 minecart_track_editor_panel_ != nullptr &&
1096 });
1097 }
1098
1099 // Overlay Manager Panel
1100 {
1101 auto overlay_panel = std::make_unique<OverlayManagerPanel>();
1102 overlay_manager_panel_ = overlay_panel.get();
1104 std::move(overlay_panel));
1105 }
1106
1107 } else {
1108 owned_object_selector_panel_ = std::move(object_selector);
1109 owned_door_editor_panel_ = std::move(door_editor);
1110 owned_custom_collision_panel_ = std::move(custom_collision_panel);
1111 owned_water_fill_panel_ = std::move(water_fill_panel);
1112 owned_room_tag_editor_panel_ = std::move(room_tag_panel);
1113 }
1114
1115 if (core::FeatureFlags::get().kEnableCustomObjects) {
1117 }
1118
1120 [this](gui::DungeonPaletteChange change) {
1122 });
1124
1125 // Oracle of Secrets: load editor-authored water fill zones (best-effort).
1127
1128 if (workbench_panel_) {
1135 }
1136
1137 is_loaded_ = true;
1138 return absl::OkStatus();
1139}
1140
1142 if (palette_listener_id_ >= 0) {
1143 return;
1144 }
1145
1147 [this](const std::string& group_name, int palette_index) {
1148 HandleArenaPaletteChanged(group_name, palette_index);
1149 });
1150}
1151
1152void DungeonEditorV2::HandleArenaPaletteChanged(const std::string& group_name,
1153 int palette_index) {
1154 if (!gfx::PaletteManager::Get().IsManaging(game_data_)) {
1155 return;
1156 }
1157
1158 if (group_name == "hud") {
1161 } else if (group_name == "dungeon_main") {
1164 }
1165}
1166
1170
1171 std::array<bool, zelda3::kNumberOfRooms> rendered_rooms{};
1172 auto render_once = [this, &rendered_rooms](int room_id) {
1173 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()) ||
1174 room_id >= static_cast<int>(rendered_rooms.size()) ||
1175 rendered_rooms[room_id]) {
1176 return;
1177 }
1178 rooms_[room_id].RenderRoomGraphics();
1179 rendered_rooms[room_id] = true;
1180 };
1181
1182 for (int i = 0; i < active_rooms_.Size; i++) {
1183 render_once(active_rooms_[i]);
1184 }
1185
1186 render_once(current_room_id_);
1187
1188 auto apply_palette = [](DungeonCanvasViewer* viewer, uint64_t palette_id,
1189 const gfx::PaletteGroup& palette_group,
1190 bool force_shared_palette_refresh) {
1191 if (!viewer) {
1192 return;
1193 }
1194 viewer->SetCurrentPaletteId(palette_id);
1195 viewer->SetCurrentPaletteGroup(
1196 palette_group,
1197 /*force_refresh=*/force_shared_palette_refresh &&
1199 };
1200
1201 if (current_room_id_ >= 0 &&
1202 current_room_id_ < static_cast<int>(rooms_.size()) && game_data()) {
1203 const bool force_shared_palette_refresh =
1205 auto& dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
1207 static_cast<uint64_t>(dungeon_main_pal_group.size())) {
1209 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1212 game_data());
1213 apply_palette(workbench_viewer_.get(), current_palette_id_,
1214 current_palette_group_, force_shared_palette_refresh);
1216 current_palette_group_, force_shared_palette_refresh);
1217 room_viewers_.ForEach([this, change, &apply_palette,
1218 &dungeon_main_pal_group, &render_once,
1219 force_shared_palette_refresh](
1220 int room_id,
1221 std::unique_ptr<DungeonCanvasViewer>& viewer) {
1222 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1223 return;
1224 }
1225 const int palette_id = rooms_[room_id].ResolveDungeonPaletteId();
1226 if (palette_id < 0 ||
1227 palette_id >= static_cast<int>(dungeon_main_pal_group.size())) {
1228 return;
1229 }
1231 change.palette_id >= 0 && palette_id != change.palette_id) {
1232 return;
1233 }
1234 const auto room_palette_group =
1236 dungeon_main_pal_group.palette_ref(palette_id), game_data());
1237 if (viewer && viewer->object_interaction().IsObjectLoaded()) {
1238 render_once(room_id);
1239 }
1240 apply_palette(viewer.get(), palette_id, room_palette_group,
1241 force_shared_palette_refresh);
1242 });
1245 }
1248 }
1249 }
1250 }
1251
1252 // Tile edits remain bound to their original room even when the active room
1253 // changes or closes. Refresh that room's palette without reopening its session.
1255 !game_data()) {
1256 return;
1257 }
1258 const int tile_room_id = object_tile_editor_panel_->current_room_id();
1259 const auto* tile_room = rooms_.GetIfLoaded(tile_room_id);
1260 if (!tile_room) {
1261 return;
1262 }
1263 const int tile_palette_id = tile_room->ResolveDungeonPaletteId();
1264 const auto& dungeon_palettes = game_data()->palette_groups.dungeon_main;
1265 if (tile_palette_id < 0 ||
1266 tile_palette_id >= static_cast<int>(dungeon_palettes.size()) ||
1268 change.palette_id >= 0 && tile_palette_id != change.palette_id)) {
1269 return;
1270 }
1272 tile_room_id,
1274 dungeon_palettes.palette_ref(tile_palette_id), game_data()));
1275}
1276
1280 change.palette_id < 0) {
1282 [](int, zelda3::Room& room) { room.MarkGraphicsDirty(); });
1283 return;
1284 }
1285
1286 // Editing palette bytes does not change a room header, so the room's normal
1287 // property cache cannot detect this dependency change. Mark every cached
1288 // room that resolves to the edited concrete palette; inactive rooms will
1289 // redraw lazily the next time they are shown.
1290 rooms_.ForEachMaterialized([change](int, zelda3::Room& room) {
1291 if (room.ResolveDungeonPaletteId() == change.palette_id) {
1292 room.MarkGraphicsDirty();
1293 }
1294 });
1295}
1296
1302
1303 // Mirror the workbench inspector to the LEFT when user_settings says so.
1304 // Persisted across sessions via UserSettings::dungeon_inspector_side.
1308 }
1309
1310 const auto& theme = AgentUI::GetTheme();
1311 if (room_window_class_.ClassId == 0) {
1312 room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
1313 }
1314
1315 if (!is_loaded_) {
1316 gui::PanelWindow loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);
1317 loading_card.SetDefaultSize(400, 200);
1318 if (loading_card.Begin()) {
1319 ImGui::TextColored(theme.text_secondary_gray,
1320 tr("Loading dungeon data..."));
1321 ImGui::TextWrapped(
1322 tr("Independent editor cards will appear once ROM data is loaded."));
1323 }
1324 loading_card.End();
1325 return absl::OkStatus();
1326 }
1327
1328 if (!IsWorkbenchWorkflowEnabled() || active_rooms_.Size > 0) {
1330 }
1331
1332 // Keyboard Shortcuts (only if not typing in a text field)
1333 if (!ImGui::GetIO().WantTextInput) {
1334 // Room Cycling (Ctrl+Tab)
1335 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && ImGui::GetIO().KeyCtrl) {
1337 if (recent_rooms_.size() > 1) {
1338 int current_idx = -1;
1339 for (int i = 0; i < static_cast<int>(recent_rooms_.size()); ++i) {
1340 if (recent_rooms_[i] == current_room_id_) {
1341 current_idx = i;
1342 break;
1343 }
1344 }
1345 if (current_idx != -1) {
1346 int next_idx;
1347 if (ImGui::GetIO().KeyShift) {
1348 next_idx =
1349 (current_idx + 1) % static_cast<int>(recent_rooms_.size());
1350 } else {
1351 next_idx =
1352 (current_idx - 1 + static_cast<int>(recent_rooms_.size())) %
1353 static_cast<int>(recent_rooms_.size());
1354 }
1355 OnRoomSelected(recent_rooms_[next_idx]);
1356 }
1357 }
1358 } else if (active_rooms_.size() > 1) {
1359 int current_idx = -1;
1360 for (int i = 0; i < active_rooms_.size(); ++i) {
1361 if (active_rooms_[i] == current_room_id_) {
1362 current_idx = i;
1363 break;
1364 }
1365 }
1366
1367 if (current_idx != -1) {
1368 int next_idx;
1369 if (ImGui::GetIO().KeyShift) {
1370 next_idx =
1371 (current_idx - 1 + active_rooms_.size()) % active_rooms_.size();
1372 } else {
1373 next_idx = (current_idx + 1) % active_rooms_.size();
1374 }
1375 OnRoomSelected(active_rooms_[next_idx]);
1376 }
1377 }
1378 }
1379
1380 // Adjacent Room Navigation (Ctrl+Arrows)
1381 if (ImGui::GetIO().KeyCtrl) {
1382 int next_room = -1;
1383 const int kCols = 16;
1384
1385 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
1386 if (current_room_id_ >= kCols)
1387 next_room = current_room_id_ - kCols;
1388 } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
1389 if (IsValidRoomId(current_room_id_ + kCols))
1390 next_room = current_room_id_ + kCols;
1391 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
1392 if (current_room_id_ % kCols > 0)
1393 next_room = current_room_id_ - 1;
1394 } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
1395 if (current_room_id_ % kCols < kCols - 1 &&
1397 next_room = current_room_id_ + 1;
1398 }
1399
1400 if (next_room != -1) {
1402 OnRoomSelected(next_room, /*request_focus=*/false);
1403 } else {
1405 }
1406 }
1407 }
1408 }
1409
1410 // Process any pending room swaps after all drawing is complete
1411 // This prevents ImGui state corruption from modifying collections mid-frame
1413
1414 return absl::OkStatus();
1415}
1416
1418 if (!status_bar)
1419 return;
1420 const int room_id = current_room_id();
1421
1422 if (room_id >= 0) {
1423 StatusBarSegmentOptions room_opts;
1424 room_opts.tooltip = "Click to refocus viewer on this room";
1425 room_opts.on_click = [this, room_id]() {
1429 }
1430 };
1431 status_bar->SetCustomSegment("Room", absl::StrFormat("0x%03X", room_id),
1432 std::move(room_opts));
1433 }
1434
1435 const int loaded = LoadedRoomCount();
1436 const int total = TotalRoomCount();
1437 if (total > 0) {
1438 StatusBarSegmentOptions rooms_opts;
1439 rooms_opts.tooltip =
1440 absl::StrFormat("%d rooms loaded of %d total", loaded, total);
1441 status_bar->SetCustomSegment("Rooms",
1442 absl::StrFormat("%d/%d", loaded, total),
1443 std::move(rooms_opts));
1444 }
1445
1446 if (room_id >= 0 && CurrentRoomHasPendingChanges()) {
1447 StatusBarSegmentOptions current_room_opts;
1448 current_room_opts.tooltip =
1449 "This room has pending editor changes that are not yet applied to the "
1450 "ROM buffer.";
1451 status_bar->SetCustomSegment("RoomState", "Pending",
1452 std::move(current_room_opts));
1453 }
1454
1455 StatusBarSegmentOptions mode_opts;
1456 mode_opts.tooltip = "Click to toggle Workbench / Standalone workflow";
1457 mode_opts.on_click = [this]() {
1459 };
1463 std::move(mode_opts));
1464}
1465
1467 return rooms_.LoadedCount();
1468}
1469
1471 int count = 0;
1472 rooms_.ForEachMaterialized([&count](int, const zelda3::Room& room) {
1473 if (room.HasUnsavedChanges()) {
1474 ++count;
1475 }
1476 });
1477 return count;
1478}
1479
1481 return PendingRoomCount() > 0;
1482}
1483
1485 if (minecart_track_editor_panel_ != nullptr &&
1487 return true;
1488 }
1489 if (object_tile_editor_panel_ != nullptr &&
1491 return true;
1492 }
1493 if (HasPendingRoomChanges()) {
1494 return true;
1495 }
1496 if (std::any_of(entrances_.begin(), entrances_.end(),
1497 [](const zelda3::RoomEntrance& entrance) {
1498 return entrance.dirty();
1499 })) {
1500 return true;
1501 }
1502 if (std::any_of(spawn_points_.begin(), spawn_points_.end(),
1503 [](const zelda3::DungeonSpawnPoint& spawn) {
1504 return spawn.dirty();
1505 })) {
1506 return true;
1507 }
1508 return game_data_ != nullptr && game_data_->pit_damage_table.dirty();
1509}
1510
1515
1517 return minecart_track_editor_panel_ != nullptr
1519 : absl::OkStatus();
1520}
1521
1523 if (current_room_id_ < 0 ||
1524 current_room_id_ >= static_cast<int>(rooms_.size())) {
1525 return false;
1526 }
1527 if (const auto* room = rooms_.GetIfMaterialized(current_room_id_)) {
1528 return room->HasUnsavedChanges();
1529 }
1530 return false;
1531}
1532
1534 if (!core::FeatureFlags::get().dungeon.kUseWorkbench) {
1535 return false;
1536 }
1538 return true;
1539 }
1540 return dependencies_.window_manager->IsWindowOpen("dungeon.workbench");
1541}
1542
1543void DungeonEditorV2::SetWorkbenchWorkflowMode(bool enabled, bool show_toast) {
1544 auto* window_manager = dependencies_.window_manager;
1545 if (!window_manager) {
1546 return;
1547 }
1548
1549 const size_t session_id = window_manager->GetActiveSessionId();
1550 const bool was_enabled = IsWorkbenchWorkflowEnabled();
1551
1552 if (enabled) {
1553 window_manager->OpenWindow(session_id, "dungeon.workbench");
1554
1555 if (!was_enabled) {
1557
1558 // The Workbench replaces room navigation, but not standalone editing
1559 // tools. Keep those tool windows open so user-owned HM/ZS-style layouts
1560 // survive workflow switches; the embedded inspector yields ownership to
1561 // any standalone tool that is already visible.
1562 for (const auto& descriptor :
1563 window_manager->GetWindowsInCategory(session_id, "Dungeon")) {
1564 const std::string& card_id = descriptor.card_id;
1565 if (!IsWorkbenchNavigationWindowId(card_id) ||
1566 !window_manager->IsWindowOpen(session_id, card_id) ||
1567 window_manager->IsWindowPinned(session_id, card_id)) {
1568 continue;
1569 }
1571 window_manager->CloseWindow(session_id, card_id);
1572 }
1573 }
1574 } else {
1575 window_manager->CloseWindow(session_id, "dungeon.workbench");
1576 window_manager->OpenWindow(session_id, kRoomSelectorId);
1577 window_manager->OpenWindow(session_id, kRoomMatrixId);
1578 if (current_room_id_ >= 0) {
1580 }
1581 for (const std::string& card_id :
1583 window_manager->OpenWindow(session_id, card_id);
1584 }
1586 }
1587
1588 if (show_toast && dependencies_.toast_manager && was_enabled != enabled) {
1590 enabled ? "Dungeon workflow: Workbench"
1591 : "Dungeon workflow: Standalone Panels",
1593 }
1594}
1595
1597 bool show_toast) {
1601}
1602
1606
1608 for (int i = 0; i < active_rooms_.Size; i++) {
1609 int room_id = active_rooms_[i];
1610 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1611 bool panel_visible = true;
1613 panel_visible = dependencies_.window_manager->IsWindowOpen(card_id);
1614 }
1615
1616 if (!panel_visible) {
1618 room_cards_.erase(room_id);
1619 active_rooms_.erase(active_rooms_.Data + i);
1620 ReleaseRoomPanelSlotId(room_id);
1621 // Clean up viewer
1622 room_viewers_.Erase(room_id);
1623 i--;
1624 continue;
1625 }
1626
1627 bool is_pinned = dependencies_.window_manager &&
1629 std::string active_category =
1632 : "";
1633
1634 if (active_category != "Dungeon" && !is_pinned) {
1635 continue;
1636 }
1637
1638 // Ensure room card exists (should have been created by ShowRoomPanel/ShowPanel)
1639 if (room_cards_.find(room_id) == room_cards_.end()) {
1640 ShowRoomPanel(room_id);
1641 }
1642
1643 auto& room_card = room_cards_[room_id];
1644 bool open = true;
1645
1646 ImGui::SetNextWindowClass(&room_window_class_);
1647 if (room_dock_id_ == 0) {
1648 room_dock_id_ = ImGui::GetID("DungeonRoomDock");
1649 }
1650 ImGui::SetNextWindowDockID(room_dock_id_, ImGuiCond_FirstUseEver);
1651
1652 if (room_card->Begin(&open)) {
1653 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) {
1654 OnRoomSelected(room_id, /*request_focus=*/false);
1655 }
1656 DrawRoomTab(room_id);
1657 }
1658 room_card->End();
1659
1660 if (!open) {
1663 }
1664
1665 room_cards_.erase(room_id);
1666 active_rooms_.erase(active_rooms_.Data + i);
1667 room_viewers_.Erase(room_id);
1668 ReleaseRoomPanelSlotId(room_id);
1669 i--;
1670 }
1671 }
1672}
1673
1675 if (auto it = room_panel_slot_ids_.find(room_id);
1676 it != room_panel_slot_ids_.end()) {
1677 return it->second;
1678 }
1679 const int slot_id = next_room_panel_slot_id_++;
1680 room_panel_slot_ids_[room_id] = slot_id;
1681 return slot_id;
1682}
1683
1685 room_panel_slot_ids_.erase(room_id);
1686}
1687
1689 const auto& theme = AgentUI::GetTheme();
1690 if (room_id < 0 || room_id >= 0x128) {
1691 ImGui::Text(tr("Invalid room ID: %d"), room_id);
1692 return;
1693 }
1694
1695 auto& room = rooms_[room_id];
1696
1697 if (!room.IsLoaded()) {
1698 auto status = room_loader_.LoadRoom(room_id, room);
1699 if (!status.ok()) {
1700 ImGui::TextColored(theme.text_error_red, tr("Failed to load room: %s"),
1701 status.message().data());
1702 return;
1703 }
1704
1706 auto sys_status = dungeon_editor_system_->ReloadRoom(room_id);
1707 if (!sys_status.ok()) {
1708 LOG_ERROR("DungeonEditorV2", "Failed to load system data: %s",
1709 sys_status.message().data());
1710 }
1711 }
1712 }
1713
1714 if (room.IsLoaded()) {
1715 bool needs_render = false;
1716
1717 if (room.blocks().empty()) {
1719 room.LoadRoomGraphics();
1720 needs_render = true;
1721 }
1722
1723 if (!room.AreObjectsLoaded()) {
1724 room.LoadObjects();
1725 needs_render = true;
1726 }
1727
1728 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1729 if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
1730 room.RenderRoomGraphics();
1731 }
1732 }
1733
1734 if (room.IsLoaded()) {
1735 ImGui::TextColored(theme.text_success_green, ICON_MD_CHECK " Loaded");
1736 } else {
1737 ImGui::TextColored(theme.text_error_red, ICON_MD_PENDING " Not Loaded");
1738 }
1739 ImGui::SameLine();
1740 ImGui::TextDisabled(tr("Objects: %zu"), room.GetTileObjects().size());
1741
1742 if (core::FeatureFlags::get().dungeon.kUseWorkbench &&
1744 ImGui::SameLine();
1745 const std::string return_label = absl::StrFormat(
1746 ICON_MD_WORKSPACES " Workbench##return_workbench_%03X", room_id);
1747 if (ImGui::SmallButton(return_label.c_str())) {
1749 }
1750 if (ImGui::IsItemHovered()) {
1751 ImGui::SetTooltip(
1752 tr("Switch back to the integrated Dungeon Workbench workflow."));
1753 }
1754 }
1755
1756 // Warp to Room button — sends room ID to running Mesen2 emulator
1757 ImGui::SameLine();
1758 {
1760 bool connected = client && client->IsConnected();
1761 if (!connected) {
1762 ImGui::BeginDisabled();
1763 }
1764 std::string warp_label =
1765 absl::StrFormat(ICON_MD_ROCKET_LAUNCH " Warp##warp_%03X", room_id);
1766 if (ImGui::SmallButton(warp_label.c_str())) {
1767 auto status = client->WriteWord(0x7E00A0, static_cast<uint16_t>(room_id));
1768 if (status.ok()) {
1771 absl::StrFormat("Warped to room 0x%03X", room_id),
1773 }
1774 } else {
1777 absl::StrFormat("Warp failed: %s", status.message()),
1779 }
1780 }
1781 }
1782 if (!connected) {
1783 ImGui::EndDisabled();
1784 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1785 ImGui::SetTooltip(tr("Connect to Mesen2 first"));
1786 }
1787 } else {
1788 if (ImGui::IsItemHovered()) {
1789 ImGui::SetTooltip(tr("Warp to room 0x%03X in Mesen2"), room_id);
1790 }
1791 }
1792 }
1793
1794 ImGui::Separator();
1795
1796 // Use per-room viewer; reserve a bottom strip for the shared dungeon
1797 // status bar so this Windows-mode standalone window mirrors the Workbench
1798 // canvas pane's affordances.
1799 if (auto* viewer = GetViewerForRoom(room_id)) {
1800 const float status_bar_reserved =
1801 std::max(ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
1803 ImGui::GetStyle().ItemSpacing.y;
1804 if (ImGui::BeginChild("##StandaloneRoomBody",
1805 ImVec2(0.0f, -status_bar_reserved), false)) {
1806 viewer->DrawDungeonCanvas(room_id);
1808 }
1809 ImGui::EndChild();
1810
1811 bool room_dirty = false;
1812 if (room_id >= 0 && room_id < static_cast<int>(rooms_.size())) {
1813 room_dirty = rooms_[room_id].HasUnsavedChanges();
1814 }
1815 const char* tool_mode =
1816 viewer->object_interaction().mode_manager().GetModeName();
1817 auto status = DungeonStatusBar::BuildState(*viewer, tool_mode, room_dirty);
1818 status.workflow_mode = workflow_mode_names::kStandalone;
1819 status.workflow_primary = false;
1820 status.can_undo = undo_manager_.CanUndo();
1821 status.can_redo = undo_manager_.CanRedo();
1822 status.undo_depth = static_cast<int>(undo_manager_.UndoStackSize());
1823 status.on_undo = [this]() {
1824 Undo().IgnoreError();
1825 };
1826 status.on_redo = [this]() {
1827 Redo().IgnoreError();
1828 };
1829 DungeonStatusBar::Draw(status);
1830 }
1831}
1832
1834 if (ImGui::GetCurrentContext() == nullptr) {
1836 return;
1837 }
1838 room_canvas_delete_shortcut_frame_ = ImGui::GetFrameCount();
1839}
1840
1842 if (!room_canvas_delete_shortcut_frame_.has_value()) {
1843 return;
1844 }
1845 if (ImGui::GetCurrentContext() == nullptr ||
1846 *room_canvas_delete_shortcut_frame_ != ImGui::GetFrameCount()) {
1848 }
1849}
1850
1852 DungeonCanvasViewer& viewer) {
1854 if (!room_canvas_delete_shortcut_frame_.has_value() ||
1855 ImGui::GetIO().WantTextInput || ImGui::IsAnyItemActive() ||
1856 !viewer.CanHandleRoomCanvasShortcut()) {
1857 return false;
1858 }
1859
1861 viewer.DeleteSelectedObjects();
1862 return true;
1863}
1864
1866 switch (intent) {
1868 OnRoomSelected(room_id, /*request_focus=*/true);
1869 break;
1871 // Explicitly pivot to panel workflow when user asks for standalone room.
1872 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1873 return;
1874 }
1877 }
1878 // Update shared state (same as OnRoomSelected with request_focus=true)
1879 OnRoomSelected(room_id, /*request_focus=*/false);
1880 // Now force-open a standalone panel for this room
1881 ShowRoomPanel(room_id);
1882 break;
1883 }
1885 OnRoomSelected(room_id, /*request_focus=*/false);
1886 break;
1887 }
1888}
1889
1890void DungeonEditorV2::OnRoomSelected(int room_id, bool request_focus) {
1891 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1892 LOG_WARN("DungeonEditorV2", "Ignoring invalid room selection: %d", room_id);
1893 return;
1894 }
1895 if (room_id != current_room_id_ && workbench_panel_) {
1897 }
1898 current_room_id_ = room_id;
1899 room_selector_.set_current_room_id(static_cast<uint16_t>(room_id));
1900
1901 // Track recent rooms (remove if already present, add to front)
1902 recent_rooms_.erase(
1903 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
1904 recent_rooms_.end());
1905 recent_rooms_.push_front(room_id);
1906 if (recent_rooms_.size() > kMaxRecentRooms) {
1907 recent_rooms_.pop_back();
1908 }
1909
1911 dungeon_editor_system_->SetExternalRoom(&rooms_[room_id]);
1912 }
1913
1914 // Update all sub-panels (Object Editor, Sprite Editor, etc.)
1915 SyncPanelsToRoom(room_id);
1916
1917 // Sync palette with current room (must happen before early return for focus changes)
1918
1919 // Sync palette with current room (must happen before early return for focus changes)
1920 if (room_id >= 0 && room_id < (int)rooms_.size()) {
1921 auto& room = rooms_[room_id];
1922 if (!room.IsLoaded()) {
1923 room_loader_.LoadRoom(room_id, room);
1924 }
1925
1926 if (room.IsLoaded()) {
1928 current_palette_id_ = room.ResolveDungeonPaletteId();
1931
1932 // Update viewer and object editor palette
1933 if (auto* viewer = GetViewerForRoom(room_id)) {
1934 ConfigureViewerRenderContext(viewer, room_id);
1935 viewer->SetCurrentPaletteId(current_palette_id_);
1936
1937 if (game_data()) {
1938 auto dungeon_main_pal_group =
1940 if (current_palette_id_ < (int)dungeon_main_pal_group.size()) {
1941 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1945 viewer->SetCurrentPaletteGroup(current_palette_group_);
1949 }
1950 // Sync palette to graphics panel for proper sheet coloring
1954 }
1957 room_id, current_palette_group_);
1958 }
1959 }
1960 }
1961 }
1962 }
1963 }
1964
1965 // Workbench mode uses a single stable window and does not spawn per-room
1966 // panels. Keep selection + panels in sync and return.
1968 if (dependencies_.window_manager && request_focus) {
1969 // Only force-show if it's already visible or if it's the first initialization
1970 // This avoids obtrusive behavior when the user explicitly closed it.
1971 if (dependencies_.window_manager->IsWindowOpen("dungeon.workbench")) {
1972 dependencies_.window_manager->OpenWindow("dungeon.workbench");
1973 }
1974 }
1975 return;
1976 }
1977
1978 // Check if room is already open
1979 for (int i = 0; i < active_rooms_.Size; i++) {
1980 if (active_rooms_[i] == room_id) {
1981 // Always ensure panel is visible, even if already in active_rooms_
1983 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1985 }
1986 if (request_focus) {
1987 FocusRoom(room_id);
1988 }
1989 return;
1990 }
1991 }
1992
1993 active_rooms_.push_back(room_id);
1995
1997 // Use unified ResourceLabelProvider for room names
1998 std::string room_name = absl::StrFormat(
1999 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2000
2001 std::string base_card_id = absl::StrFormat("dungeon.room_%d", room_id);
2002
2004 {.card_id = base_card_id,
2005 .display_name = room_name,
2006 .window_title = ICON_MD_GRID_ON " " + room_name,
2007 .icon = ICON_MD_GRID_ON,
2008 .category = "Dungeon",
2009 .workflow_group = "Rooms",
2010 .shortcut_hint = "",
2011 .visibility_flag = nullptr,
2012 .priority = 200 + room_id});
2013
2015 }
2016}
2017
2019 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
2020 return;
2021 }
2022 current_entrance_id_ = entrance_id;
2024
2025 const int room_id = ResolveEntranceRoomId(entrance_id);
2026 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2027 return;
2028 }
2029 OnRoomSelected(room_id);
2030}
2031
2032int DungeonEditorV2::ResolveEntranceRoomId(int entrance_id) const {
2033 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
2034 return -1;
2035 }
2036 if (entrance_id < zelda3::kNumDungeonSpawnPoints) {
2037 const auto& spawn = spawn_points_[entrance_id];
2038 return spawn.spawn_id() == entrance_id ? spawn.room_id : -1;
2039 }
2040 return entrances_[entrance_id].room_;
2041}
2042
2044 int room_id) const {
2045 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2046 return 0xFF;
2047 }
2048 if (current_entrance_id_ < 0 ||
2049 current_entrance_id_ >= static_cast<int>(entrances_.size())) {
2050 return 0xFF;
2051 }
2053 const auto& spawn = spawn_points_[current_entrance_id_];
2054 if (spawn.spawn_id() != current_entrance_id_ || spawn.room_id != room_id) {
2055 return 0xFF;
2056 }
2057 return spawn.main_gfx;
2058 }
2059
2060 const auto& entrance = entrances_[current_entrance_id_];
2061 if (entrance.room_ != room_id) {
2062 return 0xFF;
2063 }
2064 return entrance.blockset_;
2065}
2066
2068 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2069 return;
2070 }
2071 auto& room = rooms_[room_id];
2072 if (!room.IsLoaded()) {
2073 return;
2074 }
2075 room.SetRenderEntranceBlockset(
2077}
2078
2080 int room_id) {
2081 if (viewer == nullptr) {
2082 return;
2083 }
2084 const uint8_t entrance_blockset =
2086 if (entrance_blockset == 0xFF) {
2088 } else {
2089 viewer->SetEntranceRenderContext(current_entrance_id_, entrance_blockset);
2090 }
2091}
2092
2093void DungeonEditorV2::add_room(int room_id) {
2094 OnRoomSelected(room_id);
2095}
2096
2098 auto it = room_cards_.find(room_id);
2099 if (it != room_cards_.end()) {
2100 it->second->Focus();
2101 }
2102}
2103
2109 OpenWindow("dungeon.workbench");
2110 } else {
2112 }
2113 }
2114}
2115
2117 if (enabled && dependencies_.window_manager) {
2119 OpenWindow("dungeon.workbench");
2120 if (workbench_panel_) {
2122 }
2123 } else {
2127 }
2130 }
2131 }
2132}
2133
2137
2140 LOG_ERROR("DungeonEditorV2", "Cannot place object: Invalid room ID %d",
2142 const std::string message = absl::StrFormat(
2143 "Cannot place 0x%02X: invalid room %d", obj.id_, current_room_id_);
2146 } else if (dependencies_.toast_manager) {
2148 }
2149 return;
2150 }
2151
2152 auto& room = rooms_[current_room_id_];
2153
2154 LOG_INFO("DungeonEditorV2",
2155 "Placing object ID=0x%02X at position (%d,%d) in room %03X", obj.id_,
2156 obj.x_, obj.y_, current_room_id_);
2157
2158 room.RenderRoomGraphics();
2159 LOG_DEBUG("DungeonEditorV2",
2160 "Object placed and room re-rendered successfully");
2161 // Brief success feedback so the user knows the placement was accepted.
2162 // Kept minimal (no inline panel update) — success is non-spammy by design.
2165 absl::StrFormat("Placed 0x%02X in room %03X", obj.id_,
2168 }
2169}
2170
2172 int room_id, const zelda3::RoomObject& object) {
2173 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2174 LOG_WARN("DungeonEditorV2", "Edit Graphics ignored (invalid room id %d)",
2175 room_id);
2176 return;
2177 }
2178
2179 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
2180 if (!editor_manager) {
2181 LOG_WARN("DungeonEditorV2",
2182 "Edit Graphics ignored (editor manager unavailable)");
2183 return;
2184 }
2185
2186 auto& room = rooms_[room_id];
2188 room.LoadRoomGraphics();
2189
2190 uint16_t sheet_id = 0;
2191 uint16_t tile_index = 0;
2192 bool resolved_sheet = false;
2193 if (auto tiles_or = object.GetTiles();
2194 tiles_or.ok() && !tiles_or.value().empty()) {
2195 const uint16_t tile_id = tiles_or.value().front().id_;
2196 const size_t block_index = static_cast<size_t>(tile_id / 64);
2197 const auto blocks = room.blocks();
2198 if (block_index < blocks.size()) {
2199 sheet_id = blocks[block_index];
2200 resolved_sheet = true;
2201 }
2202 const int tiles_per_row = gfx::kTilesheetWidth / 8;
2203 const int tiles_per_col = gfx::kTilesheetHeight / 8;
2204 const int tiles_per_sheet = tiles_per_row * tiles_per_col;
2205 if (tiles_per_sheet > 0) {
2206 tile_index = static_cast<uint16_t>(tile_id % tiles_per_sheet);
2207 }
2208 }
2209
2210 editor_manager->SwitchToEditor(EditorType::kGraphics, true);
2211 if (auto* editor_set = editor_manager->GetCurrentEditorSet()) {
2212 if (auto* graphics = editor_set->GetGraphicsEditor()) {
2213 if (resolved_sheet) {
2214 graphics->SelectSheet(sheet_id);
2215 graphics->HighlightTile(sheet_id, tile_index,
2216 absl::StrFormat("Object 0x%02X", object.id_));
2217 }
2218 }
2219 }
2220
2223 }
2224}
2225
2227 int room_id, const zelda3::RoomObject& object) {
2228 if (object_tile_editor_panel_ == nullptr) {
2229 return absl::FailedPreconditionError(
2230 "Object Tile Editor panel is unavailable");
2231 }
2232 if (dependencies_.window_manager == nullptr) {
2233 return absl::FailedPreconditionError(
2234 "Workspace window manager is unavailable");
2235 }
2236 if (rom_ == nullptr || !rom_->is_loaded()) {
2237 return absl::FailedPreconditionError("ROM not loaded");
2238 }
2239 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2240 return absl::OutOfRangeError("Dungeon room id is outside the room store");
2241 }
2243 return absl::UnimplementedError(
2244 "Object tile editing is supported only for audited objects 0x11F and "
2245 "0x120");
2246 }
2247 if (game_data_ == nullptr ||
2249 return absl::FailedPreconditionError("Dungeon palette data is unavailable");
2250 }
2251
2252 auto& room = rooms_[room_id];
2253 if (!room.IsLoaded()) {
2254 RETURN_IF_ERROR(room_loader_.LoadRoom(room_id, room));
2255 }
2257 room.PrepareForRender();
2258
2259 const int palette_id = room.ResolveDungeonPaletteId();
2260 const auto& dungeon_palettes = game_data_->palette_groups.dungeon_main;
2261 if (palette_id < 0 ||
2262 palette_id >= static_cast<int>(dungeon_palettes.size())) {
2263 return absl::OutOfRangeError(
2264 "Room resolves to an unavailable dungeon palette");
2265 }
2266 auto room_palette = dungeon_palettes[palette_id];
2267 const auto palette_group = zelda3::BuildDungeonRenderPaletteGroupFromGameData(
2268 room_palette, game_data_);
2269
2270 const absl::Status open_status = object_tile_editor_panel_->OpenForObject(
2271 object.id_, room_id, &rooms_, palette_group);
2272 if (!open_status.ok()) {
2273 // A modified hidden session rejects replacement to prevent data loss.
2274 // Surface that existing session so the user can apply, revert, or use the
2275 // explicit discard action described by the returned status.
2279 }
2280 return open_status;
2281 }
2285 return absl::NotFoundError(
2286 "Object Tile Editor window is not registered in this session");
2287 }
2288 return absl::OkStatus();
2289}
2290
2291void DungeonEditorV2::SwapRoomInPanel(int old_room_id, int new_room_id) {
2292 // Defer the swap until after the current frame's draw phase completes
2293 // This prevents modifying data structures while ImGui is still using them
2294 if (new_room_id < 0 || new_room_id >= static_cast<int>(rooms_.size())) {
2295 return;
2296 }
2297 pending_swap_.old_room_id = old_room_id;
2298 pending_swap_.new_room_id = new_room_id;
2299 pending_swap_.pending = true;
2300}
2301
2303 if (!pending_swap_.pending) {
2304 return;
2305 }
2306
2307 int old_room_id = pending_swap_.old_room_id;
2308 int new_room_id = pending_swap_.new_room_id;
2309 pending_swap_.pending = false;
2310
2311 // Find the position of old_room in active_rooms_
2312 int swap_index = -1;
2313 for (int i = 0; i < active_rooms_.Size; i++) {
2314 if (active_rooms_[i] == old_room_id) {
2315 swap_index = i;
2316 break;
2317 }
2318 }
2319
2320 if (swap_index < 0) {
2321 // Old room not found in active rooms, just select the new one
2322 OnRoomSelected(new_room_id);
2323 return;
2324 }
2325
2326 // Avoid swapping into an already-open room panel (the per-room maps assume
2327 // room IDs are unique). In this case, just focus/select the existing room.
2328 for (int i = 0; i < active_rooms_.Size; i++) {
2329 if (i != swap_index && active_rooms_[i] == new_room_id) {
2330 OnRoomSelected(new_room_id);
2331 return;
2332 }
2333 }
2334
2335 // Preserve the old panel's stable ImGui window identity by transferring its
2336 // slot id to the new room id.
2337 int slot_id = -1;
2338 if (auto it = room_panel_slot_ids_.find(old_room_id);
2339 it != room_panel_slot_ids_.end()) {
2340 slot_id = it->second;
2341 room_panel_slot_ids_.erase(it);
2342 } else {
2343 slot_id = next_room_panel_slot_id_++;
2344 }
2345 room_panel_slot_ids_[new_room_id] = slot_id;
2346
2347 // Preserve the viewer instance so canvas pan/zoom and UI state don't reset
2348 // when navigating with arrows (swap-in-panel).
2349 room_viewers_.Rename(old_room_id, new_room_id);
2350
2351 // Replace old room with new room in active_rooms_
2352 active_rooms_[swap_index] = new_room_id;
2354
2355 // Unregister old panel
2357 std::string old_card_id = absl::StrFormat("dungeon.room_%d", old_room_id);
2358 const bool old_pinned =
2361
2362 // Register new panel
2363 // Use unified ResourceLabelProvider for room names
2364 std::string new_room_name = absl::StrFormat(
2365 "[%03X] %s", new_room_id, zelda3::GetRoomLabel(new_room_id).c_str());
2366
2367 std::string new_card_id = absl::StrFormat("dungeon.room_%d", new_room_id);
2368
2370 {.card_id = new_card_id,
2371 .display_name = new_room_name,
2372 .window_title = ICON_MD_GRID_ON " " + new_room_name,
2373 .icon = ICON_MD_GRID_ON,
2374 .category = "Dungeon",
2375 .workflow_group = "Rooms",
2376 .shortcut_hint = "",
2377 .visibility_flag = nullptr,
2378 .priority = 200 + new_room_id});
2379
2380 if (old_pinned) {
2381 dependencies_.window_manager->SetWindowPinned(new_card_id, true);
2382 }
2384 }
2385
2386 // Clean up old room's card and viewer
2387 room_cards_.erase(old_room_id);
2388
2389 // Update current selection
2390 OnRoomSelected(new_room_id, /*request_focus=*/false);
2391}
2392
2395 return;
2396 }
2397
2398 const bool enabled = pending_workflow_mode_.enabled;
2399 const bool show_toast = pending_workflow_mode_.show_toast;
2401 SetWorkbenchWorkflowMode(enabled, show_toast);
2402}
2403
2406 return;
2407 }
2408
2412
2413 auto* window_manager = dependencies_.window_manager;
2414 if (!window_manager) {
2415 return;
2416 }
2417
2418 if (!window_manager->OpenWindow(request.session_id, request.window_id)) {
2419 return;
2420 }
2421
2422 if (ImGui::GetCurrentContext() != nullptr) {
2423 const std::string window_name = window_manager->GetWorkspaceWindowName(
2424 request.session_id, request.window_id);
2425 if (!window_name.empty()) {
2426 ImGui::SetWindowFocus(window_name.c_str());
2427 }
2428 }
2429}
2430
2432 room_viewers_.Touch(room_id);
2433}
2434
2436 // No-op: LruCache handles removal internally via Erase()
2437 (void)room_id;
2438}
2439
2441 if (!viewer) {
2442 return;
2443 }
2444
2445 viewer->SetShowObjectPanelCallback([this]() {
2448 OpenWindow("dungeon.workbench");
2449 return;
2450 }
2452 });
2453 viewer->SetShowSpritePanelCallback([this]() {
2456 OpenWindow("dungeon.workbench");
2457 return;
2458 }
2459 OpenWindow("dungeon.sprite_editor");
2460 });
2461 viewer->SetShowItemPanelCallback([this]() {
2464 OpenWindow("dungeon.workbench");
2465 return;
2466 }
2467 OpenWindow("dungeon.item_editor");
2468 });
2469 viewer->SetShowRoomListCallback([this]() {
2470 OpenWindow(IsWorkbenchWorkflowEnabled() ? "dungeon.workbench"
2472 });
2473 viewer->SetShowRoomMatrixCallback([this]() {
2476 OpenWindow("dungeon.workbench");
2477 return;
2478 }
2480 });
2481 viewer->SetShowEntranceListCallback([this]() {
2484 OpenWindow("dungeon.workbench");
2485 return;
2486 }
2488 });
2489 viewer->SetShowRoomGraphicsCallback([this]() {
2492 OpenWindow("dungeon.workbench");
2493 return;
2494 }
2496 });
2497 viewer->SetShowDoorEditorCallback([this]() {
2500 OpenWindow("dungeon.workbench");
2501 return;
2502 }
2503 OpenWindow("dungeon.door_editor");
2504 });
2505 viewer->SetShowDungeonSettingsCallback([this]() {
2506 if (workbench_panel_) {
2508 }
2509 OpenWindow("dungeon.workbench");
2510 });
2512 [this](int target_room_id, const zelda3::RoomObject& object) {
2513 OpenGraphicsEditorForObject(target_room_id, object);
2514 });
2516 [this](int target_room_id, const zelda3::RoomObject& object) {
2517 const absl::Status status =
2518 OpenObjectTileEditorForObject(target_room_id, object);
2519 if (status.ok()) {
2520 return;
2521 }
2522 LOG_ERROR("DungeonEditorV2", "Edit Object Tiles failed: %s",
2523 status.message().data());
2526 absl::StrFormat("Edit Object Tiles failed: %s", status.message()),
2528 }
2529 });
2530 viewer->SetSaveRoomCallback([this](int target_room_id) {
2531 auto status = SaveRoom(target_room_id);
2532 if (!status.ok()) {
2533 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
2534 status.message().data());
2537 absl::StrFormat("Apply Room failed: %s", status.message()),
2539 }
2540 return;
2541 }
2543 dependencies_.toast_manager->Show("Applied room to ROM buffer",
2545 }
2546 });
2547
2550}
2551
2554 (void)room_id;
2555 return GetWorkbenchViewer();
2556 }
2557
2558 // Set eviction predicate to protect active rooms from eviction
2559 room_viewers_.SetEvictionPredicate([this](const int& candidate) {
2560 for (int i = 0; i < active_rooms_.size(); ++i) {
2561 if (active_rooms_[i] == candidate)
2562 return false;
2563 }
2564 return true;
2565 });
2566
2567 if (auto* existing = room_viewers_.Get(room_id)) {
2568 // Viewer already exists - Get() already touched LRU
2569 auto* viewer_ptr = existing->get();
2570 ConfigureViewerRenderContext(viewer_ptr, room_id);
2572
2573 // Update pinned state from manager
2575 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2576 viewer_ptr->SetPinned(
2578 viewer_ptr->SetPinCallback([this, card_id, room_id](bool pinned) {
2580 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2581 if (auto* v = GetViewerForRoom(room_id)) {
2582 v->SetPinned(pinned);
2583 }
2584 }
2585 });
2586 }
2587
2588 return viewer_ptr;
2589 }
2590
2591 // Creating a new viewer - Insert will handle LRU and eviction
2592 {
2593 auto viewer = std::make_unique<DungeonCanvasViewer>(rom_);
2594 viewer->SetCompactHeaderMode(false);
2595 viewer->SetRoomDetailsExpanded(true);
2596 DungeonCanvasViewer* viewer_ptr = viewer.get();
2597 viewer->SetRooms(&rooms_);
2598 viewer->SetRenderer(renderer_);
2599 viewer->SetCurrentPaletteGroup(current_palette_group_);
2600 viewer->SetCurrentPaletteId(current_palette_id_);
2601 viewer->SetGameData(game_data_);
2602 ConfigureViewerRenderContext(viewer_ptr, room_id);
2603 ApplyEntranceRenderContext(room_id);
2604
2605 // These hooks must remain correct even when a room panel swaps rooms while
2606 // keeping the same viewer instance (to preserve canvas pan/zoom + UI
2607 // state). Use the viewer's best-effort current room context instead of
2608 // capturing room_id at creation time.
2609 viewer->object_interaction().SetMutationCallback([this, viewer_ptr]() {
2610 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2611 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2612 const auto domain =
2614 if (domain == MutationDomain::kTileObjects) {
2615 BeginUndoSnapshot(rid);
2616 } else if (domain == MutationDomain::kCustomCollision) {
2617 BeginCollisionUndoSnapshot(rid);
2618 } else if (domain == MutationDomain::kWaterFill) {
2619 BeginWaterFillUndoSnapshot(rid);
2620 }
2621 }
2622 });
2623
2624 viewer->object_interaction().SetCacheInvalidationCallback([this,
2625 viewer_ptr]() {
2626 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2627 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2628 const auto domain =
2630 if (domain == MutationDomain::kTileObjects) {
2631 rooms_[rid].MarkObjectsDirty();
2632 rooms_[rid].RenderRoomGraphics();
2633 // Drag edits invalidate incrementally; finalize once the drag ends
2634 // (TileObjectHandler emits an extra invalidation on release).
2635 const auto mode =
2636 viewer_ptr->object_interaction().mode_manager().GetMode();
2638 FinalizeUndoAction(rid);
2639 }
2640 } else if (domain == MutationDomain::kCustomCollision) {
2641 const auto mode =
2642 viewer_ptr->object_interaction().mode_manager().GetMode();
2643 const auto& st =
2645 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2646 return;
2647 }
2648 FinalizeCollisionUndoAction(rid);
2649 } else if (domain == MutationDomain::kWaterFill) {
2650 const auto mode =
2651 viewer_ptr->object_interaction().mode_manager().GetMode();
2652 const auto& st =
2654 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2655 return;
2656 }
2657 FinalizeWaterFillUndoAction(rid);
2658 }
2659 }
2660 });
2661
2662 viewer->object_interaction().SetObjectPlacedCallback(
2663 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2664
2665 if (dungeon_editor_system_) {
2666 viewer->SetEditorSystem(dungeon_editor_system_.get());
2667 }
2668 viewer->SetRoomNavigationCallback([this](int target_room) {
2669 if (target_room >= 0 && target_room < static_cast<int>(rooms_.size())) {
2670 OnRoomSelected(target_room);
2671 }
2672 });
2673 // Swap callback swaps the room in the current panel instead of opening new
2674 viewer->SetRoomSwapCallback([this](int old_room, int new_room) {
2675 SwapRoomInPanel(old_room, new_room);
2676 });
2677 WireViewerPanelCallbacks(viewer.get());
2678
2679 // Wire up pinning for room panels.
2680 if (dependencies_.window_manager) {
2681 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2682 viewer->SetPinned(dependencies_.window_manager->IsWindowPinned(card_id));
2683 viewer->SetPinCallback([this, card_id, room_id](bool pinned) {
2684 if (dependencies_.window_manager) {
2685 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2686 // Sync state back to viewer in all panels showing this room
2687 if (auto* v = GetViewerForRoom(room_id)) {
2688 v->SetPinned(pinned);
2689 }
2690 }
2691 });
2692 }
2693
2694 auto* stored = room_viewers_.Insert(room_id, std::move(viewer));
2695 return stored->get();
2696 }
2697}
2698
2699void DungeonEditorV2::RefreshWorkbenchViewerRuntimeContext(
2700 DungeonCanvasViewer* viewer, int room_id) {
2701 if (!viewer) {
2702 return;
2703 }
2704
2705 if (viewer->rom() != rom_) {
2706 viewer->SetRom(rom_);
2707 }
2708 if (viewer->rooms() != &rooms_) {
2709 viewer->SetRooms(&rooms_);
2710 }
2711 if (viewer->game_data() != game_data_) {
2712 viewer->SetGameData(game_data_);
2713 }
2714 viewer->SetRenderer(renderer_);
2715 viewer->SetCurrentPaletteGroup(current_palette_group_);
2716 viewer->SetCurrentPaletteId(current_palette_id_);
2717 ConfigureViewerRenderContext(viewer, room_id);
2718 viewer->SetEditorSystem(dungeon_editor_system_.get());
2719 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2720 if (viewer->project() != dependencies_.project) {
2721 viewer->SetProject(dependencies_.project);
2722 }
2723}
2724
2725DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchViewer() {
2726 if (!workbench_viewer_) {
2727 workbench_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2728 auto* viewer = workbench_viewer_.get();
2729 viewer->SetCompactHeaderMode(true);
2730 viewer->SetRoomDetailsExpanded(false);
2731 viewer->SetHeaderVisible(false);
2732 viewer->SetHeaderHiddenMetadataHudVisible(false);
2733 viewer->SetRooms(&rooms_);
2734 viewer->SetRenderer(renderer_);
2735 viewer->SetCurrentPaletteGroup(current_palette_group_);
2736 viewer->SetCurrentPaletteId(current_palette_id_);
2737 viewer->SetGameData(game_data_);
2738
2739 // Workbench uses a single viewer; these hooks use the viewer's current room
2740 // context (set at DrawDungeonCanvas start) so room switching stays correct.
2741 viewer->object_interaction().SetMutationCallback([this, viewer]() {
2742 const int rid = viewer ? viewer->current_room_id() : -1;
2743 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2744 const auto domain = viewer->object_interaction().last_mutation_domain();
2745 if (domain == MutationDomain::kTileObjects) {
2746 BeginUndoSnapshot(rid);
2747 } else if (domain == MutationDomain::kCustomCollision) {
2748 BeginCollisionUndoSnapshot(rid);
2749 } else if (domain == MutationDomain::kWaterFill) {
2750 BeginWaterFillUndoSnapshot(rid);
2751 }
2752 }
2753 });
2754 viewer->object_interaction().SetCacheInvalidationCallback([this, viewer]() {
2755 const int rid = viewer ? viewer->current_room_id() : -1;
2756 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2757 const auto domain =
2758 viewer->object_interaction().last_invalidation_domain();
2759 if (domain == MutationDomain::kTileObjects) {
2760 rooms_[rid].MarkObjectsDirty();
2761 rooms_[rid].RenderRoomGraphics();
2762 const auto mode =
2763 viewer->object_interaction().mode_manager().GetMode();
2764 if (mode != InteractionMode::DraggingObjects) {
2765 FinalizeUndoAction(rid);
2766 }
2767 } else if (domain == MutationDomain::kCustomCollision) {
2768 const auto mode =
2769 viewer->object_interaction().mode_manager().GetMode();
2770 const auto& st =
2771 viewer->object_interaction().mode_manager().GetModeState();
2772 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2773 return;
2774 }
2775 FinalizeCollisionUndoAction(rid);
2776 } else if (domain == MutationDomain::kWaterFill) {
2777 const auto mode =
2778 viewer->object_interaction().mode_manager().GetMode();
2779 const auto& st =
2780 viewer->object_interaction().mode_manager().GetModeState();
2781 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2782 return;
2783 }
2784 FinalizeWaterFillUndoAction(rid);
2785 }
2786 }
2787 });
2788
2789 viewer->object_interaction().SetObjectPlacedCallback(
2790 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2791
2792 if (dungeon_editor_system_) {
2793 viewer->SetEditorSystem(dungeon_editor_system_.get());
2794 }
2795
2796 // In workbench mode, arrow navigation swaps the current room without
2797 // changing window identities.
2798 viewer->SetRoomSwapCallback([this](int /*old_room*/, int new_room) {
2799 OnRoomSelected(new_room, /*request_focus=*/false);
2800 });
2801 viewer->SetRoomNavigationCallback([this](int target_room) {
2802 OnRoomSelected(target_room, /*request_focus=*/false);
2803 });
2804
2805 WireViewerPanelCallbacks(viewer);
2806 }
2807
2808 auto* viewer = workbench_viewer_.get();
2809 RefreshWorkbenchViewerRuntimeContext(viewer, current_room_id_);
2810 return viewer;
2811}
2812
2813DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchCompareViewer(int room_id) {
2814 if (!workbench_compare_viewer_) {
2815 workbench_compare_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2816 auto* viewer = workbench_compare_viewer_.get();
2817 viewer->SetCompactHeaderMode(true);
2818 viewer->SetRoomDetailsExpanded(false);
2819 viewer->SetHeaderHiddenMetadataHudVisible(false);
2820 viewer->SetRooms(&rooms_);
2821 viewer->SetRenderer(renderer_);
2822 viewer->SetCurrentPaletteGroup(current_palette_group_);
2823 viewer->SetCurrentPaletteId(current_palette_id_);
2824 viewer->SetGameData(game_data_);
2825
2826 // Compare viewer is read-only by default: no object selection/mutation, but
2827 // still allows canvas pan/zoom.
2828 viewer->SetObjectInteractionEnabled(false);
2829 viewer->SetHeaderReadOnly(true);
2830 viewer->SetHeaderVisible(false);
2831
2832 if (dungeon_editor_system_) {
2833 // Allows consistent rendering paths that depend on the editor system, but
2834 // interaction is still disabled.
2835 viewer->SetEditorSystem(dungeon_editor_system_.get());
2836 }
2837
2838 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2839 viewer->SetProject(dependencies_.project);
2840 }
2841
2842 auto* viewer = workbench_compare_viewer_.get();
2843 RefreshWorkbenchViewerRuntimeContext(viewer, room_id);
2844 viewer->SetObjectInteractionEnabled(false);
2845 viewer->SetHeaderReadOnly(true);
2846 viewer->SetHeaderVisible(false);
2847 return viewer;
2848}
2849
2850void DungeonEditorV2::SyncPanelsToRoom(int room_id) {
2851 // Update object editor card with current viewer
2852 if (object_selector_panel_) {
2853 object_selector_panel_->SetCanvasViewerProvider(
2854 [this]() { return GetViewerForRoom(current_room_id_); });
2855 object_selector_panel_->SetCurrentRoom(room_id);
2856 object_selector_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2857 }
2858 if (object_editor_content_) {
2859 object_editor_content_->SetCanvasViewerProvider(
2860 [this]() { return GetViewerForRoom(current_room_id_); });
2861 object_editor_content_->SetCurrentRoom(room_id);
2862 object_editor_content_->SetCanvasViewer(GetViewerForRoom(room_id));
2863 }
2864 if (door_editor_panel_) {
2865 door_editor_panel_->SetCanvasViewerProvider(
2866 [this]() { return GetViewerForRoom(current_room_id_); });
2867 door_editor_panel_->SetCurrentRoom(room_id);
2868 door_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2869 }
2870
2871 // Update sprite and item editor panels with current viewer
2872 if (sprite_editor_panel_) {
2873 sprite_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2874 }
2875 if (item_editor_panel_) {
2876 item_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2877 }
2878 if (custom_collision_panel_) {
2879 auto* viewer = GetViewerForRoom(room_id);
2880 custom_collision_panel_->SetCanvasViewer(viewer);
2881 if (viewer) {
2882 custom_collision_panel_->SetInteraction(&viewer->object_interaction());
2883 }
2884 }
2885 if (water_fill_panel_) {
2886 auto* viewer = GetViewerForRoom(room_id);
2887 water_fill_panel_->SetCanvasViewer(viewer);
2888 if (viewer) {
2889 water_fill_panel_->SetInteraction(&viewer->object_interaction());
2890 }
2891 }
2892
2893 if (room_tag_editor_panel_) {
2894 room_tag_editor_panel_->SetCurrentRoomId(room_id);
2895 }
2896
2897 if (overlay_manager_panel_) {
2898 auto* viewer = GetViewerForRoom(room_id);
2899 if (viewer) {
2901 overlay_state.show_grid = viewer->mutable_show_grid();
2902 overlay_state.show_object_bounds = viewer->mutable_show_object_bounds();
2903 overlay_state.show_coordinate_overlay =
2904 viewer->mutable_show_coordinate_overlay();
2905 overlay_state.show_room_debug_info =
2906 viewer->mutable_show_room_debug_info();
2907 overlay_state.show_texture_debug = viewer->mutable_show_texture_debug();
2908 overlay_state.show_layer_info = viewer->mutable_show_layer_info();
2909 overlay_state.show_minecart_tracks =
2910 viewer->mutable_show_minecart_tracks();
2911 overlay_state.show_custom_collision =
2912 viewer->mutable_show_custom_collision_overlay();
2913 overlay_state.show_track_collision =
2914 viewer->mutable_show_track_collision_overlay();
2915 overlay_state.show_camera_quadrants =
2916 viewer->mutable_show_camera_quadrant_overlay();
2917 overlay_state.show_minecart_sprites =
2918 viewer->mutable_show_minecart_sprite_overlay();
2919 overlay_state.show_collision_legend =
2920 viewer->mutable_show_track_collision_legend();
2921 overlay_manager_panel_->SetState(overlay_state);
2922 }
2923 }
2924}
2925
2926void DungeonEditorV2::ShowRoomPanel(int room_id) {
2927 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2928 return;
2929 }
2930
2931 bool already_active = false;
2932 for (int i = 0; i < active_rooms_.Size; ++i) {
2933 if (active_rooms_[i] == room_id) {
2934 already_active = true;
2935 break;
2936 }
2937 }
2938 if (!already_active) {
2939 active_rooms_.push_back(room_id);
2940 room_selector_.set_active_rooms(active_rooms_);
2941 }
2942
2943 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2944
2945 if (dependencies_.window_manager) {
2946 if (!dependencies_.window_manager->GetWindowDescriptor(
2947 dependencies_.window_manager->GetActiveSessionId(), card_id)) {
2948 std::string room_name = absl::StrFormat(
2949 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2950 dependencies_.window_manager->RegisterWindow(
2951 {.card_id = card_id,
2952 .display_name = room_name,
2953 .window_title = ICON_MD_GRID_ON " " + room_name,
2954 .icon = ICON_MD_GRID_ON,
2955 .category = "Dungeon",
2956 .workflow_group = "Rooms",
2957 .shortcut_hint = "",
2958 .visibility_flag = nullptr,
2959 .priority = 200 + room_id});
2960 }
2961 dependencies_.window_manager->OpenWindow(card_id);
2962 }
2963
2964 // Create or update the PanelWindow for this room
2965 if (room_cards_.find(room_id) == room_cards_.end()) {
2966 std::string base_name = absl::StrFormat(
2967 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2968 const int slot_id = GetOrCreateRoomPanelSlotId(room_id);
2969 std::string card_name_str = absl::StrFormat(
2970 "%s###RoomPanelSlot%d", MakePanelTitle(base_name).c_str(), slot_id);
2971
2972 auto card = std::make_shared<gui::PanelWindow>(card_name_str.c_str(),
2974 card->SetDefaultSize(620, 700);
2975 room_cards_[room_id] = card;
2976 }
2977}
2978
2979} // namespace yaze::editor
void Publish(const T &event)
Definition event_bus.h:35
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
bool is_loaded() const
Definition rom.h:155
static Flags & get()
Definition features.h:119
bool loaded() const
Check if the manifest has been loaded.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
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 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 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::unique_ptr< CustomCollisionPanel > owned_custom_collision_panel_
class CustomCollisionPanel * custom_collision_panel_
std::optional< int > room_canvas_delete_shortcut_frame_
void ToggleWorkbenchWorkflowMode(bool show_toast=true)
void ContributeStatus(StatusBar *status_bar) override
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_
void SetDependencies(const EditorDependencies &deps) override
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_
std::unordered_map< int, int > room_panel_slot_ids_
class DungeonWorkbenchContent * workbench_panel_
int ResolveEntranceRoomId(int entrance_id) const
void OnRoomSelected(int room_id, bool request_focus=true)
PendingCollisionUndo pending_collision_undo_
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 WireViewerPanelCallbacks(DungeonCanvasViewer *viewer)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
PendingWaterFillUndo pending_water_fill_undo_
std::unique_ptr< MinecartTrackEditorPanel > owned_minecart_track_editor_panel_
gui::PaletteEditorWidget palette_editor_
std::unique_ptr< ObjectEditorContent > owned_object_editor_content_
std::unique_ptr< DoorEditorContent > owned_door_editor_panel_
bool ConsumeRoomCanvasDeleteShortcut(DungeonCanvasViewer &viewer)
static bool IsValidRoomId(int room_id)
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)
class WaterFillPanel * water_fill_panel_
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)
ObjectEditorContent * object_editor_content_
static constexpr const char * kRoomMatrixId
void HandleDungeonPaletteChanged(gui::DungeonPaletteChange change)
static constexpr const char * kRoomSelectorId
PaletteEditorContent * palette_editor_panel_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
std::unique_ptr< WaterFillPanel > owned_water_fill_panel_
absl::Status ApplyMinecartCollisionBatch(const std::vector< zelda3::TrackCollisionResult > &preview, const zelda3::GeneratorOptions &options)
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 SetOpenMinecartEditorWindowCallback(std::function< bool()> callback)
void SetOpenTileEditorWindowCallback(std::function< bool()> callback)
void SetTileEditorPanel(ObjectTileEditorPanel *panel)
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 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 SetOnInspectorSideChanged(std::function< void(bool)> cb)
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.
void SetPitDamageTableProvider(std::function< zelda3::PitDamageTable *()> provider)
The EditorManager controls the main editor window and manages the various editor classes.
UndoManager undo_manager_
Definition editor.h:339
virtual void SetDependencies(const EditorDependencies &deps)
Definition editor.h:250
zelda3::GameData * game_data() const
Definition editor.h:325
EditorDependencies dependencies_
Definition editor.h:338
EditorType type_
Definition editor.h:337
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
std::string GetId() const override
Unique identifier for this panel.
void SetProjectChangedCallback(ProjectChangedCallback callback)
absl::Status RebindProjectContext(project::YazeProject *project)
void SetCollisionBatchApplyCallback(CollisionBatchApplyCallback callback)
void SetProjectDraftChangedCallback(ProjectDraftChangedCallback callback)
void SetProjectSaveCallback(ProjectSaveCallback callback)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
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 SetRooms(DungeonRoomStore *rooms)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
A session-aware status bar displayed at the bottom of the application.
Definition status_bar.h:57
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
void SetEditorMode(const std::string &mode)
Set the current editor mode or tool.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
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
std::string GetDungeonInspectorSide() const
void SetDungeonInspectorSide(std::string side)
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 ShowAllWindowsInCategory(size_t session_id, const std::string &category)
void UnregisterWindow(size_t session_id, const std::string &base_window_id)
void RegisterPanelAlias(const std::string &legacy_base_id, const std::string &canonical_base_id)
Register a legacy panel ID alias that resolves to a canonical ID.
static std::shared_ptr< MesenSocketClient > & GetClient()
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:574
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:581
static Arena & Get()
Definition arena.cc:24
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.
void MarkObjectsDirty()
Definition room.h:431
void set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:596
bool HasUnsavedChanges() const
Definition room.h:611
void MarkGraphicsDirty()
Definition room.h:472
bool IsLoaded() const
Definition room.h:919
void ClearWaterFillZone()
Definition room.h:583
void ClearWaterFillDirty()
Definition room.h:604
int ResolveDungeonPaletteId() const
Definition room.cc:854
void SetGameData(GameData *data)
Definition room.h:1012
void SetWaterFillTile(int x, int y, bool filled)
Definition room.h:560
#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_TUNE
Definition icons.h:2022
#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:103
#define LOG_ERROR(category, format,...)
Definition log.h:110
#define LOG_WARN(category, format,...)
Definition log.h:108
#define LOG_INFO(category, format,...)
Definition log.h:106
const AgentUITheme & GetTheme()
bool IsTransientDungeonRoomWindowId(const std::string &card_id)
constexpr const char * kStandalone
constexpr const char * kWorkbench
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)
RoomSelectionIntent
Intent for room selection in the dungeon editor.
constexpr int kTilesheetHeight
Definition snes_tile.h:17
constexpr int kTilesheetWidth
Definition snes_tile.h:16
std::string MakePanelTitle(const std::string &title)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:673
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:183
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
Unified dependency container for all editor types.
Definition editor.h:169
project::YazeProject * project
Definition editor.h:173
GlobalEditorContext * global_context
Definition editor.h:175
gfx::IRenderer * renderer
Definition editor.h:191
WorkspaceWindowManager * window_manager
Definition editor.h:181
static JumpToRoomRequestEvent Create(int room, size_t session=0)
Optional behavior for an interactive status bar segment.
Definition status_bar.h:27
std::function< void()> on_click
Definition status_bar.h:28
Represents a group of palettes.
DungeonRenderPaletteSource source
static constexpr float kStatusBarHeight
Definition ui_config.h:21
Dungeon overlay configuration (per-project).
Definition project.h:93
RomWritePolicy write_policy
Definition project.h:110
core::HackManifest hack_manifest
Definition project.h:212
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1548
std::string symbols_filename
Definition project.h:190
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
PitDamageTable pit_damage_table
Definition game_data.h:108