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"
75#include "zelda3/dungeon/room.h"
78
79namespace yaze::editor {
80
84 const absl::Status status =
86 if (!status.ok()) {
87 LOG_WARN("DungeonEditorV2", "Minecart project rebind failed: %s",
88 status.message());
89 }
91 }
92}
93
95 if (minecart_track_editor_panel_ == nullptr) {
96 return;
97 }
98
99 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
100 if (editor_manager == nullptr) {
104 return;
105 }
106
107 const size_t session_id = dependencies_.session_id;
109 [editor_manager, session_id](
110 const project::DungeonOverlaySettings& overlay) -> absl::Status {
111 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
112 session_id)) {
113 return absl::FailedPreconditionError(
114 "Minecart overlay edit requires its project context to be "
115 "active");
116 }
117 editor_manager->GetCurrentProject()->dungeon_overlay = overlay;
118 editor_manager->MarkCurrentProjectDirty();
119 return absl::OkStatus();
120 });
122 [editor_manager, session_id]() -> absl::Status {
123 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
124 session_id)) {
125 return absl::FailedPreconditionError(
126 "Minecart overlay draft requires its project context to be "
127 "active");
128 }
129 editor_manager->MarkCurrentProjectDirty();
130 return absl::OkStatus();
131 });
133 [editor_manager, session_id]() -> absl::Status {
134 if (!editor_manager->IsCurrentProjectContextOwnedBySession(
135 session_id)) {
136 return absl::FailedPreconditionError(
137 "Minecart project save requires its project context to be "
138 "active");
139 }
140 return editor_manager->SaveProject();
141 });
142}
143
144namespace {
145
146bool IsTransientDungeonRoomWindowId(const std::string& card_id) {
147 constexpr const char* kPrefix = "dungeon.room_";
148 constexpr size_t kPrefixLength = 13;
149 if (card_id.rfind(kPrefix, 0) != 0 || card_id.size() <= kPrefixLength) {
150 return false;
151 }
152 return std::all_of(card_id.begin() + kPrefixLength, card_id.end(),
153 [](unsigned char ch) { return std::isdigit(ch) != 0; });
154}
155
156} // namespace
157
159 : rom_(rom), room_loader_(rom), room_selector_(rom), rooms_(rom) {
161 if (rom) {
163 }
164}
165
167 if (palette_listener_id_ >= 0) {
170 }
171
172 // Clear viewer references in panels BEFORE room_viewers_ is destroyed.
173 // Panels are owned by WorkspaceWindowManager and outlive this editor, so they need
174 // to have their viewer pointers cleared to prevent dangling pointer access.
177 }
180 }
181 if (door_editor_panel_) {
183 }
186 }
187 if (item_editor_panel_) {
189 }
193 }
194 if (water_fill_panel_) {
197 }
198}
199
201 if (!rom_ || !rom_->is_loaded()) {
202 return absl::FailedPreconditionError("ROM not loaded");
203 }
204
207
208 std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>
209 refreshed_entrances;
210 std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>
211 refreshed_spawn_points;
212 RETURN_IF_ERROR(room_loader_.LoadRoomEntrances(refreshed_entrances));
213 RETURN_IF_ERROR(room_loader_.LoadDungeonSpawnPoints(refreshed_spawn_points));
214
215 struct RefreshedRoom {
216 int room_id;
217 zelda3::Room room;
218 };
219 std::vector<std::pair<int, bool>> materialized_rooms;
221 [&materialized_rooms](int room_id, const zelda3::Room& room) {
222 materialized_rooms.emplace_back(room_id, room.IsLoaded());
223 });
224
225 std::vector<RefreshedRoom> refreshed_rooms;
226 refreshed_rooms.reserve(materialized_rooms.size());
227 for (const auto& [room_id, was_loaded] : materialized_rooms) {
228 zelda3::Room refreshed_room;
229 if (was_loaded) {
230 RETURN_IF_ERROR(room_loader_.LoadRoom(room_id, refreshed_room));
231 } else {
232 refreshed_room = zelda3::LoadRoomHeaderFromRom(rom_, room_id);
233 refreshed_room.SetGameData(game_data_);
234 }
235 refreshed_rooms.push_back({room_id, std::move(refreshed_room)});
236 }
237
238 // All ROM reads succeeded. Clear every interaction that can retain indices
239 // into Room vectors before replacing any Room contents.
243 }
246 }
247 auto prepare_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
248 if (viewer) {
249 viewer->RefreshRomBackedState(rom_, game_data_, &rooms_, room_id);
250 }
251 };
252 room_viewers_.ForEach(
253 [&prepare_viewer](int room_id,
254 std::unique_ptr<DungeonCanvasViewer>& viewer) {
255 prepare_viewer(viewer.get(), room_id);
256 });
257 prepare_viewer(workbench_viewer_.get(), current_room_id_);
258 const int compare_room_id =
260 IsValidRoomId(workbench_compare_viewer_->current_room_id())
261 ? workbench_compare_viewer_->current_room_id()
263 prepare_viewer(workbench_compare_viewer_.get(), compare_room_id);
264
266 dungeon_editor_system_->ClearHistory();
267 if (auto object_editor = dungeon_editor_system_->GetObjectEditor()) {
268 (void)object_editor->ClearSelection();
269 object_editor->ClearHistory();
270 }
271 }
274 pending_undo_ = {};
275 has_pending_undo_ = false;
278 pending_swap_ = {};
281
285 for (auto& refreshed : refreshed_rooms) {
286 auto* room = rooms_.GetIfMaterialized(refreshed.room_id);
287 if (room) {
288 *room = std::move(refreshed.room);
289 }
290 }
291 entrances_ = std::move(refreshed_entrances);
292 spawn_points_ = std::move(refreshed_spawn_points);
294
298
301 auto* current_room = IsValidRoomId(current_room_id_)
303 : nullptr;
304 RETURN_IF_ERROR(dungeon_editor_system_->RefreshRomBackedState(
305 current_room, current_room_id_));
306 }
307
312 if (auto* current_room = rooms_.GetIfMaterialized(current_room_id_);
313 current_room && game_data_ &&
315 const int resolved_palette_id = current_room->ResolveDungeonPaletteId();
317 resolved_palette_id >= 0 &&
318 resolved_palette_id <
319 static_cast<int>(
321 ? static_cast<uint64_t>(resolved_palette_id)
322 : 0;
328 }
329
330 if (is_loaded_) {
334 }
335
336 auto bind_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
337 if (!viewer) {
338 return;
339 }
340 viewer->SetRenderer(renderer_);
346 ConfigureViewerRenderContext(viewer, room_id);
348 };
349 room_viewers_.ForEach(
350 [&bind_viewer](int room_id,
351 std::unique_ptr<DungeonCanvasViewer>& viewer) {
352 bind_viewer(viewer.get(), room_id);
353 });
354 bind_viewer(workbench_viewer_.get(), current_room_id_);
355 bind_viewer(workbench_compare_viewer_.get(), compare_room_id);
356
362 }
365 }
368 }
371 }
375 }
376 if (workbench_panel_) {
378 }
379
380 DungeonCanvasViewer* current_viewer = nullptr;
382 current_viewer = workbench_viewer_.get();
383 } else if (auto* viewer = room_viewers_.Get(current_room_id_)) {
384 current_viewer = viewer->get();
385 }
386 if (current_viewer) {
388 }
389
390 return absl::OkStatus();
391}
392
395 room.ClearWaterFillZone();
396 room.ClearWaterFillDirty();
397 });
398
399 bool legacy_imported = false;
400 const bool water_fill_save_enabled =
402 std::vector<zelda3::WaterFillZoneEntry> zones;
403
404 auto zones_or = zelda3::LoadWaterFillTable(rom_);
405 if (zones_or.ok()) {
406 zones = std::move(zones_or.value());
407 } else {
408 LOG_WARN("DungeonEditorV2", "WaterFillTable parse failed: %s",
409 zones_or.status().message().data());
412 absl::StrFormat("WaterFill table parse failed: %s",
413 zones_or.status().message()),
415 }
416 }
417
418 if (zones.empty()) {
419 std::string sym_path;
424 }
425 auto legacy_or = zelda3::LoadLegacyWaterGateZones(rom_, sym_path);
426 if (legacy_or.ok()) {
427 zones = std::move(legacy_or.value());
428 legacy_imported = !zones.empty();
429 } else {
430 LOG_WARN("DungeonEditorV2", "Legacy water gate import failed: %s",
431 legacy_or.status().message().data());
432 }
433 }
434
435 for (const auto& z : zones) {
436 if (z.room_id < 0 || z.room_id >= static_cast<int>(rooms_.size())) {
437 continue;
438 }
439 auto& room = rooms_[z.room_id];
440 room.set_water_fill_sram_bit_mask(z.sram_bit_mask);
441 for (uint16_t off : z.fill_offsets) {
442 const int x = static_cast<int>(off % 64);
443 const int y = static_cast<int>(off / 64);
444 room.SetWaterFillTile(x, y, true);
445 }
446
447 // A passive legacy import is pending migration only when this project is
448 // allowed to write the WaterFill table. Otherwise retain the overlay as
449 // read-only reference data without creating unsavable room dirtiness.
450 if (!legacy_imported || !water_fill_save_enabled) {
451 room.ClearWaterFillDirty();
452 }
453 }
454
455 if (legacy_imported && dependencies_.toast_manager) {
457 water_fill_save_enabled
458 ? "Imported legacy water gate zones (save to write new table)"
459 : "Loaded legacy water gate zones read-only (WaterFill saving is "
460 "disabled)",
462 }
463}
464
468 }
470 const bool rom_changed = dependencies_.rom && dependencies_.rom != rom_;
471 if (rom_changed) {
473 // The system owns ROM-backed views; ensure it matches the current ROM.
475 }
478 if (game_data_) {
480 }
481 }
482
483 // Setup docking class for room windows
484 room_window_class_.DockingAllowUnclassed = true;
485 room_window_class_.DockingAlwaysTabBar = true;
486
488 return;
489 auto* window_manager = dependencies_.window_manager;
490
491 // Legacy panel IDs persisted in older layouts/settings.
492 window_manager->RegisterPanelAlias("dungeon.object_tools", kObjectSelectorId);
493 window_manager->RegisterPanelAlias("dungeon.entrances",
494 "dungeon.entrance_properties");
495
496 // Register panels with WorkspaceWindowManager (no boolean flags - visibility is
497 // managed entirely by WorkspaceWindowManager::ShowPanel/HidePanel/IsPanelVisible)
498 window_manager->RegisterPanel(
499 {.card_id = "dungeon.workbench",
500 .display_name = "Dungeon Workbench",
501 .window_title = " Dungeon Workbench",
502 .icon = ICON_MD_WORKSPACES,
503 .category = "Dungeon",
504 .shortcut_hint = "Ctrl+Shift+W",
505 .visibility_flag = nullptr,
506 .priority = 5,
507 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
508 .disabled_tooltip = "Load a ROM to edit dungeon rooms"});
509
510 window_manager->RegisterPanel(
511 {.card_id = kRoomSelectorId,
512 .display_name = "Room List",
513 .window_title = " Room List",
514 .icon = ICON_MD_LIST,
515 .category = "Dungeon",
516 .shortcut_hint = "Ctrl+Shift+R",
517 .visibility_flag = nullptr,
518 .priority = 20,
519 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
520 .disabled_tooltip = "Load a ROM to browse dungeon rooms"});
521
522 window_manager->RegisterPanel(
523 {.card_id = kEntranceListId,
524 .display_name = "Entrance List",
525 .window_title = " Entrance List",
526 .icon = ICON_MD_DOOR_FRONT,
527 .category = "Dungeon",
528 .shortcut_hint = "Ctrl+Shift+E",
529 .visibility_flag = nullptr,
530 .priority = 25,
531 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
532 .disabled_tooltip = "Load a ROM to browse dungeon entrances"});
533
534 window_manager->RegisterPanel(
535 {.card_id = "dungeon.entrance_properties",
536 .display_name = "Entrance Properties",
537 .window_title = " Entrance Properties",
538 .icon = ICON_MD_TUNE,
539 .category = "Dungeon",
540 .shortcut_hint = "",
541 .visibility_flag = nullptr,
542 .priority = 26,
543 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
544 .disabled_tooltip = "Load a ROM to edit entrance properties"});
545
546 window_manager->RegisterPanel(
547 {.card_id = kRoomMatrixId,
548 .display_name = "Room Matrix",
549 .window_title = " Room Matrix",
550 .icon = ICON_MD_GRID_VIEW,
551 .category = "Dungeon",
552 .shortcut_hint = "Ctrl+Shift+M",
553 .visibility_flag = nullptr,
554 .priority = 30,
555 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
556 .disabled_tooltip = "Load a ROM to view the room matrix"});
557
558 window_manager->RegisterPanel(
559 {.card_id = kRoomGraphicsId,
560 .display_name = "Room Graphics",
561 .window_title = " Room Graphics",
562 .icon = ICON_MD_IMAGE,
563 .category = "Dungeon",
564 .shortcut_hint = "Ctrl+Shift+G",
565 .visibility_flag = nullptr,
566 .priority = 50,
567 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
568 .disabled_tooltip = "Load a ROM to view room graphics"});
569
570 window_manager->RegisterPanel(
571 {.card_id = kObjectSelectorId,
572 .display_name = "Object Selector",
573 .window_title = " Object Selector",
574 .icon = ICON_MD_CATEGORY,
575 .category = "Dungeon",
576 .shortcut_hint = "",
577 .visibility_flag = nullptr,
578 .priority = 60,
579 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
580 .disabled_tooltip = "Load a ROM to browse dungeon objects"});
581
582 window_manager->RegisterPanel(
583 {.card_id = kDoorEditorId,
584 .display_name = "Door Editor",
585 .window_title = " Door Editor",
586 .icon = ICON_MD_DOOR_FRONT,
587 .category = "Dungeon",
588 .shortcut_hint = "",
589 .visibility_flag = nullptr,
590 .priority = 69,
591 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
592 .disabled_tooltip = "Load a ROM to edit dungeon doors"});
593
594 window_manager->RegisterPanel(
595 {.card_id = kPaletteEditorId,
596 .display_name = "Palette Editor",
597 .window_title = " Palette Editor",
598 .icon = ICON_MD_PALETTE,
599 .category = "Dungeon",
600 // Avoid conflicting with the global Command Palette (Ctrl/Cmd+Shift+P).
601 .shortcut_hint = "Ctrl+Shift+Alt+P",
602 .visibility_flag = nullptr,
603 .priority = 70,
604 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
605 .disabled_tooltip = "Load a ROM to edit dungeon palettes"});
606
607 // Show default panels on startup.
608 // Workbench mode intentionally suppresses parallel standalone defaults to
609 // avoid two competing workflows being open at once.
611 /*show_toast=*/false);
612
613 // Wire intent-aware callback for double-click / context menu
615 [this](int room_id, RoomSelectionIntent intent) {
616 OnRoomSelected(room_id, intent);
617 });
618
619 // Register WindowContent instances
620 window_manager->RegisterWindowContent(std::make_unique<RoomBrowserContent>(
621 &room_selector_, [this](int room_id) { OnRoomSelected(room_id); }));
622
623 window_manager->RegisterWindowContent(
624 std::make_unique<DungeonEntranceListPanel>(
626 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
627
628 {
629 auto matrix_panel = std::make_unique<RoomMatrixContent>(
631 [this](int room_id) { OnRoomSelected(room_id); },
632 [this](int old_room, int new_room) {
633 SwapRoomInPanel(old_room, new_room);
634 },
635 &rooms_);
636 matrix_panel->SetRoomIntentCallback(
637 [this](int room_id, RoomSelectionIntent intent) {
638 OnRoomSelected(room_id, intent);
639 });
640 window_manager->RegisterWindowContent(std::move(matrix_panel));
641 }
642
643 {
644 auto workbench = std::make_unique<DungeonWorkbenchContent>(
646 [this](int room_id) { OnRoomSelected(room_id); },
647 [this](int room_id, RoomSelectionIntent intent) {
648 OnRoomSelected(room_id, intent);
649 },
650 [this](int room_id) {
651 auto status = SaveRoom(room_id);
652 if (!status.ok()) {
653 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
654 status.message().data());
657 absl::StrFormat("Apply Room failed: %s", status.message()),
659 }
660 return;
661 }
663 dependencies_.toast_manager->Show("Applied room to ROM buffer",
665 }
666 },
667 [this]() { SaveAllRooms(); }, [this]() { return GetWorkbenchViewer(); },
668 [this](int room_id) { return GetWorkbenchCompareViewer(room_id); },
669 [this]() -> const std::deque<int>& { return recent_rooms_; },
670 [this](int room_id) {
671 recent_rooms_.erase(
672 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
673 recent_rooms_.end());
674 },
675 [this](bool enabled) { QueueWorkbenchWorkflowMode(enabled); }, rom_);
676 workbench_panel_ = workbench.get();
678 [this]() { return undo_manager_.CanUndo(); },
679 [this]() { return undo_manager_.CanRedo(); },
680 [this]() { Undo().IgnoreError(); }, [this]() { Redo().IgnoreError(); },
681 [this]() { return undo_manager_.GetUndoDescription(); },
682 [this]() { return undo_manager_.GetRedoDescription(); },
683 [this]() { return static_cast<int>(undo_manager_.UndoStackSize()); });
684 workbench_panel_->SetOnInspectorSideChanged([this](bool on_left) {
687 : "right");
688 dependencies_.user_settings->Save().IgnoreError();
689 }
690 });
692 return game_data_ ? &game_data_->pit_damage_table : nullptr;
693 });
694 window_manager->RegisterWindowContent(std::move(workbench));
695 }
696
697 window_manager->RegisterWindowContent(std::make_unique<DungeonEntrancesPanel>(
699 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
700
701 // Note: RoomGraphicsContent and PaletteEditorContent are registered
702 // in Load() after their dependencies (renderer_, palette_editor_) are initialized
703}
704
705absl::Status DungeonEditorV2::Load() {
706 if (!rom_ || !rom_->is_loaded()) {
707 return absl::FailedPreconditionError("ROM not loaded");
708 }
709
710 // Initialize ObjectDimensionTable so DimensionService's fallback path works.
711 // DimensionService::Get() queries this table internally but has no Load API.
712 auto& dim_table = zelda3::ObjectDimensionTable::Get();
713 if (!dim_table.IsLoaded()) {
714 RETURN_IF_ERROR(dim_table.LoadFromRom(rom_));
715 }
716
719
720 if (!game_data()) {
721 return absl::FailedPreconditionError("GameData not available");
722 }
723 auto dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
724 current_palette_ = dungeon_main_pal_group[current_palette_group_id_];
727
733 [this](int room_id) { OnRoomSelected(room_id); });
735 [this](int room_id, RoomSelectionIntent intent) {
736 OnRoomSelected(room_id, intent);
737 });
739 [this](int entrance_id) { OnEntranceSelected(entrance_id); });
740
741 // Canvas viewers are lazily created in GetViewerForRoom
742
743 if (game_data()) {
745 } else {
747 }
748
751
752 // Register panels that depend on initialized state (renderer, palette_editor_)
754 auto graphics_panel = std::make_unique<RoomGraphicsContent>(
756 room_graphics_panel_ = graphics_panel.get();
758 std::move(graphics_panel));
759 auto palette_panel =
760 std::make_unique<PaletteEditorContent>(&palette_editor_);
761 palette_editor_panel_ = palette_panel.get();
763 std::move(palette_panel));
764 }
765
766 dungeon_editor_system_ = std::make_unique<zelda3::DungeonEditorSystem>(rom_);
767 (void)dungeon_editor_system_->Initialize();
769
770 // Initialize browse-only object selector and the dedicated object editor.
771 auto object_selector = std::make_unique<ObjectSelectorContent>(
772 rom_, nullptr, dungeon_editor_system_->GetObjectEditor(),
774 auto object_editor = std::make_unique<ObjectEditorContent>(
775 dungeon_editor_system_->GetObjectEditor());
776
777 // Wire up object change callback to trigger room re-rendering
778 dungeon_editor_system_->GetObjectEditor()->SetObjectChangedCallback(
779 [this](size_t /*object_index*/, const zelda3::RoomObject& /*object*/) {
780 if (current_room_id_ >= 0 && current_room_id_ < (int)rooms_.size()) {
781 rooms_[current_room_id_].RenderRoomGraphics();
782 }
783 });
784
785 // Set rooms and initial palette group for correct preview rendering
786 object_selector->SetRooms(&rooms_);
787 object_selector->SetCurrentPaletteGroup(current_palette_group_);
788
789 auto open_workbench_selection_inspector = [this]() {
790 if (workbench_panel_) {
792 }
793 OpenWindow("dungeon.workbench");
794 };
795
796 object_selector->SetOpenObjectEditorCallback(
797 open_workbench_selection_inspector);
798
799 // Keep raw pointers for later access
800 object_selector_panel_ = object_selector.get();
801 object_editor_content_ = object_editor.get();
802
803 auto jump_to_reciprocal_door = [this](int neighbor_room_id,
804 size_t door_index) {
805 if (neighbor_room_id < 0 ||
806 neighbor_room_id >= static_cast<int>(rooms_.size())) {
807 return;
808 }
809 OnRoomSelected(neighbor_room_id, true);
810 if (auto* viewer = GetViewerForRoom(neighbor_room_id)) {
811 viewer->object_interaction()
812 .entity_coordinator()
813 .door_handler()
814 .SelectDoor(door_index);
815 }
816 };
817 object_editor->SetJumpToReciprocalDoorCallback(jump_to_reciprocal_door);
818 owned_object_editor_content_ = std::move(object_editor);
819
820 auto door_editor = std::make_unique<DoorEditorContent>();
821 door_editor->SetRooms(&rooms_);
822 door_editor->SetOpenSelectionInspectorCallback(
823 open_workbench_selection_inspector);
824 door_editor->SetJumpToReciprocalDoorCallback(jump_to_reciprocal_door);
825 door_editor_panel_ = door_editor.get();
826
827 // Propagate game_data to the object editor panel if available
828 if (game_data()) {
830 }
835 }
836
837 // Room-scoped utility tools are embedded in the Workbench drawer by default,
838 // but the same WindowContent instances remain available in the explicit
839 // Window workflow. Workbench mode closes and hides these entries so they do
840 // not compete with the integrated drawer.
841 auto custom_collision_panel =
842 std::make_unique<CustomCollisionPanel>(nullptr, nullptr);
843 custom_collision_panel_ = custom_collision_panel.get();
844
845 auto water_fill_panel = std::make_unique<WaterFillPanel>(nullptr, nullptr);
846 water_fill_panel_ = water_fill_panel.get();
847
848 auto room_tag_panel = std::make_unique<RoomTagEditorPanel>();
849 room_tag_panel->SetProject(dependencies_.project);
850 room_tag_panel->SetRooms(&rooms_);
851 room_tag_panel->SetCurrentRoomId(current_room_id_);
852 room_tag_editor_panel_ = room_tag_panel.get();
853
854 std::unique_ptr<MinecartTrackEditorPanel> minecart_panel;
855 if (core::FeatureFlags::get().kEnableCustomObjects) {
856 minecart_panel = std::make_unique<MinecartTrackEditorPanel>();
857 minecart_track_editor_panel_ = minecart_panel.get();
858
865 [this](int room_id) { OnRoomSelected(room_id); });
866 }
868 } else {
870 }
871
872 if (core::FeatureFlags::get().kEnableCustomObjects && dependencies_.project) {
873 // Initialize custom object manager with project-configured path.
878 } else {
879 // Avoid inheriting stale singleton state from previous projects.
881 }
882 }
883
884 // Register the ObjectSelectorContent directly (it inherits from WindowContent)
885 // Panel manager takes ownership
888 std::move(object_selector));
889 dependencies_.window_manager->RegisterWindowContent(std::move(door_editor));
890
891 // Register sprite and item editor panels with canvas viewer = nullptr
892 // They will get the viewer reference in OnRoomSelected when a room is selected
893 auto sprite_panel = std::make_unique<SpriteEditorPanel>(&current_room_id_,
894 &rooms_, nullptr);
895 sprite_panel->SetOpenSelectionInspectorCallback(
896 open_workbench_selection_inspector);
897 sprite_editor_panel_ = sprite_panel.get();
899 std::move(sprite_panel));
900
901 auto item_panel =
902 std::make_unique<ItemEditorPanel>(&current_room_id_, &rooms_, nullptr);
903 item_panel->SetOpenSelectionInspectorCallback(
904 open_workbench_selection_inspector);
905 item_editor_panel_ = item_panel.get();
906 dependencies_.window_manager->RegisterWindowContent(std::move(item_panel));
907
909 std::move(custom_collision_panel));
911 std::move(water_fill_panel));
913 std::move(room_tag_panel));
914 if (minecart_panel) {
916 std::move(minecart_panel));
917 }
918
919 // Object Tile Editor Panel
920 {
921 auto tile_editor_panel =
922 std::make_unique<ObjectTileEditorPanel>(renderer_, rom_);
923 tile_editor_panel->SetCurrentPaletteGroup(current_palette_group_);
924 tile_editor_panel->SetStandardWritePreflightCallback(
925 [this](const std::vector<std::pair<uint32_t, uint32_t>>& ranges) {
926 if (ranges.empty() || dependencies_.project == nullptr ||
928 return absl::OkStatus();
929 }
933 "dungeon object tile data", "ObjectTileEditorPanel",
935 });
936 tile_editor_panel->SetStandardTilesAppliedCallback([this]() {
939 }
940 });
941
942 // Wire creation callback: when a new custom object is saved,
943 // register it with the manager, persist to project, and refresh UI.
944 tile_editor_panel->SetObjectCreatedCallback(
945 [this](int object_id, const std::string& filename) {
947 filename);
949 dependencies_.project->custom_object_files[object_id].push_back(
950 filename);
951 (void)dependencies_.project->Save();
952 }
956 }
957 });
958
959 object_tile_editor_panel_ = tile_editor_panel.get();
961 std::move(tile_editor_panel));
962 }
963
964 // Wire tile editor panel and project references to the object selector
969 [this]() {
970 return dependencies_.window_manager != nullptr &&
971 object_tile_editor_panel_ != nullptr &&
974 });
978 }
979 }
980
981 // Overlay Manager Panel
982 {
983 auto overlay_panel = std::make_unique<OverlayManagerPanel>();
984 overlay_manager_panel_ = overlay_panel.get();
986 std::move(overlay_panel));
987 }
988
989 } else {
990 owned_object_selector_panel_ = std::move(object_selector);
991 owned_door_editor_panel_ = std::move(door_editor);
992 owned_custom_collision_panel_ = std::move(custom_collision_panel);
993 owned_water_fill_panel_ = std::move(water_fill_panel);
994 owned_room_tag_editor_panel_ = std::move(room_tag_panel);
995 owned_minecart_track_editor_panel_ = std::move(minecart_panel);
996 }
997
999 [this](gui::DungeonPaletteChange change) {
1001 });
1003
1004 // Oracle of Secrets: load editor-authored water fill zones (best-effort).
1006
1007 if (workbench_panel_) {
1014 }
1015
1016 is_loaded_ = true;
1017 return absl::OkStatus();
1018}
1019
1021 if (palette_listener_id_ >= 0) {
1022 return;
1023 }
1024
1026 [this](const std::string& group_name, int palette_index) {
1027 HandleArenaPaletteChanged(group_name, palette_index);
1028 });
1029}
1030
1031void DungeonEditorV2::HandleArenaPaletteChanged(const std::string& group_name,
1032 int palette_index) {
1033 if (!gfx::PaletteManager::Get().IsManaging(game_data_)) {
1034 return;
1035 }
1036
1037 if (group_name == "hud") {
1040 } else if (group_name == "dungeon_main") {
1043 }
1044}
1045
1049
1050 std::array<bool, zelda3::kNumberOfRooms> rendered_rooms{};
1051 auto render_once = [this, &rendered_rooms](int room_id) {
1052 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()) ||
1053 room_id >= static_cast<int>(rendered_rooms.size()) ||
1054 rendered_rooms[room_id]) {
1055 return;
1056 }
1057 rooms_[room_id].RenderRoomGraphics();
1058 rendered_rooms[room_id] = true;
1059 };
1060
1061 for (int i = 0; i < active_rooms_.Size; i++) {
1062 render_once(active_rooms_[i]);
1063 }
1064
1065 render_once(current_room_id_);
1066
1067 auto apply_palette = [](DungeonCanvasViewer* viewer, uint64_t palette_id,
1068 const gfx::PaletteGroup& palette_group,
1069 bool force_shared_palette_refresh) {
1070 if (!viewer) {
1071 return;
1072 }
1073 viewer->SetCurrentPaletteId(palette_id);
1074 viewer->SetCurrentPaletteGroup(
1075 palette_group,
1076 /*force_refresh=*/force_shared_palette_refresh &&
1078 };
1079
1080 if (current_room_id_ >= 0 &&
1081 current_room_id_ < static_cast<int>(rooms_.size()) && game_data()) {
1082 const bool force_shared_palette_refresh =
1084 auto& dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
1086 static_cast<uint64_t>(dungeon_main_pal_group.size())) {
1088 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1089 if (auto pal_group =
1091 pal_group.ok()) {
1092 current_palette_group_ = pal_group.value();
1093 apply_palette(workbench_viewer_.get(), current_palette_id_,
1094 current_palette_group_, force_shared_palette_refresh);
1096 current_palette_group_, force_shared_palette_refresh);
1097 room_viewers_.ForEach(
1098 [this, change, &apply_palette, &dungeon_main_pal_group,
1099 &render_once, force_shared_palette_refresh](
1100 int room_id, std::unique_ptr<DungeonCanvasViewer>& viewer) {
1101 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1102 return;
1103 }
1104 const int palette_id = rooms_[room_id].ResolveDungeonPaletteId();
1105 if (palette_id < 0 ||
1106 palette_id >=
1107 static_cast<int>(dungeon_main_pal_group.size())) {
1108 return;
1109 }
1110 if (change.source ==
1112 change.palette_id >= 0 && palette_id != change.palette_id) {
1113 return;
1114 }
1115 auto room_palette_group = gfx::CreatePaletteGroupFromLargePalette(
1116 dungeon_main_pal_group.palette_ref(palette_id));
1117 if (room_palette_group.ok()) {
1118 if (viewer && viewer->object_interaction().IsObjectLoaded()) {
1119 render_once(room_id);
1120 }
1121 apply_palette(viewer.get(), palette_id,
1122 room_palette_group.value(),
1123 force_shared_palette_refresh);
1124 }
1125 });
1129 }
1132 }
1136 }
1137 }
1138 }
1139 }
1140}
1141
1145 change.palette_id < 0) {
1147 [](int, zelda3::Room& room) { room.MarkGraphicsDirty(); });
1148 return;
1149 }
1150
1151 // Editing palette bytes does not change a room header, so the room's normal
1152 // property cache cannot detect this dependency change. Mark every cached
1153 // room that resolves to the edited concrete palette; inactive rooms will
1154 // redraw lazily the next time they are shown.
1155 rooms_.ForEachMaterialized([change](int, zelda3::Room& room) {
1156 if (room.ResolveDungeonPaletteId() == change.palette_id) {
1157 room.MarkGraphicsDirty();
1158 }
1159 });
1160}
1161
1164
1165 // Mirror the workbench inspector to the LEFT when user_settings says so.
1166 // Persisted across sessions via UserSettings::dungeon_inspector_side.
1170 }
1171
1172 const auto& theme = AgentUI::GetTheme();
1173 if (room_window_class_.ClassId == 0) {
1174 room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
1175 }
1176
1177 if (!is_loaded_) {
1178 gui::PanelWindow loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);
1179 loading_card.SetDefaultSize(400, 200);
1180 if (loading_card.Begin()) {
1181 ImGui::TextColored(theme.text_secondary_gray,
1182 tr("Loading dungeon data..."));
1183 ImGui::TextWrapped(
1184 tr("Independent editor cards will appear once ROM data is loaded."));
1185 }
1186 loading_card.End();
1187 return absl::OkStatus();
1188 }
1189
1190 if (!IsWorkbenchWorkflowEnabled() || active_rooms_.Size > 0) {
1192 }
1193
1194 // Keyboard Shortcuts (only if not typing in a text field)
1195 if (!ImGui::GetIO().WantTextInput) {
1196 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
1197 // Delegate delete to current room viewer.
1198 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
1199 viewer->DeleteSelectedObjects();
1200 }
1201 }
1202
1203 if (ImGui::GetIO().KeyCtrl && ImGui::GetIO().KeyShift &&
1204 ImGui::IsKeyPressed(ImGuiKey_W, false)) {
1206 }
1207
1208 // Room Cycling (Ctrl+Tab)
1209 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && ImGui::GetIO().KeyCtrl) {
1211 if (recent_rooms_.size() > 1) {
1212 int current_idx = -1;
1213 for (int i = 0; i < static_cast<int>(recent_rooms_.size()); ++i) {
1214 if (recent_rooms_[i] == current_room_id_) {
1215 current_idx = i;
1216 break;
1217 }
1218 }
1219 if (current_idx != -1) {
1220 int next_idx;
1221 if (ImGui::GetIO().KeyShift) {
1222 next_idx =
1223 (current_idx + 1) % static_cast<int>(recent_rooms_.size());
1224 } else {
1225 next_idx =
1226 (current_idx - 1 + static_cast<int>(recent_rooms_.size())) %
1227 static_cast<int>(recent_rooms_.size());
1228 }
1229 OnRoomSelected(recent_rooms_[next_idx]);
1230 }
1231 }
1232 } else if (active_rooms_.size() > 1) {
1233 int current_idx = -1;
1234 for (int i = 0; i < active_rooms_.size(); ++i) {
1235 if (active_rooms_[i] == current_room_id_) {
1236 current_idx = i;
1237 break;
1238 }
1239 }
1240
1241 if (current_idx != -1) {
1242 int next_idx;
1243 if (ImGui::GetIO().KeyShift) {
1244 next_idx =
1245 (current_idx - 1 + active_rooms_.size()) % active_rooms_.size();
1246 } else {
1247 next_idx = (current_idx + 1) % active_rooms_.size();
1248 }
1249 OnRoomSelected(active_rooms_[next_idx]);
1250 }
1251 }
1252 }
1253
1254 // Adjacent Room Navigation (Ctrl+Arrows)
1255 if (ImGui::GetIO().KeyCtrl) {
1256 int next_room = -1;
1257 const int kCols = 16;
1258
1259 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
1260 if (current_room_id_ >= kCols)
1261 next_room = current_room_id_ - kCols;
1262 } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
1263 if (IsValidRoomId(current_room_id_ + kCols))
1264 next_room = current_room_id_ + kCols;
1265 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
1266 if (current_room_id_ % kCols > 0)
1267 next_room = current_room_id_ - 1;
1268 } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
1269 if (current_room_id_ % kCols < kCols - 1 &&
1271 next_room = current_room_id_ + 1;
1272 }
1273
1274 if (next_room != -1) {
1276 OnRoomSelected(next_room, /*request_focus=*/false);
1277 } else {
1279 }
1280 }
1281 }
1282 }
1283
1284 // Process any pending room swaps after all drawing is complete
1285 // This prevents ImGui state corruption from modifying collections mid-frame
1287
1288 return absl::OkStatus();
1289}
1290
1292 if (!status_bar)
1293 return;
1294 const int room_id = current_room_id();
1295
1296 if (room_id >= 0) {
1297 StatusBarSegmentOptions room_opts;
1298 room_opts.tooltip = "Click to refocus viewer on this room";
1299 room_opts.on_click = [this, room_id]() {
1303 }
1304 };
1305 status_bar->SetCustomSegment("Room", absl::StrFormat("0x%03X", room_id),
1306 std::move(room_opts));
1307 }
1308
1309 const int loaded = LoadedRoomCount();
1310 const int total = TotalRoomCount();
1311 if (total > 0) {
1312 StatusBarSegmentOptions rooms_opts;
1313 rooms_opts.tooltip =
1314 absl::StrFormat("%d rooms loaded of %d total", loaded, total);
1315 status_bar->SetCustomSegment("Rooms",
1316 absl::StrFormat("%d/%d", loaded, total),
1317 std::move(rooms_opts));
1318 }
1319
1320 if (room_id >= 0 && CurrentRoomHasPendingChanges()) {
1321 StatusBarSegmentOptions current_room_opts;
1322 current_room_opts.tooltip =
1323 "This room has pending editor changes that are not yet applied to the "
1324 "ROM buffer.";
1325 status_bar->SetCustomSegment("RoomState", "Pending",
1326 std::move(current_room_opts));
1327 }
1328
1329 StatusBarSegmentOptions mode_opts;
1330 mode_opts.tooltip = "Click to toggle Workbench / Standalone workflow";
1331 mode_opts.on_click = [this]() {
1333 };
1337 std::move(mode_opts));
1338}
1339
1341 return rooms_.LoadedCount();
1342}
1343
1345 int count = 0;
1346 rooms_.ForEachMaterialized([&count](int, const zelda3::Room& room) {
1347 if (room.HasUnsavedChanges()) {
1348 ++count;
1349 }
1350 });
1351 return count;
1352}
1353
1355 return PendingRoomCount() > 0;
1356}
1357
1359 if (minecart_track_editor_panel_ != nullptr &&
1361 return true;
1362 }
1363 if (object_tile_editor_panel_ != nullptr &&
1365 return true;
1366 }
1367 if (HasPendingRoomChanges()) {
1368 return true;
1369 }
1370 if (std::any_of(entrances_.begin(), entrances_.end(),
1371 [](const zelda3::RoomEntrance& entrance) {
1372 return entrance.dirty();
1373 })) {
1374 return true;
1375 }
1376 if (std::any_of(spawn_points_.begin(), spawn_points_.end(),
1377 [](const zelda3::DungeonSpawnPoint& spawn) {
1378 return spawn.dirty();
1379 })) {
1380 return true;
1381 }
1382 return game_data_ != nullptr && game_data_->pit_damage_table.dirty();
1383}
1384
1389
1391 return minecart_track_editor_panel_ != nullptr
1393 : absl::OkStatus();
1394}
1395
1397 if (current_room_id_ < 0 ||
1398 current_room_id_ >= static_cast<int>(rooms_.size())) {
1399 return false;
1400 }
1401 if (const auto* room = rooms_.GetIfMaterialized(current_room_id_)) {
1402 return room->HasUnsavedChanges();
1403 }
1404 return false;
1405}
1406
1408 if (!core::FeatureFlags::get().dungeon.kUseWorkbench) {
1409 return false;
1410 }
1412 return true;
1413 }
1414 return dependencies_.window_manager->IsWindowOpen("dungeon.workbench");
1415}
1416
1417void DungeonEditorV2::SetWorkbenchWorkflowMode(bool enabled, bool show_toast) {
1418 auto* window_manager = dependencies_.window_manager;
1419 if (!window_manager) {
1420 return;
1421 }
1422
1423 const size_t session_id = window_manager->GetActiveSessionId();
1424 const bool was_enabled = IsWorkbenchWorkflowEnabled();
1425
1426 if (enabled) {
1427 window_manager->OpenWindow(session_id, "dungeon.workbench");
1428
1429 // Workbench-local tool windows (Object/Door/Sprite/Item Selector, Palette,
1430 // Room Graphics/Tags, Custom Collision, Water Fill, Minecart) are NOT
1431 // auto-closed on Workbench entry. The Workbench drawer embeds the same
1432 // tools, but users may also keep a standalone window open if they prefer
1433 // a multi-window layout. Only navigation windows (room selector/matrix
1434 // and per-room windows) are collapsed when entering Workbench mode.
1435 for (const auto& descriptor :
1436 window_manager->GetWindowsInCategory(session_id, "Dungeon")) {
1437 const std::string& card_id = descriptor.card_id;
1438 if (card_id == "dungeon.workbench") {
1439 continue;
1440 }
1441 if (window_manager->IsWindowPinned(session_id, card_id)) {
1442 continue;
1443 }
1444 const bool is_room_window = IsTransientDungeonRoomWindowId(card_id);
1445 if (card_id == kRoomSelectorId || card_id == kRoomMatrixId ||
1446 is_room_window) {
1447 window_manager->CloseWindow(session_id, card_id);
1448 }
1449 }
1450 } else {
1451 window_manager->CloseWindow(session_id, "dungeon.workbench");
1452 window_manager->OpenWindow(session_id, kRoomSelectorId);
1453 window_manager->OpenWindow(session_id, kRoomMatrixId);
1454 if (current_room_id_ >= 0) {
1456 }
1457 }
1458
1459 if (show_toast && dependencies_.toast_manager && was_enabled != enabled) {
1461 enabled ? "Dungeon workflow: Workbench"
1462 : "Dungeon workflow: Standalone Panels",
1464 }
1465}
1466
1468 bool show_toast) {
1472}
1473
1477
1479 for (int i = 0; i < active_rooms_.Size; i++) {
1480 int room_id = active_rooms_[i];
1481 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1482 bool panel_visible = true;
1484 panel_visible = dependencies_.window_manager->IsWindowOpen(card_id);
1485 }
1486
1487 if (!panel_visible) {
1489 room_cards_.erase(room_id);
1490 active_rooms_.erase(active_rooms_.Data + i);
1491 ReleaseRoomPanelSlotId(room_id);
1492 // Clean up viewer
1493 room_viewers_.Erase(room_id);
1494 i--;
1495 continue;
1496 }
1497
1498 bool is_pinned = dependencies_.window_manager &&
1500 std::string active_category =
1503 : "";
1504
1505 if (active_category != "Dungeon" && !is_pinned) {
1506 continue;
1507 }
1508
1509 // Ensure room card exists (should have been created by ShowRoomPanel/ShowPanel)
1510 if (room_cards_.find(room_id) == room_cards_.end()) {
1511 ShowRoomPanel(room_id);
1512 }
1513
1514 auto& room_card = room_cards_[room_id];
1515 bool open = true;
1516
1517 ImGui::SetNextWindowClass(&room_window_class_);
1518 if (room_dock_id_ == 0) {
1519 room_dock_id_ = ImGui::GetID("DungeonRoomDock");
1520 }
1521 ImGui::SetNextWindowDockID(room_dock_id_, ImGuiCond_FirstUseEver);
1522
1523 if (room_card->Begin(&open)) {
1524 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) {
1525 OnRoomSelected(room_id, /*request_focus=*/false);
1526 }
1527 DrawRoomTab(room_id);
1528 }
1529 room_card->End();
1530
1531 if (!open) {
1534 }
1535
1536 room_cards_.erase(room_id);
1537 active_rooms_.erase(active_rooms_.Data + i);
1538 room_viewers_.Erase(room_id);
1539 ReleaseRoomPanelSlotId(room_id);
1540 i--;
1541 }
1542 }
1543}
1544
1546 if (auto it = room_panel_slot_ids_.find(room_id);
1547 it != room_panel_slot_ids_.end()) {
1548 return it->second;
1549 }
1550 const int slot_id = next_room_panel_slot_id_++;
1551 room_panel_slot_ids_[room_id] = slot_id;
1552 return slot_id;
1553}
1554
1556 room_panel_slot_ids_.erase(room_id);
1557}
1558
1560 const auto& theme = AgentUI::GetTheme();
1561 if (room_id < 0 || room_id >= 0x128) {
1562 ImGui::Text(tr("Invalid room ID: %d"), room_id);
1563 return;
1564 }
1565
1566 auto& room = rooms_[room_id];
1567
1568 if (!room.IsLoaded()) {
1569 auto status = room_loader_.LoadRoom(room_id, room);
1570 if (!status.ok()) {
1571 ImGui::TextColored(theme.text_error_red, tr("Failed to load room: %s"),
1572 status.message().data());
1573 return;
1574 }
1575
1577 auto sys_status = dungeon_editor_system_->ReloadRoom(room_id);
1578 if (!sys_status.ok()) {
1579 LOG_ERROR("DungeonEditorV2", "Failed to load system data: %s",
1580 sys_status.message().data());
1581 }
1582 }
1583 }
1584
1585 if (room.IsLoaded()) {
1586 bool needs_render = false;
1587
1588 if (room.blocks().empty()) {
1590 room.LoadRoomGraphics();
1591 needs_render = true;
1592 }
1593
1594 if (!room.AreObjectsLoaded()) {
1595 room.LoadObjects();
1596 needs_render = true;
1597 }
1598
1599 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1600 if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
1601 room.RenderRoomGraphics();
1602 }
1603 }
1604
1605 if (room.IsLoaded()) {
1606 ImGui::TextColored(theme.text_success_green, ICON_MD_CHECK " Loaded");
1607 } else {
1608 ImGui::TextColored(theme.text_error_red, ICON_MD_PENDING " Not Loaded");
1609 }
1610 ImGui::SameLine();
1611 ImGui::TextDisabled(tr("Objects: %zu"), room.GetTileObjects().size());
1612
1613 if (core::FeatureFlags::get().dungeon.kUseWorkbench &&
1615 ImGui::SameLine();
1616 const std::string return_label = absl::StrFormat(
1617 ICON_MD_WORKSPACES " Workbench##return_workbench_%03X", room_id);
1618 if (ImGui::SmallButton(return_label.c_str())) {
1620 }
1621 if (ImGui::IsItemHovered()) {
1622 ImGui::SetTooltip(
1623 tr("Switch back to the integrated Dungeon Workbench workflow "
1624 "(Ctrl+Shift+W)."));
1625 }
1626 }
1627
1628 // Warp to Room button — sends room ID to running Mesen2 emulator
1629 ImGui::SameLine();
1630 {
1632 bool connected = client && client->IsConnected();
1633 if (!connected) {
1634 ImGui::BeginDisabled();
1635 }
1636 std::string warp_label =
1637 absl::StrFormat(ICON_MD_ROCKET_LAUNCH " Warp##warp_%03X", room_id);
1638 if (ImGui::SmallButton(warp_label.c_str())) {
1639 auto status = client->WriteWord(0x7E00A0, static_cast<uint16_t>(room_id));
1640 if (status.ok()) {
1643 absl::StrFormat("Warped to room 0x%03X", room_id),
1645 }
1646 } else {
1649 absl::StrFormat("Warp failed: %s", status.message()),
1651 }
1652 }
1653 }
1654 if (!connected) {
1655 ImGui::EndDisabled();
1656 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1657 ImGui::SetTooltip(tr("Connect to Mesen2 first"));
1658 }
1659 } else {
1660 if (ImGui::IsItemHovered()) {
1661 ImGui::SetTooltip(tr("Warp to room 0x%03X in Mesen2"), room_id);
1662 }
1663 }
1664 }
1665
1666 ImGui::Separator();
1667
1668 // Use per-room viewer; reserve a bottom strip for the shared dungeon
1669 // status bar so this Windows-mode standalone window mirrors the Workbench
1670 // canvas pane's affordances.
1671 if (auto* viewer = GetViewerForRoom(room_id)) {
1672 const float status_bar_reserved =
1673 std::max(ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
1675 ImGui::GetStyle().ItemSpacing.y;
1676 if (ImGui::BeginChild("##StandaloneRoomBody",
1677 ImVec2(0.0f, -status_bar_reserved), false)) {
1678 viewer->DrawDungeonCanvas(room_id);
1679 }
1680 ImGui::EndChild();
1681
1682 bool room_dirty = false;
1683 if (room_id >= 0 && room_id < static_cast<int>(rooms_.size())) {
1684 room_dirty = rooms_[room_id].HasUnsavedChanges();
1685 }
1686 const char* tool_mode =
1687 viewer->object_interaction().mode_manager().GetModeName();
1688 auto status = DungeonStatusBar::BuildState(*viewer, tool_mode, room_dirty);
1689 status.workflow_mode = workflow_mode_names::kStandalone;
1690 status.workflow_primary = false;
1691 status.can_undo = undo_manager_.CanUndo();
1692 status.can_redo = undo_manager_.CanRedo();
1693 status.undo_depth = static_cast<int>(undo_manager_.UndoStackSize());
1694 status.on_undo = [this]() {
1695 Undo().IgnoreError();
1696 };
1697 status.on_redo = [this]() {
1698 Redo().IgnoreError();
1699 };
1700 DungeonStatusBar::Draw(status);
1701 }
1702}
1703
1705 switch (intent) {
1707 OnRoomSelected(room_id, /*request_focus=*/true);
1708 break;
1710 // Explicitly pivot to panel workflow when user asks for standalone room.
1711 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1712 return;
1713 }
1716 }
1717 // Update shared state (same as OnRoomSelected with request_focus=true)
1718 OnRoomSelected(room_id, /*request_focus=*/false);
1719 // Now force-open a standalone panel for this room
1720 ShowRoomPanel(room_id);
1721 break;
1722 }
1724 OnRoomSelected(room_id, /*request_focus=*/false);
1725 break;
1726 }
1727}
1728
1729void DungeonEditorV2::OnRoomSelected(int room_id, bool request_focus) {
1730 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1731 LOG_WARN("DungeonEditorV2", "Ignoring invalid room selection: %d", room_id);
1732 return;
1733 }
1734 if (room_id != current_room_id_ && workbench_panel_) {
1736 }
1737 current_room_id_ = room_id;
1738 room_selector_.set_current_room_id(static_cast<uint16_t>(room_id));
1739
1740 // Track recent rooms (remove if already present, add to front)
1741 recent_rooms_.erase(
1742 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
1743 recent_rooms_.end());
1744 recent_rooms_.push_front(room_id);
1745 if (recent_rooms_.size() > kMaxRecentRooms) {
1746 recent_rooms_.pop_back();
1747 }
1748
1750 dungeon_editor_system_->SetExternalRoom(&rooms_[room_id]);
1751 }
1752
1753 // Update all sub-panels (Object Editor, Sprite Editor, etc.)
1754 SyncPanelsToRoom(room_id);
1755
1756 // Sync palette with current room (must happen before early return for focus changes)
1757
1758 // Sync palette with current room (must happen before early return for focus changes)
1759 if (room_id >= 0 && room_id < (int)rooms_.size()) {
1760 auto& room = rooms_[room_id];
1761 if (!room.IsLoaded()) {
1762 room_loader_.LoadRoom(room_id, room);
1763 }
1764
1765 if (room.IsLoaded()) {
1767 current_palette_id_ = room.ResolveDungeonPaletteId();
1770
1771 // Update viewer and object editor palette
1772 if (auto* viewer = GetViewerForRoom(room_id)) {
1773 ConfigureViewerRenderContext(viewer, room_id);
1774 viewer->SetCurrentPaletteId(current_palette_id_);
1775
1776 if (game_data()) {
1777 auto dungeon_main_pal_group =
1779 if (current_palette_id_ < (int)dungeon_main_pal_group.size()) {
1780 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1781 auto result =
1783 if (result.ok()) {
1784 current_palette_group_ = result.value();
1785 viewer->SetCurrentPaletteGroup(current_palette_group_);
1789 }
1790 // Sync palette to graphics panel for proper sheet coloring
1794 }
1797 room_id, current_palette_group_);
1798 }
1799 }
1800 }
1801 }
1802 }
1803 }
1804 }
1805
1806 // Workbench mode uses a single stable window and does not spawn per-room
1807 // panels. Keep selection + panels in sync and return.
1809 if (dependencies_.window_manager && request_focus) {
1810 // Only force-show if it's already visible or if it's the first initialization
1811 // This avoids obtrusive behavior when the user explicitly closed it.
1812 if (dependencies_.window_manager->IsWindowOpen("dungeon.workbench")) {
1813 dependencies_.window_manager->OpenWindow("dungeon.workbench");
1814 }
1815 }
1816 return;
1817 }
1818
1819 // Check if room is already open
1820 for (int i = 0; i < active_rooms_.Size; i++) {
1821 if (active_rooms_[i] == room_id) {
1822 // Always ensure panel is visible, even if already in active_rooms_
1824 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1826 }
1827 if (request_focus) {
1828 FocusRoom(room_id);
1829 }
1830 return;
1831 }
1832 }
1833
1834 active_rooms_.push_back(room_id);
1836
1838 // Use unified ResourceLabelProvider for room names
1839 std::string room_name = absl::StrFormat(
1840 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
1841
1842 std::string base_card_id = absl::StrFormat("dungeon.room_%d", room_id);
1843
1845 {.card_id = base_card_id,
1846 .display_name = room_name,
1847 .window_title = ICON_MD_GRID_ON " " + room_name,
1848 .icon = ICON_MD_GRID_ON,
1849 .category = "Dungeon",
1850 .shortcut_hint = "",
1851 .visibility_flag = nullptr,
1852 .priority = 200 + room_id});
1853
1855 }
1856}
1857
1859 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
1860 return;
1861 }
1862 current_entrance_id_ = entrance_id;
1864
1865 const int room_id = ResolveEntranceRoomId(entrance_id);
1866 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1867 return;
1868 }
1869 OnRoomSelected(room_id);
1870}
1871
1872int DungeonEditorV2::ResolveEntranceRoomId(int entrance_id) const {
1873 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
1874 return -1;
1875 }
1876 if (entrance_id < zelda3::kNumDungeonSpawnPoints) {
1877 const auto& spawn = spawn_points_[entrance_id];
1878 return spawn.spawn_id() == entrance_id ? spawn.room_id : -1;
1879 }
1880 return entrances_[entrance_id].room_;
1881}
1882
1884 int room_id) const {
1885 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1886 return 0xFF;
1887 }
1888 if (current_entrance_id_ < 0 ||
1889 current_entrance_id_ >= static_cast<int>(entrances_.size())) {
1890 return 0xFF;
1891 }
1893 const auto& spawn = spawn_points_[current_entrance_id_];
1894 if (spawn.spawn_id() != current_entrance_id_ || spawn.room_id != room_id) {
1895 return 0xFF;
1896 }
1897 return spawn.main_gfx;
1898 }
1899
1900 const auto& entrance = entrances_[current_entrance_id_];
1901 if (entrance.room_ != room_id) {
1902 return 0xFF;
1903 }
1904 return entrance.blockset_;
1905}
1906
1908 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1909 return;
1910 }
1911 auto& room = rooms_[room_id];
1912 if (!room.IsLoaded()) {
1913 return;
1914 }
1915 room.SetRenderEntranceBlockset(
1917}
1918
1920 int room_id) {
1921 if (viewer == nullptr) {
1922 return;
1923 }
1924 const uint8_t entrance_blockset =
1926 if (entrance_blockset == 0xFF) {
1928 } else {
1929 viewer->SetEntranceRenderContext(current_entrance_id_, entrance_blockset);
1930 }
1931}
1932
1933void DungeonEditorV2::add_room(int room_id) {
1934 OnRoomSelected(room_id);
1935}
1936
1938 auto it = room_cards_.find(room_id);
1939 if (it != room_cards_.end()) {
1940 it->second->Focus();
1941 }
1942}
1943
1949 OpenWindow("dungeon.workbench");
1950 } else {
1952 }
1953 }
1954}
1955
1957 if (enabled && dependencies_.window_manager) {
1959 OpenWindow("dungeon.workbench");
1960 if (workbench_panel_) {
1962 }
1963 } else {
1967 }
1970 }
1971 }
1972}
1973
1977
1980 LOG_ERROR("DungeonEditorV2", "Cannot place object: Invalid room ID %d",
1982 const std::string message = absl::StrFormat(
1983 "Cannot place 0x%02X: invalid room %d", obj.id_, current_room_id_);
1986 } else if (dependencies_.toast_manager) {
1988 }
1989 return;
1990 }
1991
1992 auto& room = rooms_[current_room_id_];
1993
1994 LOG_INFO("DungeonEditorV2",
1995 "Placing object ID=0x%02X at position (%d,%d) in room %03X", obj.id_,
1996 obj.x_, obj.y_, current_room_id_);
1997
1998 room.RenderRoomGraphics();
1999 LOG_DEBUG("DungeonEditorV2",
2000 "Object placed and room re-rendered successfully");
2001 // Brief success feedback so the user knows the placement was accepted.
2002 // Kept minimal (no inline panel update) — success is non-spammy by design.
2005 absl::StrFormat("Placed 0x%02X in room %03X", obj.id_,
2008 }
2009}
2010
2012 int room_id, const zelda3::RoomObject& object) {
2013 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2014 LOG_WARN("DungeonEditorV2", "Edit Graphics ignored (invalid room id %d)",
2015 room_id);
2016 return;
2017 }
2018
2019 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
2020 if (!editor_manager) {
2021 LOG_WARN("DungeonEditorV2",
2022 "Edit Graphics ignored (editor manager unavailable)");
2023 return;
2024 }
2025
2026 auto& room = rooms_[room_id];
2028 room.LoadRoomGraphics();
2029
2030 uint16_t sheet_id = 0;
2031 uint16_t tile_index = 0;
2032 bool resolved_sheet = false;
2033 if (auto tiles_or = object.GetTiles();
2034 tiles_or.ok() && !tiles_or.value().empty()) {
2035 const uint16_t tile_id = tiles_or.value().front().id_;
2036 const size_t block_index = static_cast<size_t>(tile_id / 64);
2037 const auto blocks = room.blocks();
2038 if (block_index < blocks.size()) {
2039 sheet_id = blocks[block_index];
2040 resolved_sheet = true;
2041 }
2042 const int tiles_per_row = gfx::kTilesheetWidth / 8;
2043 const int tiles_per_col = gfx::kTilesheetHeight / 8;
2044 const int tiles_per_sheet = tiles_per_row * tiles_per_col;
2045 if (tiles_per_sheet > 0) {
2046 tile_index = static_cast<uint16_t>(tile_id % tiles_per_sheet);
2047 }
2048 }
2049
2050 editor_manager->SwitchToEditor(EditorType::kGraphics, true);
2051 if (auto* editor_set = editor_manager->GetCurrentEditorSet()) {
2052 if (auto* graphics = editor_set->GetGraphicsEditor()) {
2053 if (resolved_sheet) {
2054 graphics->SelectSheet(sheet_id);
2055 graphics->HighlightTile(sheet_id, tile_index,
2056 absl::StrFormat("Object 0x%02X", object.id_));
2057 }
2058 }
2059 }
2060
2063 }
2064}
2065
2067 int room_id, const zelda3::RoomObject& object) {
2068 if (object_tile_editor_panel_ == nullptr) {
2069 return absl::FailedPreconditionError(
2070 "Object Tile Editor panel is unavailable");
2071 }
2072 if (dependencies_.window_manager == nullptr) {
2073 return absl::FailedPreconditionError(
2074 "Workspace window manager is unavailable");
2075 }
2076 if (rom_ == nullptr || !rom_->is_loaded()) {
2077 return absl::FailedPreconditionError("ROM not loaded");
2078 }
2079 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2080 return absl::OutOfRangeError("Dungeon room id is outside the room store");
2081 }
2083 return absl::UnimplementedError(
2084 "Object tile editing is supported only for audited objects 0x11F and "
2085 "0x120");
2086 }
2087 if (game_data_ == nullptr ||
2089 return absl::FailedPreconditionError("Dungeon palette data is unavailable");
2090 }
2091
2092 auto& room = rooms_[room_id];
2093 if (!room.IsLoaded()) {
2094 RETURN_IF_ERROR(room_loader_.LoadRoom(room_id, room));
2095 }
2097 room.PrepareForRender();
2098
2099 const int palette_id = room.ResolveDungeonPaletteId();
2100 const auto& dungeon_palettes = game_data_->palette_groups.dungeon_main;
2101 if (palette_id < 0 ||
2102 palette_id >= static_cast<int>(dungeon_palettes.size())) {
2103 return absl::OutOfRangeError(
2104 "Room resolves to an unavailable dungeon palette");
2105 }
2106 auto room_palette = dungeon_palettes[palette_id];
2107 ASSIGN_OR_RETURN(auto palette_group,
2109
2110 const absl::Status open_status = object_tile_editor_panel_->OpenForObject(
2111 object.id_, room_id, &rooms_, palette_group);
2112 if (!open_status.ok()) {
2113 // A modified hidden session rejects replacement to prevent data loss.
2114 // Surface that existing session so the user can apply, revert, or use the
2115 // explicit discard action described by the returned status.
2119 }
2120 return open_status;
2121 }
2125 return absl::NotFoundError(
2126 "Object Tile Editor window is not registered in this session");
2127 }
2128 return absl::OkStatus();
2129}
2130
2131void DungeonEditorV2::SwapRoomInPanel(int old_room_id, int new_room_id) {
2132 // Defer the swap until after the current frame's draw phase completes
2133 // This prevents modifying data structures while ImGui is still using them
2134 if (new_room_id < 0 || new_room_id >= static_cast<int>(rooms_.size())) {
2135 return;
2136 }
2137 pending_swap_.old_room_id = old_room_id;
2138 pending_swap_.new_room_id = new_room_id;
2139 pending_swap_.pending = true;
2140}
2141
2143 if (!pending_swap_.pending) {
2144 return;
2145 }
2146
2147 int old_room_id = pending_swap_.old_room_id;
2148 int new_room_id = pending_swap_.new_room_id;
2149 pending_swap_.pending = false;
2150
2151 // Find the position of old_room in active_rooms_
2152 int swap_index = -1;
2153 for (int i = 0; i < active_rooms_.Size; i++) {
2154 if (active_rooms_[i] == old_room_id) {
2155 swap_index = i;
2156 break;
2157 }
2158 }
2159
2160 if (swap_index < 0) {
2161 // Old room not found in active rooms, just select the new one
2162 OnRoomSelected(new_room_id);
2163 return;
2164 }
2165
2166 // Avoid swapping into an already-open room panel (the per-room maps assume
2167 // room IDs are unique). In this case, just focus/select the existing room.
2168 for (int i = 0; i < active_rooms_.Size; i++) {
2169 if (i != swap_index && active_rooms_[i] == new_room_id) {
2170 OnRoomSelected(new_room_id);
2171 return;
2172 }
2173 }
2174
2175 // Preserve the old panel's stable ImGui window identity by transferring its
2176 // slot id to the new room id.
2177 int slot_id = -1;
2178 if (auto it = room_panel_slot_ids_.find(old_room_id);
2179 it != room_panel_slot_ids_.end()) {
2180 slot_id = it->second;
2181 room_panel_slot_ids_.erase(it);
2182 } else {
2183 slot_id = next_room_panel_slot_id_++;
2184 }
2185 room_panel_slot_ids_[new_room_id] = slot_id;
2186
2187 // Preserve the viewer instance so canvas pan/zoom and UI state don't reset
2188 // when navigating with arrows (swap-in-panel).
2189 room_viewers_.Rename(old_room_id, new_room_id);
2190
2191 // Replace old room with new room in active_rooms_
2192 active_rooms_[swap_index] = new_room_id;
2194
2195 // Unregister old panel
2197 std::string old_card_id = absl::StrFormat("dungeon.room_%d", old_room_id);
2198 const bool old_pinned =
2201
2202 // Register new panel
2203 // Use unified ResourceLabelProvider for room names
2204 std::string new_room_name = absl::StrFormat(
2205 "[%03X] %s", new_room_id, zelda3::GetRoomLabel(new_room_id).c_str());
2206
2207 std::string new_card_id = absl::StrFormat("dungeon.room_%d", new_room_id);
2208
2210 {.card_id = new_card_id,
2211 .display_name = new_room_name,
2212 .window_title = ICON_MD_GRID_ON " " + new_room_name,
2213 .icon = ICON_MD_GRID_ON,
2214 .category = "Dungeon",
2215 .shortcut_hint = "",
2216 .visibility_flag = nullptr,
2217 .priority = 200 + new_room_id});
2218
2219 if (old_pinned) {
2220 dependencies_.window_manager->SetWindowPinned(new_card_id, true);
2221 }
2223 }
2224
2225 // Clean up old room's card and viewer
2226 room_cards_.erase(old_room_id);
2227
2228 // Update current selection
2229 OnRoomSelected(new_room_id, /*request_focus=*/false);
2230}
2231
2234 return;
2235 }
2236
2237 const bool enabled = pending_workflow_mode_.enabled;
2238 const bool show_toast = pending_workflow_mode_.show_toast;
2240 SetWorkbenchWorkflowMode(enabled, show_toast);
2241}
2242
2244 room_viewers_.Touch(room_id);
2245}
2246
2248 // No-op: LruCache handles removal internally via Erase()
2249 (void)room_id;
2250}
2251
2253 if (!viewer) {
2254 return;
2255 }
2256
2257 viewer->SetShowObjectPanelCallback([this]() {
2260 OpenWindow("dungeon.workbench");
2261 return;
2262 }
2264 });
2265 viewer->SetShowSpritePanelCallback([this]() {
2268 OpenWindow("dungeon.workbench");
2269 return;
2270 }
2271 OpenWindow("dungeon.sprite_editor");
2272 });
2273 viewer->SetShowItemPanelCallback([this]() {
2276 OpenWindow("dungeon.workbench");
2277 return;
2278 }
2279 OpenWindow("dungeon.item_editor");
2280 });
2281 viewer->SetShowRoomListCallback([this]() {
2282 OpenWindow(IsWorkbenchWorkflowEnabled() ? "dungeon.workbench"
2284 });
2285 viewer->SetShowRoomMatrixCallback([this]() {
2288 OpenWindow("dungeon.workbench");
2289 return;
2290 }
2292 });
2293 viewer->SetShowEntranceListCallback([this]() {
2296 OpenWindow("dungeon.workbench");
2297 return;
2298 }
2300 });
2301 viewer->SetShowRoomGraphicsCallback([this]() {
2304 OpenWindow("dungeon.workbench");
2305 return;
2306 }
2308 });
2309 viewer->SetShowDoorEditorCallback([this]() {
2312 OpenWindow("dungeon.workbench");
2313 return;
2314 }
2315 OpenWindow("dungeon.door_editor");
2316 });
2317 viewer->SetShowDungeonSettingsCallback([this]() {
2318 if (workbench_panel_) {
2320 }
2321 OpenWindow("dungeon.workbench");
2322 });
2324 [this](int target_room_id, const zelda3::RoomObject& object) {
2325 OpenGraphicsEditorForObject(target_room_id, object);
2326 });
2328 [this](int target_room_id, const zelda3::RoomObject& object) {
2329 const absl::Status status =
2330 OpenObjectTileEditorForObject(target_room_id, object);
2331 if (status.ok()) {
2332 return;
2333 }
2334 LOG_ERROR("DungeonEditorV2", "Edit Object Tiles failed: %s",
2335 status.message().data());
2338 absl::StrFormat("Edit Object Tiles failed: %s", status.message()),
2340 }
2341 });
2342 viewer->SetSaveRoomCallback([this](int target_room_id) {
2343 auto status = SaveRoom(target_room_id);
2344 if (!status.ok()) {
2345 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
2346 status.message().data());
2349 absl::StrFormat("Apply Room failed: %s", status.message()),
2351 }
2352 return;
2353 }
2355 dependencies_.toast_manager->Show("Applied room to ROM buffer",
2357 }
2358 });
2359
2362}
2363
2366 (void)room_id;
2367 return GetWorkbenchViewer();
2368 }
2369
2370 // Set eviction predicate to protect active rooms from eviction
2371 room_viewers_.SetEvictionPredicate([this](const int& candidate) {
2372 for (int i = 0; i < active_rooms_.size(); ++i) {
2373 if (active_rooms_[i] == candidate)
2374 return false;
2375 }
2376 return true;
2377 });
2378
2379 if (auto* existing = room_viewers_.Get(room_id)) {
2380 // Viewer already exists - Get() already touched LRU
2381 auto* viewer_ptr = existing->get();
2382 ConfigureViewerRenderContext(viewer_ptr, room_id);
2384
2385 // Update pinned state from manager
2387 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2388 viewer_ptr->SetPinned(
2390 viewer_ptr->SetPinCallback([this, card_id, room_id](bool pinned) {
2392 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2393 if (auto* v = GetViewerForRoom(room_id)) {
2394 v->SetPinned(pinned);
2395 }
2396 }
2397 });
2398 }
2399
2400 return viewer_ptr;
2401 }
2402
2403 // Creating a new viewer - Insert will handle LRU and eviction
2404 {
2405 auto viewer = std::make_unique<DungeonCanvasViewer>(rom_);
2406 viewer->SetCompactHeaderMode(false);
2407 viewer->SetRoomDetailsExpanded(true);
2408 DungeonCanvasViewer* viewer_ptr = viewer.get();
2409 viewer->SetRooms(&rooms_);
2410 viewer->SetRenderer(renderer_);
2411 viewer->SetCurrentPaletteGroup(current_palette_group_);
2412 viewer->SetCurrentPaletteId(current_palette_id_);
2413 viewer->SetGameData(game_data_);
2414 ConfigureViewerRenderContext(viewer_ptr, room_id);
2415 ApplyEntranceRenderContext(room_id);
2416
2417 // These hooks must remain correct even when a room panel swaps rooms while
2418 // keeping the same viewer instance (to preserve canvas pan/zoom + UI
2419 // state). Use the viewer's best-effort current room context instead of
2420 // capturing room_id at creation time.
2421 viewer->object_interaction().SetMutationCallback([this, viewer_ptr]() {
2422 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2423 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2424 const auto domain =
2426 if (domain == MutationDomain::kTileObjects) {
2427 BeginUndoSnapshot(rid);
2428 } else if (domain == MutationDomain::kCustomCollision) {
2429 BeginCollisionUndoSnapshot(rid);
2430 } else if (domain == MutationDomain::kWaterFill) {
2431 BeginWaterFillUndoSnapshot(rid);
2432 }
2433 }
2434 });
2435
2436 viewer->object_interaction().SetCacheInvalidationCallback([this,
2437 viewer_ptr]() {
2438 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2439 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2440 const auto domain =
2442 if (domain == MutationDomain::kTileObjects) {
2443 rooms_[rid].MarkObjectsDirty();
2444 rooms_[rid].RenderRoomGraphics();
2445 // Drag edits invalidate incrementally; finalize once the drag ends
2446 // (TileObjectHandler emits an extra invalidation on release).
2447 const auto mode =
2448 viewer_ptr->object_interaction().mode_manager().GetMode();
2450 FinalizeUndoAction(rid);
2451 }
2452 } else if (domain == MutationDomain::kCustomCollision) {
2453 const auto mode =
2454 viewer_ptr->object_interaction().mode_manager().GetMode();
2455 const auto& st =
2457 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2458 return;
2459 }
2460 FinalizeCollisionUndoAction(rid);
2461 } else if (domain == MutationDomain::kWaterFill) {
2462 const auto mode =
2463 viewer_ptr->object_interaction().mode_manager().GetMode();
2464 const auto& st =
2466 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2467 return;
2468 }
2469 FinalizeWaterFillUndoAction(rid);
2470 }
2471 }
2472 });
2473
2474 viewer->object_interaction().SetObjectPlacedCallback(
2475 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2476
2477 if (dungeon_editor_system_) {
2478 viewer->SetEditorSystem(dungeon_editor_system_.get());
2479 }
2480 viewer->SetRoomNavigationCallback([this](int target_room) {
2481 if (target_room >= 0 && target_room < static_cast<int>(rooms_.size())) {
2482 OnRoomSelected(target_room);
2483 }
2484 });
2485 // Swap callback swaps the room in the current panel instead of opening new
2486 viewer->SetRoomSwapCallback([this](int old_room, int new_room) {
2487 SwapRoomInPanel(old_room, new_room);
2488 });
2489 WireViewerPanelCallbacks(viewer.get());
2490
2491 // Wire up pinning for room panels.
2492 if (dependencies_.window_manager) {
2493 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2494 viewer->SetPinned(dependencies_.window_manager->IsWindowPinned(card_id));
2495 viewer->SetPinCallback([this, card_id, room_id](bool pinned) {
2496 if (dependencies_.window_manager) {
2497 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2498 // Sync state back to viewer in all panels showing this room
2499 if (auto* v = GetViewerForRoom(room_id)) {
2500 v->SetPinned(pinned);
2501 }
2502 }
2503 });
2504 }
2505
2506 auto* stored = room_viewers_.Insert(room_id, std::move(viewer));
2507 return stored->get();
2508 }
2509}
2510
2511void DungeonEditorV2::RefreshWorkbenchViewerRuntimeContext(
2512 DungeonCanvasViewer* viewer, int room_id) {
2513 if (!viewer) {
2514 return;
2515 }
2516
2517 if (viewer->rom() != rom_) {
2518 viewer->SetRom(rom_);
2519 }
2520 if (viewer->rooms() != &rooms_) {
2521 viewer->SetRooms(&rooms_);
2522 }
2523 if (viewer->game_data() != game_data_) {
2524 viewer->SetGameData(game_data_);
2525 }
2526 viewer->SetRenderer(renderer_);
2527 viewer->SetCurrentPaletteGroup(current_palette_group_);
2528 viewer->SetCurrentPaletteId(current_palette_id_);
2529 ConfigureViewerRenderContext(viewer, room_id);
2530 viewer->SetEditorSystem(dungeon_editor_system_.get());
2531 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2532 if (viewer->project() != dependencies_.project) {
2533 viewer->SetProject(dependencies_.project);
2534 }
2535}
2536
2537DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchViewer() {
2538 if (!workbench_viewer_) {
2539 workbench_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2540 auto* viewer = workbench_viewer_.get();
2541 viewer->SetCompactHeaderMode(true);
2542 viewer->SetRoomDetailsExpanded(false);
2543 viewer->SetHeaderVisible(false);
2544 viewer->SetHeaderHiddenMetadataHudVisible(false);
2545 viewer->SetRooms(&rooms_);
2546 viewer->SetRenderer(renderer_);
2547 viewer->SetCurrentPaletteGroup(current_palette_group_);
2548 viewer->SetCurrentPaletteId(current_palette_id_);
2549 viewer->SetGameData(game_data_);
2550
2551 // Workbench uses a single viewer; these hooks use the viewer's current room
2552 // context (set at DrawDungeonCanvas start) so room switching stays correct.
2553 viewer->object_interaction().SetMutationCallback([this, viewer]() {
2554 const int rid = viewer ? viewer->current_room_id() : -1;
2555 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2556 const auto domain = viewer->object_interaction().last_mutation_domain();
2557 if (domain == MutationDomain::kTileObjects) {
2558 BeginUndoSnapshot(rid);
2559 } else if (domain == MutationDomain::kCustomCollision) {
2560 BeginCollisionUndoSnapshot(rid);
2561 } else if (domain == MutationDomain::kWaterFill) {
2562 BeginWaterFillUndoSnapshot(rid);
2563 }
2564 }
2565 });
2566 viewer->object_interaction().SetCacheInvalidationCallback([this, viewer]() {
2567 const int rid = viewer ? viewer->current_room_id() : -1;
2568 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2569 const auto domain =
2570 viewer->object_interaction().last_invalidation_domain();
2571 if (domain == MutationDomain::kTileObjects) {
2572 rooms_[rid].MarkObjectsDirty();
2573 rooms_[rid].RenderRoomGraphics();
2574 const auto mode =
2575 viewer->object_interaction().mode_manager().GetMode();
2576 if (mode != InteractionMode::DraggingObjects) {
2577 FinalizeUndoAction(rid);
2578 }
2579 } else if (domain == MutationDomain::kCustomCollision) {
2580 const auto mode =
2581 viewer->object_interaction().mode_manager().GetMode();
2582 const auto& st =
2583 viewer->object_interaction().mode_manager().GetModeState();
2584 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2585 return;
2586 }
2587 FinalizeCollisionUndoAction(rid);
2588 } else if (domain == MutationDomain::kWaterFill) {
2589 const auto mode =
2590 viewer->object_interaction().mode_manager().GetMode();
2591 const auto& st =
2592 viewer->object_interaction().mode_manager().GetModeState();
2593 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2594 return;
2595 }
2596 FinalizeWaterFillUndoAction(rid);
2597 }
2598 }
2599 });
2600
2601 viewer->object_interaction().SetObjectPlacedCallback(
2602 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2603
2604 if (dungeon_editor_system_) {
2605 viewer->SetEditorSystem(dungeon_editor_system_.get());
2606 }
2607
2608 // In workbench mode, arrow navigation swaps the current room without
2609 // changing window identities.
2610 viewer->SetRoomSwapCallback([this](int /*old_room*/, int new_room) {
2611 OnRoomSelected(new_room, /*request_focus=*/false);
2612 });
2613 viewer->SetRoomNavigationCallback([this](int target_room) {
2614 OnRoomSelected(target_room, /*request_focus=*/false);
2615 });
2616
2617 WireViewerPanelCallbacks(viewer);
2618 }
2619
2620 auto* viewer = workbench_viewer_.get();
2621 RefreshWorkbenchViewerRuntimeContext(viewer, current_room_id_);
2622 return viewer;
2623}
2624
2625DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchCompareViewer(int room_id) {
2626 if (!workbench_compare_viewer_) {
2627 workbench_compare_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2628 auto* viewer = workbench_compare_viewer_.get();
2629 viewer->SetCompactHeaderMode(true);
2630 viewer->SetRoomDetailsExpanded(false);
2631 viewer->SetHeaderHiddenMetadataHudVisible(false);
2632 viewer->SetRooms(&rooms_);
2633 viewer->SetRenderer(renderer_);
2634 viewer->SetCurrentPaletteGroup(current_palette_group_);
2635 viewer->SetCurrentPaletteId(current_palette_id_);
2636 viewer->SetGameData(game_data_);
2637
2638 // Compare viewer is read-only by default: no object selection/mutation, but
2639 // still allows canvas pan/zoom.
2640 viewer->SetObjectInteractionEnabled(false);
2641 viewer->SetHeaderReadOnly(true);
2642 viewer->SetHeaderVisible(false);
2643
2644 if (dungeon_editor_system_) {
2645 // Allows consistent rendering paths that depend on the editor system, but
2646 // interaction is still disabled.
2647 viewer->SetEditorSystem(dungeon_editor_system_.get());
2648 }
2649
2650 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2651 viewer->SetProject(dependencies_.project);
2652 }
2653
2654 auto* viewer = workbench_compare_viewer_.get();
2655 RefreshWorkbenchViewerRuntimeContext(viewer, room_id);
2656 viewer->SetObjectInteractionEnabled(false);
2657 viewer->SetHeaderReadOnly(true);
2658 viewer->SetHeaderVisible(false);
2659 return viewer;
2660}
2661
2662void DungeonEditorV2::SyncPanelsToRoom(int room_id) {
2663 // Update object editor card with current viewer
2664 if (object_selector_panel_) {
2665 object_selector_panel_->SetCanvasViewerProvider(
2666 [this]() { return GetViewerForRoom(current_room_id_); });
2667 object_selector_panel_->SetCurrentRoom(room_id);
2668 object_selector_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2669 }
2670 if (object_editor_content_) {
2671 object_editor_content_->SetCanvasViewerProvider(
2672 [this]() { return GetViewerForRoom(current_room_id_); });
2673 object_editor_content_->SetCurrentRoom(room_id);
2674 object_editor_content_->SetCanvasViewer(GetViewerForRoom(room_id));
2675 }
2676 if (door_editor_panel_) {
2677 door_editor_panel_->SetCanvasViewerProvider(
2678 [this]() { return GetViewerForRoom(current_room_id_); });
2679 door_editor_panel_->SetCurrentRoom(room_id);
2680 door_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2681 }
2682
2683 // Update sprite and item editor panels with current viewer
2684 if (sprite_editor_panel_) {
2685 sprite_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2686 }
2687 if (item_editor_panel_) {
2688 item_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2689 }
2690 if (custom_collision_panel_) {
2691 auto* viewer = GetViewerForRoom(room_id);
2692 custom_collision_panel_->SetCanvasViewer(viewer);
2693 if (viewer) {
2694 custom_collision_panel_->SetInteraction(&viewer->object_interaction());
2695 }
2696 }
2697 if (water_fill_panel_) {
2698 auto* viewer = GetViewerForRoom(room_id);
2699 water_fill_panel_->SetCanvasViewer(viewer);
2700 if (viewer) {
2701 water_fill_panel_->SetInteraction(&viewer->object_interaction());
2702 }
2703 }
2704
2705 if (room_tag_editor_panel_) {
2706 room_tag_editor_panel_->SetCurrentRoomId(room_id);
2707 }
2708
2709 if (overlay_manager_panel_) {
2710 auto* viewer = GetViewerForRoom(room_id);
2711 if (viewer) {
2713 overlay_state.show_grid = viewer->mutable_show_grid();
2714 overlay_state.show_object_bounds = viewer->mutable_show_object_bounds();
2715 overlay_state.show_coordinate_overlay =
2716 viewer->mutable_show_coordinate_overlay();
2717 overlay_state.show_room_debug_info =
2718 viewer->mutable_show_room_debug_info();
2719 overlay_state.show_texture_debug = viewer->mutable_show_texture_debug();
2720 overlay_state.show_layer_info = viewer->mutable_show_layer_info();
2721 overlay_state.show_minecart_tracks =
2722 viewer->mutable_show_minecart_tracks();
2723 overlay_state.show_custom_collision =
2724 viewer->mutable_show_custom_collision_overlay();
2725 overlay_state.show_track_collision =
2726 viewer->mutable_show_track_collision_overlay();
2727 overlay_state.show_camera_quadrants =
2728 viewer->mutable_show_camera_quadrant_overlay();
2729 overlay_state.show_minecart_sprites =
2730 viewer->mutable_show_minecart_sprite_overlay();
2731 overlay_state.show_collision_legend =
2732 viewer->mutable_show_track_collision_legend();
2733 overlay_manager_panel_->SetState(overlay_state);
2734 }
2735 }
2736}
2737
2738void DungeonEditorV2::ShowRoomPanel(int room_id) {
2739 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2740 return;
2741 }
2742
2743 bool already_active = false;
2744 for (int i = 0; i < active_rooms_.Size; ++i) {
2745 if (active_rooms_[i] == room_id) {
2746 already_active = true;
2747 break;
2748 }
2749 }
2750 if (!already_active) {
2751 active_rooms_.push_back(room_id);
2752 room_selector_.set_active_rooms(active_rooms_);
2753 }
2754
2755 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2756
2757 if (dependencies_.window_manager) {
2758 if (!dependencies_.window_manager->GetWindowDescriptor(
2759 dependencies_.window_manager->GetActiveSessionId(), card_id)) {
2760 std::string room_name = absl::StrFormat(
2761 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2762 dependencies_.window_manager->RegisterWindow(
2763 {.card_id = card_id,
2764 .display_name = room_name,
2765 .window_title = ICON_MD_GRID_ON " " + room_name,
2766 .icon = ICON_MD_GRID_ON,
2767 .category = "Dungeon",
2768 .shortcut_hint = "",
2769 .visibility_flag = nullptr,
2770 .priority = 200 + room_id});
2771 }
2772 dependencies_.window_manager->OpenWindow(card_id);
2773 }
2774
2775 // Create or update the PanelWindow for this room
2776 if (room_cards_.find(room_id) == room_cards_.end()) {
2777 std::string base_name = absl::StrFormat(
2778 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2779 const int slot_id = GetOrCreateRoomPanelSlotId(room_id);
2780 std::string card_name_str = absl::StrFormat(
2781 "%s###RoomPanelSlot%d", MakePanelTitle(base_name).c_str(), slot_id);
2782
2783 auto card = std::make_shared<gui::PanelWindow>(card_name_str.c_str(),
2785 card->SetDefaultSize(620, 700);
2786 room_cards_[room_id] = card;
2787 }
2788}
2789
2790} // 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_
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
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_
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_
static constexpr const char * kPaletteEditorId
PendingWorkflowMode pending_workflow_mode_
void InvalidateDungeonPaletteUsers(gui::DungeonPaletteChange change)
void HandleArenaPaletteChanged(const std::string &group_name, int palette_index)
void SetCustomObjectsFolder(const std::string &folder)
void SetProject(project::YazeProject *project)
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)
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 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 SetOnInspectorSideChanged(std::function< void(bool)> cb)
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:334
virtual void SetDependencies(const EditorDependencies &deps)
Definition editor.h:250
zelda3::GameData * game_data() const
Definition editor.h:320
EditorDependencies dependencies_
Definition editor.h:333
EditorType type_
Definition editor.h:332
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetProjectChangedCallback(ProjectChangedCallback callback)
void SetRoomNavigationCallback(RoomNavigationCallback callback)
absl::Status RebindProjectContext(project::YazeProject *project)
absl::Status SetProject(project::YazeProject *project)
void SetProjectDraftChangedCallback(ProjectDraftChangedCallback callback)
void SetProjectSaveCallback(ProjectSaveCallback callback)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetPlacementError(const std::string &message)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetRooms(DungeonRoomStore *rooms)
void SetGameData(zelda3::GameData *game_data)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetCurrentPaletteGroupForRoom(int room_id, const gfx::PaletteGroup &group)
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:54
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)
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:497
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:145
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:504
static Arena & Get()
Definition arena.cc:21
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()
void AddObjectFile(int object_id, const std::string &filename)
void Initialize(const std::string &custom_objects_folder)
static ObjectDimensionTable & Get()
static bool IsEditableStandardObject(int16_t object_id)
Dungeon Room Entrance or Spawn Point.
void set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:574
bool HasUnsavedChanges() const
Definition room.h:589
void MarkGraphicsDirty()
Definition room.h:450
bool IsLoaded() const
Definition room.h:872
void ClearWaterFillZone()
Definition room.h:561
void ClearWaterFillDirty()
Definition room.h:582
int ResolveDungeonPaletteId() const
Definition room.cc:727
void SetGameData(GameData *data)
Definition room.h:964
void SetWaterFillTile(int x, int y, bool filled)
Definition room.h:538
#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:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
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.
absl::StatusOr< PaletteGroup > CreatePaletteGroupFromLargePalette(SnesPalette &palette, int num_colors)
Create a PaletteGroup by dividing a large palette into sub-palettes.
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:546
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)
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
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:197
std::string custom_objects_folder
Definition project.h:192
core::HackManifest hack_manifest
Definition project.h:212
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1491
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