yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2.cc
Go to the documentation of this file.
1// Related header
2#include "dungeon_editor_v2.h"
3#include "util/i18n/tr.h"
4
5// C system headers
6#include <cctype>
7#include <cstdio>
8
9// C++ standard library headers
10#include <algorithm>
11#include <array>
12#include <iterator>
13#include <memory>
14#include <string>
15#include <utility>
16#include <vector>
17
18// Third-party library headers
19#include "absl/status/status.h"
20#include "absl/strings/str_format.h"
21#include "absl/types/span.h"
22#include "imgui/imgui.h"
23
24// Project headers
63#include "app/gui/core/icons.h"
65#include "core/features.h"
66#include "core/project.h"
67#include "rom/snes.h"
68#include "util/log.h"
69#include "util/macro.h"
75#include "zelda3/dungeon/room.h"
78
79namespace yaze::editor {
80
81namespace {
82
83bool IsTransientDungeonRoomWindowId(const std::string& card_id) {
84 constexpr const char* kPrefix = "dungeon.room_";
85 constexpr size_t kPrefixLength = 13;
86 if (card_id.rfind(kPrefix, 0) != 0 || card_id.size() <= kPrefixLength) {
87 return false;
88 }
89 return std::all_of(card_id.begin() + kPrefixLength, card_id.end(),
90 [](unsigned char ch) { return std::isdigit(ch) != 0; });
91}
92
93} // namespace
94
96 : rom_(rom), room_loader_(rom), room_selector_(rom), rooms_(rom) {
98 if (rom) {
100 }
101}
102
104 if (palette_listener_id_ >= 0) {
107 }
108
109 // Clear viewer references in panels BEFORE room_viewers_ is destroyed.
110 // Panels are owned by WorkspaceWindowManager and outlive this editor, so they need
111 // to have their viewer pointers cleared to prevent dangling pointer access.
114 }
117 }
118 if (door_editor_panel_) {
120 }
123 }
124 if (item_editor_panel_) {
126 }
130 }
131 if (water_fill_panel_) {
134 }
135}
136
138 if (!rom_ || !rom_->is_loaded()) {
139 return absl::FailedPreconditionError("ROM not loaded");
140 }
141
144
145 std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>
146 refreshed_entrances;
147 std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>
148 refreshed_spawn_points;
149 RETURN_IF_ERROR(room_loader_.LoadRoomEntrances(refreshed_entrances));
150 RETURN_IF_ERROR(room_loader_.LoadDungeonSpawnPoints(refreshed_spawn_points));
151
152 struct RefreshedRoom {
153 int room_id;
154 zelda3::Room room;
155 };
156 std::vector<std::pair<int, bool>> materialized_rooms;
158 [&materialized_rooms](int room_id, const zelda3::Room& room) {
159 materialized_rooms.emplace_back(room_id, room.IsLoaded());
160 });
161
162 std::vector<RefreshedRoom> refreshed_rooms;
163 refreshed_rooms.reserve(materialized_rooms.size());
164 for (const auto& [room_id, was_loaded] : materialized_rooms) {
165 zelda3::Room refreshed_room;
166 if (was_loaded) {
167 RETURN_IF_ERROR(room_loader_.LoadRoom(room_id, refreshed_room));
168 } else {
169 refreshed_room = zelda3::LoadRoomHeaderFromRom(rom_, room_id);
170 refreshed_room.SetGameData(game_data_);
171 }
172 refreshed_rooms.push_back({room_id, std::move(refreshed_room)});
173 }
174
175 // All ROM reads succeeded. Clear every interaction that can retain indices
176 // into Room vectors before replacing any Room contents.
180 }
183 }
184 auto prepare_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
185 if (viewer) {
186 viewer->RefreshRomBackedState(rom_, game_data_, &rooms_, room_id);
187 }
188 };
189 room_viewers_.ForEach(
190 [&prepare_viewer](int room_id,
191 std::unique_ptr<DungeonCanvasViewer>& viewer) {
192 prepare_viewer(viewer.get(), room_id);
193 });
194 prepare_viewer(workbench_viewer_.get(), current_room_id_);
195 const int compare_room_id =
197 IsValidRoomId(workbench_compare_viewer_->current_room_id())
198 ? workbench_compare_viewer_->current_room_id()
200 prepare_viewer(workbench_compare_viewer_.get(), compare_room_id);
201
203 dungeon_editor_system_->ClearHistory();
204 if (auto object_editor = dungeon_editor_system_->GetObjectEditor()) {
205 (void)object_editor->ClearSelection();
206 object_editor->ClearHistory();
207 }
208 }
211 pending_undo_ = {};
212 has_pending_undo_ = false;
215 pending_swap_ = {};
218
222 for (auto& refreshed : refreshed_rooms) {
223 auto* room = rooms_.GetIfMaterialized(refreshed.room_id);
224 if (room) {
225 *room = std::move(refreshed.room);
226 }
227 }
228 entrances_ = std::move(refreshed_entrances);
229 spawn_points_ = std::move(refreshed_spawn_points);
231
234
237 auto* current_room = IsValidRoomId(current_room_id_)
239 : nullptr;
240 RETURN_IF_ERROR(dungeon_editor_system_->RefreshRomBackedState(
241 current_room, current_room_id_));
242 }
243
248 if (auto* current_room = rooms_.GetIfMaterialized(current_room_id_);
249 current_room && game_data_ &&
251 const int resolved_palette_id = current_room->ResolveDungeonPaletteId();
253 resolved_palette_id >= 0 &&
254 resolved_palette_id <
255 static_cast<int>(
257 ? static_cast<uint64_t>(resolved_palette_id)
258 : 0;
264 }
265
266 if (is_loaded_) {
270 }
271
272 auto bind_viewer = [this](DungeonCanvasViewer* viewer, int room_id) {
273 if (!viewer) {
274 return;
275 }
276 viewer->SetRenderer(renderer_);
282 ConfigureViewerRenderContext(viewer, room_id);
284 };
285 room_viewers_.ForEach(
286 [&bind_viewer](int room_id,
287 std::unique_ptr<DungeonCanvasViewer>& viewer) {
288 bind_viewer(viewer.get(), room_id);
289 });
290 bind_viewer(workbench_viewer_.get(), current_room_id_);
291 bind_viewer(workbench_compare_viewer_.get(), compare_room_id);
292
298 }
301 }
304 }
307 }
311 }
312 if (workbench_panel_) {
314 }
315
316 DungeonCanvasViewer* current_viewer = nullptr;
318 current_viewer = workbench_viewer_.get();
319 } else if (auto* viewer = room_viewers_.Get(current_room_id_)) {
320 current_viewer = viewer->get();
321 }
322 if (current_viewer) {
324 }
325
326 return absl::OkStatus();
327}
328
331 room.ClearWaterFillZone();
332 room.ClearWaterFillDirty();
333 });
334
335 bool legacy_imported = false;
336 std::vector<zelda3::WaterFillZoneEntry> zones;
337
338 auto zones_or = zelda3::LoadWaterFillTable(rom_);
339 if (zones_or.ok()) {
340 zones = std::move(zones_or.value());
341 } else {
342 LOG_WARN("DungeonEditorV2", "WaterFillTable parse failed: %s",
343 zones_or.status().message().data());
346 absl::StrFormat("WaterFill table parse failed: %s",
347 zones_or.status().message()),
349 }
350 }
351
352 if (zones.empty()) {
353 std::string sym_path;
358 }
359 auto legacy_or = zelda3::LoadLegacyWaterGateZones(rom_, sym_path);
360 if (legacy_or.ok()) {
361 zones = std::move(legacy_or.value());
362 legacy_imported = !zones.empty();
363 } else {
364 LOG_WARN("DungeonEditorV2", "Legacy water gate import failed: %s",
365 legacy_or.status().message().data());
366 }
367 }
368
369 for (const auto& z : zones) {
370 if (z.room_id < 0 || z.room_id >= static_cast<int>(rooms_.size())) {
371 continue;
372 }
373 auto& room = rooms_[z.room_id];
374 room.set_water_fill_sram_bit_mask(z.sram_bit_mask);
375 for (uint16_t off : z.fill_offsets) {
376 const int x = static_cast<int>(off % 64);
377 const int y = static_cast<int>(off / 64);
378 room.SetWaterFillTile(x, y, true);
379 }
380
381 if (!legacy_imported) {
382 room.ClearWaterFillDirty();
383 }
384 }
385
386 if (legacy_imported && dependencies_.toast_manager) {
388 "Imported legacy water gate zones (save to write new table)",
390 }
391}
392
396 }
398 const bool rom_changed = dependencies_.rom && dependencies_.rom != rom_;
399 if (rom_changed) {
401 // The system owns ROM-backed views; ensure it matches the current ROM.
403 }
406 if (game_data_) {
408 }
409 }
410
411 // Setup docking class for room windows
412 room_window_class_.DockingAllowUnclassed = true;
413 room_window_class_.DockingAlwaysTabBar = true;
414
416 return;
417 auto* window_manager = dependencies_.window_manager;
418
419 // Legacy panel IDs persisted in older layouts/settings.
420 window_manager->RegisterPanelAlias("dungeon.object_tools", kObjectSelectorId);
421 window_manager->RegisterPanelAlias("dungeon.entrances",
422 "dungeon.entrance_properties");
423
424 // Register panels with WorkspaceWindowManager (no boolean flags - visibility is
425 // managed entirely by WorkspaceWindowManager::ShowPanel/HidePanel/IsPanelVisible)
426 window_manager->RegisterPanel(
427 {.card_id = "dungeon.workbench",
428 .display_name = "Dungeon Workbench",
429 .window_title = " Dungeon Workbench",
430 .icon = ICON_MD_WORKSPACES,
431 .category = "Dungeon",
432 .shortcut_hint = "Ctrl+Shift+W",
433 .visibility_flag = nullptr,
434 .priority = 5,
435 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
436 .disabled_tooltip = "Load a ROM to edit dungeon rooms"});
437
438 window_manager->RegisterPanel(
439 {.card_id = kRoomSelectorId,
440 .display_name = "Room List",
441 .window_title = " Room List",
442 .icon = ICON_MD_LIST,
443 .category = "Dungeon",
444 .shortcut_hint = "Ctrl+Shift+R",
445 .visibility_flag = nullptr,
446 .priority = 20,
447 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
448 .disabled_tooltip = "Load a ROM to browse dungeon rooms"});
449
450 window_manager->RegisterPanel(
451 {.card_id = kEntranceListId,
452 .display_name = "Entrance List",
453 .window_title = " Entrance List",
454 .icon = ICON_MD_DOOR_FRONT,
455 .category = "Dungeon",
456 .shortcut_hint = "Ctrl+Shift+E",
457 .visibility_flag = nullptr,
458 .priority = 25,
459 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
460 .disabled_tooltip = "Load a ROM to browse dungeon entrances"});
461
462 window_manager->RegisterPanel(
463 {.card_id = "dungeon.entrance_properties",
464 .display_name = "Entrance Properties",
465 .window_title = " Entrance Properties",
466 .icon = ICON_MD_TUNE,
467 .category = "Dungeon",
468 .shortcut_hint = "",
469 .visibility_flag = nullptr,
470 .priority = 26,
471 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
472 .disabled_tooltip = "Load a ROM to edit entrance properties"});
473
474 window_manager->RegisterPanel(
475 {.card_id = kRoomMatrixId,
476 .display_name = "Room Matrix",
477 .window_title = " Room Matrix",
478 .icon = ICON_MD_GRID_VIEW,
479 .category = "Dungeon",
480 .shortcut_hint = "Ctrl+Shift+M",
481 .visibility_flag = nullptr,
482 .priority = 30,
483 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
484 .disabled_tooltip = "Load a ROM to view the room matrix"});
485
486 window_manager->RegisterPanel(
487 {.card_id = kRoomGraphicsId,
488 .display_name = "Room Graphics",
489 .window_title = " Room Graphics",
490 .icon = ICON_MD_IMAGE,
491 .category = "Dungeon",
492 .shortcut_hint = "Ctrl+Shift+G",
493 .visibility_flag = nullptr,
494 .priority = 50,
495 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
496 .disabled_tooltip = "Load a ROM to view room graphics"});
497
498 window_manager->RegisterPanel(
499 {.card_id = kObjectSelectorId,
500 .display_name = "Object Selector",
501 .window_title = " Object Selector",
502 .icon = ICON_MD_CATEGORY,
503 .category = "Dungeon",
504 .shortcut_hint = "",
505 .visibility_flag = nullptr,
506 .priority = 60,
507 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
508 .disabled_tooltip = "Load a ROM to browse dungeon objects"});
509
510 window_manager->RegisterPanel(
511 {.card_id = kDoorEditorId,
512 .display_name = "Door Editor",
513 .window_title = " Door Editor",
514 .icon = ICON_MD_DOOR_FRONT,
515 .category = "Dungeon",
516 .shortcut_hint = "",
517 .visibility_flag = nullptr,
518 .priority = 69,
519 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
520 .disabled_tooltip = "Load a ROM to edit dungeon doors"});
521
522 window_manager->RegisterPanel(
523 {.card_id = kPaletteEditorId,
524 .display_name = "Palette Editor",
525 .window_title = " Palette Editor",
526 .icon = ICON_MD_PALETTE,
527 .category = "Dungeon",
528 // Avoid conflicting with the global Command Palette (Ctrl/Cmd+Shift+P).
529 .shortcut_hint = "Ctrl+Shift+Alt+P",
530 .visibility_flag = nullptr,
531 .priority = 70,
532 .enabled_condition = [this]() { return rom_ && rom_->is_loaded(); },
533 .disabled_tooltip = "Load a ROM to edit dungeon palettes"});
534
535 // Show default panels on startup.
536 // Workbench mode intentionally suppresses parallel standalone defaults to
537 // avoid two competing workflows being open at once.
539 /*show_toast=*/false);
540
541 // Wire intent-aware callback for double-click / context menu
543 [this](int room_id, RoomSelectionIntent intent) {
544 OnRoomSelected(room_id, intent);
545 });
546
547 // Register WindowContent instances
548 window_manager->RegisterWindowContent(std::make_unique<RoomBrowserContent>(
549 &room_selector_, [this](int room_id) { OnRoomSelected(room_id); }));
550
551 window_manager->RegisterWindowContent(
552 std::make_unique<DungeonEntranceListPanel>(
554 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
555
556 {
557 auto matrix_panel = std::make_unique<RoomMatrixContent>(
559 [this](int room_id) { OnRoomSelected(room_id); },
560 [this](int old_room, int new_room) {
561 SwapRoomInPanel(old_room, new_room);
562 },
563 &rooms_);
564 matrix_panel->SetRoomIntentCallback(
565 [this](int room_id, RoomSelectionIntent intent) {
566 OnRoomSelected(room_id, intent);
567 });
568 window_manager->RegisterWindowContent(std::move(matrix_panel));
569 }
570
571 {
572 auto workbench = std::make_unique<DungeonWorkbenchContent>(
574 [this](int room_id) { OnRoomSelected(room_id); },
575 [this](int room_id, RoomSelectionIntent intent) {
576 OnRoomSelected(room_id, intent);
577 },
578 [this](int room_id) {
579 auto status = SaveRoom(room_id);
580 if (!status.ok()) {
581 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
582 status.message().data());
585 absl::StrFormat("Apply Room failed: %s", status.message()),
587 }
588 return;
589 }
591 dependencies_.toast_manager->Show("Applied room to ROM buffer",
593 }
594 },
595 [this]() { SaveAllRooms(); }, [this]() { return GetWorkbenchViewer(); },
596 [this](int room_id) { return GetWorkbenchCompareViewer(room_id); },
597 [this]() -> const std::deque<int>& { return recent_rooms_; },
598 [this](int room_id) {
599 recent_rooms_.erase(
600 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
601 recent_rooms_.end());
602 },
603 [this](bool enabled) { QueueWorkbenchWorkflowMode(enabled); }, rom_);
604 workbench_panel_ = workbench.get();
606 [this]() { return undo_manager_.CanUndo(); },
607 [this]() { return undo_manager_.CanRedo(); },
608 [this]() { Undo().IgnoreError(); }, [this]() { Redo().IgnoreError(); },
609 [this]() { return undo_manager_.GetUndoDescription(); },
610 [this]() { return undo_manager_.GetRedoDescription(); },
611 [this]() { return static_cast<int>(undo_manager_.UndoStackSize()); });
612 workbench_panel_->SetOnInspectorSideChanged([this](bool on_left) {
615 : "right");
616 dependencies_.user_settings->Save().IgnoreError();
617 }
618 });
620 return game_data_ ? &game_data_->pit_damage_table : nullptr;
621 });
622 window_manager->RegisterWindowContent(std::move(workbench));
623 }
624
625 window_manager->RegisterWindowContent(std::make_unique<DungeonEntrancesPanel>(
627 [this](int entrance_id) { OnEntranceSelected(entrance_id); }));
628
629 // Note: RoomGraphicsContent and PaletteEditorContent are registered
630 // in Load() after their dependencies (renderer_, palette_editor_) are initialized
631}
632
633absl::Status DungeonEditorV2::Load() {
634 if (!rom_ || !rom_->is_loaded()) {
635 return absl::FailedPreconditionError("ROM not loaded");
636 }
637
638 // Initialize ObjectDimensionTable so DimensionService's fallback path works.
639 // DimensionService::Get() queries this table internally but has no Load API.
640 auto& dim_table = zelda3::ObjectDimensionTable::Get();
641 if (!dim_table.IsLoaded()) {
642 RETURN_IF_ERROR(dim_table.LoadFromRom(rom_));
643 }
644
647
648 if (!game_data()) {
649 return absl::FailedPreconditionError("GameData not available");
650 }
651 auto dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
652 current_palette_ = dungeon_main_pal_group[current_palette_group_id_];
655
660 [this](int room_id) { OnRoomSelected(room_id); });
662 [this](int room_id, RoomSelectionIntent intent) {
663 OnRoomSelected(room_id, intent);
664 });
666 [this](int entrance_id) { OnEntranceSelected(entrance_id); });
667
668 // Canvas viewers are lazily created in GetViewerForRoom
669
670 if (game_data()) {
672 } else {
674 }
675
678
679 // Register panels that depend on initialized state (renderer, palette_editor_)
681 auto graphics_panel = std::make_unique<RoomGraphicsContent>(
683 room_graphics_panel_ = graphics_panel.get();
685 std::move(graphics_panel));
686 auto palette_panel =
687 std::make_unique<PaletteEditorContent>(&palette_editor_);
688 palette_editor_panel_ = palette_panel.get();
690 std::move(palette_panel));
691 }
692
693 dungeon_editor_system_ = std::make_unique<zelda3::DungeonEditorSystem>(rom_);
694 (void)dungeon_editor_system_->Initialize();
696
697 // Initialize browse-only object selector and the dedicated object editor.
698 auto object_selector = std::make_unique<ObjectSelectorContent>(
699 rom_, nullptr, dungeon_editor_system_->GetObjectEditor(),
701 auto object_editor = std::make_unique<ObjectEditorContent>(
702 dungeon_editor_system_->GetObjectEditor());
703
704 // Wire up object change callback to trigger room re-rendering
705 dungeon_editor_system_->GetObjectEditor()->SetObjectChangedCallback(
706 [this](size_t /*object_index*/, const zelda3::RoomObject& /*object*/) {
707 if (current_room_id_ >= 0 && current_room_id_ < (int)rooms_.size()) {
708 rooms_[current_room_id_].RenderRoomGraphics();
709 }
710 });
711
712 // Set rooms and initial palette group for correct preview rendering
713 object_selector->SetRooms(&rooms_);
714 object_selector->SetCurrentPaletteGroup(current_palette_group_);
715
716 auto open_workbench_selection_inspector = [this]() {
717 if (workbench_panel_) {
719 }
720 OpenWindow("dungeon.workbench");
721 };
722
723 object_selector->SetOpenObjectEditorCallback(
724 open_workbench_selection_inspector);
725
726 // Keep raw pointers for later access
727 object_selector_panel_ = object_selector.get();
728 object_editor_content_ = object_editor.get();
729
730 auto jump_to_reciprocal_door = [this](int neighbor_room_id,
731 size_t door_index) {
732 if (neighbor_room_id < 0 ||
733 neighbor_room_id >= static_cast<int>(rooms_.size())) {
734 return;
735 }
736 OnRoomSelected(neighbor_room_id, true);
737 if (auto* viewer = GetViewerForRoom(neighbor_room_id)) {
738 viewer->object_interaction()
739 .entity_coordinator()
740 .door_handler()
741 .SelectDoor(door_index);
742 }
743 };
744 object_editor->SetJumpToReciprocalDoorCallback(jump_to_reciprocal_door);
745 owned_object_editor_content_ = std::move(object_editor);
746
747 auto door_editor = std::make_unique<DoorEditorContent>();
748 door_editor->SetRooms(&rooms_);
749 door_editor->SetOpenSelectionInspectorCallback(
750 open_workbench_selection_inspector);
751 door_editor->SetJumpToReciprocalDoorCallback(jump_to_reciprocal_door);
752 door_editor_panel_ = door_editor.get();
753
754 // Propagate game_data to the object editor panel if available
755 if (game_data()) {
757 }
762 }
763
764 // Room-scoped utility tools are embedded in the Workbench drawer by default,
765 // but the same WindowContent instances remain available in the explicit
766 // Window workflow. Workbench mode closes and hides these entries so they do
767 // not compete with the integrated drawer.
768 auto custom_collision_panel =
769 std::make_unique<CustomCollisionPanel>(nullptr, nullptr);
770 custom_collision_panel_ = custom_collision_panel.get();
771
772 auto water_fill_panel = std::make_unique<WaterFillPanel>(nullptr, nullptr);
773 water_fill_panel_ = water_fill_panel.get();
774
775 auto room_tag_panel = std::make_unique<RoomTagEditorPanel>();
776 room_tag_panel->SetProject(dependencies_.project);
777 room_tag_panel->SetRooms(&rooms_);
778 room_tag_panel->SetCurrentRoomId(current_room_id_);
779 room_tag_editor_panel_ = room_tag_panel.get();
780
781 std::unique_ptr<MinecartTrackEditorPanel> minecart_panel;
782 if (core::FeatureFlags::get().kEnableCustomObjects) {
783 minecart_panel = std::make_unique<MinecartTrackEditorPanel>();
784 minecart_track_editor_panel_ = minecart_panel.get();
785
793 [this](int room_id) { OnRoomSelected(room_id); });
794 }
795 } else {
797 }
798
800 // Initialize custom object manager with project-configured path.
805 } else {
806 // Avoid inheriting stale singleton state from previous projects.
808 }
809 }
810
811 // Register the ObjectSelectorContent directly (it inherits from WindowContent)
812 // Panel manager takes ownership
815 std::move(object_selector));
816 dependencies_.window_manager->RegisterWindowContent(std::move(door_editor));
817
818 // Register sprite and item editor panels with canvas viewer = nullptr
819 // They will get the viewer reference in OnRoomSelected when a room is selected
820 auto sprite_panel = std::make_unique<SpriteEditorPanel>(&current_room_id_,
821 &rooms_, nullptr);
822 sprite_panel->SetOpenSelectionInspectorCallback(
823 open_workbench_selection_inspector);
824 sprite_editor_panel_ = sprite_panel.get();
826 std::move(sprite_panel));
827
828 auto item_panel =
829 std::make_unique<ItemEditorPanel>(&current_room_id_, &rooms_, nullptr);
830 item_panel->SetOpenSelectionInspectorCallback(
831 open_workbench_selection_inspector);
832 item_editor_panel_ = item_panel.get();
833 dependencies_.window_manager->RegisterWindowContent(std::move(item_panel));
834
836 std::move(custom_collision_panel));
838 std::move(water_fill_panel));
840 std::move(room_tag_panel));
841 if (minecart_panel) {
843 std::move(minecart_panel));
844 }
845
846 // Object Tile Editor Panel
847 {
848 auto tile_editor_panel =
849 std::make_unique<ObjectTileEditorPanel>(renderer_, rom_);
850 tile_editor_panel->SetCurrentPaletteGroup(current_palette_group_);
851
852 // Wire creation callback: when a new custom object is saved,
853 // register it with the manager, persist to project, and refresh UI.
854 tile_editor_panel->SetObjectCreatedCallback(
855 [this](int object_id, const std::string& filename) {
857 filename);
859 dependencies_.project->custom_object_files[object_id].push_back(
860 filename);
861 (void)dependencies_.project->Save();
862 }
866 }
867 });
868
869 object_tile_editor_panel_ = tile_editor_panel.get();
871 std::move(tile_editor_panel));
872 }
873
874 // Wire tile editor panel and project references to the object selector
881 }
882 }
883
884 // Overlay Manager Panel
885 {
886 auto overlay_panel = std::make_unique<OverlayManagerPanel>();
887 overlay_manager_panel_ = overlay_panel.get();
889 std::move(overlay_panel));
890 }
891
892 } else {
893 owned_object_selector_panel_ = std::move(object_selector);
894 owned_door_editor_panel_ = std::move(door_editor);
895 owned_custom_collision_panel_ = std::move(custom_collision_panel);
896 owned_water_fill_panel_ = std::move(water_fill_panel);
897 owned_room_tag_editor_panel_ = std::move(room_tag_panel);
898 owned_minecart_track_editor_panel_ = std::move(minecart_panel);
899 }
900
902 [this](gui::DungeonPaletteChange change) {
904 });
906
907 // Oracle of Secrets: load editor-authored water fill zones (best-effort).
909
910 if (workbench_panel_) {
917 }
918
919 is_loaded_ = true;
920 return absl::OkStatus();
921}
922
924 if (palette_listener_id_ >= 0) {
925 return;
926 }
927
929 [this](const std::string& group_name, int palette_index) {
930 HandleArenaPaletteChanged(group_name, palette_index);
931 });
932}
933
934void DungeonEditorV2::HandleArenaPaletteChanged(const std::string& group_name,
935 int palette_index) {
936 if (!gfx::PaletteManager::Get().IsManaging(game_data_)) {
937 return;
938 }
939
940 if (group_name == "hud") {
943 } else if (group_name == "dungeon_main") {
946 }
947}
948
952
953 std::array<bool, zelda3::kNumberOfRooms> rendered_rooms{};
954 auto render_once = [this, &rendered_rooms](int room_id) {
955 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()) ||
956 room_id >= static_cast<int>(rendered_rooms.size()) ||
957 rendered_rooms[room_id]) {
958 return;
959 }
960 rooms_[room_id].RenderRoomGraphics();
961 rendered_rooms[room_id] = true;
962 };
963
964 for (int i = 0; i < active_rooms_.Size; i++) {
965 render_once(active_rooms_[i]);
966 }
967
968 render_once(current_room_id_);
969
970 auto apply_palette = [](DungeonCanvasViewer* viewer, uint64_t palette_id,
971 const gfx::PaletteGroup& palette_group,
972 bool force_shared_palette_refresh) {
973 if (!viewer) {
974 return;
975 }
976 viewer->SetCurrentPaletteId(palette_id);
978 palette_group,
979 /*force_refresh=*/force_shared_palette_refresh &&
981 };
982
983 if (current_room_id_ >= 0 &&
984 current_room_id_ < static_cast<int>(rooms_.size()) && game_data()) {
985 const bool force_shared_palette_refresh =
987 auto& dungeon_main_pal_group = game_data()->palette_groups.dungeon_main;
989 static_cast<uint64_t>(dungeon_main_pal_group.size())) {
991 current_palette_ = dungeon_main_pal_group[current_palette_id_];
992 if (auto pal_group =
994 pal_group.ok()) {
995 current_palette_group_ = pal_group.value();
996 apply_palette(workbench_viewer_.get(), current_palette_id_,
997 current_palette_group_, force_shared_palette_refresh);
999 current_palette_group_, force_shared_palette_refresh);
1000 room_viewers_.ForEach(
1001 [this, change, &apply_palette, &dungeon_main_pal_group,
1002 &render_once, force_shared_palette_refresh](
1003 int room_id, std::unique_ptr<DungeonCanvasViewer>& viewer) {
1004 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1005 return;
1006 }
1007 const int palette_id = rooms_[room_id].ResolveDungeonPaletteId();
1008 if (palette_id < 0 ||
1009 palette_id >=
1010 static_cast<int>(dungeon_main_pal_group.size())) {
1011 return;
1012 }
1013 if (change.source ==
1015 change.palette_id >= 0 && palette_id != change.palette_id) {
1016 return;
1017 }
1018 auto room_palette_group = gfx::CreatePaletteGroupFromLargePalette(
1019 dungeon_main_pal_group.palette_ref(palette_id));
1020 if (room_palette_group.ok()) {
1021 if (viewer && viewer->object_interaction().IsObjectLoaded()) {
1022 render_once(room_id);
1023 }
1024 apply_palette(viewer.get(), palette_id,
1025 room_palette_group.value(),
1026 force_shared_palette_refresh);
1027 }
1028 });
1032 }
1035 }
1039 }
1040 }
1041 }
1042 }
1043}
1044
1048 change.palette_id < 0) {
1050 [](int, zelda3::Room& room) { room.MarkGraphicsDirty(); });
1051 return;
1052 }
1053
1054 // Editing palette bytes does not change a room header, so the room's normal
1055 // property cache cannot detect this dependency change. Mark every cached
1056 // room that resolves to the edited concrete palette; inactive rooms will
1057 // redraw lazily the next time they are shown.
1058 rooms_.ForEachMaterialized([change](int, zelda3::Room& room) {
1059 if (room.ResolveDungeonPaletteId() == change.palette_id) {
1060 room.MarkGraphicsDirty();
1061 }
1062 });
1063}
1064
1067
1068 // Mirror the workbench inspector to the LEFT when user_settings says so.
1069 // Persisted across sessions via UserSettings::dungeon_inspector_side.
1073 }
1074
1075 const auto& theme = AgentUI::GetTheme();
1076 if (room_window_class_.ClassId == 0) {
1077 room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
1078 }
1079
1080 if (!is_loaded_) {
1081 gui::PanelWindow loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);
1082 loading_card.SetDefaultSize(400, 200);
1083 if (loading_card.Begin()) {
1084 ImGui::TextColored(theme.text_secondary_gray,
1085 tr("Loading dungeon data..."));
1086 ImGui::TextWrapped(
1087 tr("Independent editor cards will appear once ROM data is loaded."));
1088 }
1089 loading_card.End();
1090 return absl::OkStatus();
1091 }
1092
1093 if (!IsWorkbenchWorkflowEnabled() || active_rooms_.Size > 0) {
1095 }
1096
1097 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
1098 // Delegate delete to current room viewer
1099 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
1100 viewer->DeleteSelectedObjects();
1101 }
1102 }
1103
1104 // Keyboard Shortcuts (only if not typing in a text field)
1105 if (!ImGui::GetIO().WantTextInput) {
1106 if (ImGui::GetIO().KeyCtrl && ImGui::GetIO().KeyShift &&
1107 ImGui::IsKeyPressed(ImGuiKey_W, false)) {
1109 }
1110
1111 // Room Cycling (Ctrl+Tab)
1112 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && ImGui::GetIO().KeyCtrl) {
1114 if (recent_rooms_.size() > 1) {
1115 int current_idx = -1;
1116 for (int i = 0; i < static_cast<int>(recent_rooms_.size()); ++i) {
1117 if (recent_rooms_[i] == current_room_id_) {
1118 current_idx = i;
1119 break;
1120 }
1121 }
1122 if (current_idx != -1) {
1123 int next_idx;
1124 if (ImGui::GetIO().KeyShift) {
1125 next_idx =
1126 (current_idx + 1) % static_cast<int>(recent_rooms_.size());
1127 } else {
1128 next_idx =
1129 (current_idx - 1 + static_cast<int>(recent_rooms_.size())) %
1130 static_cast<int>(recent_rooms_.size());
1131 }
1132 OnRoomSelected(recent_rooms_[next_idx]);
1133 }
1134 }
1135 } else if (active_rooms_.size() > 1) {
1136 int current_idx = -1;
1137 for (int i = 0; i < active_rooms_.size(); ++i) {
1138 if (active_rooms_[i] == current_room_id_) {
1139 current_idx = i;
1140 break;
1141 }
1142 }
1143
1144 if (current_idx != -1) {
1145 int next_idx;
1146 if (ImGui::GetIO().KeyShift) {
1147 next_idx =
1148 (current_idx - 1 + active_rooms_.size()) % active_rooms_.size();
1149 } else {
1150 next_idx = (current_idx + 1) % active_rooms_.size();
1151 }
1152 OnRoomSelected(active_rooms_[next_idx]);
1153 }
1154 }
1155 }
1156
1157 // Adjacent Room Navigation (Ctrl+Arrows)
1158 if (ImGui::GetIO().KeyCtrl) {
1159 int next_room = -1;
1160 const int kCols = 16;
1161
1162 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
1163 if (current_room_id_ >= kCols)
1164 next_room = current_room_id_ - kCols;
1165 } else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
1166 if (IsValidRoomId(current_room_id_ + kCols))
1167 next_room = current_room_id_ + kCols;
1168 } else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
1169 if (current_room_id_ % kCols > 0)
1170 next_room = current_room_id_ - 1;
1171 } else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
1172 if (current_room_id_ % kCols < kCols - 1 &&
1174 next_room = current_room_id_ + 1;
1175 }
1176
1177 if (next_room != -1) {
1179 OnRoomSelected(next_room, /*request_focus=*/false);
1180 } else {
1182 }
1183 }
1184 }
1185 }
1186
1187 // Process any pending room swaps after all drawing is complete
1188 // This prevents ImGui state corruption from modifying collections mid-frame
1190
1191 return absl::OkStatus();
1192}
1193
1195 if (!status_bar)
1196 return;
1197 const int room_id = current_room_id();
1198
1199 if (room_id >= 0) {
1200 StatusBarSegmentOptions room_opts;
1201 room_opts.tooltip = "Click to refocus viewer on this room";
1202 room_opts.on_click = [this, room_id]() {
1206 }
1207 };
1208 status_bar->SetCustomSegment("Room", absl::StrFormat("0x%03X", room_id),
1209 std::move(room_opts));
1210 }
1211
1212 const int loaded = LoadedRoomCount();
1213 const int total = TotalRoomCount();
1214 if (total > 0) {
1215 StatusBarSegmentOptions rooms_opts;
1216 rooms_opts.tooltip =
1217 absl::StrFormat("%d rooms loaded of %d total", loaded, total);
1218 status_bar->SetCustomSegment("Rooms",
1219 absl::StrFormat("%d/%d", loaded, total),
1220 std::move(rooms_opts));
1221 }
1222
1223 if (room_id >= 0 && CurrentRoomHasPendingChanges()) {
1224 StatusBarSegmentOptions current_room_opts;
1225 current_room_opts.tooltip =
1226 "This room has pending editor changes that are not yet applied to the "
1227 "ROM buffer.";
1228 status_bar->SetCustomSegment("RoomState", "Pending",
1229 std::move(current_room_opts));
1230 }
1231
1232 StatusBarSegmentOptions mode_opts;
1233 mode_opts.tooltip = "Click to toggle Workbench / Standalone workflow";
1234 mode_opts.on_click = [this]() {
1236 };
1240 std::move(mode_opts));
1241}
1242
1244 return rooms_.LoadedCount();
1245}
1246
1248 int count = 0;
1249 rooms_.ForEachMaterialized([&count](int, const zelda3::Room& room) {
1250 if (room.HasUnsavedChanges()) {
1251 ++count;
1252 }
1253 });
1254 return count;
1255}
1256
1258 return PendingRoomCount() > 0;
1259}
1260
1262 if (HasPendingRoomChanges()) {
1263 return true;
1264 }
1265 if (std::any_of(entrances_.begin(), entrances_.end(),
1266 [](const zelda3::RoomEntrance& entrance) {
1267 return entrance.dirty();
1268 })) {
1269 return true;
1270 }
1271 if (std::any_of(spawn_points_.begin(), spawn_points_.end(),
1272 [](const zelda3::DungeonSpawnPoint& spawn) {
1273 return spawn.dirty();
1274 })) {
1275 return true;
1276 }
1277 return game_data_ != nullptr && game_data_->pit_damage_table.dirty();
1278}
1279
1281 if (current_room_id_ < 0 ||
1282 current_room_id_ >= static_cast<int>(rooms_.size())) {
1283 return false;
1284 }
1285 if (const auto* room = rooms_.GetIfMaterialized(current_room_id_)) {
1286 return room->HasUnsavedChanges();
1287 }
1288 return false;
1289}
1290
1292 if (!core::FeatureFlags::get().dungeon.kUseWorkbench) {
1293 return false;
1294 }
1296 return true;
1297 }
1298 return dependencies_.window_manager->IsWindowOpen("dungeon.workbench");
1299}
1300
1301void DungeonEditorV2::SetWorkbenchWorkflowMode(bool enabled, bool show_toast) {
1302 auto* window_manager = dependencies_.window_manager;
1303 if (!window_manager) {
1304 return;
1305 }
1306
1307 const size_t session_id = window_manager->GetActiveSessionId();
1308 const bool was_enabled = IsWorkbenchWorkflowEnabled();
1309
1310 if (enabled) {
1311 window_manager->OpenWindow(session_id, "dungeon.workbench");
1312
1313 // Workbench-local tool windows (Object/Door/Sprite/Item Selector, Palette,
1314 // Room Graphics/Tags, Custom Collision, Water Fill, Minecart) are NOT
1315 // auto-closed on Workbench entry. The Workbench drawer embeds the same
1316 // tools, but users may also keep a standalone window open if they prefer
1317 // a multi-window layout. Only navigation windows (room selector/matrix
1318 // and per-room windows) are collapsed when entering Workbench mode.
1319 for (const auto& descriptor :
1320 window_manager->GetWindowsInCategory(session_id, "Dungeon")) {
1321 const std::string& card_id = descriptor.card_id;
1322 if (card_id == "dungeon.workbench") {
1323 continue;
1324 }
1325 if (window_manager->IsWindowPinned(session_id, card_id)) {
1326 continue;
1327 }
1328 const bool is_room_window = IsTransientDungeonRoomWindowId(card_id);
1329 if (card_id == kRoomSelectorId || card_id == kRoomMatrixId ||
1330 is_room_window) {
1331 window_manager->CloseWindow(session_id, card_id);
1332 }
1333 }
1334 } else {
1335 window_manager->CloseWindow(session_id, "dungeon.workbench");
1336 window_manager->OpenWindow(session_id, kRoomSelectorId);
1337 window_manager->OpenWindow(session_id, kRoomMatrixId);
1338 if (current_room_id_ >= 0) {
1340 }
1341 }
1342
1343 if (show_toast && dependencies_.toast_manager && was_enabled != enabled) {
1345 enabled ? "Dungeon workflow: Workbench"
1346 : "Dungeon workflow: Standalone Panels",
1348 }
1349}
1350
1352 bool show_toast) {
1356}
1357
1361
1363 for (int i = 0; i < active_rooms_.Size; i++) {
1364 int room_id = active_rooms_[i];
1365 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1366 bool panel_visible = true;
1368 panel_visible = dependencies_.window_manager->IsWindowOpen(card_id);
1369 }
1370
1371 if (!panel_visible) {
1373 room_cards_.erase(room_id);
1374 active_rooms_.erase(active_rooms_.Data + i);
1375 ReleaseRoomPanelSlotId(room_id);
1376 // Clean up viewer
1377 room_viewers_.Erase(room_id);
1378 i--;
1379 continue;
1380 }
1381
1382 bool is_pinned = dependencies_.window_manager &&
1384 std::string active_category =
1387 : "";
1388
1389 if (active_category != "Dungeon" && !is_pinned) {
1390 continue;
1391 }
1392
1393 // Ensure room card exists (should have been created by ShowRoomPanel/ShowPanel)
1394 if (room_cards_.find(room_id) == room_cards_.end()) {
1395 ShowRoomPanel(room_id);
1396 }
1397
1398 auto& room_card = room_cards_[room_id];
1399 bool open = true;
1400
1401 ImGui::SetNextWindowClass(&room_window_class_);
1402 if (room_dock_id_ == 0) {
1403 room_dock_id_ = ImGui::GetID("DungeonRoomDock");
1404 }
1405 ImGui::SetNextWindowDockID(room_dock_id_, ImGuiCond_FirstUseEver);
1406
1407 if (room_card->Begin(&open)) {
1408 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) {
1409 OnRoomSelected(room_id, /*request_focus=*/false);
1410 }
1411 DrawRoomTab(room_id);
1412 }
1413 room_card->End();
1414
1415 if (!open) {
1418 }
1419
1420 room_cards_.erase(room_id);
1421 active_rooms_.erase(active_rooms_.Data + i);
1422 room_viewers_.Erase(room_id);
1423 ReleaseRoomPanelSlotId(room_id);
1424 i--;
1425 }
1426 }
1427}
1428
1430 if (auto it = room_panel_slot_ids_.find(room_id);
1431 it != room_panel_slot_ids_.end()) {
1432 return it->second;
1433 }
1434 const int slot_id = next_room_panel_slot_id_++;
1435 room_panel_slot_ids_[room_id] = slot_id;
1436 return slot_id;
1437}
1438
1440 room_panel_slot_ids_.erase(room_id);
1441}
1442
1444 const auto& theme = AgentUI::GetTheme();
1445 if (room_id < 0 || room_id >= 0x128) {
1446 ImGui::Text(tr("Invalid room ID: %d"), room_id);
1447 return;
1448 }
1449
1450 auto& room = rooms_[room_id];
1451
1452 if (!room.IsLoaded()) {
1453 auto status = room_loader_.LoadRoom(room_id, room);
1454 if (!status.ok()) {
1455 ImGui::TextColored(theme.text_error_red, tr("Failed to load room: %s"),
1456 status.message().data());
1457 return;
1458 }
1459
1461 auto sys_status = dungeon_editor_system_->ReloadRoom(room_id);
1462 if (!sys_status.ok()) {
1463 LOG_ERROR("DungeonEditorV2", "Failed to load system data: %s",
1464 sys_status.message().data());
1465 }
1466 }
1467 }
1468
1469 if (room.IsLoaded()) {
1470 bool needs_render = false;
1471
1472 if (room.blocks().empty()) {
1474 room.LoadRoomGraphics();
1475 needs_render = true;
1476 }
1477
1478 if (!room.AreObjectsLoaded()) {
1479 room.LoadObjects();
1480 needs_render = true;
1481 }
1482
1483 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1484 if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
1485 room.RenderRoomGraphics();
1486 }
1487 }
1488
1489 if (room.IsLoaded()) {
1490 ImGui::TextColored(theme.text_success_green, ICON_MD_CHECK " Loaded");
1491 } else {
1492 ImGui::TextColored(theme.text_error_red, ICON_MD_PENDING " Not Loaded");
1493 }
1494 ImGui::SameLine();
1495 ImGui::TextDisabled(tr("Objects: %zu"), room.GetTileObjects().size());
1496
1497 if (core::FeatureFlags::get().dungeon.kUseWorkbench &&
1499 ImGui::SameLine();
1500 const std::string return_label = absl::StrFormat(
1501 ICON_MD_WORKSPACES " Workbench##return_workbench_%03X", room_id);
1502 if (ImGui::SmallButton(return_label.c_str())) {
1504 }
1505 if (ImGui::IsItemHovered()) {
1506 ImGui::SetTooltip(
1507 tr("Switch back to the integrated Dungeon Workbench workflow "
1508 "(Ctrl+Shift+W)."));
1509 }
1510 }
1511
1512 // Warp to Room button — sends room ID to running Mesen2 emulator
1513 ImGui::SameLine();
1514 {
1516 bool connected = client && client->IsConnected();
1517 if (!connected) {
1518 ImGui::BeginDisabled();
1519 }
1520 std::string warp_label =
1521 absl::StrFormat(ICON_MD_ROCKET_LAUNCH " Warp##warp_%03X", room_id);
1522 if (ImGui::SmallButton(warp_label.c_str())) {
1523 auto status = client->WriteWord(0x7E00A0, static_cast<uint16_t>(room_id));
1524 if (status.ok()) {
1527 absl::StrFormat("Warped to room 0x%03X", room_id),
1529 }
1530 } else {
1533 absl::StrFormat("Warp failed: %s", status.message()),
1535 }
1536 }
1537 }
1538 if (!connected) {
1539 ImGui::EndDisabled();
1540 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1541 ImGui::SetTooltip(tr("Connect to Mesen2 first"));
1542 }
1543 } else {
1544 if (ImGui::IsItemHovered()) {
1545 ImGui::SetTooltip(tr("Warp to room 0x%03X in Mesen2"), room_id);
1546 }
1547 }
1548 }
1549
1550 ImGui::Separator();
1551
1552 // Use per-room viewer; reserve a bottom strip for the shared dungeon
1553 // status bar so this Windows-mode standalone window mirrors the Workbench
1554 // canvas pane's affordances.
1555 if (auto* viewer = GetViewerForRoom(room_id)) {
1556 const float status_bar_reserved =
1557 std::max(ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
1559 ImGui::GetStyle().ItemSpacing.y;
1560 if (ImGui::BeginChild("##StandaloneRoomBody",
1561 ImVec2(0.0f, -status_bar_reserved), false)) {
1562 viewer->DrawDungeonCanvas(room_id);
1563 }
1564 ImGui::EndChild();
1565
1566 bool room_dirty = false;
1567 if (room_id >= 0 && room_id < static_cast<int>(rooms_.size())) {
1568 room_dirty = rooms_[room_id].HasUnsavedChanges();
1569 }
1570 const char* tool_mode =
1571 viewer->object_interaction().mode_manager().GetModeName();
1572 auto status = DungeonStatusBar::BuildState(*viewer, tool_mode, room_dirty);
1573 status.workflow_mode = workflow_mode_names::kStandalone;
1574 status.workflow_primary = false;
1575 status.can_undo = undo_manager_.CanUndo();
1576 status.can_redo = undo_manager_.CanRedo();
1577 status.undo_depth = static_cast<int>(undo_manager_.UndoStackSize());
1578 status.on_undo = [this]() {
1579 Undo().IgnoreError();
1580 };
1581 status.on_redo = [this]() {
1582 Redo().IgnoreError();
1583 };
1584 DungeonStatusBar::Draw(status);
1585 }
1586}
1587
1589 switch (intent) {
1591 OnRoomSelected(room_id, /*request_focus=*/true);
1592 break;
1594 // Explicitly pivot to panel workflow when user asks for standalone room.
1595 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1596 return;
1597 }
1600 }
1601 // Update shared state (same as OnRoomSelected with request_focus=true)
1602 OnRoomSelected(room_id, /*request_focus=*/false);
1603 // Now force-open a standalone panel for this room
1604 ShowRoomPanel(room_id);
1605 break;
1606 }
1608 OnRoomSelected(room_id, /*request_focus=*/false);
1609 break;
1610 }
1611}
1612
1613void DungeonEditorV2::OnRoomSelected(int room_id, bool request_focus) {
1614 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1615 LOG_WARN("DungeonEditorV2", "Ignoring invalid room selection: %d", room_id);
1616 return;
1617 }
1618 if (room_id != current_room_id_ && workbench_panel_) {
1620 }
1621 current_room_id_ = room_id;
1622 room_selector_.set_current_room_id(static_cast<uint16_t>(room_id));
1623
1624 // Track recent rooms (remove if already present, add to front)
1625 recent_rooms_.erase(
1626 std::remove(recent_rooms_.begin(), recent_rooms_.end(), room_id),
1627 recent_rooms_.end());
1628 recent_rooms_.push_front(room_id);
1629 if (recent_rooms_.size() > kMaxRecentRooms) {
1630 recent_rooms_.pop_back();
1631 }
1632
1634 dungeon_editor_system_->SetExternalRoom(&rooms_[room_id]);
1635 }
1636
1637 // Update all sub-panels (Object Editor, Sprite Editor, etc.)
1638 SyncPanelsToRoom(room_id);
1639
1640 // Sync palette with current room (must happen before early return for focus changes)
1641
1642 // Sync palette with current room (must happen before early return for focus changes)
1643 if (room_id >= 0 && room_id < (int)rooms_.size()) {
1644 auto& room = rooms_[room_id];
1645 if (!room.IsLoaded()) {
1646 room_loader_.LoadRoom(room_id, room);
1647 }
1648
1649 if (room.IsLoaded()) {
1651 current_palette_id_ = room.ResolveDungeonPaletteId();
1654
1655 // Update viewer and object editor palette
1656 if (auto* viewer = GetViewerForRoom(room_id)) {
1657 ConfigureViewerRenderContext(viewer, room_id);
1658 viewer->SetCurrentPaletteId(current_palette_id_);
1659
1660 if (game_data()) {
1661 auto dungeon_main_pal_group =
1663 if (current_palette_id_ < (int)dungeon_main_pal_group.size()) {
1664 current_palette_ = dungeon_main_pal_group[current_palette_id_];
1665 auto result =
1667 if (result.ok()) {
1668 current_palette_group_ = result.value();
1669 viewer->SetCurrentPaletteGroup(current_palette_group_);
1673 }
1674 // Sync palette to graphics panel for proper sheet coloring
1678 }
1679 }
1680 }
1681 }
1682 }
1683 }
1684 }
1685
1686 // Workbench mode uses a single stable window and does not spawn per-room
1687 // panels. Keep selection + panels in sync and return.
1689 if (dependencies_.window_manager && request_focus) {
1690 // Only force-show if it's already visible or if it's the first initialization
1691 // This avoids obtrusive behavior when the user explicitly closed it.
1692 if (dependencies_.window_manager->IsWindowOpen("dungeon.workbench")) {
1693 dependencies_.window_manager->OpenWindow("dungeon.workbench");
1694 }
1695 }
1696 return;
1697 }
1698
1699 // Check if room is already open
1700 for (int i = 0; i < active_rooms_.Size; i++) {
1701 if (active_rooms_[i] == room_id) {
1702 // Always ensure panel is visible, even if already in active_rooms_
1704 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
1706 }
1707 if (request_focus) {
1708 FocusRoom(room_id);
1709 }
1710 return;
1711 }
1712 }
1713
1714 active_rooms_.push_back(room_id);
1716
1718 // Use unified ResourceLabelProvider for room names
1719 std::string room_name = absl::StrFormat(
1720 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
1721
1722 std::string base_card_id = absl::StrFormat("dungeon.room_%d", room_id);
1723
1725 {.card_id = base_card_id,
1726 .display_name = room_name,
1727 .window_title = ICON_MD_GRID_ON " " + room_name,
1728 .icon = ICON_MD_GRID_ON,
1729 .category = "Dungeon",
1730 .shortcut_hint = "",
1731 .visibility_flag = nullptr,
1732 .priority = 200 + room_id});
1733
1735 }
1736}
1737
1739 if (entrance_id < 0 || entrance_id >= static_cast<int>(entrances_.size())) {
1740 return;
1741 }
1742 current_entrance_id_ = entrance_id;
1744 int room_id = entrances_[entrance_id].room_;
1745 OnRoomSelected(room_id);
1746}
1747
1749 int room_id) const {
1750 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1751 return 0xFF;
1752 }
1753 if (current_entrance_id_ < 0 ||
1754 current_entrance_id_ >= static_cast<int>(entrances_.size())) {
1755 return 0xFF;
1756 }
1757 const auto& entrance = entrances_[current_entrance_id_];
1758 if (entrance.room_ != room_id) {
1759 return 0xFF;
1760 }
1761 return entrance.blockset_;
1762}
1763
1765 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1766 return;
1767 }
1768 auto& room = rooms_[room_id];
1769 if (!room.IsLoaded()) {
1770 return;
1771 }
1772 room.SetRenderEntranceBlockset(
1774}
1775
1777 int room_id) {
1778 if (viewer == nullptr) {
1779 return;
1780 }
1781 const uint8_t entrance_blockset =
1783 if (entrance_blockset == 0xFF) {
1785 } else {
1786 viewer->SetEntranceRenderContext(current_entrance_id_, entrance_blockset);
1787 }
1788}
1789
1790void DungeonEditorV2::add_room(int room_id) {
1791 OnRoomSelected(room_id);
1792}
1793
1795 auto it = room_cards_.find(room_id);
1796 if (it != room_cards_.end()) {
1797 it->second->Focus();
1798 }
1799}
1800
1806 OpenWindow("dungeon.workbench");
1807 } else {
1809 }
1810 }
1811}
1812
1814 if (enabled && dependencies_.window_manager) {
1816 OpenWindow("dungeon.workbench");
1817 if (workbench_panel_) {
1819 }
1820 } else {
1824 }
1827 }
1828 }
1829}
1830
1834
1837 LOG_ERROR("DungeonEditorV2", "Cannot place object: Invalid room ID %d",
1839 const std::string message = absl::StrFormat(
1840 "Cannot place 0x%02X: invalid room %d", obj.id_, current_room_id_);
1843 } else if (dependencies_.toast_manager) {
1845 }
1846 return;
1847 }
1848
1849 auto& room = rooms_[current_room_id_];
1850
1851 LOG_INFO("DungeonEditorV2",
1852 "Placing object ID=0x%02X at position (%d,%d) in room %03X", obj.id_,
1853 obj.x_, obj.y_, current_room_id_);
1854
1855 room.RenderRoomGraphics();
1856 LOG_DEBUG("DungeonEditorV2",
1857 "Object placed and room re-rendered successfully");
1858 // Brief success feedback so the user knows the placement was accepted.
1859 // Kept minimal (no inline panel update) — success is non-spammy by design.
1862 absl::StrFormat("Placed 0x%02X in room %03X", obj.id_,
1865 }
1866}
1867
1869 int room_id, const zelda3::RoomObject& object) {
1870 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1871 LOG_WARN("DungeonEditorV2", "Edit Graphics ignored (invalid room id %d)",
1872 room_id);
1873 return;
1874 }
1875
1876 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
1877 if (!editor_manager) {
1878 LOG_WARN("DungeonEditorV2",
1879 "Edit Graphics ignored (editor manager unavailable)");
1880 return;
1881 }
1882
1883 auto& room = rooms_[room_id];
1885 room.LoadRoomGraphics();
1886
1887 uint16_t sheet_id = 0;
1888 uint16_t tile_index = 0;
1889 bool resolved_sheet = false;
1890 if (auto tiles_or = object.GetTiles();
1891 tiles_or.ok() && !tiles_or.value().empty()) {
1892 const uint16_t tile_id = tiles_or.value().front().id_;
1893 const size_t block_index = static_cast<size_t>(tile_id / 64);
1894 const auto blocks = room.blocks();
1895 if (block_index < blocks.size()) {
1896 sheet_id = blocks[block_index];
1897 resolved_sheet = true;
1898 }
1899 const int tiles_per_row = gfx::kTilesheetWidth / 8;
1900 const int tiles_per_col = gfx::kTilesheetHeight / 8;
1901 const int tiles_per_sheet = tiles_per_row * tiles_per_col;
1902 if (tiles_per_sheet > 0) {
1903 tile_index = static_cast<uint16_t>(tile_id % tiles_per_sheet);
1904 }
1905 }
1906
1907 editor_manager->SwitchToEditor(EditorType::kGraphics, true);
1908 if (auto* editor_set = editor_manager->GetCurrentEditorSet()) {
1909 if (auto* graphics = editor_set->GetGraphicsEditor()) {
1910 if (resolved_sheet) {
1911 graphics->SelectSheet(sheet_id);
1912 graphics->HighlightTile(sheet_id, tile_index,
1913 absl::StrFormat("Object 0x%02X", object.id_));
1914 }
1915 }
1916 }
1917
1920 }
1921}
1922
1923void DungeonEditorV2::SwapRoomInPanel(int old_room_id, int new_room_id) {
1924 // Defer the swap until after the current frame's draw phase completes
1925 // This prevents modifying data structures while ImGui is still using them
1926 if (new_room_id < 0 || new_room_id >= static_cast<int>(rooms_.size())) {
1927 return;
1928 }
1929 pending_swap_.old_room_id = old_room_id;
1930 pending_swap_.new_room_id = new_room_id;
1931 pending_swap_.pending = true;
1932}
1933
1935 if (!pending_swap_.pending) {
1936 return;
1937 }
1938
1939 int old_room_id = pending_swap_.old_room_id;
1940 int new_room_id = pending_swap_.new_room_id;
1941 pending_swap_.pending = false;
1942
1943 // Find the position of old_room in active_rooms_
1944 int swap_index = -1;
1945 for (int i = 0; i < active_rooms_.Size; i++) {
1946 if (active_rooms_[i] == old_room_id) {
1947 swap_index = i;
1948 break;
1949 }
1950 }
1951
1952 if (swap_index < 0) {
1953 // Old room not found in active rooms, just select the new one
1954 OnRoomSelected(new_room_id);
1955 return;
1956 }
1957
1958 // Avoid swapping into an already-open room panel (the per-room maps assume
1959 // room IDs are unique). In this case, just focus/select the existing room.
1960 for (int i = 0; i < active_rooms_.Size; i++) {
1961 if (i != swap_index && active_rooms_[i] == new_room_id) {
1962 OnRoomSelected(new_room_id);
1963 return;
1964 }
1965 }
1966
1967 // Preserve the old panel's stable ImGui window identity by transferring its
1968 // slot id to the new room id.
1969 int slot_id = -1;
1970 if (auto it = room_panel_slot_ids_.find(old_room_id);
1971 it != room_panel_slot_ids_.end()) {
1972 slot_id = it->second;
1973 room_panel_slot_ids_.erase(it);
1974 } else {
1975 slot_id = next_room_panel_slot_id_++;
1976 }
1977 room_panel_slot_ids_[new_room_id] = slot_id;
1978
1979 // Preserve the viewer instance so canvas pan/zoom and UI state don't reset
1980 // when navigating with arrows (swap-in-panel).
1981 room_viewers_.Rename(old_room_id, new_room_id);
1982
1983 // Replace old room with new room in active_rooms_
1984 active_rooms_[swap_index] = new_room_id;
1986
1987 // Unregister old panel
1989 std::string old_card_id = absl::StrFormat("dungeon.room_%d", old_room_id);
1990 const bool old_pinned =
1993
1994 // Register new panel
1995 // Use unified ResourceLabelProvider for room names
1996 std::string new_room_name = absl::StrFormat(
1997 "[%03X] %s", new_room_id, zelda3::GetRoomLabel(new_room_id).c_str());
1998
1999 std::string new_card_id = absl::StrFormat("dungeon.room_%d", new_room_id);
2000
2002 {.card_id = new_card_id,
2003 .display_name = new_room_name,
2004 .window_title = ICON_MD_GRID_ON " " + new_room_name,
2005 .icon = ICON_MD_GRID_ON,
2006 .category = "Dungeon",
2007 .shortcut_hint = "",
2008 .visibility_flag = nullptr,
2009 .priority = 200 + new_room_id});
2010
2011 if (old_pinned) {
2012 dependencies_.window_manager->SetWindowPinned(new_card_id, true);
2013 }
2015 }
2016
2017 // Clean up old room's card and viewer
2018 room_cards_.erase(old_room_id);
2019
2020 // Update current selection
2021 OnRoomSelected(new_room_id, /*request_focus=*/false);
2022}
2023
2026 return;
2027 }
2028
2029 const bool enabled = pending_workflow_mode_.enabled;
2030 const bool show_toast = pending_workflow_mode_.show_toast;
2032 SetWorkbenchWorkflowMode(enabled, show_toast);
2033}
2034
2036 room_viewers_.Touch(room_id);
2037}
2038
2040 // No-op: LruCache handles removal internally via Erase()
2041 (void)room_id;
2042}
2043
2045 if (!viewer) {
2046 return;
2047 }
2048
2049 viewer->SetShowObjectPanelCallback([this]() {
2052 OpenWindow("dungeon.workbench");
2053 return;
2054 }
2056 });
2057 viewer->SetShowSpritePanelCallback([this]() {
2060 OpenWindow("dungeon.workbench");
2061 return;
2062 }
2063 OpenWindow("dungeon.sprite_editor");
2064 });
2065 viewer->SetShowItemPanelCallback([this]() {
2068 OpenWindow("dungeon.workbench");
2069 return;
2070 }
2071 OpenWindow("dungeon.item_editor");
2072 });
2073 viewer->SetShowRoomListCallback([this]() {
2074 OpenWindow(IsWorkbenchWorkflowEnabled() ? "dungeon.workbench"
2076 });
2077 viewer->SetShowRoomMatrixCallback([this]() {
2080 OpenWindow("dungeon.workbench");
2081 return;
2082 }
2084 });
2085 viewer->SetShowEntranceListCallback([this]() {
2088 OpenWindow("dungeon.workbench");
2089 return;
2090 }
2092 });
2093 viewer->SetShowRoomGraphicsCallback([this]() {
2096 OpenWindow("dungeon.workbench");
2097 return;
2098 }
2100 });
2101 viewer->SetShowDoorEditorCallback([this]() {
2104 OpenWindow("dungeon.workbench");
2105 return;
2106 }
2107 OpenWindow("dungeon.door_editor");
2108 });
2109 viewer->SetShowDungeonSettingsCallback([this]() {
2110 if (workbench_panel_) {
2112 }
2113 OpenWindow("dungeon.workbench");
2114 });
2116 [this](int target_room_id, const zelda3::RoomObject& object) {
2117 OpenGraphicsEditorForObject(target_room_id, object);
2118 });
2119 viewer->SetSaveRoomCallback([this](int target_room_id) {
2120 auto status = SaveRoom(target_room_id);
2121 if (!status.ok()) {
2122 LOG_ERROR("DungeonEditorV2", "Apply Room failed: %s",
2123 status.message().data());
2126 absl::StrFormat("Apply Room failed: %s", status.message()),
2128 }
2129 return;
2130 }
2132 dependencies_.toast_manager->Show("Applied room to ROM buffer",
2134 }
2135 });
2136
2139}
2140
2143 (void)room_id;
2144 return GetWorkbenchViewer();
2145 }
2146
2147 // Set eviction predicate to protect active rooms from eviction
2148 room_viewers_.SetEvictionPredicate([this](const int& candidate) {
2149 for (int i = 0; i < active_rooms_.size(); ++i) {
2150 if (active_rooms_[i] == candidate)
2151 return false;
2152 }
2153 return true;
2154 });
2155
2156 if (auto* existing = room_viewers_.Get(room_id)) {
2157 // Viewer already exists - Get() already touched LRU
2158 auto* viewer_ptr = existing->get();
2159 ConfigureViewerRenderContext(viewer_ptr, room_id);
2161
2162 // Update pinned state from manager
2164 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2165 viewer_ptr->SetPinned(
2167 viewer_ptr->SetPinCallback([this, card_id, room_id](bool pinned) {
2169 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2170 if (auto* v = GetViewerForRoom(room_id)) {
2171 v->SetPinned(pinned);
2172 }
2173 }
2174 });
2175 }
2176
2177 return viewer_ptr;
2178 }
2179
2180 // Creating a new viewer - Insert will handle LRU and eviction
2181 {
2182 auto viewer = std::make_unique<DungeonCanvasViewer>(rom_);
2183 viewer->SetCompactHeaderMode(false);
2184 viewer->SetRoomDetailsExpanded(true);
2185 DungeonCanvasViewer* viewer_ptr = viewer.get();
2186 viewer->SetRooms(&rooms_);
2187 viewer->SetRenderer(renderer_);
2188 viewer->SetCurrentPaletteGroup(current_palette_group_);
2189 viewer->SetCurrentPaletteId(current_palette_id_);
2190 viewer->SetGameData(game_data_);
2191 ConfigureViewerRenderContext(viewer_ptr, room_id);
2192 ApplyEntranceRenderContext(room_id);
2193
2194 // These hooks must remain correct even when a room panel swaps rooms while
2195 // keeping the same viewer instance (to preserve canvas pan/zoom + UI
2196 // state). Use the viewer's best-effort current room context instead of
2197 // capturing room_id at creation time.
2198 viewer->object_interaction().SetMutationCallback([this, viewer_ptr]() {
2199 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2200 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2201 const auto domain =
2203 if (domain == MutationDomain::kTileObjects) {
2204 BeginUndoSnapshot(rid);
2205 } else if (domain == MutationDomain::kCustomCollision) {
2206 BeginCollisionUndoSnapshot(rid);
2207 } else if (domain == MutationDomain::kWaterFill) {
2208 BeginWaterFillUndoSnapshot(rid);
2209 }
2210 }
2211 });
2212
2213 viewer->object_interaction().SetCacheInvalidationCallback([this,
2214 viewer_ptr]() {
2215 const int rid = viewer_ptr ? viewer_ptr->current_room_id() : -1;
2216 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2217 const auto domain =
2219 if (domain == MutationDomain::kTileObjects) {
2220 rooms_[rid].MarkObjectsDirty();
2221 rooms_[rid].RenderRoomGraphics();
2222 // Drag edits invalidate incrementally; finalize once the drag ends
2223 // (TileObjectHandler emits an extra invalidation on release).
2224 const auto mode =
2225 viewer_ptr->object_interaction().mode_manager().GetMode();
2227 FinalizeUndoAction(rid);
2228 }
2229 } else if (domain == MutationDomain::kCustomCollision) {
2230 const auto mode =
2231 viewer_ptr->object_interaction().mode_manager().GetMode();
2232 const auto& st =
2234 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2235 return;
2236 }
2237 FinalizeCollisionUndoAction(rid);
2238 } else if (domain == MutationDomain::kWaterFill) {
2239 const auto mode =
2240 viewer_ptr->object_interaction().mode_manager().GetMode();
2241 const auto& st =
2243 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2244 return;
2245 }
2246 FinalizeWaterFillUndoAction(rid);
2247 }
2248 }
2249 });
2250
2251 viewer->object_interaction().SetObjectPlacedCallback(
2252 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2253
2254 if (dungeon_editor_system_) {
2255 viewer->SetEditorSystem(dungeon_editor_system_.get());
2256 }
2257 viewer->SetRoomNavigationCallback([this](int target_room) {
2258 if (target_room >= 0 && target_room < static_cast<int>(rooms_.size())) {
2259 OnRoomSelected(target_room);
2260 }
2261 });
2262 // Swap callback swaps the room in the current panel instead of opening new
2263 viewer->SetRoomSwapCallback([this](int old_room, int new_room) {
2264 SwapRoomInPanel(old_room, new_room);
2265 });
2266 WireViewerPanelCallbacks(viewer.get());
2267
2268 // Wire up pinning for room panels.
2269 if (dependencies_.window_manager) {
2270 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2271 viewer->SetPinned(dependencies_.window_manager->IsWindowPinned(card_id));
2272 viewer->SetPinCallback([this, card_id, room_id](bool pinned) {
2273 if (dependencies_.window_manager) {
2274 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2275 // Sync state back to viewer in all panels showing this room
2276 if (auto* v = GetViewerForRoom(room_id)) {
2277 v->SetPinned(pinned);
2278 }
2279 }
2280 });
2281 }
2282
2283 auto* stored = room_viewers_.Insert(room_id, std::move(viewer));
2284 return stored->get();
2285 }
2286}
2287
2288void DungeonEditorV2::RefreshWorkbenchViewerRuntimeContext(
2289 DungeonCanvasViewer* viewer, int room_id) {
2290 if (!viewer) {
2291 return;
2292 }
2293
2294 if (viewer->rom() != rom_) {
2295 viewer->SetRom(rom_);
2296 }
2297 if (viewer->rooms() != &rooms_) {
2298 viewer->SetRooms(&rooms_);
2299 }
2300 if (viewer->game_data() != game_data_) {
2301 viewer->SetGameData(game_data_);
2302 }
2303 viewer->SetRenderer(renderer_);
2304 viewer->SetCurrentPaletteGroup(current_palette_group_);
2305 viewer->SetCurrentPaletteId(current_palette_id_);
2306 ConfigureViewerRenderContext(viewer, room_id);
2307 viewer->SetEditorSystem(dungeon_editor_system_.get());
2308 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2309 if (viewer->project() != dependencies_.project) {
2310 viewer->SetProject(dependencies_.project);
2311 }
2312}
2313
2314DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchViewer() {
2315 if (!workbench_viewer_) {
2316 workbench_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2317 auto* viewer = workbench_viewer_.get();
2318 viewer->SetCompactHeaderMode(true);
2319 viewer->SetRoomDetailsExpanded(false);
2320 viewer->SetHeaderVisible(false);
2321 viewer->SetHeaderHiddenMetadataHudVisible(false);
2322 viewer->SetRooms(&rooms_);
2323 viewer->SetRenderer(renderer_);
2324 viewer->SetCurrentPaletteGroup(current_palette_group_);
2325 viewer->SetCurrentPaletteId(current_palette_id_);
2326 viewer->SetGameData(game_data_);
2327
2328 // Workbench uses a single viewer; these hooks use the viewer's current room
2329 // context (set at DrawDungeonCanvas start) so room switching stays correct.
2330 viewer->object_interaction().SetMutationCallback([this, viewer]() {
2331 const int rid = viewer ? viewer->current_room_id() : -1;
2332 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2333 const auto domain = viewer->object_interaction().last_mutation_domain();
2334 if (domain == MutationDomain::kTileObjects) {
2335 BeginUndoSnapshot(rid);
2336 } else if (domain == MutationDomain::kCustomCollision) {
2337 BeginCollisionUndoSnapshot(rid);
2338 } else if (domain == MutationDomain::kWaterFill) {
2339 BeginWaterFillUndoSnapshot(rid);
2340 }
2341 }
2342 });
2343 viewer->object_interaction().SetCacheInvalidationCallback([this, viewer]() {
2344 const int rid = viewer ? viewer->current_room_id() : -1;
2345 if (rid >= 0 && rid < static_cast<int>(rooms_.size())) {
2346 const auto domain =
2347 viewer->object_interaction().last_invalidation_domain();
2348 if (domain == MutationDomain::kTileObjects) {
2349 rooms_[rid].MarkObjectsDirty();
2350 rooms_[rid].RenderRoomGraphics();
2351 const auto mode =
2352 viewer->object_interaction().mode_manager().GetMode();
2353 if (mode != InteractionMode::DraggingObjects) {
2354 FinalizeUndoAction(rid);
2355 }
2356 } else if (domain == MutationDomain::kCustomCollision) {
2357 const auto mode =
2358 viewer->object_interaction().mode_manager().GetMode();
2359 const auto& st =
2360 viewer->object_interaction().mode_manager().GetModeState();
2361 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2362 return;
2363 }
2364 FinalizeCollisionUndoAction(rid);
2365 } else if (domain == MutationDomain::kWaterFill) {
2366 const auto mode =
2367 viewer->object_interaction().mode_manager().GetMode();
2368 const auto& st =
2369 viewer->object_interaction().mode_manager().GetModeState();
2370 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2371 return;
2372 }
2373 FinalizeWaterFillUndoAction(rid);
2374 }
2375 }
2376 });
2377
2378 viewer->object_interaction().SetObjectPlacedCallback(
2379 [this](const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2380
2381 if (dungeon_editor_system_) {
2382 viewer->SetEditorSystem(dungeon_editor_system_.get());
2383 }
2384
2385 // In workbench mode, arrow navigation swaps the current room without
2386 // changing window identities.
2387 viewer->SetRoomSwapCallback([this](int /*old_room*/, int new_room) {
2388 OnRoomSelected(new_room, /*request_focus=*/false);
2389 });
2390 viewer->SetRoomNavigationCallback([this](int target_room) {
2391 OnRoomSelected(target_room, /*request_focus=*/false);
2392 });
2393
2394 WireViewerPanelCallbacks(viewer);
2395 }
2396
2397 auto* viewer = workbench_viewer_.get();
2398 RefreshWorkbenchViewerRuntimeContext(viewer, current_room_id_);
2399 return viewer;
2400}
2401
2402DungeonCanvasViewer* DungeonEditorV2::GetWorkbenchCompareViewer(int room_id) {
2403 if (!workbench_compare_viewer_) {
2404 workbench_compare_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2405 auto* viewer = workbench_compare_viewer_.get();
2406 viewer->SetCompactHeaderMode(true);
2407 viewer->SetRoomDetailsExpanded(false);
2408 viewer->SetHeaderHiddenMetadataHudVisible(false);
2409 viewer->SetRooms(&rooms_);
2410 viewer->SetRenderer(renderer_);
2411 viewer->SetCurrentPaletteGroup(current_palette_group_);
2412 viewer->SetCurrentPaletteId(current_palette_id_);
2413 viewer->SetGameData(game_data_);
2414
2415 // Compare viewer is read-only by default: no object selection/mutation, but
2416 // still allows canvas pan/zoom.
2417 viewer->SetObjectInteractionEnabled(false);
2418 viewer->SetHeaderReadOnly(true);
2419 viewer->SetHeaderVisible(false);
2420
2421 if (dungeon_editor_system_) {
2422 // Allows consistent rendering paths that depend on the editor system, but
2423 // interaction is still disabled.
2424 viewer->SetEditorSystem(dungeon_editor_system_.get());
2425 }
2426
2427 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2428 viewer->SetProject(dependencies_.project);
2429 }
2430
2431 auto* viewer = workbench_compare_viewer_.get();
2432 RefreshWorkbenchViewerRuntimeContext(viewer, room_id);
2433 viewer->SetObjectInteractionEnabled(false);
2434 viewer->SetHeaderReadOnly(true);
2435 viewer->SetHeaderVisible(false);
2436 return viewer;
2437}
2438
2439void DungeonEditorV2::SyncPanelsToRoom(int room_id) {
2440 // Update object editor card with current viewer
2441 if (object_selector_panel_) {
2442 object_selector_panel_->SetCanvasViewerProvider(
2443 [this]() { return GetViewerForRoom(current_room_id_); });
2444 object_selector_panel_->SetCurrentRoom(room_id);
2445 object_selector_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2446 }
2447 if (object_editor_content_) {
2448 object_editor_content_->SetCanvasViewerProvider(
2449 [this]() { return GetViewerForRoom(current_room_id_); });
2450 object_editor_content_->SetCurrentRoom(room_id);
2451 object_editor_content_->SetCanvasViewer(GetViewerForRoom(room_id));
2452 }
2453 if (door_editor_panel_) {
2454 door_editor_panel_->SetCanvasViewerProvider(
2455 [this]() { return GetViewerForRoom(current_room_id_); });
2456 door_editor_panel_->SetCurrentRoom(room_id);
2457 door_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2458 }
2459
2460 // Update sprite and item editor panels with current viewer
2461 if (sprite_editor_panel_) {
2462 sprite_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2463 }
2464 if (item_editor_panel_) {
2465 item_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2466 }
2467 if (custom_collision_panel_) {
2468 auto* viewer = GetViewerForRoom(room_id);
2469 custom_collision_panel_->SetCanvasViewer(viewer);
2470 if (viewer) {
2471 custom_collision_panel_->SetInteraction(&viewer->object_interaction());
2472 }
2473 }
2474 if (water_fill_panel_) {
2475 auto* viewer = GetViewerForRoom(room_id);
2476 water_fill_panel_->SetCanvasViewer(viewer);
2477 if (viewer) {
2478 water_fill_panel_->SetInteraction(&viewer->object_interaction());
2479 }
2480 }
2481
2482 if (object_tile_editor_panel_) {
2483 object_tile_editor_panel_->SetCurrentPaletteGroup(current_palette_group_);
2484 }
2485
2486 if (room_tag_editor_panel_) {
2487 room_tag_editor_panel_->SetCurrentRoomId(room_id);
2488 }
2489
2490 if (overlay_manager_panel_) {
2491 auto* viewer = GetViewerForRoom(room_id);
2492 if (viewer) {
2494 overlay_state.show_grid = viewer->mutable_show_grid();
2495 overlay_state.show_object_bounds = viewer->mutable_show_object_bounds();
2496 overlay_state.show_coordinate_overlay =
2497 viewer->mutable_show_coordinate_overlay();
2498 overlay_state.show_room_debug_info =
2499 viewer->mutable_show_room_debug_info();
2500 overlay_state.show_texture_debug = viewer->mutable_show_texture_debug();
2501 overlay_state.show_layer_info = viewer->mutable_show_layer_info();
2502 overlay_state.show_minecart_tracks =
2503 viewer->mutable_show_minecart_tracks();
2504 overlay_state.show_custom_collision =
2505 viewer->mutable_show_custom_collision_overlay();
2506 overlay_state.show_track_collision =
2507 viewer->mutable_show_track_collision_overlay();
2508 overlay_state.show_camera_quadrants =
2509 viewer->mutable_show_camera_quadrant_overlay();
2510 overlay_state.show_minecart_sprites =
2511 viewer->mutable_show_minecart_sprite_overlay();
2512 overlay_state.show_collision_legend =
2513 viewer->mutable_show_track_collision_legend();
2514 overlay_manager_panel_->SetState(overlay_state);
2515 }
2516 }
2517}
2518
2519void DungeonEditorV2::ShowRoomPanel(int room_id) {
2520 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
2521 return;
2522 }
2523
2524 bool already_active = false;
2525 for (int i = 0; i < active_rooms_.Size; ++i) {
2526 if (active_rooms_[i] == room_id) {
2527 already_active = true;
2528 break;
2529 }
2530 }
2531 if (!already_active) {
2532 active_rooms_.push_back(room_id);
2533 room_selector_.set_active_rooms(active_rooms_);
2534 }
2535
2536 std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
2537
2538 if (dependencies_.window_manager) {
2539 if (!dependencies_.window_manager->GetWindowDescriptor(
2540 dependencies_.window_manager->GetActiveSessionId(), card_id)) {
2541 std::string room_name = absl::StrFormat(
2542 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2543 dependencies_.window_manager->RegisterWindow(
2544 {.card_id = card_id,
2545 .display_name = room_name,
2546 .window_title = ICON_MD_GRID_ON " " + room_name,
2547 .icon = ICON_MD_GRID_ON,
2548 .category = "Dungeon",
2549 .shortcut_hint = "",
2550 .visibility_flag = nullptr,
2551 .priority = 200 + room_id});
2552 }
2553 dependencies_.window_manager->OpenWindow(card_id);
2554 }
2555
2556 // Create or update the PanelWindow for this room
2557 if (room_cards_.find(room_id) == room_cards_.end()) {
2558 std::string base_name = absl::StrFormat(
2559 "[%03X] %s", room_id, zelda3::GetRoomLabel(room_id).c_str());
2560 const int slot_id = GetOrCreateRoomPanelSlotId(room_id);
2561 std::string card_name_str = absl::StrFormat(
2562 "%s###RoomPanelSlot%d", MakePanelTitle(base_name).c_str(), slot_id);
2563
2564 auto card = std::make_shared<gui::PanelWindow>(card_name_str.c_str(),
2566 card->SetDefaultSize(620, 700);
2567 room_cards_[room_id] = card;
2568 }
2569}
2570
2571} // namespace yaze::editor
void Publish(const T &event)
Definition event_bus.h:35
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
bool is_loaded() const
Definition rom.h:144
static Flags & get()
Definition features.h:119
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetShowEntranceListCallback(std::function< void()> callback)
void SetEditorSystem(zelda3::DungeonEditorSystem *system)
void SetMinecartTrackPanel(MinecartTrackEditorPanel *panel)
void SetShowItemPanelCallback(std::function< void()> callback)
void SetShowRoomGraphicsCallback(std::function< void()> callback)
void SetProject(const project::YazeProject *project)
void SetRenderer(gfx::IRenderer *renderer)
void SetEditGraphicsCallback(std::function< void(int, const zelda3::RoomObject &)> callback)
void SetShowRoomListCallback(std::function< void()> callback)
void RefreshRomBackedState(Rom *rom, zelda3::GameData *game_data, DungeonRoomStore *rooms, int room_id)
DungeonObjectInteraction & object_interaction()
void SetShowRoomMatrixCallback(std::function< void()> callback)
void SetShowSpritePanelCallback(std::function< void()> callback)
void SetShowObjectPanelCallback(std::function< void()> callback)
void SetEntranceRenderContext(int entrance_id, uint8_t entrance_blockset)
const project::YazeProject * project() const
void SetSaveRoomCallback(std::function< void(int)> callback)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group, bool force_refresh=false)
void SetShowDungeonSettingsCallback(std::function< void()> callback)
void SetRooms(DungeonRoomStore *rooms)
void SetGameData(zelda3::GameData *game_data)
void SetShowDoorEditorCallback(std::function< void()> callback)
void SetPinCallback(std::function< void(bool)> callback)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
std::unique_ptr< CustomCollisionPanel > owned_custom_collision_panel_
class CustomCollisionPanel * custom_collision_panel_
void ToggleWorkbenchWorkflowMode(bool show_toast=true)
void ContributeStatus(StatusBar *status_bar) override
void OpenGraphicsEditorForObject(int room_id, const zelda3::RoomObject &object)
class ItemEditorPanel * item_editor_panel_
std::unique_ptr< RoomTagEditorPanel > owned_room_tag_editor_panel_
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
gfx::PaletteGroup current_palette_group_
void OnEntranceSelected(int entrance_id)
static constexpr const char * kEntranceListId
class SpriteEditorPanel * sprite_editor_panel_
void HandleObjectPlaced(const zelda3::RoomObject &obj)
std::unique_ptr< zelda3::DungeonEditorSystem > dungeon_editor_system_
std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > entrances_
void OpenWindow(const std::string &window_id)
DoorEditorContent * door_editor_panel_
std::unordered_map< int, int > room_panel_slot_ids_
class DungeonWorkbenchContent * workbench_panel_
void OnRoomSelected(int room_id, bool request_focus=true)
PendingCollisionUndo pending_collision_undo_
ObjectTileEditorPanel * object_tile_editor_panel_
ObjectSelectorContent * object_selector_panel_
OverlayManagerPanel * overlay_manager_panel_
uint8_t ResolveSelectedEntranceBlocksetForRoom(int room_id) const
DungeonCanvasViewer * GetWorkbenchCompareViewer(int room_id)
void WireViewerPanelCallbacks(DungeonCanvasViewer *viewer)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
PendingWaterFillUndo pending_water_fill_undo_
std::unique_ptr< MinecartTrackEditorPanel > owned_minecart_track_editor_panel_
gui::PaletteEditorWidget palette_editor_
std::unique_ptr< ObjectEditorContent > owned_object_editor_content_
std::unique_ptr< DoorEditorContent > owned_door_editor_panel_
static bool IsValidRoomId(int room_id)
void SwapRoomInPanel(int old_room_id, int new_room_id)
RoomGraphicsContent * room_graphics_panel_
static constexpr size_t kMaxRecentRooms
std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > spawn_points_
void SetWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
class RoomTagEditorPanel * room_tag_editor_panel_
std::optional< SaveTransactionSnapshot > save_transaction_snapshot_
std::unique_ptr< ObjectSelectorContent > owned_object_selector_panel_
static constexpr const char * kDoorEditorId
DungeonCanvasViewer * GetViewerForRoom(int room_id)
class WaterFillPanel * water_fill_panel_
DungeonCanvasViewer * GetWorkbenchViewer()
std::unordered_map< int, std::shared_ptr< gui::PanelWindow > > room_cards_
void ApplyEntranceRenderContext(int room_id)
static constexpr const char * kObjectSelectorId
void ConfigureViewerRenderContext(DungeonCanvasViewer *viewer, int room_id)
static constexpr const char * kRoomGraphicsId
void QueueWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
ObjectEditorContent * object_editor_content_
static constexpr const char * kRoomMatrixId
void HandleDungeonPaletteChanged(gui::DungeonPaletteChange change)
static constexpr const char * kRoomSelectorId
PaletteEditorContent * palette_editor_panel_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
std::unique_ptr< WaterFillPanel > owned_water_fill_panel_
static constexpr const char * kPaletteEditorId
PendingWorkflowMode pending_workflow_mode_
void InvalidateDungeonPaletteUsers(gui::DungeonPaletteChange change)
void HandleArenaPaletteChanged(const std::string &group_name, int palette_index)
void SetCustomObjectsFolder(const std::string &folder)
void SetProject(project::YazeProject *project)
void SetTileEditorPanel(ObjectTileEditorPanel *panel)
absl::Status LoadRoom(int room_id, zelda3::Room &room)
absl::Status LoadRoomEntrances(std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > &entrances)
absl::Status LoadDungeonSpawnPoints(std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > &spawn_points)
void SetGameData(zelda3::GameData *game_data)
void SetRoomSelectedWithIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
void SetEntranceSelectedCallback(std::function< void(int)> callback)
void set_entrances(std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > *entrances)
void set_current_entrance_id(int entrance_id)
void set_rooms(DungeonRoomStore *rooms)
void SetRoomSelectedCallback(std::function< void(int)> callback)
void SetProject(const project::YazeProject *project)
void set_active_rooms(const ImVector< int > &rooms)
zelda3::Room * GetIfMaterialized(int room_id)
void SetGameData(zelda3::GameData *game_data)
static void Draw(const DungeonStatusBarState &state)
static DungeonStatusBarState BuildState(const DungeonCanvasViewer &viewer, const char *tool_mode, bool room_dirty)
void SetUndoRedoProvider(std::function< bool()> can_undo, std::function< bool()> can_redo, std::function< void()> on_undo, std::function< void()> on_redo, std::function< std::string()> undo_desc, std::function< std::string()> redo_desc, std::function< int()> undo_depth)
void SetOnInspectorSideChanged(std::function< void(bool)> cb)
void SetEmbeddedEditorPanels(WindowContent *object_selector, WindowContent *door_editor, WindowContent *sprite_editor, WindowContent *item_editor, WindowContent *room_graphics, WindowContent *palette_editor)
void SetEmbeddedToolPanels(RoomTagEditorPanel *room_tags, CustomCollisionPanel *custom_collision, WaterFillPanel *water_fill, MinecartTrackEditorPanel *minecart_tracks)
void NotifyRoomChanged(int previous_room_id)
Called by the editor when the current room changes.
void SetPitDamageTableProvider(std::function< zelda3::PitDamageTable *()> provider)
The EditorManager controls the main editor window and manages the various editor classes.
UndoManager undo_manager_
Definition editor.h:334
zelda3::GameData * game_data() const
Definition editor.h:320
EditorDependencies dependencies_
Definition editor.h:333
EditorType type_
Definition editor.h:332
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetProject(project::YazeProject *project)
void SetRoomNavigationCallback(RoomNavigationCallback callback)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetPlacementError(const std::string &message)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetRooms(DungeonRoomStore *rooms)
void SetGameData(zelda3::GameData *game_data)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
Set the current palette group for graphics rendering.
void SetRooms(DungeonRoomStore *rooms)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
A session-aware status bar displayed at the bottom of the application.
Definition status_bar.h:54
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
void SetEditorMode(const std::string &mode)
Set the current editor mode or tool.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
std::string GetRedoDescription() const
Description of the action that would be redone (for UI)
std::string GetUndoDescription() const
Description of the action that would be undone (for UI)
size_t UndoStackSize() const
std::string GetDungeonInspectorSide() const
void SetDungeonInspectorSide(std::string side)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void SetWindowPinned(size_t session_id, const std::string &base_window_id, bool pinned)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
void RegisterWindow(size_t session_id, const WindowDescriptor &descriptor)
bool IsWindowOpen(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
bool IsWindowPinned(size_t session_id, const std::string &base_window_id) const
void ShowAllWindowsInCategory(size_t session_id, const std::string &category)
void UnregisterWindow(size_t session_id, const std::string &base_window_id)
void RegisterPanelAlias(const std::string &legacy_base_id, const std::string &canonical_base_id)
Register a legacy panel ID alias that resolves to a canonical ID.
static std::shared_ptr< MesenSocketClient > & GetClient()
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:441
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:448
static Arena & Get()
Definition arena.cc:21
void Initialize(zelda3::GameData *game_data)
Initialize the palette manager with GameData.
static PaletteManager & Get()
Get the singleton instance.
void SetOnDungeonPaletteChanged(std::function< void(DungeonPaletteChange)> callback)
void SetDungeonRenderPaletteMode(bool enabled)
void Initialize(zelda3::GameData *game_data)
Draggable, dockable panel for editor sub-windows.
bool Begin(bool *p_open=nullptr)
void SetDefaultSize(float width, float height)
static CustomObjectManager & Get()
void AddObjectFile(int object_id, const std::string &filename)
void Initialize(const std::string &custom_objects_folder)
static ObjectDimensionTable & Get()
Dungeon Room Entrance or Spawn Point.
void set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:574
bool HasUnsavedChanges() const
Definition room.h:589
void MarkGraphicsDirty()
Definition room.h:450
bool IsLoaded() const
Definition room.h:872
void ClearWaterFillZone()
Definition room.h:561
void ClearWaterFillDirty()
Definition room.h:582
int ResolveDungeonPaletteId() const
Definition room.cc:727
void SetGameData(GameData *data)
Definition room.h:964
void SetWaterFillTile(int x, int y, bool filled)
Definition room.h:538
#define ICON_MD_ROCKET_LAUNCH
Definition icons.h:1612
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_PENDING
Definition icons.h:1398
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_CATEGORY
Definition icons.h:382
#define ICON_MD_WORKSPACES
Definition icons.h:2186
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
const AgentUITheme & GetTheme()
bool IsTransientDungeonRoomWindowId(const std::string &card_id)
constexpr const char * kStandalone
constexpr const char * kWorkbench
Editors are the view controllers for the application.
RoomSelectionIntent
Intent for room selection in the dungeon editor.
absl::StatusOr< PaletteGroup > CreatePaletteGroupFromLargePalette(SnesPalette &palette, int num_colors)
Create a PaletteGroup by dividing a large palette into sub-palettes.
constexpr int kTilesheetHeight
Definition snes_tile.h:17
constexpr int kTilesheetWidth
Definition snes_tile.h:16
std::string MakePanelTitle(const std::string &title)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:546
std::string GetRoomLabel(int id)
Convenience function to get a room label.
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadLegacyWaterGateZones(Rom *rom, const std::string &symbol_path)
std::unique_ptr< DungeonEditorSystem > CreateDungeonEditorSystem(Rom *rom, GameData *game_data)
Factory function to create dungeon editor system.
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
project::YazeProject * project
Definition editor.h:173
GlobalEditorContext * global_context
Definition editor.h:175
gfx::IRenderer * renderer
Definition editor.h:191
WorkspaceWindowManager * window_manager
Definition editor.h:181
static JumpToRoomRequestEvent Create(int room, size_t session=0)
Optional behavior for an interactive status bar segment.
Definition status_bar.h:27
std::function< void()> on_click
Definition status_bar.h:28
Represents a group of palettes.
DungeonRenderPaletteSource source
static constexpr float kStatusBarHeight
Definition ui_config.h:21
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:197
std::string custom_objects_folder
Definition project.h:192
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1486
std::string symbols_filename
Definition project.h:190
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
PitDamageTable pit_damage_table
Definition game_data.h:108