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");
484 if (encoded.empty()) {
485 return absl::InvalidArgumentError(
"Replacement stream must not be empty");
488 ParseStream(encoded, kind, 0,
static_cast<uint32_t
>(encoded.size()));
489 if (!logical_end.ok()) {
490 return logical_end.status();
492 if (*logical_end != encoded.size()) {
493 return absl::InvalidArgumentError(
494 "Replacement stream contains bytes after its logical terminator");
496 return absl::OkStatus();
500 const std::vector<uint8_t>& encoded) {
501 uint32_t door_list_pc = std::numeric_limits<uint32_t>::max();
503 encoded, 0,
static_cast<uint32_t
>(encoded.size()), &door_list_pc);
504 if (!logical_end.ok()) {
505 return logical_end.status();
507 if (*logical_end != encoded.size() || door_list_pc >= encoded.size()) {
508 return absl::DataLossError(
509 "Object stream does not have a valid door-list target");
515 std::vector<DungeonStreamPcRange> intervals) {
516 std::sort(intervals.begin(), intervals.end(),
517 [](
const auto& a,
const auto& b) {
518 return std::tie(a.begin, a.end) < std::tie(b.begin, b.end);
520 std::vector<DungeonStreamPcRange> result;
521 for (
const auto& interval : intervals) {
522 if (result.empty() || interval.begin > result.back().end) {
523 result.push_back(interval);
525 result.back().end = std::max(result.back().end, interval.end);
532 const std::vector<DungeonStreamPcRange>& containers,
533 const std::vector<DungeonStreamPcRange>& occupied) {
534 std::vector<DungeonStreamPcRange> free;
535 for (
const auto& container : containers) {
536 uint32_t cursor = container.begin;
537 for (
const auto& used : occupied) {
538 if (used.end <= cursor) {
541 if (used.begin >= container.end) {
544 if (used.begin > cursor) {
545 free.push_back({cursor, std::min(used.begin, container.end)});
547 cursor = std::max(cursor, std::min(used.end, container.end));
548 if (cursor >= container.end) {
552 if (cursor < container.end) {
553 free.push_back({cursor, container.end});
562 return absl::OutOfRangeError(absl::StrFormat(
563 "PC address 0x%06X maps through SNES WRAM banks $7E/$7F", pc));
566 if (
SnesToPc(snes) != pc || (snes & 0xFFFFu) < 0x8000u) {
567 return absl::OutOfRangeError(absl::StrFormat(
568 "PC address 0x%06X is not representable as a LoROM pointer", pc));
571 return std::vector<uint8_t>{
static_cast<uint8_t
>(snes & 0xFF),
572 static_cast<uint8_t
>((snes >> 8) & 0xFF),
573 static_cast<uint8_t
>((snes >> 16) & 0xFF)};
575 const uint32_t fixed_snes =
576 (
static_cast<uint32_t
>(layout.
pointer_bank) << 16) | (snes & 0xFFFFu);
578 return absl::OutOfRangeError(absl::StrFormat(
579 "PC address 0x%06X is outside fixed pointer bank 0x%02X", pc,
582 return std::vector<uint8_t>{
static_cast<uint8_t
>(snes & 0xFF),
583 static_cast<uint8_t
>((snes >> 8) & 0xFF)};
587 const std::vector<DungeonStreamPcRange>& free_intervals,
590 return absl::ResourceExhaustedError(
591 "Replacement cannot fit in one LoROM bank");
595 const uint32_t bank_begin =
598 for (
const auto& interval : free_intervals) {
599 const uint32_t begin = std::max(interval.begin, bank_begin);
600 const uint32_t end = std::min(interval.end, bank_end);
601 if (begin <= end &&
static_cast<uint64_t
>(begin) + size <= end) {
605 return absl::ResourceExhaustedError(
606 absl::StrFormat(
"No %u-byte allocation fits fixed pointer bank 0x%02X",
610 for (
const auto& interval : free_intervals) {
611 uint32_t candidate = interval.begin;
612 while (candidate < interval.end) {
613 const uint32_t bank_end =
615 const uint32_t usable_end = std::min(interval.end, bank_end);
616 if (
static_cast<uint64_t
>(candidate) + size <= usable_end &&
620 if (bank_end <= candidate) {
623 candidate = bank_end;
626 return absl::ResourceExhaustedError(
627 absl::StrFormat(
"No declared free interval fits %u bytes", size));
632 std::vector<DungeonStreamPcRange> next;
633 next.reserve(free_intervals->size() + 1);
634 for (
const auto& interval : *free_intervals) {
635 if (!Intersects(interval, consumed)) {
636 next.push_back(interval);
639 if (interval.begin < consumed.
begin) {
640 next.push_back({interval.begin, consumed.
begin});
642 if (consumed.
end < interval.end) {
643 next.push_back({consumed.
end, interval.end});
646 *free_intervals = std::move(next);
650 for (
size_t i = 1; i < writes.size(); ++i) {
651 if (writes[i - 1].room_id >= writes[i].room_id) {
661 if (!inventory.
ok()) {
662 return absl::FailedPreconditionError(
663 "Source inventory contains invalid pointers or malformed streams");
667 return absl::AbortedError(
668 "Write plan does not match the current inventory snapshot");
671 return absl::InvalidArgumentError(
672 "Write plan must contain at least one payload write");
678 return absl::InvalidArgumentError(
679 "Write plan must be strictly sorted by room ID");
685 return absl::InvalidArgumentError(
686 "Copy-on-write plan must contain matching payload and pointer "
689 const size_t expected_auxiliary_count =
694 return absl::InvalidArgumentError(
695 "Copy-on-write plan has the wrong number of auxiliary pointer "
704 return absl::InvalidArgumentError(
705 "Repack-all plans currently support only fixed-bank pot-item "
709 return absl::InvalidArgumentError(absl::StrFormat(
713 return absl::InvalidArgumentError(
714 "Pot-item repack requires exactly one normalized allocation "
718 return absl::InvalidArgumentError(
719 "Repack-all plan must update every pointer-table entry");
722 return absl::InvalidArgumentError(
723 "Pot-item repack plan must not contain auxiliary pointers");
728 std::vector<DungeonStreamPcRange> planned_payloads;
730 std::map<std::vector<uint8_t>,
size_t> payload_by_pointer;
731 std::map<std::vector<uint8_t>, uint32_t> address_by_payload;
736 return absl::InvalidArgumentError(
"Write plan room mapping is invalid");
738 auto encoded_status =
740 if (!encoded_status.ok()) {
741 return encoded_status;
743 const uint64_t payload_end64 =
744 static_cast<uint64_t
>(payload.address) + payload.bytes.size();
745 if (payload_end64 > std::numeric_limits<uint32_t>::max()) {
746 return absl::OutOfRangeError(
"Payload write address overflows PC space");
749 payload.address,
static_cast<uint32_t
>(payload_end64)};
750 const uint32_t payload_bank_end =
752 if (payload_range.end > payload_bank_end) {
753 return absl::FailedPreconditionError(
754 "Planned payload crosses a LoROM bank boundary");
759 const bool in_allowed_range = std::any_of(
760 allowed_ranges.begin(), allowed_ranges.end(),
761 [&](
const auto& range) { return Contains(range, payload_range); });
762 if (!in_allowed_range) {
763 return absl::FailedPreconditionError(absl::StrFormat(
764 "Payload for room %u is outside declared allocator-owned %s",
769 for (
const auto& prior : planned_payloads) {
770 if (Intersects(prior, payload_range)) {
771 return absl::InvalidArgumentError(
"Planned payload writes overlap");
774 planned_payloads.push_back(payload_range);
777 if (!expected_pointer.ok()) {
778 return expected_pointer.status();
780 if (!payload_by_pointer.emplace(*expected_pointer, i).second) {
781 return absl::InvalidArgumentError(
782 "Multiple payload writes encode the same pointer target");
785 !address_by_payload.emplace(payload.bytes, payload.address).second) {
786 return absl::InvalidArgumentError(
787 "Write plan contains duplicate logical payloads");
792 const uint32_t expected_slot =
794 if (pointer.room_id != payload.room_id ||
795 pointer.address != expected_slot ||
796 pointer.bytes != *expected_pointer) {
797 return absl::InvalidArgumentError(
798 "Pointer write does not match its planned payload");
805 if (!door_offset.ok()) {
806 return door_offset.status();
808 auto expected_door_pointer =
810 if (!expected_door_pointer.ok()) {
811 return expected_door_pointer.status();
813 const uint32_t expected_door_slot =
kDoorPointers + payload.room_id * 3;
814 if (auxiliary.room_id != payload.room_id ||
815 auxiliary.address != expected_door_slot ||
816 auxiliary.bytes != *expected_door_pointer) {
817 return absl::InvalidArgumentError(
818 "Object door pointer does not match its planned payload");
820 if (
static_cast<uint64_t
>(auxiliary.address) + auxiliary.bytes.size() >
822 return absl::OutOfRangeError(
823 "Object door-pointer write is outside the source ROM");
829 std::vector<uint32_t> lowest_room_by_payload(
830 plan.
payload_writes.size(), std::numeric_limits<uint32_t>::max());
831 std::vector<bool> referenced_payloads(plan.
payload_writes.size(),
false);
834 const uint32_t expected_slot =
836 if (pointer.room_id != room_id || pointer.address != expected_slot) {
837 return absl::InvalidArgumentError(
838 "Repack-all pointer writes must cover every room in order");
840 const auto payload_it = payload_by_pointer.find(pointer.bytes);
841 if (payload_it == payload_by_pointer.end()) {
842 return absl::InvalidArgumentError(
843 "Repack-all pointer does not target a planned payload");
845 const size_t payload_index = payload_it->second;
846 referenced_payloads[payload_index] =
true;
847 lowest_room_by_payload[payload_index] =
848 std::min(lowest_room_by_payload[payload_index], room_id);
851 if (!referenced_payloads[i] ||
853 return absl::InvalidArgumentError(
854 "Repack-all payload owner must be its lowest referencing room");
858 return absl::OkStatus();
865 auto normalized_layout = ValidateAndNormalizeLayout(rom, requested_layout);
866 if (!normalized_layout.ok()) {
867 return normalized_layout.status();
871 inventory.
layout = std::move(*normalized_layout);
877 const uint32_t pointer_width =
879 std::map<uint32_t, std::vector<size_t>> valid_by_start;
880 std::vector<DungeonStreamPcRange> occupied;
881 const std::span<const uint8_t> bytes(rom.
data(), rom.
size());
889 auto pointer = DecodePointer(rom, inventory.
layout, room_id);
893 absl::StrCat(
"Room pointer is invalid: ",
894 pointer.status().message())});
895 inventory.
streams.push_back(record);
899 const auto* data_range =
901 if (data_range ==
nullptr) {
902 inventory.
issues.push_back(
906 "Room %u pointer PC 0x%06X is outside declared data ranges",
908 inventory.
streams.push_back(record);
912 const uint32_t bank_end =
913 ((record.
data_pc / kLoRomBankSize) + 1) * kLoRomBankSize;
914 const uint32_t parse_limit = std::min(data_range->end, bank_end);
917 if (!logical_end.ok()) {
918 inventory.
issues.push_back(
920 absl::StrCat(
"Room stream is malformed: ",
921 logical_end.status().message())});
922 inventory.
streams.push_back(record);
930 inventory.
streams.push_back(record);
931 const size_t index = inventory.
streams.size() - 1;
932 valid_by_start[record.
data_pc].push_back(index);
936 struct UniqueStream {
939 std::vector<uint32_t> room_ids;
941 std::vector<UniqueStream> unique;
942 unique.reserve(valid_by_start.size());
943 for (
const auto& [start, indices] : valid_by_start) {
945 stream.begin = start;
946 stream.end = inventory.
streams[indices.front()].logical_end_pc;
947 for (
size_t index : indices) {
948 stream.room_ids.push_back(inventory.
streams[index].room_id);
950 if (stream.room_ids.size() > 1) {
951 inventory.
aliases.push_back({start, stream.room_ids});
953 unique.push_back(std::move(stream));
956 for (
size_t i = 0; i < unique.size(); ++i) {
957 for (
size_t j = i + 1; j < unique.size(); ++j) {
958 if (unique[j].begin >= unique[i].end) {
961 const uint32_t intersection_end = std::min(unique[i].end, unique[j].end);
962 if (unique[j].begin >= intersection_end) {
965 const auto kind = unique[j].end == unique[i].end
971 {unique[j].begin, intersection_end}});
985 const std::vector<DungeonStreamReplacement>& requested_replacements) {
986 if (!inventory.
ok()) {
987 return absl::FailedPreconditionError(
988 "Cannot plan writes from an inventory with stream issues");
990 if (requested_replacements.empty()) {
991 return absl::InvalidArgumentError(
"No dungeon stream replacements given");
994 std::vector<DungeonStreamReplacement> replacements = requested_replacements;
995 std::sort(replacements.begin(), replacements.end(),
996 [](
const auto& a,
const auto& b) { return a.room_id < b.room_id; });
997 for (
size_t i = 0; i < replacements.size(); ++i) {
999 return absl::OutOfRangeError(
"Replacement room ID is out of range");
1001 if (i > 0 && replacements[i - 1].room_id == replacements[i].room_id) {
1002 return absl::InvalidArgumentError(
"Replacement room IDs must be unique");
1004 auto status = ValidateCompleteEncodedStream(inventory.
layout.
kind,
1005 replacements[i].encoded_stream);
1007 return absl::InvalidArgumentError(
1008 absl::StrFormat(
"Room %u replacement is invalid: %s",
1009 replacements[i].room_id, status.message()));
1022 return absl::OutOfRangeError(
1023 "Object door-pointer table is outside the source ROM");
1027 for (
const auto& replacement : replacements) {
1028 const uint32_t size =
1029 static_cast<uint32_t
>(replacement.encoded_stream.size());
1030 auto address = FindFirstFit(free, plan.
layout, size);
1031 if (!address.ok()) {
1032 return address.status();
1034 auto pointer_bytes = EncodePointer(plan.
layout, *address);
1035 if (!pointer_bytes.ok()) {
1036 return pointer_bytes.status();
1040 {replacement.room_id, *address, replacement.encoded_stream});
1042 {replacement.room_id,
1044 std::move(*pointer_bytes)});
1046 auto door_offset = ObjectDoorListOffset(replacement.encoded_stream);
1047 if (!door_offset.ok()) {
1048 return door_offset.status();
1050 auto door_pointer = EncodePointer(plan.
layout, *address + *door_offset);
1051 if (!door_pointer.ok()) {
1052 return door_pointer.status();
1055 {replacement.room_id,
kDoorPointers + replacement.room_id * 3,
1056 std::move(*door_pointer)});
1058 ConsumeInterval(&free, {*address, *address + size});
1065 const std::vector<DungeonStreamReplacement>& requested_replacements) {
1066 if (!inventory.
ok()) {
1067 return absl::FailedPreconditionError(
1068 "Cannot repack streams from an inventory with stream issues");
1073 return absl::InvalidArgumentError(
1074 "Repack-all currently supports only fixed-bank pot-item streams");
1077 return absl::InvalidArgumentError(absl::StrFormat(
1078 "Pot-item repack requires exactly %d pointers",
kNumberOfRooms));
1081 return absl::InvalidArgumentError(
1082 "Pot-item repack requires exactly one normalized allocation range");
1085 return absl::FailedPreconditionError(
1086 "Pot-item inventory does not cover every pointer-table entry");
1089 std::vector<std::vector<uint8_t>> logical_streams(
1094 seen_rooms[record.room_id] || record.encoded_stream.empty() ||
1095 record.encoded_stream.size() != record.size()) {
1096 return absl::FailedPreconditionError(
1097 "Pot-item inventory contains an incomplete logical stream snapshot");
1099 seen_rooms[record.room_id] =
true;
1100 logical_streams[record.room_id] = record.encoded_stream;
1102 if (std::find(seen_rooms.begin(), seen_rooms.end(),
false) !=
1104 return absl::FailedPreconditionError(
1105 "Pot-item inventory is missing a room stream snapshot");
1108 std::vector<DungeonStreamReplacement> replacements = requested_replacements;
1109 std::sort(replacements.begin(), replacements.end(),
1110 [](
const auto& a,
const auto& b) { return a.room_id < b.room_id; });
1111 for (
size_t i = 0; i < replacements.size(); ++i) {
1114 return absl::OutOfRangeError(
"Replacement room ID is out of range");
1116 if (i > 0 && replacements[i - 1].room_id == replacement.
room_id) {
1117 return absl::InvalidArgumentError(
"Replacement room IDs must be unique");
1119 const absl::Status status = ValidateCompleteEncodedStream(
1122 return absl::InvalidArgumentError(
1123 absl::StrFormat(
"Room %u replacement is invalid: %s",
1124 replacement.
room_id, status.message()));
1129 const uint32_t bank_begin =
SnesToPc(
1131 const uint32_t bank_end = bank_begin + kLoRomBankSize;
1132 uint64_t available_bytes = 0;
1134 if (range.begin < bank_begin || range.end > bank_end) {
1135 return absl::InvalidArgumentError(absl::StrFormat(
1136 "Pot-item repack range [0x%06X,0x%06X) crosses or lies outside "
1137 "fixed pointer bank 0x%02X",
1140 available_bytes +=
static_cast<uint64_t
>(range.end) - range.begin;
1143 struct RepackGroup {
1144 uint32_t owner_room = 0;
1145 std::vector<uint8_t> bytes;
1146 std::vector<uint32_t> room_ids;
1148 std::map<std::vector<uint8_t>, std::vector<uint32_t>> rooms_by_payload;
1151 rooms_by_payload[logical_streams[room_id]].push_back(room_id);
1153 std::vector<RepackGroup> groups;
1154 groups.reserve(rooms_by_payload.size());
1155 uint64_t required_bytes = 0;
1156 for (
auto& [bytes, room_ids] : rooms_by_payload) {
1157 required_bytes += bytes.size();
1158 groups.push_back({room_ids.front(), std::move(bytes), std::move(room_ids)});
1160 if (required_bytes > available_bytes) {
1161 return absl::ResourceExhaustedError(
1162 absl::StrCat(
"Pot-item repack needs ", required_bytes,
1163 " bytes but declared ranges provide ", available_bytes));
1165 std::sort(groups.begin(), groups.end(), [](
const auto& a,
const auto& b) {
1166 return a.owner_room < b.owner_room;
1179 for (
const RepackGroup& group : groups) {
1180 const uint32_t address = next_address;
1181 plan.
payload_writes.push_back({group.owner_room, address, group.bytes});
1182 for (
const uint32_t room_id : group.room_ids) {
1183 target_by_room[room_id] = address;
1185 next_address +=
static_cast<uint32_t
>(group.bytes.size());
1188 const uint32_t pointer_width =
1193 EncodePointer(inventory.
layout, target_by_room[room_id]));
1196 std::move(pointer)});
1203 if (rom ==
nullptr || !rom->
is_loaded()) {
1204 return absl::InvalidArgumentError(
"ROM not loaded");
1207 return absl::AbortedError(
"Dungeon stream plan source size is stale");
1209 const uint32_t current_crc =
1212 return absl::AbortedError(absl::StrFormat(
1213 "Dungeon stream plan is stale (source CRC %08X, current CRC %08X)",
1218 if (!inventory.ok()) {
1219 return inventory.status();
1221 auto validation = ValidatePlanAgainstInventory(plan, *inventory);
1222 if (!validation.ok()) {
1227 auto allow_writes = [&](
const std::vector<DungeonStreamWrite>& writes,
1228 const char* label) -> absl::Status {
1229 for (
const auto& write : writes) {
1230 const uint64_t end =
1231 static_cast<uint64_t
>(write.address) + write.bytes.size();
1232 if (end > std::numeric_limits<uint32_t>::max()) {
1233 return absl::OutOfRangeError(
"Dungeon stream write range overflows");
1236 write_fence.
Allow(write.address,
static_cast<uint32_t
>(end), label));
1238 return absl::OkStatus();
1243 "DungeonStreamAuxiliaryPointer"));
1246 const std::vector<uint8_t> snapshot = rom->
vector();
1247 const bool was_dirty = rom->
dirty();
1248 auto rollback = [&]() {
1255 rom->
WriteVector(
static_cast<int>(write.address), write.bytes);
1263 rom->
WriteVector(
static_cast<int>(write.address), write.bytes);
1271 rom->
WriteVector(
static_cast<int>(write.address), write.bytes);
1277 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< 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 kDoorPointers
@ kPointerOutsideDataRanges
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamRepack(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
constexpr int kRoomsSpritePointer
constexpr int kRoomItemsPointers
absl::Status ApplyDungeonStreamWritePlan(Rom *rom, const DungeonStreamWritePlan &plan)
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamWrites(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
absl::StatusOr< DungeonStreamInventory > InventoryDungeonStreams(const Rom &rom, const DungeonStreamLayout &requested_layout)
constexpr int kNumberOfRooms
constexpr int kRoomObjectPointer
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