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
4// C system headers
5#include <cstdio>
6
7// C++ standard library headers
8#include <algorithm>
9#include <iterator>
10#include <memory>
11#include <string>
12#include <utility>
13#include <vector>
14
15// Third-party library headers
16#include "absl/status/status.h"
17#include "absl/strings/str_format.h"
18#include "absl/types/span.h"
19#include "imgui/imgui.h"
20
21// Project headers
60#include "app/gui/core/icons.h"
61#include "core/features.h"
62#include "core/project.h"
63#include "rom/snes.h"
64#include "util/log.h"
65#include "util/macro.h"
71#include "zelda3/dungeon/room.h"
74
75namespace yaze::editor {
76
77namespace {
78
79absl::Status SaveWaterFillZones(Rom* rom, DungeonRoomStore& rooms) {
80 if (!rom || !rom->is_loaded()) {
81 return absl::FailedPreconditionError("ROM not loaded");
82 }
83
84 bool any_dirty = false;
85 rooms.ForEachMaterialized([&any_dirty](int, const zelda3::Room& room) {
86 if (room.water_fill_dirty()) {
87 any_dirty = true;
88 }
89 });
90 if (!any_dirty) {
91 return absl::OkStatus();
92 }
93
94 std::vector<zelda3::WaterFillZoneEntry> zones;
95 zones.reserve(8);
96 for (int room_id = 0; room_id < static_cast<int>(rooms.size()); ++room_id) {
97 auto* room = rooms.GetIfMaterialized(room_id);
98 if (room == nullptr) {
99 continue;
100 }
101 if (!room->has_water_fill_zone()) {
102 continue;
103 }
104
105 const int tile_count = room->WaterFillTileCount();
106 if (tile_count <= 0) {
107 continue;
108 }
109 if (tile_count > 255) {
110 return absl::InvalidArgumentError(absl::StrFormat(
111 "Water fill zone in room 0x%02X has %d tiles (max 255)", room_id,
112 tile_count));
113 }
114
116 z.room_id = room_id;
118 z.fill_offsets.reserve(static_cast<size_t>(tile_count));
119
120 const auto& map = room->water_fill_zone().tiles;
121 for (size_t i = 0; i < map.size(); ++i) {
122 if (map[i] != 0) {
123 z.fill_offsets.push_back(static_cast<uint16_t>(i));
124 }
125 }
126 zones.push_back(std::move(z));
127 }
128
129 if (zones.size() > 8) {
130 return absl::InvalidArgumentError(absl::StrFormat(
131 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
132 zones.size()));
133 }
134
135 // Canonicalize SRAM mask assignment using the shared normalizer so editor
136 // save behavior matches JSON import/export workflows.
138 for (const auto& z : zones) {
139 if (auto* room = rooms.GetIfMaterialized(z.room_id)) {
140 room->set_water_fill_sram_bit_mask(z.sram_bit_mask);
141 }
142 }
143
146 [](int, zelda3::Room& room) { room.ClearWaterFillDirty(); });
147
148 return absl::OkStatus();
149}
150
151} // namespace
152
154 // Clear viewer references in panels BEFORE room_viewers_ is destroyed.
155 // Panels are owned by WorkspaceWindowManager and outlive this editor, so they need
156 // to have their viewer pointers cleared to prevent dangling pointer access.
159 }
162 }
163 if (door_editor_panel_) {
165 }
168 }
169 if (item_editor_panel_) {
171 }
175 }
176 if (water_fill_panel_) {
179 }
182 }
183}
184
188 }
189 const bool rom_changed = dependencies_.rom && dependencies_.rom != rom_;
190 if (rom_changed) {
192 // The system owns ROM-backed views; ensure it matches the current ROM.
194 }
197 if (game_data_) {
199 }
200 }
201
202 // Setup docking class for room windows
203 room_window_class_.DockingAllowUnclassed = true;
204 room_window_class_.DockingAlwaysTabBar = true;
205
207 return;
208 auto* window_manager = dependencies_.window_manager;
209
210 // Legacy panel IDs persisted in older layouts/settings.
211 window_manager->RegisterPanelAlias("dungeon.object_tools",
213 window_manager->RegisterPanelAlias("dungeon.entrances",
214 "dungeon.entrance_properties");
215
216 // Register panels with WorkspaceWindowManager (no boolean flags - visibility is
217 // managed entirely by WorkspaceWindowManager::ShowPanel/HidePanel/IsPanelVisible)
218 window_manager->RegisterPanel(
219 {.card_id = "dungeon.workbench",
220 .display_name = "Dungeon Workbench",
221 .window_title = " Dungeon Workbench",
222 .icon = ICON_MD_WORKSPACES,
223 .category = "Dungeon",
224 .shortcut_hint = "Ctrl+Shift+W",
225 .visibility_flag = nullptr,
226 .priority = 5,
227 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
228 .disabled_tooltip = "Load a ROM to edit dungeon rooms"});
229
230 window_manager->RegisterPanel(
231 {.card_id = kRoomSelectorId,
232 .display_name = "Room List",
233 .window_title = " Room List",
234 .icon = ICON_MD_LIST,
235 .category = "Dungeon",
236 .shortcut_hint = "Ctrl+Shift+R",
237 .visibility_flag = nullptr,
238 .priority = 20,
239 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
240 .disabled_tooltip = "Load a ROM to browse dungeon rooms"});
241
242 window_manager->RegisterPanel(
243 {.card_id = kEntranceListId,
244 .display_name = "Entrance List",
245 .window_title = " Entrance List",
246 .icon = ICON_MD_DOOR_FRONT,
247 .category = "Dungeon",
248 .shortcut_hint = "Ctrl+Shift+E",
249 .visibility_flag = nullptr,
250 .priority = 25,
251 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
252 .disabled_tooltip = "Load a ROM to browse dungeon entrances"});
253
254 window_manager->RegisterPanel(
255 {.card_id = "dungeon.entrance_properties",
256 .display_name = "Entrance Properties",
257 .window_title = " Entrance Properties",
258 .icon = ICON_MD_TUNE,
259 .category = "Dungeon",
260 .shortcut_hint = "",
261 .visibility_flag = nullptr,
262 .priority = 26,
263 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
264 .disabled_tooltip = "Load a ROM to edit entrance properties"});
265
266 window_manager->RegisterPanel(
267 {.card_id = kRoomMatrixId,
268 .display_name = "Room Matrix",
269 .window_title = " Room Matrix",
270 .icon = ICON_MD_GRID_VIEW,
271 .category = "Dungeon",
272 .shortcut_hint = "Ctrl+Shift+M",
273 .visibility_flag = nullptr,
274 .priority = 30,
275 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
276 .disabled_tooltip = "Load a ROM to view the room matrix"});
277
278 window_manager->RegisterPanel(
279 {.card_id = kRoomGraphicsId,
280 .display_name = "Room Graphics",
281 .window_title = " Room Graphics",
282 .icon = ICON_MD_IMAGE,
283 .category = "Dungeon",
284 .shortcut_hint = "Ctrl+Shift+G",
285 .visibility_flag = nullptr,
286 .priority = 50,
287 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
288 .disabled_tooltip = "Load a ROM to view room graphics"});
289
290 window_manager->RegisterPanel(
291 {.card_id = kObjectSelectorId,
292 .display_name = "Object Selector",
293 .window_title = " Object Selector",
294 .icon = ICON_MD_CATEGORY,
295 .category = "Dungeon",
296 .shortcut_hint = "",
297 .visibility_flag = nullptr,
298 .priority = 60,
299 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
300 .disabled_tooltip = "Load a ROM to browse dungeon objects"});
301
302 window_manager->RegisterPanel(
303 {.card_id = kObjectEditorId,
304 .display_name = "Object Editor",
305 .window_title = " Object Editor",
306 .icon = ICON_MD_TUNE,
307 .category = "Dungeon",
308 .shortcut_hint = "",
309 .visibility_flag = nullptr,
310 .priority = 61,
311 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
312 .disabled_tooltip = "Load a ROM to edit selected dungeon objects"});
313
314 window_manager->RegisterPanel(
315 {.card_id = kDoorEditorId,
316 .display_name = "Door Editor",
317 .window_title = " Door Editor",
318 .icon = ICON_MD_DOOR_FRONT,
319 .category = "Dungeon",
320 .shortcut_hint = "",
321 .visibility_flag = nullptr,
322 .priority = 69,
323 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
324 .disabled_tooltip = "Load a ROM to edit dungeon doors"});
325
326 window_manager->RegisterPanel(
327 {.card_id = kPaletteEditorId,
328 .display_name = "Palette Editor",
329 .window_title = " Palette Editor",
330 .icon = ICON_MD_PALETTE,
331 .category = "Dungeon",
332 // Avoid conflicting with the global Command Palette (Ctrl/Cmd+Shift+P).
333 .shortcut_hint = "Ctrl+Shift+Alt+P",
334 .visibility_flag = nullptr,
335 .priority = 70,
336 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
337 .disabled_tooltip = "Load a ROM to edit dungeon palettes"});
338
339 window_manager->RegisterPanel(
340 {.card_id = "dungeon.room_tags",
341 .display_name = "Room Tags",
342 .window_title = " Room Tags",
343 .icon = ICON_MD_LABEL,
344 .category = "Dungeon",
345 .shortcut_hint = "",
346 .visibility_flag = nullptr,
347 .priority = 45,
348 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
349 .disabled_tooltip = "Load a ROM to view room tags"});
350
351 window_manager->RegisterPanel(
352 {.card_id = "dungeon.dungeon_map",
353 .display_name = "Dungeon Map",
354 .window_title = " Dungeon Map",
355 .icon = ICON_MD_MAP,
356 .category = "Dungeon",
357 .shortcut_hint = "Ctrl+Shift+D",
358 .visibility_flag = nullptr,
359 .priority = 32,
360 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
361 .disabled_tooltip = "Load a ROM to view the dungeon map"});
362
363 // Show default panels on startup.
364 // Workbench mode intentionally suppresses parallel standalone defaults to
365 // avoid two competing workflows being open at once.
367 /*show_toast=*/false);
368
369 // Wire intent-aware callback for double-click / context menu
371 [this](int room_id, RoomSelectionIntent intent) {
372 OnRoomSelected(room_id, intent);
373 });
374
375 // Register WindowContent instances
376 window_manager->RegisterWindowContent(
377 std::make_unique<RoomBrowserContent>(
378 &room_selector_, [this](int room_id) { OnRoomSelected(room_id); }));
379
380 window_manager->RegisterWindowContent(
381 std::make_unique<DungeonEntranceListPanel>(
383 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
384
385 {
386 auto matrix_panel = std::make_unique<RoomMatrixContent>(
388 [this](int room_id) { OnRoomSelected(room_id); },
389 [this](int old_room, int new_room) {
390 SwapRoomInPanel(old_room, new_room);
391 },
392 &rooms_);
393 matrix_panel->SetRoomIntentCallback(
394 [this](int room_id, RoomSelectionIntent intent) {
395 OnRoomSelected(room_id, intent);
396 });
397 window_manager->RegisterWindowContent(std::move(matrix_panel));
398 }
399
400 {
401 auto dungeon_map = std::make_unique<DungeonMapPanel>(
403 [this](int room_id) { OnRoomSelected(room_id); }, &rooms_);
404 dungeon_map->SetRoomIntentCallback(
405 [this](int room_id, RoomSelectionIntent intent) {
406 OnRoomSelected(room_id, intent);
407 });
409 dungeon_map->SetHackManifest(&dependencies_.project->hack_manifest);
410 }
411 window_manager->RegisterWindowContent(std::move(dungeon_map));
412 }
413
414 {
415 auto workbench = std::make_unique<DungeonWorkbenchContent>(
417 [this](int room_id) { OnRoomSelected(room_id); },
418 [this](int room_id, RoomSelectionIntent intent) {
419 OnRoomSelected(room_id, intent);
420 },
421 [this](int room_id) {
422 auto status = SaveRoom(room_id);
423 if (!status.ok()) {
424 LOG_ERROR("DungeonEditorV2", "Save Room failed: %s",
425 status.message().data());
428 absl::StrFormat("Save Room failed: %s", status.message()),
430 }
431 return;
432 }
434 dependencies_.toast_manager->Show("Room saved",
436 }
437 },
438 [this]() { return GetWorkbenchViewer(); },
439 [this]() { return GetWorkbenchCompareViewer(); },
440 [this]() -> const std::deque<int>& { return recent_rooms_; },
441 [this](int room_id) {
442 recent_rooms_.erase(
443 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
444 recent_rooms_.end());
445 },
446 [this](const std::string& id) { OpenWindow(id); },
447 [this](bool enabled) { QueueWorkbenchWorkflowMode(enabled); }, rom_);
448 workbench_panel_ = workbench.get();
450 [this]() { return undo_manager_.CanUndo(); },
451 [this]() { return undo_manager_.CanRedo(); },
452 [this]() { Undo().IgnoreError(); }, [this]() { Redo().IgnoreError(); },
453 [this]() { return undo_manager_.GetUndoDescription(); },
454 [this]() { return undo_manager_.GetRedoDescription(); },
455 [this]() { return static_cast<int>(undo_manager_.UndoStackSize()); });
456 window_manager->RegisterWindowContent(std::move(workbench));
457 }
458
459 window_manager->RegisterWindowContent(std::make_unique<DungeonEntrancesPanel>(
461 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
462
463 // Note: RoomGraphicsContent and PaletteEditorContent are registered
464 // in Load() after their dependencies (renderer_, palette_editor_) are initialized
465}
466
467absl::Status DungeonEditorV2::Load() {
468 if (!rom_ || !rom_->is_loaded()) {
469 return absl::FailedPreconditionError("ROM not loaded");
470 }
471
472 // Initialize ObjectDimensionTable so DimensionService's fallback path works.
473 // DimensionService::Get() queries this table internally but has no Load API.
474 auto& dim_table = zelda3::ObjectDimensionTable::Get();
475 if (!dim_table.IsLoaded()) {
476 RETURN_IF_ERROR(dim_table.LoadFromRom(rom_));
477 }
478
480
481 if (!game_data()) {
482 return absl::FailedPreconditionError("GameData not available");
483 }
484 auto dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
485 current_palette_ = dungeon_main_pal_group[current_palette_group_id_];
488
493 [this](int room_id) { OnRoomSelected(room_id); });
495 [this](int room_id, RoomSelectionIntent intent) {
496 OnRoomSelected(room_id, intent);
497 });
498
499 // Canvas viewers are lazily created in GetViewerForRoom
500
501 if (game_data()) {
503 } else {
505 }
506
509
510 // Register panels that depend on initialized state (renderer, palette_editor_)
512 auto graphics_panel = std::make_unique<RoomGraphicsContent>(
514 room_graphics_panel_ = graphics_panel.get();
516 std::move(graphics_panel));
518 std::make_unique<PaletteEditorContent>(&palette_editor_));
519 }
520
521 dungeon_editor_system_ = std::make_unique<zelda3::DungeonEditorSystem>(rom_);
522 (void)dungeon_editor_system_->Initialize();
524
525 // Initialize browse-only object selector and the dedicated object editor.
526 auto object_selector = std::make_unique<ObjectSelectorContent>(
527 renderer_, rom_, nullptr, dungeon_editor_system_->GetObjectEditor());
528 auto object_editor =
529 std::make_unique<ObjectEditorContent>(dungeon_editor_system_->GetObjectEditor());
530
531 // Wire up object change callback to trigger room re-rendering
532 dungeon_editor_system_->GetObjectEditor()->SetObjectChangedCallback(
533 [this](size_t /*object_index*/, const zelda3::RoomObject& /*object*/) {
534 if (current_room_id_ >= 0 && current_room_id_ < (int)rooms_.size()) {
535 rooms_[current_room_id_].RenderRoomGraphics();
536 }
537 });
538
539 // Set rooms and initial palette group for correct preview rendering
540 object_selector->SetRooms(&rooms_);
541 object_selector->SetCurrentPaletteGroup(current_palette_group_);
542
543 object_selector->SetOpenObjectEditorCallback(
544 [this]() { OpenWindow(kObjectEditorId); });
545
546 // Keep raw pointers for later access
547 object_selector_panel_ = object_selector.get();
548 object_editor_content_ = object_editor.get();
549
550 auto door_editor = std::make_unique<DoorEditorContent>();
551 door_editor->SetRooms(&rooms_);
552 door_editor_panel_ = door_editor.get();
553
554 // Propagate game_data to the object editor panel if available
555 if (game_data()) {
557 }
562 }
563
564 // Wire tile editor callback before transferring ownership
565 object_selector->set_tile_editor_callback([this](int16_t object_id) {
568 &rooms_);
569 OpenWindow("dungeon.object_tile_editor");
570 }
571 });
572
573 // Register the ObjectSelectorContent directly (it inherits from WindowContent)
574 // Panel manager takes ownership
577 std::move(object_selector));
579 std::move(object_editor));
580 dependencies_.window_manager->RegisterWindowContent(std::move(door_editor));
581
582 // Register sprite and item editor panels with canvas viewer = nullptr
583 // They will get the viewer reference in OnRoomSelected when a room is selected
584 auto sprite_panel = std::make_unique<SpriteEditorPanel>(&current_room_id_,
585 &rooms_, nullptr);
586 sprite_editor_panel_ = sprite_panel.get();
588 std::move(sprite_panel));
589
590 auto item_panel =
591 std::make_unique<ItemEditorPanel>(&current_room_id_, &rooms_, nullptr);
592 item_editor_panel_ = item_panel.get();
593 dependencies_.window_manager->RegisterWindowContent(std::move(item_panel));
594
595 auto collision_panel = std::make_unique<CustomCollisionPanel>(
596 nullptr, nullptr); // Placeholder, will be set in OnRoomSelected
597 custom_collision_panel_ = collision_panel.get();
599 std::move(collision_panel));
600
601 auto water_fill_panel =
602 std::make_unique<WaterFillPanel>(nullptr, nullptr); // Placeholder
603 water_fill_panel_ = water_fill_panel.get();
605 std::move(water_fill_panel));
606
607 // Object Tile Editor Panel
608 {
609 auto tile_editor_panel =
610 std::make_unique<ObjectTileEditorPanel>(renderer_, rom_);
611 tile_editor_panel->SetCurrentPaletteGroup(current_palette_group_);
612
613 // Wire creation callback: when a new custom object is saved,
614 // register it with the manager, persist to project, and refresh UI.
615 tile_editor_panel->SetObjectCreatedCallback(
616 [this](int object_id, const std::string& filename) {
618 filename);
620 dependencies_.project->custom_object_files[object_id].push_back(
621 filename);
622 (void)dependencies_.project->Save();
623 }
626 }
627 });
628
629 object_tile_editor_panel_ = tile_editor_panel.get();
631 std::move(tile_editor_panel));
632 }
633
634 // Wire tile editor panel and project references to the object selector
641 }
642 }
643
644 auto settings_panel = std::make_unique<DungeonSettingsPanel>(nullptr);
645 settings_panel->SetSaveRoomCallback([this](int id) { SaveRoom(id); });
646 settings_panel->SetSaveAllRoomsCallback([this]() { SaveAllRooms(); });
647 settings_panel->SetCurrentRoomId(&current_room_id_);
648 dungeon_settings_panel_ = settings_panel.get();
650 std::move(settings_panel));
651
652 // Room Tag Editor Panel
653 {
654 auto room_tag_panel = std::make_unique<RoomTagEditorPanel>();
655 room_tag_panel->SetProject(dependencies_.project);
656 room_tag_panel->SetRooms(&rooms_);
657 room_tag_panel->SetCurrentRoomId(current_room_id_);
658 room_tag_editor_panel_ = room_tag_panel.get();
660 std::move(room_tag_panel));
661 }
662
663 // Overlay Manager Panel
664 {
665 auto overlay_panel = std::make_unique<OverlayManagerPanel>();
666 overlay_manager_panel_ = overlay_panel.get();
668 std::move(overlay_panel));
669 }
670
671 // Feature Flag: Custom Objects / Minecart Tracks
672 if (core::FeatureFlags::get().kEnableCustomObjects) {
674 auto minecart_panel = std::make_unique<MinecartTrackEditorPanel>();
675 minecart_track_editor_panel_ = minecart_panel.get();
677 std::move(minecart_panel));
678 }
679
681 // Update project root for track editor
689 [this](int room_id) { OnRoomSelected(room_id); });
690 }
691
692 // Initialize custom object manager with project-configured path
697 } else {
698 // Avoid inheriting stale singleton state from previous projects.
700 }
701 }
702 }
703 } else {
704 owned_object_selector_panel_ = std::move(object_selector);
705 owned_object_editor_content_ = std::move(object_editor);
706 owned_door_editor_panel_ = std::move(door_editor);
707 }
708
709 palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) {
710 auto apply_palette = [this](DungeonCanvasViewer* viewer) {
711 if (!viewer) {
712 return;
713 }
714 viewer->SetCurrentPaletteId(current_palette_id_);
715 viewer->SetCurrentPaletteGroup(current_palette_group_);
716 };
717
718 if (current_room_id_ >= 0 && current_room_id_ < (int)rooms_.size() &&
719 game_data()) {
720 auto& dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
721 if (current_palette_id_ >= 0 &&
722 current_palette_id_ < static_cast<int>(dungeon_main_pal_group.size())) {
724 current_palette_ = dungeon_main_pal_group[current_palette_id_];
725 if (auto pal_group =
727 pal_group.ok()) {
728 current_palette_group_ = pal_group.value();
729 apply_palette(workbench_viewer_.get());
730 apply_palette(workbench_compare_viewer_.get());
732 if (auto* existing_viewer = room_viewers_.Get(current_room_id_)) {
733 apply_palette(existing_viewer->get());
734 }
735 }
739 }
742 }
743 }
744 }
745 }
746
747 bool rendered_current_room = false;
748 for (int i = 0; i < active_rooms_.Size; i++) {
749 int room_id = active_rooms_[i];
750 if (room_id >= 0 && room_id < (int)rooms_.size()) {
751 rooms_[room_id].RenderRoomGraphics();
752 rendered_current_room |= room_id == current_room_id_;
753 }
754 }
755
756 if (!rendered_current_room && current_room_id_ >= 0 &&
757 current_room_id_ < static_cast<int>(rooms_.size())) {
758 rooms_[current_room_id_].RenderRoomGraphics();
759 }
760 });
761
762 // Oracle of Secrets: load editor-authored water fill zones (best-effort).
763 {
765 room.ClearWaterFillZone();
766 room.ClearWaterFillDirty();
767 });
768
769 bool legacy_imported = false;
770 std::vector<zelda3::WaterFillZoneEntry> zones;
771
772 auto zones_or = zelda3::LoadWaterFillTable(rom_);
773 if (zones_or.ok()) {
774 zones = std::move(zones_or.value());
775 } else {
776 LOG_WARN("DungeonEditorV2", "WaterFillTable parse failed: %s",
777 zones_or.status().message().data());
780 absl::StrFormat("WaterFill table parse failed: %s",
781 zones_or.status().message()),
783 }
784 }
785
786 if (zones.empty()) {
787 std::string sym_path;
792 }
793 auto legacy_or = zelda3::LoadLegacyWaterGateZones(rom_, sym_path);
794 if (legacy_or.ok()) {
795 zones = std::move(legacy_or.value());
796 legacy_imported = !zones.empty();
797 } else {
798 LOG_WARN("DungeonEditorV2", "Legacy water gate import failed: %s",
799 legacy_or.status().message().data());
800 }
801 }
802
803 for (const auto& z : zones) {
804 if (z.room_id < 0 || z.room_id >= static_cast<int>(rooms_.size())) {
805 continue;
806 }
807 auto& room = rooms_[z.room_id];
808 room.set_water_fill_sram_bit_mask(z.sram_bit_mask);
809 for (uint16_t off : z.fill_offsets) {
810 const int x = static_cast<int>(off % 64);
811 const int y = static_cast<int>(off / 64);
812 room.SetWaterFillTile(x, y, true);
813 }
814
815 if (!legacy_imported) {
816 room.ClearWaterFillDirty();
817 }
818 }
819
820 if (legacy_imported && dependencies_.toast_manager) {
822 "Imported legacy water gate zones (save to write new table)",
824 }
825 }
826
827 is_loaded_ = true;
828 return absl::OkStatus();
829}
830
833
834 const auto& theme = AgentUI::GetTheme();
835 if (room_window_class_.ClassId == 0) {
836 room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
837 }
838
839 if (!is_loaded_) {
840 gui::PanelWindow loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);
841 loading_card.SetDefaultSize(400, 200);
842 if (loading_card.Begin()) {
843 ImGui::TextColored(theme.text_secondary_gray, "Loading dungeon data...");
844 ImGui::TextWrapped(
845 "Independent editor cards will appear once ROM data is loaded.");
846 }
847 loading_card.End();
848 return absl::OkStatus();
849 }
850
851 if (!IsWorkbenchWorkflowEnabled() || active_rooms_.Size > 0) {
853 }
854
855 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
856 // Delegate delete to current room viewer
857 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
858 viewer->DeleteSelectedObjects();
859 }
860 }
861
862 // Keyboard Shortcuts (only if not typing in a text field)
863 if (!ImGui::GetIO().WantTextInput) {
864 if (ImGui::GetIO().KeyCtrl && ImGui::GetIO().KeyShift &&
865 ImGui::IsKeyPressed(ImGuiKey_W, false)) {
867 }
868
869 // Room Cycling (Ctrl+Tab)
870 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && ImGui::GetIO().KeyCtrl) {
872 if (recent_rooms_.size() > 1) {
873 int current_idx = -1;
874 for (int i = 0; i < static_cast<int>(recent_rooms_.size()); ++i) {
876 current_idx = i;
877 break;
878 }
879 }
880 if (current_idx != -1) {
881 int next_idx;
882 if (ImGui::GetIO().KeyShift) {
883 next_idx =
884 (current_idx + 1) % static_cast<int>(recent_rooms_.size());
885 } else {
886 next_idx =
887 (current_idx - 1 + static_cast<int>(recent_rooms_.size())) %
888 static_cast<int>(recent_rooms_.size());
889 }
890 OnRoomSelected(recent_rooms_[next_idx]);
891 }
892 }
893 } else if (active_rooms_.size() > 1) {
894 int current_idx = -1;
895 for (int i = 0; i < active_rooms_.size(); ++i) {
897 current_idx = i;
898 break;
899 }
900 }
901
902 if (current_idx != -1) {
903 int next_idx;
904 if (ImGui::GetIO().KeyShift) {
905 next_idx =
906 (current_idx - 1 + active_rooms_.size()) % active_rooms_.size();
907 } else {
908 next_idx = (current_idx + 1) % active_rooms_.size();
909 }
910 OnRoomSelected(active_rooms_[next_idx]);
911 }
912 }
913 }
914
915 // Adjacent Room Navigation (Ctrl+Arrows)
916 if (ImGui::GetIO().KeyCtrl) {
917 int next_room = -1;
918 const int kCols = 16;
919
920 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
921 if (current_room_id_ >= kCols)
922 next_room = current_room_id_ - kCols;
923 } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
924 if (IsValidRoomId(current_room_id_ + kCols))
925 next_room = current_room_id_ + kCols;
926 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
927 if (current_room_id_ % kCols > 0)
928 next_room = current_room_id_ - 1;
929 } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
930 if (current_room_id_ % kCols < kCols - 1 &&
932 next_room = current_room_id_ + 1;
933 }
934
935 if (next_room != -1) {
937 OnRoomSelected(next_room, /*request_focus=*/false);
938 } else {
940 }
941 }
942 }
943 }
944
945 // Process any pending room swaps after all drawing is complete
946 // This prevents ImGui state corruption from modifying collections mid-frame
948
949 return absl::OkStatus();
950}
951
953 if (!status_bar)
954 return;
955 const int room_id = current_room_id();
956
957 if (room_id >= 0) {
958 StatusBarSegmentOptions room_opts;
959 room_opts.tooltip = "Click to refocus viewer on this room";
960 room_opts.on_click = [this, room_id]() {
964 }
965 };
966 status_bar->SetCustomSegment("Room", absl::StrFormat("0x%03X", room_id),
967 std::move(room_opts));
968 }
969
970 const int loaded = LoadedRoomCount();
971 const int total = TotalRoomCount();
972 if (total > 0) {
973 StatusBarSegmentOptions rooms_opts;
974 rooms_opts.tooltip =
975 absl::StrFormat("%d rooms loaded of %d total", loaded, total);
976 status_bar->SetCustomSegment("Rooms", absl::StrFormat("%d/%d", loaded, total),
977 std::move(rooms_opts));
978 }
979
980 StatusBarSegmentOptions mode_opts;
981 mode_opts.tooltip = "Click to toggle Workbench / Standalone workflow";
982 mode_opts.on_click = [this]() { ToggleWorkbenchWorkflowMode(true); };
983 status_bar->SetEditorMode(
984 IsWorkbenchWorkflowEnabled() ? "Workbench" : "Standalone",
985 std::move(mode_opts));
986}
987
988absl::Status DungeonEditorV2::Save() {
989 if (!rom_ || !rom_->is_loaded()) {
990 return absl::FailedPreconditionError("ROM not loaded");
991 }
992
993 const auto& flags = core::FeatureFlags::get().dungeon;
994
995 if (flags.kSavePalettes && gfx::PaletteManager::Get().HasUnsavedChanges()) {
996 auto status = gfx::PaletteManager::Get().SaveAllToRom();
997 if (!status.ok()) {
998 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
999 status.message().data());
1000 return status;
1001 }
1002 LOG_INFO("DungeonEditorV2", "Saved %zu modified colors to ROM",
1003 gfx::PaletteManager::Get().GetModifiedColorCount());
1004 }
1005
1006 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1007 absl::Status save_status = absl::OkStatus();
1008 rooms_.ForEachLoaded([&](int room_id, zelda3::Room&) {
1009 if (!save_status.ok()) {
1010 return;
1011 }
1012 auto status = SaveRoomData(room_id);
1013 if (!status.ok()) {
1014 save_status = status;
1015 }
1016 });
1017 if (!save_status.ok()) {
1018 return save_status;
1019 }
1020 }
1021
1022 if (flags.kSaveTorches) {
1023 auto status = zelda3::SaveAllTorches(
1024 rom_, static_cast<int>(rooms_.size()),
1025 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
1026 if (!status.ok()) {
1027 LOG_ERROR("DungeonEditorV2", "Failed to save torches: %s",
1028 status.message().data());
1029 return status;
1030 }
1031 }
1032
1033 if (flags.kSavePits) {
1034 auto status = zelda3::SaveAllPits(rom_);
1035 if (!status.ok()) {
1036 LOG_ERROR("DungeonEditorV2", "Failed to save pits: %s",
1037 status.message().data());
1038 return status;
1039 }
1040 }
1041
1042 if (flags.kSaveBlocks) {
1043 auto status = zelda3::SaveAllBlocks(rom_);
1044 if (!status.ok()) {
1045 LOG_ERROR("DungeonEditorV2", "Failed to save blocks: %s",
1046 status.message().data());
1047 return status;
1048 }
1049 }
1050
1051 if (flags.kSaveCollision) {
1052 auto status = zelda3::SaveAllCollision(
1053 rom_, static_cast<int>(rooms_.size()),
1054 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
1055 if (!status.ok()) {
1056 LOG_ERROR("DungeonEditorV2", "Failed to save collision: %s",
1057 status.message().data());
1058 return status;
1059 }
1060 }
1061
1062 if (flags.kSaveWaterFillZones) {
1063 auto status = SaveWaterFillZones(rom_, rooms_);
1064 if (!status.ok()) {
1065 LOG_ERROR("DungeonEditorV2", "Failed to save water fill zones: %s",
1066 status.message().data());
1067 return status;
1068 }
1069 }
1070
1071 if (flags.kSaveChests) {
1072 auto status = zelda3::SaveAllChests(
1073 rom_, static_cast<int>(rooms_.size()),
1074 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
1075 if (!status.ok()) {
1076 LOG_ERROR("DungeonEditorV2", "Failed to save chests: %s",
1077 status.message().data());
1078 return status;
1079 }
1080 }
1081
1082 if (flags.kSavePotItems) {
1083 auto status = zelda3::SaveAllPotItems(
1084 rom_, static_cast<int>(rooms_.size()),
1085 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
1086 if (!status.ok()) {
1087 LOG_ERROR("DungeonEditorV2", "Failed to save pot items: %s",
1088 status.message().data());
1089 return status;
1090 }
1091 }
1092
1093 return absl::OkStatus();
1094}
1095
1096std::vector<std::pair<uint32_t, uint32_t>> DungeonEditorV2::CollectWriteRanges()
1097 const {
1098 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1099
1100 if (!rom_ || !rom_->is_loaded()) {
1101 return ranges;
1102 }
1103
1104 const auto& flags = core::FeatureFlags::get().dungeon;
1105 const auto& rom_data = rom_->vector();
1106
1107 // Oracle of Secrets: the water fill table lives in a reserved tail region.
1108 // Include it in write-range reporting whenever we have dirty water fill data,
1109 // even if no rooms are currently loaded (SaveWaterFillZones() is room-indexed
1110 // and independent of room loading state).
1111 if (flags.kSaveWaterFillZones &&
1112 zelda3::kWaterFillTableEnd <= static_cast<int>(rom_data.size())) {
1113 bool any_dirty = false;
1114 rooms_.ForEachMaterialized([&](int, const zelda3::Room& room) {
1115 if (room.water_fill_dirty()) {
1116 any_dirty = true;
1117 }
1118 });
1119 if (any_dirty) {
1120 ranges.emplace_back(zelda3::kWaterFillTableStart,
1122 }
1123 }
1124
1125 // Custom collision writes update the pointer table and append blobs into the
1126 // expanded collision region. SaveAllCollision() is room-indexed, so include
1127 // these ranges whenever any room has dirty custom collision data.
1128 if (flags.kSaveCollision) {
1129 const int ptrs_size = zelda3::kNumberOfRooms * 3;
1130 const bool has_ptr_table =
1132 static_cast<int>(rom_data.size()));
1133 const bool has_data_region = (zelda3::kCustomCollisionDataSoftEnd <=
1134 static_cast<int>(rom_data.size()));
1135 if (has_ptr_table && has_data_region) {
1136 bool any_dirty = false;
1137 rooms_.ForEachMaterialized([&](int, const zelda3::Room& room) {
1138 if (room.custom_collision_dirty()) {
1139 any_dirty = true;
1140 }
1141 });
1142 if (any_dirty) {
1143 ranges.emplace_back(zelda3::kCustomCollisionRoomPointers,
1145 ranges.emplace_back(zelda3::kCustomCollisionDataPosition,
1147 }
1148 }
1149 }
1150
1151 rooms_.ForEachLoaded([&](int room_id, const zelda3::Room& room) {
1152 room_id = room.id();
1153
1154 // Header range
1155 if (flags.kSaveRoomHeaders) {
1156 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
1157 int header_ptr_table =
1158 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
1159 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
1161 header_ptr_table = yaze::SnesToPc(header_ptr_table);
1162 int table_offset = header_ptr_table + (room_id * 2);
1163
1164 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1165 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1166 (rom_data[table_offset + 1] << 8) |
1167 rom_data[table_offset];
1168 int header_location = yaze::SnesToPc(address);
1169 ranges.emplace_back(header_location, header_location + 14);
1170 }
1171 }
1172 }
1173
1174 // Object range
1175 if (flags.kSaveObjects) {
1176 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1177 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
1178 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
1180 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
1181 int entry_offset = obj_ptr_table + (room_id * 3);
1182
1183 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
1184 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1185 (rom_data[entry_offset + 1] << 8) |
1186 rom_data[entry_offset];
1187 int objects_location = yaze::SnesToPc(tile_addr);
1188
1189 auto encoded = room.EncodeObjects();
1190 ranges.emplace_back(objects_location,
1191 objects_location + encoded.size() + 2);
1192 }
1193 }
1194 }
1195 });
1196
1197 return ranges;
1198}
1199
1200absl::Status DungeonEditorV2::SaveRoom(int room_id) {
1201 if (!rom_ || !rom_->is_loaded()) {
1202 return absl::FailedPreconditionError("ROM not loaded");
1203 }
1204 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1205 return absl::InvalidArgumentError("Invalid room ID");
1206 }
1207
1208 const auto& flags = core::FeatureFlags::get().dungeon;
1209 if (flags.kSavePalettes && gfx::PaletteManager::Get().HasUnsavedChanges()) {
1210 auto status = gfx::PaletteManager::Get().SaveAllToRom();
1211 if (!status.ok()) {
1212 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
1213 status.message().data());
1214 return status;
1215 }
1216 }
1217 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1218 RETURN_IF_ERROR(SaveRoomData(room_id));
1219 }
1220
1221 if (flags.kSaveTorches) {
1223 rom_, static_cast<int>(rooms_.size()),
1224 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1225 }
1226 if (flags.kSavePits) {
1228 }
1229 if (flags.kSaveBlocks) {
1231 }
1232 if (flags.kSaveCollision) {
1234 rom_, static_cast<int>(rooms_.size()),
1235 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1236 }
1237 if (flags.kSaveWaterFillZones) {
1238 RETURN_IF_ERROR(SaveWaterFillZones(rom_, rooms_));
1239 }
1240 if (flags.kSaveChests) {
1242 rom_, static_cast<int>(rooms_.size()),
1243 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1244 }
1245 if (flags.kSavePotItems) {
1247 rom_, static_cast<int>(rooms_.size()),
1248 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1249 }
1250
1251 return absl::OkStatus();
1252}
1253
1255 return rooms_.LoadedCount();
1256}
1257
1258absl::Status DungeonEditorV2::SaveRoomData(int room_id) {
1259 if (!rom_ || !rom_->is_loaded()) {
1260 return absl::FailedPreconditionError("ROM not loaded");
1261 }
1262 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1263 return absl::InvalidArgumentError("Invalid room ID");
1264 }
1265
1266 auto& room = rooms_[room_id];
1267 if (!room.IsLoaded()) {
1268 return absl::OkStatus();
1269 }
1270
1271 // ROM safety: validate loaded room content before writing any bytes.
1272 {
1273 zelda3::DungeonValidator validator;
1274 const auto result = validator.ValidateRoom(room);
1275 for (const auto& w : result.warnings) {
1276 LOG_WARN("DungeonEditorV2", "Room 0x%03X validation warning: %s", room_id,
1277 w.c_str());
1278 }
1279 if (!result.is_valid) {
1280 for (const auto& e : result.errors) {
1281 LOG_ERROR("DungeonEditorV2", "Room 0x%03X validation error: %s",
1282 room_id, e.c_str());
1283 }
1286 absl::StrFormat(
1287 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1288 room_id, result.errors.size()),
1290 }
1291 return absl::FailedPreconditionError(
1292 absl::StrFormat("Room 0x%03X failed validation", room_id));
1293 }
1294 }
1295
1296 const auto& flags = core::FeatureFlags::get().dungeon;
1297
1298 // HACK MANIFEST VALIDATION
1300 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1301 const auto& manifest = dependencies_.project->hack_manifest;
1302 const auto& rom_data = rom_->vector();
1303
1304 // 1. Validate Header Range
1305 if (flags.kSaveRoomHeaders) {
1306 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
1307 int header_ptr_table =
1308 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
1309 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
1311 header_ptr_table = yaze::SnesToPc(header_ptr_table);
1312 int table_offset = header_ptr_table + (room_id * 2);
1313
1314 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1315 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1316 (rom_data[table_offset + 1] << 8) |
1317 rom_data[table_offset];
1318 int header_location = yaze::SnesToPc(address);
1319 ranges.emplace_back(header_location, header_location + 14);
1320 }
1321 }
1322 }
1323
1324 // 2. Validate Object Range
1325 if (flags.kSaveObjects) {
1326 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1327 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
1328 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
1330 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
1331 int entry_offset = obj_ptr_table + (room_id * 3);
1332
1333 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
1334 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1335 (rom_data[entry_offset + 1] << 8) |
1336 rom_data[entry_offset];
1337 int objects_location = yaze::SnesToPc(tile_addr);
1338
1339 // Estimate size based on current encoding
1340 // Note: we check the *target* location (where we will write)
1341 // The EncodeObjects() size is what we *will* write.
1342 // We add 2 bytes for the size/header that SaveObjects writes.
1343 auto encoded = room.EncodeObjects();
1344 ranges.emplace_back(objects_location,
1345 objects_location + encoded.size() + 2);
1346 }
1347 }
1348 }
1349
1350 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1351 // address space (LoROM), so convert before analysis.
1352 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1354 manifest, write_policy, ranges, absl::StrFormat("room 0x%03X", room_id),
1355 "DungeonEditorV2", dependencies_.toast_manager));
1356 }
1357
1358 if (flags.kSaveObjects) {
1359 auto status = room.SaveObjects();
1360 if (!status.ok()) {
1361 LOG_ERROR("DungeonEditorV2", "Failed to save room objects: %s",
1362 status.message().data());
1363 return status;
1364 }
1365 }
1366
1367 if (flags.kSaveSprites) {
1368 auto status = room.SaveSprites();
1369 if (!status.ok()) {
1370 LOG_ERROR("DungeonEditorV2", "Failed to save room sprites: %s",
1371 status.message().data());
1372 return status;
1373 }
1374 }
1375
1376 if (flags.kSaveRoomHeaders) {
1377 auto status = room.SaveRoomHeader();
1378 if (!status.ok()) {
1379 LOG_ERROR("DungeonEditorV2", "Failed to save room header: %s",
1380 status.message().data());
1381 return status;
1382 }
1383 }
1384
1385 if (flags.kSaveObjects && dungeon_editor_system_) {
1386 auto sys_status = dungeon_editor_system_->SaveRoom(room.id());
1387 if (!sys_status.ok()) {
1388 LOG_ERROR("DungeonEditorV2", "Failed to save room system data: %s",
1389 sys_status.message().data());
1390 }
1391 }
1392
1393 return absl::OkStatus();
1394}
1395
1397 if (!core::FeatureFlags::get().dungeon.kUseWorkbench) {
1398 return false;
1399 }
1401 return true;
1402 }
1403 return dependencies_.window_manager->IsWindowOpen("dungeon.workbench");
1404}
1405
1406void DungeonEditorV2::SetWorkbenchWorkflowMode(bool enabled, bool show_toast) {
1407 auto* window_manager = dependencies_.window_manager;
1408 if (!window_manager) {
1409 return;
1410 }
1411
1412 const size_t session_id = window_manager->GetActiveSessionId();
1413 const bool was_enabled = IsWorkbenchWorkflowEnabled();
1414
1415 if (enabled) {
1416 window_manager->OpenWindow(session_id, "dungeon.workbench");
1417
1418 // Hide standalone workflow windows unless explicitly pinned.
1419 for (const auto& descriptor :
1420 window_manager->GetWindowsInCategory(session_id, "Dungeon")) {
1421 const std::string& card_id = descriptor.card_id;
1422 if (card_id == "dungeon.workbench") {
1423 continue;
1424 }
1425 if (window_manager->IsWindowPinned(session_id, card_id)) {
1426 continue;
1427 }
1428 const bool is_room_window = card_id.rfind("dungeon.room_", 0) == 0;
1429 if (card_id == kRoomSelectorId || card_id == kRoomMatrixId ||
1430 is_room_window) {
1431 window_manager->CloseWindow(session_id, card_id);
1432 }
1433 }
1434 } else {
1435 window_manager->CloseWindow(session_id, "dungeon.workbench");
1436 window_manager->OpenWindow(session_id, kRoomSelectorId);
1437 window_manager->OpenWindow(session_id, kRoomMatrixId);
1438 if (current_room_id_ >= 0) {
1440 }
1441 }
1442
1443 if (show_toast && dependencies_.toast_manager && was_enabled != enabled) {
1445 enabled ? "Dungeon workflow: Workbench"
1446 : "Dungeon workflow: Standalone Panels",
1448 }
1449}
1450
1452 bool show_toast) {
1456}
1457
1461
1463 for (int i = 0; i < active_rooms_.Size; i++) {
1464 int room_id = active_rooms_[i];
1465 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1466 bool panel_visible = true;
1468 panel_visible = dependencies_.window_manager->IsWindowOpen(card_id);
1469 }
1470
1471 if (!panel_visible) {
1473 room_cards_.erase(room_id);
1474 active_rooms_.erase(active_rooms_.Data + i);
1475 ReleaseRoomPanelSlotId(room_id);
1476 // Clean up viewer
1477 room_viewers_.Erase(room_id);
1478 i--;
1479 continue;
1480 }
1481
1482 bool is_pinned = dependencies_.window_manager &&
1484 std::string active_category =
1487 : "";
1488
1489 if (active_category != "Dungeon" && !is_pinned) {
1490 continue;
1491 }
1492
1493 // Ensure room card exists (should have been created by ShowRoomPanel/ShowPanel)
1494 if (room_cards_.find(room_id) == room_cards_.end()) {
1495 ShowRoomPanel(room_id);
1496 }
1497
1498 auto& room_card = room_cards_[room_id];
1499 bool open = true;
1500
1501 ImGui::SetNextWindowClass(&room_window_class_);
1502 if (room_dock_id_ == 0) {
1503 room_dock_id_ = ImGui::GetID("DungeonRoomDock");
1504 }
1505 ImGui::SetNextWindowDockID(room_dock_id_, ImGuiCond_FirstUseEver);
1506
1507 if (room_card->Begin(&open)) {
1508 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) {
1509 OnRoomSelected(room_id, /*request_focus=*/false);
1510 }
1511 DrawRoomTab(room_id);
1512 }
1513 room_card->End();
1514
1515 if (!open) {
1518 }
1519
1520 room_cards_.erase(room_id);
1521 active_rooms_.erase(active_rooms_.Data + i);
1522 room_viewers_.Erase(room_id);
1523 ReleaseRoomPanelSlotId(room_id);
1524 i--;
1525 }
1526 }
1527}
1528
1530 if (auto it = room_panel_slot_ids_.find(room_id);
1531 it != room_panel_slot_ids_.end()) {
1532 return it->second;
1533 }
1534 const int slot_id = next_room_panel_slot_id_++;
1535 room_panel_slot_ids_[room_id] = slot_id;
1536 return slot_id;
1537}
1538
1540 room_panel_slot_ids_.erase(room_id);
1541}
1542
1544 const auto& theme = AgentUI::GetTheme();
1545 if (room_id < 0 || room_id >= 0x128) {
1546 ImGui::Text("Invalid room ID: %d", room_id);
1547 return;
1548 }
1549
1550 auto& room = rooms_[room_id];
1551
1552 if (!room.IsLoaded()) {
1553 auto status = room_loader_.LoadRoom(room_id, room);
1554 if (!status.ok()) {
1555 ImGui::TextColored(theme.text_error_red, "Failed to load room: %s",
1556 status.message().data());
1557 return;
1558 }
1559
1561 auto sys_status = dungeon_editor_system_->ReloadRoom(room_id);
1562 if (!sys_status.ok()) {
1563 LOG_ERROR("DungeonEditorV2", "Failed to load system data: %s",
1564 sys_status.message().data());
1565 }
1566 }
1567 }
1568
1569 if (room.IsLoaded()) {
1570 bool needs_render = false;
1571
1572 if (room.blocks().empty()) {
1573 room.LoadRoomGraphics(room.blockset());
1574 needs_render = true;
1575 }
1576
1577 if (!room.AreObjectsLoaded()) {
1578 room.LoadObjects();
1579 needs_render = true;
1580 }
1581
1582 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1583 if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
1584 room.RenderRoomGraphics();
1585 }
1586 }
1587
1588 if (room.IsLoaded()) {
1589 ImGui::TextColored(theme.text_success_green, ICON_MD_CHECK " Loaded");
1590 } else {
1591 ImGui::TextColored(theme.text_error_red, ICON_MD_PENDING " Not Loaded");
1592 }
1593 ImGui::SameLine();
1594 ImGui::TextDisabled("Objects: %zu", room.GetTileObjects().size());
1595
1596 if (core::FeatureFlags::get().dungeon.kUseWorkbench &&
1598 ImGui::SameLine();
1599 const std::string return_label = absl::StrFormat(
1600 ICON_MD_WORKSPACES " Workbench##return_workbench_%03X", room_id);
1601 if (ImGui::SmallButton(return_label.c_str())) {
1603 }
1604 if (ImGui::IsItemHovered()) {
1605 ImGui::SetTooltip(
1606 "Switch back to the integrated Dungeon Workbench workflow "
1607 "(Ctrl+Shift+W).");
1608 }
1609 }
1610
1611 // Warp to Room button — sends room ID to running Mesen2 emulator
1612 ImGui::SameLine();
1613 {
1615 bool connected = client && client->IsConnected();
1616 if (!connected) {
1617 ImGui::BeginDisabled();
1618 }
1619 std::string warp_label =
1620 absl::StrFormat(ICON_MD_ROCKET_LAUNCH " Warp##warp_%03X", room_id);
1621 if (ImGui::SmallButton(warp_label.c_str())) {
1622 auto status = client->WriteWord(0x7E00A0, static_cast<uint16_t>(room_id));
1623 if (status.ok()) {
1626 absl::StrFormat("Warped to room 0x%03X", room_id),
1628 }
1629 } else {
1632 absl::StrFormat("Warp failed: %s", status.message()),
1634 }
1635 }
1636 }
1637 if (!connected) {
1638 ImGui::EndDisabled();
1639 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1640 ImGui::SetTooltip("Connect to Mesen2 first");
1641 }
1642 } else {
1643 if (ImGui::IsItemHovered()) {
1644 ImGui::SetTooltip("Warp to room 0x%03X in Mesen2", room_id);
1645 }
1646 }
1647 }
1648
1649 ImGui::Separator();
1650
1651 // Use per-room viewer
1652 if (auto* viewer = GetViewerForRoom(room_id)) {
1653 viewer->DrawDungeonCanvas(room_id);
1654 }
1655}
1656
1658 switch (intent) {
1660 OnRoomSelected(room_id, /*request_focus=*/true);
1661 break;
1663 // Explicitly pivot to panel workflow when user asks for standalone room.
1664 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1665 return;
1666 }
1669 }
1670 // Update shared state (same as OnRoomSelected with request_focus=true)
1671 OnRoomSelected(room_id, /*request_focus=*/false);
1672 // Now force-open a standalone panel for this room
1673 ShowRoomPanel(room_id);
1674 break;
1675 }
1677 OnRoomSelected(room_id, /*request_focus=*/false);
1678 break;
1679 }
1680}
1681
1682void DungeonEditorV2::OnRoomSelected(int room_id, bool request_focus) {
1683 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1684 LOG_WARN("DungeonEditorV2", "Ignoring invalid room selection: %d", room_id);
1685 return;
1686 }
1687 if (room_id != current_room_id_ && workbench_panel_) {
1689 }
1690 current_room_id_ = room_id;
1691 room_selector_.set_current_room_id(static_cast<uint16_t>(room_id));
1692
1693 // Track recent rooms (remove if already present, add to front)
1694 recent_rooms_.erase(
1695 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
1696 recent_rooms_.end());
1697 recent_rooms_.push_front(room_id);
1698 if (recent_rooms_.size() > kMaxRecentRooms) {
1699 recent_rooms_.pop_back();
1700 }
1701
1703 dungeon_editor_system_->SetExternalRoom(&rooms_[room_id]);
1704 }
1705
1706 // Update all sub-panels (Object Editor, Sprite Editor, etc.)
1707 SyncPanelsToRoom(room_id);
1708
1709 // Sync palette with current room (must happen before early return for focus changes)
1710
1711 // Sync palette with current room (must happen before early return for focus changes)
1712 if (room_id >= 0 && room_id < (int)rooms_.size()) {
1713 auto& room = rooms_[room_id];
1714 if (!room.IsLoaded()) {
1715 room_loader_.LoadRoom(room_id, room);
1716 }
1717
1718 if (room.IsLoaded()) {
1719 current_palette_id_ = room.ResolveDungeonPaletteId();
1722
1723 // Update viewer and object editor palette
1724 if (auto* viewer = GetViewerForRoom(room_id)) {
1725 viewer->SetCurrentPaletteId(current_palette_id_);
1726
1727 if (game_data()) {
1728 auto dungeon_main_pal_group =
1730 if (current_palette_id_ < (int)dungeon_main_pal_group.size()) {
1731 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1732 auto result =
1734 if (result.ok()) {
1735 current_palette_group_ = result.value();
1736 viewer->SetCurrentPaletteGroup(current_palette_group_);
1740 }
1741 // Sync palette to graphics panel for proper sheet coloring
1745 }
1746 }
1747 }
1748 }
1749 }
1750 }
1751 }
1752
1753 // Workbench mode uses a single stable window and does not spawn per-room
1754 // panels. Keep selection + panels in sync and return.
1756 if (dependencies_.window_manager && request_focus) {
1757 // Only force-show if it's already visible or if it's the first initialization
1758 // This avoids obtrusive behavior when the user explicitly closed it.
1759 if (dependencies_.window_manager->IsWindowOpen("dungeon.workbench")) {
1760 dependencies_.window_manager->OpenWindow("dungeon.workbench");
1761 }
1762 }
1763 return;
1764 }
1765
1766 // Check if room is already open
1767 for (int i = 0; i < active_rooms_.Size; i++) {
1768 if (active_rooms_[i] == room_id) {
1769 // Always ensure panel is visible, even if already in active_rooms_
1771 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1773 }
1774 if (request_focus) {
1775 FocusRoom(room_id);
1776 }
1777 return;
1778 }
1779 }
1780
1781 active_rooms_.push_back(room_id);
1783
1785 // Use unified ResourceLabelProvider for room names
1786 std::string room_name = absl::StrFormat(
1787 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
1788
1789 std::string base_card_id = absl::StrFormat("dungeon.room_%d", room_id);
1790
1792 {.card_id = base_card_id,
1793 .display_name = room_name,
1794 .window_title = ICON_MD_GRID_ON " " + room_name,
1795 .icon = ICON_MD_GRID_ON,
1796 .category = "Dungeon",
1797 .shortcut_hint = "",
1798 .visibility_flag = nullptr,
1799 .priority = 200 + room_id});
1800
1802 }
1803}
1804
1806 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
1807 return;
1808 }
1809 int room_id = entrances_[entrance_id].room_;
1810 OnRoomSelected(room_id);
1811}
1812
1814 auto status = Save();
1815 if (status.ok()) {
1818 }
1819 } else {
1820 LOG_ERROR("DungeonEditorV2", "SaveAllRooms failed: %s",
1821 status.message().data());
1824 absl::StrFormat("Save all failed: %s", status.message()),
1826 }
1827 }
1828}
1829
1830void DungeonEditorV2::add_room(int room_id) {
1831 OnRoomSelected(room_id);
1832}
1833
1835 auto it = room_cards_.find(room_id);
1836 if (it != room_cards_.end()) {
1837 it->second->Focus();
1838 }
1839}
1840
1847
1849 if (enabled && dependencies_.window_manager) {
1851 OpenWindow("dungeon.workbench");
1852 } else {
1854 }
1860 }
1861 }
1862}
1863
1867
1870 LOG_ERROR("DungeonEditorV2", "Cannot place object: Invalid room ID %d",
1874 absl::StrFormat("Object 0x%02X: no room selected (invalid room %d)",
1875 obj.id_, current_room_id_),
1877 }
1880 "Cannot place 0x%02X: invalid room %d", obj.id_, current_room_id_));
1881 }
1882 return;
1883 }
1884
1885 auto& room = rooms_[current_room_id_];
1886
1887 LOG_INFO("DungeonEditorV2",
1888 "Placing object ID=0x%02X at position (%d,%d) in room %03X", obj.id_,
1889 obj.x_, obj.y_, current_room_id_);
1890
1891 room.RenderRoomGraphics();
1892 LOG_DEBUG("DungeonEditorV2",
1893 "Object placed and room re-rendered successfully");
1894 // Brief success feedback so the user knows the placement was accepted.
1895 // Kept minimal (no inline panel update) — success is non-spammy by design.
1898 absl::StrFormat("Placed 0x%02X in room %03X", obj.id_,
1901 }
1902}
1903
1905 int room_id, const zelda3::RoomObject& object) {
1906 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1907 LOG_WARN("DungeonEditorV2", "Edit Graphics ignored (invalid room id %d)",
1908 room_id);
1909 return;
1910 }
1911
1912 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
1913 if (!editor_manager) {
1914 LOG_WARN("DungeonEditorV2",
1915 "Edit Graphics ignored (editor manager unavailable)");
1916 return;
1917 }
1918
1919 auto& room = rooms_[room_id];
1920 room.LoadRoomGraphics(room.blockset());
1921
1922 uint16_t sheet_id = 0;
1923 uint16_t tile_index = 0;
1924 bool resolved_sheet = false;
1925 if (auto tiles_or = object.GetTiles();
1926 tiles_or.ok() && !tiles_or.value().empty()) {
1927 const uint16_t tile_id = tiles_or.value().front().id_;
1928 const size_t block_index = static_cast<size_t>(tile_id / 64);
1929 const auto blocks = room.blocks();
1930 if (block_index < blocks.size()) {
1931 sheet_id = blocks[block_index];
1932 resolved_sheet = true;
1933 }
1934 const int tiles_per_row = gfx::kTilesheetWidth / 8;
1935 const int tiles_per_col = gfx::kTilesheetHeight / 8;
1936 const int tiles_per_sheet = tiles_per_row * tiles_per_col;
1937 if (tiles_per_sheet > 0) {
1938 tile_index = static_cast<uint16_t>(tile_id % tiles_per_sheet);
1939 }
1940 }
1941
1942 editor_manager->SwitchToEditor(EditorType::kGraphics, true);
1943 if (auto* editor_set = editor_manager->GetCurrentEditorSet()) {
1944 if (auto* graphics = editor_set->GetGraphicsEditor()) {
1945 if (resolved_sheet) {
1946 graphics->SelectSheet(sheet_id);
1947 graphics->HighlightTile(sheet_id, tile_index,
1948 absl::StrFormat("Object 0x%02X", object.id_));
1949 }
1950 }
1951 }
1952
1955 }
1956}
1957
1958void DungeonEditorV2::SwapRoomInPanel(int old_room_id, int new_room_id) {
1959 // Defer the swap until after the current frame's draw phase completes
1960 // This prevents modifying data structures while ImGui is still using them
1961 if (new_room_id < 0 || new_room_id >= static_cast<int>(rooms_.size())) {
1962 return;
1963 }
1964 pending_swap_.old_room_id = old_room_id;
1965 pending_swap_.new_room_id = new_room_id;
1966 pending_swap_.pending = true;
1967}
1968
1970 if (!pending_swap_.pending) {
1971 return;
1972 }
1973
1974 int old_room_id = pending_swap_.old_room_id;
1975 int new_room_id = pending_swap_.new_room_id;
1976 pending_swap_.pending = false;
1977
1978 // Find the position of old_room in active_rooms_
1979 int swap_index = -1;
1980 for (int i = 0; i < active_rooms_.Size; i++) {
1981 if (active_rooms_[i] == old_room_id) {
1982 swap_index = i;
1983 break;
1984 }
1985 }
1986
1987 if (swap_index < 0) {
1988 // Old room not found in active rooms, just select the new one
1989 OnRoomSelected(new_room_id);
1990 return;
1991 }
1992
1993 // Avoid swapping into an already-open room panel (the per-room maps assume
1994 // room IDs are unique). In this case, just focus/select the existing room.
1995 for (int i = 0; i < active_rooms_.Size; i++) {
1996 if (i != swap_index && active_rooms_[i] == new_room_id) {
1997 OnRoomSelected(new_room_id);
1998 return;
1999 }
2000 }
2001
2002 // Preserve the old panel's stable ImGui window identity by transferring its
2003 // slot id to the new room id.
2004 int slot_id = -1;
2005 if (auto it = room_panel_slot_ids_.find(old_room_id);
2006 it != room_panel_slot_ids_.end()) {
2007 slot_id = it->second;
2008 room_panel_slot_ids_.erase(it);
2009 } else {
2010 slot_id = next_room_panel_slot_id_++;
2011 }
2012 room_panel_slot_ids_[new_room_id] = slot_id;
2013
2014 // Preserve the viewer instance so canvas pan/zoom and UI state don't reset
2015 // when navigating with arrows (swap-in-panel).
2016 room_viewers_.Rename(old_room_id, new_room_id);
2017
2018 // Replace old room with new room in active_rooms_
2019 active_rooms_[swap_index] = new_room_id;
2021
2022 // Unregister old panel
2024 std::string old_card_id = absl::StrFormat("dungeon.room_%d", old_room_id);
2025 const bool old_pinned =
2028
2029 // Register new panel
2030 // Use unified ResourceLabelProvider for room names
2031 std::string new_room_name = absl::StrFormat(
2032 "[%03X] %s", new_room_id, zelda3::GetRoomLabel(new_room_id).c_str());
2033
2034 std::string new_card_id = absl::StrFormat("dungeon.room_%d", new_room_id);
2035
2037 {.card_id = new_card_id,
2038 .display_name = new_room_name,
2039 .window_title = ICON_MD_GRID_ON " " + new_room_name,
2040 .icon = ICON_MD_GRID_ON,
2041 .category = "Dungeon",
2042 .shortcut_hint = "",
2043 .visibility_flag = nullptr,
2044 .priority = 200 + new_room_id});
2045
2046 if (old_pinned) {
2047 dependencies_.window_manager->SetWindowPinned(new_card_id, true);
2048 }
2050 }
2051
2052 // Clean up old room's card and viewer
2053 room_cards_.erase(old_room_id);
2054
2055 // Update current selection
2056 OnRoomSelected(new_room_id, /*request_focus=*/false);
2057}
2058
2061 return;
2062 }
2063
2064 const bool enabled = pending_workflow_mode_.enabled;
2065 const bool show_toast = pending_workflow_mode_.show_toast;
2067 SetWorkbenchWorkflowMode(enabled, show_toast);
2068}
2069
2071 room_viewers_.Touch(room_id);
2072}
2073
2075 // No-op: LruCache handles removal internally via Erase()
2076 (void)room_id;
2077}
2078
2081 (void)room_id;
2082 return GetWorkbenchViewer();
2083 }
2084
2085 // Set eviction predicate to protect active rooms from eviction
2086 room_viewers_.SetEvictionPredicate([this](const int& candidate) {
2087 for (int i = 0; i < active_rooms_.size(); ++i) {
2088 if (active_rooms_[i] == candidate)
2089 return false;
2090 }
2091 return true;
2092 });
2093
2094 if (auto* existing = room_viewers_.Get(room_id)) {
2095 // Viewer already exists - Get() already touched LRU
2096 auto* viewer_ptr = existing->get();
2097
2098 // Update pinned state from manager
2100 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2101 viewer_ptr->SetPinned(
2103 viewer_ptr->SetPinCallback([this, card_id, room_id](bool pinned) {
2105 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2106 if (auto* v = GetViewerForRoom(room_id)) {
2107 v->SetPinned(pinned);
2108 }
2109 }
2110 });
2111 }
2112
2113 return viewer_ptr;
2114 }
2115
2116 // Creating a new viewer - Insert will handle LRU and eviction
2117 {
2118 auto viewer = std::make_unique<DungeonCanvasViewer>(rom_);
2119 viewer->SetCompactHeaderMode(false);
2120 viewer->SetRoomDetailsExpanded(true);
2121 DungeonCanvasViewer* viewer_ptr = viewer.get();
2122 viewer->SetRooms(&rooms_);
2123 viewer->SetRenderer(renderer_);
2124 viewer->SetCurrentPaletteGroup(current_palette_group_);
2125 viewer->SetCurrentPaletteId(current_palette_id_);
2126 viewer->SetGameData(game_data_);
2127
2128 // These hooks must remain correct even when a room panel swaps rooms while
2129 // keeping the same viewer instance (to preserve canvas pan/zoom + UI
2130 // state). Use the viewer's best-effort current room context instead of
2131 // capturing room_id at creation time.
2132 viewer->object_interaction().SetMutationCallback([this, viewer_ptr]() {
2133 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2134 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2135 const auto domain =
2137 if (domain == MutationDomain::kTileObjects) {
2138 BeginUndoSnapshot(rid);
2139 } else if (domain == MutationDomain::kCustomCollision) {
2140 BeginCollisionUndoSnapshot(rid);
2141 } else if (domain == MutationDomain::kWaterFill) {
2142 BeginWaterFillUndoSnapshot(rid);
2143 }
2144 }
2145 });
2146
2147 viewer->object_interaction().SetCacheInvalidationCallback([this,
2148 viewer_ptr]() {
2149 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2150 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2151 const auto domain =
2153 if (domain == MutationDomain::kTileObjects) {
2154 rooms_[rid].MarkObjectsDirty();
2155 rooms_[rid].RenderRoomGraphics();
2156 // Drag edits invalidate incrementally; finalize once the drag ends
2157 // (TileObjectHandler emits an extra invalidation on release).
2158 const auto mode =
2159 viewer_ptr->object_interaction().mode_manager().GetMode();
2161 FinalizeUndoAction(rid);
2162 }
2163 } else if (domain == MutationDomain::kCustomCollision) {
2164 const auto mode =
2165 viewer_ptr->object_interaction().mode_manager().GetMode();
2166 const auto& st =
2168 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2169 return;
2170 }
2171 FinalizeCollisionUndoAction(rid);
2172 } else if (domain == MutationDomain::kWaterFill) {
2173 const auto mode =
2174 viewer_ptr->object_interaction().mode_manager().GetMode();
2175 const auto& st =
2177 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2178 return;
2179 }
2180 FinalizeWaterFillUndoAction(rid);
2181 }
2182 }
2183 });
2184
2185 viewer->object_interaction().SetObjectPlacedCallback(
2186 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2187
2188 if (dungeon_editor_system_) {
2189 viewer->SetEditorSystem(dungeon_editor_system_.get());
2190 }
2191 viewer->SetRoomNavigationCallback([this](int target_room) {
2192 if (target_room >= 0 && target_room < static_cast<int>(rooms_.size())) {
2193 OnRoomSelected(target_room);
2194 }
2195 });
2196 // Swap callback swaps the room in the current panel instead of opening new
2197 viewer->SetRoomSwapCallback([this](int old_room, int new_room) {
2198 SwapRoomInPanel(old_room, new_room);
2199 });
2200 viewer->SetShowObjectPanelCallback(
2201 [this]() { OpenWindow(kObjectSelectorId); });
2202 viewer->SetShowSpritePanelCallback(
2203 [this]() { OpenWindow("dungeon.sprite_editor"); });
2204 viewer->SetShowItemPanelCallback(
2205 [this]() { OpenWindow("dungeon.item_editor"); });
2206 viewer->SetShowRoomListCallback([this]() {
2207 OpenWindow(IsWorkbenchWorkflowEnabled() ? "dungeon.workbench"
2208 : kRoomSelectorId);
2209 });
2210 viewer->SetShowRoomMatrixCallback([this]() { OpenWindow(kRoomMatrixId); });
2211 viewer->SetShowEntranceListCallback(
2212 [this]() { OpenWindow(kEntranceListId); });
2213 viewer->SetShowRoomGraphicsCallback(
2214 [this]() { OpenWindow(kRoomGraphicsId); });
2215 viewer->SetShowDungeonSettingsCallback(
2216 [this]() { OpenWindow("dungeon.settings"); });
2217 viewer->SetEditGraphicsCallback(
2218 [this](int target_room_id, const zelda3::RoomObject& object) {
2219 OpenGraphicsEditorForObject(target_room_id, object);
2220 });
2221 viewer->SetSaveRoomCallback([this](int target_room_id) {
2222 auto status = SaveRoom(target_room_id);
2223 if (!status.ok()) {
2224 LOG_ERROR("DungeonEditorV2", "Save Room failed: %s",
2225 status.message().data());
2226 if (dependencies_.toast_manager) {
2227 dependencies_.toast_manager->Show(
2228 absl::StrFormat("Save Room failed: %s", status.message()),
2230 }
2231 return;
2232 }
2233 if (dependencies_.toast_manager) {
2234 dependencies_.toast_manager->Show("Room saved", ToastType::kSuccess);
2235 }
2236 });
2237
2238 // Wire up pinning for room panels
2239 if (dependencies_.window_manager) {
2240 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2241 viewer->SetPinned(dependencies_.window_manager->IsWindowPinned(card_id));
2242 viewer->SetPinCallback([this, card_id, room_id](bool pinned) {
2243 if (dependencies_.window_manager) {
2244 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2245 // Sync state back to viewer in all panels showing this room
2246 if (auto* v = GetViewerForRoom(room_id)) {
2247 v->SetPinned(pinned);
2248 }
2249 }
2250 });
2251 }
2252
2253 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2254 viewer->SetProject(dependencies_.project);
2255
2256 auto* stored = room_viewers_.Insert(room_id, std::move(viewer));
2257 return stored->get();
2258 }
2259}
2260
2261DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchViewer() {
2262 if (!workbench_viewer_) {
2263 workbench_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2264 auto* viewer = workbench_viewer_.get();
2265 viewer->SetCompactHeaderMode(true);
2266 viewer->SetRoomDetailsExpanded(false);
2267 viewer->SetHeaderVisible(false);
2268 viewer->SetHeaderHiddenMetadataHudVisible(false);
2269 viewer->SetRooms(&rooms_);
2270 viewer->SetRenderer(renderer_);
2271 viewer->SetCurrentPaletteGroup(current_palette_group_);
2272 viewer->SetCurrentPaletteId(current_palette_id_);
2273 viewer->SetGameData(game_data_);
2274
2275 // Workbench uses a single viewer; these hooks use the viewer's current room
2276 // context (set at DrawDungeonCanvas start) so room switching stays correct.
2277 viewer->object_interaction().SetMutationCallback([this, viewer]() {
2278 const int rid = viewer ? viewer->current_room_id() : -1;
2279 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2280 const auto domain = viewer->object_interaction().last_mutation_domain();
2281 if (domain == MutationDomain::kTileObjects) {
2282 BeginUndoSnapshot(rid);
2283 } else if (domain == MutationDomain::kCustomCollision) {
2284 BeginCollisionUndoSnapshot(rid);
2285 } else if (domain == MutationDomain::kWaterFill) {
2286 BeginWaterFillUndoSnapshot(rid);
2287 }
2288 }
2289 });
2290 viewer->object_interaction().SetCacheInvalidationCallback([this, viewer]() {
2291 const int rid = viewer ? viewer->current_room_id() : -1;
2292 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2293 const auto domain =
2294 viewer->object_interaction().last_invalidation_domain();
2295 if (domain == MutationDomain::kTileObjects) {
2296 rooms_[rid].MarkObjectsDirty();
2297 rooms_[rid].RenderRoomGraphics();
2298 const auto mode =
2299 viewer->object_interaction().mode_manager().GetMode();
2300 if (mode != InteractionMode::DraggingObjects) {
2301 FinalizeUndoAction(rid);
2302 }
2303 } else if (domain == MutationDomain::kCustomCollision) {
2304 const auto mode =
2305 viewer->object_interaction().mode_manager().GetMode();
2306 const auto& st =
2307 viewer->object_interaction().mode_manager().GetModeState();
2308 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2309 return;
2310 }
2311 FinalizeCollisionUndoAction(rid);
2312 } else if (domain == MutationDomain::kWaterFill) {
2313 const auto mode =
2314 viewer->object_interaction().mode_manager().GetMode();
2315 const auto& st =
2316 viewer->object_interaction().mode_manager().GetModeState();
2317 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2318 return;
2319 }
2320 FinalizeWaterFillUndoAction(rid);
2321 }
2322 }
2323 });
2324
2325 viewer->object_interaction().SetObjectPlacedCallback(
2326 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2327
2328 if (dungeon_editor_system_) {
2329 viewer->SetEditorSystem(dungeon_editor_system_.get());
2330 }
2331
2332 // In workbench mode, arrow navigation swaps the current room without
2333 // changing window identities.
2334 viewer->SetRoomSwapCallback([this](int /*old_room*/, int new_room) {
2335 OnRoomSelected(new_room, /*request_focus=*/false);
2336 });
2337 viewer->SetRoomNavigationCallback([this](int target_room) {
2338 OnRoomSelected(target_room, /*request_focus=*/false);
2339 });
2340
2341 viewer->SetShowObjectPanelCallback(
2342 [this]() { OpenWindow(kObjectSelectorId); });
2343 viewer->SetShowSpritePanelCallback(
2344 [this]() { OpenWindow("dungeon.sprite_editor"); });
2345 viewer->SetShowItemPanelCallback(
2346 [this]() { OpenWindow("dungeon.item_editor"); });
2347 viewer->SetShowRoomListCallback([this]() {
2348 OpenWindow(IsWorkbenchWorkflowEnabled() ? "dungeon.workbench"
2349 : kRoomSelectorId);
2350 });
2351 viewer->SetShowRoomMatrixCallback([this]() { OpenWindow(kRoomMatrixId); });
2352 viewer->SetShowEntranceListCallback(
2353 [this]() { OpenWindow(kEntranceListId); });
2354 viewer->SetShowRoomGraphicsCallback(
2355 [this]() { OpenWindow(kRoomGraphicsId); });
2356 viewer->SetShowDungeonSettingsCallback(
2357 [this]() { OpenWindow("dungeon.settings"); });
2358 viewer->SetEditGraphicsCallback(
2359 [this](int target_room_id, const zelda3::RoomObject& object) {
2360 OpenGraphicsEditorForObject(target_room_id, object);
2361 });
2362 viewer->SetSaveRoomCallback([this](int target_room_id) {
2363 auto status = SaveRoom(target_room_id);
2364 if (!status.ok()) {
2365 LOG_ERROR("DungeonEditorV2", "Save Room failed: %s",
2366 status.message().data());
2367 if (dependencies_.toast_manager) {
2368 dependencies_.toast_manager->Show(
2369 absl::StrFormat("Save Room failed: %s", status.message()),
2370 ToastType::kError);
2371 }
2372 return;
2373 }
2374 if (dependencies_.toast_manager) {
2375 dependencies_.toast_manager->Show("Room saved", ToastType::kSuccess);
2376 }
2377 });
2378
2379 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2380 viewer->SetProject(dependencies_.project);
2381 }
2382
2383 return workbench_viewer_.get();
2384}
2385
2386DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchCompareViewer() {
2387 if (!workbench_compare_viewer_) {
2388 workbench_compare_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2389 auto* viewer = workbench_compare_viewer_.get();
2390 viewer->SetCompactHeaderMode(true);
2391 viewer->SetRoomDetailsExpanded(false);
2392 viewer->SetHeaderHiddenMetadataHudVisible(false);
2393 viewer->SetRooms(&rooms_);
2394 viewer->SetRenderer(renderer_);
2395 viewer->SetCurrentPaletteGroup(current_palette_group_);
2396 viewer->SetCurrentPaletteId(current_palette_id_);
2397 viewer->SetGameData(game_data_);
2398
2399 // Compare viewer is read-only by default: no object selection/mutation, but
2400 // still allows canvas pan/zoom.
2401 viewer->SetObjectInteractionEnabled(false);
2402 viewer->SetHeaderReadOnly(true);
2403 viewer->SetHeaderVisible(false);
2404
2405 if (dungeon_editor_system_) {
2406 // Allows consistent rendering paths that depend on the editor system, but
2407 // interaction is still disabled.
2408 viewer->SetEditorSystem(dungeon_editor_system_.get());
2409 }
2410
2411 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2412 viewer->SetProject(dependencies_.project);
2413 }
2414
2415 return workbench_compare_viewer_.get();
2416}
2417
2418absl::Status DungeonEditorV2::Undo() {
2419 // Finalize any in-progress edit before undoing.
2420 if (pending_undo_.room_id >= 0) {
2421 FinalizeUndoAction(pending_undo_.room_id);
2422 }
2423 if (pending_collision_undo_.room_id >= 0) {
2424 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2425 }
2426 if (pending_water_fill_undo_.room_id >= 0) {
2427 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2428 }
2429 return undo_manager_.Undo();
2430}
2431
2432absl::Status DungeonEditorV2::Redo() {
2433 // Finalize any in-progress edit before redoing.
2434 if (pending_undo_.room_id >= 0) {
2435 FinalizeUndoAction(pending_undo_.room_id);
2436 }
2437 if (pending_collision_undo_.room_id >= 0) {
2438 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2439 }
2440 if (pending_water_fill_undo_.room_id >= 0) {
2441 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2442 }
2443 return undo_manager_.Redo();
2444}
2445
2446absl::Status DungeonEditorV2::Cut() {
2447 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
2448 viewer->object_interaction().HandleCopySelected();
2449 viewer->object_interaction().HandleDeleteSelected();
2450 }
2451 return absl::OkStatus();
2452}
2453
2454absl::Status DungeonEditorV2::Copy() {
2455 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
2456 viewer->object_interaction().HandleCopySelected();
2457 }
2458 return absl::OkStatus();
2459}
2460
2461absl::Status DungeonEditorV2::Paste() {
2462 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
2463 viewer->object_interaction().HandlePasteObjects();
2464 }
2465 return absl::OkStatus();
2466}
2467
2468void DungeonEditorV2::BeginUndoSnapshot(int room_id) {
2469 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2470 return;
2471
2472 // Detect leaked undo snapshots (double-Begin without Finalize).
2473 if (has_pending_undo_) {
2474 LOG_ERROR("DungeonEditor",
2475 "BeginUndoSnapshot called twice without FinalizeUndoAction. "
2476 "Previous snapshot for room %d is being leaked. Finalizing now.",
2477 pending_undo_.room_id);
2478 // Auto-finalize the leaked snapshot to prevent silent state loss.
2479 if (pending_undo_.room_id >= 0) {
2480 FinalizeUndoAction(pending_undo_.room_id);
2481 }
2482 }
2483
2484 pending_undo_.room_id = room_id;
2485 pending_undo_.before_objects = rooms_[room_id].GetTileObjects();
2486 has_pending_undo_ = true;
2487}
2488
2489void DungeonEditorV2::FinalizeUndoAction(int room_id) {
2490 if (pending_undo_.room_id < 0 || pending_undo_.room_id != room_id)
2491 return;
2492 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2493 return;
2494
2495 auto after_objects = rooms_[room_id].GetTileObjects();
2496
2497 auto action = std::make_unique<DungeonObjectsAction>(
2498 room_id, std::move(pending_undo_.before_objects),
2499 std::move(after_objects),
2500 [this](int rid, const std::vector<zelda3::RoomObject>& objects) {
2501 RestoreRoomObjects(rid, objects);
2502 });
2503 undo_manager_.Push(std::move(action));
2504
2505 pending_undo_.room_id = -1;
2506 pending_undo_.before_objects.clear();
2507 has_pending_undo_ = false;
2508}
2509
2510void DungeonEditorV2::SyncPanelsToRoom(int room_id) {
2511 // Update object editor card with current viewer
2512 if (object_selector_panel_) {
2513 object_selector_panel_->SetCanvasViewerProvider([this]() {
2514 return GetViewerForRoom(current_room_id_);
2515 });
2516 object_selector_panel_->SetCurrentRoom(room_id);
2517 object_selector_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2518 }
2519 if (object_editor_content_) {
2520 object_editor_content_->SetCanvasViewerProvider([this]() {
2521 return GetViewerForRoom(current_room_id_);
2522 });
2523 object_editor_content_->SetCurrentRoom(room_id);
2524 object_editor_content_->SetCanvasViewer(GetViewerForRoom(room_id));
2525 }
2526 if (door_editor_panel_) {
2527 door_editor_panel_->SetCanvasViewerProvider([this]() {
2528 return GetViewerForRoom(current_room_id_);
2529 });
2530 door_editor_panel_->SetCurrentRoom(room_id);
2531 door_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2532 }
2533
2534 // Update sprite and item editor panels with current viewer
2535 if (sprite_editor_panel_) {
2536 sprite_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2537 }
2538 if (item_editor_panel_) {
2539 item_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2540 }
2541 if (custom_collision_panel_) {
2542 auto* viewer = GetViewerForRoom(room_id);
2543 custom_collision_panel_->SetCanvasViewer(viewer);
2544 if (viewer) {
2545 custom_collision_panel_->SetInteraction(&viewer->object_interaction());
2546 }
2547 }
2548 if (water_fill_panel_) {
2549 auto* viewer = GetViewerForRoom(room_id);
2550 water_fill_panel_->SetCanvasViewer(viewer);
2551 if (viewer) {
2552 water_fill_panel_->SetInteraction(&viewer->object_interaction());
2553 }
2554 }
2555
2556 if (dungeon_settings_panel_) {
2557 dungeon_settings_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2558 }
2559
2560 if (object_tile_editor_panel_) {
2561 object_tile_editor_panel_->SetCurrentPaletteGroup(current_palette_group_);
2562 }
2563
2564 if (room_tag_editor_panel_) {
2565 room_tag_editor_panel_->SetCurrentRoomId(room_id);
2566 }
2567
2568 if (overlay_manager_panel_) {
2569 auto* viewer = GetViewerForRoom(room_id);
2570 if (viewer) {
2572 overlay_state.show_grid = viewer->mutable_show_grid();
2573 overlay_state.show_object_bounds = viewer->mutable_show_object_bounds();
2574 overlay_state.show_coordinate_overlay =
2575 viewer->mutable_show_coordinate_overlay();
2576 overlay_state.show_room_debug_info =
2577 viewer->mutable_show_room_debug_info();
2578 overlay_state.show_texture_debug = viewer->mutable_show_texture_debug();
2579 overlay_state.show_layer_info = viewer->mutable_show_layer_info();
2580 overlay_state.show_minecart_tracks =
2581 viewer->mutable_show_minecart_tracks();
2582 overlay_state.show_custom_collision =
2583 viewer->mutable_show_custom_collision_overlay();
2584 overlay_state.show_track_collision =
2585 viewer->mutable_show_track_collision_overlay();
2586 overlay_state.show_camera_quadrants =
2587 viewer->mutable_show_camera_quadrant_overlay();
2588 overlay_state.show_minecart_sprites =
2589 viewer->mutable_show_minecart_sprite_overlay();
2590 overlay_state.show_collision_legend =
2591 viewer->mutable_show_track_collision_legend();
2592 overlay_manager_panel_->SetState(overlay_state);
2593 }
2594 }
2595}
2596
2597void DungeonEditorV2::ShowRoomPanel(int room_id) {
2598 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2599 return;
2600 }
2601
2602 bool already_active = false;
2603 for (int i = 0; i < active_rooms_.Size; ++i) {
2604 if (active_rooms_[i] == room_id) {
2605 already_active = true;
2606 break;
2607 }
2608 }
2609 if (!already_active) {
2610 active_rooms_.push_back(room_id);
2611 room_selector_.set_active_rooms(active_rooms_);
2612 }
2613
2614 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2615
2616 if (dependencies_.window_manager) {
2617 if (!dependencies_.window_manager->GetWindowDescriptor(
2618 dependencies_.window_manager->GetActiveSessionId(), card_id)) {
2619 std::string room_name = absl::StrFormat(
2620 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2621 dependencies_.window_manager->RegisterWindow(
2622 {.card_id = card_id,
2623 .display_name = room_name,
2624 .window_title = ICON_MD_GRID_ON " " + room_name,
2625 .icon = ICON_MD_GRID_ON,
2626 .category = "Dungeon",
2627 .shortcut_hint = "",
2628 .visibility_flag = nullptr,
2629 .priority = 200 + room_id});
2630 }
2631 dependencies_.window_manager->OpenWindow(card_id);
2632 }
2633
2634 // Create or update the PanelWindow for this room
2635 if (room_cards_.find(room_id) == room_cards_.end()) {
2636 std::string base_name = absl::StrFormat(
2637 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2638 const int slot_id = GetOrCreateRoomPanelSlotId(room_id);
2639 std::string card_name_str = absl::StrFormat(
2640 "%s###RoomPanelSlot%d", MakePanelTitle(base_name).c_str(), slot_id);
2641
2642 auto card = std::make_shared<gui::PanelWindow>(card_name_str.c_str(),
2644 card->SetDefaultSize(620, 700);
2645 room_cards_[room_id] = card;
2646 }
2647}
2648
2649void DungeonEditorV2::RestoreRoomObjects(
2650 int room_id, const std::vector<zelda3::RoomObject>& objects) {
2651 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2652 return;
2653
2654 auto& room = rooms_[room_id];
2655 room.SetTileObjects(objects);
2656 room.RenderRoomGraphics();
2657}
2658
2659void DungeonEditorV2::BeginCollisionUndoSnapshot(int room_id) {
2660 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2661 return;
2662
2663 if (pending_collision_undo_.room_id >= 0) {
2664 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2665 }
2666
2667 pending_collision_undo_.room_id = room_id;
2668 pending_collision_undo_.before = rooms_[room_id].custom_collision();
2669}
2670
2671void DungeonEditorV2::FinalizeCollisionUndoAction(int room_id) {
2672 if (pending_collision_undo_.room_id < 0 ||
2673 pending_collision_undo_.room_id != room_id) {
2674 return;
2675 }
2676 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2677 return;
2678
2679 auto after = rooms_[room_id].custom_collision();
2680 if (pending_collision_undo_.before.has_data == after.has_data &&
2681 pending_collision_undo_.before.tiles == after.tiles) {
2682 pending_collision_undo_.room_id = -1;
2683 pending_collision_undo_.before = {};
2684 return;
2685 }
2686
2687 auto action = std::make_unique<DungeonCustomCollisionAction>(
2688 room_id, std::move(pending_collision_undo_.before), std::move(after),
2689 [this](int rid, const zelda3::CustomCollisionMap& map) {
2690 RestoreRoomCustomCollision(rid, map);
2691 });
2692 undo_manager_.Push(std::move(action));
2693
2694 pending_collision_undo_.room_id = -1;
2695 pending_collision_undo_.before = {};
2696}
2697
2698void DungeonEditorV2::RestoreRoomCustomCollision(
2699 int room_id, const zelda3::CustomCollisionMap& map) {
2700 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2701 return;
2702
2703 auto& room = rooms_[room_id];
2704 room.custom_collision() = map;
2705 room.MarkCustomCollisionDirty();
2706}
2707
2708namespace {
2709
2711 WaterFillSnapshot snap;
2713
2714 const auto& zone = room.water_fill_zone();
2715 // Preserve deterministic ordering (ascending offsets) for stable diffs.
2716 for (size_t i = 0; i < zone.tiles.size(); ++i) {
2717 if (zone.tiles[i] != 0) {
2718 snap.offsets.push_back(static_cast<uint16_t>(i));
2719 }
2720 }
2721 return snap;
2722}
2723
2724} // namespace
2725
2726void DungeonEditorV2::BeginWaterFillUndoSnapshot(int room_id) {
2727 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2728 return;
2729
2730 if (pending_water_fill_undo_.room_id >= 0) {
2731 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2732 }
2733
2734 pending_water_fill_undo_.room_id = room_id;
2735 pending_water_fill_undo_.before = MakeWaterFillSnapshot(rooms_[room_id]);
2736}
2737
2738void DungeonEditorV2::FinalizeWaterFillUndoAction(int room_id) {
2739 if (pending_water_fill_undo_.room_id < 0 ||
2740 pending_water_fill_undo_.room_id != room_id) {
2741 return;
2742 }
2743 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2744 return;
2745
2746 auto after = MakeWaterFillSnapshot(rooms_[room_id]);
2747 if (pending_water_fill_undo_.before.sram_bit_mask == after.sram_bit_mask &&
2748 pending_water_fill_undo_.before.offsets == after.offsets) {
2749 pending_water_fill_undo_.room_id = -1;
2750 pending_water_fill_undo_.before = {};
2751 return;
2752 }
2753
2754 auto action = std::make_unique<DungeonWaterFillAction>(
2755 room_id, std::move(pending_water_fill_undo_.before), std::move(after),
2756 [this](int rid, const WaterFillSnapshot& snap) {
2757 RestoreRoomWaterFill(rid, snap);
2758 });
2759 undo_manager_.Push(std::move(action));
2760
2761 pending_water_fill_undo_.room_id = -1;
2762 pending_water_fill_undo_.before = {};
2763}
2764
2765void DungeonEditorV2::RestoreRoomWaterFill(int room_id,
2766 const WaterFillSnapshot& snap) {
2767 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2768 return;
2769
2770 auto& room = rooms_[room_id];
2771 room.ClearWaterFillZone();
2772 room.set_water_fill_sram_bit_mask(snap.sram_bit_mask);
2773 for (uint16_t off : snap.offsets) {
2774 const int x = static_cast<int>(off % 64);
2775 const int y = static_cast<int>(off / 64);
2776 room.SetWaterFillTile(x, y, /*filled=*/true);
2777 }
2778 room.MarkWaterFillDirty();
2779}
2780
2781} // 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
const auto & vector() const
Definition rom.h:143
bool is_loaded() const
Definition rom.h:132
static Flags & get()
Definition features.h:118
bool loaded() const
Check if the manifest has been loaded.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
DungeonObjectInteraction & object_interaction()
void SetRooms(DungeonRoomStore *rooms)
void SetPinCallback(std::function< void(bool)> callback)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
absl::Status SaveRoom(int room_id)
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)
class ItemEditorPanel * item_editor_panel_
absl::Status SaveRoomData(int room_id)
std::array< zelda3::RoomEntrance, 0x8C > entrances_
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)
class DungeonSettingsPanel * dungeon_settings_panel_
std::unique_ptr< zelda3::DungeonEditorSystem > dungeon_editor_system_
void OpenWindow(const std::string &window_id)
DoorEditorContent * door_editor_panel_
std::unordered_map< int, int > room_panel_slot_ids_
class DungeonWorkbenchContent * workbench_panel_
void OnRoomSelected(int room_id, bool request_focus=true)
ObjectTileEditorPanel * object_tile_editor_panel_
ObjectSelectorContent * object_selector_panel_
OverlayManagerPanel * overlay_manager_panel_
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
gui::PaletteEditorWidget palette_editor_
std::unique_ptr< ObjectEditorContent > owned_object_editor_content_
std::unique_ptr< DoorEditorContent > owned_door_editor_panel_
static constexpr const char * kObjectEditorId
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
void SetWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
class RoomTagEditorPanel * room_tag_editor_panel_
std::unique_ptr< ObjectSelectorContent > owned_object_selector_panel_
static constexpr const char * kDoorEditorId
DungeonCanvasViewer * GetViewerForRoom(int room_id)
absl::Status Update() override
class WaterFillPanel * water_fill_panel_
DungeonCanvasViewer * GetWorkbenchViewer()
std::unordered_map< int, std::shared_ptr< gui::PanelWindow > > room_cards_
static constexpr const char * kObjectSelectorId
static constexpr const char * kRoomGraphicsId
void QueueWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
ObjectEditorContent * object_editor_content_
static constexpr const char * kRoomMatrixId
static constexpr const char * kRoomSelectorId
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
std::vector< std::pair< uint32_t, uint32_t > > CollectWriteRanges() const
static constexpr const char * kPaletteEditorId
PendingWorkflowMode pending_workflow_mode_
DungeonCanvasViewer * GetWorkbenchCompareViewer()
void SetCustomObjectsFolder(const std::string &folder)
void SetProject(project::YazeProject *project)
void SetTileEditorPanel(ObjectTileEditorPanel *panel)
absl::Status LoadRoomEntrances(std::array< zelda3::RoomEntrance, 0x8C > &entrances)
absl::Status LoadRoom(int room_id, zelda3::Room &room)
void set_entrances(std::array< zelda3::RoomEntrance, 0x8C > *entrances)
void SetRoomSelectedWithIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
void set_rooms(DungeonRoomStore *rooms)
void SetRoomSelectedCallback(std::function< void(int)> callback)
void set_active_rooms(const ImVector< int > &rooms)
zelda3::Room * GetIfMaterialized(int room_id)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
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 NotifyRoomChanged(int previous_room_id)
Called by the editor when the current room changes.
The EditorManager controls the main editor window and manages the various editor classes.
UndoManager undo_manager_
Definition editor.h:317
zelda3::GameData * game_data() const
Definition editor.h:307
EditorDependencies dependencies_
Definition editor.h:316
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetProject(project::YazeProject *project)
void SetRoomNavigationCallback(RoomNavigationCallback callback)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetPlacementError(const std::string &message)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetGameData(zelda3::GameData *game_data)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void OpenForObject(int16_t object_id, int room_id, DungeonRoomStore *rooms)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
Set the current palette group for graphics rendering.
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
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()
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
static Arena & Get()
Definition arena.cc:21
bool HasUnsavedChanges() const
Check if there are ANY unsaved changes.
void Initialize(zelda3::GameData *game_data)
Initialize the palette manager with GameData.
static PaletteManager & Get()
Get the singleton instance.
absl::Status SaveAllToRom()
Save ALL modified palettes to ROM.
void SetDungeonRenderPaletteMode(bool enabled)
void Initialize(zelda3::GameData *game_data)
void SetOnPaletteChanged(std::function< void(int palette_id)> callback)
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)
ValidationResult ValidateRoom(const Room &room)
static ObjectDimensionTable & Get()
void set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:483
bool custom_collision_dirty() const
Definition room.h:432
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:437
int WaterFillTileCount() const
Definition room.h:478
void ClearWaterFillZone()
Definition room.h:470
bool has_water_fill_zone() const
Definition room.h:439
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:1526
void ClearWaterFillDirty()
Definition room.h:491
bool water_fill_dirty() const
Definition room.h:490
uint8_t water_fill_sram_bit_mask() const
Definition room.h:480
void SetWaterFillTile(int x, int y, bool filled)
Definition room.h:447
int id() const
Definition room.h:599
#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_MAP
Definition icons.h:1173
#define ICON_MD_LABEL
Definition icons.h:1053
#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()
WaterFillSnapshot MakeWaterFillSnapshot(const zelda3::Room &room)
absl::Status SaveWaterFillZones(Rom *rom, DungeonRoomStore &rooms)
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)
absl::Status SaveAllChests(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2700
constexpr int kWaterFillTableEnd
constexpr int kCustomCollisionDataSoftEnd
std::string GetRoomLabel(int id)
Convenience function to get a room label.
constexpr int kWaterFillTableStart
absl::Status NormalizeWaterFillZoneMasks(std::vector< WaterFillZoneEntry > *zones)
absl::Status SaveAllPotItems(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2767
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadLegacyWaterGateZones(Rom *rom, const std::string &symbol_path)
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2390
absl::Status SaveAllPits(Rom *rom)
Definition room.cc:2401
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:2432
constexpr int kCustomCollisionDataPosition
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:2568
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)
constexpr int kRoomObjectPointer
absl::Status WriteWaterFillTable(Rom *rom, const std::vector< WaterFillZoneEntry > &zones)
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
project::YazeProject * project
Definition editor.h:168
GlobalEditorContext * global_context
Definition editor.h:170
gfx::IRenderer * renderer
Definition editor.h:186
WorkspaceWindowManager * window_manager
Definition editor.h:176
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
RomWritePolicy write_policy
Definition project.h:110
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:189
std::string custom_objects_folder
Definition project.h:184
core::HackManifest hack_manifest
Definition project.h:204
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1319
std::string symbols_filename
Definition project.h:182
gfx::PaletteGroupMap palette_groups
Definition game_data.h:91
std::vector< uint16_t > fill_offsets
std::array< uint8_t, 64 *64 > tiles
Definition room.h:189