yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
water_fill_zone.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <fstream>
7#include <limits>
8#include <optional>
9#include <regex>
10#include <string>
11#include <unordered_map>
12#include <utility>
13#include <vector>
14
15#if defined(YAZE_WITH_JSON)
16#include "nlohmann/json.hpp"
17#endif
18
19#include "absl/status/status.h"
20#include "absl/strings/str_format.h"
21#include "rom/rom.h"
22#include "rom/snes.h"
23#include "rom/write_fence.h"
24#include "util/macro.h"
26
27namespace yaze::zelda3 {
28
29namespace {
30
31constexpr int kRoomCount = kNumberOfRooms;
32constexpr int kWaterFillRuntimeRoomCount = 0x100;
33constexpr int kGridSize = 64;
34constexpr int kGridTiles = kGridSize * kGridSize;
35constexpr uint16_t kCollisionSingleTileMarker = 0xF0F0;
36constexpr uint16_t kCollisionEndMarker = 0xFFFF;
37
38bool IsWaterFillRuntimeRoomId(int room_id) {
39 return room_id >= 0 && room_id < kWaterFillRuntimeRoomCount;
40}
41
42bool IsSingleBitMask(uint8_t mask) {
43 return mask != 0 && (mask & (mask - 1)) == 0;
44}
45
46absl::Status ValidateZone(const WaterFillZoneEntry& z) {
48 return absl::OutOfRangeError(
49 "WaterFillZoneEntry room_id must fit the runtime room byte");
50 }
52 return absl::InvalidArgumentError(
53 "WaterFillZoneEntry sram_bit_mask must be a single bit");
54 }
55 if (z.fill_offsets.size() > 255) {
56 return absl::InvalidArgumentError(
57 "WaterFillZoneEntry fill_offsets exceeds 255 tiles (db count limit)");
58 }
59 for (uint16_t off : z.fill_offsets) {
60 if (off >= kGridTiles) {
61 return absl::OutOfRangeError(
62 absl::StrFormat("WaterFillZoneEntry offset out of range: %u", off));
63 }
64 }
65 return absl::OkStatus();
66}
67
68std::vector<WaterFillZoneEntry> DedupAndSort(
69 std::vector<WaterFillZoneEntry> zones) {
70 // Ensure stable output for deterministic ROM writes.
71 std::sort(zones.begin(), zones.end(),
72 [](const WaterFillZoneEntry& a, const WaterFillZoneEntry& b) {
73 return a.room_id < b.room_id;
74 });
75 for (auto& z : zones) {
76 std::sort(z.fill_offsets.begin(), z.fill_offsets.end());
77 z.fill_offsets.erase(
78 std::unique(z.fill_offsets.begin(), z.fill_offsets.end()),
79 z.fill_offsets.end());
80 }
81 return zones;
82}
83
84std::optional<std::string> GuessSymbolPathFromRom(const std::string& rom_path) {
85 if (rom_path.empty()) {
86 return std::nullopt;
87 }
88
89 // Common convention in this repo: <rom>.sfc -> <rom>.sym
90 // Example: oos168x.sfc -> oos168x.sym
91 auto dot = rom_path.find_last_of('.');
92 if (dot == std::string::npos) {
93 return std::nullopt;
94 }
95 std::string sym = rom_path.substr(0, dot) + ".sym";
96 std::ifstream f(sym);
97 if (!f.good()) {
98 return std::nullopt;
99 }
100 return sym;
101}
102
103absl::StatusOr<uint32_t> FindCustomCollisionRoomEndPc(
104 const std::vector<uint8_t>& rom_data, uint32_t start_pc) {
105 if (start_pc >= rom_data.size()) {
106 return absl::OutOfRangeError("Collision data pointer out of range");
107 }
108
109 size_t cursor = static_cast<size_t>(start_pc);
110 bool single_mode = false;
111 while (cursor + 1 < rom_data.size()) {
112 const uint16_t val =
113 static_cast<uint16_t>(rom_data[cursor] | (rom_data[cursor + 1] << 8));
114 cursor += 2;
115
116 if (val == kCollisionEndMarker) {
117 break;
118 }
119 if (val == kCollisionSingleTileMarker) {
120 single_mode = true;
121 continue;
122 }
123
124 if (!single_mode) {
125 if (cursor + 1 >= rom_data.size()) {
126 return absl::OutOfRangeError("Collision rectangle header out of range");
127 }
128 const uint8_t w = rom_data[cursor];
129 const uint8_t h = rom_data[cursor + 1];
130 cursor += 2;
131 cursor += static_cast<size_t>(w) * static_cast<size_t>(h);
132 } else {
133 if (cursor >= rom_data.size()) {
134 return absl::OutOfRangeError("Collision single tile out of range");
135 }
136 cursor += 1;
137 }
138 }
139
140 return static_cast<uint32_t>(cursor);
141}
142
144 const std::vector<uint8_t>& rom_data) {
145 const int ptrs_size = kNumberOfRooms * 3;
146 if (kCustomCollisionRoomPointers + ptrs_size >
147 static_cast<int>(rom_data.size())) {
148 // Vanilla ROMs don't have the expanded collision bank or pointer table.
149 // The caller will already fail if the reserved region isn't present.
150 return absl::OkStatus();
151 }
152
153 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
154 const int ptr_offset = kCustomCollisionRoomPointers + (room_id * 3);
155 if (ptr_offset + 2 >= static_cast<int>(rom_data.size())) {
156 return absl::OutOfRangeError("Collision pointer table out of range");
157 }
158
159 const uint32_t snes_ptr = rom_data[ptr_offset] |
160 (rom_data[ptr_offset + 1] << 8) |
161 (rom_data[ptr_offset + 2] << 16);
162 if (snes_ptr == 0) {
163 continue;
164 }
165
166 const uint32_t pc = SnesToPc(snes_ptr);
167 if (pc >= static_cast<uint32_t>(kWaterFillTableStart) &&
168 pc < static_cast<uint32_t>(kWaterFillTableEnd)) {
169 return absl::FailedPreconditionError(
170 absl::StrFormat("Custom collision pointer for room 0x%02X overlaps "
171 "WaterFill reserved region (pc=0x%06X)",
172 room_id, pc));
173 }
174
175 ASSIGN_OR_RETURN(const uint32_t end_pc,
176 FindCustomCollisionRoomEndPc(rom_data, pc));
177 if (end_pc > static_cast<uint32_t>(kCustomCollisionDataSoftEnd)) {
178 return absl::FailedPreconditionError(absl::StrFormat(
179 "Custom collision data for room 0x%02X overlaps WaterFill reserved "
180 "region (end=0x%06X, reserved_start=0x%06X)",
181 room_id, end_pc, kCustomCollisionDataSoftEnd));
182 }
183 }
184
185 return absl::OkStatus();
186}
187
188} // namespace
189
190absl::StatusOr<std::vector<WaterFillZoneEntry>> LoadWaterFillTable(Rom* rom) {
191 if (!rom || !rom->is_loaded()) {
192 return absl::InvalidArgumentError("ROM not loaded");
193 }
194
195 const auto& data = rom->vector();
196 if (kWaterFillTableEnd > static_cast<int>(data.size())) {
197 // On vanilla ROMs (no expanded collision bank), just treat as absent.
198 return std::vector<WaterFillZoneEntry>{};
199 }
200
201 // ROM safety: ensure custom collision does not overlap the reserved WaterFill
202 // table region. This catches corrupted ROM layouts early and prevents the
203 // editor from presenting potentially invalid authoring state.
204 RETURN_IF_ERROR(ValidateCustomCollisionDoesNotOverlapWaterFillReserved(data));
205
206 const uint8_t zone_count = data[static_cast<size_t>(kWaterFillTableStart)];
207 if (zone_count == 0) {
208 return std::vector<WaterFillZoneEntry>{};
209 }
210
211 // Heuristic: we only have 8 bits in $7EF411 per current runtime design.
212 // If the region contains random bytes, this avoids false-positives.
213 if (zone_count > 8) {
214 return std::vector<WaterFillZoneEntry>{};
215 }
216
217 const size_t header_size = 1u + (static_cast<size_t>(zone_count) * 4u);
218 if (header_size > static_cast<size_t>(kWaterFillTableReservedSize)) {
219 return absl::FailedPreconditionError(
220 "WaterFill table header exceeds reserved region");
221 }
222
223 struct Entry {
224 int room_id;
225 uint8_t mask;
226 uint16_t data_off;
227 };
228 std::vector<Entry> entries;
229 entries.reserve(zone_count);
230
231 std::unordered_map<int, size_t> room_to_index;
232 std::unordered_map<uint8_t, int> mask_to_room;
233 for (size_t i = 0; i < zone_count; ++i) {
234 size_t off = static_cast<size_t>(kWaterFillTableStart) + 1u + (i * 4u);
235 int room_id = data[off];
236 uint8_t mask = data[off + 1];
237 uint16_t data_off =
238 static_cast<uint16_t>(data[off + 2] | (data[off + 3] << 8));
239
240 if (room_id < 0 || room_id >= kRoomCount) {
241 return absl::FailedPreconditionError(
242 absl::StrFormat("WaterFill entry room_id invalid: %d", room_id));
243 }
244 if (!IsSingleBitMask(mask)) {
245 return absl::FailedPreconditionError(
246 absl::StrFormat("WaterFill entry invalid sram mask: 0x%02X", mask));
247 }
248 if (room_to_index.contains(room_id)) {
249 return absl::FailedPreconditionError(absl::StrFormat(
250 "Duplicate WaterFill entry for room 0x%02X", room_id));
251 }
252 room_to_index[room_id] = entries.size();
253 if (mask_to_room.contains(mask)) {
254 return absl::FailedPreconditionError(absl::StrFormat(
255 "Duplicate WaterFill mask 0x%02X for rooms 0x%02X and 0x%02X", mask,
256 mask_to_room[mask], room_id));
257 }
258 mask_to_room[mask] = room_id;
259
260 // data_off is relative to table start.
261 if (data_off >= kWaterFillTableReservedSize) {
262 return absl::FailedPreconditionError(absl::StrFormat(
263 "WaterFill entry data offset out of range: 0x%04X", data_off));
264 }
265 if (data_off < header_size) {
266 return absl::FailedPreconditionError(
267 "WaterFill entry data offset overlaps table header");
268 }
269
270 entries.push_back(Entry{room_id, mask, data_off});
271 }
272
273 std::vector<WaterFillZoneEntry> zones;
274 zones.reserve(entries.size());
275 for (const auto& e : entries) {
276 const size_t data_pos = static_cast<size_t>(kWaterFillTableStart) +
277 static_cast<size_t>(e.data_off);
278 if (data_pos >= static_cast<size_t>(kWaterFillTableEnd)) {
279 return absl::FailedPreconditionError(
280 "WaterFill entry data_pos out of range");
281 }
282 const uint8_t tile_count = data[data_pos];
283 if (tile_count > 255) {
284 return absl::FailedPreconditionError("WaterFill tile_count invalid");
285 }
286 const size_t needed = 1u + static_cast<size_t>(tile_count) * 2u;
287 if (data_pos + needed > static_cast<size_t>(kWaterFillTableEnd)) {
288 return absl::FailedPreconditionError(
289 "WaterFill entry tile data exceeds reserved region");
290 }
291
293 z.room_id = e.room_id;
294 z.sram_bit_mask = e.mask;
295 z.fill_offsets.reserve(tile_count);
296 for (size_t i = 0; i < tile_count; ++i) {
297 const size_t o = data_pos + 1u + (i * 2u);
298 uint16_t off = static_cast<uint16_t>(data[o] | (data[o + 1] << 8));
299 if (off >= kGridTiles) {
300 return absl::FailedPreconditionError(
301 absl::StrFormat("WaterFill offset out of range: %u", off));
302 }
303 z.fill_offsets.push_back(off);
304 }
305
306 zones.push_back(std::move(z));
307 }
308
309 zones = DedupAndSort(std::move(zones));
310 for (const auto& z : zones) {
311 RETURN_IF_ERROR(ValidateZone(z));
312 }
313 return zones;
314}
315
316absl::Status WriteWaterFillTable(Rom* rom,
317 const std::vector<WaterFillZoneEntry>& zones) {
318 if (!rom || !rom->is_loaded()) {
319 return absl::InvalidArgumentError("ROM not loaded");
320 }
321
322 const auto& rom_data = rom->vector();
323 if (kWaterFillTableEnd > static_cast<int>(rom_data.size())) {
324 return absl::OutOfRangeError(
325 "WaterFill reserved region not present in this ROM");
326 }
327
328 // Safety: never clobber legacy/custom collision data that might already
329 // occupy the reserved tail region.
331 ValidateCustomCollisionDoesNotOverlapWaterFillReserved(rom_data));
332
333 // Enforce current SRAM bitfield capacity.
334 if (zones.size() > 8) {
335 return absl::InvalidArgumentError(
336 "Too many water fill zones: max 8 (fits in $7EF411 bitfield)");
337 }
338
339 // Validate + normalize.
340 auto normalized = DedupAndSort(zones);
341 std::unordered_map<int, uint8_t> seen_rooms;
342 std::unordered_map<uint8_t, int> seen_masks;
343 for (const auto& z : normalized) {
344 RETURN_IF_ERROR(ValidateZone(z));
345 if (seen_rooms.contains(z.room_id)) {
346 return absl::InvalidArgumentError(absl::StrFormat(
347 "Duplicate water fill zone for room 0x%02X", z.room_id));
348 }
349 seen_rooms[z.room_id] = z.sram_bit_mask;
350 if (seen_masks.contains(z.sram_bit_mask)) {
351 return absl::InvalidArgumentError(absl::StrFormat(
352 "Duplicate SRAM mask 0x%02X used by rooms 0x%02X and 0x%02X",
353 z.sram_bit_mask, seen_masks[z.sram_bit_mask], z.room_id));
354 }
355 seen_masks[z.sram_bit_mask] = z.room_id;
356 }
357
358 std::vector<uint8_t> bytes;
359 bytes.reserve(static_cast<size_t>(kWaterFillTableReservedSize));
360 bytes.push_back(static_cast<uint8_t>(normalized.size()));
361
362 // Header entries with placeholder offsets.
363 for (const auto& z : normalized) {
364 bytes.push_back(static_cast<uint8_t>(z.room_id));
365 bytes.push_back(z.sram_bit_mask);
366 bytes.push_back(0x00); // data_offset lo
367 bytes.push_back(0x00); // data_offset hi
368 }
369
370 // Data sections.
371 for (size_t i = 0; i < normalized.size(); ++i) {
372 const auto& z = normalized[i];
373 const uint16_t data_off = static_cast<uint16_t>(bytes.size());
374 const size_t entry_off = 1u + (i * 4u);
375 bytes[entry_off + 2] = data_off & 0xFF;
376 bytes[entry_off + 3] = (data_off >> 8) & 0xFF;
377
378 bytes.push_back(static_cast<uint8_t>(z.fill_offsets.size()));
379 for (uint16_t off : z.fill_offsets) {
380 bytes.push_back(off & 0xFF);
381 bytes.push_back((off >> 8) & 0xFF);
382 }
383 }
384
385 if (bytes.size() > static_cast<size_t>(kWaterFillTableReservedSize)) {
386 return absl::ResourceExhaustedError(
387 absl::StrFormat("WaterFill table too large (%zu bytes), reserved=%d",
388 bytes.size(), kWaterFillTableReservedSize));
389 }
390
391 // Write full reserved region (zero-filled tail) for determinism.
392 std::vector<uint8_t> region(static_cast<size_t>(kWaterFillTableReservedSize),
393 0);
394 std::copy(bytes.begin(), bytes.end(), region.begin());
395
396 // Save-time guardrails: this writer must only touch the reserved WaterFill
397 // region, never other ROM content.
399 RETURN_IF_ERROR(fence.Allow(static_cast<uint32_t>(kWaterFillTableStart),
400 static_cast<uint32_t>(kWaterFillTableEnd),
401 "WaterFillTable"));
402 yaze::rom::ScopedWriteFence scope(rom, &fence);
403 return rom->WriteVector(kWaterFillTableStart, std::move(region));
404}
405
406absl::StatusOr<std::vector<WaterFillZoneEntry>> LoadLegacyWaterGateZones(
407 Rom* rom, const std::string& symbol_path) {
408 if (!rom || !rom->is_loaded()) {
409 return absl::InvalidArgumentError("ROM not loaded");
410 }
411
412 std::string path = symbol_path;
413 if (path.empty()) {
414 if (auto guess = GuessSymbolPathFromRom(rom->filename());
415 guess.has_value()) {
416 path = *guess;
417 }
418 }
419 if (path.empty()) {
420 return std::vector<WaterFillZoneEntry>{};
421 }
422
423 std::ifstream file(path);
424 if (!file.is_open()) {
425 return std::vector<WaterFillZoneEntry>{};
426 }
427
428 std::optional<uint32_t> room25_snes;
429 std::optional<uint32_t> room27_snes;
430
431 // WLA symbol file format: "BB:AAAA Label".
432 const std::regex re(R"(^\s*([0-9A-Fa-f]{2}):([0-9A-Fa-f]{4})\s+(.+?)\s*$)");
433 std::string line;
434 while (std::getline(file, line)) {
435 std::smatch m;
436 if (!std::regex_match(line, m, re)) {
437 continue;
438 }
439 const int bank = std::stoi(m[1].str(), nullptr, 16);
440 const int addr = std::stoi(m[2].str(), nullptr, 16);
441 const std::string label = m[3].str();
442
443 if (label == "Oracle_WaterGate_Room25_Data") {
444 room25_snes = static_cast<uint32_t>((bank << 16) | addr);
445 } else if (label == "Oracle_WaterGate_Room27_Data") {
446 room27_snes = static_cast<uint32_t>((bank << 16) | addr);
447 }
448 }
449
450 const auto& data = rom->vector();
451 auto read_zone = [&](int room_id, uint8_t mask,
452 std::optional<uint32_t> snes_opt)
453 -> absl::StatusOr<std::optional<WaterFillZoneEntry>> {
454 if (!snes_opt.has_value()) {
455 return std::optional<WaterFillZoneEntry>{};
456 }
457 const uint32_t pc = SnesToPc(*snes_opt);
458 if (pc >= data.size()) {
459 return absl::OutOfRangeError(
460 "Legacy water gate data pointer out of range");
461 }
462 const uint8_t count = data[pc];
463 const size_t needed = 1u + static_cast<size_t>(count) * 2u;
464 if (pc + needed > data.size()) {
465 return absl::OutOfRangeError("Legacy water gate data exceeds ROM");
466 }
467
469 z.room_id = room_id;
470 z.sram_bit_mask = mask;
471 z.fill_offsets.reserve(count);
472 for (size_t i = 0; i < count; ++i) {
473 const size_t o = pc + 1u + (i * 2u);
474 uint16_t off = static_cast<uint16_t>(data[o] | (data[o + 1] << 8));
475 if (off >= kGridTiles) {
476 return absl::FailedPreconditionError(
477 absl::StrFormat("Legacy water gate offset out of range: %u", off));
478 }
479 z.fill_offsets.push_back(off);
480 }
481
482 RETURN_IF_ERROR(ValidateZone(z));
483 return std::optional<WaterFillZoneEntry>(std::move(z));
484 };
485
486 std::vector<WaterFillZoneEntry> zones;
487
488 // Legacy mapping from water_collision.asm:
489 // bit 0 = room 0x27, bit 1 = room 0x25
490 ASSIGN_OR_RETURN(auto z27, read_zone(0x27, 0x01, room27_snes));
491 ASSIGN_OR_RETURN(auto z25, read_zone(0x25, 0x02, room25_snes));
492
493 if (z27.has_value())
494 zones.push_back(std::move(*z27));
495 if (z25.has_value())
496 zones.push_back(std::move(*z25));
497
498 zones = DedupAndSort(std::move(zones));
499 return zones;
500}
501
503 std::vector<WaterFillZoneEntry>* zones) {
504 if (zones == nullptr) {
505 return absl::InvalidArgumentError("zones is null");
506 }
507 if (zones->size() > 8) {
508 return absl::InvalidArgumentError(absl::StrFormat(
509 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
510 zones->size()));
511 }
512
513 for (const auto& z : *zones) {
514 if (!IsWaterFillRuntimeRoomId(z.room_id)) {
515 return absl::OutOfRangeError(absl::StrFormat(
516 "WaterFill room_id must fit the runtime room byte: %d", z.room_id));
517 }
518 }
519
520 // Ensure stable room ordering + deterministic offset layout.
521 *zones = DedupAndSort(std::move(*zones));
522
523 std::unordered_map<int, bool> seen_rooms;
524 uint8_t used_masks = 0;
525 std::vector<WaterFillZoneEntry*> unassigned;
526 unassigned.reserve(zones->size());
527
528 for (auto& z : *zones) {
529 if (seen_rooms.contains(z.room_id)) {
530 return absl::InvalidArgumentError(absl::StrFormat(
531 "Duplicate water fill zone for room 0x%02X", z.room_id));
532 }
533 seen_rooms[z.room_id] = true;
534
535 const uint8_t mask = z.sram_bit_mask;
536 const bool valid =
537 (mask != 0) && IsSingleBitMask(mask) && ((used_masks & mask) == 0);
538 if (valid) {
539 used_masks |= mask;
540 } else {
541 z.sram_bit_mask = 0;
542 unassigned.push_back(&z);
543 }
544 }
545
546 constexpr uint8_t kBits[8] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};
547 for (auto* z : unassigned) {
548 uint8_t assigned = 0;
549 for (uint8_t bit : kBits) {
550 if ((used_masks & bit) == 0) {
551 assigned = bit;
552 break;
553 }
554 }
555 if (assigned == 0) {
556 return absl::ResourceExhaustedError(
557 "No free SRAM bits left in $7EF411 for water fill zones");
558 }
559 z->sram_bit_mask = assigned;
560 used_masks |= assigned;
561 }
562
563 return absl::OkStatus();
564}
565
566absl::StatusOr<std::string> DumpWaterFillZonesToJsonString(
567 const std::vector<WaterFillZoneEntry>& zones) {
568#if !defined(YAZE_WITH_JSON)
569 return absl::UnimplementedError(
570 "JSON support not enabled. Build with -DYAZE_WITH_JSON=ON");
571#else
572 using json = nlohmann::json;
573
574 std::vector<WaterFillZoneEntry> sorted = zones;
575 std::sort(sorted.begin(), sorted.end(),
576 [](const WaterFillZoneEntry& a, const WaterFillZoneEntry& b) {
577 return a.room_id < b.room_id;
578 });
579 for (auto& z : sorted) {
580 if (!IsWaterFillRuntimeRoomId(z.room_id)) {
581 return absl::OutOfRangeError(absl::StrFormat(
582 "WaterFill room_id must fit the runtime room byte: %d", z.room_id));
583 }
584 std::sort(z.fill_offsets.begin(), z.fill_offsets.end());
585 z.fill_offsets.erase(
586 std::unique(z.fill_offsets.begin(), z.fill_offsets.end()),
587 z.fill_offsets.end());
588 }
589
590 json root;
591 root["version"] = 1;
592 json arr = json::array();
593 for (const auto& z : sorted) {
594 json item;
595 item["room_id"] = absl::StrFormat("0x%02X", z.room_id);
596 item["mask"] = absl::StrFormat("0x%02X", z.sram_bit_mask);
597 item["offsets"] = z.fill_offsets;
598 arr.push_back(std::move(item));
599 }
600 root["zones"] = std::move(arr);
601 return root.dump(/*indent=*/2);
602#endif
603}
604
605absl::StatusOr<std::vector<WaterFillZoneEntry>>
606LoadWaterFillZonesFromJsonString(const std::string& json_content) {
607#if !defined(YAZE_WITH_JSON)
608 return absl::UnimplementedError(
609 "JSON support not enabled. Build with -DYAZE_WITH_JSON=ON");
610#else
611 using json = nlohmann::json;
612
613 json root;
614 try {
615 root = json::parse(json_content);
616 } catch (const json::parse_error& e) {
617 return absl::InvalidArgumentError(std::string("JSON parse error: ") +
618 e.what());
619 }
620
621 const int version = root.value("version", 1);
622 if (version != 1) {
623 return absl::InvalidArgumentError(
624 absl::StrFormat("Unsupported water fill JSON version: %d", version));
625 }
626 if (!root.contains("zones") || !root["zones"].is_array()) {
627 return absl::InvalidArgumentError("Missing or invalid 'zones' array");
628 }
629
630 auto parse_int = [](const json& v) -> std::optional<int> {
631 if (v.is_number_integer()) {
632 return v.get<int>();
633 }
634 if (v.is_number_unsigned()) {
635 const auto u = v.get<unsigned int>();
636 if (u > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
637 return std::nullopt;
638 }
639 return static_cast<int>(u);
640 }
641 if (v.is_string()) {
642 try {
643 size_t idx = 0;
644 const std::string s = v.get<std::string>();
645 const int parsed = std::stoi(s, &idx, 0);
646 if (idx == s.size()) {
647 return parsed;
648 }
649 } catch (...) {}
650 }
651 return std::nullopt;
652 };
653
654 std::vector<WaterFillZoneEntry> zones;
655 zones.reserve(root["zones"].size());
656
657 std::unordered_map<int, bool> seen_rooms;
658 for (const auto& item : root["zones"]) {
659 if (!item.is_object()) {
660 continue;
661 }
662
663 const json& room_v = item.contains("room_id") ? item["room_id"]
664 : item.contains("room") ? item["room"]
665 : json();
666 const auto room_id_opt = parse_int(room_v);
667 if (!room_id_opt.has_value() || !IsWaterFillRuntimeRoomId(*room_id_opt)) {
668 return absl::InvalidArgumentError(
669 "Invalid room_id in water fill JSON (must fit runtime room byte)");
670 }
671 const int room_id = *room_id_opt;
672
673 if (seen_rooms.contains(room_id)) {
674 return absl::InvalidArgumentError(absl::StrFormat(
675 "Duplicate room_id in water fill JSON: 0x%02X", room_id));
676 }
677 seen_rooms[room_id] = true;
678
679 const json& mask_v = item.contains("mask") ? item["mask"]
680 : item.contains("sram_mask") ? item["sram_mask"]
681 : item.contains("sram_bit_mask")
682 ? item["sram_bit_mask"]
683 : json();
684 uint8_t mask = 0;
685 if (!mask_v.is_null()) {
686 const auto m_opt = parse_int(mask_v);
687 if (!m_opt.has_value() || *m_opt < 0 || *m_opt > 0xFF) {
688 return absl::InvalidArgumentError(
689 absl::StrFormat("Invalid mask for room 0x%02X", room_id));
690 }
691 mask = static_cast<uint8_t>(*m_opt);
692 }
693
694 // Allow 0 (Auto) or a single-bit mask.
695 if (mask != 0 && !IsSingleBitMask(mask)) {
696 return absl::InvalidArgumentError(absl::StrFormat(
697 "Invalid mask 0x%02X for room 0x%02X", mask, room_id));
698 }
699
700 const json& offsets_v = item.contains("offsets") ? item["offsets"]
701 : item.contains("fill_offsets")
702 ? item["fill_offsets"]
703 : json::array();
704 if (!offsets_v.is_array()) {
705 return absl::InvalidArgumentError(
706 absl::StrFormat("Invalid offsets array for room 0x%02X", room_id));
707 }
708
710 z.room_id = room_id;
711 z.sram_bit_mask = mask;
712 z.fill_offsets.reserve(offsets_v.size());
713 for (const auto& off_v : offsets_v) {
714 const auto o_opt = parse_int(off_v);
715 if (!o_opt.has_value() || *o_opt < 0 || *o_opt >= kGridTiles) {
716 return absl::InvalidArgumentError(
717 absl::StrFormat("Invalid offset for room 0x%02X", room_id));
718 }
719 z.fill_offsets.push_back(static_cast<uint16_t>(*o_opt));
720 if (z.fill_offsets.size() > 255) {
721 return absl::InvalidArgumentError(absl::StrFormat(
722 "WaterFill offsets exceed 255 tiles for room 0x%02X", room_id));
723 }
724 }
725
726 std::sort(z.fill_offsets.begin(), z.fill_offsets.end());
727 z.fill_offsets.erase(
728 std::unique(z.fill_offsets.begin(), z.fill_offsets.end()),
729 z.fill_offsets.end());
730
731 zones.push_back(std::move(z));
732 }
733
734 if (zones.size() > 8) {
735 return absl::InvalidArgumentError(absl::StrFormat(
736 "Too many water fill zones in JSON: %zu (max 8)", zones.size()));
737 }
738
739 zones = DedupAndSort(std::move(zones));
740 return zones;
741#endif
742}
743
744} // namespace yaze::zelda3
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
auto filename() const
Definition rom.h:157
const auto & vector() const
Definition rom.h:155
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:658
bool is_loaded() const
Definition rom.h:144
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Definition write_fence.h:32
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::optional< std::string > GuessSymbolPathFromRom(const std::string &rom_path)
std::vector< WaterFillZoneEntry > DedupAndSort(std::vector< WaterFillZoneEntry > zones)
absl::StatusOr< uint32_t > FindCustomCollisionRoomEndPc(const std::vector< uint8_t > &rom_data, uint32_t start_pc)
absl::Status ValidateZone(const WaterFillZoneEntry &z)
absl::Status ValidateCustomCollisionDoesNotOverlapWaterFillReserved(const std::vector< uint8_t > &rom_data)
Zelda 3 specific classes and functions.
absl::StatusOr< std::string > DumpWaterFillZonesToJsonString(const std::vector< WaterFillZoneEntry > &zones)
constexpr int kWaterFillTableEnd
constexpr int kCustomCollisionDataSoftEnd
nlohmann::json json
constexpr int kWaterFillTableStart
absl::Status NormalizeWaterFillZoneMasks(std::vector< WaterFillZoneEntry > *zones)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadLegacyWaterGateZones(Rom *rom, const std::string &symbol_path)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillZonesFromJsonString(const std::string &json_content)
constexpr int kWaterFillTableReservedSize
constexpr int kNumberOfRooms
constexpr int kCustomCollisionRoomPointers
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
absl::Status WriteWaterFillTable(Rom *rom, const std::vector< WaterFillZoneEntry > &zones)
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< uint16_t > fill_offsets