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 <optional>
5#include <string>
6#include <string_view>
7
8#include "absl/strings/ascii.h"
9#include "absl/strings/match.h"
10#include "absl/strings/str_format.h"
13#include "app/gui/core/input.h"
14#include "core/project.h"
15#include "imgui/imgui.h"
19#include "zelda3/dungeon/room.h"
22
23namespace yaze::editor {
24namespace {
25
26std::string ItemPreview(uint8_t item_id, const project::YazeProject* project) {
27 return absl::StrFormat("%02X %s", item_id,
28 GetDungeonChestItemLabel(item_id, project));
29}
30
32 int room_id, size_t index, uint8_t item_id, bool big_chest) {
33 const auto status = viewer.EditChest(room_id, index, item_id, big_chest);
34 state.error = status.ok() ? "" : std::string(status.message());
35}
36
37std::optional<size_t> FindChestObject(const zelda3::Room& room,
38 size_t ordinal) {
39 const auto& objects = room.GetTileObjects();
40 for (size_t i = 0; i < objects.size(); ++i) {
41 if (zelda3::ChestIndexForObject(objects, i) == ordinal) {
42 return i;
43 }
44 }
45 return std::nullopt;
46}
47
48void DrawChestPlacement(int room_id, bool has_mapping,
49 DungeonCanvasViewer& viewer,
51 const bool active_room = viewer.current_room_id() == room_id;
52 ImGui::BeginDisabled(viewer.header_read_only() || !active_room ||
53 !has_mapping);
54 const bool inline_buttons =
55 ImGui::GetContentRegionAvail().x >=
56 ImGui::CalcTextSize("Add small chestAdd big chest").x +
57 ImGui::GetStyle().FramePadding.x * 4 +
58 ImGui::GetStyle().ItemSpacing.x;
59 const float width = inline_buttons ? (ImGui::GetContentRegionAvail().x -
60 ImGui::GetStyle().ItemSpacing.x) /
61 2
62 : -1;
63 for (int big = 0; big <= 1; ++big) {
64 if (big && inline_buttons) {
65 ImGui::SameLine();
66 }
67 if (ImGui::Button(big ? "Add big chest" : "Add small chest",
68 ImVec2(width, 0))) {
69 const int id = big ? 0xFB1 : 0xF99;
70 viewer.SetPreviewObject(
72 viewer.SetObjectInteractionEnabled(true);
73 state.error.clear();
74 }
75 gui::AutoRegisterLastItem("button", big ? "ChestAddBig" : "ChestAddSmall");
76 }
77 ImGui::EndDisabled();
78 const auto* preview = viewer.object_interaction().GetPlacementPreview();
79 if (active_room && preview && zelda3::IsStatefulChestObjectId(preview->id_)) {
80 ImGui::TextWrapped(
81 "Click in the canvas to place a %s chest. Escape cancels.",
82 preview->id_ == 0xFB1 ? "big" : "small");
83 } else if (!active_room) {
84 ImGui::TextWrapped(
85 "Open this room in the canvas to place or select a chest.");
86 }
87 ImGui::TextWrapped(
88 "New chests start with 1 Rupee. Choose a chest below to change its "
89 "reward.");
90}
91
92bool DrawChestActions(int room_id, size_t index, bool has_mapping,
93 zelda3::Room& room, DungeonCanvasViewer& viewer,
95 const auto object_index =
96 has_mapping ? FindChestObject(room, index) : std::nullopt;
97 ImGui::BeginDisabled(!object_index || viewer.current_room_id() != room_id);
98 if (ImGui::Button("Select in canvas", ImVec2(-1, 0))) {
99 const auto& object = room.GetTileObjects()[*object_index];
101 viewer.object_interaction().SetSelectedObjects({*object_index});
102 viewer.ScrollToTile(object.x_, object.y_);
103 viewer.TriggerCanvasPingRect(object.x_ * 8, object.y_ * 8, object.width_,
104 object.height_);
105 }
106 gui::AutoRegisterLastItem("button", "ChestSelect");
107 ImGui::EndDisabled();
108 ImGui::BeginDisabled(viewer.header_read_only() || !object_index);
109 bool deleted = false;
110 if (ImGui::Button("Delete chest", ImVec2(-1, 0))) {
111 const auto status = viewer.DeleteChest(room_id, index);
112 state.error = status.ok() ? "" : std::string(status.message());
113 deleted = status.ok();
114 }
115 gui::AutoRegisterLastItem("button", "ChestDelete");
116 if (ImGui::IsItemHovered()) {
117 ImGui::SetTooltip(
118 "Remove the chest object and its reward. Undo restores both.");
119 }
120 ImGui::EndDisabled();
121 return deleted;
122}
123
124void DrawItemChoice(int room_id, size_t index, const chest_data chest,
125 DungeonCanvasViewer& viewer,
127 ImGui::TextUnformatted("Reward");
128 ImGui::SetNextItemWidth(-1);
129 const bool open = ImGui::BeginCombo(
130 "##ChestReward", ItemPreview(chest.id, viewer.project()).c_str(),
131 ImGuiComboFlags_HeightLarge);
132 gui::AutoRegisterLastItem("combo", "ChestReward");
133 if (!open) {
134 return;
135 }
136 ImGui::SetNextItemWidth(-1);
137 const bool search_changed =
138 ImGui::InputTextWithHint("##ChestSearch", "Search name or hex ID...",
139 state.search.data(), state.search.size());
140 gui::AutoRegisterLastItem("input", "ChestSearch");
141 ImGui::Separator();
142 const std::string search = absl::AsciiStrToLower(state.search.data());
143 // Keep search visible when opening on a late item. The selected result may
144 // scroll into view without moving the search control out of the popup.
145 if (ImGui::BeginChild("##ChestRewardChoices",
146 ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 8))) {
147 if (search_changed) {
148 ImGui::SetScrollY(0);
149 }
150 const auto* labels =
151 viewer.project() ? &viewer.project()->resource_labels : nullptr;
152 bool has_matches = false;
153 for (int id = 0; id <= 0xFF; ++id) {
154 // Retain any existing unknown value and explicitly named hack items, but
155 // do not suggest unverified high IDs as ordinary vanilla rewards.
156 if (id >= static_cast<int>(
158 id != chest.id &&
159 std::string_view(zelda3::GetItemReceiptLabelSource(
160 static_cast<uint8_t>(id), labels)) != "project") {
161 continue;
162 }
163 const std::string label =
164 ItemPreview(static_cast<uint8_t>(id), viewer.project());
165 if (!absl::StrContains(absl::AsciiStrToLower(label), search)) {
166 continue;
167 }
168 has_matches = true;
169 if (ImGui::Selectable(label.c_str(), chest.id == id)) {
170 ApplyEdit(viewer, state, room_id, index, static_cast<uint8_t>(id),
171 chest.size);
172 // The selectable is in a child window, so close its containing combo
173 // explicitly after a committed choice.
174 ImGui::CloseCurrentPopup();
175 }
176 gui::AutoRegisterLastItem("selectable",
177 absl::StrFormat("ChestReward%02X", id));
178 if (chest.id == id && search.empty()) {
179 ImGui::SetItemDefaultFocus();
180 }
181 }
182 if (!has_matches) {
183 ImGui::TextDisabled("No matching rewards");
184 }
185 }
186 ImGui::EndChild();
187 ImGui::EndCombo();
188}
189
190} // namespace
191
192std::string GetDungeonChestItemLabel(uint8_t item_id) {
193 return zelda3::GetItemReceiptLabel(item_id);
194}
195
196std::string GetDungeonChestItemLabel(uint8_t item_id,
197 const project::YazeProject* project) {
199 item_id, project ? &project->resource_labels : nullptr);
200}
201
202void DrawDungeonChestEditor(int room_id, zelda3::Room& room,
203 DungeonCanvasViewer& viewer) {
204 ImGui::TextUnformatted("Chests");
205 if (!viewer.rooms() || viewer.rooms()->GetIfLoaded(room_id) != &room ||
206 !room.AreChestsLoaded()) {
207 ImGui::TextWrapped(
208 "Open this room in the canvas to load its chest contents.");
209 return;
210 }
211 auto& state = viewer.chest_editor_state();
212 if (state.room_id != room_id) {
213 state = DungeonChestEditorState{};
214 state.room_id = room_id;
215 }
216 gui::AutoWidgetScope scope("Dungeon/ChestEditor");
217 ImGui::PushID("DungeonChestEditor");
218 ImGui::PushID(room_id);
219 const auto& chests = room.GetChests();
220 size_t chest_objects = 0;
221 for (const auto& object : room.GetTileObjects()) {
222 chest_objects += zelda3::IsStatefulChestObjectId(object.id_);
223 }
224 const auto mapping_status =
226 const bool has_mapping = mapping_status.ok();
227 ImGui::TextWrapped("%zu contents records · %zu chest objects", chests.size(),
228 chest_objects);
229 if (!has_mapping) {
230 ImGui::TextWrapped(
231 "Chest objects and contents do not match in room order. Rewards remain "
232 "editable; adding, deleting, and changing chest type require a "
233 "matching "
234 "object for each record.");
235 ImGui::TextWrapped("%s", std::string(mapping_status.message()).c_str());
236 }
237 if (chests.size() > zelda3::kMaxChests) {
238 ImGui::TextWrapped("This room exceeds the standard six-chest limit.");
239 }
240 DrawChestPlacement(room_id, has_mapping, viewer, state);
241 if (chests.empty()) {
242 ImGui::TextWrapped("No chest contents records in this room.");
243 state.selected_index = 0;
244 state.last_canvas_selection.reset();
245 state.error.clear();
246 ImGui::PopID();
247 ImGui::PopID();
248 return;
249 }
250 if (viewer.current_room_id() == room_id) {
251 const auto selection =
253 const auto canvas_selection = selection.size() == 1
254 ? std::optional<size_t>(selection.front())
255 : std::nullopt;
256 if (canvas_selection != state.last_canvas_selection) {
257 state.last_canvas_selection = canvas_selection;
258 if (has_mapping && canvas_selection) {
259 const auto ordinal = zelda3::ChestIndexForObject(room.GetTileObjects(),
260 *canvas_selection);
261 if (ordinal) {
262 state.selected_index = static_cast<int>(*ordinal);
263 state.search.fill(0);
264 state.error.clear();
265 }
266 }
267 }
268 }
269 state.selected_index =
270 std::clamp(state.selected_index, 0, static_cast<int>(chests.size()) - 1);
271 ImGui::SetNextItemWidth(-1);
272 const std::string current = absl::StrFormat(
273 "Chest %d · %s", state.selected_index + 1,
274 ItemPreview(chests[state.selected_index].id, viewer.project()));
275 const bool open = ImGui::BeginCombo("##ChestRecord", current.c_str());
276 gui::AutoRegisterLastItem("combo", "ChestRecord");
277 if (open) {
278 for (size_t i = 0; i < chests.size(); ++i) {
279 const std::string label = absl::StrFormat(
280 "Chest %zu · %s", i + 1, ItemPreview(chests[i].id, viewer.project()));
281 if (ImGui::Selectable(label.c_str(), state.selected_index == i)) {
282 state.selected_index = static_cast<int>(i);
283 state.search.fill(0);
284 state.error.clear();
285 }
286 gui::AutoRegisterLastItem("selectable",
287 absl::StrFormat("ChestRecord%zu", i));
288 }
289 ImGui::EndCombo();
290 }
291 const size_t index = static_cast<size_t>(state.selected_index);
292 if (DrawChestActions(room_id, index, has_mapping, room, viewer, state)) {
293 state.search.fill(0);
294 ImGui::PopID();
295 ImGui::PopID();
296 return;
297 }
298 ImGui::PushID(state.selected_index);
299 // The mutation callback may replace the vector. Do not keep a record
300 // reference across calls into the editor's undo-backed mutation path.
301 const auto chest = chests[index];
302 ImGui::BeginDisabled(viewer.header_read_only());
303 DrawItemChoice(room_id, index, chest, viewer, state);
304 bool big = room.GetChests()[index].size;
305 ImGui::BeginDisabled(!has_mapping);
306 if (ImGui::Checkbox("Big chest", &big)) {
307 ApplyEdit(viewer, state, room_id, index, room.GetChests()[index].id, big);
308 }
309 gui::AutoRegisterLastItem("checkbox", "ChestBig");
310 ImGui::EndDisabled();
311 if (ImGui::TreeNode("Advanced##ChestAdvanced")) {
312 int item_id = room.GetChests()[index].id;
313 ImGui::SetNextItemWidth(-1);
314 const bool changed =
315 gui::InputScalarDeferred("##ChestItemId", ImGuiDataType_S32, &item_id,
316 "%02X", ImGuiInputTextFlags_CharsHexadecimal,
317 {reinterpret_cast<uintptr_t>(&viewer),
318 static_cast<uint64_t>(room_id), index});
319 gui::AutoRegisterLastItem("input_int", "ChestItemId");
320 if (changed) {
321 if (item_id < 0 || item_id > 0xFF) {
322 state.error = "Item ID must fit one byte (00-FF).";
323 } else {
324 ApplyEdit(viewer, state, room_id, index, static_cast<uint8_t>(item_id),
325 room.GetChests()[index].size);
326 }
327 }
328 ImGui::TextWrapped(
329 "Raw item ID (hex). Unknown hack-specific values are kept.");
330 ImGui::TreePop();
331 }
332 ImGui::EndDisabled();
333 if (!state.error.empty()) {
334 ImGui::TextWrapped("Edit not applied: %s", state.error.c_str());
335 }
336 ImGui::PopID();
337 ImGui::PopID();
338 ImGui::PopID();
339}
340
341} // namespace yaze::editor
DungeonChestEditorState & chest_editor_state()
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::optional< size_t > FindChestObject(const zelda3::Room &room, size_t ordinal)
void ApplyEdit(DungeonCanvasViewer &viewer, DungeonChestEditorState &state, int room_id, size_t index, uint8_t item_id, bool big_chest)
void DrawItemChoice(int room_id, size_t index, const chest_data chest, DungeonCanvasViewer &viewer, DungeonChestEditorState &state)
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:392
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
Legacy chest data structure.
Definition zelda.h:438
Treasure chest.
Definition zelda.h:425
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:353
static const std::vector< std::string > & GetItemReceiptNames()
Automatic widget registration helpers for ImGui Test Engine integration.