13#include "absl/status/status.h"
14#include "absl/status/statusor.h"
15#include "absl/strings/str_cat.h"
16#include "absl/strings/str_format.h"
49 return range.
begin <= address && address < range.
end;
53 const std::vector<DungeonStreamPcRange>& input, uint32_t rom_size,
54 const char* label,
bool allow_empty) {
55 if (input.empty() && !allow_empty) {
56 return absl::InvalidArgumentError(
57 absl::StrCat(label,
" must not be empty"));
60 std::vector<DungeonStreamPcRange> ranges = input;
61 std::sort(ranges.begin(), ranges.end(), [](
const auto& a,
const auto& b) {
62 return std::tie(a.begin, a.end) < std::tie(b.begin, b.end);
64 std::vector<DungeonStreamPcRange> normalized;
65 for (
const auto& range : ranges) {
66 if (range.begin >= range.end) {
67 return absl::InvalidArgumentError(
68 absl::StrFormat(
"%s contains empty/reversed range [0x%06X,0x%06X)",
69 label, range.begin, range.end));
71 if (range.end > rom_size) {
72 return absl::OutOfRangeError(
73 absl::StrFormat(
"%s range [0x%06X,0x%06X) exceeds ROM size 0x%06X",
74 label, range.begin, range.end, rom_size));
77 return absl::InvalidArgumentError(absl::StrFormat(
78 "%s range [0x%06X,0x%06X) maps through SNES WRAM banks $7E/$7F",
79 label, range.begin, range.end));
81 if (!normalized.empty() && range.begin < normalized.back().end) {
82 return absl::InvalidArgumentError(absl::StrFormat(
83 "%s ranges overlap at PC 0x%06X", label, range.begin));
85 if (!normalized.empty() && range.begin == normalized.back().end) {
86 normalized.back().end = range.end;
88 normalized.push_back(range);
95 const std::vector<DungeonStreamPcRange>& data_ranges,
96 const std::vector<DungeonStreamPcRange>& allocation_ranges) {
97 for (
const auto& allocation : allocation_ranges) {
98 const bool contained = std::any_of(
99 data_ranges.begin(), data_ranges.end(),
100 [&](
const auto& data) { return Contains(data, allocation); });
102 return absl::InvalidArgumentError(absl::StrFormat(
103 "Allocation range [0x%06X,0x%06X) is not contained in one data "
105 allocation.begin, allocation.end));
108 return absl::OkStatus();
112 auto result = rom.
ReadWord(
static_cast<int>(address));
114 return result.status();
116 return static_cast<uint32_t
>(*result);
120 auto result = rom.
ReadLong(
static_cast<int>(address));
122 return result.status();
129 switch (layout.
kind) {
132 return absl::InvalidArgumentError(
133 "Object streams require 24-bit long pointers");
139 if ((*raw & 0xFFFFu) < 0x8000u ||
141 return absl::FailedPreconditionError(absl::StrFormat(
142 "Object pointer-table source at PC 0x%06X does not resolve to "
143 "configured table PC 0x%06X",
146 return absl::OkStatus();
151 return absl::InvalidArgumentError(
152 "Sprite streams require fixed-bank-16 pointers in bank 0x09");
158 if (*raw < 0x8000u ||
161 return absl::FailedPreconditionError(absl::StrFormat(
162 "Sprite pointer-table source at PC 0x%06X does not resolve to "
163 "configured table PC 0x%06X",
166 return absl::OkStatus();
171 return absl::InvalidArgumentError(
172 "Pot-item streams require fixed-bank-16 pointers in bank 0x01");
175 return absl::FailedPreconditionError(absl::StrFormat(
176 "Pot-item pointer table must be the fixed PC table at 0x%06X",
179 return absl::OkStatus();
181 return absl::InvalidArgumentError(
"Unknown dungeon stream kind");
200 return absl::InvalidArgumentError(
"ROM not loaded");
202 if (rom.
size() > std::numeric_limits<uint32_t>::max()) {
203 return absl::OutOfRangeError(
"ROM is too large for PC-native layout");
206 return absl::InvalidArgumentError(
207 absl::StrFormat(
"Pointer count %u is outside supported range 1..%d",
212 const uint32_t rom_size =
static_cast<uint32_t
>(rom.
size());
215 if (!data_ranges.ok()) {
216 return data_ranges.status();
219 "allocation_ranges",
true);
220 if (!allocation_ranges.ok()) {
221 return allocation_ranges.status();
226 auto allocation_status =
228 if (!allocation_status.ok()) {
229 return allocation_status;
233 const uint64_t table_end =
236 if (table_end > rom_size) {
237 return absl::OutOfRangeError(
238 "Dungeon stream pointer table is out of range");
243 pointer_table_range.end)) {
244 return absl::InvalidArgumentError(
245 "Dungeon stream pointer table maps through SNES WRAM banks $7E/$7F");
248 const uint32_t pointer_table_word =
250 const uint64_t runtime_table_end =
251 static_cast<uint64_t
>(pointer_table_word) +
253 if (runtime_table_end > 0x10000u) {
254 return absl::InvalidArgumentError(
255 "Fixed-bank-16 pointer table crosses its runtime CPU bank");
261 if (pointer_source_range.
begin < pointer_source_range.
end) {
262 if (pointer_source_range.
end > rom_size) {
263 return absl::OutOfRangeError(
264 "Live pointer-table source is outside the source ROM");
266 if (Intersects(pointer_table_range, pointer_source_range)) {
267 return absl::InvalidArgumentError(
268 "Dungeon stream pointer table must not overlap the live "
269 "pointer-table source");
275 const uint64_t door_pointer_end =
278 if (door_pointer_end > rom_size) {
279 return absl::OutOfRangeError(
280 "Complete object door-pointer table is outside the source ROM");
283 static_cast<uint32_t
>(door_pointer_end)};
284 if (Intersects(pointer_table_range, door_pointer_range)) {
285 return absl::InvalidArgumentError(
286 "Dungeon stream pointer table must not overlap the object "
287 "door-pointer table");
292 if (Intersects(pointer_table_range, data)) {
293 return absl::InvalidArgumentError(
294 "Dungeon stream data ranges must not overlap the pointer table");
296 if (pointer_source_range.
begin < pointer_source_range.
end &&
297 Intersects(pointer_source_range, data)) {
298 return absl::InvalidArgumentError(
299 "Dungeon stream data ranges must not overlap the live "
300 "pointer-table source");
302 if (door_pointer_range.
begin < door_pointer_range.
end &&
303 Intersects(door_pointer_range, data)) {
304 return absl::InvalidArgumentError(
305 "Dungeon stream data ranges must not overlap the object "
306 "door-pointer table");
311 if (!source_status.ok()) {
312 return source_status;
334 snes = (
static_cast<uint32_t
>(layout.
pointer_bank) << 16) | *raw;
336 if ((snes & 0xFFFFu) < 0x8000u) {
337 return absl::FailedPreconditionError(absl::StrFormat(
338 "Pointer word 0x%04X is not a LoROM address", snes & 0xFFFFu));
340 const uint8_t bank =
static_cast<uint8_t
>((snes >> 16) & 0xFFu);
341 if (bank == 0x7E || bank == 0x7F || bank == 0xFE || bank == 0xFF) {
342 return absl::FailedPreconditionError(
343 absl::StrFormat(
"Pointer uses SNES WRAM bank or mirror 0x%02X", bank));
346 if (pc >= rom.
size()) {
347 return absl::OutOfRangeError(
348 absl::StrFormat(
"Pointer resolves outside ROM at PC 0x%06X", pc));
354 const std::vector<DungeonStreamPcRange>& ranges, uint32_t address) {
356 std::upper_bound(ranges.begin(), ranges.end(), address,
358 return value < range.begin;
360 if (it == ranges.begin()) {
368 uint32_t start, uint32_t limit,
369 uint32_t* door_list_pc =
nullptr) {
370 if (start + 2 > limit) {
371 return absl::DataLossError(
"Object stream is missing its two-byte header");
373 uint32_t cursor = start + 2;
374 for (
int list = 0; list < 2; ++list) {
376 if (cursor + 2 > limit) {
377 return absl::DataLossError(
378 absl::StrFormat(
"Object list %d has no 0xFFFF terminator", list));
380 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
384 if (cursor + 3 > limit) {
385 return absl::DataLossError(
386 absl::StrFormat(
"Object list %d has a truncated record", list));
392 bool in_doors =
false;
394 if (cursor + 2 > limit) {
395 return absl::DataLossError(in_doors ?
"Object door list has no terminator"
396 :
"Object list 2 has no door marker");
399 if (bytes[cursor] == 0xF0 && bytes[cursor + 1] == 0xFF) {
401 if (door_list_pc !=
nullptr) {
402 *door_list_pc = cursor;
407 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
411 if (door_list_pc !=
nullptr) {
412 *door_list_pc = cursor;
416 if (cursor + 3 > limit) {
417 return absl::DataLossError(
"Object list 2 has a truncated record");
423 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
431 uint32_t start, uint32_t limit) {
432 if (start >= limit) {
433 return absl::DataLossError(
"Sprite stream is missing its sort byte");
435 uint32_t cursor = start + 1;
436 while (cursor < limit) {
437 if (bytes[cursor] == 0xFF) {
440 if (cursor + 3 > limit) {
441 return absl::DataLossError(
"Sprite stream has a truncated record");
445 return absl::DataLossError(
"Sprite stream has no 0xFF terminator");
449 uint32_t start, uint32_t limit) {
450 uint32_t cursor = start;
452 if (cursor + 2 > limit) {
453 return absl::DataLossError(
"Pot-item stream has no 0xFFFF terminator");
455 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
458 if (cursor + 3 > limit) {
459 return absl::DataLossError(
"Pot-item stream has a truncated record");
465absl::StatusOr<uint32_t>
ParseStream(std::span<const uint8_t> bytes,
468 if (limit > bytes.size() || start >= limit) {
469 return absl::OutOfRangeError(
"Dungeon stream parse bounds are invalid");
479 return absl::InvalidArgumentError(
"Unknown dungeon stream kind");
485 return absl::OutOfRangeError(
486 "Dungeon stream room is outside the room table");
489 return absl::InvalidArgumentError(
"ROM not loaded");
491 if (rom.
size() > std::numeric_limits<uint32_t>::max()) {
492 return absl::OutOfRangeError(
"ROM is too large for PC-native stream reads");
506 const uint32_t table_bank_end =
508 if (table_end > rom.
size() || table_end > table_bank_end) {
509 return absl::OutOfRangeError(
510 "Dungeon stream pointer table is truncated or crosses its bank");
514 if (Intersects(pointer_table, pointer_source)) {
515 return absl::FailedPreconditionError(
516 "Dungeon stream pointer table overlaps its live pointer source");
520 std::min<uint32_t>(
static_cast<uint32_t
>(rom.
size()),
530 for (
const auto& metadata : {pointer_table, pointer_source}) {
532 return absl::FailedPreconditionError(
533 "Dungeon stream pointer resolves inside pointer metadata");
535 if (metadata.begin > start) {
536 limit = std::min(limit, metadata.begin);
539 const uint32_t storage_limit = limit;
542 if (address.ok() && *address > start) {
543 limit = std::min(limit, *address);
546 const std::span<const uint8_t> bytes(rom.
data(), rom.
size());
552 limit + 2 <= storage_limit && (limit - start) % 3 == 0 &&
553 bytes[limit] == 0xFF && bytes[limit + 1] == 0xFF) {
563 record.
encoded_stream.assign(bytes.begin() + start, bytes.begin() + end);
569 if (encoded.empty()) {
570 return absl::InvalidArgumentError(
"Replacement stream must not be empty");
573 ParseStream(encoded, kind, 0,
static_cast<uint32_t
>(encoded.size()));
574 if (!logical_end.ok()) {
575 return logical_end.status();
577 if (*logical_end != encoded.size()) {
578 return absl::InvalidArgumentError(
579 "Replacement stream contains bytes after its logical terminator");
581 return absl::OkStatus();
585 const std::vector<uint8_t>& encoded) {
586 uint32_t door_list_pc = std::numeric_limits<uint32_t>::max();
588 encoded, 0,
static_cast<uint32_t
>(encoded.size()), &door_list_pc);
589 if (!logical_end.ok()) {
590 return logical_end.status();
592 if (*logical_end != encoded.size() || door_list_pc >= encoded.size()) {
593 return absl::DataLossError(
594 "Object stream does not have a valid door-list target");
600 std::vector<DungeonStreamPcRange> intervals) {
601 std::sort(intervals.begin(), intervals.end(),
602 [](
const auto& a,
const auto& b) {
603 return std::tie(a.begin, a.end) < std::tie(b.begin, b.end);
605 std::vector<DungeonStreamPcRange> result;
606 for (
const auto& interval : intervals) {
607 if (result.empty() || interval.begin > result.back().end) {
608 result.push_back(interval);
610 result.back().end = std::max(result.back().end, interval.end);
617 const std::vector<DungeonStreamPcRange>& containers,
618 const std::vector<DungeonStreamPcRange>& occupied) {
619 std::vector<DungeonStreamPcRange> free;
620 for (
const auto& container : containers) {
621 uint32_t cursor = container.begin;
622 for (
const auto& used : occupied) {
623 if (used.end <= cursor) {
626 if (used.begin >= container.end) {
629 if (used.begin > cursor) {
630 free.push_back({cursor, std::min(used.begin, container.end)});
632 cursor = std::max(cursor, std::min(used.end, container.end));
633 if (cursor >= container.end) {
637 if (cursor < container.end) {
638 free.push_back({cursor, container.end});
647 return absl::OutOfRangeError(absl::StrFormat(
648 "PC address 0x%06X maps through SNES WRAM banks $7E/$7F", pc));
651 if (
SnesToPc(snes) != pc || (snes & 0xFFFFu) < 0x8000u) {
652 return absl::OutOfRangeError(absl::StrFormat(
653 "PC address 0x%06X is not representable as a LoROM pointer", pc));
656 return std::vector<uint8_t>{
static_cast<uint8_t
>(snes & 0xFF),
657 static_cast<uint8_t
>((snes >> 8) & 0xFF),
658 static_cast<uint8_t
>((snes >> 16) & 0xFF)};
660 const uint32_t fixed_snes =
661 (
static_cast<uint32_t
>(layout.
pointer_bank) << 16) | (snes & 0xFFFFu);
663 return absl::OutOfRangeError(absl::StrFormat(
664 "PC address 0x%06X is outside fixed pointer bank 0x%02X", pc,
667 return std::vector<uint8_t>{
static_cast<uint8_t
>(snes & 0xFF),
668 static_cast<uint8_t
>((snes >> 8) & 0xFF)};
672 const std::vector<DungeonStreamPcRange>& free_intervals,
675 return absl::ResourceExhaustedError(
676 "Replacement cannot fit in one LoROM bank");
680 const uint32_t bank_begin =
683 for (
const auto& interval : free_intervals) {
684 const uint32_t begin = std::max(interval.begin, bank_begin);
685 const uint32_t end = std::min(interval.end, bank_end);
686 if (begin <= end &&
static_cast<uint64_t
>(begin) + size <= end) {
690 return absl::ResourceExhaustedError(
691 absl::StrFormat(
"No %u-byte allocation fits fixed pointer bank 0x%02X",
695 for (
const auto& interval : free_intervals) {
696 uint32_t candidate = interval.begin;
697 while (candidate < interval.end) {
698 const uint32_t bank_end =
700 const uint32_t usable_end = std::min(interval.end, bank_end);
701 if (
static_cast<uint64_t
>(candidate) + size <= usable_end &&
705 if (bank_end <= candidate) {
708 candidate = bank_end;
711 return absl::ResourceExhaustedError(
712 absl::StrFormat(
"No declared free interval fits %u bytes", size));
717 std::vector<DungeonStreamPcRange> next;
718 next.reserve(free_intervals->size() + 1);
719 for (
const auto& interval : *free_intervals) {
720 if (!Intersects(interval, consumed)) {
721 next.push_back(interval);
724 if (interval.begin < consumed.
begin) {
725 next.push_back({interval.begin, consumed.
begin});
727 if (consumed.
end < interval.end) {
728 next.push_back({consumed.
end, interval.end});
731 *free_intervals = std::move(next);
735 for (
size_t i = 1; i < writes.size(); ++i) {
736 if (writes[i - 1].room_id >= writes[i].room_id) {
746 if (!inventory.
ok()) {
747 return absl::FailedPreconditionError(
748 "Source inventory contains invalid pointers or malformed streams");
752 return absl::AbortedError(
753 "Write plan does not match the current inventory snapshot");
756 return absl::InvalidArgumentError(
757 "Write plan must contain at least one payload write");
763 return absl::InvalidArgumentError(
764 "Write plan must be strictly sorted by room ID");
770 return absl::InvalidArgumentError(
771 "Copy-on-write plan must contain matching payload and pointer "
774 const size_t expected_auxiliary_count =
779 return absl::InvalidArgumentError(
780 "Copy-on-write plan has the wrong number of auxiliary pointer "
789 return absl::InvalidArgumentError(
790 "Repack-all plans currently support only fixed-bank pot-item "
794 return absl::InvalidArgumentError(absl::StrFormat(
798 return absl::InvalidArgumentError(
799 "Pot-item repack requires exactly one normalized allocation "
803 return absl::InvalidArgumentError(
804 "Repack-all plan must update every pointer-table entry");
807 return absl::InvalidArgumentError(
808 "Pot-item repack plan must not contain auxiliary pointers");
813 std::vector<DungeonStreamPcRange> planned_payloads;
815 std::map<std::vector<uint8_t>,
size_t> payload_by_pointer;
816 std::map<std::vector<uint8_t>, uint32_t> address_by_payload;
821 return absl::InvalidArgumentError(
"Write plan room mapping is invalid");
823 auto encoded_status =
825 if (!encoded_status.ok()) {
826 return encoded_status;
828 const uint64_t payload_end64 =
829 static_cast<uint64_t
>(payload.address) + payload.bytes.size();
830 if (payload_end64 > std::numeric_limits<uint32_t>::max()) {
831 return absl::OutOfRangeError(
"Payload write address overflows PC space");
834 payload.address,
static_cast<uint32_t
>(payload_end64)};
835 const uint32_t payload_bank_end =
837 if (payload_range.end > payload_bank_end) {
838 return absl::FailedPreconditionError(
839 "Planned payload crosses a LoROM bank boundary");
844 const bool in_allowed_range = std::any_of(
845 allowed_ranges.begin(), allowed_ranges.end(),
846 [&](
const auto& range) { return Contains(range, payload_range); });
847 if (!in_allowed_range) {
848 return absl::FailedPreconditionError(absl::StrFormat(
849 "Payload for room %u is outside declared allocator-owned %s",
854 for (
const auto& prior : planned_payloads) {
855 if (Intersects(prior, payload_range)) {
856 return absl::InvalidArgumentError(
"Planned payload writes overlap");
859 planned_payloads.push_back(payload_range);
862 if (!expected_pointer.ok()) {
863 return expected_pointer.status();
865 if (!payload_by_pointer.emplace(*expected_pointer, i).second) {
866 return absl::InvalidArgumentError(
867 "Multiple payload writes encode the same pointer target");
870 !address_by_payload.emplace(payload.bytes, payload.address).second) {
871 return absl::InvalidArgumentError(
872 "Write plan contains duplicate logical payloads");
877 const uint32_t expected_slot =
879 if (pointer.room_id != payload.room_id ||
880 pointer.address != expected_slot ||
881 pointer.bytes != *expected_pointer) {
882 return absl::InvalidArgumentError(
883 "Pointer write does not match its planned payload");
890 if (!door_offset.ok()) {
891 return door_offset.status();
893 auto expected_door_pointer =
895 if (!expected_door_pointer.ok()) {
896 return expected_door_pointer.status();
898 const uint32_t expected_door_slot =
kDoorPointers + payload.room_id * 3;
899 if (auxiliary.room_id != payload.room_id ||
900 auxiliary.address != expected_door_slot ||
901 auxiliary.bytes != *expected_door_pointer) {
902 return absl::InvalidArgumentError(
903 "Object door pointer does not match its planned payload");
905 if (
static_cast<uint64_t
>(auxiliary.address) + auxiliary.bytes.size() >
907 return absl::OutOfRangeError(
908 "Object door-pointer write is outside the source ROM");
914 std::vector<uint32_t> lowest_room_by_payload(
915 plan.
payload_writes.size(), std::numeric_limits<uint32_t>::max());
916 std::vector<bool> referenced_payloads(plan.
payload_writes.size(),
false);
919 const uint32_t expected_slot =
921 if (pointer.room_id != room_id || pointer.address != expected_slot) {
922 return absl::InvalidArgumentError(
923 "Repack-all pointer writes must cover every room in order");
925 const auto payload_it = payload_by_pointer.find(pointer.bytes);
926 if (payload_it == payload_by_pointer.end()) {
927 return absl::InvalidArgumentError(
928 "Repack-all pointer does not target a planned payload");
930 const size_t payload_index = payload_it->second;
931 referenced_payloads[payload_index] =
true;
932 lowest_room_by_payload[payload_index] =
933 std::min(lowest_room_by_payload[payload_index], room_id);
936 if (!referenced_payloads[i] ||
938 return absl::InvalidArgumentError(
939 "Repack-all payload owner must be its lowest referencing room");
943 return absl::OkStatus();
951 return absl::OutOfRangeError(
952 "Object stream room is outside the room table");
960 if (table_end > rom.
size()) {
961 return absl::OutOfRangeError(
"Object stream pointer table is truncated");
964 static_cast<uint32_t
>(table_end))) {
965 return absl::FailedPreconditionError(
966 "Object stream pointer table crosses SNES WRAM banks");
968 ASSIGN_OR_RETURN(
const uint32_t start, DecodePointer(rom, layout, room_id));
970 static_cast<uint32_t
>(table_end)};
976 std::min<uint32_t>(
static_cast<uint32_t
>(rom.
size()),
977 ((start / kLoRomBankSize) + 1) * kLoRomBankSize);
979 if (region_end >= 0) {
980 limit = std::min(limit,
static_cast<uint32_t
>(region_end));
982 for (
const auto& table : {pointer_table, pointer_source, door_table}) {
983 if (ContainsAddress(table, start)) {
984 return absl::FailedPreconditionError(
985 "Object stream pointer resolves inside a pointer table");
987 if (table.begin > start) {
988 limit = std::min(limit, table.begin);
992 const auto address = DecodePointer(rom, layout, other);
993 if (address.ok() && *address > start) {
994 limit = std::min(limit, *address);
997 const std::span<const uint8_t> bytes(rom.
data(), rom.
size());
998 ASSIGN_OR_RETURN(
const uint32_t end, ParseObjectStream(bytes, start, limit));
1004 record.
valid =
true;
1005 record.
encoded_stream.assign(bytes.begin() + start, bytes.begin() + end);
1021 auto normalized_layout = ValidateAndNormalizeLayout(rom, requested_layout);
1022 if (!normalized_layout.ok()) {
1023 return normalized_layout.status();
1027 inventory.
layout = std::move(*normalized_layout);
1033 const uint32_t pointer_width =
1035 std::map<uint32_t, std::vector<size_t>> valid_by_start;
1036 std::vector<DungeonStreamPcRange> occupied;
1037 const std::span<const uint8_t> bytes(rom.
data(), rom.
size());
1045 auto pointer = DecodePointer(rom, inventory.
layout, room_id);
1046 if (!pointer.ok()) {
1049 absl::StrCat(
"Room pointer is invalid: ",
1050 pointer.status().message())});
1051 inventory.
streams.push_back(record);
1055 const auto* data_range =
1057 if (data_range ==
nullptr) {
1058 inventory.
issues.push_back(
1062 "Room %u pointer PC 0x%06X is outside declared data ranges",
1064 inventory.
streams.push_back(record);
1068 const uint32_t bank_end =
1069 ((record.
data_pc / kLoRomBankSize) + 1) * kLoRomBankSize;
1070 const uint32_t parse_limit = std::min(data_range->end, bank_end);
1073 if (!logical_end.ok()) {
1074 inventory.
issues.push_back(
1076 absl::StrCat(
"Room stream is malformed: ",
1077 logical_end.status().message())});
1078 inventory.
streams.push_back(record);
1083 record.
valid =
true;
1086 inventory.
streams.push_back(record);
1087 const size_t index = inventory.
streams.size() - 1;
1088 valid_by_start[record.
data_pc].push_back(index);
1092 struct UniqueStream {
1095 std::vector<uint32_t> room_ids;
1097 std::vector<UniqueStream> unique;
1098 unique.reserve(valid_by_start.size());
1099 for (
const auto& [start, indices] : valid_by_start) {
1100 UniqueStream stream;
1101 stream.begin = start;
1102 stream.end = inventory.
streams[indices.front()].logical_end_pc;
1103 for (
size_t index : indices) {
1104 stream.room_ids.push_back(inventory.
streams[index].room_id);
1106 if (stream.room_ids.size() > 1) {
1107 inventory.
aliases.push_back({start, stream.room_ids});
1109 unique.push_back(std::move(stream));
1112 for (
size_t i = 0; i < unique.size(); ++i) {
1113 for (
size_t j = i + 1; j < unique.size(); ++j) {
1114 if (unique[j].begin >= unique[i].end) {
1117 const uint32_t intersection_end = std::min(unique[i].end, unique[j].end);
1118 if (unique[j].begin >= intersection_end) {
1121 const auto kind = unique[j].end == unique[i].end
1124 inventory.
overlaps.push_back({kind,
1127 {unique[j].begin, intersection_end}});
1141 const std::vector<DungeonStreamReplacement>& requested_replacements) {
1142 if (!inventory.
ok()) {
1143 return absl::FailedPreconditionError(
1144 "Cannot plan writes from an inventory with stream issues");
1146 if (requested_replacements.empty()) {
1147 return absl::InvalidArgumentError(
"No dungeon stream replacements given");
1150 std::vector<DungeonStreamReplacement> replacements = requested_replacements;
1151 std::sort(replacements.begin(), replacements.end(),
1152 [](
const auto& a,
const auto& b) { return a.room_id < b.room_id; });
1153 for (
size_t i = 0; i < replacements.size(); ++i) {
1155 return absl::OutOfRangeError(
"Replacement room ID is out of range");
1157 if (i > 0 && replacements[i - 1].room_id == replacements[i].room_id) {
1158 return absl::InvalidArgumentError(
"Replacement room IDs must be unique");
1160 auto status = ValidateCompleteEncodedStream(inventory.
layout.
kind,
1161 replacements[i].encoded_stream);
1163 return absl::InvalidArgumentError(
1164 absl::StrFormat(
"Room %u replacement is invalid: %s",
1165 replacements[i].room_id, status.message()));
1178 return absl::OutOfRangeError(
1179 "Object door-pointer table is outside the source ROM");
1183 for (
const auto& replacement : replacements) {
1184 const uint32_t size =
1185 static_cast<uint32_t
>(replacement.encoded_stream.size());
1186 auto address = FindFirstFit(free, plan.
layout, size);
1187 if (!address.ok()) {
1188 return address.status();
1190 auto pointer_bytes = EncodePointer(plan.
layout, *address);
1191 if (!pointer_bytes.ok()) {
1192 return pointer_bytes.status();
1196 {replacement.room_id, *address, replacement.encoded_stream});
1198 {replacement.room_id,
1200 std::move(*pointer_bytes)});
1202 auto door_offset = ObjectDoorListOffset(replacement.encoded_stream);
1203 if (!door_offset.ok()) {
1204 return door_offset.status();
1206 auto door_pointer = EncodePointer(plan.
layout, *address + *door_offset);
1207 if (!door_pointer.ok()) {
1208 return door_pointer.status();
1211 {replacement.room_id,
kDoorPointers + replacement.room_id * 3,
1212 std::move(*door_pointer)});
1214 ConsumeInterval(&free, {*address, *address + size});
1221 const std::vector<DungeonStreamReplacement>& requested_replacements) {
1222 if (!inventory.
ok()) {
1223 return absl::FailedPreconditionError(
1224 "Cannot repack streams from an inventory with stream issues");
1229 return absl::InvalidArgumentError(
1230 "Repack-all currently supports only fixed-bank pot-item streams");
1233 return absl::InvalidArgumentError(absl::StrFormat(
1234 "Pot-item repack requires exactly %d pointers",
kNumberOfRooms));
1237 return absl::InvalidArgumentError(
1238 "Pot-item repack requires exactly one normalized allocation range");
1241 return absl::FailedPreconditionError(
1242 "Pot-item inventory does not cover every pointer-table entry");
1245 std::vector<std::vector<uint8_t>> logical_streams(
1250 seen_rooms[record.room_id] || record.encoded_stream.empty() ||
1251 record.encoded_stream.size() != record.size()) {
1252 return absl::FailedPreconditionError(
1253 "Pot-item inventory contains an incomplete logical stream snapshot");
1255 seen_rooms[record.room_id] =
true;
1256 logical_streams[record.room_id] = record.encoded_stream;
1258 if (std::find(seen_rooms.begin(), seen_rooms.end(),
false) !=
1260 return absl::FailedPreconditionError(
1261 "Pot-item inventory is missing a room stream snapshot");
1264 std::vector<DungeonStreamReplacement> replacements = requested_replacements;
1265 std::sort(replacements.begin(), replacements.end(),
1266 [](
const auto& a,
const auto& b) { return a.room_id < b.room_id; });
1267 for (
size_t i = 0; i < replacements.size(); ++i) {
1270 return absl::OutOfRangeError(
"Replacement room ID is out of range");
1272 if (i > 0 && replacements[i - 1].room_id == replacement.
room_id) {
1273 return absl::InvalidArgumentError(
"Replacement room IDs must be unique");
1275 const absl::Status status = ValidateCompleteEncodedStream(
1278 return absl::InvalidArgumentError(
1279 absl::StrFormat(
"Room %u replacement is invalid: %s",
1280 replacement.
room_id, status.message()));
1285 const uint32_t bank_begin =
SnesToPc(
1287 const uint32_t bank_end = bank_begin + kLoRomBankSize;
1288 uint64_t available_bytes = 0;
1290 if (range.begin < bank_begin || range.end > bank_end) {
1291 return absl::InvalidArgumentError(absl::StrFormat(
1292 "Pot-item repack range [0x%06X,0x%06X) crosses or lies outside "
1293 "fixed pointer bank 0x%02X",
1296 available_bytes +=
static_cast<uint64_t
>(range.end) - range.begin;
1299 struct RepackGroup {
1300 uint32_t owner_room = 0;
1301 std::vector<uint8_t> bytes;
1302 std::vector<uint32_t> room_ids;
1304 std::map<std::vector<uint8_t>, std::vector<uint32_t>> rooms_by_payload;
1307 rooms_by_payload[logical_streams[room_id]].push_back(room_id);
1309 std::vector<RepackGroup> groups;
1310 groups.reserve(rooms_by_payload.size());
1311 uint64_t required_bytes = 0;
1312 for (
auto& [bytes, room_ids] : rooms_by_payload) {
1313 required_bytes += bytes.size();
1314 groups.push_back({room_ids.front(), std::move(bytes), std::move(room_ids)});
1316 if (required_bytes > available_bytes) {
1317 return absl::ResourceExhaustedError(
1318 absl::StrCat(
"Pot-item repack needs ", required_bytes,
1319 " bytes but declared ranges provide ", available_bytes));
1321 std::sort(groups.begin(), groups.end(), [](
const auto& a,
const auto& b) {
1322 return a.owner_room < b.owner_room;
1335 for (
const RepackGroup& group : groups) {
1336 const uint32_t address = next_address;
1337 plan.
payload_writes.push_back({group.owner_room, address, group.bytes});
1338 for (
const uint32_t room_id : group.room_ids) {
1339 target_by_room[room_id] = address;
1341 next_address +=
static_cast<uint32_t
>(group.bytes.size());
1344 const uint32_t pointer_width =
1349 EncodePointer(inventory.
layout, target_by_room[room_id]));
1352 std::move(pointer)});
1359 if (rom ==
nullptr || !rom->
is_loaded()) {
1360 return absl::InvalidArgumentError(
"ROM not loaded");
1363 return absl::AbortedError(
"Dungeon stream plan source size is stale");
1365 const uint32_t current_crc =
1368 return absl::AbortedError(absl::StrFormat(
1369 "Dungeon stream plan is stale (source CRC %08X, current CRC %08X)",
1374 if (!inventory.ok()) {
1375 return inventory.status();
1377 auto validation = ValidatePlanAgainstInventory(plan, *inventory);
1378 if (!validation.ok()) {
1383 auto allow_writes = [&](
const std::vector<DungeonStreamWrite>& writes,
1384 const char* label) -> absl::Status {
1385 for (
const auto& write : writes) {
1386 const uint64_t end =
1387 static_cast<uint64_t
>(write.address) + write.bytes.size();
1388 if (end > std::numeric_limits<uint32_t>::max()) {
1389 return absl::OutOfRangeError(
"Dungeon stream write range overflows");
1392 write_fence.
Allow(write.address,
static_cast<uint32_t
>(end), label));
1394 return absl::OkStatus();
1399 "DungeonStreamAuxiliaryPointer"));
1402 const std::vector<uint8_t> snapshot = rom->
vector();
1403 const bool was_dirty = rom->
dirty();
1404 auto rollback = [&]() {
1411 rom->
WriteVector(
static_cast<int>(write.address), write.bytes);
1419 rom->
WriteVector(
static_cast<int>(write.address), write.bytes);
1427 rom->
WriteVector(
static_cast<int>(write.address), write.bytes);
1433 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
void set_dirty(bool dirty)
const auto & vector() const
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
absl::StatusOr< uint16_t > ReadWord(int offset) const
absl::StatusOr< uint32_t > ReadLong(int offset) const
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
uint32_t CalculateCrc32(const uint8_t *data, size_t size)
absl::Status ValidateAllocationSubset(const std::vector< DungeonStreamPcRange > &data_ranges, const std::vector< DungeonStreamPcRange > &allocation_ranges)
std::vector< DungeonStreamPcRange > ComplementIntervals(const std::vector< DungeonStreamPcRange > &containers, const std::vector< DungeonStreamPcRange > &occupied)
bool IntersectsWramMappedPc(uint32_t begin, uint32_t end)
bool Contains(const DungeonStreamPcRange &outer, const DungeonStreamPcRange &inner)
bool ContainsAddress(const DungeonStreamPcRange &range, uint32_t address)
absl::StatusOr< DungeonStreamLayout > ValidateAndNormalizeLayout(const Rom &rom, const DungeonStreamLayout &input)
absl::StatusOr< uint32_t > ObjectDoorListOffset(const std::vector< uint8_t > &encoded)
const DungeonStreamPcRange * FindContainingRange(const std::vector< DungeonStreamPcRange > &ranges, uint32_t address)
absl::StatusOr< DungeonStreamRecord > ReadFixedBankRoomStream(const Rom &rom, int room_id, DungeonStreamKind kind)
absl::StatusOr< std::vector< uint8_t > > EncodePointer(const DungeonStreamLayout &layout, uint32_t pc)
constexpr uint32_t kWramMappedPcEnd
uint32_t PointerWidth(DungeonPointerEncoding encoding)
absl::StatusOr< uint32_t > FindFirstFit(const std::vector< DungeonStreamPcRange > &free_intervals, const DungeonStreamLayout &layout, uint32_t size)
DungeonStreamPcRange KnownPointerSourceRange(DungeonStreamKind kind)
absl::StatusOr< uint32_t > ReadLongAt(const Rom &rom, uint32_t address)
std::vector< DungeonStreamPcRange > UnionIntervals(std::vector< DungeonStreamPcRange > intervals)
absl::StatusOr< uint32_t > ReadWordAt(const Rom &rom, uint32_t address)
absl::StatusOr< uint32_t > ParseStream(std::span< const uint8_t > bytes, DungeonStreamKind kind, uint32_t start, uint32_t limit)
bool IsSortedUniqueByRoom(const std::vector< DungeonStreamWrite > &writes)
constexpr uint32_t kWramMappedPcBegin
absl::StatusOr< uint32_t > DecodePointer(const Rom &rom, const DungeonStreamLayout &layout, uint32_t room_id)
void ConsumeInterval(std::vector< DungeonStreamPcRange > *free_intervals, DungeonStreamPcRange consumed)
absl::StatusOr< uint32_t > ParsePotItemStream(std::span< const uint8_t > bytes, uint32_t start, uint32_t limit)
absl::Status ValidatePlanAgainstInventory(const DungeonStreamWritePlan &plan, const DungeonStreamInventory &inventory)
absl::StatusOr< std::vector< DungeonStreamPcRange > > NormalizeRanges(const std::vector< DungeonStreamPcRange > &input, uint32_t rom_size, const char *label, bool allow_empty)
absl::StatusOr< uint32_t > ParseSpriteStream(std::span< const uint8_t > bytes, uint32_t start, uint32_t limit)
absl::Status ValidateKnownPointerTableSource(const Rom &rom, const DungeonStreamLayout &layout)
absl::StatusOr< uint32_t > ParseObjectStream(std::span< const uint8_t > bytes, uint32_t start, uint32_t limit, uint32_t *door_list_pc=nullptr)
constexpr uint32_t kLoRomBankSize
absl::Status ValidateCompleteEncodedStream(DungeonStreamKind kind, const std::vector< uint8_t > &encoded)
Zelda 3 specific classes and functions.
constexpr int kSpritesDataEndExclusive
constexpr int kDoorPointers
@ kPointerOutsideDataRanges
constexpr int kRoomItemsDataEnd
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamRepack(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
constexpr int kRoomsSpritePointer
constexpr int GetDungeonObjectDataRegionEnd(int pc_address)
constexpr int kRoomItemsPointers
absl::Status ApplyDungeonStreamWritePlan(Rom *rom, const DungeonStreamWritePlan &plan)
absl::StatusOr< DungeonStreamRecord > ReadDungeonPotItemStream(const Rom &rom, int room_id)
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamWrites(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
absl::StatusOr< DungeonStreamInventory > InventoryDungeonStreams(const Rom &rom, const DungeonStreamLayout &requested_layout)
absl::StatusOr< DungeonStreamRecord > ReadDungeonObjectStream(const Rom &rom, int room_id)
constexpr int kNumberOfRooms
constexpr int kSpritesDataEmptyRoom
constexpr int kRoomObjectPointer
absl::StatusOr< DungeonStreamRecord > ReadDungeonSpriteStream(const Rom &rom, int room_id)
uint32_t PcToSnes(uint32_t addr)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
std::vector< DungeonStreamPcRange > allocatable_free_intervals
std::vector< DungeonStreamPcRange > occupied_intervals
std::vector< DungeonStreamPcRange > free_intervals
std::vector< DungeonStreamAliasGroup > aliases
DungeonStreamLayout layout
std::vector< DungeonStreamIssue > issues
std::vector< DungeonStreamOverlap > overlaps
std::vector< DungeonStreamRecord > streams
std::vector< DungeonStreamPcRange > allocation_ranges
DungeonPointerEncoding pointer_encoding
uint32_t pointer_table_pc
std::vector< DungeonStreamPcRange > data_ranges
std::vector< uint8_t > encoded_stream
std::vector< uint8_t > encoded_stream
std::vector< DungeonStreamWrite > pointer_writes
DungeonStreamWriteMode mode
std::vector< DungeonStreamWrite > payload_writes
std::vector< DungeonStreamWrite > auxiliary_pointer_writes
DungeonStreamLayout layout