9#include "absl/strings/ascii.h"
10#include "absl/strings/match.h"
11#include "absl/strings/str_format.h"
16#include "imgui/imgui.h"
17#include "imgui/imgui_internal.h"
29 return absl::StrFormat(
"%02X %s", item_id,
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());
42 for (
size_t i = 0; i < objects.size(); ++i) {
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) /
65 for (
int big = 0; big <= 1; ++big) {
66 if (big && inline_buttons) {
69 if (ImGui::Button(big ?
"Add big chest" :
"Add small chest",
71 const int id = big ? 0xFB1 : 0xF99;
83 "Click in the canvas to place a %s chest. Escape cancels.",
84 preview->id_ == 0xFB1 ?
"big" :
"small");
85 }
else if (!active_room) {
87 "Open this room in the canvas to place or select a chest.");
90 "New chests start with 1 Rupee. Choose a chest below to change its "
97 const auto object_index =
99 ImGui::BeginDisabled(!object_index || viewer.
current_room_id() != room_id);
100 if (ImGui::Button(
"Select in canvas", ImVec2(-1, 0))) {
109 ImGui::EndDisabled();
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();
118 if (ImGui::IsItemHovered()) {
120 "Remove the chest object and its reward. Undo restores both.");
122 ImGui::EndDisabled();
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(
133 ImGuiComboFlags_HeightLarge);
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());
144 const std::string search = absl::AsciiStrToLower(search_buffer.data());
147 if (ImGui::BeginChild(
"##ChestRewardChoices",
148 ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 8))) {
149 if (search_changed) {
150 ImGui::SetScrollY(0);
154 bool has_matches =
false;
155 for (
int id = 0;
id <= 0xFF; ++id) {
158 if (
id >=
static_cast<int>(
162 static_cast<uint8_t
>(
id), labels)) !=
"project") {
165 const std::string label =
167 if (!absl::StrContains(absl::AsciiStrToLower(label), search)) {
171 if (ImGui::Selectable(label.c_str(), item_id ==
id)) {
172 select_item(
static_cast<uint8_t
>(
id));
175 ImGui::CloseCurrentPopup();
178 absl::StrFormat(
"ChestReward%02X",
id));
179 if (item_id ==
id && search.empty()) {
180 ImGui::SetItemDefaultFocus();
184 ImGui::TextDisabled(
"No matching rewards");
192 std::vector<uint64_t> result;
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));
204 std::vector<uint16_t> result;
206 result.push_back(
chest.id | (
static_cast<uint16_t
>(
chest.size) << 8));
216 if (!room || !room->AreChestsLoaded() ||
219 state.
index >= room->GetChests().size() ||
222 return absl::FailedPreconditionError(
223 "This chest changed while the editor was open. Cancel and reopen it.");
226 return absl::FailedPreconditionError(
"Chest contents are read-only.");
228 return absl::OkStatus();
237 room->rom() !=
rom_) {
238 return absl::FailedPreconditionError(
"Chest contents are not editable");
240 if (index >= room->GetChests().size()) {
241 return absl::InvalidArgumentError(
242 "Select an existing chest contents record");
246 state.request_open =
true;
247 state.room_id = room_id;
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;
256 room->GetTileObjects(), room->GetChests())
258 state.search.fill(0);
260 return absl::OkStatus();
264 if (!ImGui::GetCurrentContext()) {
268 const int frame = ImGui::GetFrameCount();
269 if (state.last_draw_frame == frame) {
272 state.last_draw_frame = 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)) {
289 ImGui::TextUnformatted(
"Open this room to load its chest records.");
294 if (state.request_open) {
295 ImGui::OpenPopup(popup_name.c_str());
296 state.request_open =
false;
299 ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Appearing);
301 if (ImGui::BeginPopupModal(popup_name.c_str(), &state.open,
302 ImGuiWindowFlags_AlwaysAutoResize |
303 ImGuiWindowFlags_NoSavedSettings)) {
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);
314 ImGui::EndDisabled();
315 if (!state.mapping_valid) {
317 "Chest records do not match their objects. Only the reward can be "
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);
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());
332 ImGui::BeginDisabled(!target_status.ok());
333 if (ImGui::Button(
"OK")) {
334 const auto status = ValidateChestDialogTarget(*
this, state);
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).";
342 static_cast<uint8_t
>(state.item_id), state.big_chest);
343 state.error = status.ok() ?
"" : std::string(status.message());
346 ImGui::CloseCurrentPopup();
351 ImGui::EndDisabled();
353 const bool cancel = ImGui::Button(
"Cancel");
355 if (cancel || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
357 ImGui::CloseCurrentPopup();
377 ImGui::TextUnformatted(
"Chests");
381 "Open this room in the canvas to load its chest contents.");
385 if (state.room_id != room_id) {
390 ImGui::PushID(
"DungeonChestEditor");
391 ImGui::PushID(room_id);
393 size_t chest_objects = 0;
397 const auto mapping_status =
399 const bool has_mapping = mapping_status.ok();
400 ImGui::TextWrapped(
"%zu contents records · %zu chest objects", chests.size(),
404 "Chest objects and contents do not match in room order. Rewards remain "
405 "editable; adding, deleting, and changing chest type require a "
407 "object for each record.");
408 ImGui::TextWrapped(
"%s", std::string(mapping_status.message()).c_str());
411 ImGui::TextWrapped(
"This room exceeds the standard six-chest limit.");
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();
424 const auto selection =
426 const auto canvas_selection = selection.size() == 1
427 ? std::optional<size_t>(selection.front())
429 if (canvas_selection != state.last_canvas_selection) {
430 state.last_canvas_selection = canvas_selection;
431 if (has_mapping && canvas_selection) {
435 state.selected_index =
static_cast<int>(*ordinal);
436 state.search.fill(0);
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());
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);
460 absl::StrFormat(
"ChestRecord%zu", i));
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);
471 ImGui::PushID(state.selected_index);
474 const auto chest = chests[index];
476 DrawItemChoice(
chest.id, viewer, state.search, [&](uint8_t
id) {
477 ApplyEdit(viewer, state, room_id, index, id, chest.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);
485 ImGui::EndDisabled();
486 if (ImGui::TreeNode(
"Advanced##ChestAdvanced")) {
487 int item_id = room.
GetChests()[index].id;
488 ImGui::SetNextItemWidth(-1);
491 "%02X", ImGuiInputTextFlags_CharsHexadecimal,
492 {
reinterpret_cast<uintptr_t
>(&viewer),
493 static_cast<uint64_t
>(room_id), index});
496 if (item_id < 0 || item_id > 0xFF) {
497 state.error =
"Item ID must fit one byte (00-FF).";
499 ApplyEdit(viewer, state, room_id, index,
static_cast<uint8_t
>(item_id),
504 "Raw item ID (hex). Unknown hack-specific values are kept.");
507 ImGui::EndDisabled();
508 if (!state.error.empty()) {
509 ImGui::TextWrapped(
"Edit not applied: %s", state.error.c_str());
DungeonChestEditDialogState chest_edit_dialog_state_
DungeonRoomStore * rooms() const
DungeonChestEditorState & chest_editor_state()
absl::Status OpenChestEditor(int room_id, size_t index)
ChestEditCallback chest_edit_callback_
int current_room_id() const
DungeonObjectInteraction & object_interaction()
void DrawChestEditorPopup()
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)
bool IsObjectInteractionEnabled() const
bool header_read_only() const
DungeonRoomStore * rooms_
void SetObjectInteractionEnabled(bool enabled)
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)
const std::vector< chest_data > & GetChests() const
const std::vector< RoomObject > & GetTileObjects() const
bool AreChestsLoaded() const
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)
absl::Status ValidateChestObjectMapping(const std::vector< RoomObject > &objects, const std::vector< chest_data > &chests)
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)
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
std::vector< uint64_t > source_objects
std::vector< uint16_t > source_chests
Modern project structure with comprehensive settings consolidation.
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
static const std::vector< std::string > & GetItemReceiptNames()