192 return absl::InvalidArgumentError(
"ROM not loaded");
195 const auto& data = rom->
vector();
198 return std::vector<WaterFillZoneEntry>{};
204 RETURN_IF_ERROR(ValidateCustomCollisionDoesNotOverlapWaterFillReserved(data));
207 if (zone_count == 0) {
208 return std::vector<WaterFillZoneEntry>{};
213 if (zone_count > 8) {
214 return std::vector<WaterFillZoneEntry>{};
217 const size_t header_size = 1u + (
static_cast<size_t>(zone_count) * 4u);
219 return absl::FailedPreconditionError(
220 "WaterFill table header exceeds reserved region");
228 std::vector<Entry> entries;
229 entries.reserve(zone_count);
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) {
235 int room_id = data[off];
236 uint8_t mask = data[off + 1];
238 static_cast<uint16_t
>(data[off + 2] | (data[off + 3] << 8));
240 if (room_id < 0 || room_id >= kRoomCount) {
241 return absl::FailedPreconditionError(
242 absl::StrFormat(
"WaterFill entry room_id invalid: %d", room_id));
244 if (!IsSingleBitMask(mask)) {
245 return absl::FailedPreconditionError(
246 absl::StrFormat(
"WaterFill entry invalid sram mask: 0x%02X", mask));
248 if (room_to_index.contains(room_id)) {
249 return absl::FailedPreconditionError(absl::StrFormat(
250 "Duplicate WaterFill entry for room 0x%02X", room_id));
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));
258 mask_to_room[mask] = room_id;
262 return absl::FailedPreconditionError(absl::StrFormat(
263 "WaterFill entry data offset out of range: 0x%04X", data_off));
265 if (data_off < header_size) {
266 return absl::FailedPreconditionError(
267 "WaterFill entry data offset overlaps table header");
270 entries.push_back(Entry{room_id, mask, data_off});
273 std::vector<WaterFillZoneEntry> zones;
274 zones.reserve(entries.size());
275 for (
const auto& e : entries) {
277 static_cast<size_t>(e.data_off);
279 return absl::FailedPreconditionError(
280 "WaterFill entry data_pos out of range");
282 const uint8_t tile_count = data[data_pos];
283 if (tile_count > 255) {
284 return absl::FailedPreconditionError(
"WaterFill tile_count invalid");
286 const size_t needed = 1u +
static_cast<size_t>(tile_count) * 2u;
288 return absl::FailedPreconditionError(
289 "WaterFill entry tile data exceeds reserved region");
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));
306 zones.push_back(std::move(z));
309 zones = DedupAndSort(std::move(zones));
310 for (
const auto& z : zones) {
317 const std::vector<WaterFillZoneEntry>& zones) {
319 return absl::InvalidArgumentError(
"ROM not loaded");
322 const auto& rom_data = rom->
vector();
324 return absl::OutOfRangeError(
325 "WaterFill reserved region not present in this ROM");
331 ValidateCustomCollisionDoesNotOverlapWaterFillReserved(rom_data));
334 if (zones.size() > 8) {
335 return absl::InvalidArgumentError(
336 "Too many water fill zones: max 8 (fits in $7EF411 bitfield)");
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) {
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));
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));
355 seen_masks[z.sram_bit_mask] = z.room_id;
358 std::vector<uint8_t> bytes;
360 bytes.push_back(
static_cast<uint8_t
>(normalized.size()));
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);
367 bytes.push_back(0x00);
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;
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);
386 return absl::ResourceExhaustedError(
387 absl::StrFormat(
"WaterFill table too large (%zu bytes), reserved=%d",
394 std::copy(bytes.begin(), bytes.end(), region.begin());
407 Rom* rom,
const std::string& symbol_path) {
409 return absl::InvalidArgumentError(
"ROM not loaded");
412 std::string path = symbol_path;
414 if (
auto guess = GuessSymbolPathFromRom(rom->
filename());
420 return std::vector<WaterFillZoneEntry>{};
423 std::ifstream file(path);
424 if (!file.is_open()) {
425 return std::vector<WaterFillZoneEntry>{};
428 std::optional<uint32_t> room25_snes;
429 std::optional<uint32_t> room27_snes;
432 const std::regex re(R
"(^\s*([0-9A-Fa-f]{2}):([0-9A-Fa-f]{4})\s+(.+?)\s*$)");
434 while (std::getline(file, line)) {
436 if (!std::regex_match(line, m, re)) {
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();
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);
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>{};
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");
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");
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));
483 return std::optional<WaterFillZoneEntry>(std::move(z));
486 std::vector<WaterFillZoneEntry> zones;
494 zones.push_back(std::move(*z27));
496 zones.push_back(std::move(*z25));
498 zones = DedupAndSort(std::move(zones));
503 std::vector<WaterFillZoneEntry>* zones) {
504 if (zones ==
nullptr) {
505 return absl::InvalidArgumentError(
"zones is null");
507 if (zones->size() > 8) {
508 return absl::InvalidArgumentError(absl::StrFormat(
509 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
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));
521 *zones = DedupAndSort(std::move(*zones));
523 std::unordered_map<int, bool> seen_rooms;
524 uint8_t used_masks = 0;
525 std::vector<WaterFillZoneEntry*> unassigned;
526 unassigned.reserve(zones->size());
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));
533 seen_rooms[z.room_id] =
true;
535 const uint8_t mask = z.sram_bit_mask;
537 (mask != 0) && IsSingleBitMask(mask) && ((used_masks & mask) == 0);
542 unassigned.push_back(&z);
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) {
556 return absl::ResourceExhaustedError(
557 "No free SRAM bits left in $7EF411 for water fill zones");
559 z->sram_bit_mask = assigned;
560 used_masks |= assigned;
563 return absl::OkStatus();
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");
572 using json = nlohmann::json;
574 std::vector<WaterFillZoneEntry> sorted = zones;
575 std::sort(sorted.begin(), sorted.end(),
577 return a.room_id < b.room_id;
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));
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());
592 json arr = json::array();
593 for (
const auto& z : sorted) {
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));
600 root[
"zones"] = std::move(arr);
607#if !defined(YAZE_WITH_JSON)
608 return absl::UnimplementedError(
609 "JSON support not enabled. Build with -DYAZE_WITH_JSON=ON");
611 using json = nlohmann::json;
615 root = json::parse(json_content);
616 }
catch (
const json::parse_error& e) {
617 return absl::InvalidArgumentError(std::string(
"JSON parse error: ") +
621 const int version = root.value(
"version", 1);
623 return absl::InvalidArgumentError(
624 absl::StrFormat(
"Unsupported water fill JSON version: %d", version));
626 if (!root.contains(
"zones") || !root[
"zones"].is_array()) {
627 return absl::InvalidArgumentError(
"Missing or invalid 'zones' array");
630 auto parse_int = [](
const json& v) -> std::optional<int> {
631 if (v.is_number_integer()) {
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())) {
639 return static_cast<int>(u);
644 const std::string s = v.get<std::string>();
645 const int parsed = std::stoi(s, &idx, 0);
646 if (idx == s.size()) {
654 std::vector<WaterFillZoneEntry> zones;
655 zones.reserve(root[
"zones"].size());
657 std::unordered_map<int, bool> seen_rooms;
658 for (
const auto& item : root[
"zones"]) {
659 if (!item.is_object()) {
663 const json& room_v = item.contains(
"room_id") ? item[
"room_id"]
664 : item.contains(
"room") ? item[
"room"]
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)");
671 const int room_id = *room_id_opt;
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));
677 seen_rooms[room_id] =
true;
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"]
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));
691 mask =
static_cast<uint8_t
>(*m_opt);
695 if (mask != 0 && !IsSingleBitMask(mask)) {
696 return absl::InvalidArgumentError(absl::StrFormat(
697 "Invalid mask 0x%02X for room 0x%02X", mask, room_id));
700 const json& offsets_v = item.contains(
"offsets") ? item[
"offsets"]
701 : item.contains(
"fill_offsets")
702 ? item[
"fill_offsets"]
704 if (!offsets_v.is_array()) {
705 return absl::InvalidArgumentError(
706 absl::StrFormat(
"Invalid offsets array for room 0x%02X", room_id));
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));
719 z.
fill_offsets.push_back(
static_cast<uint16_t
>(*o_opt));
721 return absl::InvalidArgumentError(absl::StrFormat(
722 "WaterFill offsets exceed 255 tiles for room 0x%02X", room_id));
731 zones.push_back(std::move(z));
734 if (zones.size() > 8) {
735 return absl::InvalidArgumentError(absl::StrFormat(
736 "Too many water fill zones in JSON: %zu (max 8)", zones.size()));
739 zones = DedupAndSort(std::move(zones));
std::vector< uint16_t > fill_offsets