yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_chest_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <functional>
5#include <optional>
6#include <string>
7#include <string_view>
8
9#include "absl/strings/ascii.h"
10#include "absl/strings/match.h"
11#include "absl/strings/str_format.h"
14#include "app/gui/core/input.h"
15#include "core/project.h"
16#include "imgui/imgui.h"
17#include "imgui/imgui_internal.h"
21#include "zelda3/dungeon/room.h"
24
25namespace yaze::editor {
26namespace {
27
28std::string ItemPreview(uint8_t item_id, const project::YazeProject* project) {
29 return absl::StrFormat("%02X %s", item_id,
30 GetDungeonChestItemLabel(item_id, project));
31}
32
34 int room_id, size_t index, uint8_t item_id, bool big_chest) {
35 const auto status = viewer.EditChest(room_id, index, item_id, big_chest);
36 state.error = status.ok() ? "" : std::string(status.message());
37}
38
39std::optional<size_t> FindChestObject(const zelda3::Room& room,
40 size_t ordinal) {
41 const auto& objects = room.GetTileObjects();
42 for (size_t i = 0; i < objects.size(); ++i) {
43 if (zelda3::ChestIndexForObject(objects, i) == ordinal) {
44 return i;
45 }
46 }
47 return std::nullopt;
48}
49
50void DrawChestPlacement(int room_id, bool has_mapping,
51 DungeonCanvasViewer& viewer,
53 const bool active_room = viewer.current_room_id() == room_id;
54 ImGui::BeginDisabled(viewer.header_read_only() || !active_room ||
55 !has_mapping);
56 const bool inline_buttons =
57 ImGui::GetContentRegionAvail().x >=
58 ImGui::CalcTextSize("Add small chestAdd big chest").x +
59 ImGui::GetStyle().FramePadding.x * 4 +
60 ImGui::GetStyle().ItemSpacing.x;
61 const float width = inline_buttons ? (ImGui::GetContentRegionAvail().x -
62 ImGui::GetStyle().ItemSpacing.x) /
63 2
64 : -1;
65 for (int big = 0; big <= 1; ++big) {
66 if (big && inline_buttons) {
67 ImGui::SameLine();
68 }
69 if (ImGui::Button(big ? "Add big chest" : "Add small chest",
70 ImVec2(width, 0))) {
71 const int id = big ? 0xFB1 : 0xF99;
72 viewer.SetPreviewObject(
74 viewer.SetObjectInteractionEnabled(true);
75 state.error.clear();
76 }
77 gui::AutoRegisterLastItem("button", big ? "ChestAddBig" : "ChestAddSmall");
78 }
79 ImGui::EndDisabled();
80 const auto* preview = viewer.object_interaction().GetPlacementPreview();
81 if (active_room && preview && zelda3::IsStatefulChestObjectId(preview->id_)) {
82 ImGui::TextWrapped(
83 "Click in the canvas to place a %s chest. Escape cancels.",
84 preview->id_ == 0xFB1 ? "big" : "small");
85 } else if (!active_room) {
86 ImGui::TextWrapped(
87 "Open this room in the canvas to place or select a chest.");
88 }
89 ImGui::TextWrapped(
90 "New chests start with 1 Rupee. Choose a chest below to change its "
91 "reward.");
92}
93
94bool DrawChestActions(int room_id, size_t index, bool has_mapping,
95 zelda3::Room& room, DungeonCanvasViewer& viewer,
97 const auto object_index =
98 has_mapping ? FindChestObject(room, index) : std::nullopt;
99 ImGui::BeginDisabled(!object_index || viewer.current_room_id() != room_id);
100 if (ImGui::Button("Select in canvas", ImVec2(-1, 0))) {
101 const auto& object = room.GetTileObjects()[*object_index];
103 viewer.object_interaction().SetSelectedObjects({*object_index});
104 viewer.ScrollToTile(object.x_, object.y_);
105 viewer.TriggerCanvasPingRect(object.x_ * 8, object.y_ * 8, object.width_,
106 object.height_);
107 }
108 gui::AutoRegisterLastItem("button", "ChestSelect");
109 ImGui::EndDisabled();
110 ImGui::BeginDisabled(viewer.header_read_only() || !object_index);
111 bool deleted = false;
112 if (ImGui::Button("Delete chest", ImVec2(-1, 0))) {
113 const auto status = viewer.DeleteChest(room_id, index);
114 state.error = status.ok() ? "" : std::string(status.message());
115 deleted = status.ok();
116 }
117 gui::AutoRegisterLastItem("button", "ChestDelete");
118 if (ImGui::IsItemHovered()) {
119 ImGui::SetTooltip(
120 "Remove the chest object and its reward. Undo restores both.");
121 }
122 ImGui::EndDisabled();
123 return deleted;
124}
125
126void DrawItemChoice(uint8_t item_id, DungeonCanvasViewer& viewer,
127 std::array<char, 64>& search_buffer,
128 const std::function<void(uint8_t)>& select_item) {
129 ImGui::TextUnformatted("Reward");
130 ImGui::SetNextItemWidth(-1);
131 const bool open = ImGui::BeginCombo(
132 "##ChestReward", ItemPreview(item_id, viewer.project()).c_str(),
133 ImGuiComboFlags_HeightLarge);
134 gui::AutoRegisterLastItem("combo", "ChestReward");
135 if (!open) {
136 return;
137 }
138 ImGui::SetNextItemWidth(-1);
139 const bool search_changed =
140 ImGui::InputTextWithHint("##ChestSearch", "Search name or hex ID...",
141 search_buffer.data(), search_buffer.size());
142 gui::AutoRegisterLastItem("input", "ChestSearch");
143 ImGui::Separator();
144 const std::string search = absl::AsciiStrToLower(search_buffer.data());
145 // Keep search visible when opening on a late item. The selected result may
146 // scroll into view without moving the search control out of the popup.
147 if (ImGui::BeginChild("##ChestRewardChoices",
148 ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 8))) {
149 if (search_changed) {
150 ImGui::SetScrollY(0);
151 }
152 const auto* labels =
153 viewer.project() ? &viewer.project()->resource_labels : nullptr;
154 bool has_matches = false;
155 for (int id = 0; id <= 0xFF; ++id) {
156 // Retain any existing unknown value and explicitly named hack items, but
157 // do not suggest unverified high IDs as ordinary vanilla rewards.
158 if (id >= static_cast<int>(
160 id != item_id &&
161 std::string_view(zelda3::GetItemReceiptLabelSource(
162 static_cast<uint8_t>(id), labels)) != "project") {
163 continue;
164 }
165 const std::string label =
166 ItemPreview(static_cast<uint8_t>(id), viewer.project());
167 if (!absl::StrContains(absl::AsciiStrToLower(label), search)) {
168 continue;
169 }
170 has_matches = true;
171 if (ImGui::Selectable(label.c_str(), item_id == id)) {
172 select_item(static_cast<uint8_t>(id));
173 // The selectable is in a child window, so close its containing combo
174 // explicitly after a committed choice.
175 ImGui::CloseCurrentPopup();
176 }
177 gui::AutoRegisterLastItem("selectable",
178 absl::StrFormat("ChestReward%02X", id));
179 if (item_id == id && search.empty()) {
180 ImGui::SetItemDefaultFocus();
181 }
182 }
183 if (!has_matches) {
184 ImGui::TextDisabled("No matching rewards");
185 }
186 }
187 ImGui::EndChild();
188 ImGui::EndCombo();
189}
190
191std::vector<uint64_t> ChestObjectSignature(const zelda3::Room& room) {
192 std::vector<uint64_t> result;
193 for (const auto& object : room.GetTileObjects()) {
194 result.push_back(static_cast<uint16_t>(object.id_) |
195 (static_cast<uint64_t>(object.x_) << 16) |
196 (static_cast<uint64_t>(object.y_) << 24) |
197 (static_cast<uint64_t>(object.size_) << 32) |
198 (static_cast<uint64_t>(object.layer_) << 40));
199 }
200 return result;
201}
202
203std::vector<uint16_t> ChestRecordSignature(const zelda3::Room& room) {
204 std::vector<uint16_t> result;
205 for (const auto& chest : room.GetChests()) {
206 result.push_back(chest.id | (static_cast<uint16_t>(chest.size) << 8));
207 }
208 return result;
209}
210
212 const DungeonCanvasViewer& viewer,
213 const DungeonChestEditDialogState& state) {
214 const auto* room =
215 viewer.rooms() ? viewer.rooms()->GetIfLoaded(state.room_id) : nullptr;
216 if (!room || !room->AreChestsLoaded() ||
217 reinterpret_cast<uintptr_t>(room) != state.room_identity ||
218 reinterpret_cast<uintptr_t>(viewer.rom()) != state.rom_identity ||
219 state.index >= room->GetChests().size() ||
220 ChestObjectSignature(*room) != state.source_objects ||
221 ChestRecordSignature(*room) != state.source_chests) {
222 return absl::FailedPreconditionError(
223 "This chest changed while the editor was open. Cancel and reopen it.");
224 }
225 if (viewer.header_read_only() || !viewer.IsObjectInteractionEnabled()) {
226 return absl::FailedPreconditionError("Chest contents are read-only.");
227 }
228 return absl::OkStatus();
229}
230
231} // namespace
232
233absl::Status DungeonCanvasViewer::OpenChestEditor(int room_id, size_t index) {
234 auto* room = rooms_ ? rooms_->GetIfLoaded(room_id) : nullptr;
236 !chest_edit_callback_ || !room || !room->AreChestsLoaded() ||
237 room->rom() != rom_) {
238 return absl::FailedPreconditionError("Chest contents are not editable");
239 }
240 if (index >= room->GetChests().size()) {
241 return absl::InvalidArgumentError(
242 "Select an existing chest contents record");
243 }
244 auto& state = chest_edit_dialog_state_;
245 state.open = true;
246 state.request_open = true;
247 state.room_id = room_id;
248 state.index = index;
249 state.room_identity = reinterpret_cast<uintptr_t>(room);
250 state.rom_identity = reinterpret_cast<uintptr_t>(rom_);
251 state.source_objects = ChestObjectSignature(*room);
252 state.source_chests = ChestRecordSignature(*room);
253 state.item_id = room->GetChests()[index].id;
254 state.big_chest = room->GetChests()[index].size;
255 state.mapping_valid = zelda3::ValidateChestObjectMapping(
256 room->GetTileObjects(), room->GetChests())
257 .ok();
258 state.search.fill(0);
259 state.error.clear();
260 return absl::OkStatus();
261}
262
264 if (!ImGui::GetCurrentContext()) {
265 return;
266 }
267 auto& state = chest_edit_dialog_state_;
268 const int frame = ImGui::GetFrameCount();
269 if (state.last_draw_frame == frame) {
270 return;
271 }
272 state.last_draw_frame = frame;
273 // Use a viewer-owned ID independent of the canvas/inspector's current child.
274 // Workbench and standalone callers can safely render this once per frame.
275 const auto owner = reinterpret_cast<uintptr_t>(this);
276 ImGui::PushOverrideID(ImHashData(&owner, sizeof(owner)));
277 const std::string popup_name =
278 absl::StrFormat("Edit Chest###DungeonChestDialog%p", this);
279 if (state.show_diagnostics) {
280 ImGui::SetNextWindowSize(ImVec2(380, 580), ImGuiCond_FirstUseEver);
281 const std::string title =
282 absl::StrFormat("Chest Records###ChestRecords%p", this);
283 if (ImGui::Begin(title.c_str(), &state.show_diagnostics,
284 ImGuiWindowFlags_NoSavedSettings)) {
285 if (auto* room = rooms_ ? rooms_->GetIfLoaded(state.diagnostics_room_id)
286 : nullptr) {
287 DrawDungeonChestEditor(state.diagnostics_room_id, *room, *this);
288 } else {
289 ImGui::TextUnformatted("Open this room to load its chest records.");
290 }
291 }
292 ImGui::End();
293 }
294 if (state.request_open) {
295 ImGui::OpenPopup(popup_name.c_str());
296 state.request_open = false;
297 }
298 if (state.open) {
299 ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Appearing);
300 }
301 if (ImGui::BeginPopupModal(popup_name.c_str(), &state.open,
302 ImGuiWindowFlags_AlwaysAutoResize |
303 ImGuiWindowFlags_NoSavedSettings)) {
304 {
305 gui::AutoWidgetScope scope("Dungeon/ChestDialog");
306 ImGui::Text("Room %03X · Chest %zu", state.room_id, state.index + 1);
307 const auto target_status = ValidateChestDialogTarget(*this, state);
308 ImGui::BeginDisabled(!target_status.ok());
309 DrawItemChoice(static_cast<uint8_t>(state.item_id), *this, state.search,
310 [&](uint8_t id) { state.item_id = id; });
311 ImGui::BeginDisabled(!state.mapping_valid);
312 ImGui::Checkbox("Big chest", &state.big_chest);
313 gui::AutoRegisterLastItem("checkbox", "ChestBig");
314 ImGui::EndDisabled();
315 if (!state.mapping_valid) {
316 ImGui::TextWrapped(
317 "Chest records do not match their objects. Only the reward can be "
318 "changed.");
319 }
320 ImGui::SetNextItemWidth(ImGui::CalcTextSize("000").x +
321 ImGui::GetStyle().FramePadding.x * 2);
322 ImGui::InputScalar("Item ID", ImGuiDataType_S32, &state.item_id, nullptr,
323 nullptr, "%02X", ImGuiInputTextFlags_CharsHexadecimal);
324 gui::AutoRegisterLastItem("input_int", "ChestItemId");
325 ImGui::EndDisabled();
326 if (!target_status.ok()) {
327 ImGui::TextWrapped("%s", std::string(target_status.message()).c_str());
328 } else if (!state.error.empty()) {
329 ImGui::TextWrapped("Edit not applied: %s", state.error.c_str());
330 }
331 ImGui::Separator();
332 ImGui::BeginDisabled(!target_status.ok());
333 if (ImGui::Button("OK")) {
334 const auto status = ValidateChestDialogTarget(*this, state);
335 if (!status.ok()) {
336 state.error = std::string(status.message());
337 } else if (state.item_id < 0 || state.item_id > 0xFF) {
338 state.error = "Item ID must fit one byte (00-FF).";
339 } else {
340 const auto status =
341 EditChest(state.room_id, state.index,
342 static_cast<uint8_t>(state.item_id), state.big_chest);
343 state.error = status.ok() ? "" : std::string(status.message());
344 if (status.ok()) {
345 state.open = false;
346 ImGui::CloseCurrentPopup();
347 }
348 }
349 }
350 gui::AutoRegisterLastItem("button", "ChestOK");
351 ImGui::EndDisabled();
352 ImGui::SameLine();
353 const bool cancel = ImGui::Button("Cancel");
354 gui::AutoRegisterLastItem("button", "ChestCancel");
355 if (cancel || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
356 state.open = false;
357 ImGui::CloseCurrentPopup();
358 }
359 }
360 ImGui::EndPopup();
361 }
362 ImGui::PopID();
363}
364
365std::string GetDungeonChestItemLabel(uint8_t item_id) {
366 return zelda3::GetItemReceiptLabel(item_id);
367}
368
369std::string GetDungeonChestItemLabel(uint8_t item_id,
370 const project::YazeProject* project) {
372 item_id, project ? &project->resource_labels : nullptr);
373}
374
375void DrawDungeonChestEditor(int room_id, zelda3::Room& room,
376 DungeonCanvasViewer& viewer) {
377 ImGui::TextUnformatted("Chests");
378 if (!viewer.rooms() || viewer.rooms()->GetIfLoaded(room_id) != &room ||
379 !room.AreChestsLoaded()) {
380 ImGui::TextWrapped(
381 "Open this room in the canvas to load its chest contents.");
382 return;
383 }
384 auto& state = viewer.chest_editor_state();
385 if (state.room_id != room_id) {
386 state = DungeonChestEditorState{};
387 state.room_id = room_id;
388 }
389 gui::AutoWidgetScope scope("Dungeon/ChestEditor");
390 ImGui::PushID("DungeonChestEditor");
391 ImGui::PushID(room_id);
392 const auto& chests = room.GetChests();
393 size_t chest_objects = 0;
394 for (const auto& object : room.GetTileObjects()) {
395 chest_objects += zelda3::IsStatefulChestObjectId(object.id_);
396 }
397 const auto mapping_status =
399 const bool has_mapping = mapping_status.ok();
400 ImGui::TextWrapped("%zu contents records · %zu chest objects", chests.size(),
401 chest_objects);
402 if (!has_mapping) {
403 ImGui::TextWrapped(
404 "Chest objects and contents do not match in room order. Rewards remain "
405 "editable; adding, deleting, and changing chest type require a "
406 "matching "
407 "object for each record.");
408 ImGui::TextWrapped("%s", std::string(mapping_status.message()).c_str());
409 }
410 if (chests.size() > zelda3::kMaxChests) {
411 ImGui::TextWrapped("This room exceeds the standard six-chest limit.");
412 }
413 DrawChestPlacement(room_id, has_mapping, viewer, state);
414 if (chests.empty()) {
415 ImGui::TextWrapped("No chest contents records in this room.");
416 state.selected_index = 0;
417 state.last_canvas_selection.reset();
418 state.error.clear();
419 ImGui::PopID();
420 ImGui::PopID();
421 return;
422 }
423 if (viewer.current_room_id() == room_id) {
424 const auto selection =
426 const auto canvas_selection = selection.size() == 1
427 ? std::optional<size_t>(selection.front())
428 : std::nullopt;
429 if (canvas_selection != state.last_canvas_selection) {
430 state.last_canvas_selection = canvas_selection;
431 if (has_mapping && canvas_selection) {
432 const auto ordinal = zelda3::ChestIndexForObject(room.GetTileObjects(),
433 *canvas_selection);
434 if (ordinal) {
435 state.selected_index = static_cast<int>(*ordinal);
436 state.search.fill(0);
437 state.error.clear();
438 }
439 }
440 }
441 }
442 state.selected_index =
443 std::clamp(state.selected_index, 0, static_cast<int>(chests.size()) - 1);
444 ImGui::SetNextItemWidth(-1);
445 const std::string current = absl::StrFormat(
446 "Chest %d · %s", state.selected_index + 1,
447 ItemPreview(chests[state.selected_index].id, viewer.project()));
448 const bool open = ImGui::BeginCombo("##ChestRecord", current.c_str());
449 gui::AutoRegisterLastItem("combo", "ChestRecord");
450 if (open) {
451 for (size_t i = 0; i < chests.size(); ++i) {
452 const std::string label = absl::StrFormat(
453 "Chest %zu · %s", i + 1, ItemPreview(chests[i].id, viewer.project()));
454 if (ImGui::Selectable(label.c_str(), state.selected_index == i)) {
455 state.selected_index = static_cast<int>(i);
456 state.search.fill(0);
457 state.error.clear();
458 }
459 gui::AutoRegisterLastItem("selectable",
460 absl::StrFormat("ChestRecord%zu", i));
461 }
462 ImGui::EndCombo();
463 }
464 const size_t index = static_cast<size_t>(state.selected_index);
465 if (DrawChestActions(room_id, index, has_mapping, room, viewer, state)) {
466 state.search.fill(0);
467 ImGui::PopID();
468 ImGui::PopID();
469 return;
470 }
471 ImGui::PushID(state.selected_index);
472 // The mutation callback may replace the vector. Do not keep a record
473 // reference across calls into the editor's undo-backed mutation path.
474 const auto chest = chests[index];
475 ImGui::BeginDisabled(viewer.header_read_only());
476 DrawItemChoice(chest.id, viewer, state.search, [&](uint8_t id) {
477 ApplyEdit(viewer, state, room_id, index, id, chest.size);
478 });
479 bool big = room.GetChests()[index].size;
480 ImGui::BeginDisabled(!has_mapping);
481 if (ImGui::Checkbox("Big chest", &big)) {
482 ApplyEdit(viewer, state, room_id, index, room.GetChests()[index].id, big);
483 }
484 gui::AutoRegisterLastItem("checkbox", "ChestBig");
485 ImGui::EndDisabled();
486 if (ImGui::TreeNode("Advanced##ChestAdvanced")) {
487 int item_id = room.GetChests()[index].id;
488 ImGui::SetNextItemWidth(-1);
489 const bool changed =
490 gui::InputScalarDeferred("##ChestItemId", ImGuiDataType_S32, &item_id,
491 "%02X", ImGuiInputTextFlags_CharsHexadecimal,
492 {reinterpret_cast<uintptr_t>(&viewer),
493 static_cast<uint64_t>(room_id), index});
494 gui::AutoRegisterLastItem("input_int", "ChestItemId");
495 if (changed) {
496 if (item_id < 0 || item_id > 0xFF) {
497 state.error = "Item ID must fit one byte (00-FF).";
498 } else {
499 ApplyEdit(viewer, state, room_id, index, static_cast<uint8_t>(item_id),
500 room.GetChests()[index].size);
501 }
502 }
503 ImGui::TextWrapped(
504 "Raw item ID (hex). Unknown hack-specific values are kept.");
505 ImGui::TreePop();
506 }
507 ImGui::EndDisabled();
508 if (!state.error.empty()) {
509 ImGui::TextWrapped("Edit not applied: %s", state.error.c_str());
510 }
511 ImGui::PopID();
512 ImGui::PopID();
513 ImGui::PopID();
514}
515
516} // namespace yaze::editor
DungeonChestEditDialogState chest_edit_dialog_state_
DungeonChestEditorState & chest_editor_state()
absl::Status OpenChestEditor(int room_id, size_t index)
DungeonObjectInteraction & object_interaction()
void TriggerCanvasPingRect(int pixel_x, int pixel_y, int pixel_w, int pixel_h)
absl::Status EditChest(int room_id, size_t index, uint8_t item_id, bool big_chest)
void SetPreviewObject(const zelda3::RoomObject &object)
absl::Status DeleteChest(int room_id, size_t index)
const project::YazeProject * project() const
void ScrollToTile(int tile_x, int tile_y)
void SetSelectedObjects(const std::vector< size_t > &indices)
const zelda3::RoomObject * GetPlacementPreview() const
std::vector< size_t > GetSelectedObjectIndices() const
zelda3::Room * GetIfLoaded(int room_id)
RAII scope that enables automatic widget registration.
const std::vector< chest_data > & GetChests() const
Definition room.h:323
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
bool AreChestsLoaded() const
Definition room.h:974
void DrawChestPlacement(int room_id, bool has_mapping, DungeonCanvasViewer &viewer, DungeonChestEditorState &state)
std::string ItemPreview(uint8_t item_id, const project::YazeProject *project)
std::vector< uint64_t > ChestObjectSignature(const zelda3::Room &room)
std::optional< size_t > FindChestObject(const zelda3::Room &room, size_t ordinal)
std::vector< uint16_t > ChestRecordSignature(const zelda3::Room &room)
void ApplyEdit(DungeonCanvasViewer &viewer, DungeonChestEditorState &state, int room_id, size_t index, uint8_t item_id, bool big_chest)
absl::Status ValidateChestDialogTarget(const DungeonCanvasViewer &viewer, const DungeonChestEditDialogState &state)
void DrawItemChoice(uint8_t item_id, DungeonCanvasViewer &viewer, std::array< char, 64 > &search_buffer, const std::function< void(uint8_t)> &select_item)
bool DrawChestActions(int room_id, size_t index, bool has_mapping, zelda3::Room &room, DungeonCanvasViewer &viewer, DungeonChestEditorState &state)
Editors are the view controllers for the application.
std::string GetDungeonChestItemLabel(uint8_t item_id)
void DrawDungeonChestEditor(int room_id, zelda3::Room &room, 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:360
absl::Status ValidateChestObjectMapping(const std::vector< RoomObject > &objects, const std::vector< chest_data > &chests)
Definition chest_edit.cc:74
const char * GetItemReceiptLabelSource(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
std::optional< size_t > ChestIndexForObject(const std::vector< RoomObject > &objects, size_t object_index)
Definition chest_edit.cc:63
std::string GetItemReceiptLabel(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
constexpr bool IsStatefulChestObjectId(int object_id)
constexpr size_t kMaxChests
Treasure chest.
Definition zelda.h:425
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:369
static const std::vector< std::string > & GetItemReceiptNames()
Automatic widget registration helpers for ImGui Test Engine integration.