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