16#include "absl/status/status.h"
17#include "absl/strings/str_format.h"
18#include "absl/types/span.h"
19#include "imgui/imgui.h"
81 return absl::FailedPreconditionError(
"ROM not loaded");
84 bool any_dirty =
false;
91 return absl::OkStatus();
94 std::vector<zelda3::WaterFillZoneEntry> zones;
96 for (
int room_id = 0; room_id < static_cast<int>(rooms.
size()); ++room_id) {
98 if (room ==
nullptr) {
106 if (tile_count <= 0) {
109 if (tile_count > 255) {
110 return absl::InvalidArgumentError(absl::StrFormat(
111 "Water fill zone in room 0x%02X has %d tiles (max 255)", room_id,
118 z.
fill_offsets.reserve(
static_cast<size_t>(tile_count));
121 for (
size_t i = 0; i < map.size(); ++i) {
126 zones.push_back(std::move(z));
129 if (zones.size() > 8) {
130 return absl::InvalidArgumentError(absl::StrFormat(
131 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
138 for (
const auto& z : zones) {
148 return absl::OkStatus();
213 window_manager->RegisterPanelAlias(
"dungeon.entrances",
214 "dungeon.entrance_properties");
218 window_manager->RegisterPanel(
219 {.card_id =
"dungeon.workbench",
220 .display_name =
"Dungeon Workbench",
221 .window_title =
" Dungeon Workbench",
223 .category =
"Dungeon",
224 .shortcut_hint =
"Ctrl+Shift+W",
225 .visibility_flag =
nullptr,
228 .disabled_tooltip =
"Load a ROM to edit dungeon rooms"});
230 window_manager->RegisterPanel(
232 .display_name =
"Room List",
233 .window_title =
" Room List",
235 .category =
"Dungeon",
236 .shortcut_hint =
"Ctrl+Shift+R",
237 .visibility_flag =
nullptr,
240 .disabled_tooltip =
"Load a ROM to browse dungeon rooms"});
242 window_manager->RegisterPanel(
244 .display_name =
"Entrance List",
245 .window_title =
" Entrance List",
247 .category =
"Dungeon",
248 .shortcut_hint =
"Ctrl+Shift+E",
249 .visibility_flag =
nullptr,
252 .disabled_tooltip =
"Load a ROM to browse dungeon entrances"});
254 window_manager->RegisterPanel(
255 {.card_id =
"dungeon.entrance_properties",
256 .display_name =
"Entrance Properties",
257 .window_title =
" Entrance Properties",
259 .category =
"Dungeon",
261 .visibility_flag =
nullptr,
264 .disabled_tooltip =
"Load a ROM to edit entrance properties"});
266 window_manager->RegisterPanel(
268 .display_name =
"Room Matrix",
269 .window_title =
" Room Matrix",
271 .category =
"Dungeon",
272 .shortcut_hint =
"Ctrl+Shift+M",
273 .visibility_flag =
nullptr,
276 .disabled_tooltip =
"Load a ROM to view the room matrix"});
278 window_manager->RegisterPanel(
280 .display_name =
"Room Graphics",
281 .window_title =
" Room Graphics",
283 .category =
"Dungeon",
284 .shortcut_hint =
"Ctrl+Shift+G",
285 .visibility_flag =
nullptr,
288 .disabled_tooltip =
"Load a ROM to view room graphics"});
290 window_manager->RegisterPanel(
292 .display_name =
"Object Selector",
293 .window_title =
" Object Selector",
295 .category =
"Dungeon",
297 .visibility_flag =
nullptr,
300 .disabled_tooltip =
"Load a ROM to browse dungeon objects"});
302 window_manager->RegisterPanel(
304 .display_name =
"Object Editor",
305 .window_title =
" Object Editor",
307 .category =
"Dungeon",
309 .visibility_flag =
nullptr,
312 .disabled_tooltip =
"Load a ROM to edit selected dungeon objects"});
314 window_manager->RegisterPanel(
316 .display_name =
"Door Editor",
317 .window_title =
" Door Editor",
319 .category =
"Dungeon",
321 .visibility_flag =
nullptr,
324 .disabled_tooltip =
"Load a ROM to edit dungeon doors"});
326 window_manager->RegisterPanel(
328 .display_name =
"Palette Editor",
329 .window_title =
" Palette Editor",
331 .category =
"Dungeon",
333 .shortcut_hint =
"Ctrl+Shift+Alt+P",
334 .visibility_flag =
nullptr,
337 .disabled_tooltip =
"Load a ROM to edit dungeon palettes"});
339 window_manager->RegisterPanel(
340 {.card_id =
"dungeon.room_tags",
341 .display_name =
"Room Tags",
342 .window_title =
" Room Tags",
344 .category =
"Dungeon",
346 .visibility_flag =
nullptr,
349 .disabled_tooltip =
"Load a ROM to view room tags"});
351 window_manager->RegisterPanel(
352 {.card_id =
"dungeon.dungeon_map",
353 .display_name =
"Dungeon Map",
354 .window_title =
" Dungeon Map",
356 .category =
"Dungeon",
357 .shortcut_hint =
"Ctrl+Shift+D",
358 .visibility_flag =
nullptr,
361 .disabled_tooltip =
"Load a ROM to view the dungeon map"});
376 window_manager->RegisterWindowContent(
377 std::make_unique<RoomBrowserContent>(
380 window_manager->RegisterWindowContent(
381 std::make_unique<DungeonEntranceListPanel>(
386 auto matrix_panel = std::make_unique<RoomMatrixContent>(
389 [
this](
int old_room,
int new_room) {
393 matrix_panel->SetRoomIntentCallback(
397 window_manager->RegisterWindowContent(std::move(matrix_panel));
401 auto dungeon_map = std::make_unique<DungeonMapPanel>(
404 dungeon_map->SetRoomIntentCallback(
411 window_manager->RegisterWindowContent(std::move(dungeon_map));
415 auto workbench = std::make_unique<DungeonWorkbenchContent>(
421 [
this](
int room_id) {
424 LOG_ERROR(
"DungeonEditorV2",
"Save Room failed: %s",
425 status.message().data());
428 absl::StrFormat(
"Save Room failed: %s", status.message()),
440 [
this]() ->
const std::deque<int>& {
return recent_rooms_; },
441 [
this](
int room_id) {
446 [
this](
const std::string& id) {
OpenWindow(
id); },
452 [
this]() {
Undo().IgnoreError(); }, [
this]() {
Redo().IgnoreError(); },
456 window_manager->RegisterWindowContent(std::move(workbench));
459 window_manager->RegisterWindowContent(std::make_unique<DungeonEntrancesPanel>(
469 return absl::FailedPreconditionError(
"ROM not loaded");
475 if (!dim_table.IsLoaded()) {
482 return absl::FailedPreconditionError(
"GameData not available");
512 auto graphics_panel = std::make_unique<RoomGraphicsContent>(
516 std::move(graphics_panel));
526 auto object_selector = std::make_unique<ObjectSelectorContent>(
535 rooms_[current_room_id_].RenderRoomGraphics();
540 object_selector->SetRooms(&
rooms_);
543 object_selector->SetOpenObjectEditorCallback(
550 auto door_editor = std::make_unique<DoorEditorContent>();
551 door_editor->SetRooms(&
rooms_);
565 object_selector->set_tile_editor_callback([
this](int16_t object_id) {
577 std::move(object_selector));
579 std::move(object_editor));
588 std::move(sprite_panel));
595 auto collision_panel = std::make_unique<CustomCollisionPanel>(
599 std::move(collision_panel));
601 auto water_fill_panel =
602 std::make_unique<WaterFillPanel>(
nullptr,
nullptr);
605 std::move(water_fill_panel));
609 auto tile_editor_panel =
615 tile_editor_panel->SetObjectCreatedCallback(
616 [
this](
int object_id,
const std::string& filename) {
631 std::move(tile_editor_panel));
644 auto settings_panel = std::make_unique<DungeonSettingsPanel>(
nullptr);
645 settings_panel->SetSaveRoomCallback([
this](
int id) {
SaveRoom(
id); });
646 settings_panel->SetSaveAllRoomsCallback([
this]() {
SaveAllRooms(); });
650 std::move(settings_panel));
654 auto room_tag_panel = std::make_unique<RoomTagEditorPanel>();
656 room_tag_panel->SetRooms(&
rooms_);
660 std::move(room_tag_panel));
665 auto overlay_panel = std::make_unique<OverlayManagerPanel>();
668 std::move(overlay_panel));
674 auto minecart_panel = std::make_unique<MinecartTrackEditorPanel>();
677 std::move(minecart_panel));
733 apply_palette(existing_viewer->get());
747 bool rendered_current_room =
false;
750 if (room_id >= 0 && room_id < (
int)
rooms_.
size()) {
751 rooms_[room_id].RenderRoomGraphics();
769 bool legacy_imported =
false;
770 std::vector<zelda3::WaterFillZoneEntry> zones;
774 zones = std::move(zones_or.value());
776 LOG_WARN(
"DungeonEditorV2",
"WaterFillTable parse failed: %s",
777 zones_or.status().message().data());
780 absl::StrFormat(
"WaterFill table parse failed: %s",
781 zones_or.status().message()),
787 std::string sym_path;
794 if (legacy_or.ok()) {
795 zones = std::move(legacy_or.value());
796 legacy_imported = !zones.empty();
798 LOG_WARN(
"DungeonEditorV2",
"Legacy water gate import failed: %s",
799 legacy_or.status().message().data());
803 for (
const auto& z : zones) {
804 if (z.room_id < 0 || z.room_id >=
static_cast<int>(
rooms_.
size())) {
807 auto& room =
rooms_[z.room_id];
809 for (uint16_t off : z.fill_offsets) {
810 const int x =
static_cast<int>(off % 64);
811 const int y =
static_cast<int>(off / 64);
815 if (!legacy_imported) {
822 "Imported legacy water gate zones (save to write new table)",
828 return absl::OkStatus();
842 if (loading_card.
Begin()) {
843 ImGui::TextColored(theme.text_secondary_gray,
"Loading dungeon data...");
845 "Independent editor cards will appear once ROM data is loaded.");
848 return absl::OkStatus();
855 if (ImGui::IsKeyPressed(ImGuiKey_Delete)) {
858 viewer->DeleteSelectedObjects();
863 if (!ImGui::GetIO().WantTextInput) {
864 if (ImGui::GetIO().KeyCtrl && ImGui::GetIO().KeyShift &&
865 ImGui::IsKeyPressed(ImGuiKey_W,
false)) {
870 if (ImGui::IsKeyPressed(ImGuiKey_Tab) && ImGui::GetIO().KeyCtrl) {
873 int current_idx = -1;
874 for (
int i = 0; i < static_cast<int>(
recent_rooms_.size()); ++i) {
880 if (current_idx != -1) {
882 if (ImGui::GetIO().KeyShift) {
894 int current_idx = -1;
902 if (current_idx != -1) {
904 if (ImGui::GetIO().KeyShift) {
916 if (ImGui::GetIO().KeyCtrl) {
918 const int kCols = 16;
920 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
923 }
else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
926 }
else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
929 }
else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
935 if (next_room != -1) {
949 return absl::OkStatus();
959 room_opts.
tooltip =
"Click to refocus viewer on this room";
960 room_opts.
on_click = [
this, room_id]() {
967 std::move(room_opts));
975 absl::StrFormat(
"%d rooms loaded of %d total", loaded, total);
976 status_bar->
SetCustomSegment(
"Rooms", absl::StrFormat(
"%d/%d", loaded, total),
977 std::move(rooms_opts));
981 mode_opts.
tooltip =
"Click to toggle Workbench / Standalone workflow";
985 std::move(mode_opts));
990 return absl::FailedPreconditionError(
"ROM not loaded");
998 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
999 status.message().data());
1002 LOG_INFO(
"DungeonEditorV2",
"Saved %zu modified colors to ROM",
1006 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1007 absl::Status save_status = absl::OkStatus();
1009 if (!save_status.ok()) {
1014 save_status = status;
1017 if (!save_status.ok()) {
1022 if (flags.kSaveTorches) {
1025 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
1027 LOG_ERROR(
"DungeonEditorV2",
"Failed to save torches: %s",
1028 status.message().data());
1033 if (flags.kSavePits) {
1036 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pits: %s",
1037 status.message().data());
1042 if (flags.kSaveBlocks) {
1045 LOG_ERROR(
"DungeonEditorV2",
"Failed to save blocks: %s",
1046 status.message().data());
1051 if (flags.kSaveCollision) {
1054 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
1056 LOG_ERROR(
"DungeonEditorV2",
"Failed to save collision: %s",
1057 status.message().data());
1062 if (flags.kSaveWaterFillZones) {
1063 auto status = SaveWaterFillZones(
rom_,
rooms_);
1065 LOG_ERROR(
"DungeonEditorV2",
"Failed to save water fill zones: %s",
1066 status.message().data());
1071 if (flags.kSaveChests) {
1074 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
1076 LOG_ERROR(
"DungeonEditorV2",
"Failed to save chests: %s",
1077 status.message().data());
1082 if (flags.kSavePotItems) {
1085 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
1087 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pot items: %s",
1088 status.message().data());
1093 return absl::OkStatus();
1098 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1111 if (flags.kSaveWaterFillZones &&
1113 bool any_dirty =
false;
1128 if (flags.kSaveCollision) {
1130 const bool has_ptr_table =
1132 static_cast<int>(rom_data.size()));
1134 static_cast<int>(rom_data.size()));
1135 if (has_ptr_table && has_data_region) {
1136 bool any_dirty =
false;
1152 room_id = room.
id();
1155 if (flags.kSaveRoomHeaders) {
1157 int header_ptr_table =
1162 int table_offset = header_ptr_table + (room_id * 2);
1164 if (table_offset + 1 <
static_cast<int>(rom_data.size())) {
1166 (rom_data[table_offset + 1] << 8) |
1167 rom_data[table_offset];
1169 ranges.emplace_back(header_location, header_location + 14);
1175 if (flags.kSaveObjects) {
1181 int entry_offset = obj_ptr_table + (room_id * 3);
1183 if (entry_offset + 2 <
static_cast<int>(rom_data.size())) {
1184 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1185 (rom_data[entry_offset + 1] << 8) |
1186 rom_data[entry_offset];
1190 ranges.emplace_back(objects_location,
1191 objects_location + encoded.size() + 2);
1202 return absl::FailedPreconditionError(
"ROM not loaded");
1204 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size())) {
1205 return absl::InvalidArgumentError(
"Invalid room ID");
1212 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
1213 status.message().data());
1217 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1221 if (flags.kSaveTorches) {
1224 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1226 if (flags.kSavePits) {
1229 if (flags.kSaveBlocks) {
1232 if (flags.kSaveCollision) {
1235 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1237 if (flags.kSaveWaterFillZones) {
1240 if (flags.kSaveChests) {
1243 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1245 if (flags.kSavePotItems) {
1248 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); }));
1251 return absl::OkStatus();
1260 return absl::FailedPreconditionError(
"ROM not loaded");
1262 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size())) {
1263 return absl::InvalidArgumentError(
"Invalid room ID");
1266 auto& room =
rooms_[room_id];
1267 if (!room.IsLoaded()) {
1268 return absl::OkStatus();
1275 for (
const auto& w : result.warnings) {
1276 LOG_WARN(
"DungeonEditorV2",
"Room 0x%03X validation warning: %s", room_id,
1279 if (!result.is_valid) {
1280 for (
const auto& e : result.errors) {
1281 LOG_ERROR(
"DungeonEditorV2",
"Room 0x%03X validation error: %s",
1282 room_id, e.c_str());
1287 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1288 room_id, result.errors.size()),
1291 return absl::FailedPreconditionError(
1292 absl::StrFormat(
"Room 0x%03X failed validation", room_id));
1300 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1305 if (flags.kSaveRoomHeaders) {
1307 int header_ptr_table =
1312 int table_offset = header_ptr_table + (room_id * 2);
1314 if (table_offset + 1 <
static_cast<int>(rom_data.size())) {
1316 (rom_data[table_offset + 1] << 8) |
1317 rom_data[table_offset];
1319 ranges.emplace_back(header_location, header_location + 14);
1325 if (flags.kSaveObjects) {
1331 int entry_offset = obj_ptr_table + (room_id * 3);
1333 if (entry_offset + 2 <
static_cast<int>(rom_data.size())) {
1334 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1335 (rom_data[entry_offset + 1] << 8) |
1336 rom_data[entry_offset];
1343 auto encoded = room.EncodeObjects();
1344 ranges.emplace_back(objects_location,
1345 objects_location + encoded.size() + 2);
1354 manifest, write_policy, ranges, absl::StrFormat(
"room 0x%03X", room_id),
1358 if (flags.kSaveObjects) {
1359 auto status = room.SaveObjects();
1361 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room objects: %s",
1362 status.message().data());
1367 if (flags.kSaveSprites) {
1368 auto status = room.SaveSprites();
1370 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room sprites: %s",
1371 status.message().data());
1376 if (flags.kSaveRoomHeaders) {
1377 auto status = room.SaveRoomHeader();
1379 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room header: %s",
1380 status.message().data());
1387 if (!sys_status.ok()) {
1388 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room system data: %s",
1389 sys_status.message().data());
1393 return absl::OkStatus();
1408 if (!window_manager) {
1416 window_manager->OpenWindow(session_id,
"dungeon.workbench");
1419 for (
const auto& descriptor :
1420 window_manager->GetWindowsInCategory(session_id,
"Dungeon")) {
1421 const std::string& card_id = descriptor.card_id;
1422 if (card_id ==
"dungeon.workbench") {
1425 if (window_manager->IsWindowPinned(session_id, card_id)) {
1428 const bool is_room_window = card_id.rfind(
"dungeon.room_", 0) == 0;
1431 window_manager->CloseWindow(session_id, card_id);
1435 window_manager->CloseWindow(session_id,
"dungeon.workbench");
1445 enabled ?
"Dungeon workflow: Workbench"
1446 :
"Dungeon workflow: Standalone Panels",
1465 std::string card_id = absl::StrFormat(
"dungeon.room_%d", room_id);
1466 bool panel_visible =
true;
1471 if (!panel_visible) {
1484 std::string active_category =
1489 if (active_category !=
"Dungeon" && !is_pinned) {
1505 ImGui::SetNextWindowDockID(
room_dock_id_, ImGuiCond_FirstUseEver);
1507 if (room_card->Begin(&open)) {
1508 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) {
1545 if (room_id < 0 || room_id >= 0x128) {
1546 ImGui::Text(
"Invalid room ID: %d", room_id);
1550 auto& room =
rooms_[room_id];
1552 if (!room.IsLoaded()) {
1555 ImGui::TextColored(theme.text_error_red,
"Failed to load room: %s",
1556 status.message().data());
1562 if (!sys_status.ok()) {
1563 LOG_ERROR(
"DungeonEditorV2",
"Failed to load system data: %s",
1564 sys_status.message().data());
1569 if (room.IsLoaded()) {
1570 bool needs_render =
false;
1572 if (room.blocks().empty()) {
1573 room.LoadRoomGraphics(room.blockset());
1574 needs_render =
true;
1577 if (!room.AreObjectsLoaded()) {
1579 needs_render =
true;
1582 auto& bg1_bitmap = room.bg1_buffer().bitmap();
1583 if (needs_render || !bg1_bitmap.is_active() || bg1_bitmap.width() == 0) {
1584 room.RenderRoomGraphics();
1588 if (room.IsLoaded()) {
1589 ImGui::TextColored(theme.text_success_green,
ICON_MD_CHECK " Loaded");
1591 ImGui::TextColored(theme.text_error_red,
ICON_MD_PENDING " Not Loaded");
1594 ImGui::TextDisabled(
"Objects: %zu", room.GetTileObjects().size());
1599 const std::string return_label = absl::StrFormat(
1601 if (ImGui::SmallButton(return_label.c_str())) {
1604 if (ImGui::IsItemHovered()) {
1606 "Switch back to the integrated Dungeon Workbench workflow "
1615 bool connected = client && client->IsConnected();
1617 ImGui::BeginDisabled();
1619 std::string warp_label =
1621 if (ImGui::SmallButton(warp_label.c_str())) {
1622 auto status = client->WriteWord(0x7E00A0,
static_cast<uint16_t
>(room_id));
1626 absl::StrFormat(
"Warped to room 0x%03X", room_id),
1632 absl::StrFormat(
"Warp failed: %s", status.message()),
1638 ImGui::EndDisabled();
1639 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1640 ImGui::SetTooltip(
"Connect to Mesen2 first");
1643 if (ImGui::IsItemHovered()) {
1644 ImGui::SetTooltip(
"Warp to room 0x%03X in Mesen2", room_id);
1653 viewer->DrawDungeonCanvas(room_id);
1664 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size())) {
1683 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size())) {
1684 LOG_WARN(
"DungeonEditorV2",
"Ignoring invalid room selection: %d", room_id);
1712 if (room_id >= 0 && room_id < (
int)
rooms_.
size()) {
1713 auto& room =
rooms_[room_id];
1714 if (!room.IsLoaded()) {
1718 if (room.IsLoaded()) {
1728 auto dungeon_main_pal_group =
1771 std::string card_id = absl::StrFormat(
"dungeon.room_%d", room_id);
1774 if (request_focus) {
1786 std::string room_name = absl::StrFormat(
1789 std::string base_card_id = absl::StrFormat(
"dungeon.room_%d", room_id);
1792 {.card_id = base_card_id,
1793 .display_name = room_name,
1796 .category =
"Dungeon",
1797 .shortcut_hint =
"",
1798 .visibility_flag =
nullptr,
1799 .priority = 200 + room_id});
1806 if (entrance_id < 0 || entrance_id >=
static_cast<int>(
entrances_.size())) {
1814 auto status =
Save();
1820 LOG_ERROR(
"DungeonEditorV2",
"SaveAllRooms failed: %s",
1821 status.message().data());
1824 absl::StrFormat(
"Save all failed: %s", status.message()),
1837 it->second->Focus();
1870 LOG_ERROR(
"DungeonEditorV2",
"Cannot place object: Invalid room ID %d",
1874 absl::StrFormat(
"Object 0x%02X: no room selected (invalid room %d)",
1888 "Placing object ID=0x%02X at position (%d,%d) in room %03X", obj.
id_,
1891 room.RenderRoomGraphics();
1893 "Object placed and room re-rendered successfully");
1898 absl::StrFormat(
"Placed 0x%02X in room %03X", obj.
id_,
1906 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size())) {
1907 LOG_WARN(
"DungeonEditorV2",
"Edit Graphics ignored (invalid room id %d)",
1913 if (!editor_manager) {
1915 "Edit Graphics ignored (editor manager unavailable)");
1919 auto& room =
rooms_[room_id];
1920 room.LoadRoomGraphics(room.blockset());
1922 uint16_t sheet_id = 0;
1923 uint16_t tile_index = 0;
1924 bool resolved_sheet =
false;
1925 if (
auto tiles_or =
object.GetTiles();
1926 tiles_or.ok() && !tiles_or.value().empty()) {
1927 const uint16_t tile_id = tiles_or.value().front().id_;
1928 const size_t block_index =
static_cast<size_t>(tile_id / 64);
1929 const auto blocks = room.blocks();
1930 if (block_index < blocks.size()) {
1931 sheet_id = blocks[block_index];
1932 resolved_sheet =
true;
1936 const int tiles_per_sheet = tiles_per_row * tiles_per_col;
1937 if (tiles_per_sheet > 0) {
1938 tile_index =
static_cast<uint16_t
>(tile_id % tiles_per_sheet);
1943 if (
auto* editor_set = editor_manager->GetCurrentEditorSet()) {
1944 if (
auto* graphics = editor_set->GetGraphicsEditor()) {
1945 if (resolved_sheet) {
1946 graphics->SelectSheet(sheet_id);
1947 graphics->HighlightTile(sheet_id, tile_index,
1948 absl::StrFormat(
"Object 0x%02X",
object.id_));
1961 if (new_room_id < 0 || new_room_id >=
static_cast<int>(
rooms_.
size())) {
1979 int swap_index = -1;
1987 if (swap_index < 0) {
2007 slot_id = it->second;
2024 std::string old_card_id = absl::StrFormat(
"dungeon.room_%d", old_room_id);
2025 const bool old_pinned =
2031 std::string new_room_name = absl::StrFormat(
2034 std::string new_card_id = absl::StrFormat(
"dungeon.room_%d", new_room_id);
2037 {.card_id = new_card_id,
2038 .display_name = new_room_name,
2041 .category =
"Dungeon",
2042 .shortcut_hint =
"",
2043 .visibility_flag =
nullptr,
2044 .priority = 200 + new_room_id});
2086 room_viewers_.SetEvictionPredicate([
this](
const int& candidate) {
2096 auto* viewer_ptr = existing->get();
2100 std::string card_id = absl::StrFormat(
"dungeon.room_%d", room_id);
2103 viewer_ptr->
SetPinCallback([
this, card_id, room_id](
bool pinned) {
2105 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2106 if (auto* v = GetViewerForRoom(room_id)) {
2107 v->SetPinned(pinned);
2118 auto viewer = std::make_unique<DungeonCanvasViewer>(rom_);
2119 viewer->SetCompactHeaderMode(
false);
2120 viewer->SetRoomDetailsExpanded(
true);
2123 viewer->SetRenderer(renderer_);
2124 viewer->SetCurrentPaletteGroup(current_palette_group_);
2125 viewer->SetCurrentPaletteId(current_palette_id_);
2126 viewer->SetGameData(game_data_);
2132 viewer->object_interaction().SetMutationCallback([
this, viewer_ptr]() {
2134 if (rid >= 0 && rid <
static_cast<int>(rooms_.size())) {
2138 BeginUndoSnapshot(rid);
2140 BeginCollisionUndoSnapshot(rid);
2142 BeginWaterFillUndoSnapshot(rid);
2147 viewer->object_interaction().SetCacheInvalidationCallback([
this,
2150 if (rid >= 0 && rid <
static_cast<int>(rooms_.size())) {
2154 rooms_[rid].MarkObjectsDirty();
2155 rooms_[rid].RenderRoomGraphics();
2161 FinalizeUndoAction(rid);
2171 FinalizeCollisionUndoAction(rid);
2180 FinalizeWaterFillUndoAction(rid);
2185 viewer->object_interaction().SetObjectPlacedCallback(
2186 [
this](
const zelda3::RoomObject& obj) { HandleObjectPlaced(obj); });
2188 if (dungeon_editor_system_) {
2189 viewer->SetEditorSystem(dungeon_editor_system_.get());
2191 viewer->SetRoomNavigationCallback([
this](
int target_room) {
2192 if (target_room >= 0 && target_room <
static_cast<int>(rooms_.size())) {
2193 OnRoomSelected(target_room);
2197 viewer->SetRoomSwapCallback([
this](
int old_room,
int new_room) {
2198 SwapRoomInPanel(old_room, new_room);
2200 viewer->SetShowObjectPanelCallback(
2201 [
this]() { OpenWindow(kObjectSelectorId); });
2202 viewer->SetShowSpritePanelCallback(
2203 [
this]() { OpenWindow(
"dungeon.sprite_editor"); });
2204 viewer->SetShowItemPanelCallback(
2205 [
this]() { OpenWindow(
"dungeon.item_editor"); });
2206 viewer->SetShowRoomListCallback([
this]() {
2207 OpenWindow(IsWorkbenchWorkflowEnabled() ?
"dungeon.workbench"
2210 viewer->SetShowRoomMatrixCallback([
this]() { OpenWindow(kRoomMatrixId); });
2211 viewer->SetShowEntranceListCallback(
2212 [
this]() { OpenWindow(kEntranceListId); });
2213 viewer->SetShowRoomGraphicsCallback(
2214 [
this]() { OpenWindow(kRoomGraphicsId); });
2215 viewer->SetShowDungeonSettingsCallback(
2216 [
this]() { OpenWindow(
"dungeon.settings"); });
2217 viewer->SetEditGraphicsCallback(
2218 [
this](
int target_room_id,
const zelda3::RoomObject&
object) {
2219 OpenGraphicsEditorForObject(target_room_id,
object);
2221 viewer->SetSaveRoomCallback([
this](
int target_room_id) {
2222 auto status = SaveRoom(target_room_id);
2224 LOG_ERROR(
"DungeonEditorV2",
"Save Room failed: %s",
2225 status.message().data());
2226 if (dependencies_.toast_manager) {
2227 dependencies_.toast_manager->Show(
2228 absl::StrFormat(
"Save Room failed: %s", status.message()),
2233 if (dependencies_.toast_manager) {
2239 if (dependencies_.window_manager) {
2240 std::string card_id = absl::StrFormat(
"dungeon.room_%d", room_id);
2241 viewer->SetPinned(dependencies_.window_manager->IsWindowPinned(card_id));
2242 viewer->SetPinCallback([
this, card_id, room_id](
bool pinned) {
2243 if (dependencies_.window_manager) {
2244 dependencies_.window_manager->SetWindowPinned(card_id, pinned);
2246 if (auto* v = GetViewerForRoom(room_id)) {
2247 v->SetPinned(pinned);
2253 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2254 viewer->SetProject(dependencies_.project);
2256 auto* stored = room_viewers_.Insert(room_id, std::move(viewer));
2257 return stored->get();
2262 if (!workbench_viewer_) {
2263 workbench_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2264 auto* viewer = workbench_viewer_.get();
2265 viewer->SetCompactHeaderMode(
true);
2266 viewer->SetRoomDetailsExpanded(
false);
2267 viewer->SetHeaderVisible(
false);
2268 viewer->SetHeaderHiddenMetadataHudVisible(
false);
2269 viewer->SetRooms(&rooms_);
2270 viewer->SetRenderer(renderer_);
2271 viewer->SetCurrentPaletteGroup(current_palette_group_);
2272 viewer->SetCurrentPaletteId(current_palette_id_);
2273 viewer->SetGameData(game_data_);
2277 viewer->object_interaction().SetMutationCallback([
this, viewer]() {
2278 const int rid = viewer ? viewer->current_room_id() : -1;
2279 if (rid >= 0 && rid <
static_cast<int>(rooms_.size())) {
2280 const auto domain = viewer->object_interaction().last_mutation_domain();
2281 if (domain == MutationDomain::kTileObjects) {
2282 BeginUndoSnapshot(rid);
2283 }
else if (domain == MutationDomain::kCustomCollision) {
2284 BeginCollisionUndoSnapshot(rid);
2285 }
else if (domain == MutationDomain::kWaterFill) {
2286 BeginWaterFillUndoSnapshot(rid);
2290 viewer->object_interaction().SetCacheInvalidationCallback([
this, viewer]() {
2291 const int rid = viewer ? viewer->current_room_id() : -1;
2292 if (rid >= 0 && rid <
static_cast<int>(rooms_.size())) {
2294 viewer->object_interaction().last_invalidation_domain();
2295 if (domain == MutationDomain::kTileObjects) {
2296 rooms_[rid].MarkObjectsDirty();
2297 rooms_[rid].RenderRoomGraphics();
2299 viewer->object_interaction().mode_manager().GetMode();
2300 if (mode != InteractionMode::DraggingObjects) {
2301 FinalizeUndoAction(rid);
2303 }
else if (domain == MutationDomain::kCustomCollision) {
2305 viewer->object_interaction().mode_manager().GetMode();
2307 viewer->object_interaction().mode_manager().GetModeState();
2308 if (mode == InteractionMode::PaintCollision && st.is_painting) {
2311 FinalizeCollisionUndoAction(rid);
2312 }
else if (domain == MutationDomain::kWaterFill) {
2314 viewer->object_interaction().mode_manager().GetMode();
2316 viewer->object_interaction().mode_manager().GetModeState();
2317 if (mode == InteractionMode::PaintWaterFill && st.is_painting) {
2320 FinalizeWaterFillUndoAction(rid);
2325 viewer->object_interaction().SetObjectPlacedCallback(
2328 if (dungeon_editor_system_) {
2329 viewer->SetEditorSystem(dungeon_editor_system_.get());
2334 viewer->SetRoomSwapCallback([
this](
int ,
int new_room) {
2335 OnRoomSelected(new_room,
false);
2337 viewer->SetRoomNavigationCallback([
this](
int target_room) {
2338 OnRoomSelected(target_room,
false);
2341 viewer->SetShowObjectPanelCallback(
2342 [
this]() { OpenWindow(kObjectSelectorId); });
2343 viewer->SetShowSpritePanelCallback(
2344 [
this]() { OpenWindow(
"dungeon.sprite_editor"); });
2345 viewer->SetShowItemPanelCallback(
2346 [
this]() { OpenWindow(
"dungeon.item_editor"); });
2347 viewer->SetShowRoomListCallback([
this]() {
2348 OpenWindow(IsWorkbenchWorkflowEnabled() ?
"dungeon.workbench"
2351 viewer->SetShowRoomMatrixCallback([
this]() { OpenWindow(kRoomMatrixId); });
2352 viewer->SetShowEntranceListCallback(
2353 [
this]() { OpenWindow(kEntranceListId); });
2354 viewer->SetShowRoomGraphicsCallback(
2355 [
this]() { OpenWindow(kRoomGraphicsId); });
2356 viewer->SetShowDungeonSettingsCallback(
2357 [
this]() { OpenWindow(
"dungeon.settings"); });
2358 viewer->SetEditGraphicsCallback(
2360 OpenGraphicsEditorForObject(target_room_id,
object);
2362 viewer->SetSaveRoomCallback([
this](
int target_room_id) {
2363 auto status = SaveRoom(target_room_id);
2365 LOG_ERROR(
"DungeonEditorV2",
"Save Room failed: %s",
2366 status.message().data());
2367 if (dependencies_.toast_manager) {
2368 dependencies_.toast_manager->Show(
2369 absl::StrFormat(
"Save Room failed: %s", status.message()),
2374 if (dependencies_.toast_manager) {
2375 dependencies_.toast_manager->Show(
"Room saved", ToastType::kSuccess);
2379 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2380 viewer->SetProject(dependencies_.project);
2383 return workbench_viewer_.get();
2387 if (!workbench_compare_viewer_) {
2388 workbench_compare_viewer_ = std::make_unique<DungeonCanvasViewer>(rom_);
2389 auto* viewer = workbench_compare_viewer_.get();
2390 viewer->SetCompactHeaderMode(
true);
2391 viewer->SetRoomDetailsExpanded(
false);
2392 viewer->SetHeaderHiddenMetadataHudVisible(
false);
2393 viewer->SetRooms(&rooms_);
2394 viewer->SetRenderer(renderer_);
2395 viewer->SetCurrentPaletteGroup(current_palette_group_);
2396 viewer->SetCurrentPaletteId(current_palette_id_);
2397 viewer->SetGameData(game_data_);
2401 viewer->SetObjectInteractionEnabled(
false);
2402 viewer->SetHeaderReadOnly(
true);
2403 viewer->SetHeaderVisible(
false);
2405 if (dungeon_editor_system_) {
2408 viewer->SetEditorSystem(dungeon_editor_system_.get());
2411 viewer->SetMinecartTrackPanel(minecart_track_editor_panel_);
2412 viewer->SetProject(dependencies_.project);
2415 return workbench_compare_viewer_.get();
2418absl::Status DungeonEditorV2::Undo() {
2420 if (pending_undo_.room_id >= 0) {
2421 FinalizeUndoAction(pending_undo_.room_id);
2423 if (pending_collision_undo_.room_id >= 0) {
2424 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2426 if (pending_water_fill_undo_.room_id >= 0) {
2427 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2429 return undo_manager_.Undo();
2432absl::Status DungeonEditorV2::Redo() {
2434 if (pending_undo_.room_id >= 0) {
2435 FinalizeUndoAction(pending_undo_.room_id);
2437 if (pending_collision_undo_.room_id >= 0) {
2438 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2440 if (pending_water_fill_undo_.room_id >= 0) {
2441 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2443 return undo_manager_.Redo();
2446absl::Status DungeonEditorV2::Cut() {
2447 if (
auto* viewer = GetViewerForRoom(current_room_id_)) {
2448 viewer->object_interaction().HandleCopySelected();
2449 viewer->object_interaction().HandleDeleteSelected();
2451 return absl::OkStatus();
2454absl::Status DungeonEditorV2::Copy() {
2455 if (
auto* viewer = GetViewerForRoom(current_room_id_)) {
2456 viewer->object_interaction().HandleCopySelected();
2458 return absl::OkStatus();
2461absl::Status DungeonEditorV2::Paste() {
2462 if (
auto* viewer = GetViewerForRoom(current_room_id_)) {
2463 viewer->object_interaction().HandlePasteObjects();
2465 return absl::OkStatus();
2468void DungeonEditorV2::BeginUndoSnapshot(
int room_id) {
2469 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2473 if (has_pending_undo_) {
2475 "BeginUndoSnapshot called twice without FinalizeUndoAction. "
2476 "Previous snapshot for room %d is being leaked. Finalizing now.",
2477 pending_undo_.room_id);
2479 if (pending_undo_.room_id >= 0) {
2480 FinalizeUndoAction(pending_undo_.room_id);
2484 pending_undo_.room_id = room_id;
2485 pending_undo_.before_objects = rooms_[room_id].GetTileObjects();
2486 has_pending_undo_ =
true;
2489void DungeonEditorV2::FinalizeUndoAction(
int room_id) {
2490 if (pending_undo_.room_id < 0 || pending_undo_.room_id != room_id)
2492 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2495 auto after_objects = rooms_[room_id].GetTileObjects();
2497 auto action = std::make_unique<DungeonObjectsAction>(
2498 room_id, std::move(pending_undo_.before_objects),
2499 std::move(after_objects),
2500 [
this](
int rid,
const std::vector<zelda3::RoomObject>& objects) {
2501 RestoreRoomObjects(rid, objects);
2503 undo_manager_.Push(std::move(action));
2505 pending_undo_.room_id = -1;
2506 pending_undo_.before_objects.clear();
2507 has_pending_undo_ =
false;
2510void DungeonEditorV2::SyncPanelsToRoom(
int room_id) {
2512 if (object_selector_panel_) {
2513 object_selector_panel_->SetCanvasViewerProvider([
this]() {
2514 return GetViewerForRoom(current_room_id_);
2516 object_selector_panel_->SetCurrentRoom(room_id);
2517 object_selector_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2519 if (object_editor_content_) {
2520 object_editor_content_->SetCanvasViewerProvider([
this]() {
2521 return GetViewerForRoom(current_room_id_);
2523 object_editor_content_->SetCurrentRoom(room_id);
2524 object_editor_content_->SetCanvasViewer(GetViewerForRoom(room_id));
2526 if (door_editor_panel_) {
2527 door_editor_panel_->SetCanvasViewerProvider([
this]() {
2528 return GetViewerForRoom(current_room_id_);
2530 door_editor_panel_->SetCurrentRoom(room_id);
2531 door_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2535 if (sprite_editor_panel_) {
2536 sprite_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2538 if (item_editor_panel_) {
2539 item_editor_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2541 if (custom_collision_panel_) {
2542 auto* viewer = GetViewerForRoom(room_id);
2543 custom_collision_panel_->SetCanvasViewer(viewer);
2545 custom_collision_panel_->SetInteraction(&viewer->object_interaction());
2548 if (water_fill_panel_) {
2549 auto* viewer = GetViewerForRoom(room_id);
2550 water_fill_panel_->SetCanvasViewer(viewer);
2552 water_fill_panel_->SetInteraction(&viewer->object_interaction());
2556 if (dungeon_settings_panel_) {
2557 dungeon_settings_panel_->SetCanvasViewer(GetViewerForRoom(room_id));
2560 if (object_tile_editor_panel_) {
2561 object_tile_editor_panel_->SetCurrentPaletteGroup(current_palette_group_);
2564 if (room_tag_editor_panel_) {
2565 room_tag_editor_panel_->SetCurrentRoomId(room_id);
2568 if (overlay_manager_panel_) {
2569 auto* viewer = GetViewerForRoom(room_id);
2572 overlay_state.
show_grid = viewer->mutable_show_grid();
2575 viewer->mutable_show_coordinate_overlay();
2577 viewer->mutable_show_room_debug_info();
2581 viewer->mutable_show_minecart_tracks();
2583 viewer->mutable_show_custom_collision_overlay();
2585 viewer->mutable_show_track_collision_overlay();
2587 viewer->mutable_show_camera_quadrant_overlay();
2589 viewer->mutable_show_minecart_sprite_overlay();
2591 viewer->mutable_show_track_collision_legend();
2592 overlay_manager_panel_->SetState(overlay_state);
2597void DungeonEditorV2::ShowRoomPanel(
int room_id) {
2598 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size())) {
2602 bool already_active =
false;
2603 for (
int i = 0; i < active_rooms_.Size; ++i) {
2604 if (active_rooms_[i] == room_id) {
2605 already_active =
true;
2609 if (!already_active) {
2610 active_rooms_.push_back(room_id);
2611 room_selector_.set_active_rooms(active_rooms_);
2614 std::string card_id = absl::StrFormat(
"dungeon.room_%d", room_id);
2616 if (dependencies_.window_manager) {
2617 if (!dependencies_.window_manager->GetWindowDescriptor(
2618 dependencies_.window_manager->GetActiveSessionId(), card_id)) {
2619 std::string room_name = absl::StrFormat(
2621 dependencies_.window_manager->RegisterWindow(
2622 {.card_id = card_id,
2623 .display_name = room_name,
2626 .category =
"Dungeon",
2627 .shortcut_hint =
"",
2628 .visibility_flag =
nullptr,
2629 .priority = 200 + room_id});
2631 dependencies_.window_manager->OpenWindow(card_id);
2635 if (room_cards_.find(room_id) == room_cards_.end()) {
2636 std::string base_name = absl::StrFormat(
2638 const int slot_id = GetOrCreateRoomPanelSlotId(room_id);
2639 std::string card_name_str = absl::StrFormat(
2640 "%s###RoomPanelSlot%d",
MakePanelTitle(base_name).c_str(), slot_id);
2642 auto card = std::make_shared<gui::PanelWindow>(card_name_str.c_str(),
2644 card->SetDefaultSize(620, 700);
2645 room_cards_[room_id] = card;
2649void DungeonEditorV2::RestoreRoomObjects(
2650 int room_id,
const std::vector<zelda3::RoomObject>& objects) {
2651 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2654 auto& room = rooms_[room_id];
2655 room.SetTileObjects(objects);
2656 room.RenderRoomGraphics();
2659void DungeonEditorV2::BeginCollisionUndoSnapshot(
int room_id) {
2660 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2663 if (pending_collision_undo_.room_id >= 0) {
2664 FinalizeCollisionUndoAction(pending_collision_undo_.room_id);
2667 pending_collision_undo_.room_id = room_id;
2668 pending_collision_undo_.before = rooms_[room_id].custom_collision();
2671void DungeonEditorV2::FinalizeCollisionUndoAction(
int room_id) {
2672 if (pending_collision_undo_.room_id < 0 ||
2673 pending_collision_undo_.room_id != room_id) {
2676 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2679 auto after = rooms_[room_id].custom_collision();
2680 if (pending_collision_undo_.before.has_data == after.has_data &&
2681 pending_collision_undo_.before.tiles == after.tiles) {
2682 pending_collision_undo_.room_id = -1;
2683 pending_collision_undo_.before = {};
2687 auto action = std::make_unique<DungeonCustomCollisionAction>(
2688 room_id, std::move(pending_collision_undo_.before), std::move(after),
2690 RestoreRoomCustomCollision(rid, map);
2692 undo_manager_.Push(std::move(action));
2694 pending_collision_undo_.room_id = -1;
2695 pending_collision_undo_.before = {};
2698void DungeonEditorV2::RestoreRoomCustomCollision(
2700 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2703 auto& room = rooms_[room_id];
2704 room.custom_collision() = map;
2705 room.MarkCustomCollisionDirty();
2716 for (
size_t i = 0; i < zone.tiles.size(); ++i) {
2717 if (zone.tiles[i] != 0) {
2718 snap.
offsets.push_back(
static_cast<uint16_t
>(i));
2726void DungeonEditorV2::BeginWaterFillUndoSnapshot(
int room_id) {
2727 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2730 if (pending_water_fill_undo_.room_id >= 0) {
2731 FinalizeWaterFillUndoAction(pending_water_fill_undo_.room_id);
2734 pending_water_fill_undo_.room_id = room_id;
2735 pending_water_fill_undo_.before = MakeWaterFillSnapshot(rooms_[room_id]);
2738void DungeonEditorV2::FinalizeWaterFillUndoAction(
int room_id) {
2739 if (pending_water_fill_undo_.room_id < 0 ||
2740 pending_water_fill_undo_.room_id != room_id) {
2743 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2746 auto after = MakeWaterFillSnapshot(rooms_[room_id]);
2747 if (pending_water_fill_undo_.before.sram_bit_mask == after.sram_bit_mask &&
2748 pending_water_fill_undo_.before.offsets == after.offsets) {
2749 pending_water_fill_undo_.room_id = -1;
2750 pending_water_fill_undo_.before = {};
2754 auto action = std::make_unique<DungeonWaterFillAction>(
2755 room_id, std::move(pending_water_fill_undo_.before), std::move(after),
2757 RestoreRoomWaterFill(rid, snap);
2759 undo_manager_.Push(std::move(action));
2761 pending_water_fill_undo_.room_id = -1;
2762 pending_water_fill_undo_.before = {};
2765void DungeonEditorV2::RestoreRoomWaterFill(
int room_id,
2767 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size()))
2770 auto& room = rooms_[room_id];
2771 room.ClearWaterFillZone();
2773 for (uint16_t off : snap.
offsets) {
2774 const int x =
static_cast<int>(off % 64);
2775 const int y =
static_cast<int>(off / 64);
2776 room.SetWaterFillTile(x, y,
true);
2778 room.MarkWaterFillDirty();
void Publish(const T &event)
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
bool loaded() const
Check if the manifest has been loaded.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
int current_room_id() const
void SetPinned(bool pinned)
DungeonObjectInteraction & object_interaction()
void SetRooms(DungeonRoomStore *rooms)
void SetPinCallback(std::function< void(bool)> callback)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
int GetOrCreateRoomPanelSlotId(int room_id)
std::deque< int > recent_rooms_
absl::Status SaveRoom(int room_id)
void SetAgentMode(bool enabled)
class CustomCollisionPanel * custom_collision_panel_
uint64_t current_palette_group_id_
void ToggleWorkbenchWorkflowMode(bool show_toast=true)
void add_room(int room_id)
void ContributeStatus(StatusBar *status_bar) override
void OpenGraphicsEditorForObject(int room_id, const zelda3::RoomObject &object)
class ItemEditorPanel * item_editor_panel_
absl::Status SaveRoomData(int room_id)
void ShowRoomPanel(int room_id)
int next_room_panel_slot_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
absl::Status Save() override
gfx::IRenderer * renderer_
void TouchViewerLru(int room_id)
class SpriteEditorPanel * sprite_editor_panel_
void FocusRoom(int room_id)
void HandleObjectPlaced(const zelda3::RoomObject &obj)
class DungeonSettingsPanel * dungeon_settings_panel_
std::unique_ptr< zelda3::DungeonEditorSystem > dungeon_editor_system_
void OpenWindow(const std::string &window_id)
void SyncPanelsToRoom(int room_id)
DoorEditorContent * door_editor_panel_
void Initialize() override
std::unordered_map< int, int > room_panel_slot_ids_
class DungeonWorkbenchContent * workbench_panel_
bool IsWorkbenchWorkflowEnabled() const
void OnRoomSelected(int room_id, bool request_focus=true)
~DungeonEditorV2() override
ObjectTileEditorPanel * object_tile_editor_panel_
ObjectSelectorContent * object_selector_panel_
OverlayManagerPanel * overlay_manager_panel_
void ProcessDeferredTextures()
gfx::SnesPalette current_palette_
ImVector< int > active_rooms_
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
gui::PaletteEditorWidget palette_editor_
std::unique_ptr< ObjectEditorContent > owned_object_editor_content_
std::unique_ptr< DoorEditorContent > owned_door_editor_panel_
static constexpr const char * kObjectEditorId
static bool IsValidRoomId(int room_id)
void SwapRoomInPanel(int old_room_id, int new_room_id)
RoomGraphicsContent * room_graphics_panel_
static constexpr size_t kMaxRecentRooms
void DrawRoomTab(int room_id)
void SetWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
class RoomTagEditorPanel * room_tag_editor_panel_
void ProcessPendingSwap()
std::unique_ptr< ObjectSelectorContent > owned_object_selector_panel_
PendingSwap pending_swap_
static constexpr const char * kDoorEditorId
void ProcessPendingWorkflowMode()
DungeonCanvasViewer * GetViewerForRoom(int room_id)
absl::Status Update() override
class WaterFillPanel * water_fill_panel_
absl::Status Undo() override
int LoadedRoomCount() const
ImGuiWindowClass room_window_class_
void SelectObject(int obj_id)
DungeonRoomLoader room_loader_
DungeonCanvasViewer * GetWorkbenchViewer()
std::unordered_map< int, std::shared_ptr< gui::PanelWindow > > room_cards_
absl::Status Redo() override
void RemoveViewerFromLru(int room_id)
static constexpr const char * kObjectSelectorId
static constexpr const char * kRoomGraphicsId
void QueueWorkbenchWorkflowMode(bool enabled, bool show_toast=true)
ObjectEditorContent * object_editor_content_
static constexpr const char * kRoomMatrixId
static constexpr const char * kRoomSelectorId
DungeonRoomSelector room_selector_
zelda3::GameData * game_data_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
void ReleaseRoomPanelSlotId(int room_id)
absl::Status Load() override
int current_room_id() const
std::vector< std::pair< uint32_t, uint32_t > > CollectWriteRanges() const
int TotalRoomCount() const
static constexpr const char * kPaletteEditorId
PendingWorkflowMode pending_workflow_mode_
DungeonCanvasViewer * GetWorkbenchCompareViewer()
uint64_t current_palette_id_
MutationDomain last_mutation_domain() const
InteractionModeManager & mode_manager()
MutationDomain last_invalidation_domain() const
void SetCustomObjectsFolder(const std::string &folder)
void SetProject(project::YazeProject *project)
void InvalidatePreviewCache()
void SetTileEditorPanel(ObjectTileEditorPanel *panel)
absl::Status LoadRoomEntrances(std::array< zelda3::RoomEntrance, 0x8C > &entrances)
absl::Status LoadRoom(int room_id, zelda3::Room &room)
void set_entrances(std::array< zelda3::RoomEntrance, 0x8C > *entrances)
void SetRoomSelectedWithIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
void set_current_room_id(uint16_t room_id)
void set_rooms(DungeonRoomStore *rooms)
void SetRoomSelectedCallback(std::function< void(int)> callback)
void set_active_rooms(const ImVector< int > &rooms)
zelda3::Room * GetIfMaterialized(int room_id)
void ForEachMaterialized(Fn &&fn)
void ForEachLoaded(Fn &&fn)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetUndoRedoProvider(std::function< bool()> can_undo, std::function< bool()> can_redo, std::function< void()> on_undo, std::function< void()> on_redo, std::function< std::string()> undo_desc, std::function< std::string()> redo_desc, std::function< int()> undo_depth)
void NotifyRoomChanged(int previous_room_id)
Called by the editor when the current room changes.
The EditorManager controls the main editor window and manages the various editor classes.
UndoManager undo_manager_
zelda3::GameData * game_data() const
EditorDependencies dependencies_
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 SetRooms(DungeonRoomStore *rooms)
void SetRoomNavigationCallback(RoomNavigationCallback callback)
void SetProjectRoot(const std::string &root)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetPlacementError(const std::string &message)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetGameData(zelda3::GameData *game_data)
DungeonObjectSelector & object_selector()
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SelectObject(int obj_id)
void SetAgentOptimizedLayout(bool enabled)
void OpenForObject(int16_t object_id, int room_id, DungeonRoomStore *rooms)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
Set the current palette group for graphics rendering.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
A session-aware status bar displayed at the bottom of the application.
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
void SetEditorMode(const std::string &mode)
Set the current editor mode or tool.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
std::string GetRedoDescription() const
Description of the action that would be redone (for UI)
std::string GetUndoDescription() const
Description of the action that would be undone (for UI)
size_t UndoStackSize() const
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
size_t GetActiveSessionId() const
void SetWindowPinned(size_t session_id, const std::string &base_window_id, bool pinned)
std::string GetActiveCategory() const
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
void RegisterWindow(size_t session_id, const WindowDescriptor &descriptor)
bool IsWindowOpen(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
bool IsWindowPinned(size_t session_id, const std::string &base_window_id) const
void ShowAllWindowsInCategory(size_t session_id, const std::string &category)
void UnregisterWindow(size_t session_id, const std::string &base_window_id)
void RegisterPanelAlias(const std::string &legacy_base_id, const std::string &canonical_base_id)
Register a legacy panel ID alias that resolves to a canonical ID.
static std::shared_ptr< MesenSocketClient > & GetClient()
void ProcessTextureQueue(IRenderer *renderer)
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.
Draggable, dockable panel for editor sub-windows.
bool Begin(bool *p_open=nullptr)
void SetDefaultSize(float width, float height)
static CustomObjectManager & Get()
void AddObjectFile(int object_id, const std::string &filename)
void Initialize(const std::string &custom_objects_folder)
ValidationResult ValidateRoom(const Room &room)
static ObjectDimensionTable & Get()
void set_water_fill_sram_bit_mask(uint8_t mask)
bool custom_collision_dirty() const
const WaterFillZoneMap & water_fill_zone() const
int WaterFillTileCount() const
void ClearWaterFillZone()
bool has_water_fill_zone() const
std::vector< uint8_t > EncodeObjects() const
void ClearWaterFillDirty()
bool water_fill_dirty() const
uint8_t water_fill_sram_bit_mask() const
void SetWaterFillTile(int x, int y, bool filled)
#define ICON_MD_ROCKET_LAUNCH
#define ICON_MD_GRID_VIEW
#define ICON_MD_DOOR_FRONT
#define ICON_MD_WORKSPACES
#define LOG_DEBUG(category, format,...)
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
const AgentUITheme & GetTheme()
WaterFillSnapshot MakeWaterFillSnapshot(const zelda3::Room &room)
absl::Status SaveWaterFillZones(Rom *rom, DungeonRoomStore &rooms)
Editors are the view controllers for the application.
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
RoomSelectionIntent
Intent for room selection in the dungeon editor.
absl::StatusOr< PaletteGroup > CreatePaletteGroupFromLargePalette(SnesPalette &palette, int num_colors)
Create a PaletteGroup by dividing a large palette into sub-palettes.
constexpr int kTilesheetHeight
constexpr int kTilesheetWidth
std::string MakePanelTitle(const std::string &title)
absl::Status SaveAllChests(Rom *rom, absl::Span< const Room > rooms)
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)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadLegacyWaterGateZones(Rom *rom, const std::string &symbol_path)
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
absl::Status SaveAllPits(Rom *rom)
absl::Status SaveAllBlocks(Rom *rom)
constexpr int kCustomCollisionDataPosition
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
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
#define RETURN_IF_ERROR(expr)
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
project::YazeProject * project
GlobalEditorContext * global_context
ToastManager * toast_manager
gfx::IRenderer * renderer
WorkspaceWindowManager * window_manager
static JumpToRoomRequestEvent Create(int room, size_t session=0)
bool * show_room_debug_info
bool * show_coordinate_overlay
bool * show_camera_quadrants
bool * show_custom_collision
bool * show_minecart_sprites
bool * show_minecart_tracks
bool * show_texture_debug
bool * show_collision_legend
bool * show_track_collision
bool * show_object_bounds
Optional behavior for an interactive status bar segment.
std::function< void()> on_click
std::vector< uint16_t > offsets
PaletteGroup dungeon_main
std::unordered_map< int, std::vector< std::string > > custom_object_files
std::string custom_objects_folder
core::HackManifest hack_manifest
std::string GetAbsolutePath(const std::string &relative_path) const
std::string symbols_filename
gfx::PaletteGroupMap palette_groups
std::vector< uint16_t > fill_offsets
std::array< uint8_t, 64 *64 > tiles