yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_room_transfer_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstring>
6
7#include "absl/strings/str_format.h"
12#include "app/gui/core/input.h"
13#include "imgui/imgui.h"
14#include "imgui/misc/cpp/imgui_stdlib.h"
15#include "util/file_util.h"
16
17namespace yaze::editor {
18namespace {
19
21 uint16_t bit;
22 const char* label;
23 const char* widget;
24};
25
26constexpr std::array<TransferDomain, 7> kDomains{{
27 {kTransferObjects, "Objects and chest rewards", "TransferObjects"},
28 {kTransferDoors, "Doors", "TransferDoors"},
29 {kTransferSprites, "Sprites", "TransferSprites"},
30 {kTransferItems, "Pot items", "TransferItems"},
31 {kTransferMetadata, "Room properties", "TransferMetadata"},
32 {kTransferCollision, "Custom collision", "TransferCollision"},
33 {kTransferWater, "Water fill zones", "TransferWater"},
34}};
35
37 state.preview.reset();
38 state.error.clear();
39 state.status.clear();
40 state.can_retry_without_properties = false;
41}
42
45 InvalidatePreview(state);
46 const auto preview = viewer.PreviewRoomTransfer(
47 state.import_json ? -1 : state.source_room_id, state.json,
48 {state.domains, state.copy_destinations});
49 if (preview.ok()) {
50 state.preview = std::make_shared<DungeonRoomTransferPlan>(*preview);
51 } else {
52 state.error = std::string(preview.status().message());
54 (state.domains & kTransferMetadata) != 0 &&
55 (state.domains & ~kTransferMetadata) != 0 &&
56 preview.status()
58 .has_value();
59 }
60}
61
63 const auto& before = plan.before.contents;
64 const auto& after = plan.after.contents;
65 auto count = [&](const char* label, uint16_t bit, size_t old_count,
66 size_t new_count) {
67 if ((plan.options.domains & bit) != 0) {
68 ImGui::TextWrapped("%s: %zu → %zu (replace)", label, old_count,
69 new_count);
70 } else {
71 ImGui::TextWrapped("%s: %zu (preserve)", label, old_count);
72 }
73 };
74 count("Objects", kTransferObjects, before.objects.size(),
75 after.objects.size());
76 count("Chest rewards", kTransferObjects, before.chests.size(),
77 after.chests.size());
78 count("Doors", kTransferDoors, before.doors.size(), after.doors.size());
79 count("Sprites", kTransferSprites, before.sprites.size(),
80 after.sprites.size());
81 count("Pot items", kTransferItems, before.items.size(), after.items.size());
82 for (const auto& domain : kDomains) {
83 if (domain.bit >= kTransferMetadata) {
84 ImGui::TextWrapped(
85 "%s: %s", domain.label,
86 (plan.options.domains & domain.bit) != 0 ? "replace" : "preserve");
87 }
88 }
89 ImGui::TextWrapped("Stair and warp destinations: %s",
90 plan.options.copy_destinations ? "replace" : "preserve");
91 if ((plan.options.domains & kTransferWater) && plan.after.water.has_data) {
92 ImGui::TextWrapped("Water-fill save bit: %02X (other rooms retain theirs)",
94 }
95}
96
97} // namespace
98
100 const std::string& path) {
101 if (path.empty())
102 return; // Native dialog cancellation leaves the form alone.
103 const auto document = ReadDungeonRoomDocumentFile(path);
104 if (!document.ok()) {
105 state.error = std::string(document.status().message());
106 state.status.clear();
107 return;
108 }
109 InvalidatePreview(state);
110 state.json = *document;
111 state.import_json = true;
112 state.status = "Room file loaded. Preview Replacement before applying.";
113}
114
116 auto& state = viewer.room_transfer_state();
117 const int target = viewer.current_room_id();
118 if (state.room_id != target || state.rom != viewer.rom()) {
119 const int popup_room = state.popup_room_id;
120 const bool popup_open = state.popup_open;
121 state = {};
122 state.room_id = target;
123 state.rom = viewer.rom();
124 state.source_room_id = target == 0 ? 1 : 0;
125 state.popup_room_id = popup_room;
126 state.popup_open = popup_open;
127 }
128 gui::AutoWidgetScope scope("Dungeon/RoomTransfer");
129 ImGui::PushID("DungeonRoomTransfer");
130 ImGui::PushID(&viewer);
131 ImGui::PushID(target);
132 const bool valid_target =
133 viewer.rooms() && viewer.rooms()->GetIfLoaded(target);
134 if (!valid_target) {
135 ImGui::TextWrapped(
136 "Open a room in the canvas to clone or import its contents.");
137 ImGui::PopID();
138 ImGui::PopID();
139 ImGui::PopID();
140 return;
141 }
142 ImGui::TextWrapped("Replace contents of room %03X", target);
143 ImGui::TextWrapped(
144 "Selected domains are replaced. Unchecked domains stay in this room.");
145 if (ImGui::Button("Copy Room JSON", ImVec2(-1, 0))) {
146 const auto json = viewer.ExportRoomDocument(target);
147 state.error = json.ok() ? "" : std::string(json.status().message());
148 state.status.clear();
149 if (json.ok()) {
150 ImGui::SetClipboardText(json->c_str());
151 state.status = "Room JSON copied to clipboard.";
152 }
153 }
154 gui::AutoRegisterLastItem("button", "TransferExport");
155
156#ifndef __EMSCRIPTEN__
157 if (ImGui::Button("Save Room File...", ImVec2(-1, 0))) {
158 const auto json = viewer.ExportRoomDocument(target);
159 if (!json.ok()) {
160 state.error = std::string(json.status().message());
161 state.status.clear();
162 } else {
164 absl::StrFormat("room_%03X.yaze-room.json", target), "json");
165 if (!path.empty()) {
166 const auto result = WriteDungeonRoomDocumentFile(path, *json);
167 state.error = result.ok() ? "" : std::string(result.message());
168 state.status = result.ok() ? "Room template saved to " + path : "";
169 }
170 }
171 }
172 gui::AutoRegisterLastItem("button", "TransferSaveFile");
173#endif
174
175 const bool editable =
176 !viewer.header_read_only() && viewer.IsObjectInteractionEnabled();
177 ImGui::BeginDisabled(!editable);
178 if (ImGui::RadioButton("Clone", !state.import_json)) {
179 state.import_json = false;
180 InvalidatePreview(state);
181 }
182 gui::AutoRegisterLastItem("radio", "TransferCloneMode");
183 ImGui::SameLine();
184 if (ImGui::RadioButton("Import JSON", state.import_json)) {
185 state.import_json = true;
186 InvalidatePreview(state);
187 }
188 gui::AutoRegisterLastItem("radio", "TransferImportMode");
189 if (state.import_json) {
190#ifndef __EMSCRIPTEN__
191 if (ImGui::Button("Open Room File...", ImVec2(-1, 0))) {
193 options.filters.push_back({"Yaze room template", "json"});
196 }
197 gui::AutoRegisterLastItem("button", "TransferOpenFile");
198#endif
199 if (ImGui::Button("Paste JSON", ImVec2(-1, 0))) {
200 const char* clipboard = ImGui::GetClipboardText();
201 InvalidatePreview(state);
202 if (!clipboard) {
203 state.error = "The clipboard contains no text.";
204 } else if (strnlen(clipboard, kMaxDungeonRoomDocumentBytes + 1) >
206 state.error = "Room JSON exceeds the 1 MiB document limit.";
207 } else {
208 state.json = clipboard;
209 }
210 }
211 gui::AutoRegisterLastItem("button", "TransferPaste");
212 if (ImGui::InputTextMultiline("##RoomJSON", &state.json, ImVec2(-1, 130))) {
213 InvalidatePreview(state);
214 }
215 gui::AutoRegisterLastItem("input_text", "TransferJSON");
216 } else {
217 ImGui::TextUnformatted("Source room (hex)");
218 ImGui::SetNextItemWidth(-1);
219 if (gui::InputScalarDeferred("##TransferSource", ImGuiDataType_S32,
220 &state.source_room_id, "%03X",
221 ImGuiInputTextFlags_CharsHexadecimal,
222 {reinterpret_cast<uintptr_t>(&viewer),
223 static_cast<uint64_t>(target)})) {
224 InvalidatePreview(state);
225 }
226 gui::AutoRegisterLastItem("input_int", "TransferSource");
227 }
228
229 ImGui::TextUnformatted("Replace these domains");
230 for (const auto& domain : kDomains) {
231 bool enabled = (state.domains & domain.bit) != 0;
232 if (ImGui::Checkbox(domain.label, &enabled)) {
233 if (enabled) {
234 state.domains |= domain.bit;
235 } else {
236 state.domains &= ~domain.bit;
237 }
238 if ((state.domains & kTransferMetadata) == 0) {
239 state.copy_destinations = false;
240 }
241 InvalidatePreview(state);
242 }
243 gui::AutoRegisterLastItem("checkbox", domain.widget);
244 }
245 ImGui::BeginDisabled((state.domains & kTransferMetadata) == 0);
246 if (ImGui::Checkbox("Copy destination links", &state.copy_destinations)) {
247 InvalidatePreview(state);
248 }
249 gui::AutoRegisterLastItem("checkbox", "TransferDestinations");
250 ImGui::EndDisabled();
251 ImGui::TextWrapped(
252 "Destinations are preserved by default. Copied links may need changes "
253 "for the new room.");
254 const bool valid_source =
255 state.import_json ? !state.json.empty() &&
256 state.json.size() <= kMaxDungeonRoomDocumentBytes
257 : state.source_room_id >= 0 &&
258 state.source_room_id < zelda3::kNumberOfRooms &&
259 state.source_room_id != target;
260 ImGui::BeginDisabled(!valid_source || state.domains == 0);
261 if (ImGui::Button("Preview Replacement", ImVec2(-1, 0))) {
262 PreviewReplacement(viewer, state);
263 }
264 gui::AutoRegisterLastItem("button", "TransferPreview");
265 ImGui::EndDisabled();
266 ImGui::EndDisabled();
267
268 if (state.preview) {
269 ImGui::Separator();
270 ImGui::TextWrapped("Preview for room %03X", state.preview->target_room_id);
271 DrawCounts(*state.preview);
272 if (!state.preview->changed()) {
273 ImGui::TextWrapped("Selected contents already match.");
274 }
275 ImGui::BeginDisabled(!editable || !state.preview->changed());
276 const bool apply = ImGui::Button("Apply Replacement", ImVec2(-1, 0));
277 gui::AutoRegisterLastItem("button", "TransferApply");
278 ImGui::EndDisabled();
279 if (apply) {
280 // Publication can refresh this viewer; retain the reviewed plan during
281 // its callback rather than borrowing it from presentation state.
282 const auto plan = state.preview;
283 const auto status = viewer.ApplyRoomTransfer(*plan);
284 state.preview.reset();
285 state.error = status.ok() ? "" : std::string(status.message());
286 state.status =
287 status.ok() ? "Replacement applied. Undo restores this room." : "";
288 }
289 }
290 if (!editable) {
291 ImGui::TextWrapped(
292 "This view is read-only. Room JSON can still be copied or exported.");
293 }
294 if (!valid_source && !state.import_json) {
295 ImGui::TextWrapped("Choose a different source room between 000 and 127.");
296 }
297 if (state.import_json && state.json.size() > kMaxDungeonRoomDocumentBytes) {
298 ImGui::TextWrapped("Room JSON exceeds the 1 MiB document limit.");
299 }
300 ImGui::TextWrapped(
301 "Room JSON reuses numeric asset IDs. Graphics, palettes, layouts and "
302 "messages must already exist in the target project.");
303 ImGui::TextWrapped(
304 "Entrances, incoming links, pit damage, sprite sort mode and reserved "
305 "header data stay in the target room.");
306 if (!state.error.empty()) {
307 ImGui::TextWrapped("Room transfer: %s", state.error.c_str());
308 }
309 if (state.can_retry_without_properties) {
310 ImGui::TextWrapped(
311 "Keep this room's shared properties and preview the other selected "
312 "contents instead.");
313 ImGui::BeginDisabled(!editable || !valid_source);
314 if (ImGui::Button("Preview without room properties", ImVec2(-1, 0))) {
315 state.domains &= ~kTransferMetadata;
316 state.copy_destinations = false;
317 PreviewReplacement(viewer, state);
318 }
319 gui::AutoRegisterLastItem("button", "TransferPreviewWithoutProperties");
320 ImGui::EndDisabled();
321 }
322 if (!state.status.empty()) {
323 ImGui::TextWrapped("%s", state.status.c_str());
324 }
325 ImGui::PopID();
326 ImGui::PopID();
327 ImGui::PopID();
328}
329
331 auto& state = viewer.room_transfer_state();
332 ImGui::PushID(&viewer);
333 constexpr const char* kPopup = "Clone / Import Room##RoomTransfer";
334 if (state.request_popup) {
335 state.request_popup = false;
336 ImGui::OpenPopup(kPopup);
337 }
338 ImGui::SetNextWindowSize(ImVec2(440, 680), ImGuiCond_FirstUseEver);
339 bool open = true;
340 if (ImGui::BeginPopupModal(kPopup, &open, ImGuiWindowFlags_NoSavedSettings)) {
341 state.popup_open = true;
342 if (state.popup_room_id != viewer.current_room_id()) {
343 ImGui::CloseCurrentPopup();
344 } else {
346 if (ImGui::Button("Close", ImVec2(-1, 0))) {
347 ImGui::CloseCurrentPopup();
348 }
349 gui::AutoRegisterLastItem("button", "TransferClose");
350 }
351 ImGui::EndPopup();
352 } else {
353 state.popup_open = false;
354 }
355 ImGui::PopID();
356}
357
358} // namespace yaze::editor
absl::Status ApplyRoomTransfer(const DungeonRoomTransferPlan &plan)
absl::StatusOr< std::string > ExportRoomDocument(int room_id)
absl::StatusOr< DungeonRoomTransferPlan > PreviewRoomTransfer(int source_room_id, const std::string &json, const DungeonRoomTransferOptions &options)
DungeonRoomTransferEditorState & room_transfer_state()
zelda3::Room * GetIfLoaded(int room_id)
RAII scope that enables automatic widget registration.
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
void PreviewReplacement(DungeonCanvasViewer &viewer, DungeonRoomTransferEditorState &state)
Editors are the view controllers for the application.
constexpr uint16_t kTransferMetadata
constexpr uint16_t kTransferItems
constexpr uint16_t kTransferDoors
void DrawDungeonRoomTransferPopup(DungeonCanvasViewer &viewer)
constexpr uint16_t kTransferCollision
constexpr uint16_t kTransferSprites
void DrawDungeonRoomTransferEditor(DungeonCanvasViewer &viewer)
absl::Status WriteDungeonRoomDocumentFile(const std::string &path, const std::string &json)
constexpr char kRoomTransferSharedHeaderPayload[]
constexpr uint16_t kTransferWater
void LoadDungeonRoomTransferFile(DungeonRoomTransferEditorState &state, const std::string &path)
constexpr uint16_t kTransferObjects
absl::StatusOr< std::string > ReadDungeonRoomDocumentFile(const std::string &path)
constexpr size_t kMaxDungeonRoomDocumentBytes
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 kNumberOfRooms
std::shared_ptr< DungeonRoomTransferPlan > preview
std::vector< FileDialogFilter > filters
Definition file_util.h:17
Automatic widget registration helpers for ImGui Test Engine integration.