184 return absl::InvalidArgumentError(
"ROM not loaded");
187 const auto& data = rom->
vector();
190 return std::vector<WaterFillZoneEntry>{};
196 RETURN_IF_ERROR(ValidateCustomCollisionDoesNotOverlapWaterFillReserved(data));
199 if (zone_count == 0) {
200 return std::vector<WaterFillZoneEntry>{};
205 if (zone_count > 8) {
206 return std::vector<WaterFillZoneEntry>{};
209 const size_t header_size = 1u + (
static_cast<size_t>(zone_count) * 4u);
211 return absl::FailedPreconditionError(
212 "WaterFill table header exceeds reserved region");
220 std::vector<Entry> entries;
221 entries.reserve(zone_count);
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) {
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));
231 if (room_id < 0 || room_id >= kRoomCount) {
232 return absl::FailedPreconditionError(
233 absl::StrFormat(
"WaterFill entry room_id invalid: %d", room_id));
235 if (!IsSingleBitMask(mask)) {
236 return absl::FailedPreconditionError(
237 absl::StrFormat(
"WaterFill entry invalid sram mask: 0x%02X", mask));
239 if (room_to_index.contains(room_id)) {
240 return absl::FailedPreconditionError(
241 absl::StrFormat(
"Duplicate WaterFill entry for room 0x%02X", room_id));
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));
249 mask_to_room[mask] = room_id;
253 return absl::FailedPreconditionError(
254 absl::StrFormat(
"WaterFill entry data offset out of range: 0x%04X",
257 if (data_off < header_size) {
258 return absl::FailedPreconditionError(
259 "WaterFill entry data offset overlaps table header");
262 entries.push_back(Entry{room_id, mask, data_off});
265 std::vector<WaterFillZoneEntry> zones;
266 zones.reserve(entries.size());
267 for (
const auto& e : entries) {
268 const size_t data_pos =
271 return absl::FailedPreconditionError(
"WaterFill entry data_pos out of range");
273 const uint8_t tile_count = data[data_pos];
274 if (tile_count > 255) {
275 return absl::FailedPreconditionError(
"WaterFill tile_count invalid");
277 const size_t needed = 1u +
static_cast<size_t>(tile_count) * 2u;
279 return absl::FailedPreconditionError(
280 "WaterFill entry tile data exceeds reserved region");
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));
297 zones.push_back(std::move(z));
300 zones = DedupAndSort(std::move(zones));
301 for (
const auto& z : zones) {
308 const std::vector<WaterFillZoneEntry>& zones) {
310 return absl::InvalidArgumentError(
"ROM not loaded");
313 const auto& rom_data = rom->
vector();
315 return absl::OutOfRangeError(
316 "WaterFill reserved region not present in this ROM");
321 RETURN_IF_ERROR(ValidateCustomCollisionDoesNotOverlapWaterFillReserved(rom_data));
324 if (zones.size() > 8) {
325 return absl::InvalidArgumentError(
326 "Too many water fill zones: max 8 (fits in $7EF411 bitfield)");
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) {
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));
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],
346 seen_masks[z.sram_bit_mask] = z.room_id;
349 std::vector<uint8_t> bytes;
351 bytes.push_back(
static_cast<uint8_t
>(normalized.size()));
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);
358 bytes.push_back(0x00);
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;
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);
377 return absl::ResourceExhaustedError(absl::StrFormat(
378 "WaterFill table too large (%zu bytes), reserved=%d", bytes.size(),
385 std::copy(bytes.begin(), bytes.end(), region.begin());
398 Rom* rom,
const std::string& symbol_path) {
400 return absl::InvalidArgumentError(
"ROM not loaded");
403 std::string path = symbol_path;
405 if (
auto guess = GuessSymbolPathFromRom(rom->
filename()); guess.has_value()) {
410 return std::vector<WaterFillZoneEntry>{};
413 std::ifstream file(path);
414 if (!file.is_open()) {
415 return std::vector<WaterFillZoneEntry>{};
418 std::optional<uint32_t> room25_snes;
419 std::optional<uint32_t> room27_snes;
422 const std::regex re(R
"(^\s*([0-9A-Fa-f]{2}):([0-9A-Fa-f]{4})\s+(.+?)\s*$)");
424 while (std::getline(file, line)) {
426 if (!std::regex_match(line, m, re)) {
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();
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);
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>{};
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");
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");
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));
472 return std::optional<WaterFillZoneEntry>(std::move(z));
475 std::vector<WaterFillZoneEntry> zones;
482 if (z27.has_value()) zones.push_back(std::move(*z27));
483 if (z25.has_value()) zones.push_back(std::move(*z25));
485 zones = DedupAndSort(std::move(zones));
490 if (zones ==
nullptr) {
491 return absl::InvalidArgumentError(
"zones is null");
493 if (zones->size() > 8) {
494 return absl::InvalidArgumentError(absl::StrFormat(
495 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
500 *zones = DedupAndSort(std::move(*zones));
502 std::unordered_map<int, bool> seen_rooms;
503 uint8_t used_masks = 0;
504 std::vector<WaterFillZoneEntry*> unassigned;
505 unassigned.reserve(zones->size());
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));
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));
516 seen_rooms[z.room_id] =
true;
518 const uint8_t mask = z.sram_bit_mask;
520 (mask != 0) && IsSingleBitMask(mask) && ((used_masks & mask) == 0);
525 unassigned.push_back(&z);
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) {
540 return absl::ResourceExhaustedError(
541 "No free SRAM bits left in $7EF411 for water fill zones");
543 z->sram_bit_mask = assigned;
544 used_masks |= assigned;
547 return absl::OkStatus();
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");
556 using json = nlohmann::json;
558 std::vector<WaterFillZoneEntry> sorted = zones;
559 std::sort(sorted.begin(), sorted.end(),
561 return a.room_id < b.room_id;
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());
572 json arr = json::array();
573 for (
const auto& z : sorted) {
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));
580 root[
"zones"] = std::move(arr);
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");
591 using json = nlohmann::json;
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());
601 const int version = root.value(
"version", 1);
603 return absl::InvalidArgumentError(
604 absl::StrFormat(
"Unsupported water fill JSON version: %d", version));
606 if (!root.contains(
"zones") || !root[
"zones"].is_array()) {
607 return absl::InvalidArgumentError(
"Missing or invalid 'zones' array");
610 auto parse_int = [](
const json& v) -> std::optional<int> {
611 if (v.is_number_integer()) {
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())) {
619 return static_cast<int>(u);
624 const std::string s = v.get<std::string>();
625 const int parsed = std::stoi(s, &idx, 0);
626 if (idx == s.size()) {
635 std::vector<WaterFillZoneEntry> zones;
636 zones.reserve(root[
"zones"].size());
638 std::unordered_map<int, bool> seen_rooms;
639 for (
const auto& item : root[
"zones"]) {
640 if (!item.is_object()) {
645 item.contains(
"room_id") ? item[
"room_id"]
646 : item.contains(
"room") ? item[
"room"]
648 const auto room_id_opt = parse_int(room_v);
649 if (!room_id_opt.has_value() || *room_id_opt < 0 ||
651 return absl::InvalidArgumentError(
"Invalid room_id in water fill JSON");
653 const int room_id = *room_id_opt;
655 if (seen_rooms.contains(room_id)) {
656 return absl::InvalidArgumentError(
657 absl::StrFormat(
"Duplicate room_id in water fill JSON: 0x%02X",
660 seen_rooms[room_id] =
true;
663 item.contains(
"mask") ? item[
"mask"]
664 : item.contains(
"sram_mask") ? item[
"sram_mask"]
665 : item.contains(
"sram_bit_mask") ? item[
"sram_bit_mask"]
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));
674 mask =
static_cast<uint8_t
>(*m_opt);
678 if (mask != 0 && !IsSingleBitMask(mask)) {
679 return absl::InvalidArgumentError(
680 absl::StrFormat(
"Invalid mask 0x%02X for room 0x%02X", mask,
684 const json& offsets_v =
685 item.contains(
"offsets") ? item[
"offsets"]
686 : item.contains(
"fill_offsets") ? item[
"fill_offsets"]
688 if (!offsets_v.is_array()) {
689 return absl::InvalidArgumentError(
690 absl::StrFormat(
"Invalid offsets array for room 0x%02X", room_id));
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));
703 z.
fill_offsets.push_back(
static_cast<uint16_t
>(*o_opt));
705 return absl::InvalidArgumentError(absl::StrFormat(
706 "WaterFill offsets exceed 255 tiles for room 0x%02X", room_id));
715 zones.push_back(std::move(z));
718 if (zones.size() > 8) {
719 return absl::InvalidArgumentError(
720 absl::StrFormat(
"Too many water fill zones in JSON: %zu (max 8)",
724 zones = DedupAndSort(std::move(zones));
std::vector< uint16_t > fill_offsets