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
54#include "app/gui/core/icons.h"
55#include "core/features.h"
56#include "core/project.h"
57#include "rom/snes.h"
58#include "util/log.h"
59#include "util/macro.h"
65#include "zelda3/dungeon/room.h"
68
69namespace yaze::editor {
70
71namespace {
72
73absl::Status SaveWaterFillZones(Rom* rom,
74 std::array<zelda3::Room, 0x128>& rooms) {
75 if (!rom || !rom->is_loaded()) {
76 return absl::FailedPreconditionError("ROM not loaded");
77 }
78
79 bool any_dirty = false;
80 for (const auto& room : rooms) {
81 if (room.water_fill_dirty()) {
82 any_dirty = true;
83 break;
84 }
85 }
86 if (!any_dirty) {
87 return absl::OkStatus();
88 }
89
90 std::vector<zelda3::WaterFillZoneEntry> zones;
91 zones.reserve(8);
92 for (int room_id = 0; room_id < static_cast<int>(rooms.size()); ++room_id) {
93 auto& room = rooms[room_id];
94 if (!room.has_water_fill_zone()) {
95 continue;
96 }
97
98 const int tile_count = room.WaterFillTileCount();
99 if (tile_count <= 0) {
100 continue;
101 }
102 if (tile_count > 255) {
103 return absl::InvalidArgumentError(absl::StrFormat(
104 "Water fill zone in room 0x%02X has %d tiles (max 255)", room_id,
105 tile_count));
106 }
107
109 z.room_id = room_id;
110 z.sram_bit_mask = room.water_fill_sram_bit_mask();
111 z.fill_offsets.reserve(static_cast<size_t>(tile_count));
112
113 const auto& map = room.water_fill_zone().tiles;
114 for (size_t i = 0; i < map.size(); ++i) {
115 if (map[i] != 0) {
116 z.fill_offsets.push_back(static_cast<uint16_t>(i));
117 }
118 }
119 zones.push_back(std::move(z));
120 }
121
122 if (zones.size() > 8) {
123 return absl::InvalidArgumentError(absl::StrFormat(
124 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
125 zones.size()));
126 }
127
128 // Canonicalize SRAM mask assignment using the shared normalizer so editor
129 // save behavior matches JSON import/export workflows.
131 for (const auto& z : zones) {
132 if (z.room_id >= 0 && z.room_id < static_cast<int>(rooms.size())) {
133 rooms[z.room_id].set_water_fill_sram_bit_mask(z.sram_bit_mask);
134 }
135 }
136
138 for (auto& room : rooms) {
139 room.ClearWaterFillDirty();
140 }
141
142 return absl::OkStatus();
143}
144
145} // namespace
146
148 // Clear viewer references in panels BEFORE room_viewers_ is destroyed.
149 // Panels are owned by PanelManager and outlive this editor, so they need
150 // to have their viewer pointers cleared to prevent dangling pointer access.
153 }
156 }
157 if (item_editor_panel_) {
159 }
163 }
164 if (water_fill_panel_) {
167 }
170 }
171}
172
176 }
177 const bool rom_changed = dependencies_.rom && dependencies_.rom != rom_;
178 if (rom_changed) {
180 // The system owns ROM-backed views; ensure it matches the current ROM.
182 }
185 if (game_data_) {
187 }
188 }
189
190 // Setup docking class for room windows
191 room_window_class_.DockingAllowUnclassed = true;
192 room_window_class_.DockingAlwaysTabBar = true;
193
195 return;
196 auto* panel_manager = dependencies_.panel_manager;
197
198 // Legacy panel IDs persisted in older layouts/settings.
199 panel_manager->RegisterPanelAlias("dungeon.object_tools",
200 "dungeon.object_editor");
201 panel_manager->RegisterPanelAlias("dungeon.entrances",
202 "dungeon.entrance_properties");
203
204 // Register panels with PanelManager (no boolean flags - visibility is
205 // managed entirely by PanelManager::ShowPanel/HidePanel/IsPanelVisible)
206 panel_manager->RegisterPanel(
207 {.card_id = "dungeon.workbench",
208 .display_name = "Dungeon Workbench",
209 .window_title = " Dungeon Workbench",
210 .icon = ICON_MD_WORKSPACES,
211 .category = "Dungeon",
212 .shortcut_hint = "",
213 .visibility_flag = nullptr,
214 .priority = 5,
215 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
216 .disabled_tooltip = "Load a ROM to edit dungeon rooms"});
217
218 panel_manager->RegisterPanel(
219 {.card_id = kRoomSelectorId,
220 .display_name = "Room List",
221 .window_title = " Room List",
222 .icon = ICON_MD_LIST,
223 .category = "Dungeon",
224 .shortcut_hint = "Ctrl+Shift+R",
225 .visibility_flag = nullptr,
226 .priority = 20,
227 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
228 .disabled_tooltip = "Load a ROM to browse dungeon rooms"});
229
230 panel_manager->RegisterPanel(
231 {.card_id = kEntranceListId,
232 .display_name = "Entrance List",
233 .window_title = " Entrance List",
234 .icon = ICON_MD_DOOR_FRONT,
235 .category = "Dungeon",
236 .shortcut_hint = "Ctrl+Shift+E",
237 .visibility_flag = nullptr,
238 .priority = 25,
239 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
240 .disabled_tooltip = "Load a ROM to browse dungeon entrances"});
241
242 panel_manager->RegisterPanel(
243 {.card_id = "dungeon.entrance_properties",
244 .display_name = "Entrance Properties",
245 .window_title = " Entrance Properties",
246 .icon = ICON_MD_TUNE,
247 .category = "Dungeon",
248 .shortcut_hint = "",
249 .visibility_flag = nullptr,
250 .priority = 26,
251 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
252 .disabled_tooltip = "Load a ROM to edit entrance properties"});
253
254 panel_manager->RegisterPanel(
255 {.card_id = kRoomMatrixId,
256 .display_name = "Room Matrix",
257 .window_title = " Room Matrix",
258 .icon = ICON_MD_GRID_VIEW,
259 .category = "Dungeon",
260 .shortcut_hint = "Ctrl+Shift+M",
261 .visibility_flag = nullptr,
262 .priority = 30,
263 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
264 .disabled_tooltip = "Load a ROM to view the room matrix"});
265
266 panel_manager->RegisterPanel(
267 {.card_id = kRoomGraphicsId,
268 .display_name = "Room Graphics",
269 .window_title = " Room Graphics",
270 .icon = ICON_MD_IMAGE,
271 .category = "Dungeon",
272 .shortcut_hint = "Ctrl+Shift+G",
273 .visibility_flag = nullptr,
274 .priority = 50,
275 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
276 .disabled_tooltip = "Load a ROM to view room graphics"});
277
278 panel_manager->RegisterPanel(
279 {.card_id = kPaletteEditorId,
280 .display_name = "Palette Editor",
281 .window_title = " Palette Editor",
282 .icon = ICON_MD_PALETTE,
283 .category = "Dungeon",
284 // Avoid conflicting with the global Command Palette (Ctrl/Cmd+Shift+P).
285 .shortcut_hint = "Ctrl+Shift+Alt+P",
286 .visibility_flag = nullptr,
287 .priority = 70,
288 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
289 .disabled_tooltip = "Load a ROM to edit dungeon palettes"});
290
291 panel_manager->RegisterPanel(
292 {.card_id = "dungeon.room_tags",
293 .display_name = "Room Tags",
294 .window_title = " Room Tags",
295 .icon = ICON_MD_LABEL,
296 .category = "Dungeon",
297 .shortcut_hint = "",
298 .visibility_flag = nullptr,
299 .priority = 45,
300 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
301 .disabled_tooltip = "Load a ROM to view room tags"});
302
303 panel_manager->RegisterPanel(
304 {.card_id = "dungeon.dungeon_map",
305 .display_name = "Dungeon Map",
306 .window_title = " Dungeon Map",
307 .icon = ICON_MD_MAP,
308 .category = "Dungeon",
309 .shortcut_hint = "Ctrl+Shift+D",
310 .visibility_flag = nullptr,
311 .priority = 32,
312 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
313 .disabled_tooltip = "Load a ROM to view the dungeon map"});
314
315 // Show default panels on startup.
316 // Workbench mode intentionally suppresses parallel standalone defaults to
317 // avoid two competing workflows being open at once.
319 /*show_toast=*/false);
320
321 // Wire intent-aware callback for double-click / context menu
323 [this](int room_id, RoomSelectionIntent intent) {
324 OnRoomSelected(room_id, intent);
325 });
326
327 // Register EditorPanel instances
328 panel_manager->RegisterEditorPanel(std::make_unique<DungeonRoomSelectorPanel>(
329 &room_selector_, [this](int room_id) { OnRoomSelected(room_id); }));
330
331 panel_manager->RegisterEditorPanel(std::make_unique<DungeonEntranceListPanel>(
333 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
334
335 {
336 auto matrix_panel = std::make_unique<DungeonRoomMatrixPanel>(
338 [this](int room_id) { OnRoomSelected(room_id); },
339 [this](int old_room, int new_room) {
340 SwapRoomInPanel(old_room, new_room);
341 },
342 &rooms_);
343 matrix_panel->SetRoomIntentCallback(
344 [this](int room_id, RoomSelectionIntent intent) {
345 OnRoomSelected(room_id, intent);
346 });
347 panel_manager->RegisterEditorPanel(std::move(matrix_panel));
348 }
349
350 {
351 auto dungeon_map = std::make_unique<DungeonMapPanel>(
353 [this](int room_id) { OnRoomSelected(room_id); }, &rooms_);
354 dungeon_map->SetRoomIntentCallback(
355 [this](int room_id, RoomSelectionIntent intent) {
356 OnRoomSelected(room_id, intent);
357 });
359 dungeon_map->SetHackManifest(&dependencies_.project->hack_manifest);
360 }
361 panel_manager->RegisterEditorPanel(std::move(dungeon_map));
362 }
363
364 {
365 auto workbench = std::make_unique<DungeonWorkbenchPanel>(
367 [this](int room_id) { OnRoomSelected(room_id); },
368 [this](int room_id, RoomSelectionIntent intent) {
369 OnRoomSelected(room_id, intent);
370 },
371 [this](int room_id) {
372 auto status = SaveRoom(room_id);
373 if (!status.ok()) {
374 LOG_ERROR("DungeonEditorV2", "Save Room failed: %s",
375 status.message().data());
378 absl::StrFormat("Save Room failed: %s", status.message()),
380 }
381 return;
382 }
384 dependencies_.toast_manager->Show("Room saved",
386 }
387 },
388 [this]() { return GetWorkbenchViewer(); },
389 [this]() { return GetWorkbenchCompareViewer(); },
390 [this]() -> const std::deque<int>& { return recent_rooms_; },
391 [this](int room_id) {
392 recent_rooms_.erase(
393 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
394 recent_rooms_.end());
395 },
396 [this](const std::string& id) { ShowPanel(id); },
397 [this](bool enabled) { QueueWorkbenchWorkflowMode(enabled); }, rom_);
398 workbench_panel_ = workbench.get();
400 [this]() { return undo_manager_.CanUndo(); },
401 [this]() { return undo_manager_.CanRedo(); },
402 [this]() { Undo().IgnoreError(); }, [this]() { Redo().IgnoreError(); },
403 [this]() { return undo_manager_.GetUndoDescription(); },
404 [this]() { return undo_manager_.GetRedoDescription(); },
405 [this]() { return static_cast<int>(undo_manager_.UndoStackSize()); });
406 panel_manager->RegisterEditorPanel(std::move(workbench));
407 }
408
409 panel_manager->RegisterEditorPanel(std::make_unique<DungeonEntrancesPanel>(
411 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
412
413 // Note: DungeonRoomGraphicsPanel and DungeonPaletteEditorPanel are registered
414 // in Load() after their dependencies (renderer_, palette_editor_) are initialized
415}
416
417absl::Status DungeonEditorV2::Load() {
418 if (!rom_ || !rom_->is_loaded()) {
419 return absl::FailedPreconditionError("ROM not loaded");
420 }
421
422 // Initialize ObjectDimensionTable so DimensionService's fallback path works.
423 // DimensionService::Get() queries this table internally but has no Load API.
424 auto& dim_table = zelda3::ObjectDimensionTable::Get();
425 if (!dim_table.IsLoaded()) {
426 RETURN_IF_ERROR(dim_table.LoadFromRom(rom_));
427 }
428
430
431 if (!game_data()) {
432 return absl::FailedPreconditionError("GameData not available");
433 }
434 auto dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
435 current_palette_ = dungeon_main_pal_group[current_palette_group_id_];
438
443 [this](int room_id) { OnRoomSelected(room_id); });
445 [this](int room_id, RoomSelectionIntent intent) {
446 OnRoomSelected(room_id, intent);
447 });
448
449 // Canvas viewers are lazily created in GetViewerForRoom
450
451 if (!render_service_) {
453 std::make_unique<emu::render::EmulatorRenderService>(rom_);
454 auto status = render_service_->Initialize();
455 if (!status.ok()) {
456 LOG_ERROR("DungeonEditorV2", "Failed to initialize render service: %s",
457 status.message().data());
458 }
459 }
460
461 if (game_data()) {
463 } else {
465 }
466
468
469 // Register panels that depend on initialized state (renderer, palette_editor_)
471 auto graphics_panel = std::make_unique<DungeonRoomGraphicsPanel>(
473 room_graphics_panel_ = graphics_panel.get();
474 dependencies_.panel_manager->RegisterEditorPanel(std::move(graphics_panel));
476 std::make_unique<DungeonPaletteEditorPanel>(&palette_editor_));
477 }
478
479 dungeon_editor_system_ = std::make_unique<zelda3::DungeonEditorSystem>(rom_);
480 (void)dungeon_editor_system_->Initialize();
482
483 // Initialize unified object editor panel
484 // Note: Initially passing nullptr for viewer, will be set on selection
485 auto object_editor = std::make_unique<ObjectEditorPanel>(
486 renderer_, rom_, nullptr, dungeon_editor_system_->GetObjectEditor());
487
488 // Wire up object change callback to trigger room re-rendering
489 dungeon_editor_system_->GetObjectEditor()->SetObjectChangedCallback(
490 [this](size_t /*object_index*/, const zelda3::RoomObject& /*object*/) {
491 if (current_room_id_ >= 0 && current_room_id_ < (int)rooms_.size()) {
492 rooms_[current_room_id_].RenderRoomGraphics();
493 }
494 });
495
496 // Set rooms and initial palette group for correct preview rendering
497 object_editor->SetRooms(&rooms_);
498 object_editor->SetCurrentPaletteGroup(current_palette_group_);
499
500 // Keep raw pointer for later access
501 object_editor_panel_ = object_editor.get();
502
503 // Propagate game_data to the object editor panel if available
504 if (game_data()) {
506 }
511 }
512
513 // Wire tile editor callback before transferring ownership
514 object_editor->set_tile_editor_callback([this](int16_t object_id) {
517 &rooms_);
518 ShowPanel("dungeon.object_tile_editor");
519 }
520 });
521
522 // Register the ObjectEditorPanel directly (it inherits from EditorPanel)
523 // Panel manager takes ownership
525 dependencies_.panel_manager->RegisterEditorPanel(std::move(object_editor));
526
527 // Register sprite and item editor panels with canvas viewer = nullptr
528 // They will get the viewer reference in OnRoomSelected when a room is selected
529 auto sprite_panel = std::make_unique<SpriteEditorPanel>(&current_room_id_,
530 &rooms_, nullptr);
531 sprite_editor_panel_ = sprite_panel.get();
532 dependencies_.panel_manager->RegisterEditorPanel(std::move(sprite_panel));
533
534 auto item_panel =
535 std::make_unique<ItemEditorPanel>(&current_room_id_, &rooms_, nullptr);
536 item_editor_panel_ = item_panel.get();
537 dependencies_.panel_manager->RegisterEditorPanel(std::move(item_panel));
538
539 auto collision_panel = std::make_unique<CustomCollisionPanel>(
540 nullptr, nullptr); // Placeholder, will be set in OnRoomSelected
541 custom_collision_panel_ = collision_panel.get();
543 std::move(collision_panel));
544
545 auto water_fill_panel =
546 std::make_unique<WaterFillPanel>(nullptr, nullptr); // Placeholder
547 water_fill_panel_ = water_fill_panel.get();
549 std::move(water_fill_panel));
550
551 // Object Tile Editor Panel
552 {
553 auto tile_editor_panel =
554 std::make_unique<ObjectTileEditorPanel>(renderer_, rom_);
555 tile_editor_panel->SetCurrentPaletteGroup(current_palette_group_);
556
557 // Wire creation callback: when a new custom object is saved,
558 // register it with the manager, persist to project, and refresh UI.
559 tile_editor_panel->SetObjectCreatedCallback(
560 [this](int object_id, const std::string& filename) {
562 filename);
564 dependencies_.project->custom_object_files[object_id].push_back(
565 filename);
566 (void)dependencies_.project->Save();
567 }
570 }
571 });
572
573 object_tile_editor_panel_ = tile_editor_panel.get();
575 std::move(tile_editor_panel));
576 }
577
578 // Wire tile editor panel and project references to the object selector
585 }
586 }
587
588 auto settings_panel = std::make_unique<DungeonSettingsPanel>(nullptr);
589 settings_panel->SetSaveRoomCallback([this](int id) { SaveRoom(id); });
590 settings_panel->SetSaveAllRoomsCallback([this]() { SaveAllRooms(); });
591 settings_panel->SetCurrentRoomId(&current_room_id_);
592 dungeon_settings_panel_ = settings_panel.get();
593 dependencies_.panel_manager->RegisterEditorPanel(std::move(settings_panel));
594
595 // Room Tag Editor Panel
596 {
597 auto room_tag_panel = std::make_unique<RoomTagEditorPanel>();
598 room_tag_panel->SetProject(dependencies_.project);
599 room_tag_panel->SetRooms(&rooms_);
600 room_tag_panel->SetCurrentRoomId(current_room_id_);
601 room_tag_editor_panel_ = room_tag_panel.get();
603 std::move(room_tag_panel));
604 }
605
606 // Overlay Manager Panel
607 {
608 auto overlay_panel = std::make_unique<OverlayManagerPanel>();
609 overlay_manager_panel_ = overlay_panel.get();
611 std::move(overlay_panel));
612 }
613
614 // Feature Flag: Custom Objects / Minecart Tracks
615 if (core::FeatureFlags::get().kEnableCustomObjects) {
617 auto minecart_panel = std::make_unique<MinecartTrackEditorPanel>();
618 minecart_track_editor_panel_ = minecart_panel.get();
620 std::move(minecart_panel));
621 }
622
624 // Update project root for track editor
632 [this](int room_id) { OnRoomSelected(room_id); });
633 }
634
635 // Initialize custom object manager with project-configured path
640 }
641 }
642 }
643 } else {
644 owned_object_editor_panel_ = std::move(object_editor);
645 }
646
647 palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) {
648 for (int i = 0; i < active_rooms_.Size; i++) {
649 int room_id = active_rooms_[i];
650 if (room_id >= 0 && room_id < (int)rooms_.size()) {
651 rooms_[room_id].RenderRoomGraphics();
652 }
653 }
654 });
655
656 // Oracle of Secrets: load editor-authored water fill zones (best-effort).
657 {
658 for (auto& room : rooms_) {
659 room.ClearWaterFillZone();
660 room.ClearWaterFillDirty();
661 }
662
663 bool legacy_imported = false;
664 std::vector<zelda3::WaterFillZoneEntry> zones;
665
666 auto zones_or = zelda3::LoadWaterFillTable(rom_);
667 if (zones_or.ok()) {
668 zones = std::move(zones_or.value());
669 } else {
670 LOG_WARN("DungeonEditorV2", "WaterFillTable parse failed: %s",
671 zones_or.status().message().data());
674 absl::StrFormat("WaterFill table parse failed: %s",
675 zones_or.status().message()),
677 }
678 }
679
680 if (zones.empty()) {
681 std::string sym_path;
686 }
687 auto legacy_or = zelda3::LoadLegacyWaterGateZones(rom_, sym_path);
688 if (legacy_or.ok()) {
689 zones = std::move(legacy_or.value());
690 legacy_imported = !zones.empty();
691 } else {
692 LOG_WARN("DungeonEditorV2", "Legacy water gate import failed: %s",
693 legacy_or.status().message().data());
694 }
695 }
696
697 for (const auto& z : zones) {
698 if (z.room_id < 0 || z.room_id >= static_cast<int>(rooms_.size())) {
699 continue;
700 }
701 auto& room = rooms_[z.room_id];
702 room.set_water_fill_sram_bit_mask(z.sram_bit_mask);
703 for (uint16_t off : z.fill_offsets) {
704 const int x = static_cast<int>(off % 64);
705 const int y = static_cast<int>(off / 64);
706 room.SetWaterFillTile(x, y, true);
707 }
708
709 if (!legacy_imported) {
710 room.ClearWaterFillDirty();
711 }
712 }
713
714 if (legacy_imported && dependencies_.toast_manager) {
716 "Imported legacy water gate zones (save to write new table)",
718 }
719 }
720
721 is_loaded_ = true;
722 return absl::OkStatus();
723}
724
727
728 const auto& theme = AgentUI::GetTheme();
729 if (room_window_class_.ClassId == 0) {
730 room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
731 }
732
733 if (!is_loaded_) {
734 gui::PanelWindow loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);
735 loading_card.SetDefaultSize(400, 200);
736 if (loading_card.Begin()) {
737 ImGui::TextColored(theme.text_secondary_gray, "Loading dungeon data...");
738 ImGui::TextWrapped(
739 "Independent editor cards will appear once ROM data is loaded.");
740 }
741 loading_card.End();
742 return absl::OkStatus();
743 }
744
745 if (!IsWorkbenchWorkflowEnabled() || active_rooms_.Size > 0) {
747 }
748
749 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
750 // Delegate delete to current room viewer
751 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
752 viewer->DeleteSelectedObjects();
753 }
754 }
755
756 // Keyboard Shortcuts (only if not typing in a text field)
757 if (!ImGui::GetIO().WantTextInput) {
758 // Room Cycling (Ctrl+Tab)
759 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && ImGui::GetIO().KeyCtrl) {
761 if (recent_rooms_.size() > 1) {
762 int current_idx = -1;
763 for (int i = 0; i < static_cast<int>(recent_rooms_.size()); ++i) {
765 current_idx = i;
766 break;
767 }
768 }
769 if (current_idx != -1) {
770 int next_idx;
771 if (ImGui::GetIO().KeyShift) {
772 next_idx =
773 (current_idx + 1) % static_cast<int>(recent_rooms_.size());
774 } else {
775 next_idx =
776 (current_idx - 1 + static_cast<int>(recent_rooms_.size())) %
777 static_cast<int>(recent_rooms_.size());
778 }
779 OnRoomSelected(recent_rooms_[next_idx]);
780 }
781 }
782 } else if (active_rooms_.size() > 1) {
783 int current_idx = -1;
784 for (int i = 0; i < active_rooms_.size(); ++i) {
786 current_idx = i;
787 break;
788 }
789 }
790
791 if (current_idx != -1) {
792 int next_idx;
793 if (ImGui::GetIO().KeyShift) {
794 next_idx =
795 (current_idx - 1 + active_rooms_.size()) % active_rooms_.size();
796 } else {
797 next_idx = (current_idx + 1) % active_rooms_.size();
798 }
799 OnRoomSelected(active_rooms_[next_idx]);
800 }
801 }
802 }
803
804 // Adjacent Room Navigation (Ctrl+Arrows)
805 if (ImGui::GetIO().KeyCtrl) {
806 int next_room = -1;
807 const int kCols = 16;
808
809 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
810 if (current_room_id_ >= kCols)
811 next_room = current_room_id_ - kCols;
812 } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
813 if (IsValidRoomId(current_room_id_ + kCols))
814 next_room = current_room_id_ + kCols;
815 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
816 if (current_room_id_ % kCols > 0)
817 next_room = current_room_id_ - 1;
818 } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
819 if (current_room_id_ % kCols < kCols - 1 &&
821 next_room = current_room_id_ + 1;
822 }
823
824 if (next_room != -1) {
826 OnRoomSelected(next_room, /*request_focus=*/false);
827 } else {
829 }
830 }
831 }
832 }
833
834 // Process any pending room swaps after all drawing is complete
835 // This prevents ImGui state corruption from modifying collections mid-frame
837
838 return absl::OkStatus();
839}
840
841absl::Status DungeonEditorV2::Save() {
842 if (!rom_ || !rom_->is_loaded()) {
843 return absl::FailedPreconditionError("ROM not loaded");
844 }
845
846 const auto& flags = core::FeatureFlags::get().dungeon;
847
848 if (flags.kSavePalettes && gfx::PaletteManager::Get().HasUnsavedChanges()) {
849 auto status = gfx::PaletteManager::Get().SaveAllToRom();
850 if (!status.ok()) {
851 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
852 status.message().data());
853 return status;
854 }
855 LOG_INFO("DungeonEditorV2", "Saved %zu modified colors to ROM",
856 gfx::PaletteManager::Get().GetModifiedColorCount());
857 }
858
859 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
860 for (int room_id = 0; room_id < static_cast<int>(rooms_.size());
861 ++room_id) {
862 auto status = SaveRoomData(room_id);
863 if (!status.ok()) {
864 return status;
865 }
866 }
867 }
868
869 if (flags.kSaveTorches) {
870 auto status = zelda3::SaveAllTorches(rom_, rooms_);
871 if (!status.ok()) {
872 LOG_ERROR("DungeonEditorV2", "Failed to save torches: %s",
873 status.message().data());
874 return status;
875 }
876 }
877
878 if (flags.kSavePits) {
879 auto status = zelda3::SaveAllPits(rom_);
880 if (!status.ok()) {
881 LOG_ERROR("DungeonEditorV2", "Failed to save pits: %s",
882 status.message().data());
883 return status;
884 }
885 }
886
887 if (flags.kSaveBlocks) {
888 auto status = zelda3::SaveAllBlocks(rom_);
889 if (!status.ok()) {
890 LOG_ERROR("DungeonEditorV2", "Failed to save blocks: %s",
891 status.message().data());
892 return status;
893 }
894 }
895
896 if (flags.kSaveCollision) {
897 auto status = zelda3::SaveAllCollision(rom_, absl::MakeSpan(rooms_));
898 if (!status.ok()) {
899 LOG_ERROR("DungeonEditorV2", "Failed to save collision: %s",
900 status.message().data());
901 return status;
902 }
903 }
904
905 if (flags.kSaveWaterFillZones) {
906 auto status = SaveWaterFillZones(rom_, rooms_);
907 if (!status.ok()) {
908 LOG_ERROR("DungeonEditorV2", "Failed to save water fill zones: %s",
909 status.message().data());
910 return status;
911 }
912 }
913
914 if (flags.kSaveChests) {
915 auto status = zelda3::SaveAllChests(rom_, rooms_);
916 if (!status.ok()) {
917 LOG_ERROR("DungeonEditorV2", "Failed to save chests: %s",
918 status.message().data());
919 return status;
920 }
921 }
922
923 if (flags.kSavePotItems) {
924 auto status = zelda3::SaveAllPotItems(rom_, rooms_);
925 if (!status.ok()) {
926 LOG_ERROR("DungeonEditorV2", "Failed to save pot items: %s",
927 status.message().data());
928 return status;
929 }
930 }
931
932 return absl::OkStatus();
933}
934
935std::vector<std::pair<uint32_t, uint32_t>> DungeonEditorV2::CollectWriteRanges()
936 const {
937 std::vector<std::pair<uint32_t, uint32_t>> ranges;
938
939 if (!rom_ || !rom_->is_loaded()) {
940 return ranges;
941 }
942
943 const auto& flags = core::FeatureFlags::get().dungeon;
944 const auto& rom_data = rom_->vector();
945
946 // Oracle of Secrets: the water fill table lives in a reserved tail region.
947 // Include it in write-range reporting whenever we have dirty water fill data,
948 // even if no rooms are currently loaded (SaveWaterFillZones() is room-indexed
949 // and independent of room loading state).
950 if (flags.kSaveWaterFillZones &&
951 zelda3::kWaterFillTableEnd <= static_cast<int>(rom_data.size())) {
952 for (const auto& room : rooms_) {
953 if (room.water_fill_dirty()) {
954 ranges.emplace_back(zelda3::kWaterFillTableStart,
956 break;
957 }
958 }
959 }
960
961 // Custom collision writes update the pointer table and append blobs into the
962 // expanded collision region. SaveAllCollision() is room-indexed, so include
963 // these ranges whenever any room has dirty custom collision data.
964 if (flags.kSaveCollision) {
965 const int ptrs_size = zelda3::kNumberOfRooms * 3;
966 const bool has_ptr_table =
968 static_cast<int>(rom_data.size()));
969 const bool has_data_region = (zelda3::kCustomCollisionDataSoftEnd <=
970 static_cast<int>(rom_data.size()));
971 if (has_ptr_table && has_data_region) {
972 for (const auto& room : rooms_) {
973 if (room.custom_collision_dirty()) {
974 ranges.emplace_back(zelda3::kCustomCollisionRoomPointers,
976 ranges.emplace_back(zelda3::kCustomCollisionDataPosition,
978 break;
979 }
980 }
981 }
982 }
983
984 for (const auto& room : rooms_) {
985 if (!room.IsLoaded()) {
986 continue;
987 }
988 int room_id = room.id();
989
990 // Header range
991 if (flags.kSaveRoomHeaders) {
992 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
993 int header_ptr_table =
994 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
995 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
997 header_ptr_table = yaze::SnesToPc(header_ptr_table);
998 int table_offset = header_ptr_table + (room_id * 2);
999
1000 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1001 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1002 (rom_data[table_offset + 1] << 8) |
1003 rom_data[table_offset];
1004 int header_location = yaze::SnesToPc(address);
1005 ranges.emplace_back(header_location, header_location + 14);
1006 }
1007 }
1008 }
1009
1010 // Object range
1011 if (flags.kSaveObjects) {
1012 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1013 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
1014 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
1016 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
1017 int entry_offset = obj_ptr_table + (room_id * 3);
1018
1019 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
1020 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1021 (rom_data[entry_offset + 1] << 8) |
1022 rom_data[entry_offset];
1023 int objects_location = yaze::SnesToPc(tile_addr);
1024
1025 auto encoded = room.EncodeObjects();
1026 ranges.emplace_back(objects_location,
1027 objects_location + encoded.size() + 2);
1028 }
1029 }
1030 }
1031 }
1032
1033 return ranges;
1034}
1035
1036absl::Status DungeonEditorV2::SaveRoom(int room_id) {
1037 if (!rom_ || !rom_->is_loaded()) {
1038 return absl::FailedPreconditionError("ROM not loaded");
1039 }
1040 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1041 return absl::InvalidArgumentError("Invalid room ID");
1042 }
1043
1044 const auto& flags = core::FeatureFlags::get().dungeon;
1045 if (flags.kSavePalettes && gfx::PaletteManager::Get().HasUnsavedChanges()) {
1046 auto status = gfx::PaletteManager::Get().SaveAllToRom();
1047 if (!status.ok()) {
1048 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
1049 status.message().data());
1050 return status;
1051 }
1052 }
1053 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1054 RETURN_IF_ERROR(SaveRoomData(room_id));
1055 }
1056
1057 if (flags.kSaveTorches) {
1059 }
1060 if (flags.kSavePits) {
1062 }
1063 if (flags.kSaveBlocks) {
1065 }
1066 if (flags.kSaveCollision) {
1068 }
1069 if (flags.kSaveWaterFillZones) {
1070 RETURN_IF_ERROR(SaveWaterFillZones(rom_, rooms_));
1071 }
1072 if (flags.kSaveChests) {
1074 }
1075 if (flags.kSavePotItems) {
1077 }
1078
1079 return absl::OkStatus();
1080}
1081
1083 int count = 0;
1084 for (const auto& room : rooms_) {
1085 if (room.IsLoaded()) {
1086 ++count;
1087 }
1088 }
1089 return count;
1090}
1091
1092absl::Status DungeonEditorV2::SaveRoomData(int room_id) {
1093 if (!rom_ || !rom_->is_loaded()) {
1094 return absl::FailedPreconditionError("ROM not loaded");
1095 }
1096 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1097 return absl::InvalidArgumentError("Invalid room ID");
1098 }
1099
1100 auto& room = rooms_[room_id];
1101 if (!room.IsLoaded()) {
1102 return absl::OkStatus();
1103 }
1104
1105 // ROM safety: validate loaded room content before writing any bytes.
1106 {
1107 zelda3::DungeonValidator validator;
1108 const auto result = validator.ValidateRoom(room);
1109 for (const auto& w : result.warnings) {
1110 LOG_WARN("DungeonEditorV2", "Room 0x%03X validation warning: %s", room_id,
1111 w.c_str());
1112 }
1113 if (!result.is_valid) {
1114 for (const auto& e : result.errors) {
1115 LOG_ERROR("DungeonEditorV2", "Room 0x%03X validation error: %s",
1116 room_id, e.c_str());
1117 }
1120 absl::StrFormat(
1121 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1122 room_id, result.errors.size()),
1124 }
1125 return absl::FailedPreconditionError(
1126 absl::StrFormat("Room 0x%03X failed validation", room_id));
1127 }
1128 }
1129
1130 const auto& flags = core::FeatureFlags::get().dungeon;
1131
1132 // HACK MANIFEST VALIDATION
1134 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1135 const auto& manifest = dependencies_.project->hack_manifest;
1136 const auto& rom_data = rom_->vector();
1137
1138 // 1. Validate Header Range
1139 if (flags.kSaveRoomHeaders) {
1140 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
1141 int header_ptr_table =
1142 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
1143 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
1145 header_ptr_table = yaze::SnesToPc(header_ptr_table);
1146 int table_offset = header_ptr_table + (room_id * 2);
1147
1148 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1149 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1150 (rom_data[table_offset + 1] << 8) |
1151 rom_data[table_offset];
1152 int header_location = yaze::SnesToPc(address);
1153 ranges.emplace_back(header_location, header_location + 14);
1154 }
1155 }
1156 }
1157
1158 // 2. Validate Object Range
1159 if (flags.kSaveObjects) {
1160 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1161 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
1162 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
1164 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
1165 int entry_offset = obj_ptr_table + (room_id * 3);
1166
1167 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
1168 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1169 (rom_data[entry_offset + 1] << 8) |
1170 rom_data[entry_offset];
1171 int objects_location = yaze::SnesToPc(tile_addr);
1172
1173 // Estimate size based on current encoding
1174 // Note: we check the *target* location (where we will write)
1175 // The EncodeObjects() size is what we *will* write.
1176 // We add 2 bytes for the size/header that SaveObjects writes.
1177 auto encoded = room.EncodeObjects();
1178 ranges.emplace_back(objects_location,
1179 objects_location + encoded.size() + 2);
1180 }
1181 }
1182 }
1183
1184 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1185 // address space (LoROM), so convert before analysis.
1186 auto conflicts = manifest.AnalyzePcWriteRanges(ranges);
1187 if (!conflicts.empty()) {
1188 const auto write_policy =
1190 std::string error_msg = absl::StrFormat(
1191 "Hack manifest write conflicts while saving room 0x%03X:\n\n",
1192 room_id);
1193 for (const auto& conflict : conflicts) {
1194 absl::StrAppend(
1195 &error_msg,
1196 absl::StrFormat(
1197 "- Address 0x%06X is %s", conflict.address,
1198 core::AddressOwnershipToString(conflict.ownership)));
1199 if (!conflict.module.empty()) {
1200 absl::StrAppend(&error_msg, " (Module: ", conflict.module, ")");
1201 }
1202 absl::StrAppend(&error_msg, "\n");
1203 }
1204
1205 if (write_policy == project::RomWritePolicy::kAllow) {
1206 LOG_DEBUG("DungeonEditorV2", "%s", error_msg.c_str());
1207 } else {
1208 LOG_WARN("DungeonEditorV2", "%s", error_msg.c_str());
1209 }
1210
1212 write_policy == project::RomWritePolicy::kWarn) {
1214 "Save warning: write conflict with hack manifest (see log)",
1216 }
1217
1218 if (write_policy == project::RomWritePolicy::kBlock) {
1221 "Save blocked: write conflict with hack manifest (see log)",
1223 }
1224 return absl::PermissionDeniedError("Write conflict with Hack Manifest");
1225 }
1226 }
1227 }
1228
1229 if (flags.kSaveObjects) {
1230 auto status = room.SaveObjects();
1231 if (!status.ok()) {
1232 LOG_ERROR("DungeonEditorV2", "Failed to save room objects: %s",
1233 status.message().data());
1234 return status;
1235 }
1236 }
1237
1238 if (flags.kSaveSprites) {
1239 auto status = room.SaveSprites();
1240 if (!status.ok()) {
1241 LOG_ERROR("DungeonEditorV2", "Failed to save room sprites: %s",
1242 status.message().data());
1243 return status;
1244 }
1245 }
1246
1247 if (flags.kSaveRoomHeaders) {
1248 auto status = room.SaveRoomHeader();
1249 if (!status.ok()) {
1250 LOG_ERROR("DungeonEditorV2", "Failed to save room header: %s",
1251 status.message().data());
1252 return status;
1253 }
1254 }
1255
1256 if (flags.kSaveObjects && dungeon_editor_system_) {
1257 auto sys_status = dungeon_editor_system_->SaveRoom(room.id());
1258 if (!sys_status.ok()) {
1259 LOG_ERROR("DungeonEditorV2", "Failed to save room system data: %s",
1260 sys_status.message().data());
1261 }
1262 }
1263
1264 return absl::OkStatus();
1265}
1266
1268 if (!core::FeatureFlags::get().dungeon.kUseWorkbench) {
1269 return false;
1270 }
1272 return true;
1273 }
1274 return dependencies_.panel_manager->IsPanelVisible("dungeon.workbench");
1275}
1276
1277void DungeonEditorV2::SetWorkbenchWorkflowMode(bool enabled, bool show_toast) {
1278 auto* panel_manager = dependencies_.panel_manager;
1279 if (!panel_manager) {
1280 return;
1281 }
1282
1283 const size_t session_id = panel_manager->GetActiveSessionId();
1284 const bool was_enabled = IsWorkbenchWorkflowEnabled();
1285
1286 if (enabled) {
1287 panel_manager->ShowPanel(session_id, "dungeon.workbench");
1288
1289 // Hide standalone workflow windows unless explicitly pinned.
1290 for (const auto& descriptor :
1291 panel_manager->GetPanelsInCategory(session_id, "Dungeon")) {
1292 const std::string& card_id = descriptor.card_id;
1293 if (card_id == "dungeon.workbench") {
1294 continue;
1295 }
1296 if (panel_manager->IsPanelPinned(session_id, card_id)) {
1297 continue;
1298 }
1299 const bool is_room_window = card_id.rfind("dungeon.room_", 0) == 0;
1300 if (card_id == kRoomSelectorId || card_id == kRoomMatrixId ||
1301 is_room_window) {
1302 panel_manager->HidePanel(session_id, card_id);
1303 }
1304 }
1305 } else {
1306 panel_manager->HidePanel(session_id, "dungeon.workbench");
1307 panel_manager->ShowPanel(session_id, kRoomSelectorId);
1308 panel_manager->ShowPanel(session_id, kRoomMatrixId);
1309 if (current_room_id_ >= 0) {
1311 }
1312 }
1313
1314 if (show_toast && dependencies_.toast_manager && was_enabled != enabled) {
1316 enabled ? "Dungeon workflow: Workbench"
1317 : "Dungeon workflow: Standalone Panels",
1319 }
1320}
1321
1323 bool show_toast) {
1327}
1328
1330 for (int i = 0; i < active_rooms_.Size; i++) {
1331 int room_id = active_rooms_[i];
1332 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1333 bool panel_visible = true;
1335 panel_visible = dependencies_.panel_manager->IsPanelVisible(card_id);
1336 }
1337
1338 if (!panel_visible) {
1340 room_cards_.erase(room_id);
1341 active_rooms_.erase(active_rooms_.Data + i);
1342 ReleaseRoomPanelSlotId(room_id);
1343 // Clean up viewer
1344 room_viewers_.Erase(room_id);
1345 i--;
1346 continue;
1347 }
1348
1349 bool is_pinned = dependencies_.panel_manager &&
1351 std::string active_category =
1354 : "";
1355
1356 if (active_category != "Dungeon" && !is_pinned) {
1357 continue;
1358 }
1359
1360 // Ensure room card exists (should have been created by ShowRoomPanel/ShowPanel)
1361 if (room_cards_.find(room_id) == room_cards_.end()) {
1362 ShowRoomPanel(room_id);
1363 }
1364
1365 auto& room_card = room_cards_[room_id];
1366 bool open = true;
1367
1368 ImGui::SetNextWindowClass(&room_window_class_);
1369 if (room_dock_id_ == 0) {
1370 room_dock_id_ = ImGui::GetID("DungeonRoomDock");
1371 }
1372 ImGui::SetNextWindowDockID(room_dock_id_, ImGuiCond_FirstUseEver);
1373
1374 if (room_card->Begin(&open)) {
1375 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) {
1376 OnRoomSelected(room_id, /*request_focus=*/false);
1377 }
1378 DrawRoomTab(room_id);
1379 }
1380 room_card->End();
1381
1382 if (!open) {
1385 }
1386
1387 room_cards_.erase(room_id);
1388 active_rooms_.erase(active_rooms_.Data + i);
1389 room_viewers_.Erase(room_id);
1390 ReleaseRoomPanelSlotId(room_id);
1391 i--;
1392 }
1393 }
1394}
1395
1397 if (auto it = room_panel_slot_ids_.find(room_id);
1398 it != room_panel_slot_ids_.end()) {
1399 return it->second;
1400 }
1401 const int slot_id = next_room_panel_slot_id_++;
1402 room_panel_slot_ids_[room_id] = slot_id;
1403 return slot_id;
1404}
1405
1407 room_panel_slot_ids_.erase(room_id);
1408}
1409
1411 const auto& theme = AgentUI::GetTheme();
1412 if (room_id < 0 || room_id >= 0x128) {
1413 ImGui::Text("Invalid room ID: %d", room_id);
1414 return;
1415 }
1416
1417 auto& room = rooms_[room_id];
1418
1419 if (!room.IsLoaded()) {
1420 auto status = room_loader_.LoadRoom(room_id, room);
1421 if (!status.ok()) {
1422 ImGui::TextColored(theme.text_error_red, "Failed to load room: %s",
1423 status.message().data());
1424 return;
1425 }
1426
1428 auto sys_status = dungeon_editor_system_->ReloadRoom(room_id);
1429 if (!sys_status.ok()) {
1430 LOG_ERROR("DungeonEditorV2", "Failed to load system data: %s",
1431 sys_status.message().data());
1432 }
1433 }
1434 }
1435
1436 if (room.IsLoaded()) {
1437 bool needs_render = false;
1438
1439 // Chronological Step 1: Load Room Data from ROM
1440 // This reads the 14-byte room header (blockset, palette, effect, tags)
1441 // Reference: kRoomHeaderPointer (0xB5DD)
1442 if (room.blocks().empty()) {
1443 room.LoadRoomGraphics(room.blockset());
1444 needs_render = true;
1445 LOG_DEBUG("[DungeonEditorV2]", "Loaded room %d graphics from ROM",
1446 room_id);
1447 }
1448
1449 // Chronological Step 2: Load Objects from ROM
1450 // This reads the variable-length object stream (subtype 1, 2, 3 objects)
1451 // Reference: kRoomObjectPointer (0x874C)
1452 // CRITICAL: This step decodes floor1/floor2 bytes which dictate the floor
1453 // pattern
1454 if (room.GetTileObjects().empty()) {
1455 room.LoadObjects();
1456 needs_render = true;
1457 LOG_DEBUG("[DungeonEditorV2]", "Loaded room %d objects from ROM",
1458 room_id);
1459 }
1460
1461 // Chronological Step 3: Render Graphics to Bitmaps
1462 // This executes the draw routines (bank_01.asm logic) to populate BG1/BG2
1463 // buffers Sequence:
1464 // 1. Draw Floor (from floor1/floor2)
1465 // 2. Draw Layout (walls/floors from object list)
1466 // 3. Draw Objects (subtypes 1, 2, 3)
1467 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1468 if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
1469 room.RenderRoomGraphics();
1470 LOG_DEBUG("[DungeonEditorV2]", "Rendered room %d to bitmaps", room_id);
1471 }
1472 }
1473
1474 if (room.IsLoaded()) {
1475 ImGui::TextColored(theme.text_success_green, ICON_MD_CHECK " Loaded");
1476 } else {
1477 ImGui::TextColored(theme.text_error_red, ICON_MD_PENDING " Not Loaded");
1478 }
1479 ImGui::SameLine();
1480 ImGui::TextDisabled("Objects: %zu", room.GetTileObjects().size());
1481
1482 // Warp to Room button — sends room ID to running Mesen2 emulator
1483 ImGui::SameLine();
1484 {
1486 bool connected = client && client->IsConnected();
1487 if (!connected) {
1488 ImGui::BeginDisabled();
1489 }
1490 std::string warp_label =
1491 absl::StrFormat(ICON_MD_ROCKET_LAUNCH " Warp##warp_%03X", room_id);
1492 if (ImGui::SmallButton(warp_label.c_str())) {
1493 auto status = client->WriteWord(0x7E00A0, static_cast<uint16_t>(room_id));
1494 if (status.ok()) {
1497 absl::StrFormat("Warped to room 0x%03X", room_id),
1499 }
1500 } else {
1503 absl::StrFormat("Warp failed: %s", status.message()),
1505 }
1506 }
1507 }
1508 if (!connected) {
1509 ImGui::EndDisabled();
1510 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1511 ImGui::SetTooltip("Connect to Mesen2 first");
1512 }
1513 } else {
1514 if (ImGui::IsItemHovered()) {
1515 ImGui::SetTooltip("Warp to room 0x%03X in Mesen2", room_id);
1516 }
1517 }
1518 }
1519
1520 ImGui::Separator();
1521
1522 // Use per-room viewer
1523 if (auto* viewer = GetViewerForRoom(room_id)) {
1524 viewer->DrawDungeonCanvas(room_id);
1525 }
1526}
1527
1529 switch (intent) {
1531 OnRoomSelected(room_id, /*request_focus=*/true);
1532 break;
1534 // Explicitly pivot to panel workflow when user asks for standalone room.
1535 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1536 return;
1537 }
1540 }
1541 // Update shared state (same as OnRoomSelected with request_focus=true)
1542 OnRoomSelected(room_id, /*request_focus=*/false);
1543 // Now force-open a standalone panel for this room
1544 ShowRoomPanel(room_id);
1545 break;
1546 }
1548 OnRoomSelected(room_id, /*request_focus=*/false);
1549 break;
1550 }
1551}
1552
1553void DungeonEditorV2::OnRoomSelected(int room_id, bool request_focus) {
1554 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1555 LOG_WARN("DungeonEditorV2", "Ignoring invalid room selection: %d", room_id);
1556 return;
1557 }
1558 if (room_id != current_room_id_ && workbench_panel_) {
1560 }
1561 current_room_id_ = room_id;
1562 room_selector_.set_current_room_id(static_cast<uint16_t>(room_id));
1563
1564 // Track recent rooms (remove if already present, add to front)
1565 recent_rooms_.erase(
1566 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
1567 recent_rooms_.end());
1568 recent_rooms_.push_front(room_id);
1569 if (recent_rooms_.size() > kMaxRecentRooms) {
1570 recent_rooms_.pop_back();
1571 }
1572
1574 dungeon_editor_system_->SetExternalRoom(&rooms_[room_id]);
1575 }
1576
1577 // Update all sub-panels (Object Editor, Sprite Editor, etc.)
1578 SyncPanelsToRoom(room_id);
1579
1580 // Sync palette with current room (must happen before early return for focus changes)
1581
1582 // Sync palette with current room (must happen before early return for focus changes)
1583 if (room_id >= 0 && room_id < (int)rooms_.size()) {
1584 auto& room = rooms_[room_id];
1585 if (!room.IsLoaded()) {
1586 room_loader_.LoadRoom(room_id, room);
1587 }
1588
1589 if (room.IsLoaded()) {
1590 current_palette_id_ = room.palette();
1592
1593 // Update viewer and object editor palette
1594 if (auto* viewer = GetViewerForRoom(room_id)) {
1595 viewer->SetCurrentPaletteId(current_palette_id_);
1596
1597 if (game_data()) {
1598 auto dungeon_main_pal_group =
1600 if (current_palette_id_ < (int)dungeon_main_pal_group.size()) {
1601 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1602 auto result =
1604 if (result.ok()) {
1605 current_palette_group_ = result.value();
1606 viewer->SetCurrentPaletteGroup(current_palette_group_);
1610 }
1611 // Sync palette to graphics panel for proper sheet coloring
1615 }
1616 }
1617 }
1618 }
1619 }
1620 }
1621 }
1622
1623 // Workbench mode uses a single stable window and does not spawn per-room
1624 // panels. Keep selection + panels in sync and return.
1626 if (dependencies_.panel_manager && request_focus) {
1627 // Only force-show if it's already visible or if it's the first initialization
1628 // This avoids obtrusive behavior when the user explicitly closed it.
1629 if (dependencies_.panel_manager->IsPanelVisible("dungeon.workbench")) {
1630 dependencies_.panel_manager->ShowPanel("dungeon.workbench");
1631 }
1632 }
1633 return;
1634 }
1635
1636 // Check if room is already open
1637 for (int i = 0; i < active_rooms_.Size; i++) {
1638 if (active_rooms_[i] == room_id) {
1639 // Always ensure panel is visible, even if already in active_rooms_
1641 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1643 }
1644 if (request_focus) {
1645 FocusRoom(room_id);
1646 }
1647 return;
1648 }
1649 }
1650
1651 active_rooms_.push_back(room_id);
1653
1655 // Use unified ResourceLabelProvider for room names
1656 std::string room_name = absl::StrFormat(
1657 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
1658
1659 std::string base_card_id = absl::StrFormat("dungeon.room_%d", room_id);
1660
1662 {.card_id = base_card_id,
1663 .display_name = room_name,
1664 .window_title = ICON_MD_GRID_ON " " + room_name,
1665 .icon = ICON_MD_GRID_ON,
1666 .category = "Dungeon",
1667 .shortcut_hint = "",
1668 .visibility_flag = nullptr,
1669 .priority = 200 + room_id});
1670
1671 dependencies_.panel_manager->ShowPanel(base_card_id);
1672 }
1673}
1674
1676 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
1677 return;
1678 }
1679 int room_id = entrances_[entrance_id].room_;
1680 OnRoomSelected(room_id);
1681}
1682
1684 auto status = Save();
1685 if (status.ok()) {
1688 }
1689 } else {
1690 LOG_ERROR("DungeonEditorV2", "SaveAllRooms failed: %s",
1691 status.message().data());
1694 absl::StrFormat("Save all failed: %s", status.message()),
1696 }
1697 }
1698}
1699
1700void DungeonEditorV2::add_room(int room_id) {
1701 OnRoomSelected(room_id);
1702}
1703
1705 auto it = room_cards_.find(room_id);
1706 if (it != room_cards_.end()) {
1707 it->second->Focus();
1708 }
1709}
1710
1717
1728
1732
1735 LOG_ERROR("DungeonEditorV2", "Cannot place object: Invalid room ID %d",
1739 absl::StrFormat("Object 0x%02X: no room selected (invalid room %d)",
1740 obj.id_, current_room_id_),
1742 }
1744 object_editor_panel_->SetPlacementError(absl::StrFormat(
1745 "Cannot place 0x%02X: invalid room %d", obj.id_, current_room_id_));
1746 }
1747 return;
1748 }
1749
1750 auto& room = rooms_[current_room_id_];
1751
1752 LOG_INFO("DungeonEditorV2",
1753 "Placing object ID=0x%02X at position (%d,%d) in room %03X", obj.id_,
1754 obj.x_, obj.y_, current_room_id_);
1755
1756 room.RenderRoomGraphics();
1757 LOG_DEBUG("DungeonEditorV2",
1758 "Object placed and room re-rendered successfully");
1759 // Brief success feedback so the user knows the placement was accepted.
1760 // Kept minimal (no inline panel update) — success is non-spammy by design.
1763 absl::StrFormat("Placed 0x%02X in room %03X", obj.id_,
1766 }
1767}
1768
1770 int room_id, const zelda3::RoomObject& object) {
1771 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1772 LOG_WARN("DungeonEditorV2", "Edit Graphics ignored (invalid room id %d)",
1773 room_id);
1774 return;
1775 }
1776
1777 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
1778 if (!editor_manager) {
1779 LOG_WARN("DungeonEditorV2",
1780 "Edit Graphics ignored (editor manager unavailable)");
1781 return;
1782 }
1783
1784 auto& room = rooms_[room_id];
1785 room.LoadRoomGraphics(room.blockset());
1786
1787 uint16_t sheet_id = 0;
1788 uint16_t tile_index = 0;
1789 bool resolved_sheet = false;
1790 if (auto tiles_or = object.GetTiles();
1791 tiles_or.ok() && !tiles_or.value().empty()) {
1792 const uint16_t tile_id = tiles_or.value().front().id_;
1793 const size_t block_index = static_cast<size_t>(tile_id / 64);
1794 const auto blocks = room.blocks();
1795 if (block_index < blocks.size()) {
1796 sheet_id = blocks[block_index];
1797 resolved_sheet = true;
1798 }
1799 const int tiles_per_row = gfx::kTilesheetWidth / 8;
1800 const int tiles_per_col = gfx::kTilesheetHeight / 8;
1801 const int tiles_per_sheet = tiles_per_row * tiles_per_col;
1802 if (tiles_per_sheet > 0) {
1803 tile_index = static_cast<uint16_t>(tile_id % tiles_per_sheet);
1804 }
1805 }
1806
1807 editor_manager->SwitchToEditor(EditorType::kGraphics, true);
1808 if (auto* editor_set = editor_manager->GetCurrentEditorSet()) {
1809 if (auto* graphics = editor_set->GetGraphicsEditor()) {
1810 if (resolved_sheet) {
1811 graphics->SelectSheet(sheet_id);
1812 graphics->HighlightTile(sheet_id, tile_index,
1813 absl::StrFormat("Object 0x%02X", object.id_));
1814 }
1815 }
1816 }
1817
1820 }
1821}
1822
1823void DungeonEditorV2::SwapRoomInPanel(int old_room_id, int new_room_id) {
1824 // Defer the swap until after the current frame's draw phase completes
1825 // This prevents modifying data structures while ImGui is still using them
1826 if (new_room_id < 0 || new_room_id >= static_cast<int>(rooms_.size())) {
1827 return;
1828 }
1829 pending_swap_.old_room_id = old_room_id;
1830 pending_swap_.new_room_id = new_room_id;
1831 pending_swap_.pending = true;
1832}
1833
1835 if (!pending_swap_.pending) {
1836 return;
1837 }
1838
1839 int old_room_id = pending_swap_.old_room_id;
1840 int new_room_id = pending_swap_.new_room_id;
1841 pending_swap_.pending = false;
1842
1843 // Find the position of old_room in active_rooms_
1844 int swap_index = -1;
1845 for (int i = 0; i < active_rooms_.Size; i++) {
1846 if (active_rooms_[i] == old_room_id) {
1847 swap_index = i;
1848 break;
1849 }
1850 }
1851
1852 if (swap_index < 0) {
1853 // Old room not found in active rooms, just select the new one
1854 OnRoomSelected(new_room_id);
1855 return;
1856 }
1857
1858 // Avoid swapping into an already-open room panel (the per-room maps assume
1859 // room IDs are unique). In this case, just focus/select the existing room.
1860 for (int i = 0; i < active_rooms_.Size; i++) {
1861 if (i != swap_index && active_rooms_[i] == new_room_id) {
1862 OnRoomSelected(new_room_id);
1863 return;
1864 }
1865 }
1866
1867 // Preserve the old panel's stable ImGui window identity by transferring its
1868 // slot id to the new room id.
1869 int slot_id = -1;
1870 if (auto it = room_panel_slot_ids_.find(old_room_id);
1871 it != room_panel_slot_ids_.end()) {
1872 slot_id = it->second;
1873 room_panel_slot_ids_.erase(it);
1874 } else {
1875 slot_id = next_room_panel_slot_id_++;
1876 }
1877 room_panel_slot_ids_[new_room_id] = slot_id;
1878
1879 // Preserve the viewer instance so canvas pan/zoom and UI state don't reset
1880 // when navigating with arrows (swap-in-panel).
1881 room_viewers_.Rename(old_room_id, new_room_id);
1882
1883 // Replace old room with new room in active_rooms_
1884 active_rooms_[swap_index] = new_room_id;
1886
1887 // Unregister old panel
1889 std::string old_card_id = absl::StrFormat("dungeon.room_%d", old_room_id);
1890 const bool old_pinned =
1893
1894 // Register new panel
1895 // Use unified ResourceLabelProvider for room names
1896 std::string new_room_name = absl::StrFormat(
1897 "[%03X] %s", new_room_id, zelda3::GetRoomLabel(new_room_id).c_str());
1898
1899 std::string new_card_id = absl::StrFormat("dungeon.room_%d", new_room_id);
1900
1902 {.card_id = new_card_id,
1903 .display_name = new_room_name,
1904 .window_title = ICON_MD_GRID_ON " " + new_room_name,
1905 .icon = ICON_MD_GRID_ON,
1906 .category = "Dungeon",
1907 .shortcut_hint = "",
1908 .visibility_flag = nullptr,
1909 .priority = 200 + new_room_id});
1910
1911 if (old_pinned) {
1912 dependencies_.panel_manager->SetPanelPinned(new_card_id, true);
1913 }
1915 }
1916
1917 // Clean up old room's card and viewer
1918 room_cards_.erase(old_room_id);
1919
1920 // Update current selection
1921 OnRoomSelected(new_room_id, /*request_focus=*/false);
1922}
1923
1926 return;
1927 }
1928
1929 const bool enabled = pending_workflow_mode_.enabled;
1930 const bool show_toast = pending_workflow_mode_.show_toast;
1932 SetWorkbenchWorkflowMode(enabled, show_toast);
1933}
1934
1936 room_viewers_.Touch(room_id);
1937}
1938
1940 // No-op: LruCache handles removal internally via Erase()
1941 (void)room_id;
1942}
1943
1946 (void)room_id;
1947 return GetWorkbenchViewer();
1948 }
1949
1950 // Set eviction predicate to protect active rooms from eviction
1951 room_viewers_.SetEvictionPredicate([this](const int& candidate) {
1952 for (int i = 0; i < active_rooms_.size(); ++i) {
1953 if (active_rooms_[i] == candidate)
1954 return false;
1955 }
1956 return true;
1957 });
1958
1959 if (auto* existing = room_viewers_.Get(room_id)) {
1960 // Viewer already exists - Get() already touched LRU
1961 auto* viewer_ptr = existing->get();
1962
1963 // Update pinned state from manager
1965 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1966 viewer_ptr->SetPinned(
1968 viewer_ptr->SetPinCallback([this, card_id, room_id](bool pinned) {
1970 dependencies_.panel_manager->SetPanelPinned(card_id, pinned);
1971 if (auto* v = GetViewerForRoom(room_id)) {
1972 v->SetPinned(pinned);
1973 }
1974 }
1975 });
1976 }
1977
1978 return viewer_ptr;
1979 }
1980
1981 // Creating a new viewer - Insert will handle LRU and eviction
1982 {
1983 auto viewer = std::make_unique<DungeonCanvasViewer>(rom_);
1984 viewer->SetCompactHeaderMode(false);
1985 viewer->SetRoomDetailsExpanded(true);
1986 DungeonCanvasViewer* viewer_ptr = viewer.get();
1987 viewer->SetRooms(&rooms_);
1988 viewer->SetRenderer(renderer_);
1989 viewer->SetCurrentPaletteGroup(current_palette_group_);
1990 viewer->SetCurrentPaletteId(current_palette_id_);
1991 viewer->SetGameData(game_data_);
1992
1993 // These hooks must remain correct even when a room panel swaps rooms while
1994 // keeping the same viewer instance (to preserve canvas pan/zoom + UI
1995 // state). Use the viewer's best-effort current room context instead of
1996 // capturing room_id at creation time.
1997 viewer->object_interaction().SetMutationCallback([this, viewer_ptr]() {
1998 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
1999 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2000 const auto domain =
2002 if (domain == MutationDomain::kTileObjects) {
2003 BeginUndoSnapshot(rid);
2004 } else if (domain == MutationDomain::kCustomCollision) {
2005 BeginCollisionUndoSnapshot(rid);
2006 } else if (domain == MutationDomain::kWaterFill) {
2007 BeginWaterFillUndoSnapshot(rid);
2008 }
2009 }
2010 });
2011
2012 viewer->object_interaction().SetCacheInvalidationCallback([this,
2013 viewer_ptr]() {
2014 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2015 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2016 const auto domain =
2018 if (domain == MutationDomain::kTileObjects) {
2019 rooms_[rid].MarkObjectsDirty();
2020 rooms_[rid].RenderRoomGraphics();
2021 // Drag edits invalidate incrementally; finalize once the drag ends
2022 // (TileObjectHandler emits an extra invalidation on release).
2023 const auto mode =
2024 viewer_ptr->object_interaction().mode_manager().GetMode();
2026 FinalizeUndoAction(rid);
2027 }
2028 } else if (domain == MutationDomain::kCustomCollision) {
2029 const auto mode =
2030 viewer_ptr->object_interaction().mode_manager().GetMode();
2031 const auto& st =
2033 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2034 return;
2035 }
2036 FinalizeCollisionUndoAction(rid);
2037 } else if (domain == MutationDomain::kWaterFill) {
2038 const auto mode =
2039 viewer_ptr->object_interaction().mode_manager().GetMode();
2040 const auto& st =
2042 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2043 return;
2044 }
2045 FinalizeWaterFillUndoAction(rid);
2046 }
2047 }
2048 });
2049
2050 viewer->object_interaction().SetObjectPlacedCallback(
2051 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2052
2053 if (dungeon_editor_system_) {
2054 viewer->SetEditorSystem(dungeon_editor_system_.get());
2055 }
2056 viewer->SetRoomNavigationCallback([this](int target_room) {
2057 if (target_room >= 0 && target_room < static_cast<int>(rooms_.size())) {
2058 OnRoomSelected(target_room);
2059 }
2060 });
2061 // Swap callback swaps the room in the current panel instead of opening new
2062 viewer->SetRoomSwapCallback([this](int old_room, int new_room) {
2063 SwapRoomInPanel(old_room, new_room);
2064 });
2065 viewer->SetShowObjectPanelCallback([this]() { ShowPanel(kObjectToolsId); });
2066 viewer->SetShowSpritePanelCallback(
2067 [this]() { ShowPanel("dungeon.sprite_editor"); });
2068 viewer->SetShowItemPanelCallback(
2069 [this]() { ShowPanel("dungeon.item_editor"); });
2070 viewer->SetShowRoomListCallback([this]() { ShowPanel(kRoomSelectorId); });
2071 viewer->SetShowRoomMatrixCallback([this]() { ShowPanel(kRoomMatrixId); });
2072 viewer->SetShowEntranceListCallback(
2073 [this]() { ShowPanel(kEntranceListId); });
2074 viewer->SetShowRoomGraphicsCallback(
2075 [this]() { ShowPanel(kRoomGraphicsId); });
2076 viewer->SetShowDungeonSettingsCallback(
2077 [this]() { ShowPanel("dungeon.settings"); });
2078 viewer->SetEditGraphicsCallback(
2079 [this](int target_room_id, const zelda3::RoomObject& object) {
2080 OpenGraphicsEditorForObject(target_room_id, object);
2081 });
2082 viewer->SetSaveRoomCallback([this](int target_room_id) {
2083 auto status = SaveRoom(target_room_id);
2084 if (!status.ok()) {
2085 LOG_ERROR("DungeonEditorV2", "Save Room failed: %s",
2086 status.message().data());
2087 if (dependencies_.toast_manager) {
2088 dependencies_.toast_manager->Show(
2089 absl::StrFormat("Save Room failed: %s", status.message()),
2091 }
2092 return;
2093 }
2094 if (dependencies_.toast_manager) {
2095 dependencies_.toast_manager->Show("Room saved", ToastType::kSuccess);
2096 }
2097 });
2098
2099 // Wire up pinning for room panels
2100 if (dependencies_.panel_manager) {
2101 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2102 viewer->SetPinned(dependencies_.panel_manager->IsPanelPinned(card_id));
2103 viewer->SetPinCallback([this, card_id, room_id](bool pinned) {
2104 if (dependencies_.panel_manager) {
2105 dependencies_.panel_manager->SetPanelPinned(card_id, pinned);
2106 // Sync state back to viewer in all panels showing this room
2107 if (auto* v = GetViewerForRoom(room_id)) {
2108 v->SetPinned(pinned);
2109 }
2110 }
2111 });
2112 }
2113
2114 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2115 viewer->SetProject(dependencies_.project);
2116
2117 auto* stored = room_viewers_.Insert(room_id, std::move(viewer));
2118 return stored->get();
2119 }
2120}
2121
2122DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchViewer() {
2123 if (!workbench_viewer_) {
2124 workbench_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2125 auto* viewer = workbench_viewer_.get();
2126 viewer->SetCompactHeaderMode(true);
2127 viewer->SetRoomDetailsExpanded(false);
2128 viewer->SetHeaderVisible(false);
2129 viewer->SetRooms(&rooms_);
2130 viewer->SetRenderer(renderer_);
2131 viewer->SetCurrentPaletteGroup(current_palette_group_);
2132 viewer->SetCurrentPaletteId(current_palette_id_);
2133 viewer->SetGameData(game_data_);
2134
2135 // Workbench uses a single viewer; these hooks use the viewer's current room
2136 // context (set at DrawDungeonCanvas start) so room switching stays correct.
2137 viewer->object_interaction().SetMutationCallback([this, viewer]() {
2138 const int rid = viewer ? viewer->current_room_id() : -1;
2139 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2140 const auto domain = viewer->object_interaction().last_mutation_domain();
2141 if (domain == MutationDomain::kTileObjects) {
2142 BeginUndoSnapshot(rid);
2143 } else if (domain == MutationDomain::kCustomCollision) {
2144 BeginCollisionUndoSnapshot(rid);
2145 } else if (domain == MutationDomain::kWaterFill) {
2146 BeginWaterFillUndoSnapshot(rid);
2147 }
2148 }
2149 });
2150 viewer->object_interaction().SetCacheInvalidationCallback([this, viewer]() {
2151 const int rid = viewer ? viewer->current_room_id() : -1;
2152 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2153 const auto domain =
2154 viewer->object_interaction().last_invalidation_domain();
2155 if (domain == MutationDomain::kTileObjects) {
2156 rooms_[rid].MarkObjectsDirty();
2157 rooms_[rid].RenderRoomGraphics();
2158 const auto mode =
2159 viewer->object_interaction().mode_manager().GetMode();
2160 if (mode != InteractionMode::DraggingObjects) {
2161 FinalizeUndoAction(rid);
2162 }
2163 } else if (domain == MutationDomain::kCustomCollision) {
2164 const auto mode =
2165 viewer->object_interaction().mode_manager().GetMode();
2166 const auto& st =
2167 viewer->object_interaction().mode_manager().GetModeState();
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->object_interaction().mode_manager().GetMode();
2175 const auto& st =
2176 viewer->object_interaction().mode_manager().GetModeState();
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
2192 // In workbench mode, arrow navigation swaps the current room without
2193 // changing window identities.
2194 viewer->SetRoomSwapCallback([this](int /*old_room*/, int new_room) {
2195 OnRoomSelected(new_room, /*request_focus=*/false);
2196 });
2197 viewer->SetRoomNavigationCallback([this](int target_room) {
2198 OnRoomSelected(target_room, /*request_focus=*/false);
2199 });
2200
2201 viewer->SetShowObjectPanelCallback([this]() { ShowPanel(kObjectToolsId); });
2202 viewer->SetShowSpritePanelCallback(
2203 [this]() { ShowPanel("dungeon.sprite_editor"); });
2204 viewer->SetShowItemPanelCallback(
2205 [this]() { ShowPanel("dungeon.item_editor"); });
2206 viewer->SetShowRoomListCallback([this]() { ShowPanel(kRoomSelectorId); });
2207 viewer->SetShowRoomMatrixCallback([this]() { ShowPanel(kRoomMatrixId); });
2208 viewer->SetShowEntranceListCallback(
2209 [this]() { ShowPanel(kEntranceListId); });
2210 viewer->SetShowRoomGraphicsCallback(
2211 [this]() { ShowPanel(kRoomGraphicsId); });
2212 viewer->SetShowDungeonSettingsCallback(
2213 [this]() { ShowPanel("dungeon.settings"); });
2214 viewer->SetEditGraphicsCallback(
2215 [this](int target_room_id, const zelda3::RoomObject& object) {
2216 OpenGraphicsEditorForObject(target_room_id, object);
2217 });
2218 viewer->SetSaveRoomCallback([this](int target_room_id) {
2219 auto status = SaveRoom(target_room_id);
2220 if (!status.ok()) {
2221 LOG_ERROR("DungeonEditorV2", "Save Room failed: %s",
2222 status.message().data());
2223 if (dependencies_.toast_manager) {
2224 dependencies_.toast_manager->Show(
2225 absl::StrFormat("Save Room failed: %s", status.message()),
2226 ToastType::kError);
2227 }
2228 return;
2229 }
2230 if (dependencies_.toast_manager) {
2231 dependencies_.toast_manager->Show("Room saved", ToastType::kSuccess);
2232 }
2233 });
2234
2235 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2236 viewer->SetProject(dependencies_.project);
2237 }
2238
2239 return workbench_viewer_.get();
2240}
2241
2242DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchCompareViewer() {
2243 if (!workbench_compare_viewer_) {
2244 workbench_compare_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2245 auto* viewer = workbench_compare_viewer_.get();
2246 viewer->SetCompactHeaderMode(true);
2247 viewer->SetRoomDetailsExpanded(false);
2248 viewer->SetRooms(&rooms_);
2249 viewer->SetRenderer(renderer_);
2250 viewer->SetCurrentPaletteGroup(current_palette_group_);
2251 viewer->SetCurrentPaletteId(current_palette_id_);
2252 viewer->SetGameData(game_data_);
2253
2254 // Compare viewer is read-only by default: no object selection/mutation, but
2255 // still allows canvas pan/zoom.
2256 viewer->SetObjectInteractionEnabled(false);
2257 viewer->SetHeaderReadOnly(true);
2258 viewer->SetHeaderVisible(false);
2259
2260 if (dungeon_editor_system_) {
2261 // Allows consistent rendering paths that depend on the editor system, but
2262 // interaction is still disabled.
2263 viewer->SetEditorSystem(dungeon_editor_system_.get());
2264 }
2265
2266 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2267 viewer->SetProject(dependencies_.project);
2268 }
2269
2270 return workbench_compare_viewer_.get();
2271}
2272
2273absl::Status DungeonEditorV2::Undo() {
2274 // Finalize any in-progress edit before undoing.
2275 if (pending_undo_.room_id >= 0) {
2276 FinalizeUndoAction(pending_undo_.room_id);
2277 }
2278 if (pending_collision_undo_.room_id >= 0) {
2279 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2280 }
2281 if (pending_water_fill_undo_.room_id >= 0) {
2282 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2283 }
2284 return undo_manager_.Undo();
2285}
2286
2287absl::Status DungeonEditorV2::Redo() {
2288 // Finalize any in-progress edit before redoing.
2289 if (pending_undo_.room_id >= 0) {
2290 FinalizeUndoAction(pending_undo_.room_id);
2291 }
2292 if (pending_collision_undo_.room_id >= 0) {
2293 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2294 }
2295 if (pending_water_fill_undo_.room_id >= 0) {
2296 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2297 }
2298 return undo_manager_.Redo();
2299}
2300
2301absl::Status DungeonEditorV2::Cut() {
2302 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
2303 viewer->object_interaction().HandleCopySelected();
2304 viewer->object_interaction().HandleDeleteSelected();
2305 }
2306 return absl::OkStatus();
2307}
2308
2309absl::Status DungeonEditorV2::Copy() {
2310 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
2311 viewer->object_interaction().HandleCopySelected();
2312 }
2313 return absl::OkStatus();
2314}
2315
2316absl::Status DungeonEditorV2::Paste() {
2317 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
2318 viewer->object_interaction().HandlePasteObjects();
2319 }
2320 return absl::OkStatus();
2321}
2322
2323void DungeonEditorV2::BeginUndoSnapshot(int room_id) {
2324 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2325 return;
2326
2327 // Detect leaked undo snapshots (double-Begin without Finalize).
2328 if (has_pending_undo_) {
2329 LOG_ERROR("DungeonEditor",
2330 "BeginUndoSnapshot called twice without FinalizeUndoAction. "
2331 "Previous snapshot for room %d is being leaked. Finalizing now.",
2332 pending_undo_.room_id);
2333 // Auto-finalize the leaked snapshot to prevent silent state loss.
2334 if (pending_undo_.room_id >= 0) {
2335 FinalizeUndoAction(pending_undo_.room_id);
2336 }
2337 }
2338
2339 pending_undo_.room_id = room_id;
2340 pending_undo_.before_objects = rooms_[room_id].GetTileObjects();
2341 has_pending_undo_ = true;
2342}
2343
2344void DungeonEditorV2::FinalizeUndoAction(int room_id) {
2345 if (pending_undo_.room_id < 0 || pending_undo_.room_id != room_id)
2346 return;
2347 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2348 return;
2349
2350 auto after_objects = rooms_[room_id].GetTileObjects();
2351
2352 auto action = std::make_unique<DungeonObjectsAction>(
2353 room_id, std::move(pending_undo_.before_objects),
2354 std::move(after_objects),
2355 [this](int rid, const std::vector<zelda3::RoomObject>& objects) {
2356 RestoreRoomObjects(rid, objects);
2357 });
2358 undo_manager_.Push(std::move(action));
2359
2360 pending_undo_.room_id = -1;
2361 pending_undo_.before_objects.clear();
2362 has_pending_undo_ = false;
2363}
2364
2365void DungeonEditorV2::SyncPanelsToRoom(int room_id) {
2366 // Update object editor card with current viewer
2367 if (object_editor_panel_) {
2368 object_editor_panel_->SetCurrentRoom(room_id);
2369 object_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2370 }
2371
2372 // Update sprite and item editor panels with current viewer
2373 if (sprite_editor_panel_) {
2374 sprite_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2375 }
2376 if (item_editor_panel_) {
2377 item_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2378 }
2379 if (custom_collision_panel_) {
2380 auto* viewer = GetViewerForRoom(room_id);
2381 custom_collision_panel_->SetCanvasViewer(viewer);
2382 if (viewer) {
2383 custom_collision_panel_->SetInteraction(&viewer->object_interaction());
2384 }
2385 }
2386 if (water_fill_panel_) {
2387 auto* viewer = GetViewerForRoom(room_id);
2388 water_fill_panel_->SetCanvasViewer(viewer);
2389 if (viewer) {
2390 water_fill_panel_->SetInteraction(&viewer->object_interaction());
2391 }
2392 }
2393
2394 if (dungeon_settings_panel_) {
2395 dungeon_settings_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2396 }
2397
2398 if (object_tile_editor_panel_) {
2399 object_tile_editor_panel_->SetCurrentPaletteGroup(current_palette_group_);
2400 }
2401
2402 if (room_tag_editor_panel_) {
2403 room_tag_editor_panel_->SetCurrentRoomId(room_id);
2404 }
2405
2406 if (overlay_manager_panel_) {
2407 auto* viewer = GetViewerForRoom(room_id);
2408 if (viewer) {
2410 overlay_state.show_grid = viewer->mutable_show_grid();
2411 overlay_state.show_object_bounds = viewer->mutable_show_object_bounds();
2412 overlay_state.show_coordinate_overlay =
2413 viewer->mutable_show_coordinate_overlay();
2414 overlay_state.show_room_debug_info =
2415 viewer->mutable_show_room_debug_info();
2416 overlay_state.show_texture_debug = viewer->mutable_show_texture_debug();
2417 overlay_state.show_layer_info = viewer->mutable_show_layer_info();
2418 overlay_state.show_minecart_tracks =
2419 viewer->mutable_show_minecart_tracks();
2420 overlay_state.show_custom_collision =
2421 viewer->mutable_show_custom_collision_overlay();
2422 overlay_state.show_track_collision =
2423 viewer->mutable_show_track_collision_overlay();
2424 overlay_state.show_camera_quadrants =
2425 viewer->mutable_show_camera_quadrant_overlay();
2426 overlay_state.show_minecart_sprites =
2427 viewer->mutable_show_minecart_sprite_overlay();
2428 overlay_state.show_collision_legend =
2429 viewer->mutable_show_track_collision_legend();
2430 overlay_manager_panel_->SetState(overlay_state);
2431 }
2432 }
2433}
2434
2435void DungeonEditorV2::ShowRoomPanel(int room_id) {
2436 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2437 return;
2438 }
2439
2440 bool already_active = false;
2441 for (int i = 0; i < active_rooms_.Size; ++i) {
2442 if (active_rooms_[i] == room_id) {
2443 already_active = true;
2444 break;
2445 }
2446 }
2447 if (!already_active) {
2448 active_rooms_.push_back(room_id);
2449 room_selector_.set_active_rooms(active_rooms_);
2450 }
2451
2452 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2453
2454 if (dependencies_.panel_manager) {
2455 if (!dependencies_.panel_manager->GetPanelDescriptor(
2456 dependencies_.panel_manager->GetActiveSessionId(), card_id)) {
2457 std::string room_name = absl::StrFormat(
2458 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2459 dependencies_.panel_manager->RegisterPanel(
2460 {.card_id = card_id,
2461 .display_name = room_name,
2462 .window_title = ICON_MD_GRID_ON " " + room_name,
2463 .icon = ICON_MD_GRID_ON,
2464 .category = "Dungeon",
2465 .shortcut_hint = "",
2466 .visibility_flag = nullptr,
2467 .priority = 200 + room_id});
2468 }
2469 dependencies_.panel_manager->ShowPanel(card_id);
2470 }
2471
2472 // Create or update the PanelWindow for this room
2473 if (room_cards_.find(room_id) == room_cards_.end()) {
2474 std::string base_name = absl::StrFormat(
2475 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2476 const int slot_id = GetOrCreateRoomPanelSlotId(room_id);
2477 std::string card_name_str = absl::StrFormat(
2478 "%s###RoomPanelSlot%d", MakePanelTitle(base_name).c_str(), slot_id);
2479
2480 auto card = std::make_shared<gui::PanelWindow>(card_name_str.c_str(),
2482 card->SetDefaultSize(620, 700);
2483 room_cards_[room_id] = card;
2484 }
2485}
2486
2487void DungeonEditorV2::RestoreRoomObjects(
2488 int room_id, const std::vector<zelda3::RoomObject>& objects) {
2489 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2490 return;
2491
2492 auto& room = rooms_[room_id];
2493 room.GetTileObjects() = objects;
2494 room.RenderRoomGraphics();
2495}
2496
2497void DungeonEditorV2::BeginCollisionUndoSnapshot(int room_id) {
2498 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2499 return;
2500
2501 if (pending_collision_undo_.room_id >= 0) {
2502 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2503 }
2504
2505 pending_collision_undo_.room_id = room_id;
2506 pending_collision_undo_.before = rooms_[room_id].custom_collision();
2507}
2508
2509void DungeonEditorV2::FinalizeCollisionUndoAction(int room_id) {
2510 if (pending_collision_undo_.room_id < 0 ||
2511 pending_collision_undo_.room_id != room_id) {
2512 return;
2513 }
2514 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2515 return;
2516
2517 auto after = rooms_[room_id].custom_collision();
2518 if (pending_collision_undo_.before.has_data == after.has_data &&
2519 pending_collision_undo_.before.tiles == after.tiles) {
2520 pending_collision_undo_.room_id = -1;
2521 pending_collision_undo_.before = {};
2522 return;
2523 }
2524
2525 auto action = std::make_unique<DungeonCustomCollisionAction>(
2526 room_id, std::move(pending_collision_undo_.before), std::move(after),
2527 [this](int rid, const zelda3::CustomCollisionMap& map) {
2528 RestoreRoomCustomCollision(rid, map);
2529 });
2530 undo_manager_.Push(std::move(action));
2531
2532 pending_collision_undo_.room_id = -1;
2533 pending_collision_undo_.before = {};
2534}
2535
2536void DungeonEditorV2::RestoreRoomCustomCollision(
2537 int room_id, const zelda3::CustomCollisionMap& map) {
2538 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2539 return;
2540
2541 auto& room = rooms_[room_id];
2542 room.custom_collision() = map;
2543 room.MarkCustomCollisionDirty();
2544}
2545
2546namespace {
2547
2549 WaterFillSnapshot snap;
2551
2552 const auto& zone = room.water_fill_zone();
2553 // Preserve deterministic ordering (ascending offsets) for stable diffs.
2554 for (size_t i = 0; i < zone.tiles.size(); ++i) {
2555 if (zone.tiles[i] != 0) {
2556 snap.offsets.push_back(static_cast<uint16_t>(i));
2557 }
2558 }
2559 return snap;
2560}
2561
2562} // namespace
2563
2564void DungeonEditorV2::BeginWaterFillUndoSnapshot(int room_id) {
2565 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2566 return;
2567
2568 if (pending_water_fill_undo_.room_id >= 0) {
2569 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2570 }
2571
2572 pending_water_fill_undo_.room_id = room_id;
2573 pending_water_fill_undo_.before = MakeWaterFillSnapshot(rooms_[room_id]);
2574}
2575
2576void DungeonEditorV2::FinalizeWaterFillUndoAction(int room_id) {
2577 if (pending_water_fill_undo_.room_id < 0 ||
2578 pending_water_fill_undo_.room_id != room_id) {
2579 return;
2580 }
2581 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2582 return;
2583
2584 auto after = MakeWaterFillSnapshot(rooms_[room_id]);
2585 if (pending_water_fill_undo_.before.sram_bit_mask == after.sram_bit_mask &&
2586 pending_water_fill_undo_.before.offsets == after.offsets) {
2587 pending_water_fill_undo_.room_id = -1;
2588 pending_water_fill_undo_.before = {};
2589 return;
2590 }
2591
2592 auto action = std::make_unique<DungeonWaterFillAction>(
2593 room_id, std::move(pending_water_fill_undo_.before), std::move(after),
2594 [this](int rid, const WaterFillSnapshot& snap) {
2595 RestoreRoomWaterFill(rid, snap);
2596 });
2597 undo_manager_.Push(std::move(action));
2598
2599 pending_water_fill_undo_.room_id = -1;
2600 pending_water_fill_undo_.before = {};
2601}
2602
2603void DungeonEditorV2::RestoreRoomWaterFill(int room_id,
2604 const WaterFillSnapshot& snap) {
2605 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
2606 return;
2607
2608 auto& room = rooms_[room_id];
2609 room.ClearWaterFillZone();
2610 room.set_water_fill_sram_bit_mask(snap.sram_bit_mask);
2611 for (uint16_t off : snap.offsets) {
2612 const int x = static_cast<int>(off % 64);
2613 const int y = static_cast<int>(off / 64);
2614 room.SetWaterFillTile(x, y, /*filled=*/true);
2615 }
2616 room.MarkWaterFillDirty();
2617}
2618
2619} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h: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)
DungeonObjectInteraction & object_interaction()
void SetRooms(std::array< zelda3::Room, 0x128 > *rooms)
void SetPinCallback(std::function< void(bool)> callback)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
absl::Status SaveRoom(int room_id)
std::array< zelda3::Room, 0x128 > rooms_
class CustomCollisionPanel * custom_collision_panel_
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< ObjectEditorPanel > owned_object_editor_panel_
std::unique_ptr< zelda3::DungeonEditorSystem > dungeon_editor_system_
class DungeonWorkbenchPanel * workbench_panel_
std::unordered_map< int, int > room_panel_slot_ids_
void OnRoomSelected(int room_id, bool request_focus=true)
DungeonRoomGraphicsPanel * room_graphics_panel_
ObjectTileEditorPanel * object_tile_editor_panel_
OverlayManagerPanel * overlay_manager_panel_
ObjectEditorPanel * object_editor_panel_
gui::PaletteEditorWidget palette_editor_
void ShowPanel(const std::string &card_id)
static bool IsValidRoomId(int room_id)
void SwapRoomInPanel(int old_room_id, int new_room_id)
static constexpr size_t kMaxRecentRooms
void SetWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
class RoomTagEditorPanel * room_tag_editor_panel_
static constexpr const char * kObjectToolsId
DungeonCanvasViewer * GetViewerForRoom(int room_id)
absl::Status Update() override
class WaterFillPanel * water_fill_panel_
std::unique_ptr< emu::render::EmulatorRenderService > render_service_
DungeonCanvasViewer * GetWorkbenchViewer()
std::unordered_map< int, std::shared_ptr< gui::PanelWindow > > room_cards_
static constexpr const char * kRoomGraphicsId
void QueueWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
static constexpr const char * kRoomMatrixId
static constexpr const char * kRoomSelectorId
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)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
Set the current palette group for graphics rendering.
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 SetRoomSelectedCallback(std::function< void(int)> callback)
void set_active_rooms(const ImVector< int > &rooms)
void set_rooms(std::array< zelda3::Room, 0x128 > *rooms)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void NotifyRoomChanged(int previous_room_id)
Called by the editor when the current room changes.
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)
The EditorManager controls the main editor window and manages the various editor classes.
UndoManager undo_manager_
Definition editor.h:307
zelda3::GameData * game_data() const
Definition editor.h:297
EditorDependencies dependencies_
Definition editor.h:306
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 SetRooms(std::array< zelda3::Room, 0x128 > *rooms)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetGameData(zelda3::GameData *game_data)
void SetPlacementError(const std::string &message)
DungeonObjectSelector & object_selector()
void OpenForObject(int16_t object_id, int room_id, std::array< zelda3::Room, 0x128 > *rooms)
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.
bool ShowPanel(size_t session_id, const std::string &base_card_id)
void RegisterPanel(size_t session_id, const PanelDescriptor &base_info)
std::string GetActiveCategory() const
bool IsPanelVisible(size_t session_id, const std::string &base_card_id) const
void UnregisterPanel(size_t session_id, const std::string &base_card_id)
void ShowAllPanelsInCategory(size_t session_id, const std::string &category)
bool IsPanelPinned(size_t session_id, const std::string &base_card_id) const
void RegisterEditorPanel(std::unique_ptr< EditorPanel > panel)
Register an EditorPanel instance for central drawing.
size_t GetActiveSessionId() const
void SetPanelPinned(size_t session_id, const std::string &base_card_id, bool pinned)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
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)
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 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()
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:411
uint8_t water_fill_sram_bit_mask() const
Definition room.h:453
#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_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
std::string AddressOwnershipToString(AddressOwnership ownership)
const AgentUITheme & GetTheme()
WaterFillSnapshot MakeWaterFillSnapshot(const zelda3::Room &room)
absl::Status SaveWaterFillZones(Rom *rom, std::array< zelda3::Room, 0x128 > &rooms)
Editors are the view controllers for the application.
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:2530
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:2577
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:2210
absl::Status SaveAllPits(Rom *rom)
Definition room.cc:2300
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:2331
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:2370
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:167
gfx::IRenderer * renderer
Definition editor.h:183
RomWritePolicy write_policy
Definition project.h:108
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:145
std::string custom_objects_folder
Definition project.h:140
core::HackManifest hack_manifest
Definition project.h:160
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1287
std::string symbols_filename
Definition project.h:138
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89
std::vector< uint16_t > fill_offsets