yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_destination_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdint>
5
6#include "absl/strings/str_format.h"
11#include "app/gui/core/input.h"
12#include "imgui/imgui.h"
15
16namespace yaze::editor {
17
18namespace {
19
20// Fingerprint the authored inputs to the vanilla stair resolver. Rendering,
21// palette refreshes, and destination header writes must not invalidate a
22// selected stair's mapping; object/door/layout changes can.
24 uint64_t hash = 1469598103934665603ULL;
25 const auto mix = [&](uint64_t value) {
26 hash ^= value;
27 hash *= 1099511628211ULL;
28 };
29 mix(reinterpret_cast<uintptr_t>(room.rom()));
30 mix(static_cast<uint64_t>(room.id()));
31 mix(core::FeatureFlags::get().kEnableCustomObjects);
32 mix(zelda3::CustomObjectManager::Get().asset_generation());
33 const auto metadata = room.CaptureMetadataSnapshot();
34 mix(metadata.blockset);
35 mix(metadata.layout);
36 mix(metadata.floor1);
37 mix(metadata.floor2);
38 mix(static_cast<uint8_t>(metadata.bg2));
39 mix(metadata.layer2_mode);
40 mix(metadata.layer_merging.ID);
41 mix(static_cast<uint8_t>(metadata.collision));
42 mix(static_cast<uint8_t>(metadata.effect));
43 mix(static_cast<uint8_t>(metadata.tag1));
44 mix(static_cast<uint8_t>(metadata.tag2));
45 mix(room.has_custom_collision());
46 const auto mix_objects = [&](const std::vector<zelda3::RoomObject>& objects) {
47 mix(objects.size());
48 for (const auto& object : objects) {
49 mix(static_cast<uint16_t>(object.id_));
50 mix(object.x());
51 mix(object.y());
52 mix(object.size());
53 mix(object.GetLayerValue());
54 mix(static_cast<uint64_t>(object.options()));
55 mix(object.block_behavior_layer());
56 mix(object.torch_reserved_bit());
57 mix(object.lit_);
58 }
59 };
60 mix_objects(room.GetLayout().GetObjects());
61 mix_objects(room.GetTileObjects());
62 mix(room.GetDoors().size());
63 for (const auto& door : room.GetDoors()) {
64 mix(door.position);
65 mix(static_cast<uint8_t>(door.direction));
66 mix(static_cast<uint8_t>(door.type));
67 mix(door.byte1);
68 mix(door.byte2);
69 }
70 // ComposeYazeRoomTilemaps supplies the resolver's basic attribute pass.
71 // Only tile number and flip bits influence that pass; palette and priority
72 // bits can change during harmless presentation refreshes.
73 const auto mix_tilemap = [&](const gfx::BackgroundBuffer& layout,
74 const gfx::BackgroundBuffer& objects) {
75 const auto& layout_words = layout.buffer();
76 const auto& object_words = objects.buffer();
77 for (size_t i = 0; i < zelda3::kRoomTilemapWords; ++i) {
78 const uint16_t object_word =
79 i < object_words.size() ? object_words[i] : 0;
80 const uint16_t layout_word =
81 i < layout_words.size() ? layout_words[i] : 0;
82 mix((object_word != 0 ? object_word : layout_word) & 0xC3FF);
83 }
84 };
85 mix_tilemap(room.bg1_buffer(), room.object_bg1_buffer());
86 mix_tilemap(room.bg2_buffer(), room.object_bg2_buffer());
87 return hash;
88}
89
90} // namespace
91
93 const int room_id = viewer.current_room_id();
94 const auto* room =
95 viewer.rooms() ? viewer.rooms()->GetIfLoaded(room_id) : nullptr;
96 if (!room || room->rom() != viewer.rom()) {
97 ImGui::TextWrapped("Open a room to edit its destinations.");
98 return;
99 }
100 gui::AutoWidgetScope scope("Dungeon/Destinations");
101 ImGui::PushID(&viewer);
102 ImGui::PushID(room_id);
103 auto* storage = ImGui::GetStateStorage();
104 const auto route_key = ImGui::GetID("DestinationRoute");
105 int route = storage->GetInt(route_key, 0);
106 const auto selection = viewer.object_interaction().GetSelectedObjectIndices();
107 const auto inspect_key = ImGui::GetID("StaircaseInspectResult");
108 const auto selected_key = ImGui::GetID("StaircaseInspectSelection");
109 const auto revision_low_key = ImGui::GetID("StaircaseInspectRevisionLow");
110 const auto revision_high_key = ImGui::GetID("StaircaseInspectRevisionHigh");
111 const auto auto_route_key = ImGui::GetID("StaircaseInspectAutoRoute");
112 const int selected =
113 selection.size() == 1 ? static_cast<int>(selection.front()) : -1;
114 const uint64_t source = StairLookupSourceFingerprint(*room);
115 const int revision_low = static_cast<int>(static_cast<uint32_t>(source));
116 const int revision_high = static_cast<int>(source >> 32);
117 if (storage->GetInt(selected_key, -1) != selected ||
118 storage->GetInt(revision_low_key, -1) != revision_low ||
119 storage->GetInt(revision_high_key, -1) != revision_high) {
120 storage->SetInt(selected_key, selected);
121 storage->SetInt(revision_low_key, revision_low);
122 storage->SetInt(revision_high_key, revision_high);
123 storage->SetInt(inspect_key,
124 storage->GetBool(auto_route_key, false) ? 4 : 0);
125 }
126 ImGui::BeginDisabled(selection.size() != 1 || !room->rom() ||
127 room->has_custom_collision());
128 const bool inspect = ImGui::Button("Find selected stair slot", ImVec2(-1, 0));
129 gui::AutoRegisterLastItem("button", "FindStairSlot");
130 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
131 ImGui::SetTooltip(
132 "Select a staircase object to find its header slot. Editing a "
133 "destination does not place a stair or create a return link.");
134 }
135 ImGui::EndDisabled();
136 if (inspect) {
137 // Explicit inspection avoids replaying the room on every ImGui frame.
138 auto input = zelda3::MakeRoomCollisionInput(*room);
139 // The loaded layout is a primary-list prefix ($018834), before the room
140 // stream ($01884A). Preserve that order and the selected object's identity.
141 const auto& layout = room->GetLayout().GetObjects();
142 input.objects.insert(input.objects.begin(), layout.begin(), layout.end());
143 const bool custom_objects =
144 std::any_of(input.objects.begin(), input.objects.end(),
146 const auto mappings =
147 custom_objects
148 ? std::vector<zelda3::StaircaseSlotResolution>{}
149 : zelda3::ResolveVanillaStaircaseSlots(*room->rom(), input);
150 int found = -1;
151 for (const auto& mapping : mappings) {
152 if (mapping.object_index == selection.front() + layout.size() &&
153 mapping.slot) {
154 found = *mapping.slot;
155 break;
156 }
157 }
158 storage->SetInt(inspect_key, custom_objects ? 3 : (found >= 0 ? 1 : 2));
159 if (found >= 0) {
160 route = found + 1;
161 storage->SetInt(route_key, route);
162 storage->SetBool(auto_route_key, true);
163 }
164 }
165 if (room->has_custom_collision()) {
166 ImGui::TextWrapped("Stair lookup is unavailable with custom collision.");
167 } else if (storage->GetInt(inspect_key, 0) == 3) {
168 ImGui::TextWrapped(
169 "Stair lookup is unavailable with active custom object overrides.");
170 } else if (storage->GetInt(inspect_key, 0) == 1) {
171 ImGui::TextWrapped("Selected stair: header slot %d (vanilla preview).",
172 route);
173 } else if (storage->GetInt(inspect_key, 0) == 2) {
174 ImGui::TextWrapped(
175 "No unambiguous staircase trigger was found. The selected "
176 "object may not be a supported stair, or its trigger may "
177 "be overwritten, overlapping, outside the room or overflowing.");
178 } else if (storage->GetInt(inspect_key, 0) == 4) {
179 ImGui::TextWrapped(
180 "Selected stair mapping is stale. Find the slot again. "
181 "The rows below still edit their explicit header slots.");
182 }
183 // Every header slot is an explicit target. A selected stair lookup only
184 // highlights a row; it never retargets the edits or hides other destinations.
185 const auto error_key = ImGui::GetID("DestinationError");
186 const bool editable =
187 !viewer.header_read_only() && viewer.IsObjectInteractionEnabled();
188 int navigate_to = -1;
189 constexpr const char* planes[] = {"Upper BG1", "Lower BG1", "Lower BG2"};
190 if (ImGui::BeginTable(
191 "##DestinationRows", 4,
192 ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
193 ImGui::TableSetupColumn("Route", ImGuiTableColumnFlags_WidthFixed, 49.0f);
194 ImGui::TableSetupColumn("Room", ImGuiTableColumnFlags_WidthFixed, 38.0f);
195 ImGui::TableSetupColumn("Arrival", ImGuiTableColumnFlags_WidthStretch,
196 1.0f);
197 ImGui::TableSetupColumn("##Open", ImGuiTableColumnFlags_WidthFixed, 22.0f);
198 ImGui::TableHeadersRow();
199 for (int row = 0; row < 5; ++row) {
200 ImGui::PushID(row);
201 const bool pit = row == 0;
202 const int index = pit ? 0 : row - 1;
203 const auto room_field = pit ? RoomMetadataField::kHolewarp
205 const auto plane_field = pit ? RoomMetadataField::kPitPlane
207 int destination = pit ? room->holewarp() : room->staircase_room(index);
208 const int plane = pit ? room->CaptureMetadataSnapshot().pit_target_layer
209 : room->staircase_plane(index);
210 auto edit = [&](RoomMetadataField field, int value) {
211 const auto status =
212 viewer.EditRoomMetadata(room_id, {field, value, index});
213 storage->SetBool(error_key, !status.ok());
214 return status.ok();
215 };
216 ImGui::TableNextRow();
217 if (row == route && storage->GetInt(inspect_key, 0) == 1) {
218 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
219 ImGui::GetColorU32(ImGuiCol_Header));
220 }
221 ImGui::TableNextColumn();
222 ImGui::AlignTextToFramePadding();
223 if (pit)
224 ImGui::TextUnformatted("Pit");
225 else
226 ImGui::Text("Stair %d", row);
227 ImGui::TableNextColumn();
228 ImGui::BeginDisabled(!editable);
229 ImGui::SetNextItemWidth(-1);
230 int requested = destination;
232 "##DestinationRoom", ImGuiDataType_S32, &requested, "%02X",
233 ImGuiInputTextFlags_CharsHexadecimal,
234 {reinterpret_cast<uintptr_t>(viewer.rooms()),
235 static_cast<uint64_t>(room_id), static_cast<uint64_t>(row)})) {
236 if (edit(room_field, requested))
237 destination = requested;
238 }
239 gui::AutoRegisterLastItem("input_hex", absl::StrFormat("Room/%d", row));
240 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
241 ImGui::SetTooltip(
242 "Destination room byte (hex 00-FF); stays on page %s.",
243 room_id >= 0x100 ? "1xx" : "0xx");
244 }
245 ImGui::TableNextColumn();
246 const auto plane_label = plane >= 0 && plane < 3
247 ? std::string(planes[plane])
248 : absl::StrFormat("%02X Unmapped", plane);
249 ImGui::SetNextItemWidth(-1);
250 const bool open =
251 ImGui::BeginCombo("##DestinationPlane", plane_label.c_str());
252 gui::AutoRegisterLastItem("combo", absl::StrFormat("Plane/%d", row));
253 if (open) {
254 for (int value = 0; value < 3; ++value) {
255 if (ImGui::Selectable(planes[value], plane == value))
256 edit(plane_field, value);
257 gui::AutoRegisterLastItem("selectable",
258 absl::StrFormat("Plane/%d/%d", row, value));
259 }
260 ImGui::EndCombo();
261 }
262 ImGui::EndDisabled();
263 ImGui::TableNextColumn();
264 // $01C31F/$01C322: the header stores one byte and retains this room's page.
265 const int destination_room =
267 ImGui::BeginDisabled(!viewer.CanNavigateRooms());
268 if (ImGui::Button(">", ImVec2(-1, 0)))
269 navigate_to = destination_room;
270 gui::AutoRegisterLastItem("button", absl::StrFormat("Open/%d", row));
271 ImGui::EndDisabled();
272 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
273 ImGui::SetTooltip("Open %03X · %s", destination_room,
275 destination_room)
276 .c_str());
277 }
278 ImGui::PopID();
279 }
280 ImGui::EndTable();
281 }
282 if (storage->GetBool(error_key, false)) {
283 ImGui::TextWrapped(
284 "Destination edit rejected. Use room 00–FF and a supported arrival "
285 "layer.");
286 }
287 if (!editable)
288 ImGui::TextWrapped("This view is read-only.");
289
290 ImGui::PopID();
291 ImGui::PopID();
292 // Navigation may change the current room/view. Do not access room state after it.
293 if (navigate_to >= 0)
294 viewer.NavigateToRoom(navigate_to);
295}
296
297} // namespace yaze::editor
static Flags & get()
Definition features.h:119
absl::Status EditRoomMetadata(int room_id, const RoomMetadataEdit &edit)
DungeonObjectInteraction & object_interaction()
const project::YazeProject * project() const
std::vector< size_t > GetSelectedObjectIndices() const
zelda3::Room * GetIfLoaded(int room_id)
std::vector< uint16_t > & buffer()
RAII scope that enables automatic widget registration.
static CustomObjectManager & Get()
const std::vector< RoomObject > & GetObjects() const
Definition room_layout.h:34
const std::vector< Door > & GetDoors() const
Definition room.h:413
auto & object_bg2_buffer()
Definition room.h:1151
auto rom() const
Definition room.h:1082
const RoomLayout & GetLayout() const
Definition room.h:442
auto & bg1_buffer()
Definition room.h:1145
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
bool has_custom_collision() const
Definition room.h:563
MetadataSnapshot CaptureMetadataSnapshot() const
Definition room.cc:861
auto & object_bg1_buffer()
Definition room.h:1149
auto & bg2_buffer()
Definition room.h:1146
int id() const
Definition room.h:1012
struct destination destination
Room transition destination.
std::string GetRoomLabel(const project::YazeProject *project, int room_id)
Editors are the view controllers for the application.
void DrawDungeonDestinationEditor(DungeonCanvasViewer &viewer)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
Definition input.cc:392
constexpr int ResolveHeaderDestinationRoom(int source_room_id, uint8_t header_byte)
constexpr size_t kRoomTilemapWords
bool HasActiveCustomObjectOverride(const RoomObject &object)
RoomCollisionInput MakeRoomCollisionInput(const Room &room, const RoomTilemaps &tilemaps)
std::vector< StaircaseSlotResolution > ResolveVanillaStaircaseSlots(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
Room transition destination.
Definition zelda.h:448
Automatic widget registration helpers for ImGui Test Engine integration.