yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_stream_allocator.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <limits>
7#include <map>
8#include <span>
9#include <tuple>
10#include <utility>
11#include <vector>
12
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"
17#include "rom/rom.h"
18#include "rom/snes.h"
19#include "rom/write_fence.h"
20#include "util/macro.h"
21#include "util/rom_hash.h"
23
24namespace yaze::zelda3 {
25namespace {
26
27constexpr uint32_t kLoRomBankSize = 0x8000;
28constexpr uint32_t kWramMappedPcBegin = 0x3F0000;
29constexpr uint32_t kWramMappedPcEnd = 0x400000;
30
31bool IntersectsWramMappedPc(uint32_t begin, uint32_t end) {
32 return begin < kWramMappedPcEnd && kWramMappedPcBegin < end;
33}
34
36 return encoding == DungeonPointerEncoding::kLong24 ? 3u : 2u;
37}
38
40 const DungeonStreamPcRange& inner) {
41 return outer.begin <= inner.begin && inner.end <= outer.end;
42}
43
44bool Intersects(const DungeonStreamPcRange& a, const DungeonStreamPcRange& b) {
45 return a.begin < b.end && b.begin < a.end;
46}
47
48bool ContainsAddress(const DungeonStreamPcRange& range, uint32_t address) {
49 return range.begin <= address && address < range.end;
50}
51
52absl::StatusOr<std::vector<DungeonStreamPcRange>> NormalizeRanges(
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"));
58 }
59
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);
63 });
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));
70 }
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));
75 }
76 if (IntersectsWramMappedPc(range.begin, range.end)) {
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));
80 }
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));
84 }
85 if (!normalized.empty() && range.begin == normalized.back().end) {
86 normalized.back().end = range.end;
87 } else {
88 normalized.push_back(range);
89 }
90 }
91 return normalized;
92}
93
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); });
101 if (!contained) {
102 return absl::InvalidArgumentError(absl::StrFormat(
103 "Allocation range [0x%06X,0x%06X) is not contained in one data "
104 "range",
105 allocation.begin, allocation.end));
106 }
107 }
108 return absl::OkStatus();
109}
110
111absl::StatusOr<uint32_t> ReadWordAt(const Rom& rom, uint32_t address) {
112 auto result = rom.ReadWord(static_cast<int>(address));
113 if (!result.ok()) {
114 return result.status();
115 }
116 return static_cast<uint32_t>(*result);
117}
118
119absl::StatusOr<uint32_t> ReadLongAt(const Rom& rom, uint32_t address) {
120 auto result = rom.ReadLong(static_cast<int>(address));
121 if (!result.ok()) {
122 return result.status();
123 }
124 return *result;
125}
126
128 const Rom& rom, const DungeonStreamLayout& layout) {
129 switch (layout.kind) {
132 return absl::InvalidArgumentError(
133 "Object streams require 24-bit long pointers");
134 }
135 auto raw = ReadLongAt(rom, kRoomObjectPointer);
136 if (!raw.ok()) {
137 return raw.status();
138 }
139 if ((*raw & 0xFFFFu) < 0x8000u ||
140 SnesToPc(*raw) != layout.pointer_table_pc) {
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",
145 }
146 return absl::OkStatus();
147 }
150 layout.pointer_bank != 0x09) {
151 return absl::InvalidArgumentError(
152 "Sprite streams require fixed-bank-16 pointers in bank 0x09");
153 }
154 auto raw = ReadWordAt(rom, kRoomsSpritePointer);
155 if (!raw.ok()) {
156 return raw.status();
157 }
158 if (*raw < 0x8000u ||
159 SnesToPc((static_cast<uint32_t>(layout.pointer_bank) << 16) | *raw) !=
160 layout.pointer_table_pc) {
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",
165 }
166 return absl::OkStatus();
167 }
170 layout.pointer_bank != 0x01) {
171 return absl::InvalidArgumentError(
172 "Pot-item streams require fixed-bank-16 pointers in bank 0x01");
173 }
174 if (layout.pointer_table_pc != kRoomItemsPointers) {
175 return absl::FailedPreconditionError(absl::StrFormat(
176 "Pot-item pointer table must be the fixed PC table at 0x%06X",
178 }
179 return absl::OkStatus();
180 }
181 return absl::InvalidArgumentError("Unknown dungeon stream kind");
182}
183
185 switch (kind) {
191 // Pot-item pointers use a fixed table rather than an indirection source.
193 }
194 return {};
195}
196
197absl::StatusOr<DungeonStreamLayout> ValidateAndNormalizeLayout(
198 const Rom& rom, const DungeonStreamLayout& input) {
199 if (!rom.is_loaded()) {
200 return absl::InvalidArgumentError("ROM not loaded");
201 }
202 if (rom.size() > std::numeric_limits<uint32_t>::max()) {
203 return absl::OutOfRangeError("ROM is too large for PC-native layout");
204 }
205 if (input.pointer_count == 0 || input.pointer_count > kNumberOfRooms) {
206 return absl::InvalidArgumentError(
207 absl::StrFormat("Pointer count %u is outside supported range 1..%d",
209 }
210
211 DungeonStreamLayout layout = input;
212 const uint32_t rom_size = static_cast<uint32_t>(rom.size());
213 auto data_ranges =
214 NormalizeRanges(input.data_ranges, rom_size, "data_ranges", false);
215 if (!data_ranges.ok()) {
216 return data_ranges.status();
217 }
218 auto allocation_ranges = NormalizeRanges(input.allocation_ranges, rom_size,
219 "allocation_ranges", true);
220 if (!allocation_ranges.ok()) {
221 return allocation_ranges.status();
222 }
223 layout.data_ranges = std::move(*data_ranges);
224 layout.allocation_ranges = std::move(*allocation_ranges);
225
226 auto allocation_status =
228 if (!allocation_status.ok()) {
229 return allocation_status;
230 }
231
232 const uint32_t pointer_width = PointerWidth(layout.pointer_encoding);
233 const uint64_t table_end =
234 static_cast<uint64_t>(layout.pointer_table_pc) +
235 static_cast<uint64_t>(layout.pointer_count) * pointer_width;
236 if (table_end > rom_size) {
237 return absl::OutOfRangeError(
238 "Dungeon stream pointer table is out of range");
239 }
240 const DungeonStreamPcRange pointer_table_range{
241 layout.pointer_table_pc, static_cast<uint32_t>(table_end)};
242 if (IntersectsWramMappedPc(pointer_table_range.begin,
243 pointer_table_range.end)) {
244 return absl::InvalidArgumentError(
245 "Dungeon stream pointer table maps through SNES WRAM banks $7E/$7F");
246 }
248 const uint32_t pointer_table_word =
249 PcToSnes(layout.pointer_table_pc) & 0xFFFFu;
250 const uint64_t runtime_table_end =
251 static_cast<uint64_t>(pointer_table_word) +
252 static_cast<uint64_t>(layout.pointer_count) * pointer_width;
253 if (runtime_table_end > 0x10000u) {
254 return absl::InvalidArgumentError(
255 "Fixed-bank-16 pointer table crosses its runtime CPU bank");
256 }
257 }
258
259 const DungeonStreamPcRange pointer_source_range =
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");
265 }
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");
270 }
271 }
272
273 DungeonStreamPcRange door_pointer_range;
274 if (layout.kind == DungeonStreamKind::kObject) {
275 const uint64_t door_pointer_end =
276 static_cast<uint64_t>(kDoorPointers) +
277 static_cast<uint64_t>(kNumberOfRooms) * 3u;
278 if (door_pointer_end > rom_size) {
279 return absl::OutOfRangeError(
280 "Complete object door-pointer table is outside the source ROM");
281 }
282 door_pointer_range = {kDoorPointers,
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");
288 }
289 }
290
291 for (const auto& data : layout.data_ranges) {
292 if (Intersects(pointer_table_range, data)) {
293 return absl::InvalidArgumentError(
294 "Dungeon stream data ranges must not overlap the pointer table");
295 }
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");
301 }
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");
307 }
308 }
309
310 auto source_status = ValidateKnownPointerTableSource(rom, layout);
311 if (!source_status.ok()) {
312 return source_status;
313 }
314 return layout;
315}
316
317absl::StatusOr<uint32_t> DecodePointer(const Rom& rom,
318 const DungeonStreamLayout& layout,
319 uint32_t room_id) {
320 const uint32_t width = PointerWidth(layout.pointer_encoding);
321 const uint32_t slot = layout.pointer_table_pc + room_id * width;
322 uint32_t snes = 0;
324 auto raw = ReadLongAt(rom, slot);
325 if (!raw.ok()) {
326 return raw.status();
327 }
328 snes = *raw;
329 } else {
330 auto raw = ReadWordAt(rom, slot);
331 if (!raw.ok()) {
332 return raw.status();
333 }
334 snes = (static_cast<uint32_t>(layout.pointer_bank) << 16) | *raw;
335 }
336 if ((snes & 0xFFFFu) < 0x8000u) {
337 return absl::FailedPreconditionError(absl::StrFormat(
338 "Pointer word 0x%04X is not a LoROM address", snes & 0xFFFFu));
339 }
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));
344 }
345 const uint32_t pc = SnesToPc(snes);
346 if (pc >= rom.size()) {
347 return absl::OutOfRangeError(
348 absl::StrFormat("Pointer resolves outside ROM at PC 0x%06X", pc));
349 }
350 return pc;
351}
352
354 const std::vector<DungeonStreamPcRange>& ranges, uint32_t address) {
355 auto it =
356 std::upper_bound(ranges.begin(), ranges.end(), address,
357 [](uint32_t value, const DungeonStreamPcRange& range) {
358 return value < range.begin;
359 });
360 if (it == ranges.begin()) {
361 return nullptr;
362 }
363 --it;
364 return ContainsAddress(*it, address) ? &*it : nullptr;
365}
366
367absl::StatusOr<uint32_t> ParseObjectStream(std::span<const uint8_t> bytes,
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");
372 }
373 uint32_t cursor = start + 2;
374 for (int list = 0; list < 2; ++list) {
375 while (true) {
376 if (cursor + 2 > limit) {
377 return absl::DataLossError(
378 absl::StrFormat("Object list %d has no 0xFFFF terminator", list));
379 }
380 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
381 cursor += 2;
382 break;
383 }
384 if (cursor + 3 > limit) {
385 return absl::DataLossError(
386 absl::StrFormat("Object list %d has a truncated record", list));
387 }
388 cursor += 3;
389 }
390 }
391
392 bool in_doors = false;
393 while (true) {
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");
397 }
398 if (!in_doors) {
399 if (bytes[cursor] == 0xF0 && bytes[cursor + 1] == 0xFF) {
400 cursor += 2;
401 if (door_list_pc != nullptr) {
402 *door_list_pc = cursor;
403 }
404 in_doors = true;
405 continue;
406 }
407 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
408 // Canonical vanilla/OOS empty streams may terminate the third list
409 // directly without a 0xFFF0 marker. Their door pointer targets this
410 // final terminator.
411 if (door_list_pc != nullptr) {
412 *door_list_pc = cursor;
413 }
414 return cursor + 2;
415 }
416 if (cursor + 3 > limit) {
417 return absl::DataLossError("Object list 2 has a truncated record");
418 }
419 cursor += 3;
420 continue;
421 }
422
423 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
424 return cursor + 2;
425 }
426 cursor += 2;
427 }
428}
429
430absl::StatusOr<uint32_t> ParseSpriteStream(std::span<const uint8_t> bytes,
431 uint32_t start, uint32_t limit) {
432 if (start >= limit) {
433 return absl::DataLossError("Sprite stream is missing its sort byte");
434 }
435 uint32_t cursor = start + 1;
436 while (cursor < limit) {
437 if (bytes[cursor] == 0xFF) {
438 return cursor + 1;
439 }
440 if (cursor + 3 > limit) {
441 return absl::DataLossError("Sprite stream has a truncated record");
442 }
443 cursor += 3;
444 }
445 return absl::DataLossError("Sprite stream has no 0xFF terminator");
446}
447
448absl::StatusOr<uint32_t> ParsePotItemStream(std::span<const uint8_t> bytes,
449 uint32_t start, uint32_t limit) {
450 uint32_t cursor = start;
451 while (true) {
452 if (cursor + 2 > limit) {
453 return absl::DataLossError("Pot-item stream has no 0xFFFF terminator");
454 }
455 if (bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xFF) {
456 return cursor + 2;
457 }
458 if (cursor + 3 > limit) {
459 return absl::DataLossError("Pot-item stream has a truncated record");
460 }
461 cursor += 3;
462 }
463}
464
465absl::StatusOr<uint32_t> ParseStream(std::span<const uint8_t> bytes,
466 DungeonStreamKind kind, uint32_t start,
467 uint32_t limit) {
468 if (limit > bytes.size() || start >= limit) {
469 return absl::OutOfRangeError("Dungeon stream parse bounds are invalid");
470 }
471 switch (kind) {
473 return ParseObjectStream(bytes, start, limit);
475 return ParseSpriteStream(bytes, start, limit);
477 return ParsePotItemStream(bytes, start, limit);
478 }
479 return absl::InvalidArgumentError("Unknown dungeon stream kind");
480}
481
483 DungeonStreamKind kind, const std::vector<uint8_t>& encoded) {
484 if (encoded.empty()) {
485 return absl::InvalidArgumentError("Replacement stream must not be empty");
486 }
487 auto logical_end =
488 ParseStream(encoded, kind, 0, static_cast<uint32_t>(encoded.size()));
489 if (!logical_end.ok()) {
490 return logical_end.status();
491 }
492 if (*logical_end != encoded.size()) {
493 return absl::InvalidArgumentError(
494 "Replacement stream contains bytes after its logical terminator");
495 }
496 return absl::OkStatus();
497}
498
499absl::StatusOr<uint32_t> ObjectDoorListOffset(
500 const std::vector<uint8_t>& encoded) {
501 uint32_t door_list_pc = std::numeric_limits<uint32_t>::max();
502 auto logical_end = ParseObjectStream(
503 encoded, 0, static_cast<uint32_t>(encoded.size()), &door_list_pc);
504 if (!logical_end.ok()) {
505 return logical_end.status();
506 }
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");
510 }
511 return door_list_pc;
512}
513
514std::vector<DungeonStreamPcRange> UnionIntervals(
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);
519 });
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);
524 } else {
525 result.back().end = std::max(result.back().end, interval.end);
526 }
527 }
528 return result;
529}
530
531std::vector<DungeonStreamPcRange> ComplementIntervals(
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) {
539 continue;
540 }
541 if (used.begin >= container.end) {
542 break;
543 }
544 if (used.begin > cursor) {
545 free.push_back({cursor, std::min(used.begin, container.end)});
546 }
547 cursor = std::max(cursor, std::min(used.end, container.end));
548 if (cursor >= container.end) {
549 break;
550 }
551 }
552 if (cursor < container.end) {
553 free.push_back({cursor, container.end});
554 }
555 }
556 return free;
557}
558
559absl::StatusOr<std::vector<uint8_t>> EncodePointer(
560 const DungeonStreamLayout& layout, uint32_t pc) {
561 if (pc >= kWramMappedPcBegin && pc < kWramMappedPcEnd) {
562 return absl::OutOfRangeError(absl::StrFormat(
563 "PC address 0x%06X maps through SNES WRAM banks $7E/$7F", pc));
564 }
565 const uint32_t snes = PcToSnes(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));
569 }
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)};
574 }
575 const uint32_t fixed_snes =
576 (static_cast<uint32_t>(layout.pointer_bank) << 16) | (snes & 0xFFFFu);
577 if (SnesToPc(fixed_snes) != pc) {
578 return absl::OutOfRangeError(absl::StrFormat(
579 "PC address 0x%06X is outside fixed pointer bank 0x%02X", pc,
580 layout.pointer_bank));
581 }
582 return std::vector<uint8_t>{static_cast<uint8_t>(snes & 0xFF),
583 static_cast<uint8_t>((snes >> 8) & 0xFF)};
584}
585
586absl::StatusOr<uint32_t> FindFirstFit(
587 const std::vector<DungeonStreamPcRange>& free_intervals,
588 const DungeonStreamLayout& layout, uint32_t size) {
589 if (size == 0 || size > kLoRomBankSize) {
590 return absl::ResourceExhaustedError(
591 "Replacement cannot fit in one LoROM bank");
592 }
593
595 const uint32_t bank_begin =
596 SnesToPc((static_cast<uint32_t>(layout.pointer_bank) << 16) | 0x8000u);
597 const uint32_t bank_end = bank_begin + kLoRomBankSize;
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) {
602 return begin;
603 }
604 }
605 return absl::ResourceExhaustedError(
606 absl::StrFormat("No %u-byte allocation fits fixed pointer bank 0x%02X",
607 size, layout.pointer_bank));
608 }
609
610 for (const auto& interval : free_intervals) {
611 uint32_t candidate = interval.begin;
612 while (candidate < interval.end) {
613 const uint32_t bank_end =
614 ((candidate / kLoRomBankSize) + 1) * kLoRomBankSize;
615 const uint32_t usable_end = std::min(interval.end, bank_end);
616 if (static_cast<uint64_t>(candidate) + size <= usable_end &&
617 EncodePointer(layout, candidate).ok()) {
618 return candidate;
619 }
620 if (bank_end <= candidate) {
621 break;
622 }
623 candidate = bank_end;
624 }
625 }
626 return absl::ResourceExhaustedError(
627 absl::StrFormat("No declared free interval fits %u bytes", size));
628}
629
630void ConsumeInterval(std::vector<DungeonStreamPcRange>* free_intervals,
631 DungeonStreamPcRange consumed) {
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);
637 continue;
638 }
639 if (interval.begin < consumed.begin) {
640 next.push_back({interval.begin, consumed.begin});
641 }
642 if (consumed.end < interval.end) {
643 next.push_back({consumed.end, interval.end});
644 }
645 }
646 *free_intervals = std::move(next);
647}
648
649bool IsSortedUniqueByRoom(const std::vector<DungeonStreamWrite>& writes) {
650 for (size_t i = 1; i < writes.size(); ++i) {
651 if (writes[i - 1].room_id >= writes[i].room_id) {
652 return false;
653 }
654 }
655 return true;
656}
657
659 const DungeonStreamWritePlan& plan,
660 const DungeonStreamInventory& inventory) {
661 if (!inventory.ok()) {
662 return absl::FailedPreconditionError(
663 "Source inventory contains invalid pointers or malformed streams");
664 }
665 if (plan.source_size != inventory.source_size ||
666 plan.source_crc32 != inventory.source_crc32) {
667 return absl::AbortedError(
668 "Write plan does not match the current inventory snapshot");
669 }
670 if (plan.payload_writes.empty()) {
671 return absl::InvalidArgumentError(
672 "Write plan must contain at least one payload write");
673 }
676 (!plan.auxiliary_pointer_writes.empty() &&
678 return absl::InvalidArgumentError(
679 "Write plan must be strictly sorted by room ID");
680 }
681
682 switch (plan.mode) {
684 if (plan.payload_writes.size() != plan.pointer_writes.size()) {
685 return absl::InvalidArgumentError(
686 "Copy-on-write plan must contain matching payload and pointer "
687 "writes");
688 }
689 const size_t expected_auxiliary_count =
691 ? plan.payload_writes.size()
692 : 0;
693 if (plan.auxiliary_pointer_writes.size() != expected_auxiliary_count) {
694 return absl::InvalidArgumentError(
695 "Copy-on-write plan has the wrong number of auxiliary pointer "
696 "writes");
697 }
698 break;
699 }
704 return absl::InvalidArgumentError(
705 "Repack-all plans currently support only fixed-bank pot-item "
706 "streams");
707 }
708 if (plan.layout.pointer_count != kNumberOfRooms) {
709 return absl::InvalidArgumentError(absl::StrFormat(
710 "Pot-item repack requires exactly %d pointers", kNumberOfRooms));
711 }
712 if (plan.layout.allocation_ranges.size() != 1) {
713 return absl::InvalidArgumentError(
714 "Pot-item repack requires exactly one normalized allocation "
715 "range");
716 }
717 if (plan.pointer_writes.size() != plan.layout.pointer_count) {
718 return absl::InvalidArgumentError(
719 "Repack-all plan must update every pointer-table entry");
720 }
721 if (!plan.auxiliary_pointer_writes.empty()) {
722 return absl::InvalidArgumentError(
723 "Pot-item repack plan must not contain auxiliary pointers");
724 }
725 break;
726 }
727
728 std::vector<DungeonStreamPcRange> planned_payloads;
729 planned_payloads.reserve(plan.payload_writes.size());
730 std::map<std::vector<uint8_t>, size_t> payload_by_pointer;
731 std::map<std::vector<uint8_t>, uint32_t> address_by_payload;
732 const uint32_t pointer_width = PointerWidth(plan.layout.pointer_encoding);
733 for (size_t i = 0; i < plan.payload_writes.size(); ++i) {
734 const auto& payload = plan.payload_writes[i];
735 if (payload.room_id >= plan.layout.pointer_count || payload.bytes.empty()) {
736 return absl::InvalidArgumentError("Write plan room mapping is invalid");
737 }
738 auto encoded_status =
739 ValidateCompleteEncodedStream(plan.layout.kind, payload.bytes);
740 if (!encoded_status.ok()) {
741 return encoded_status;
742 }
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");
747 }
748 const DungeonStreamPcRange payload_range{
749 payload.address, static_cast<uint32_t>(payload_end64)};
750 const uint32_t payload_bank_end =
751 ((payload.address / kLoRomBankSize) + 1) * kLoRomBankSize;
752 if (payload_range.end > payload_bank_end) {
753 return absl::FailedPreconditionError(
754 "Planned payload crosses a LoROM bank boundary");
755 }
756 const auto& allowed_ranges = plan.mode == DungeonStreamWriteMode::kRepackAll
758 : inventory.allocatable_free_intervals;
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",
765 payload.room_id,
766 plan.mode == DungeonStreamWriteMode::kRepackAll ? "repack space"
767 : "free space"));
768 }
769 for (const auto& prior : planned_payloads) {
770 if (Intersects(prior, payload_range)) {
771 return absl::InvalidArgumentError("Planned payload writes overlap");
772 }
773 }
774 planned_payloads.push_back(payload_range);
775
776 auto expected_pointer = EncodePointer(plan.layout, payload.address);
777 if (!expected_pointer.ok()) {
778 return expected_pointer.status();
779 }
780 if (!payload_by_pointer.emplace(*expected_pointer, i).second) {
781 return absl::InvalidArgumentError(
782 "Multiple payload writes encode the same pointer target");
783 }
785 !address_by_payload.emplace(payload.bytes, payload.address).second) {
786 return absl::InvalidArgumentError(
787 "Write plan contains duplicate logical payloads");
788 }
789
791 const auto& pointer = plan.pointer_writes[i];
792 const uint32_t expected_slot =
793 plan.layout.pointer_table_pc + payload.room_id * pointer_width;
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");
799 }
800 }
803 const auto& auxiliary = plan.auxiliary_pointer_writes[i];
804 auto door_offset = ObjectDoorListOffset(payload.bytes);
805 if (!door_offset.ok()) {
806 return door_offset.status();
807 }
808 auto expected_door_pointer =
809 EncodePointer(plan.layout, payload.address + *door_offset);
810 if (!expected_door_pointer.ok()) {
811 return expected_door_pointer.status();
812 }
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");
819 }
820 if (static_cast<uint64_t>(auxiliary.address) + auxiliary.bytes.size() >
821 inventory.source_size) {
822 return absl::OutOfRangeError(
823 "Object door-pointer write is outside the source ROM");
824 }
825 }
826 }
827
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);
832 for (uint32_t room_id = 0; room_id < plan.layout.pointer_count; ++room_id) {
833 const auto& pointer = plan.pointer_writes[room_id];
834 const uint32_t expected_slot =
835 plan.layout.pointer_table_pc + room_id * pointer_width;
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");
839 }
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");
844 }
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);
849 }
850 for (size_t i = 0; i < plan.payload_writes.size(); ++i) {
851 if (!referenced_payloads[i] ||
852 plan.payload_writes[i].room_id != lowest_room_by_payload[i]) {
853 return absl::InvalidArgumentError(
854 "Repack-all payload owner must be its lowest referencing room");
855 }
856 }
857 }
858 return absl::OkStatus();
859}
860
861} // namespace
862
863absl::StatusOr<DungeonStreamInventory> InventoryDungeonStreams(
864 const Rom& rom, const DungeonStreamLayout& requested_layout) {
865 auto normalized_layout = ValidateAndNormalizeLayout(rom, requested_layout);
866 if (!normalized_layout.ok()) {
867 return normalized_layout.status();
868 }
869
870 DungeonStreamInventory inventory;
871 inventory.layout = std::move(*normalized_layout);
872 inventory.source_size = static_cast<uint32_t>(rom.size());
873 inventory.source_crc32 =
874 util::CalculateCrc32(rom.data(), static_cast<size_t>(rom.size()));
875 inventory.streams.reserve(inventory.layout.pointer_count);
876
877 const uint32_t pointer_width =
878 PointerWidth(inventory.layout.pointer_encoding);
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());
882 for (uint32_t room_id = 0; room_id < inventory.layout.pointer_count;
883 ++room_id) {
884 DungeonStreamRecord record;
885 record.room_id = room_id;
886 record.pointer_slot_pc =
887 inventory.layout.pointer_table_pc + room_id * pointer_width;
888
889 auto pointer = DecodePointer(rom, inventory.layout, room_id);
890 if (!pointer.ok()) {
892 room_id, 0,
893 absl::StrCat("Room pointer is invalid: ",
894 pointer.status().message())});
895 inventory.streams.push_back(record);
896 continue;
897 }
898 record.data_pc = *pointer;
899 const auto* data_range =
900 FindContainingRange(inventory.layout.data_ranges, record.data_pc);
901 if (data_range == nullptr) {
902 inventory.issues.push_back(
904 record.data_pc,
905 absl::StrFormat(
906 "Room %u pointer PC 0x%06X is outside declared data ranges",
907 room_id, record.data_pc)});
908 inventory.streams.push_back(record);
909 continue;
910 }
911
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);
915 auto logical_end =
916 ParseStream(bytes, inventory.layout.kind, record.data_pc, parse_limit);
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);
923 continue;
924 }
925
926 record.logical_end_pc = *logical_end;
927 record.valid = true;
928 record.encoded_stream.assign(bytes.begin() + record.data_pc,
929 bytes.begin() + record.logical_end_pc);
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);
933 occupied.push_back({record.data_pc, record.logical_end_pc});
934 }
935
936 struct UniqueStream {
937 uint32_t begin = 0;
938 uint32_t end = 0;
939 std::vector<uint32_t> room_ids;
940 };
941 std::vector<UniqueStream> unique;
942 unique.reserve(valid_by_start.size());
943 for (const auto& [start, indices] : valid_by_start) {
944 UniqueStream stream;
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);
949 }
950 if (stream.room_ids.size() > 1) {
951 inventory.aliases.push_back({start, stream.room_ids});
952 }
953 unique.push_back(std::move(stream));
954 }
955
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) {
959 break;
960 }
961 const uint32_t intersection_end = std::min(unique[i].end, unique[j].end);
962 if (unique[j].begin >= intersection_end) {
963 continue;
964 }
965 const auto kind = unique[j].end == unique[i].end
968 inventory.overlaps.push_back({kind,
969 unique[i].room_ids,
970 unique[j].room_ids,
971 {unique[j].begin, intersection_end}});
972 }
973 }
974
975 inventory.occupied_intervals = UnionIntervals(std::move(occupied));
976 inventory.free_intervals = ComplementIntervals(inventory.layout.data_ranges,
977 inventory.occupied_intervals);
978 inventory.allocatable_free_intervals = ComplementIntervals(
979 inventory.layout.allocation_ranges, inventory.occupied_intervals);
980 return inventory;
981}
982
983absl::StatusOr<DungeonStreamWritePlan> PlanDungeonStreamWrites(
984 const DungeonStreamInventory& inventory,
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");
989 }
990 if (requested_replacements.empty()) {
991 return absl::InvalidArgumentError("No dungeon stream replacements given");
992 }
993
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) {
998 if (replacements[i].room_id >= inventory.layout.pointer_count) {
999 return absl::OutOfRangeError("Replacement room ID is out of range");
1000 }
1001 if (i > 0 && replacements[i - 1].room_id == replacements[i].room_id) {
1002 return absl::InvalidArgumentError("Replacement room IDs must be unique");
1003 }
1004 auto status = ValidateCompleteEncodedStream(inventory.layout.kind,
1005 replacements[i].encoded_stream);
1006 if (!status.ok()) {
1007 return absl::InvalidArgumentError(
1008 absl::StrFormat("Room %u replacement is invalid: %s",
1009 replacements[i].room_id, status.message()));
1010 }
1011 }
1012
1014 plan.layout = inventory.layout;
1015 plan.source_size = inventory.source_size;
1016 plan.source_crc32 = inventory.source_crc32;
1019 static_cast<uint64_t>(kDoorPointers) +
1020 static_cast<uint64_t>(plan.layout.pointer_count) * 3 >
1021 plan.source_size) {
1022 return absl::OutOfRangeError(
1023 "Object door-pointer table is outside the source ROM");
1024 }
1025 std::vector<DungeonStreamPcRange> free = inventory.allocatable_free_intervals;
1026 const uint32_t pointer_width = PointerWidth(plan.layout.pointer_encoding);
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();
1033 }
1034 auto pointer_bytes = EncodePointer(plan.layout, *address);
1035 if (!pointer_bytes.ok()) {
1036 return pointer_bytes.status();
1037 }
1038
1039 plan.payload_writes.push_back(
1040 {replacement.room_id, *address, replacement.encoded_stream});
1041 plan.pointer_writes.push_back(
1042 {replacement.room_id,
1043 plan.layout.pointer_table_pc + replacement.room_id * pointer_width,
1044 std::move(*pointer_bytes)});
1046 auto door_offset = ObjectDoorListOffset(replacement.encoded_stream);
1047 if (!door_offset.ok()) {
1048 return door_offset.status();
1049 }
1050 auto door_pointer = EncodePointer(plan.layout, *address + *door_offset);
1051 if (!door_pointer.ok()) {
1052 return door_pointer.status();
1053 }
1054 plan.auxiliary_pointer_writes.push_back(
1055 {replacement.room_id, kDoorPointers + replacement.room_id * 3,
1056 std::move(*door_pointer)});
1057 }
1058 ConsumeInterval(&free, {*address, *address + size});
1059 }
1060 return plan;
1061}
1062
1063absl::StatusOr<DungeonStreamWritePlan> PlanDungeonStreamRepack(
1064 const DungeonStreamInventory& inventory,
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");
1069 }
1070 if (inventory.layout.kind != DungeonStreamKind::kPotItem ||
1071 inventory.layout.pointer_encoding !=
1073 return absl::InvalidArgumentError(
1074 "Repack-all currently supports only fixed-bank pot-item streams");
1075 }
1076 if (inventory.layout.pointer_count != kNumberOfRooms) {
1077 return absl::InvalidArgumentError(absl::StrFormat(
1078 "Pot-item repack requires exactly %d pointers", kNumberOfRooms));
1079 }
1080 if (inventory.layout.allocation_ranges.size() != 1) {
1081 return absl::InvalidArgumentError(
1082 "Pot-item repack requires exactly one normalized allocation range");
1083 }
1084 if (inventory.streams.size() != inventory.layout.pointer_count) {
1085 return absl::FailedPreconditionError(
1086 "Pot-item inventory does not cover every pointer-table entry");
1087 }
1088
1089 std::vector<std::vector<uint8_t>> logical_streams(
1090 inventory.layout.pointer_count);
1091 std::vector<bool> seen_rooms(inventory.layout.pointer_count, false);
1092 for (const DungeonStreamRecord& record : inventory.streams) {
1093 if (!record.valid || record.room_id >= inventory.layout.pointer_count ||
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");
1098 }
1099 seen_rooms[record.room_id] = true;
1100 logical_streams[record.room_id] = record.encoded_stream;
1101 }
1102 if (std::find(seen_rooms.begin(), seen_rooms.end(), false) !=
1103 seen_rooms.end()) {
1104 return absl::FailedPreconditionError(
1105 "Pot-item inventory is missing a room stream snapshot");
1106 }
1107
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) {
1112 const DungeonStreamReplacement& replacement = replacements[i];
1113 if (replacement.room_id >= inventory.layout.pointer_count) {
1114 return absl::OutOfRangeError("Replacement room ID is out of range");
1115 }
1116 if (i > 0 && replacements[i - 1].room_id == replacement.room_id) {
1117 return absl::InvalidArgumentError("Replacement room IDs must be unique");
1118 }
1119 const absl::Status status = ValidateCompleteEncodedStream(
1120 inventory.layout.kind, replacement.encoded_stream);
1121 if (!status.ok()) {
1122 return absl::InvalidArgumentError(
1123 absl::StrFormat("Room %u replacement is invalid: %s",
1124 replacement.room_id, status.message()));
1125 }
1126 logical_streams[replacement.room_id] = replacement.encoded_stream;
1127 }
1128
1129 const uint32_t bank_begin = SnesToPc(
1130 (static_cast<uint32_t>(inventory.layout.pointer_bank) << 16) | 0x8000u);
1131 const uint32_t bank_end = bank_begin + kLoRomBankSize;
1132 uint64_t available_bytes = 0;
1133 for (const DungeonStreamPcRange& range : inventory.layout.allocation_ranges) {
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",
1138 range.begin, range.end, inventory.layout.pointer_bank));
1139 }
1140 available_bytes += static_cast<uint64_t>(range.end) - range.begin;
1141 }
1142
1143 struct RepackGroup {
1144 uint32_t owner_room = 0;
1145 std::vector<uint8_t> bytes;
1146 std::vector<uint32_t> room_ids;
1147 };
1148 std::map<std::vector<uint8_t>, std::vector<uint32_t>> rooms_by_payload;
1149 for (uint32_t room_id = 0; room_id < inventory.layout.pointer_count;
1150 ++room_id) {
1151 rooms_by_payload[logical_streams[room_id]].push_back(room_id);
1152 }
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)});
1159 }
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));
1164 }
1165 std::sort(groups.begin(), groups.end(), [](const auto& a, const auto& b) {
1166 return a.owner_room < b.owner_room;
1167 });
1168
1170 plan.layout = inventory.layout;
1171 plan.source_size = inventory.source_size;
1172 plan.source_crc32 = inventory.source_crc32;
1174 plan.payload_writes.reserve(groups.size());
1175 plan.pointer_writes.reserve(inventory.layout.pointer_count);
1176
1177 std::vector<uint32_t> target_by_room(inventory.layout.pointer_count, 0);
1178 uint32_t next_address = inventory.layout.allocation_ranges.front().begin;
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;
1184 }
1185 next_address += static_cast<uint32_t>(group.bytes.size());
1186 }
1187
1188 const uint32_t pointer_width =
1189 PointerWidth(inventory.layout.pointer_encoding);
1190 for (uint32_t room_id = 0; room_id < inventory.layout.pointer_count;
1191 ++room_id) {
1192 ASSIGN_OR_RETURN(std::vector<uint8_t> pointer,
1193 EncodePointer(inventory.layout, target_by_room[room_id]));
1194 plan.pointer_writes.push_back(
1195 {room_id, inventory.layout.pointer_table_pc + room_id * pointer_width,
1196 std::move(pointer)});
1197 }
1198 return plan;
1199}
1200
1202 const DungeonStreamWritePlan& plan) {
1203 if (rom == nullptr || !rom->is_loaded()) {
1204 return absl::InvalidArgumentError("ROM not loaded");
1205 }
1206 if (rom->size() != plan.source_size) {
1207 return absl::AbortedError("Dungeon stream plan source size is stale");
1208 }
1209 const uint32_t current_crc =
1210 util::CalculateCrc32(rom->data(), static_cast<size_t>(rom->size()));
1211 if (current_crc != plan.source_crc32) {
1212 return absl::AbortedError(absl::StrFormat(
1213 "Dungeon stream plan is stale (source CRC %08X, current CRC %08X)",
1214 plan.source_crc32, current_crc));
1215 }
1216
1217 auto inventory = InventoryDungeonStreams(*rom, plan.layout);
1218 if (!inventory.ok()) {
1219 return inventory.status();
1220 }
1221 auto validation = ValidatePlanAgainstInventory(plan, *inventory);
1222 if (!validation.ok()) {
1223 return validation;
1224 }
1225
1226 yaze::rom::WriteFence write_fence;
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");
1234 }
1236 write_fence.Allow(write.address, static_cast<uint32_t>(end), label));
1237 }
1238 return absl::OkStatus();
1239 };
1240 RETURN_IF_ERROR(allow_writes(plan.payload_writes, "DungeonStreamPayload"));
1241 RETURN_IF_ERROR(allow_writes(plan.pointer_writes, "DungeonStreamPointer"));
1242 RETURN_IF_ERROR(allow_writes(plan.auxiliary_pointer_writes,
1243 "DungeonStreamAuxiliaryPointer"));
1244 yaze::rom::ScopedWriteFence write_scope(rom, &write_fence);
1245
1246 const std::vector<uint8_t> snapshot = rom->vector();
1247 const bool was_dirty = rom->dirty();
1248 auto rollback = [&]() {
1249 rom->mutable_vector() = snapshot;
1250 rom->set_dirty(was_dirty);
1251 };
1252
1253 for (const auto& write : plan.payload_writes) {
1254 auto status =
1255 rom->WriteVector(static_cast<int>(write.address), write.bytes);
1256 if (!status.ok()) {
1257 rollback();
1258 return status;
1259 }
1260 }
1261 for (const auto& write : plan.pointer_writes) {
1262 auto status =
1263 rom->WriteVector(static_cast<int>(write.address), write.bytes);
1264 if (!status.ok()) {
1265 rollback();
1266 return status;
1267 }
1268 }
1269 for (const auto& write : plan.auxiliary_pointer_writes) {
1270 auto status =
1271 rom->WriteVector(static_cast<int>(write.address), write.bytes);
1272 if (!status.ok()) {
1273 rollback();
1274 return status;
1275 }
1276 }
1277 return absl::OkStatus();
1278}
1279
1280} // namespace yaze::zelda3
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
void set_dirty(bool dirty)
Definition rom.h:146
const auto & vector() const
Definition rom.h:155
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:658
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:526
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool dirty() const
Definition rom.h:145
auto & mutable_vector()
Definition rom.h:156
bool is_loaded() const
Definition rom.h:144
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:533
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Definition write_fence.h:32
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
uint32_t CalculateCrc32(const uint8_t *data, size_t size)
Definition rom_hash.cc:62
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 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)
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)
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)
absl::Status ValidateCompleteEncodedStream(DungeonStreamKind kind, const std::vector< uint8_t > &encoded)
Zelda 3 specific classes and functions.
constexpr int kDoorPointers
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)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< DungeonStreamPcRange > allocatable_free_intervals
std::vector< DungeonStreamPcRange > occupied_intervals
std::vector< DungeonStreamPcRange > free_intervals
std::vector< DungeonStreamAliasGroup > aliases
std::vector< DungeonStreamIssue > issues
std::vector< DungeonStreamOverlap > overlaps
std::vector< DungeonStreamRecord > streams
std::vector< DungeonStreamPcRange > allocation_ranges
std::vector< DungeonStreamPcRange > data_ranges
std::vector< DungeonStreamWrite > pointer_writes
std::vector< DungeonStreamWrite > payload_writes
std::vector< DungeonStreamWrite > auxiliary_pointer_writes