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