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