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