14#include "absl/strings/str_format.h"
41#include "imgui/imgui.h"
59 if (object_id < 0x100)
61 if (object_id < 0x200)
63 if (object_id >= 0xF80)
69 switch (layer_value) {
82 switch (layer_value) {
84 return "Upper layer (BG1)";
86 return "Lower layer (BG2)";
93 static constexpr const char* kNames[] = {
94 "Off",
"Parallax",
"Dark",
"On top",
"Translucent",
95 "Addition",
"Normal",
"Transparent",
"Dark room"};
96 constexpr int kNameCount =
sizeof(kNames) /
sizeof(kNames[0]);
97 return (value >= 0 && value < kNameCount) ? kNames[value] :
"Unknown";
101 static constexpr const char* kNames[] = {
"One",
"Both",
"Both + Scroll",
102 "Moving Floor",
"Moving Water"};
103 constexpr int kNameCount =
sizeof(kNames) /
sizeof(kNames[0]);
104 return (value >= 0 && value < kNameCount) ? kNames[value] :
"Unknown";
109 static const char* kNames[] = {
110 "Nothing",
"Green Rupee",
"Rock",
"Bee",
111 "Heart (4)",
"Bomb (4)",
"Heart",
"Blue Rupee",
112 "Key",
"Arrow (5)",
"Bomb (1)",
"Heart",
113 "Magic (Small)",
"Full Magic",
"Cucco",
"Green Soldier",
114 "Bush Stal",
"Blue Soldier",
"Landmine",
"Heart",
115 "Fairy",
"Heart",
"Nothing (22)",
"Hole",
116 "Warp",
"Staircase",
"Bombable",
"Switch",
118 constexpr size_t kCount =
sizeof(kNames) /
sizeof(kNames[0]);
119 return item < kCount ? kNames[item] :
"Unknown";
124 return std::clamp(desired_width, min_width, std::max(min_width, max_width));
143 const ImVec2 min = ImGui::GetItemRectMin();
144 const ImVec2 max = ImGui::GetItemRectMax();
155 return compact && detail_requested;
159 float total_width,
float min_canvas_width,
float min_sidebar_width,
160 float splitter_width,
bool want_left,
bool want_right) {
165 auto required_width = [&](
bool left,
bool right,
bool compact_left,
166 bool compact_right) {
167 float required = min_canvas_width;
169 left ? (compact_left ? GetCompactSidebarWidth(
false, min_sidebar_width)
173 right ? (compact_right ? GetCompactSidebarWidth(
true, min_sidebar_width)
177 required += splitter_width;
180 required += splitter_width;
212 float total_width,
float min_canvas_width,
float min_sidebar_width,
213 float splitter_width,
float stored_left_width,
float stored_right_width,
214 bool want_left,
bool want_right) {
217 total_width, min_canvas_width, min_sidebar_width, splitter_width,
218 want_left, want_right);
220 const float compact_left_width =
221 GetCompactSidebarWidth(
false, min_sidebar_width);
222 const float compact_right_width =
223 GetCompactSidebarWidth(
true, min_sidebar_width);
236 : stored_right_width)
239 const float max_left_width =
240 total_width - right_width - min_canvas_width -
243 const float max_right_width =
244 total_width - left_width - min_canvas_width -
248 left_width = ClampWorkbenchPaneWidth(
255 right_width = ClampWorkbenchPaneWidth(
262 float center_width = total_width - left_width - right_width;
264 center_width -= splitter_width;
267 center_width -= splitter_width;
269 if (center_width < min_canvas_width) {
270 float deficit = min_canvas_width - center_width;
274 left_width -= shrink;
280 right_width -= shrink;
283 center_width = std::max(
284 1.0f, total_width - left_width - right_width -
297 std::function<
void(
int)> on_room_selected,
299 std::function<
void(
int)> on_save_room,
300 std::function<
void()> on_save_all_rooms,
303 std::function<
const std::deque<int>&()> get_recent_rooms,
304 std::function<
void(
int)> forget_recent_room,
305 std::function<
void(
bool)> set_workflow_mode,
Rom* rom)
306 : room_selector_(room_selector),
307 current_room_id_(current_room_id),
308 on_room_selected_(std::move(on_room_selected)),
309 on_room_selected_with_intent_(std::move(on_room_selected_with_intent)),
310 on_save_room_(std::move(on_save_room)),
311 on_save_all_rooms_(std::move(on_save_all_rooms)),
312 get_viewer_(std::move(get_viewer)),
313 get_compare_viewer_(std::move(get_compare_viewer)),
314 get_recent_rooms_(std::move(get_recent_rooms)),
315 forget_recent_room_(std::move(forget_recent_room)),
316 set_workflow_mode_(std::move(set_workflow_mode)),
322 return "dungeon.workbench";
325 return "Dungeon Workbench";
451 float button_size,
bool compact) {
452 const bool sidebar_open = ImGui::BeginChild(
"##DungeonWorkbenchSidebar",
453 ImVec2(width, height),
true);
463 const bool can_open_overview =
true;
464 const float collapse_w =
469 const float spacing = ImGui::GetStyle().ItemSpacing.x;
470 const float header_width = ImGui::GetContentRegionAvail().x;
471 const bool stack_mode_switch = compact && header_width < 220.0f;
472 const float action_cluster_w =
473 collapse_w + (can_open_overview ? (spacing + menu_w) : 0.0f);
477 "Browse",
nullptr, compact, action_cluster_w, [&]() {
478 if (can_open_overview) {
481 "Open room review tools",
true)) {
482 ImGui::OpenPopup(
"##WorkbenchSidebarQuickActions");
484 if (ImGui::BeginPopup(
"##WorkbenchSidebarQuickActions")) {
497 "Collapse navigation pane")) {
502 ImGui::Dummy(ImVec2(0.0f, 2.0f));
508 float segment_height) {
513 const float spacing = ImGui::GetStyle().ItemSpacing.x;
514 const ImVec2 button_size(segment_height, segment_height);
520 if (ImGui::IsItemHovered()) {
521 ImGui::SetTooltip(tr(
"Rooms"));
523 ImGui::SameLine(0.0f, spacing);
528 if (ImGui::IsItemHovered()) {
529 ImGui::SetTooltip(tr(
"Entrances"));
535 ImGui::TextDisabled(tr(
"Room navigation unavailable"));
539 ImGui::PushID(
"WorkbenchSidebarMode");
556 ImGui::TextDisabled(
ICON_MD_INFO " Load a ROM to edit dungeon rooms.");
560 ImGui::TextColored(theme.text_error_red, tr(
"Dungeon Workbench not wired"));
568 const float total_w = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
572 const float min_sidebar_w =
574 const float min_canvas_w = std::max(420.0f, min_sidebar_w + 96.0f);
577 total_w, min_canvas_w, min_sidebar_w, splitter_w,
614 const float total_h = std::max(ImGui::GetContentRegionAvail().y, 1.0f);
634 ImGui::SameLine(0.0f, 0.0f);
636 "##DungeonWorkbenchLeftSplitter", total_h,
638 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0.0f),
642 ImGui::SameLine(0.0f, 0.0f);
646 ImGui::SameLine(0.0f, 0.0f);
648 "##DungeonWorkbenchRightSplitter", total_h,
650 total_w - right_w - min_canvas_w -
651 (show_right ? splitter_w : 0.0f),
655 ImGui::SameLine(0.0f, 0.0f);
665 ImGui::SameLine(0.0f, 0.0f);
667 "##DungeonWorkbenchLeftSplitter", total_h,
669 total_w - right_w - min_canvas_w -
670 (show_right ? splitter_w : 0.0f),
677 ImGui::SameLine(0.0f, 0.0f);
682 ImGui::SameLine(0.0f, 0.0f);
684 "##DungeonWorkbenchRightSplitter", total_h,
686 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0.0f),
690 ImGui::SameLine(0.0f, 0.0f);
697 if (primary_viewer) {
704 bool left_sidebar_visible) {
705 const bool canvas_open = ImGui::BeginChild(
"##DungeonWorkbenchCanvas",
706 ImVec2(width, height),
false);
708 if (primary_viewer) {
709 const bool show_recent_tabs =
711 if (show_recent_tabs) {
723 const float status_bar_reserved =
725 ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
727 ImGui::GetStyle().ItemSpacing.y;
730 ImGuiWindowFlags body_flags = 0;
731 if (connected_mode) {
734 const ImVec2 content_size =
737 ImGui::SetNextWindowContentSize(
738 ImVec2(content_size.x * scale, content_size.y * scale));
739 body_flags |= ImGuiWindowFlags_HorizontalScrollbar |
740 ImGuiWindowFlags_NoScrollWithMouse;
742 const bool body_open = ImGui::BeginChild(
743 "##DungeonCanvasBody", ImVec2(0.0f, -status_bar_reserved),
false,
746 if (connected_mode) {
760 const char* tool_mode =
762 bool room_dirty =
false;
763 if (
auto* rooms = primary_viewer->
rooms();
766 room_dirty = room->HasUnsavedChanges();
774 status.workflow_primary =
true;
780 static std::string s_undo_desc;
782 status.undo_desc = s_undo_desc.empty() ? nullptr : s_undo_desc.c_str();
785 static std::string s_redo_desc;
787 status.redo_desc = s_redo_desc.empty() ? nullptr : s_redo_desc.c_str();
795 ImGui::TextDisabled(tr(
"No active viewer"));
805 const size_t object_count = interaction.GetSelectionCount();
806 const bool has_entity = interaction.HasEntitySelection();
807 if (object_count == 0 && !has_entity) {
815 ImGuiStyleVar_FramePadding,
816 ImVec2(std::max(4.0f, ImGui::GetStyle().FramePadding.x),
817 std::max(3.0f, ImGui::GetStyle().FramePadding.y - 1.0f)));
819 ImGuiStyleVar_ItemSpacing,
820 ImVec2(std::max(ImGui::GetStyle().ItemSpacing.x, 4.0f),
821 std::max(3.0f, ImGui::GetStyle().ItemSpacing.y - 1.0f)));
823 const float spacing = ImGui::GetStyle().ItemSpacing.x;
824 bool first_button =
true;
825 auto start_action = [&](
const char* label) {
826 const float button_width = ImGui::CalcTextSize(label).x +
827 (ImGui::GetStyle().FramePadding.x * 2.0f) + 8.0f;
829 const float next_x = ImGui::GetItemRectMax().x + spacing + button_width;
831 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
832 if (next_x <= max_x) {
833 ImGui::SameLine(0.0f, spacing);
836 first_button =
false;
839 auto draw_action = [&](
const char* label,
bool enabled,
840 const auto& on_press) {
843 ImGui::BeginDisabled();
849 ImGui::EndDisabled();
858 const char* local_tool_label =
nullptr;
860 if (object_count == 0 && has_entity) {
861 switch (selection.
type) {
879 const bool can_copy_selection = snapshot.
object_count > 0 ||
883 [&]() { interaction.HandleCopySelected(); });
885 [&]() { interaction.HandlePasteObjects(); });
887 [&]() { interaction.HandleDeleteSelected(); });
889 interaction.ClearSelection();
890 interaction.ClearEntitySelection();
894 if (local_tool_label !=
nullptr) {
895 draw_action(local_tool_label,
true, [&]() {
OpenTool(local_tool); });
898 ImGui::Dummy(ImVec2(0.0f, 2.0f));
902 float button_size,
bool compact,
904 const bool inspector_open = ImGui::BeginChild(
"##DungeonWorkbenchInspector",
905 ImVec2(width, height),
true);
906 if (inspector_open) {
911 ImGui::TextDisabled(tr(
"No active viewer"));
922 const char* collapse_icon =
924 const float collapse_w =
928 const float spacing = ImGui::GetStyle().ItemSpacing.x;
929 const float action_cluster_w = swap_w + spacing + collapse_w;
932 "##DungeonWorkbenchInspectorHeader",
ICON_MD_TUNE,
"Inspect",
"Inspect",
933 nullptr, compact, action_cluster_w, [&]() {
937 :
"Move inspector to the left side "
938 "(ZScream layout)")) {
944 ImGui::SameLine(0.0f, spacing);
947 "Collapse inspector")) {
952 ImGui::Dummy(ImVec2(0.0f, 2.0f));
955 ImGui::AlignTextToFramePadding();
956 ImGui::TextUnformatted(tr(
"Room"));
958 uint16_t requested_room_id =
static_cast<uint16_t
>(*current_room_id_);
959 ImGui::SetNextItemWidth(82.0f);
961 if (!can_jump_room) {
962 ImGui::BeginDisabled();
964 bool room_changed =
false;
968 "##WorkbenchRoomId", ImGuiDataType_U16, &requested_room_id,
"%04X",
969 ImGuiInputTextFlags_CharsHexadecimal,
972 "Current dungeon room ID");
974 if (!can_jump_room) {
975 ImGui::EndDisabled();
978 const int target_room_id = std::clamp(
static_cast<int>(requested_room_id),
985 if (ImGui::IsItemHovered()) {
986 const int preview_room_id = std::clamp(
988 ImGui::SetTooltip(tr(
"Open room 0x%03X"), preview_room_id);
1001 rooms = viewer->rooms();
1005 if (recent.empty()) {
1010 std::vector<int> recent_ids(recent.begin(), recent.end());
1011 std::vector<int> to_forget;
1013 constexpr ImGuiTabBarFlags kFlags = ImGuiTabBarFlags_AutoSelectNewTabs |
1014 ImGuiTabBarFlags_FittingPolicyScroll |
1015 ImGuiTabBarFlags_TabListPopupButton;
1018 const ImVec2 frame_pad = ImGui::GetStyle().FramePadding;
1020 const float extra_y = is_touch ? 6.0f : 1.0f;
1021 const float extra_x = is_touch ? 4.0f : 0.0f;
1023 ImGuiStyleVar_FramePadding,
1024 ImVec2(frame_pad.x + extra_x, frame_pad.y + extra_y));
1029 for (
int room_id : recent_ids) {
1031 const ImGuiTabItemFlags tab_flags =
1033 const auto room_name =
1035 const bool room_dirty =
1039 if (room_name.empty() || room_name ==
"Unknown") {
1040 snprintf(tab_label,
sizeof(tab_label),
"%03X%s##recent_%03X", room_id,
1041 room_dirty ?
"*" :
"", room_id);
1043 snprintf(tab_label,
sizeof(tab_label),
"%03X%s %.12s##recent_%03X",
1044 room_id, room_dirty ?
"*" :
"", room_name.c_str(), room_id);
1046 const bool selected = ImGui::BeginTabItem(tab_label, &open, tab_flags);
1049 to_forget.push_back(room_id);
1052 if (ImGui::IsItemHovered()) {
1055 ImGui::SetTooltip(
"[%03X] %s%s", room_id, label.c_str(),
1056 room_dirty ?
"\nPending room changes" :
"");
1063 if (ImGui::BeginPopupContextItem()) {
1074 to_forget.push_back(room_id);
1080 ImGui::EndTabItem();
1088 for (
int rid : to_forget) {
1122 constexpr ImGuiTableFlags kSplitFlags =
1123 ImGuiTableFlags_Resizable | ImGuiTableFlags_NoPadOuterX |
1124 ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_BordersInnerV;
1126 if (!ImGui::BeginTable(
"##DungeonWorkbenchSplit", 2, kSplitFlags)) {
1131 ImGui::TableSetupColumn(
"Active", ImGuiTableColumnFlags_WidthStretch);
1132 ImGui::TableSetupColumn(
"Compare", ImGuiTableColumnFlags_WidthStretch);
1133 ImGui::TableNextRow();
1139 ImGui::TableNextColumn();
1140 ImGui::AlignTextToFramePadding();
1142 ImGui::TextDisabled(
1149 if (split_active_open) {
1155 ImGui::TableNextColumn();
1156 ImGui::AlignTextToFramePadding();
1158 compare_viewer ? compare_viewer->
project() : active_project;
1159 ImGui::TextDisabled(
1166 if (split_compare_open) {
1167 if (compare_viewer) {
1174 ImGui::TextDisabled(tr(
"No compare viewer"));
1190 static const char*
const kShortNames[] = {
1191 "Sewers",
"HC",
"Eastern",
"Desert",
"A-Tower",
"Swamp",
"PoD",
1192 "Misery",
"Skull",
"Ice",
"Hera",
"Thieves",
"Turtle",
"GT",
1194 constexpr int kVanillaCount =
1195 static_cast<int>(
sizeof(kShortNames) /
sizeof(kShortNames[0]));
1197 auto AddRoom = [&](
int room_id,
int dungeon_id) {
1202 if (dungeon_id >= 0 && dungeon_id < kVanillaCount) {
1206 snprintf(buf,
sizeof(buf),
"Dungeon %02X", dungeon_id);
1212 for (
int i = 0; i < 0x84; ++i) {
1215 if (did >= 0 && did < kVanillaCount) {
1219 snprintf(buf,
sizeof(buf),
"Dungeon %02X", did);
1225 for (
int i = 0; i < 0x14; ++i) {
1237 ImGuiStyleVar_ItemSpacing,
1238 ImVec2(std::max(ImGui::GetStyle().ItemSpacing.x, 4.0f),
1239 std::max(4.0f, ImGui::GetStyle().ItemSpacing.y - 1.0f)));
1246 flags.kSaveSprites = value;
1247 flags.kSaveRoomHeaders = value;
1248 flags.kSaveChests = value;
1249 flags.kSavePotItems = value;
1250 flags.kSavePalettes = value;
1251 flags.kSaveCollision = value;
1252 flags.kSaveBlocks = value;
1253 flags.kSaveTorches = value;
1254 flags.kSavePits = value;
1260 if (ImGui::Checkbox(tr(
"Single-window Workbench"), &use_workbench)) {
1261 flags.kUseWorkbench = use_workbench;
1266 if (ImGui::IsItemHovered()) {
1267 ImGui::SetTooltip(tr(
1268 "Keep Dungeon editing in the integrated Workbench instead of separate "
1269 "high-level room panels."));
1273 ImGui::TextDisabled(tr(
"Data written by Apply Room / Apply Loaded Rooms"));
1274 constexpr ImGuiTableFlags kFlags =
1275 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1276 if (ImGui::BeginTable(
"##WorkbenchApplyScopeFlags", 2, kFlags)) {
1277 auto draw_checkbox = [](
const char* label,
bool* value) {
1278 ImGui::TableNextColumn();
1279 ImGui::Checkbox(label, value);
1281 ImGui::TableNextRow();
1282 draw_checkbox(
"Room Objects", &flags.kSaveObjects);
1283 draw_checkbox(
"Sprites", &flags.kSaveSprites);
1284 ImGui::TableNextRow();
1285 draw_checkbox(
"Room Headers", &flags.kSaveRoomHeaders);
1286 draw_checkbox(
"Chests", &flags.kSaveChests);
1287 ImGui::TableNextRow();
1288 draw_checkbox(
"Pot Items", &flags.kSavePotItems);
1289 draw_checkbox(
"Palettes", &flags.kSavePalettes);
1290 ImGui::TableNextRow();
1291 draw_checkbox(
"Collision Maps", &flags.kSaveCollision);
1292 ImGui::TableNextColumn();
1293 ImGui::BeginDisabled();
1294 ImGui::Checkbox(
"Water Fill", &flags.kSaveWaterFillZones);
1295 ImGui::EndDisabled();
1296 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1298 "Water Fill save scope is fixed when the project opens. Change "
1299 "save_dungeon_water_fill_zones in the .yaze file, then reopen the "
1302 ImGui::TableNextRow();
1303 draw_checkbox(
"Blocks", &flags.kSaveBlocks);
1304 draw_checkbox(
"Torches", &flags.kSaveTorches);
1305 ImGui::TableNextRow();
1306 draw_checkbox(
"Pits", &flags.kSavePits);
1307 ImGui::TableNextColumn();
1311 if (ImGui::SmallButton(tr(
"Select All##WorkbenchApplyScope"))) {
1315 if (ImGui::SmallButton(tr(
"Select None##WorkbenchApplyScope"))) {
1338 ImGui::TextDisabled(
1339 tr(
"Pit damage table is unavailable until ROM data loads."));
1345 if (!membership.room_valid) {
1346 ImGui::TextDisabled(tr(
"No valid room selected."));
1350 const uint16_t current_room = membership.room_id;
1351 const bool room_deals_damage = membership.deals_damage;
1353 room_deals_damage ? theme.status_warning : theme.text_secondary_gray,
1354 tr(
"%s Room 0x%03X %s pit damage"),
1356 room_deals_damage ?
"deals" :
"does not deal");
1357 ImGui::TextWrapped(tr(
1358 "RoomsWithPitDamage is fixed-capacity in vanilla. Editing replaces one "
1359 "listed room with another instead of growing or shrinking the table."));
1361 if (membership.dirty) {
1362 ImGui::TextColored(theme.status_warning,
1366 auto show_status = [&]() {
1378 if (room_deals_damage) {
1379 if (!membership.suggested_replacement_room.has_value()) {
1380 ImGui::TextDisabled(tr(
"No available non-damaging room slot."));
1386 ImGui::TextDisabled(tr(
"Remove current room by replacing it with:"));
1387 ImGui::SetNextItemWidth(88.0f);
1390 &edit_value, 88.0f,
true);
1395 if (ImGui::IsItemHovered()) {
1397 tr(
"Replacement room must not already be in the table."));
1400 const bool replace_clicked =
1401 ImGui::Button(tr(
"Replace current##PitDamageReplaceCurrent"));
1403 if (replace_clicked) {
1409 ? absl::StrFormat(
"Room 0x%03X removed; slot now 0x%03X",
1411 : std::string(status.message());
1414 if (!membership.suggested_victim_room.has_value()) {
1415 ImGui::TextDisabled(tr(
"No existing pit-damage slot is available."));
1421 ImGui::TextDisabled(tr(
"Add current room by replacing listed room:"));
1422 ImGui::SetNextItemWidth(88.0f);
1430 if (ImGui::IsItemHovered()) {
1431 ImGui::SetTooltip(tr(
"This room will stop dealing pit damage."));
1434 const bool add_clicked =
1435 ImGui::Button(tr(
"Add current##PitDamageAddCurrent"));
1443 ? absl::StrFormat(
"Room 0x%03X added; replaced 0x%03X",
1445 : std::string(status.message());
1450 ImGui::TextDisabled(
1451 tr(
"Apply Current Room / Apply Loaded Rooms writes this via "
1452 "SaveAllPits when the Pits scope is enabled."));
1458 ImGui::TextDisabled(tr(
"No active room"));
1463 auto draw_blend_combo = [&](
const char* combo_id,
1467 const char* current_name =
1470 ImGui::SetNextItemWidth(-1);
1471 if (ImGui::BeginCombo(combo_id, current_name)) {
1472 for (
int mode_int = 0; mode_int <= 4; ++mode_int) {
1474 const char* mode_name =
1476 const bool selected = current_mode == mode;
1477 if (ImGui::Selectable(mode_name, selected)) {
1478 layer_manager.SetLayerBlendMode(layer_type, mode);
1481 ImGui::SetItemDefaultFocus();
1490 const char* combo_id;
1493 static constexpr BlendRow kBlendRows[] = {
1494 {
"BG1 Layout",
"##WorkbenchBlendBG1Layout",
1496 {
"BG1 Objects",
"##WorkbenchBlendBG1Objects",
1498 {
"BG2 Layout",
"##WorkbenchBlendBG2Layout",
1500 {
"BG2 Objects",
"##WorkbenchBlendBG2Objects",
1504 constexpr ImGuiTableFlags kBlendTableFlags =
1505 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX;
1506 if (ImGui::BeginTable(
"##WorkbenchLayerBlend", 2, kBlendTableFlags)) {
1507 ImGui::TableSetupColumn(
"##label", ImGuiTableColumnFlags_WidthFixed, 92.0f);
1508 ImGui::TableSetupColumn(
"##combo", ImGuiTableColumnFlags_WidthStretch);
1509 for (
const auto& row : kBlendRows) {
1510 ImGui::TableNextRow();
1511 ImGui::TableNextColumn();
1512 ImGui::AlignTextToFramePadding();
1513 ImGui::TextUnformatted(row.label);
1514 ImGui::TableNextColumn();
1515 draw_blend_combo(row.combo_id, row.layer_type);
1548 if (
const auto* project = viewer.
project()) {
1557 constexpr const char* kPopupId =
"Dungeon Map##WorkbenchDungeonMapPopup";
1559 ImGui::SetNextWindowSize(ImVec2(660.0f, 520.0f), ImGuiCond_Appearing);
1560 ImGui::OpenPopup(kPopupId);
1564 bool popup_open =
true;
1565 if (ImGui::BeginPopupModal(kPopupId, &popup_open,
1566 ImGuiWindowFlags_NoSavedSettings)) {
1568 ImGui::CloseCurrentPopup();
1573 const ImVec2 map_size =
1574 ImVec2(std::max(360.0f, ImGui::GetContentRegionAvail().x),
1575 std::max(260.0f, ImGui::GetContentRegionAvail().y - 34.0f));
1576 if (ImGui::BeginChild(
"##WorkbenchDungeonMapBody", map_size,
false)) {
1581 ImGui::TextDisabled(tr(
"Dungeon map unavailable"));
1585 ImGui::CloseCurrentPopup();
1635 return "custom_collision";
1637 return "water_fill";
1641 return "object_selector";
1649 return "room_graphics";
1722 return "Custom Collision";
1724 return "Water Fill";
1726 return "Minecart Tracks";
1728 return "Object Selector";
1730 return "Door Tools";
1732 return "Sprite Tools";
1734 return "Item Tools";
1736 return "Room Graphics";
1749 return "Room tag tools are not available.";
1751 return "Custom collision tools are not available.";
1753 return "Water fill tools are not available.";
1755 return "Minecart track tools are not available.";
1757 return "Object selector is not available.";
1759 return "Door tools are not available.";
1761 return "Sprite tools are not available.";
1763 return "Item tools are not available.";
1765 return "Room graphics tools are not available.";
1767 return "Palette tools are not available.";
1769 return "No Workbench tool selected.";
1771 return "Tool is not available.";
1778 const char* unavailable_message) {
1780 ImGui::TextDisabled(
"%s", unavailable_message);
1783 content->
Draw(
nullptr);
1865 constexpr ImGuiTableFlags kFlags =
1866 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1867 if (!ImGui::BeginTable(
"##WorkbenchToolStrip", 5, kFlags)) {
1870 for (
int row = 0; row < 2; ++row) {
1871 ImGui::TableNextRow();
1872 for (
int col = 0; col < 5; ++col) {
1873 ImGui::TableNextColumn();
1878 std::snprintf(btn_id,
sizeof(btn_id),
"%s##StripTool_%s",
1881 ImGui::BeginDisabled();
1888 "Open a Dungeon Workbench tool");
1890 if (pressed && enabled) {
1894 ImGui::EndDisabled();
1896 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1916 ImGui::Dummy(ImVec2(0.0f, 2.0f));
1928 const float available_h = std::max(180.0f, ImGui::GetContentRegionAvail().y);
1929 const bool body_open =
1930 ImGui::BeginChild(
"##WorkbenchToolDrawerBody", ImVec2(0.0f, available_h),
1931 true, ImGuiWindowFlags_HorizontalScrollbar);
1939 float segment_height) {
1940 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1941 const float room_width =
1943 const float selection_width =
1945 const float tools_width =
1947 const float available_width = ImGui::GetContentRegionAvail().x;
1948 const bool stack = available_width < (room_width + selection_width +
1949 tools_width + spacing * 2.0f);
1950 auto draw_mode = [&](
const char* label,
float width,
InspectorMode mode) {
1951 const ImVec2 size(stack ? ImGui::GetContentRegionAvail().x : width,
1953 const bool pressed =
1960 "Switch Dungeon Workbench inspector mode");
1967 ImGui::SameLine(0.0f, spacing);
1973 const ImVec2 tools_size(
1974 stack ? ImGui::GetContentRegionAvail().x : tools_width, segment_height);
1980 "Switch Dungeon Workbench inspector mode");
1982 if (tools_pressed) {
1995 const bool has_entity = interaction.HasEntitySelection();
1999 ImGui::Text(
"[%03X] %s", room_id,
2003 ImGui::TextDisabled(tr(
"No room selected"));
2006 ImGui::Dummy(ImVec2(0.0f, 2.0f));
2007 if (selected_objects > 0 || has_entity) {
2010 ImGui::BulletText(tr(
"Entity selected"));
2012 if (selected_objects > 0) {
2013 ImGui::BulletText(tr(
"%zu object%s selected"), selected_objects,
2014 selected_objects == 1 ?
"" :
"s");
2022 ImGui::TextDisabled(tr(
"Nothing selected"));
2035 const bool has_selection =
2083 const std::string room_label =
2086 : std::string(
"None");
2091 ImGui::AlignTextToFramePadding();
2092 ImGui::Text(tr(
"Room 0x%03X"), room_id);
2096 snprintf(buf,
sizeof(buf),
"0x%03X", room_id);
2097 ImGui::SetClipboardText(buf);
2099 if (ImGui::IsItemHovered()) {
2100 ImGui::SetTooltip(tr(
"Copy room ID (0x%03X) to clipboard"), room_id);
2103 if (
auto* rooms = viewer.
rooms();
2104 rooms && room_id < static_cast<int>(rooms->size())) {
2105 auto& objects = (*rooms)[room_id].GetTileObjects();
2106 if (!objects.empty()) {
2107 const auto selected_indices =
2109 int requested_object_index =
2110 selected_indices.size() == 1
2111 ?
static_cast<int>(selected_indices.front())
2113 ImGui::AlignTextToFramePadding();
2114 ImGui::TextUnformatted(tr(
"Object"));
2116 ImGui::SetNextItemWidth(74.0f);
2117 bool object_index_changed =
false;
2121 "##WorkbenchObjectIndex", ImGuiDataType_S32,
2122 &requested_object_index,
"%d", 0,
2123 {
reinterpret_cast<uintptr_t
>(rooms),
2124 static_cast<uint64_t
>(room_id)});
2126 "Select and locate a room object by index");
2128 if (object_index_changed) {
2129 const size_t target_index =
static_cast<size_t>(std::clamp(
2130 requested_object_index, 0,
static_cast<int>(objects.size()) - 1));
2133 objects[target_index].y());
2136 if (ImGui::IsItemHovered()) {
2137 ImGui::SetTooltip(tr(
"Select object index 0-%zu"),
2138 objects.size() - 1);
2143 ImGui::TextUnformatted(tr(
"Room: None"));
2146 bool room_dirty =
false;
2147 if (
auto* rooms = viewer.
rooms(); rooms && room_id >= 0) {
2148 if (
const auto* room = rooms->GetIfMaterialized(room_id)) {
2149 room_dirty = room->HasUnsavedChanges();
2153 ImGui::TextColored(theme.status_warning,
2155 }
else if (room_id >= 0) {
2156 ImGui::TextDisabled(
ICON_MD_CHECK " Room matches ROM buffer");
2165 std::string project_group_name =
2168 const char* group_name =
2169 project_group_name.empty() ? nullptr : project_group_name.c_str();
2173 group_name = cache_it->second.c_str();
2177 auto* rooms = viewer.
rooms();
2178 if (rooms && room_id <
static_cast<int>(rooms->size())) {
2180 (*rooms)[room_id].blockset());
2185 room_label.c_str());
2187 ImGui::TextDisabled(
"%s", room_label.c_str());
2190 ImGui::TextDisabled(
"%s", room_label.c_str());
2213 if (
auto* rooms = viewer.
rooms();
2214 rooms && room_id >= 0 && room_id < static_cast<int>(rooms->size())) {
2215 auto& room = (*rooms)[room_id];
2217 uint8_t layout_val = room.layout_id();
2218 uint8_t blockset_val = room.blockset();
2219 uint8_t floor1_val = room.floor1();
2220 uint8_t floor2_val = room.floor2();
2221 uint8_t palette_val = room.palette();
2222 uint8_t spriteset_val = room.spriteset();
2223 uint16_t message_val = room.message_id();
2224 uint8_t bg2_val =
static_cast<uint8_t
>(room.bg2());
2225 uint8_t effect_val =
static_cast<uint8_t
>(room.effect());
2226 uint8_t collision_val =
static_cast<uint8_t
>(room.collision());
2227 uint8_t tag1_val =
static_cast<uint8_t
>(room.tag1());
2228 uint8_t tag2_val =
static_cast<uint8_t
>(room.tag2());
2230 auto render_room_graphics = [&]() {
2231 if (room.rom() && room.rom()->is_loaded()) {
2232 room.RenderRoomGraphics();
2236 constexpr float kHexW = 54.0f;
2237 constexpr ImGuiTableFlags kHeaderFlags = ImGuiTableFlags_BordersInnerV |
2238 ImGuiTableFlags_RowBg |
2239 ImGuiTableFlags_NoPadOuterX;
2240 auto draw_label = [](
const char* label) {
2241 ImGui::AlignTextToFramePadding();
2242 ImGui::TextUnformatted(label);
2244 auto draw_byte_field = [&](
const char* label,
const char* id, uint8_t value,
2245 uint8_t max_value,
const std::string& tooltip,
2247 ImGui::TableNextColumn();
2249 ImGui::TableNextColumn();
2250 ImGui::SetNextItemWidth(kHexW);
2251 uint8_t edit_value = value;
2257 if (ImGui::IsItemHovered()) {
2258 ImGui::SetTooltip(
"%s", tooltip.c_str());
2261 auto draw_word_field = [&](
const char* label,
const char* id,
2262 uint16_t value, uint16_t max_value,
2263 const std::string& tooltip,
auto apply) {
2264 ImGui::TableNextColumn();
2266 ImGui::TableNextColumn();
2267 ImGui::SetNextItemWidth(kHexW + 12.0f);
2268 uint16_t edit_value = value;
2271 apply(std::min<uint16_t>(edit_value, max_value));
2273 if (ImGui::IsItemHovered()) {
2274 ImGui::SetTooltip(
"%s", tooltip.c_str());
2278 if (ImGui::BeginTable(
"##WorkbenchRoomHeader", 4, kHeaderFlags)) {
2279 ImGui::TableSetupColumn(
"L1", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2280 ImGui::TableSetupColumn(
"V1", ImGuiTableColumnFlags_WidthFixed, 66.0f);
2281 ImGui::TableSetupColumn(
"L2", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2282 ImGui::TableSetupColumn(
"V2", ImGuiTableColumnFlags_WidthStretch);
2284 ImGui::TableNextRow();
2285 draw_byte_field(
"Lay",
"##RoomHeaderLayout", layout_val, 0x07,
2286 "Layout (0-7)", [&](uint8_t value) {
2287 room.SetLayoutId(value);
2288 room.MarkLayoutDirty();
2289 render_room_graphics();
2291 draw_byte_field(
"Blk",
"##RoomHeaderBlockset", blockset_val, 0x51,
2292 "Blockset (0-51)", [&](uint8_t value) {
2293 room.SetBlockset(value);
2294 render_room_graphics();
2297 ImGui::TableNextRow();
2298 draw_byte_field(
"F1",
"##RoomHeaderFloor1", floor1_val, 0x0F,
2299 "BG1 floor graphics (0-F)", [&](uint8_t value) {
2300 room.set_floor1(value);
2301 render_room_graphics();
2303 draw_byte_field(
"F2",
"##RoomHeaderFloor2", floor2_val, 0x0F,
2304 "BG2 floor graphics (0-F)", [&](uint8_t value) {
2305 room.set_floor2(value);
2306 render_room_graphics();
2309 ImGui::TableNextRow();
2310 draw_byte_field(
"Pal",
"##RoomHeaderPalette", palette_val, 0x47,
2311 "Palette set (0-47)", [&](uint8_t value) {
2312 room.SetPalette(value);
2313 render_room_graphics();
2318 draw_byte_field(
"Spr",
"##RoomHeaderSpriteset", spriteset_val, 0x8F,
2319 "Sprite graphics set (0-8F)", [&](uint8_t value) {
2320 room.SetSpriteset(value);
2321 render_room_graphics();
2324 ImGui::TableNextRow();
2325 draw_word_field(
"Msg",
"##RoomHeaderMessage", message_val, 0x0FFF,
2326 "Dungeon message ID (0-FFF)",
2327 [&](uint16_t value) { room.SetMessageId(value); });
2328 draw_byte_field(
"BG2",
"##RoomHeaderBg2", bg2_val, 0x08,
2329 std::string(
"BG2 mode: ") + GetBg2ModeName(bg2_val),
2330 [&](uint8_t value) {
2332 render_room_graphics();
2335 ImGui::TableNextRow();
2336 const char* effect_name =
2338 draw_byte_field(
"FX",
"##RoomHeaderEffect", effect_val, 0x07,
2339 std::string(
"Effect: ") + effect_name,
2340 [&](uint8_t value) {
2342 render_room_graphics();
2345 "Coll",
"##RoomHeaderCollision", collision_val, 0x04,
2346 std::string(
"Collision: ") + GetCollisionName(collision_val),
2347 [&](uint8_t value) {
2351 ImGui::TableNextRow();
2352 draw_byte_field(
"Tag1",
"##RoomHeaderTag1", tag1_val, 0x40,
2354 [&](uint8_t value) {
2356 render_room_graphics();
2358 draw_byte_field(
"Tag2",
"##RoomHeaderTag2", tag2_val, 0x40,
2360 [&](uint8_t value) {
2362 render_room_graphics();
2368 ImGui::Dummy(ImVec2(0.0f, 2.0f));
2370 if (ImGui::BeginTable(
"##WorkbenchRoomDestinations", 4, kHeaderFlags)) {
2371 ImGui::TableSetupColumn(
"L1", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2372 ImGui::TableSetupColumn(
"V1", ImGuiTableColumnFlags_WidthFixed, 66.0f);
2373 ImGui::TableSetupColumn(
"L2", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2374 ImGui::TableSetupColumn(
"V2", ImGuiTableColumnFlags_WidthStretch);
2376 ImGui::TableNextRow();
2377 draw_byte_field(
"Pit",
"##RoomHeaderPit", room.holewarp(), 0xFF,
2378 "Pit/holewarp destination room",
2379 [&](uint8_t value) { room.SetHolewarp(value); });
2380 draw_byte_field(
"St1",
"##RoomHeaderStair1", room.staircase_room(0), 0xFF,
2381 "Stair destination slot 1",
2382 [&](uint8_t value) { room.SetStaircaseRoom(0, value); });
2384 ImGui::TableNextRow();
2385 draw_byte_field(
"St2",
"##RoomHeaderStair2", room.staircase_room(1), 0xFF,
2386 "Stair destination slot 2",
2387 [&](uint8_t value) { room.SetStaircaseRoom(1, value); });
2388 draw_byte_field(
"St3",
"##RoomHeaderStair3", room.staircase_room(2), 0xFF,
2389 "Stair destination slot 3",
2390 [&](uint8_t value) { room.SetStaircaseRoom(2, value); });
2392 ImGui::TableNextRow();
2393 draw_byte_field(
"St4",
"##RoomHeaderStair4", room.staircase_room(3), 0xFF,
2394 "Stair destination slot 4",
2395 [&](uint8_t value) { room.SetStaircaseRoom(3, value); });
2396 draw_byte_field(
"P1",
"##RoomHeaderStairPlane1", room.staircase_plane(0),
2397 0x03,
"Stair 1 target layer/plane (0-3)",
2398 [&](uint8_t value) { room.SetStaircasePlane(0, value); });
2400 ImGui::TableNextRow();
2401 draw_byte_field(
"P2",
"##RoomHeaderStairPlane2", room.staircase_plane(1),
2402 0x03,
"Stair 2 target layer/plane (0-3)",
2403 [&](uint8_t value) { room.SetStaircasePlane(1, value); });
2404 draw_byte_field(
"P3",
"##RoomHeaderStairPlane3", room.staircase_plane(2),
2405 0x03,
"Stair 3 target layer/plane (0-3)",
2406 [&](uint8_t value) { room.SetStaircasePlane(2, value); });
2408 ImGui::TableNextRow();
2409 draw_byte_field(
"P4",
"##RoomHeaderStairPlane4", room.staircase_plane(3),
2410 0x03,
"Stair 4 target layer/plane (0-3)",
2411 [&](uint8_t value) { room.SetStaircasePlane(3, value); });
2412 ImGui::TableNextColumn();
2413 ImGui::TableNextColumn();
2418 ImGui::TextDisabled(tr(
"Room header unavailable"));
2425 ImGui::TextColored(theme.text_info, tr(
"Placement active"));
2428 interaction.mode_manager().CancelCurrentMode();
2439 const size_t obj_count = interaction.GetSelectionCount();
2440 const bool has_entity = interaction.HasEntitySelection();
2442 if (!has_entity && obj_count == 0) {
2444 ImGui::TextDisabled(tr(
"Click an object or entity to inspect"));
2449 if (obj_count > 0) {
2451 ImGui::TextColored(theme.text_primary,
ICON_MD_WIDGETS " %zu object%s",
2452 obj_count, obj_count == 1 ?
"" :
"s");
2453 if (obj_count == 1) {
2455 ImGui::TextDisabled(tr(
"Focused selection"));
2458 const auto indices = interaction.GetSelectedObjectIndices();
2461 if (indices.size() > 1 && room_id >= 0 && viewer.
rooms()) {
2462 auto& room = (*viewer.
rooms())[room_id];
2463 auto& objects = room.GetTileObjects();
2465 " Multi-selection");
2466 for (
size_t i = 0; i < indices.size() && i < 8; ++i) {
2467 size_t idx = indices[i];
2468 if (idx < objects.size()) {
2469 auto& obj = objects[idx];
2471 ImGui::BulletText(tr(
"0x%03X %s"), obj.id_, name.c_str());
2474 if (indices.size() > 8) {
2475 ImGui::TextDisabled(tr(
" ... and %zu more"), indices.size() - 8);
2481 auto& tile_handler = interaction.entity_coordinator().tile_handler();
2482 const std::vector<size_t> selection_copy(indices.begin(), indices.end());
2483 bool has_room_stream_object =
false;
2484 bool has_special_layer_object =
false;
2485 for (
const size_t index : selection_copy) {
2486 if (index >= objects.size()) {
2490 has_room_stream_object =
true;
2492 has_special_layer_object =
true;
2495 const bool has_editable_size =
2501 static int bulk_nudge_dx = 0;
2502 static int bulk_nudge_dy = 0;
2503 ImGui::TextDisabled(tr(
"Nudge (tiles)"));
2504 ImGui::PushButtonRepeat(
true);
2506 tile_handler.MoveObjects(room_id, selection_copy, 0, -1);
2509 tile_handler.MoveObjects(room_id, selection_copy, 0, 1);
2512 tile_handler.MoveObjects(room_id, selection_copy, -1, 0);
2515 tile_handler.MoveObjects(room_id, selection_copy, 1, 0);
2516 ImGui::PopButtonRepeat();
2518 ImGui::SetNextItemWidth(60);
2519 ImGui::DragInt(
"##BulkNudgeDx", &bulk_nudge_dx, 0.25f, -63, 63,
"Δx:%d");
2521 ImGui::SetNextItemWidth(60);
2522 ImGui::DragInt(
"##BulkNudgeDy", &bulk_nudge_dy, 0.25f, -63, 63,
"Δy:%d");
2524 if (ImGui::Button(tr(
"Apply##BulkNudgeApply")) &&
2525 (bulk_nudge_dx != 0 || bulk_nudge_dy != 0)) {
2526 tile_handler.MoveObjects(room_id, selection_copy, bulk_nudge_dx,
2533 if (has_room_stream_object && !has_special_layer_object) {
2534 ImGui::TextDisabled(tr(
"Object stream (set all)"));
2535 if (ImGui::SmallButton(tr(
"Primary##BulkPlacement0")))
2536 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2538 if (ImGui::SmallButton(tr(
"BG2 overlay##BulkPlacement1")))
2539 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2541 if (ImGui::SmallButton(tr(
"BG1 overlay##BulkPlacement2")))
2542 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 2);
2543 }
else if (has_special_layer_object && !has_room_stream_object) {
2544 ImGui::TextDisabled(tr(
"Special layer (set all)"));
2545 if (ImGui::SmallButton(tr(
"Upper (BG1)##BulkPlacement0")))
2546 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2548 if (ImGui::SmallButton(tr(
"Lower (BG2)##BulkPlacement1")))
2549 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2551 ImGui::TextDisabled(tr(
"Stored placement (mixed selection)"));
2552 if (ImGui::SmallButton(tr(
"Primary / Upper (BG1)##BulkPlacement0")))
2553 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2555 if (ImGui::SmallButton(tr(
"BG2 overlay / Lower (BG2)##BulkPlacement1")))
2556 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2560 ImGui::TextDisabled(tr(
"Stacking"));
2562 tile_handler.SendToFront(room_id, selection_copy);
2565 tile_handler.SendToBack(room_id, selection_copy);
2568 tile_handler.MoveForward(room_id, selection_copy);
2571 tile_handler.MoveBackward(room_id, selection_copy);
2574 ImGui::TextDisabled(tr(
"Size"));
2575 if (!has_editable_size) {
2576 ImGui::BeginDisabled();
2579 tile_handler.ResizeObjects(room_id, selection_copy, -1);
2581 if (ImGui::SmallButton(
ICON_MD_ADD "##BulkSizeInc"))
2582 tile_handler.ResizeObjects(room_id, selection_copy, 1);
2583 if (!has_editable_size) {
2584 ImGui::EndDisabled();
2589 (void)tile_handler.DuplicateObjects(room_id, selection_copy, 1, 1);
2593 {ImGuiCol_Button, theme.status_error},
2594 {ImGuiCol_ButtonHovered,
2595 ImVec4(theme.status_error.x + 0.1f, theme.status_error.y + 0.05f,
2596 theme.status_error.z + 0.05f, 1.0f)},
2599 ImGui::OpenPopup(
"##BulkDeleteConfirm");
2602 if (ImGui::BeginPopup(
"##BulkDeleteConfirm")) {
2605 selection_copy.size(), selection_copy.size() == 1 ?
"" :
"s");
2607 if (ImGui::Button(tr(
"Cancel##BulkDelCancel"))) {
2608 ImGui::CloseCurrentPopup();
2613 {ImGuiCol_Button, theme.status_error},
2616 tile_handler.DeleteObjects(room_id, selection_copy);
2617 ImGui::CloseCurrentPopup();
2625 if (indices.size() == 1 && room_id >= 0 && viewer.
rooms()) {
2626 auto& room = (*viewer.
rooms())[room_id];
2627 auto& objects = room.GetTileObjects();
2628 const size_t idx = indices.front();
2629 if (idx < objects.size()) {
2630 auto& obj = objects[idx];
2634 int displayed_layer = obj.GetLayerValue();
2635 const int pixel_x = obj.x_ * 8;
2636 const int pixel_y = obj.y_ * 8;
2641 ImGui::TextColored(theme.text_primary,
"%s", obj_name.c_str());
2642 ImGui::TextDisabled(tr(
"%s (Type %d) #%zu in list"),
2643 GetObjectCategory(obj.id_), subtype, idx);
2646 constexpr ImGuiTableFlags kPropsFlags = ImGuiTableFlags_BordersInnerV |
2647 ImGuiTableFlags_RowBg |
2648 ImGuiTableFlags_NoPadOuterX;
2649 if (ImGui::BeginTable(
"##SelObjProps", 2, kPropsFlags)) {
2650 ImGui::TableSetupColumn(
"Prop", ImGuiTableColumnFlags_WidthFixed,
2652 ImGui::TableSetupColumn(
"Val", ImGuiTableColumnFlags_WidthStretch);
2656 uint16_t obj_id =
static_cast<uint16_t
>(obj.id_ & 0x0FFF);
2661 interaction.SetObjectId(idx,
static_cast<int16_t
>(obj_id));
2669 ImGui::SetNextItemWidth(60);
2671 ImGui::DragInt(
"##SelObjX", &pos_x, 0.1f, 0, 63,
"X:%d");
2675 "Selected room object X coordinate");
2678 ImGui::SetNextItemWidth(60);
2680 ImGui::DragInt(
"##SelObjY", &pos_y, 0.1f, 0, 63,
"Y:%d");
2684 "Selected room object Y coordinate");
2686 if (x_changed || y_changed) {
2687 int delta_x = pos_x - obj.x_;
2688 int delta_y = pos_y - obj.y_;
2689 interaction.entity_coordinator().tile_handler().MoveObjects(
2690 room_id, {idx}, delta_x, delta_y);
2696 uint8_t size = obj.size_ & 0x0F;
2697 const bool size_editable =
2699 if (!size_editable) {
2700 ImGui::BeginDisabled();
2705 interaction.SetObjectSize(idx, size);
2707 if (!size_editable) {
2708 ImGui::EndDisabled();
2713 uses_room_stream ?
"Stream" :
"Layer", [&]() {
2714 int layer = displayed_layer;
2715 const char* stream_names[] = {
"Primary",
"BG2 overlay",
2717 const char* special_layer_names[] = {
"Upper layer (BG1)",
2718 "Lower layer (BG2)"};
2719 ImGui::SetNextItemWidth(-1);
2721 "##SelObjLayer", &layer,
2722 uses_room_stream ? stream_names : special_layer_names,
2723 uses_room_stream ? IM_ARRAYSIZE(stream_names)
2724 : IM_ARRAYSIZE(special_layer_names))) {
2725 const int max_layer = uses_room_stream ? 2 : 1;
2726 layer = std::clamp(layer, 0, max_layer);
2727 if (interaction.SetObjectLayer(
2730 displayed_layer = layer;
2735 ImGui::TextDisabled(
2736 "%s", uses_room_stream ? GetObjectStreamName(displayed_layer)
2737 : GetSpecialLayerName(displayed_layer));
2742 ImGui::TextDisabled(
"(%d, %d)", pixel_x, pixel_y);
2752 if (has_entity && room_id >= 0 && viewer.
rooms()) {
2753 const auto sel = interaction.GetSelectedEntity();
2754 auto& room = (*viewer.
rooms())[room_id];
2756 " Entity Selection");
2760 const auto& doors = room.GetDoors();
2761 if (sel.index < doors.size()) {
2762 const auto& door = doors[sel.index];
2768 ImGui::TextDisabled(tr(
"Direction: %s Position: 0x%02X"),
2769 dir_name.c_str(), door.position);
2771 auto [tile_x, tile_y] = door.GetTileCoords();
2772 auto [pixel_x, pixel_y] = door.GetPixelCoords();
2773 ImGui::TextDisabled(tr(
"Tile: (%d, %d) Pixel: (%d, %d)"), tile_x,
2774 tile_y, pixel_x, pixel_y);
2779 const auto& sprites = room.GetSprites();
2780 if (sel.index < sprites.size()) {
2781 const auto& sprite = sprites[sel.index];
2785 sprite_name.c_str());
2786 ImGui::TextDisabled(tr(
"ID: 0x%02X Subtype: %d Layer: %d"),
2787 sprite.id(), sprite.subtype(), sprite.layer());
2788 ImGui::TextDisabled(tr(
"Pos: (%d, %d) Pixel: (%d, %d)"), sprite.x(),
2789 sprite.y(), sprite.x() * 16, sprite.y() * 16);
2792 if (sprite.subtype() == 0x07 && sprite.id() >= 0x01 &&
2793 sprite.id() <= 0x1A) {
2795 ImGui::TextColored(theme.text_warning_yellow,
2797 overlord_name.c_str());
2803 const auto& items = room.GetPotItems();
2804 if (sel.index < items.size()) {
2805 const auto& pot_item = items[sel.index];
2806 const char* item_name = GetPotItemName(pot_item.item);
2810 ImGui::TextDisabled(tr(
"Item ID: 0x%02X Raw Pos: 0x%04X"),
2811 pot_item.item, pot_item.position);
2812 ImGui::TextDisabled(tr(
"Pixel: (%d, %d) Tile: (%d, %d)"),
2813 pot_item.GetPixelX(), pot_item.GetPixelY(),
2814 pot_item.GetTileX(), pot_item.GetTileY());
2828 constexpr ImGuiTableFlags kFlags =
2829 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
2830 if (!ImGui::BeginTable(
"##WorkbenchToolsGrid", 2, kFlags)) {
2834 auto draw_tool_button = [&](
const char* label,
WorkbenchTool tool,
2835 bool enabled =
true) {
2837 ImGui::BeginDisabled();
2845 ImGui::EndDisabled();
2852 ImGui::TableNextRow();
2853 ImGui::TableNextColumn();
2856 ImGui::TableNextColumn();
2860 ImGui::TableNextRow();
2861 ImGui::TableNextColumn();
2864 ImGui::TableNextColumn();
2871 if (ImGui::BeginTable(
"##WorkbenchRoomToolsGrid", 2, kFlags)) {
2872 auto room_tool_button = [&](
const char* label,
WorkbenchTool tool,
2874 ImGui::TableNextColumn();
2876 ImGui::BeginDisabled();
2884 ImGui::EndDisabled();
2888 ImGui::TableNextRow();
2894 ImGui::TableNextRow();
2903 if (ImGui::BeginTable(
"##WorkbenchReviewGrid", 2, kFlags)) {
2904 ImGui::TableNextRow();
2905 ImGui::TableNextColumn();
2910 ImGui::TableNextColumn();
2916 ImGui::TableNextRow();
2917 ImGui::TableNextColumn();
2922 ImGui::TableNextColumn();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void Draw(bool *p_open) override
Draw the panel content.
DungeonRoomStore * rooms() const
std::optional< int > DrawConnectedRoomMatrix(int center_room_id)
int current_room_id() const
float ConnectedCanvasScale() const
DungeonObjectInteraction & object_interaction()
zelda3::RoomLayerManager & GetRoomLayerManager(int room_id)
ImVec2 GetConnectedContentSize(int center_room_id)
const project::YazeProject * project() const
void ScrollToTile(int tile_x, int tile_y)
void DrawDungeonCanvas(int room_id)
WindowContent for displaying multiple rooms in a spatial dungeon layout.
void SetSelectedObjects(const std::vector< size_t > &indices)
std::vector< size_t > GetSelectedObjectIndices() const
InteractionModeManager & mode_manager()
size_t GetSelectionCount() const
Handles room and entrance selection UI.
ImVector< int > & mutable_active_rooms()
void DrawRoomBrowser(RoomSelectionIntent single_click_intent=RoomSelectionIntent::kFocusInWorkbench)
void DrawEntranceBrowser()
static const char * GetBlocksetGroupName(uint8_t blockset)
zelda3::Room * GetIfMaterialized(int room_id)
static void Draw(const DungeonStatusBarState &state)
static DungeonStatusBarState BuildState(const DungeonCanvasViewer &viewer, const char *tool_mode, bool room_dirty)
void DrawInspector(DungeonCanvasViewer &viewer, bool compact)
bool room_dungeon_cache_built_
void DrawInspectorShelfSelection(DungeonCanvasViewer &viewer)
std::function< void(int)> on_room_selected_
void SetAllSaveFlags(bool value)
const char * GetActiveToolIdForTesting() const
DungeonWorkbenchContent(DungeonRoomSelector *room_selector, int *current_room_id, std::function< void(int)> on_room_selected, std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent, std::function< void(int)> on_save_room, std::function< void()> on_save_all_rooms, std::function< DungeonCanvasViewer *()> get_viewer, std::function< DungeonCanvasViewer *(int)> get_compare_viewer, std::function< const std::deque< int > &()> get_recent_rooms, std::function< void(int)> forget_recent_room, std::function< void(bool)> set_workflow_mode, Rom *rom=nullptr)
bool pit_damage_status_error_
void FocusEntranceBrowser()
std::function< bool()> can_redo_
int GetPriority() const override
Get display priority for menu ordering.
CustomCollisionPanel * custom_collision_panel_
DungeonWorkbenchPitDamageControlRects pit_damage_control_rects_
bool IsToolDrawerActiveForTesting() const
void DrawSelectionShelf(DungeonCanvasViewer &viewer)
SidebarMode sidebar_mode_
uint16_t pit_damage_victim_room_id_
WorkbenchTool active_tool_
void OpenTool(WorkbenchTool tool)
bool IsWorkbenchToolAvailable(WorkbenchTool tool) const
WaterFillPanel * water_fill_panel_
void DrawInspectorHeader(float button_size, bool compact)
void FocusSelectionInspector()
void DrawInspectorCompactSummary(DungeonCanvasViewer &viewer)
void DrawSidebarModeTabs(bool stacked, float segment_height)
std::function< void()> on_redo_
void DrawInspectorPrimarySelector(float segment_height)
WindowContent * object_selector_content_
void DrawRecentRoomTabs()
void DrawInspectorToolStrip()
void DrawSidebarPane(float width, float height, float button_size, bool compact)
DungeonMapPanel * GetEmbeddedDungeonMap(DungeonCanvasViewer &viewer)
void DrawSidebarContent()
InspectorMode inspector_mode_
void DrawInspectorPane(float width, float height, float button_size, bool compact, DungeonCanvasViewer *viewer)
const char * GetWorkbenchToolIcon(WorkbenchTool tool) const
void DrawInspectorShelfRoom(DungeonCanvasViewer &viewer)
const char * GetInspectorModeIdForTesting() const
WindowContent * sprite_editor_content_
std::function< int()> undo_depth_
std::string GetEditorCategory() const override
Editor category this panel belongs to.
void OpenRoomGraphicsTool()
std::function< std::string()> undo_desc_
void DrawInspectorShelfTools(DungeonCanvasViewer &viewer)
void FocusRoomInspector()
void DrawCanvasPane(float width, float height, DungeonCanvasViewer *primary_viewer, bool left_sidebar_visible)
std::function< bool()> can_undo_
RoomTagEditorPanel * room_tag_panel_
void DrawPitDamageControls(int room_id)
void DrawWorkbenchTool(DungeonCanvasViewer &viewer, WorkbenchTool tool)
DungeonRoomSelector * room_selector_
void DrawSidebarHeader(float button_size, bool compact)
void DrawInspectorToolDrawer(DungeonCanvasViewer &viewer)
std::function< void()> on_undo_
const char * GetWorkbenchToolShortLabel(WorkbenchTool tool) const
std::function< std::string()> redo_desc_
void DrawDungeonMapPopup(DungeonCanvasViewer &viewer)
std::unique_ptr< DungeonMapPanel > embedded_dungeon_map_
char compare_search_buf_[64]
WindowContent * item_editor_content_
std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent_
void SetEmbeddedEditorPanels(WindowContent *object_selector, WindowContent *door_editor, WindowContent *sprite_editor, WindowContent *item_editor, WindowContent *room_graphics, WindowContent *palette_editor)
MinecartTrackEditorPanel * minecart_track_panel_
void SetEmbeddedToolPanels(RoomTagEditorPanel *room_tags, CustomCollisionPanel *custom_collision, WaterFillPanel *water_fill, MinecartTrackEditorPanel *minecart_tracks)
const char * GetWorkbenchToolId(WorkbenchTool tool) const
const char * GetWorkbenchToolUnavailableMessage(WorkbenchTool tool) const
bool inspector_selection_was_active_
std::function< void(int)> on_save_room_
WindowContent * palette_editor_content_
WindowContent * room_graphics_content_
std::string pit_damage_status_message_
std::string GetId() const override
Unique identifier for this panel.
void DrawSplitView(DungeonCanvasViewer &primary_viewer)
void OpenObjectSelectorTool()
std::function< zelda3::PitDamageTable *()> get_pit_damage_table_
const char * GetWorkbenchToolTitle(WorkbenchTool tool) const
std::function< void()> on_save_all_rooms_
std::function< void(bool)> set_workflow_mode_
void ShowConnectedGraph()
std::string GetIcon() const override
Material Design icon for this panel.
std::function< DungeonCanvasViewer *(int)> get_compare_viewer_
std::unordered_map< int, std::string > room_dungeon_cache_
bool compact_inspector_detail_requested_
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
void RequestDungeonMapPopup()
std::function< void(bool)> on_inspector_side_changed_
~DungeonWorkbenchContent() override
void DrawApplyScopeControls(int room_id)
uint16_t pit_damage_replacement_room_id_
void Draw(bool *p_open) override
Draw the panel content.
void OpenCustomCollisionTool()
DungeonWorkbenchLayoutState layout_state_
bool open_dungeon_map_popup_
bool show_shortcut_legend_
std::function< void(int)> forget_recent_room_
std::function< const std::deque< int > &()> get_recent_rooms_
void DrawInspectorShelf(DungeonCanvasViewer &viewer, bool compact)
void BuildRoomDungeonCache()
void DrawLayerCompositingControls(DungeonCanvasViewer &viewer, int room_id)
std::function< DungeonCanvasViewer *()> get_viewer_
WindowContent * door_editor_content_
const char * GetModeName() const
Get mode name for debugging/UI.
bool IsPlacementActive() const
Check if any placement mode is active.
void Draw(bool *p_open) override
Draw the panel content.
WindowContent showing all room tag slots and their usage across rooms.
void SetCurrentRoomId(int room_id)
void Draw(bool *p_open) override
Draw the panel content.
static void Draw(bool *p_open)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void Draw(bool *p_open) override
Draw the panel content.
Base interface for all logical window content components.
virtual void Draw(bool *p_open)=0
Draw the panel content.
void ApplyScaleSnapshot(const CanvasConfig &snapshot)
CanvasConfig & GetConfig()
static float GetTouchSafeWidgetHeight()
static bool BeginContentChild(const char *id, const ImVec2 &min_size, bool border=false, ImGuiWindowFlags flags=0)
static void EndContentChild()
static void PropertyRow(const char *label, std::function< void()> widget_callback)
static bool IsTouchDevice()
RAII guard for ImGui style colors.
RAII guard for ImGui style vars.
Dungeon Room Entrance or Spawn Point.
static const char * GetBlendModeName(LayerBlendMode mode)
Get blend mode name.
LayerBlendMode GetLayerBlendMode(LayerType layer) const
bool HasUnsavedChanges() const
zelda3_bg2_effect
Background layer 2 effects.
#define ICON_MD_GRID_VIEW
#define ICON_MD_SUMMARIZE
#define ICON_MD_VIEW_QUILT
#define ICON_MD_ALT_ROUTE
#define ICON_MD_VIEW_LIST
#define ICON_MD_ARROW_FORWARD
#define ICON_MD_SWAP_HORIZ
#define ICON_MD_COMPARE_ARROWS
#define ICON_MD_OPEN_WITH
#define ICON_MD_OPEN_IN_FULL
#define ICON_MD_FLIP_TO_FRONT
#define ICON_MD_ARROW_DOWNWARD
#define ICON_MD_MORE_HORIZ
#define ICON_MD_TRAVEL_EXPLORE
#define ICON_MD_CONTENT_PASTE
#define ICON_MD_KEYBOARD_ARROW_DOWN
#define ICON_MD_INVENTORY
#define ICON_MD_DOOR_FRONT
#define ICON_MD_CHEVRON_LEFT
#define ICON_MD_ARROW_UPWARD
#define ICON_MD_CHECK_CIRCLE
#define ICON_MD_KEYBOARD_ARROW_UP
#define ICON_MD_ARROW_BACK
#define ICON_MD_SELECT_ALL
#define ICON_MD_EDIT_NOTE
#define ICON_MD_OPEN_IN_NEW
#define ICON_MD_CONTENT_COPY
#define ICON_MD_INVENTORY_2
#define ICON_MD_WATER_DROP
#define ICON_MD_VIEW_SIDEBAR
#define ICON_MD_CHEVRON_RIGHT
#define ICON_MD_FLIP_TO_BACK
#define ICON_MD_WORKSPACES
#define ICON_MD_CROP_FREE
const AgentUITheme & GetTheme()
constexpr float kCompactLeftSidebarScale
const char * GetObjectStreamName(int layer_value)
const char * GetCollisionName(int value)
float GetCompactSidebarWidth(bool right_sidebar, float min_sidebar_width)
DungeonWorkbenchTestRect CaptureLastItemRectForTesting()
const char * GetPotItemName(uint8_t item)
constexpr float kCompactLeftSidebarMinWidth
constexpr float kCompactRightSidebarMinWidth
const char * GetSpecialLayerName(int layer_value)
const char * GetBg2ModeName(int value)
constexpr float kCompactRightSidebarScale
float ClampWorkbenchPaneWidth(float desired_width, float min_width, float max_width)
const char * GetObjectCategory(int object_id)
std::string GetRoomLabel(const project::YazeProject *project, int room_id)
std::string GetDungeonNameForRoom(const project::YazeProject *project, int room_id)
bool BeginInspectorSection(const char *label, bool default_open)
void DrawPaneHeader(const char *table_id, const char *icon, const char *title, const char *compact_title, const char *subtitle, bool compact, float action_width, const std::function< void()> &draw_actions)
float CalcIconButtonWidth(const char *icon, float button_height)
bool DrawActionButton(const char *label, const ImVec2 &size)
bool HasEditableRoomObjectSize(std::span< const zelda3::RoomObject > objects, std::span< const size_t > selected_indices)
void DrawInspectorSectionHeader(const char *label)
bool DrawHeaderIconAction(const char *id, const char *icon, float button_size, const char *tooltip, bool active=false)
constexpr const char * kConnected
constexpr const char * kWorkbench
Editors are the view controllers for the application.
DungeonWorkbenchPaneLayout ResolveDungeonWorkbenchPaneLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, float stored_left_width, float stored_right_width, bool want_left, bool want_right)
DungeonSelectionSnapshot BuildDungeonSelectionSnapshot(const DungeonObjectInteraction &interaction, const DungeonRoomStore *rooms, int room_id)
PitDamageMembershipState BuildPitDamageMembershipState(const zelda3::PitDamageTable *table, int room_id, uint16_t replacement_fallback, uint16_t victim_fallback)
bool ResolveCompactInspectorDetailRequest(bool compact, bool detail_requested)
absl::Status RemoveCurrentRoomFromPitDamage(zelda3::PitDamageTable *table, uint16_t current_room_id, uint16_t replacement_room_id)
RoomSelectionIntent
Intent for room selection in the dungeon editor.
std::string GetDungeonSelectionSummaryText(const DungeonSelectionSnapshot &snapshot)
DungeonWorkbenchResponsiveLayout ResolveDungeonWorkbenchResponsiveLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, bool want_left, bool want_right)
bool DrawDungeonWorkbenchVerticalSplitter(const char *id, float height, float *pane_width, float min_width, float max_width, bool resize_from_left_edge, float collapse_threshold)
absl::Status AddCurrentRoomToPitDamage(zelda3::PitDamageTable *table, uint16_t current_room_id, uint16_t victim_room_id)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
bool ToggleButton(const char *label, bool active, const ImVec2 &size)
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
InputHexResult InputHexByteEx(const char *label, uint8_t *data, float input_width, bool no_step)
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
std::string GetRoomTagLabel(int id)
Convenience function to get a room tag label.
LayerBlendMode
Layer blend modes for compositing.
std::string GetSpriteLabel(int id)
Convenience function to get a sprite label.
bool IsRoomObjectSizeEditable(int object_id)
int GetObjectSubtype(int object_id)
std::string GetOverlordLabel(int id)
Convenience function to get an overlord label.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
LayerType
Layer types for the 4-way visibility system.
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
constexpr int kNumberOfRooms
const std::string RoomEffect[8]
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
bool show_connected_canvas_view
bool show_right_inspector
DungeonWorkbenchResponsiveLayout responsive
DungeonWorkbenchTestRect replace_current
DungeonWorkbenchTestRect add_current
Represents a selected entity in the dungeon editor.
static constexpr float kStatusBarHeight
static constexpr float kContentMinHeightCanvas
static constexpr float kSplitterWidth
static constexpr float kContentMinWidthSidebar
Modern project structure with comprehensive settings consolidation.