8#include <initializer_list>
12#include "absl/status/status.h"
13#include "absl/strings/str_format.h"
29#include "imgui/imgui.h"
40 static constexpr const char* kNames[] = {
41 "Off",
"Parallax",
"Dark",
"On top",
"Translucent",
42 "Addition",
"Normal",
"Transparent",
"Dark room"};
43 constexpr int kNameCount =
sizeof(kNames) /
sizeof(kNames[0]);
44 return (value >= 0 && value < kNameCount) ? kNames[value] :
"Unknown";
48 static constexpr const char* kNames[] = {
"One",
"Both",
"Both + Scroll",
49 "Moving Floor",
"Moving Water"};
50 constexpr int kNameCount =
sizeof(kNames) /
sizeof(kNames[0]);
51 return (value >= 0 && value < kNameCount) ? kNames[value] :
"Unknown";
66 auto* rooms = viewer.
rooms();
67 if (!rooms || room_id < 0 || room_id >=
static_cast<int>(rooms->size()))
70 const bool stack_choices =
71 ImGui::GetContentRegionAvail().x < ImGui::GetFontSize() * 25.0f;
72 const ImGuiID error_id =
73 ImGui::GetID(absl::StrFormat(
"RoomMetadataError/%d", room_id).c_str());
75 auto* inspector_storage = ImGui::GetStateStorage();
77 reinterpret_cast<uintptr_t
>(rooms),
static_cast<uint64_t
>(room_id)};
80 room_id, {.field = field, .value = value, .index = index});
81 inspector_storage->SetBool(error_id, !status.ok());
83 auto begin_properties = [](
const char* id,
float label_width = 104.0f) {
84 if (!ImGui::BeginTable(
85 id, 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
88 ImGui::TableSetupColumn(
"Property", ImGuiTableColumnFlags_WidthFixed,
90 ImGui::TableSetupColumn(
"Value", ImGuiTableColumnFlags_WidthStretch, 1.0f);
93 auto property_row = [](
const char* label) {
95 ImGui::TableNextColumn();
96 ImGui::AlignTextToFramePadding();
97 ImGui::TextUnformatted(label);
98 ImGui::TableNextColumn();
99 ImGui::SetNextItemWidth(-1);
101 auto draw_hex = [&](
const char* label,
const char* id,
int value,
104 ImGui::SetNextItemWidth(
105 ImGui::CalcTextSize(max_value > 0xFF ?
"0000" :
"000").x +
106 ImGui::GetStyle().FramePadding.x * 2.0f);
107 auto target = identity;
108 target.element =
static_cast<uint64_t
>(field);
109 target.component =
static_cast<uint64_t
>(index);
111 id, ImGuiDataType_S32, &value, max_value > 0xFF ?
"%03X" :
"%02X",
112 ImGuiInputTextFlags_CharsHexadecimal, target)) {
113 apply(field, value, index);
117 :
"Dungeon/Workbench");
120 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
121 const char* description = label;
123 description =
"Sprite graphics set";
125 description =
"Background 1 floor pattern";
127 description =
"Background 2 floor pattern";
128 ImGui::SetTooltip(
"%s (hex 00-%X). Enter or leave the field to apply.",
129 description, max_value);
132 auto draw_choice = [&](
const char* label,
const char* id,
int value,
135 ImGui::TextUnformatted(label);
136 ImGui::SetNextItemWidth(-1);
140 const std::string preview = name(value);
141 const bool open = ImGui::BeginCombo(
id, preview.c_str());
144 :
"Dungeon/Workbench");
147 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
148 ImGui::SetTooltip(
"%s: %s (0x%02X)", label, preview.c_str(), value);
151 for (
int option = 0; option < count; ++option) {
152 const std::string option_label = name(option);
153 ImGui::PushID(option);
154 if (ImGui::Selectable(option_label.c_str(), value == option)) {
155 apply(field, option);
159 compact ?
"Dungeon/RoomProperties" :
"Dungeon/Workbench");
161 absl::StrFormat(
"%s/%d",
id + 2, option));
163 if (value == option) {
164 ImGui::SetItemDefaultFocus();
166 if (ImGui::IsItemHovered())
167 ImGui::SetTooltip(
"%s (0x%02X)", option_label.c_str(), option);
173 auto choice_group = [&](
const char* id,
float label_width,
auto draw) {
176 }
else if (begin_properties(
id, label_width)) {
181 auto label_width = [](std::initializer_list<const char*> labels) {
183 for (
const auto* label : labels)
184 width = std::max(width, ImGui::CalcTextSize(label).x);
191 if (ImGui::BeginTable(
"##CompactRoomGraphics", 4,
192 ImGuiTableFlags_SizingStretchProp)) {
193 ImGui::TableSetupColumn(
"Property 1", ImGuiTableColumnFlags_WidthFixed,
194 label_width({
"Layout",
"Palette"}));
195 ImGui::TableSetupColumn(
"Value 1", ImGuiTableColumnFlags_WidthStretch);
196 ImGui::TableSetupColumn(
"Property 2", ImGuiTableColumnFlags_WidthFixed,
197 label_width({
"Blockset",
"Spriteset"}));
198 ImGui::TableSetupColumn(
"Value 2", ImGuiTableColumnFlags_WidthStretch);
199 draw_hex(
"Layout",
"##Lyr", room.
layout_id(), 0x07,
201 draw_hex(
"Blockset",
"##BS", room.
blockset(), 0x51,
203 draw_hex(
"Palette",
"##Pal", room.
palette(), 0x47,
205 draw_hex(
"Spriteset",
"##SS", room.
spriteset(),
209 ImGui::EndDisabled();
210 if (inspector_storage->GetBool(error_id))
212 "Edit not applied. Hover the field for its supported range.");
217 const bool show_core = show_all ||
220 const bool show_mechanics = show_all ||
223 const bool show_message =
225 const bool show_tags =
229 choice_group(
"##WorkbenchRoomTags", label_width({
"Tag 1",
"Tag 2"}), [&] {
230 const auto tag_name = [&](
int value) {
233 for (
int index = 0; index < 2; ++index) {
234 const int tag = index == 0 ? room.
tag1() : room.
tag2();
236 index == 0 ?
"Tag 1" :
"Tag 2",
237 index == 0 ?
"##RoomHeaderTag1" :
"##RoomHeaderTag2", tag, 0x40,
244 if (inspector_storage->GetBool(error_id)) {
246 "Edit not applied. Check that this room is editable and the value "
247 "is within the field's supported range. Hover the field for its "
253 ImGui::SeparatorText(
"Room properties");
256 const float available = ImGui::GetContentRegionAvail().x;
257 const float property_width =
258 label_width({
"Layout",
"Blockset",
"Floor 1",
"Floor 2",
"Palette",
259 "Spriteset",
"Message",
"Pit damage"});
260 const float value_width =
261 ImGui::CalcTextSize(
"0000").x + ImGui::GetStyle().FramePadding.x * 2.0f;
262 const float pair_width =
263 property_width + value_width + ImGui::GetStyle().CellPadding.x * 4.0f;
264 const int field_count = 7 + (draw_pit_damage ? 1 : 0);
266 horizontal ? std::clamp(
static_cast<int>(std::floor(
267 available / std::max(pair_width, 1.0f))),
270 const ImGuiTableFlags graphics_flags =
271 ImGuiTableFlags_RowBg |
273 ? ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX
274 : ImGuiTableFlags_SizingStretchProp);
275 if (ImGui::BeginTable(
"##WorkbenchRoomGraphics", pairs * 2,
277 for (
int pair = 0; pair < pairs; ++pair) {
278 ImGui::TableSetupColumn(
"Property", ImGuiTableColumnFlags_WidthFixed,
280 ImGui::TableSetupColumn(
"Value",
281 horizontal ? ImGuiTableColumnFlags_WidthFixed
282 : ImGuiTableColumnFlags_WidthStretch,
283 horizontal ? value_width : 1.0f);
285 draw_hex(
"Layout",
"##RoomHeaderLayout", room.
layout_id(), 0x07,
287 draw_hex(
"Blockset",
"##RoomHeaderBlockset", room.
blockset(), 0x51,
289 draw_hex(
"Floor 1",
"##RoomHeaderFloor1", room.
floor1(), 0x0F,
291 draw_hex(
"Floor 2",
"##RoomHeaderFloor2", room.
floor2(), 0x0F,
293 draw_hex(
"Palette",
"##RoomHeaderPalette", room.
palette(), 0x47,
295 draw_hex(
"Spriteset",
"##RoomHeaderSpriteset", room.
spriteset(),
298 draw_hex(
"Message",
"##RoomHeaderMessage", room.
message_id(), 0x0FFF,
300 if (draw_pit_damage) {
301 property_row(
"Pit damage");
310 if (begin_properties(
"##WorkbenchRoomMessage", label_width({
"Message"}))) {
311 draw_hex(
"Message",
"##RoomHeaderMessage", room.
message_id(), 0x0FFF,
316 if (show_mechanics) {
318 "##WorkbenchRoomHeader",
319 label_width({
"Layer 2 type",
"Layer 2 mode",
"Collision"}), [&] {
320 draw_choice(
"Layer 2 type",
"##RoomHeaderBg2",
323 draw_choice(
"Layer 2 mode",
"##RoomHeaderEffect", room.
effect(), 8,
325 draw_choice(
"Collision",
"##RoomHeaderCollision", room.
collision(), 5,
329 ImGui::EndDisabled();
334 int canvas_x,
int canvas_y) {
336 std::string info_text;
337 if (
object.id_ >= 0x100) {
339 absl::StrFormat(
"0x%03X %s (X:%d Y:%d S:0x%02X)",
object.id_,
340 name.c_str(),
object.x_,
object.y_,
object.size_);
343 absl::StrFormat(
"0x%02X %s (X:%d Y:%d S:0x%02X)",
object.id_,
344 name.c_str(),
object.x_,
object.y_,
object.size_);
351 LOG_DEBUG(
"[LoadAndRender]",
"START room_id=%d", room_id);
354 LOG_DEBUG(
"[LoadAndRender]",
"ERROR: Invalid room ID");
355 return absl::InvalidArgumentError(
"Invalid room ID");
359 LOG_DEBUG(
"[LoadAndRender]",
"ERROR: ROM not loaded");
360 return absl::FailedPreconditionError(
"ROM not loaded");
364 LOG_DEBUG(
"[LoadAndRender]",
"ERROR: Room data not available");
365 return absl::FailedPreconditionError(
"Room data not available");
370 LOG_DEBUG(
"[LoadAndRender]",
"ERROR: Room data unavailable");
371 return absl::InvalidArgumentError(
"Invalid room ID");
373 auto& room = *room_ptr;
374 LOG_DEBUG(
"[LoadAndRender]",
"Got room reference");
377 LOG_ERROR(
"[LoadAndRender]",
"GameData not available");
378 return absl::FailedPreconditionError(
"GameData not available");
381 if (!dungeon_main.empty()) {
388 LOG_DEBUG(
"[LoadAndRender]",
"Palette loaded: group_id=%zu",
392 LOG_DEBUG(
"[LoadAndRender]",
"Calling room.RenderRoomGraphics()...");
395 "RenderRoomGraphics() complete - room buffers self-contained");
398 return absl::OkStatus();
404 ImGui::BeginDisabled();
407 constexpr ImGuiTableFlags kPropsTableFlags =
408 ImGuiTableFlags_NoPadOuterX | ImGuiTableFlags_NoBordersInBody;
410 if (ImGui::BeginTable(
"##RoomPropsTable", 2, kPropsTableFlags)) {
411 const float nav_col_width = (ImGui::GetFrameHeight() * 4.0f) +
412 (ImGui::GetStyle().ItemSpacing.x * 3.0f) +
413 (ImGui::GetStyle().FramePadding.x * 2.0f);
414 ImGui::TableSetupColumn(
"NavCol", ImGuiTableColumnFlags_WidthFixed,
416 ImGui::TableSetupColumn(
"PropsCol", ImGuiTableColumnFlags_WidthStretch);
418 ImGui::TableNextRow();
419 ImGui::TableNextColumn();
421 ImGui::TableNextColumn();
425 ImGui::TableNextRow();
426 ImGui::TableNextColumn();
428 ImGui::TableNextColumn();
436 ImGui::EndDisabled();
439 ImGui::CollapsingHeader(
"Destinations##RoomDestinations")) {
449 auto room_if_valid = [](
int candidate) -> std::optional<int> {
465 auto make_tooltip = [&](
const std::optional<int>& target,
466 const char* direction) -> std::string {
467 if (!target.has_value()) {
470 return absl::StrFormat(
471 "%s: [%03X] %s", direction, *target,
475 auto nav_button = [&](
const char* id, ImGuiDir dir,
476 const std::optional<int>& target,
477 const std::string& tooltip) {
478 const bool enabled = target.has_value();
480 ImGui::BeginDisabled();
482 const bool pressed = ImGui::ArrowButton(
id, dir);
484 ImGui::EndDisabled();
486 if (enabled && ImGui::IsItemHovered() && !tooltip.empty()) {
487 ImGui::SetTooltip(
"%s", tooltip.c_str());
489 if (pressed && enabled) {
498 ImGui::PushID(room_id);
500 nav_button(
"##RoomNavWest", ImGuiDir_Left, west, make_tooltip(west,
"West"));
502 nav_button(
"##RoomNavNorth", ImGuiDir_Up, north,
503 make_tooltip(north,
"North"));
505 nav_button(
"##RoomNavSouth", ImGuiDir_Down, south,
506 make_tooltip(south,
"South"));
508 nav_button(
"##RoomNavEast", ImGuiDir_Right, east, make_tooltip(east,
"East"));
515 ImGui::AlignTextToFramePadding();
518 ImGui::SameLine(0, 6);
522 if (ImGui::IsItemHovered()) {
523 ImGui::SetTooltip(tr(
524 "Room changes are pending in the editor. Apply Room writes them to "
525 "the loaded ROM buffer."));
551 ImGui::TextDisabled(tr(
"Floor: %d | Effect: %d | Tag1: %d | Tag2: %d"),
553 if (ImGui::CollapsingHeader(tr(
"Chest contents"))) {
564 ImGui::PushID(
"RecentRooms");
566 if (ImGui::IsItemHovered()) {
567 ImGui::SetTooltip(tr(
"Recently visited rooms"));
572 if (recent_room == room_id) {
575 ImGui::SameLine(0, 3);
576 const std::string button_label =
577 absl::StrFormat(
"%03X##RecentRoom%d", recent_room, rendered);
578 if (ImGui::SmallButton(button_label.c_str())) {
582 if (ImGui::IsItemHovered()) {
584 "[%03X] %s", recent_room,
598 if (ImGui::Button(tr(
"View")))
599 ImGui::OpenPopup(
"##RoomViewOptions");
600 if (ImGui::BeginPopup(
"##RoomViewOptions")) {
611 int current_filter = interaction.GetLayerFilter();
613 auto radio = [&](
const char* label,
int filter) {
614 if (ImGui::RadioButton(label, current_filter == filter)) {
615 interaction.SetLayerFilter(filter);
std::vector< int > recently_visited_rooms_
zelda3::GameData * game_data_
DungeonRoomStore * rooms() const
void DrawRecentRoomBreadcrumbs(int room_id)
absl::Status LoadAndRenderRoomGraphics(int room_id)
void DrawRoomNavigation(int room_id)
absl::Status EditRoomMetadata(int room_id, const RoomMetadataEdit &edit)
bool compact_header_mode_
DungeonObjectInteraction object_interaction_
const project::YazeProject * project_
void DrawCompactLayerToggles(int room_id)
std::function< void(bool)> pin_callback_
void DrawLayerControls(zelda3::Room &room, int room_id)
bool IsObjectInteractionEnabled() const
bool header_read_only() const
zelda3::RoomLayerManager & GetRoomLayerManager(int room_id)
uint8_t current_entrance_blockset_
gfx::PaletteGroup current_palette_group_
bool CanNavigateRooms() const
uint64_t current_palette_group_id_
DungeonRoomStore * rooms_
const project::YazeProject * project() const
void NavigateToRoom(int target_room)
void DrawRoomHeader(zelda3::Room &room, int room_id)
std::function< void(int)> room_navigation_callback_
void DisplayObjectInfo(const gui::CanvasRuntime &rt, const zelda3::RoomObject &object, int canvas_x, int canvas_y)
std::function< void(int, int)> room_swap_callback_
void DrawRoomPropertyTable(zelda3::Room &room, int room_id)
void SetLayersMerged(bool merged)
zelda3::Room * TryEnsureRoom(int room_id)
static constexpr int kLayer3
static constexpr int kLayer1
static constexpr int kLayer2
static constexpr int kLayerAll
static ThemeManager & Get()
uint16_t message_id() const
bool HasUnsavedChanges() const
void ReloadGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
CollisionKey collision() const
uint8_t spriteset() const
int ResolveDungeonPaletteId() const
uint8_t layout_id() const
#define ICON_MD_EXPAND_LESS
#define ICON_MD_SELECT_ALL
#define ICON_MD_EXPAND_MORE
#define LOG_DEBUG(category, format,...)
#define LOG_ERROR(category, format,...)
std::string GetLayer2ModeName(int value)
const char * GetCollisionName(int value)
const char * GetBg2ModeName(int value)
std::string GetRoomTagLabel(const project::YazeProject *project, int id)
std::string GetRoomLabel(const project::YazeProject *project, int room_id)
Editors are the view controllers for the application.
void DrawDungeonOverlayControls(DungeonCanvasViewer &viewer)
int NeighborRoomId(int room_id, zelda3::DoorDirection dir)
void DrawDungeonDestinationEditor(DungeonCanvasViewer &viewer)
void DrawDungeonRoomPropertyFields(DungeonCanvasViewer &viewer, zelda3::Room &room, int room_id, RoomPropertyLayout layout, const std::function< void()> &draw_pit_damage)
void DrawDungeonChestEditor(int room_id, zelda3::Room &room, DungeonCanvasViewer &viewer)
void DrawDungeonLayerControls(DungeonCanvasViewer &viewer, int room_id, float height)
bool ThemedIconButton(const char *icon, const char *tooltip, const ImVec2 &size, bool is_active, bool is_disabled, const char *panel_id, const char *anim_id)
Draw a standard icon button with theme-aware colors.
ImVec4 ConvertColorToImVec4(const Color &color)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
ImVec2 ScaledSize(float base_width, float base_height)
Returns a DPI-scaled version of a fixed pixel size.
void DrawText(const CanvasRuntime &rt, const std::string &text, int x, int y)
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
constexpr int kMaxDungeonSpriteset
gfx::PaletteGroup BuildDungeonRenderPaletteGroupFromGameData(const gfx::SnesPalette &dungeon_palette, const GameData *game_data)
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
const std::string RoomEffect[8]
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)
PaletteGroup dungeon_main
gfx::PaletteGroupMap palette_groups