yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
water_fill_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <exception>
7#include <fstream>
8#include <string>
9#include <unordered_map>
10#include <utility>
11#include <vector>
12
13#include "absl/strings/str_format.h"
16#include "core/features.h"
17#include "imgui/imgui.h"
18#include "util/file_util.h"
19#include "util/i18n/tr.h"
21
22namespace yaze::editor {
23
24void WaterFillPanel::Draw(bool* p_open) {
25 (void)p_open;
26 const auto& theme = AgentUI::GetTheme();
27
28 if (!viewer_ || !viewer_->HasRooms() || !viewer_->rom() ||
29 !viewer_->rom()->is_loaded() || !viewer_->rooms()) {
30 ImGui::TextDisabled(ICON_MD_INFO " No dungeon rooms loaded.");
31 return;
32 }
33
34 auto* rooms = viewer_->rooms();
35 gui::AutoWidgetScope widget_scope("Dungeon/WaterFill");
36 const int room_id = viewer_->current_room_id();
37 const bool room_id_valid =
38 (room_id >= 0 && room_id < static_cast<int>(rooms->size()));
39
40 const size_t rom_size = viewer_->rom()->vector().size();
41 const bool reserved_region_present =
43 const bool save_enabled =
45 if (!reserved_region_present) {
46 ImGui::TextColored(theme.status_error, ICON_MD_ERROR
47 " WaterFill reserved region missing (use an "
48 "expanded-collision Oracle ROM)");
49 ImGui::TextDisabled(
50 tr("Expected ROM >= 0x%X bytes (WaterFill end). Current ROM is %zu "
51 "bytes."),
53 ImGui::Separator();
54 }
55
56 if (!save_enabled) {
57 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_LOCK
58 " Read-only: Water Fill saving is disabled");
59 ImGui::TextWrapped(
60 tr("Set save_dungeon_water_fill_zones=true in the .yaze project and "
61 "reopen it before authoring zones."));
65 }
66 ImGui::Separator();
67 }
68
69 bool show_overlay = viewer_->show_water_fill_overlay();
70 if (ImGui::Checkbox(tr("Show Water Fill Overlay"), &show_overlay)) {
72 }
73
74 ImGui::Separator();
75 ImGui::TextUnformatted(tr("Authoring"));
76
77 util::FileDialogOptions json_options;
78 json_options.filters.push_back({"Water Fill Zones", "json"});
79 json_options.filters.push_back({"All Files", "*"});
80
81 ImGui::BeginDisabled(!reserved_region_present || !save_enabled);
82 if (ImGui::Button(ICON_MD_UPLOAD " Import Zones...")) {
83 std::string path =
85 if (!path.empty()) {
86 try {
87 std::string contents = util::LoadFile(path);
88 auto zones_or = zelda3::LoadWaterFillZonesFromJsonString(contents);
89 if (!zones_or.ok()) {
90 last_io_error_ = std::string(zones_or.status().message());
91 last_io_status_.clear();
92 } else {
93 auto zones = std::move(zones_or.value());
94 for (const auto& z : zones) {
95 if (z.room_id < 0 || z.room_id >= static_cast<int>(rooms->size())) {
96 continue;
97 }
98 ApplyZoneToRoom(z, &(*rooms)[z.room_id]);
99 }
101 last_io_status_ = absl::StrFormat("Imported %zu zone(s) from %s",
102 zones.size(), path.c_str());
103 last_io_error_.clear();
104 }
105 } catch (const std::exception& e) {
106 last_io_error_ = e.what();
107 last_io_status_.clear();
108 }
109 }
110 }
111 ImGui::SameLine();
112 if (ImGui::Button(ICON_MD_TUNE " Normalize Masks Now")) {
113 auto zones = CollectZones(*rooms);
114 auto st = zelda3::NormalizeWaterFillZoneMasks(&zones);
115 if (!st.ok()) {
116 last_io_error_ = std::string(st.message());
117 last_io_status_.clear();
118 } else {
119 int changed = 0;
120 for (const auto& z : zones) {
121 auto& r = (*rooms)[z.room_id];
122 if (r.water_fill_sram_bit_mask() != z.sram_bit_mask) {
123 r.set_water_fill_sram_bit_mask(z.sram_bit_mask);
124 ++changed;
125 }
126 }
127 if (changed > 0) {
129 }
131 absl::StrFormat("Normalized masks (%d room(s) updated)", changed);
132 last_io_error_.clear();
133 }
134 }
135 ImGui::EndDisabled();
136
137 ImGui::SameLine();
138 if (ImGui::Button(ICON_MD_DOWNLOAD " Export Zones...")) {
139 auto zones = CollectZones(*rooms);
140 auto json_or = zelda3::DumpWaterFillZonesToJsonString(zones);
141 if (!json_or.ok()) {
142 last_io_error_ = std::string(json_or.status().message());
143 last_io_status_.clear();
144 } else {
146 "water_fill_zones.json", "json");
147 if (!path.empty()) {
148 std::ofstream file(path);
149 if (!file.is_open()) {
151 absl::StrFormat("Cannot write file: %s", path.c_str());
152 last_io_status_.clear();
153 } else {
154 file << *json_or;
155 file.close();
156 last_io_status_ = absl::StrFormat("Exported %zu zone(s) to %s",
157 zones.size(), path.c_str());
158 last_io_error_.clear();
159 }
160 }
161 }
162 }
163
164 if (!last_io_error_.empty()) {
165 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
166 last_io_error_.c_str());
167 } else if (!last_io_status_.empty()) {
168 ImGui::TextColored(theme.status_success, ICON_MD_CHECK_CIRCLE " %s",
169 last_io_status_.c_str());
170 }
171
172 ImGui::TextWrapped(tr(
173 "Import/export uses a room-indexed JSON format. Normalize masks before "
174 "saving to avoid duplicate SRAM bits."));
175
176 ImGui::Separator();
177 if (!room_id_valid) {
178 ImGui::TextDisabled(ICON_MD_INFO " Invalid room ID.");
179 } else {
180 auto& room = (*rooms)[room_id];
181 const bool room_loaded = room.IsLoaded();
182 if (!room_loaded) {
183 ImGui::TextDisabled(
185 " Room not loaded yet (open it to paint and validate sprites).");
186 }
187
188 if (!interaction_) {
189 ImGui::TextDisabled(
190 tr("Painting requires an active interaction context."));
191 } else {
192 // Brush controls are shared across paint modes.
193 auto& state = interaction_->mode_manager().GetModeState();
194 int brush_radius = std::clamp(state.paint_brush_radius, 0, 8);
195 if (ImGui::SliderInt(tr("Brush Radius"), &brush_radius, 0, 8)) {
196 state.paint_brush_radius = brush_radius;
197 }
198 ImGui::SameLine();
199 ImGui::TextDisabled(tr("%dx%d"), (brush_radius * 2) + 1,
200 (brush_radius * 2) + 1);
201
202 bool is_painting = (interaction_->mode_manager().GetMode() ==
204 const bool can_paint =
205 reserved_region_present && room_loaded && save_enabled;
206 ImGui::BeginDisabled(!can_paint);
207 if (ImGui::Checkbox(tr("Paint Mode"), &is_painting)) {
208 if (is_painting) {
211 } else {
213 }
214 }
215 ImGui::EndDisabled();
216
217 if (is_painting) {
218 ImGui::TextColored(theme.text_warning_yellow,
219 tr("Left-drag paints; Alt-drag erases"));
220 }
221 }
222
223 const int tile_count = room.WaterFillTileCount();
224 ImGui::Separator();
225 ImGui::Text(tr("Zone Tiles: %d"), tile_count);
226 if (tile_count > 255) {
227 ImGui::TextColored(theme.status_error,
228 ICON_MD_ERROR " Too many tiles (max 255 per room)");
229 }
230
231 if (room_loaded) {
232 bool has_switch_sprite = false;
233 for (const auto& spr : room.GetSprites()) {
234 if (spr.id() == 0x04 || spr.id() == 0x21) {
235 has_switch_sprite = true;
236 break;
237 }
238 }
239 if (!has_switch_sprite) {
240 ImGui::TextColored(
241 theme.text_warning_yellow, ICON_MD_WARNING
242 " No PullSwitch (0x04) / PushSwitch (0x21) sprite found");
243 }
244 } else {
245 ImGui::TextDisabled(tr("Sprite checks require the room to be loaded."));
246 }
247
248 ImGui::Separator();
249 uint8_t mask = room.water_fill_sram_bit_mask();
250 std::string preview =
251 (mask == 0) ? "Auto (0x00)" : absl::StrFormat("0x%02X", mask);
252 ImGui::BeginDisabled(!reserved_region_present || !save_enabled ||
253 !RoomEditContext(room_id));
254 const bool mask_open =
255 ImGui::BeginCombo(tr("SRAM Bit Mask ($7EF411)"), preview.c_str());
256 gui::AutoRegisterLastItem("combo", "WaterFillMask");
257 if (mask_open) {
258 auto option = [&](const char* label, uint8_t val) {
259 const bool selected = (mask == val);
260 if (ImGui::Selectable(label, selected) && mask != val) {
261 EditRoomWaterFill(room_id, [val](zelda3::Room& edited) {
263 });
264 mask = val;
265 }
266 gui::AutoRegisterLastItem("selectable",
267 absl::StrFormat("WaterFillMask%02X", val));
268 };
269
270 option("Auto (0x00)", 0x00);
271 option("Bit 0 (0x01)", 0x01);
272 option("Bit 1 (0x02)", 0x02);
273 option("Bit 2 (0x04)", 0x04);
274 option("Bit 3 (0x08)", 0x08);
275 option("Bit 4 (0x10)", 0x10);
276 option("Bit 5 (0x20)", 0x20);
277 option("Bit 6 (0x40)", 0x40);
278 option("Bit 7 (0x80)", 0x80);
279
280 ImGui::EndCombo();
281 }
282
283 ImGui::Separator();
284 if (ImGui::Button(tr("Clear Water Fill Zone")) &&
285 (room.WaterFillTileCount() != 0 ||
286 room.water_fill_sram_bit_mask() != 0)) {
288 room_id, [](zelda3::Room& edited) { edited.ClearWaterFillZone(); });
289 }
290 gui::AutoRegisterLastItem("button", "WaterFillClear");
291 ImGui::EndDisabled();
292
293 ImGui::TextWrapped(
294 tr("Water fill zones are serialized as compact tile offset lists. "
295 "Keep zones under 255 tiles per room."));
296 }
297
298 // Overview: show all rooms that currently have zone data, plus global
299 // constraints (max 8 rooms / unique SRAM masks).
300 ImGui::Separator();
301 if (ImGui::CollapsingHeader(tr("Zone Overview"),
302 ImGuiTreeNodeFlags_DefaultOpen)) {
303 struct ZoneRow {
304 int room_id = 0;
305 int tiles = 0;
306 uint8_t mask = 0;
307 bool dirty = false;
308 };
309
310 std::vector<ZoneRow> rows;
311 rows.reserve(8);
312 std::unordered_map<uint8_t, int> mask_counts;
313 int rooms_over_tile_limit = 0;
314 int rooms_unassigned_mask = 0;
315
316 for (int rid = 0; rid < static_cast<int>(rooms->size()); ++rid) {
317 const auto* r = rooms->GetIfMaterialized(rid);
318 if (r == nullptr) {
319 continue;
320 }
321 const int tiles = r->WaterFillTileCount();
322 if (tiles <= 0) {
323 continue;
324 }
325 rows.push_back(ZoneRow{rid, tiles, r->water_fill_sram_bit_mask(),
326 r->water_fill_dirty()});
327 if (tiles > 255) {
328 rooms_over_tile_limit++;
329 }
330 if (r->water_fill_sram_bit_mask() == 0) {
331 rooms_unassigned_mask++;
332 } else {
333 mask_counts[r->water_fill_sram_bit_mask()]++;
334 }
335 }
336
337 std::sort(rows.begin(), rows.end(), [](const ZoneRow& a, const ZoneRow& b) {
338 return a.room_id < b.room_id;
339 });
340
341 int duplicate_masks = 0;
342 for (const auto& [mask, count] : mask_counts) {
343 if (mask != 0 && count > 1) {
344 duplicate_masks++;
345 }
346 }
347
348 ImGui::Text(tr("Rooms with zones: %zu / 8"), rows.size());
349 if (rows.size() > 8) {
350 ImGui::TextColored(theme.status_error,
351 ICON_MD_ERROR " Too many rooms with zones (max 8)");
352 }
353 if (rooms_over_tile_limit > 0) {
354 ImGui::TextColored(theme.status_error,
355 ICON_MD_ERROR " %d room(s) exceed 255 tiles",
356 rooms_over_tile_limit);
357 }
358 if (duplicate_masks > 0) {
359 ImGui::TextColored(theme.status_error,
361 " Duplicate SRAM bit masks detected (%d mask(s))",
362 duplicate_masks);
363 }
364 if (rooms_unassigned_mask > 0) {
365 ImGui::TextColored(theme.text_warning_yellow,
367 " %d room(s) use Auto mask (assigned on save)",
368 rooms_unassigned_mask);
369 }
370
371 if (ImGui::BeginTable("##WaterFillZoneOverview", 6,
372 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
373 ImGuiTableFlags_SizingFixedFit)) {
374 ImGui::TableSetupColumn("Room");
375 ImGui::TableSetupColumn("Tiles");
376 ImGui::TableSetupColumn("Mask");
377 ImGui::TableSetupColumn("Dirty");
378 ImGui::TableSetupColumn("Dup?");
379 ImGui::TableSetupColumn("Action");
380 ImGui::TableHeadersRow();
381
382 for (const auto& row : rows) {
383 const bool is_current = (row.room_id == room_id);
384 const bool is_dup = (row.mask != 0 && mask_counts.contains(row.mask) &&
385 mask_counts[row.mask] > 1);
386
387 ImGui::TableNextRow();
388 ImGui::TableNextColumn();
389 if (is_current) {
390 ImGui::TextColored(theme.text_info, tr("0x%02X"), row.room_id);
391 } else {
392 ImGui::Text(tr("0x%02X"), row.room_id);
393 }
394
395 ImGui::TableNextColumn();
396 ImGui::Text("%d", row.tiles);
397
398 ImGui::TableNextColumn();
399 if (row.mask == 0) {
400 ImGui::TextDisabled(tr("Auto"));
401 } else {
402 ImGui::Text(tr("0x%02X"), row.mask);
403 }
404
405 ImGui::TableNextColumn();
406 ImGui::TextUnformatted(row.dirty ? "Yes" : "No");
407
408 ImGui::TableNextColumn();
409 if (is_dup) {
410 ImGui::TextColored(theme.status_error, ICON_MD_ERROR);
411 } else {
412 ImGui::TextDisabled("-");
413 }
414
415 ImGui::TableNextColumn();
416 if (viewer_->CanNavigateRooms()) {
417 ImGui::PushID(row.room_id);
418 if (ImGui::SmallButton(tr("Open"))) {
419 viewer_->NavigateToRoom(row.room_id);
420 }
421 ImGui::PopID();
422 } else {
423 ImGui::TextDisabled("-");
424 }
425 }
426
427 ImGui::EndTable();
428 }
429 }
430}
431
432std::vector<zelda3::WaterFillZoneEntry> WaterFillPanel::CollectZones(
433 const DungeonRoomStore& rooms) {
434 std::vector<zelda3::WaterFillZoneEntry> zones;
435 zones.reserve(8);
436 for (int room_id = 0; room_id < static_cast<int>(rooms.size()); ++room_id) {
437 const auto* room = rooms.GetIfMaterialized(room_id);
438 if (room == nullptr) {
439 continue;
440 }
441 const int tile_count = room->WaterFillTileCount();
442 if (tile_count <= 0) {
443 continue;
444 }
445
447 z.room_id = room_id;
448 z.sram_bit_mask = room->water_fill_sram_bit_mask();
449 z.fill_offsets.reserve(static_cast<size_t>(tile_count));
450
451 const auto& map = room->water_fill_zone().tiles;
452 for (size_t i = 0; i < map.size(); ++i) {
453 if (map[i] != 0) {
454 z.fill_offsets.push_back(static_cast<uint16_t>(i));
455 }
456 }
457 zones.push_back(std::move(z));
458 }
459 return zones;
460}
461
463 zelda3::Room* room) {
464 if (room == nullptr) {
465 return;
466 }
467 room->ClearWaterFillZone();
469 for (uint16_t off : z.fill_offsets) {
470 const int x = static_cast<int>(off % 64);
471 const int y = static_cast<int>(off / 64);
472 room->SetWaterFillTile(x, y, true);
473 }
474}
475
476} // namespace yaze::editor
const auto & vector() const
Definition rom.h:217
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
zelda3::Room * GetIfMaterialized(int room_id)
void SetMode(InteractionMode mode)
Set interaction mode.
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
static std::vector< zelda3::WaterFillZoneEntry > CollectZones(const DungeonRoomStore &rooms)
DungeonObjectInteraction * interaction_
DungeonCanvasViewer * viewer_
const InteractionContext * RoomEditContext(int room_id) const
void EditRoomWaterFill(int room_id, Edit edit)
static void ApplyZoneToRoom(const zelda3::WaterFillZoneEntry &z, zelda3::Room *room)
void Draw(bool *p_open) override
Draw the panel content.
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 set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:647
int WaterFillTileCount() const
Definition room.h:642
void ClearWaterFillZone()
Definition room.h:634
void SetWaterFillTile(int x, int y, bool filled)
Definition room.h:611
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_LOCK
Definition icons.h:1140
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_UPLOAD
Definition icons.h:2048
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_DOWNLOAD
Definition icons.h:618
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
std::string LoadFile(const std::string &filename)
Loads the entire contents of a file into a string.
Definition file_util.cc:63
absl::StatusOr< std::string > DumpWaterFillZonesToJsonString(const std::vector< WaterFillZoneEntry > &zones)
constexpr int kWaterFillTableEnd
absl::Status NormalizeWaterFillZoneMasks(std::vector< WaterFillZoneEntry > *zones)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillZonesFromJsonString(const std::string &json_content)
constexpr bool HasWaterFillReservedRegion(std::size_t rom_size)
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::vector< FileDialogFilter > filters
Definition file_util.h:18
std::vector< uint16_t > fill_offsets
Automatic widget registration helpers for ImGui Test Engine integration.