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