9#include "absl/status/status.h"
10#include "absl/strings/str_cat.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_join.h"
13#include "nlohmann/json.hpp"
40 0x9C, 0x9D, 0x9E, 0x9F};
42 0x93, 0x94, 0x95, 0x96, 0x97, 0x9C, 0x9D, 0x9E, 0x9F};
45 return ((snes & 0x7F0000) >> 1) | (snes & 0x7FFF);
49 return ((pc << 1) & 0x7F0000) | (pc & 0x7FFF) | 0x8000;
53 return absl::StrFormat(
"$%02X:%04X", (snes >> 16) & 0xFF, snes & 0xFFFF);
56std::string HexByte(
int value) {
57 return absl::StrFormat(
"0x%02X", value);
61 return absl::StrFormat(
"$%02X", area);
64std::string RangeText(
int first,
int last) {
65 return absl::StrFormat(
"$%02X-$%02X", first, last);
69 return asm_version != 0xFF && asm_version != 0x00 && asm_version >= 3;
76bool InRom(
const Rom& rom, uint32_t pc,
size_t length) {
77 return static_cast<size_t>(pc) + length <= rom.
size();
81 const uint8_t* d = rom.
data();
82 return d[pc] | (d[pc + 1] << 8) | (d[pc + 2] << 16);
86 const uint8_t* d = rom.
data();
87 return static_cast<uint16_t
>(d[pc] | (d[pc + 1] << 8));
90std::string
HexBytes(
const uint8_t* data,
size_t count) {
92 for (
size_t i = 0; i < count; ++i) {
96 out += absl::StrFormat(
"%02X", data[i]);
102 uint32_t pc,
int entry_bytes,
int first_area,
103 int last_area, std::string past_end,
104 std::string note =
"") {
115 table.
past_end = std::move(past_end);
116 table.
note = std::move(note);
121 uint32_t snes,
int entry_bytes,
int first_area,
122 int last_area, std::string past_end,
123 std::string note =
"") {
134 table.
past_end = std::move(past_end);
135 table.
note = std::move(note);
140 std::vector<AreaTableInfo> t;
142 t.push_back(
RomTable(
"map32_high",
"Map32 pointers (high bytes)",
"map",
144 "Map32 pointers (low bytes) at $02:FB2D",
145 "TailMapExpansion.asm copies this table to $28:A400 "
146 "with 192 entries"));
147 t.push_back(
RomTable(
"map32_low",
"Map32 pointers (low bytes)",
"map",
149 "code (LoadSubscreenOverlay) at $02:FD0D",
150 "TailMapExpansion.asm copies this table to $28:A640 "
151 "with 192 entries"));
153 0xBF,
"Map32 pointers (high bytes) at $02:F94D",
154 "0 small, 1 large, 2 wide, 3 tall"));
156 0x00, 0x9F,
"Pool ByScreen1 at $28:8A38"));
159 "area_gfx",
"Area graphics ($0AA2, GFX0AA2ValsOW)",
"gfx", 0x7C9C, 1,
160 0x00, 0x9F,
"the old palette table at $00:FD3C (unused after ZS v3)"));
162 "palette_set",
"Area palette set (OverworldPalettesScreenToSet_New)",
163 "gfx", 0x4C635, 1, 0x00, 0xBF,
"",
"yaze writes entries $00-$9F only"));
164 t.push_back(
RomTable(
"pool_bg_color",
"BG color",
"gfx", 0x140000, 2, 0x00,
165 0x9F,
"Pool enable flags at $28:8140"));
166 t.push_back(
RomTable(
"pool_main_palette",
"Main palette",
"gfx", 0x140160, 1,
167 0x00, 0x9F,
"Pool mosaic table at $28:8200"));
168 t.push_back(
RomTable(
"pool_mosaic",
"Mosaic",
"gfx", 0x140200, 1, 0x00, 0x9F,
169 "Pool animated GFX table at $28:82A0"));
170 t.push_back(
RomTable(
"pool_animated_gfx",
"Animated GFX",
"gfx", 0x1402A0, 1,
171 0x00, 0x9F,
"Pool subscreen overlay table at $28:8340"));
172 t.push_back(
RomTable(
"pool_subscreen_overlay",
"Subscreen overlay",
"gfx",
174 "Pool tile GFX groups at $28:8480"));
175 t.push_back(
RomTable(
"pool_tile_gfx_groups",
"Tile GFX groups (8 sheets)",
176 "gfx", 0x140480, 8, 0x00, 0x9F,
177 "Pool default GFX groups at $28:8980, then the parent "
179 t.push_back(
RomTable(
"sw_sprite_gfx",
"Special-world sprite GFX set",
"gfx",
180 0x166E1, 1, 0x80, 0x9F,
181 "special-world sprite palettes at $02:E701"));
183 "sw_sprite_palette",
"Special-world sprite palette",
"gfx", 0x16701, 1,
185 "Sprite_LoadGfxProperties copies 0x40 bytes to $7E:FDC0; nothing "
186 "checks that entries past the authored ones hold palette data"));
187 t.push_back(
RomTable(
"sw_entry_gfx",
188 "Special-exit entry GFX ($0AA2, LoadSpecialOverworld)",
189 "gfx", 0x16821, 1, 0x80, 0x8F,
190 "palette_prop_a at $02:E831, then palette_prop_b",
191 "read only when entering through a special exit"));
193 t.push_back(
RomTable(
"pool_by_screen_1",
"Transition ByScreen1 (right)",
194 "transition", 0x140A38, 2, 0x00, 0x9F,
195 "Pool ByScreen2 at $28:8B78"));
196 t.push_back(
RomTable(
"pool_by_screen_2",
"Transition ByScreen2 (left)",
197 "transition", 0x140B78, 2, 0x00, 0x9F,
198 "Pool ByScreen3 at $28:8CB8"));
199 t.push_back(
RomTable(
"pool_by_screen_3",
"Transition ByScreen3 (down)",
200 "transition", 0x140CB8, 2, 0x00, 0x9F,
201 "Pool ByScreen4 at $28:8DF8"));
202 t.push_back(
RomTable(
"pool_by_screen_4",
"Transition ByScreen4 (up)",
203 "transition", 0x140DF8, 2, 0x00, 0x9F,
204 "Pool transition position Y at $28:8F38"));
205 t.push_back(
RomTable(
"pool_trans_pos_y",
"Transition position Y",
206 "transition", 0x140F38, 2, 0x00, 0x9F,
207 "Pool transition position X at $28:9078"));
208 t.push_back(
RomTable(
"pool_trans_pos_x",
"Transition position X",
209 "transition", 0x141078, 2, 0x00, 0x9F,
210 "Pool transition target north at $28:91B8"));
211 t.push_back(
RomTable(
"pool_trans_target_north",
"Transition target north",
212 "transition", 0x1411B8, 2, 0x00, 0x9F,
213 "Pool transition target west at $28:92F8"));
214 t.push_back(
RomTable(
"pool_trans_target_west",
"Transition target west",
215 "transition", 0x1412F8, 2, 0x00, 0x9F,
216 "Pool sprite pointers (phase 0) at $28:9438"));
218 t.push_back(
RomTable(
"pool_sprites_phase_0",
"Sprite list pointers, phase 0",
220 "Pool sprite pointers (phase 1) at $28:9578"));
221 t.push_back(
RomTable(
"pool_sprites_phase_1",
"Sprite list pointers, phase 1",
223 "Pool sprite pointers (phase 2) at $28:96B8"));
224 t.push_back(
RomTable(
"pool_sprites_phase_2",
"Sprite list pointers, phase 2",
226 "Pool sign messages at $28:97F8 (read as bank $09 "
227 "sprite pointers)"));
229 t.push_back(
RomTable(
"pool_sign_message",
"Sign message",
"text", 0x1417F8, 2,
231 "8 bytes of padding, then ZS code at $28:9940"));
233 t.push_back(
RomTable(
"music_lw",
"Music, light world (per game state)",
234 "music", 0x14303, 1, 0x00, 0x3F,
235 "the next game state's copy (+$40)",
236 "4 copies, one per game state"));
237 t.push_back(
RomTable(
"music_dw_sw",
"Music, dark and special world",
"music",
238 0x14403, 1, 0x40, 0x9F,
239 "code (AdjustOverworldAmbiance) at $02:C463",
240 "copied to WRAM $7F:5B40+ with the light-world table"));
242 t.push_back(
RomTable(
"bomb_doors",
"Bombable door locations",
"items",
243 0x12644, 2, 0x00, 0x9F,
244 "hidden item pointers at $02:A784"));
245 t.push_back(
RomTable(
"hidden_items",
"Hidden item pointers",
"items", 0x12784,
247 "data after the table at "
250 t.push_back(
RamTable(
"area_flags",
"Overworld area flags (SRAM)",
"sram",
251 0x7EF280, 1, 0x00, 0x7F,
252 "$7E:F300+, save data past the 0x80-byte area block",
253 "overlay and bomb-door bits are set for every area "
259 std::vector<AreaTableInfo> t;
260 t.push_back(
RomTable(
"map32_high",
"Map32 pointers (high bytes)",
"map",
262 "Map32 pointers (low bytes) at $02:FB2D"));
263 t.push_back(
RomTable(
"map32_low",
"Map32 pointers (low bytes)",
"map",
265 "code (LoadSubscreenOverlay) at $02:FD0D"));
266 t.push_back(
RomTable(
"area_size",
"Area size (vanilla: 0 large, 1 small)",
268 "Map32 pointers (high bytes) at $02:F94D"));
269 t.push_back(
RomTable(
"parent",
"Parent area (Overworld_ActualScreenID)",
271 "OverworldScreenTilemapChange at $02:A62C",
272 "dark world reads the light-world entry and adds $40; "
273 "special-world parents are fixed in code"));
274 t.push_back(
RomTable(
"area_gfx",
"Area graphics ($0AA2, GFXAA2ValsOW)",
"gfx",
275 0x7C9C, 1, 0x00, 0x7F,
276 "the area palette table at $00:FD1C"));
277 t.push_back(
RomTable(
"area_palette",
278 "Area palette (OverworldPalettesScreen"
280 "gfx", 0x7D1C, 1, 0x00, 0x87,
281 "code (ResetStarTileGraphics) at $00:FDA4"));
282 t.push_back(
RomTable(
"sw_sprite_gfx",
"Special-world sprite GFX ($0AA3)",
283 "gfx", 0x16811, 1, 0x80, 0x8F,
284 "special-world area GFX at $02:E821"));
285 t.push_back(
RomTable(
"sw_entry_gfx",
"Special-world area GFX ($0AA2)",
"gfx",
286 0x16821, 1, 0x80, 0x8F,
"palette_prop_a at $02:E831"));
287 t.push_back(
RomTable(
"sw_palette_a",
"Special-world palette (prop a)",
"gfx",
288 0x16831, 1, 0x80, 0x8F,
"palette_prop_b at $02:E841"));
289 t.push_back(
RomTable(
"sw_palette_b",
"Special-world palette (prop b)",
"gfx",
290 0x16841, 1, 0x80, 0x8F,
291 "code (LoadSpecialOverworld) at $02:E851"));
292 t.push_back(
RomTable(
"sign_message",
"Sign message (Overworld_SignText)",
293 "text", 0x3F51D, 2, 0x00, 0x8F,
294 "data after the table at $07:F63D"));
295 t.push_back(
RomTable(
"music_lw",
"Music, light world (per game state)",
296 "music", 0x14303, 1, 0x00, 0x3F,
297 "the next game state's copy (+$40)",
298 "4 copies, one per game state"));
299 t.push_back(
RomTable(
"music_dw_sw",
"Music, dark and special world",
"music",
300 0x14403, 1, 0x40, 0x9F,
301 "code (AdjustOverworldAmbiance) at $02:C463"));
302 t.push_back(
RomTable(
"hidden_items",
"Hidden item pointers",
"items", 0xDC2F9,
303 2, 0x00, 0x7F,
"hidden item data"));
304 t.push_back(
RamTable(
"area_flags",
"Overworld area flags (SRAM)",
"sram",
305 0x7EF280, 1, 0x00, 0x7F,
306 "$7E:F300+, save data past the 0x80-byte area block"));
329 if (area == 0x81 || area == 0x82 || area == 0x89 || area == 0x8A) {
337 const uint32_t high_entry = high_table_pc + 3 * index;
338 const uint32_t low_entry = low_table_pc + 3 * index;
339 if (!
InRom(rom, high_entry, 3) || !
InRom(rom, low_entry, 3)) {
344 auto scan = [&rom](uint32_t snes) {
347 if (pc < rom.
size()) {
351 result.
error =
"pointer is past the end of the ROM";
363 std::set<uint16_t> ids;
365 ids.insert(
static_cast<uint16_t
>((area->
high.
decoded[i] << 8) |
376 if (pointer < 0x8000) {
381 while (pc < rom.
size()) {
382 if (rom.
data()[pc] == 0xFF) {
394std::string
EvidenceCount(
int count,
const char* singular,
const char* plural) {
395 return absl::StrFormat(
"%d %s", count, count == 1 ? singular : plural);
399 std::vector<std::string> parts;
400 parts.reserve(areas.size());
401 for (
int area : areas) {
404 return absl::StrJoin(parts,
",");
443 std::vector<AreaTableInfo> tables =
444 IsZsV3(asm_version) ? ZsCustomV3Tables() : VanillaTables();
445 for (
auto& table : tables) {
446 if (table.id.rfind(
"sw_", 0) == 0) {
447 table.applies_first = 0x80;
448 }
else if (table.id ==
"music_lw") {
449 table.applies_last = 0x3F;
450 }
else if (table.id ==
"music_dw_sw") {
451 table.applies_first = 0x40;
461 scan.
snes = LoRomPcToSnes(pc);
464 scan.
error =
"stream starts past the end of the data";
468 std::vector<uint8_t>& out = scan.
decoded;
469 auto need = [&](
size_t count) {
470 return src + count <= size;
474 scan.
error =
"ran past the end of the data before $FF";
477 const uint8_t header = data[src++];
478 if (header == 0xFF) {
482 int command = header >> 5;
486 scan.
error =
"ran past the end of the data in a long header";
489 command = (header >> 2) & 7;
490 length = ((header & 0x03) << 8) | data[src++];
492 length = header & 0x1F;
502 out.insert(out.end(), data + src, data + src + length);
510 out.insert(out.end(), length, data[src++]);
517 const uint8_t a = data[src];
518 const uint8_t b = data[src + 1];
520 for (
int i = 0; i < length; ++i) {
521 out.push_back((i & 1) ? b : a);
530 uint8_t value = data[src++];
531 for (
int i = 0; i < length; ++i) {
532 out.push_back(value++);
541 size_t offset = (
static_cast<size_t>(data[src]) << 8) | data[src + 1];
543 for (
int i = 0; i < length; ++i) {
544 if (offset >= out.size()) {
545 scan.
error =
"copy offset points past the decoded bytes";
549 out.push_back(out[offset++]);
555 if (scan.
error.empty()) {
556 scan.
error =
"ran past the end of the data inside a command";
560 if (out.size() > max_output) {
561 scan.
error = absl::StrFormat(
"decoded more than %d bytes without $FF",
562 static_cast<int>(max_output));
573 std::vector<std::string> ids;
574 for (
const auto& table :
tables) {
576 ids.push_back(table.id);
584 if (rom ==
nullptr || !rom->
is_loaded()) {
585 return absl::FailedPreconditionError(
"ROM not loaded");
587 if (rom->
size() < 0x80000) {
588 return absl::FailedPreconditionError(
589 absl::StrFormat(
"ROM is %d bytes; an ALTTP ROM is at least 512 KB",
590 static_cast<int>(rom->
size())));
594 report.
rom_name = std::filesystem::path(rom->
filename()).filename().string();
598 report.
catalogue = zs_v3 ?
"zscustom_v3" :
"vanilla";
601 const uint32_t marker_pc = LoRomSnesToPc(plan.
marker_snes);
611 area.
name = it->second;
613 for (
const auto& table : report.
tables) {
614 if (table.Misses(
id)) {
621 const uint32_t pc = kPoolParentPc + id;
622 if (pc < rom->size()) {
629 }
else if (
id < 0x80) {
630 const int lw =
id & 0x3F;
631 const uint8_t parent = rom->
data()[kVanillaParentPc + lw];
632 area.
parent =
id < 0x40 ? parent : parent + 0x40;
633 }
else if (
id <= 0x9F) {
634 area.
parent = VanillaSpecialParent(
id);
639 if (zs_v3 ||
id < 0x80) {
640 area.
size = DecodeAreaSize(rom->
data()[kAreaSizePc +
id], zs_v3);
652 }
else if (
id <= 0x9F) {
654 ScanAreaMap(*rom, kMap32HighPc, kMap32LowPc,
id, &area);
657 for (
int lower = 0; lower < id; ++lower) {
671 for (
int id = 0;
id <= 0x9F; ++id) {
676 const uint16_t overlay = ReadWord(*rom, kPoolOverlayPc + 2 *
id);
678 report.
areas[overlay].overlay_users.push_back(
id);
681 for (
int overlay : kZsCodeOverlayIds) {
682 report.
areas[overlay].overlay_named_in_code =
true;
685 for (
int overlay : kVanillaCodeOverlayIds) {
686 report.
areas[overlay].overlay_named_in_code =
true;
693 for (
const auto& entrance : *entrances) {
696 if (entrance.map_pos_ != 0xFFFF &&
698 report.
areas[entrance.map_id_].entrances++;
702 if (
auto holes =
LoadHoles(rom); holes.ok()) {
703 for (
const auto& hole : *holes) {
705 report.
areas[hole.map_id_].holes++;
709 if (
auto exits =
LoadExits(rom); exits.ok()) {
710 for (
const auto& exit : *exits) {
712 report.
areas[exit.map_id_].exits++;
720 for (
int id = 0;
id <= 0x9F; ++id) {
721 for (uint32_t table_pc : kPoolSpritePointerPcs) {
722 const uint32_t entry = table_pc + 2 * id;
723 if (!InRom(*rom, entry, 2)) {
727 const int count = CountSpriteList(*rom, ReadWord(*rom, entry), &valid);
729 report.
areas[id].sprites += count;
736 for (
auto& area : report.
areas) {
737 if (area.unique_tile32 > 1) {
738 area.evidence.push_back(
739 absl::StrFormat(
"map data (%d unique tile32)", area.unique_tile32));
741 const bool multi_area =
742 area.parent.has_value() &&
745 area.evidence.push_back(absl::StrFormat(
"%s area, parent %s",
747 AreaText(*area.parent)));
749 if (area.entrances > 0) {
750 area.evidence.push_back(
751 EvidenceCount(area.entrances,
"entrance",
"entrances"));
753 if (area.holes > 0) {
754 area.evidence.push_back(EvidenceCount(area.holes,
"hole",
"holes"));
756 if (area.exits > 0) {
757 area.evidence.push_back(EvidenceCount(area.exits,
"exit",
"exits"));
759 if (area.sprites > 0) {
760 area.evidence.push_back(EvidenceCount(area.sprites,
"sprite",
"sprites"));
762 if (!area.overlay_users.empty()) {
763 area.evidence.push_back(
764 absl::StrCat(
"subscreen overlay of ", JoinAreas(area.overlay_users)));
766 if (area.overlay_named_in_code) {
767 area.evidence.push_back(
"overlay ID named in code");
769 area.used = !area.evidence.empty();
772 const std::array<std::pair<const char*, std::pair<int, int>>, 4> worlds = {
773 {{
"light", {0x00, 0x3F}},
774 {
"dark", {0x40, 0x7F}},
775 {
"special", {0x80, 0x9F}},
776 {
"special_tail", {0xA0, 0xBF}}}};
777 for (
const auto& [key, range] : worlds) {
782 for (
int id = range.first;
id <= range.second; ++id) {
795 report.
summary.push_back(summary);
811 return "already_applied";
829 std::vector<std::string> lines;
830 for (
const auto& finding :
findings) {
831 if (finding.blocking) {
832 lines.push_back(absl::StrFormat(
836 return absl::StrJoin(lines,
"\n");
842 const char*
id,
const char* label, uint32_t snes,
846 region.
label = label;
851 if (region.
pc < rom.
size()) {
852 const size_t available = std::min<size_t>(length, rom.
size() - region.
pc);
853 const uint8_t* data = rom.
data() + region.
pc;
854 for (
size_t i = 0; i < available; ++i) {
862 if (
auto owner = annotator.
ownership(snes); owner.has_value()) {
863 region.
owner = owner->first;
873 const std::vector<uint8_t>& planned,
882 std::vector<uint8_t> image;
883 const size_t window = 0x10000;
884 if (pc < rom.
size()) {
885 const size_t available = std::min<size_t>(window, rom.
size() - pc);
886 image.assign(rom.
data() + pc, rom.
data() + pc + available);
890 if (image.size() < planned.size()) {
891 image.resize(planned.size(), 0x00);
893 std::copy(planned.begin(), planned.end(), image.begin());
917 const uint32_t marker_pc = LoRomSnesToPc(plan.
marker_snes);
918 if (marker_pc < rom.
size()) {
925 check.
regions.push_back(MakeRegion(rom, annotator,
"marker",
927 check.
regions.push_back(MakeRegion(rom, annotator,
"high_table",
928 "Map32 pointers, high (192 entries)",
930 check.
regions.push_back(MakeRegion(rom, annotator,
"low_table",
931 "Map32 pointers, low (192 entries)",
933 check.
regions.push_back(MakeRegion(
934 rom, annotator,
"blank_high",
"Blank map stream, high bytes",
937 MakeRegion(rom, annotator,
"blank_low",
"Blank map stream, low bytes",
943 check.
streams.push_back(CheckBlankStream(rom,
"blank_low",
950 operand.
pc = LoRomSnesToPc(operand.
snes);
953 operand.
in_rom = operand.
pc >= 1 && InRom(rom, operand.
pc, 3);
955 operand.
state =
"missing";
958 operand.
current = ReadLong(rom, operand.
pc);
960 operand.
state =
"vanilla";
962 operand.
state =
"patched";
964 operand.
state =
"other";
972 for (uint32_t pc = marker_pc; pc > kZsFunctionSpacePc; --pc) {
973 if (rom.
data()[pc - 1] != 0) {
983 for (
const auto& table : tables) {
989 auto add = [&check](
TailCheckScope scope,
const char* id, std::string message,
991 check.
findings.push_back({scope, id, std::move(message), blocking});
997 absl::StrFormat(
"ZSCustomOverworld v3 is required; this ROM reports "
998 "ASM version 0x%02X at $28:8145",
1003 for (
const auto& region : check.
regions) {
1004 if (region.nonzero_bytes > 0) {
1005 std::string message = absl::StrFormat(
1006 "the patch would overwrite %d non-zero byte%s at %s (%s)",
1007 region.nonzero_bytes, region.nonzero_bytes == 1 ?
"" :
"s",
1008 SnesText(region.snes), region.label);
1009 if (!region.symbol.empty()) {
1010 absl::StrAppend(&message,
", symbol ", region.symbol);
1017 check.
regions.back().length)) {
1020 "the ROM is %d bytes; the patch writes up to PC "
1021 "0x%06X, so asar would expand it",
1027 for (
const auto& region : check.
regions) {
1031 if (
auto owner = annotator.
ownership(region.snes);
1032 owner.has_value() && owner->second) {
1034 absl::StrFormat(
"hack manifest: %s (%s) is %s", SnesText(region.snes),
1035 region.label, owner->first),
1039 for (
const auto& operand : check.
operands) {
1040 if (operand.state ==
"other") {
1042 absl::StrFormat(
"operand at %s holds %s, not the vanilla %s; "
1043 "another patch changed the map loader",
1044 SnesText(operand.snes), SnesText(operand.current),
1045 SnesText(operand.vanilla)),
1047 }
else if (operand.state ==
"patched" && !check.
marker_present) {
1049 absl::StrFormat(
"operand at %s already points to %s but the marker "
1051 SnesText(operand.snes), SnesText(operand.current),
1054 }
else if (operand.state ==
"missing") {
1056 absl::StrFormat(
"operand at %s is past the end of the ROM",
1057 SnesText(operand.snes)),
1061 if (
auto owner = annotator.
ownership(operand.snes);
1062 owner.has_value() && owner->second) {
1064 absl::StrFormat(
"hack manifest: operand at %s is %s",
1065 SnesText(operand.snes), owner->first),
1072 absl::StrFormat(
"ZS code ends at %s, %d bytes before the marker at "
1073 "%s; asar does not reserve %s-%s, so growing ZS code "
1074 "would run into the relocated tables",
1083 for (
const auto& stream : check.
streams) {
1087 std::string message;
1088 if (!stream.terminated_within_stream && stream.stream_bytes > 0) {
1089 message = absl::StrFormat(
1090 "blank map stream at %s is %d bytes with no $FF terminator; "
1091 "decoding reads past it and stops at %s after %d decoded bytes",
1092 SnesText(stream.snes), stream.stream_bytes, stream.stopped_at,
1093 stream.decoded_bytes);
1094 if (!stream.error.empty()) {
1095 absl::StrAppend(&message,
" (", stream.error,
")");
1098 message = absl::StrFormat(
1099 "blank map stream at %s decodes to %d bytes; a map needs %d",
1100 SnesText(stream.snes), stream.decoded_bytes, kMap32PerScreen);
1104 std::vector<std::string> unpatched_tables;
1106 if (
id !=
"map32_high" &&
id !=
"map32_low") {
1107 unpatched_tables.push_back(
id);
1110 if (!unpatched_tables.empty()) {
1113 "the patch extends only the map32 pointers; %d other per-area "
1114 "tables stop before $A0, so a tail area reads the data after "
1115 "each of them (%s)",
1116 static_cast<int>(unpatched_tables.size()),
1117 absl::StrJoin(unpatched_tables,
", ")),
1124 "after the patch the game reads map pointers from $28:A400/$28:A640, "
1125 "but yaze's overworld save writes 0x1794D/0x17B2D, so the next save "
1126 "leaves maps $00-$9F stale in game",
1131 absl::StrFormat(
"yaze loads and saves %d maps ($00-$%02X); maps "
1132 "$A0-$BF cannot be edited",
1137 bool blocking =
false;
1138 bool rom_blocking =
false;
1139 for (
const auto& finding : check.
findings) {
1140 blocking = blocking || finding.blocking;
1141 rom_blocking = rom_blocking ||
1157 return absl::OkStatus();
1159 return absl::FailedPreconditionError(absl::StrCat(
1160 "Refusing to apply TailMapExpansion.asm: the compatibility check says "
1163 "\nRun `z3ed overworld-allocation --rom <rom> --tail-check` for the "
1176 j[
"label"] = table.
label;
1177 j[
"group"] = table.
group;
1180 ? absl::StrFormat(
"%s (PC 0x%06X)",
SnesText(table.
snes), table.
pc)
1187 if (!table.
note.empty()) {
1188 j[
"note"] = table.
note;
1196 j[
"pc"] = absl::StrFormat(
"0x%06X", scan.
pc);
1200 if (!scan.
error.empty()) {
1201 j[
"error"] = scan.
error;
1208 j[
"id"] = HexByte(area.
area);
1210 if (!area.
name.empty()) {
1211 j[
"name"] = area.
name;
1213 j[
"used"] = area.
used;
1234 j[
"holes"] = area.
holes;
1235 j[
"exits"] = area.
exits;
1246 j[
"marker"] = {{
"address",
"$28:A3FF"},
1250 for (
const auto& region : check.
regions) {
1252 r[
"id"] = region.id;
1253 r[
"label"] = region.label;
1254 r[
"address"] =
SnesText(region.snes);
1255 r[
"pc"] = absl::StrFormat(
"0x%06X", region.pc);
1256 r[
"length"] = region.length;
1257 r[
"in_rom"] = region.in_rom;
1258 r[
"nonzero_bytes"] = region.nonzero_bytes;
1259 r[
"first_bytes"] = region.first_bytes;
1260 if (!region.owner.empty()) {
1261 r[
"owner"] = region.owner;
1263 if (!region.symbol.empty()) {
1264 r[
"symbol"] = region.symbol;
1266 regions.push_back(r);
1268 j[
"regions"] = regions;
1270 for (
const auto& stream : check.
streams) {
1272 s[
"id"] = stream.id;
1273 s[
"address"] =
SnesText(stream.snes);
1274 s[
"stream_bytes"] = stream.stream_bytes;
1275 s[
"terminated"] = stream.terminated;
1276 s[
"terminated_within_stream"] = stream.terminated_within_stream;
1277 s[
"decoded_bytes"] = stream.decoded_bytes;
1278 s[
"consumed_bytes"] = stream.consumed_bytes;
1279 s[
"stopped_at"] = stream.stopped_at;
1280 if (!stream.error.empty()) {
1281 s[
"error"] = stream.error;
1283 s[
"valid"] = stream.valid;
1284 streams.push_back(s);
1286 j[
"blank_streams"] = streams;
1288 for (
const auto& operand : check.
operands) {
1290 o[
"address"] =
SnesText(operand.snes);
1291 o[
"pc"] = absl::StrFormat(
"0x%06X", operand.pc);
1292 o[
"opcode"] = HexByte(operand.opcode);
1293 o[
"current"] =
SnesText(operand.current);
1294 o[
"vanilla"] =
SnesText(operand.vanilla);
1295 o[
"patched"] =
SnesText(operand.patched);
1296 o[
"state"] = operand.state;
1297 operands.push_back(o);
1299 j[
"operands"] = operands;
1304 j[
"zs_code_end"] =
nullptr;
1305 j[
"free_bytes_before_marker"] =
nullptr;
1310 for (
const auto& finding : check.
findings) {
1313 {
"message", finding.message}};
1314 (finding.blocking ? blockers : warnings).push_back(f);
1316 j[
"blockers"] = blockers;
1317 j[
"warnings"] = warnings;
1344 ordered_json summary = ordered_json::array();
1345 for (
const auto& world : report.
summary) {
1346 summary.push_back({{
"world", world.key},
1347 {
"range", RangeText(world.first_area, world.last_area)},
1348 {
"used", world.used},
1349 {
"free", world.free},
1350 {
"free_with_map_data", world.free_with_map_data},
1351 {
"free_without_map_data", world.free_without_map_data}});
1353 j[
"summary"] = summary;
1355 ordered_json tables = ordered_json::array();
1356 for (
const auto& table : report.
tables) {
1357 tables.push_back(TableJson(table));
1359 j[
"tables"] = tables;
1360 ordered_json areas = ordered_json::array();
1361 for (
const auto& area : report.
areas) {
1362 areas.push_back(AreaJson(area));
1365 if (tail_check !=
nullptr) {
1366 j[
"tail_check"] = TailJson(*tail_check);
1374 std::ostringstream out;
1375 out << absl::StrFormat(
"Overworld allocation: %s (%d bytes)\n",
1377 static_cast<int>(report.
rom_size));
1378 out << absl::StrFormat(
1379 "Layout: ASM version 0x%02X, %s tables; TailMapExpansion marker %s\n\n",
1384 out << absl::StrFormat(
" %-13s %-8s %5s %5s %9s %11s\n",
"world",
"range",
1385 "used",
"free",
"free+map",
"free-no-map");
1386 for (
const auto& world : report.
summary) {
1387 out << absl::StrFormat(
" %-13s %-8s %5d %5d %9d %11d\n", world.key,
1388 RangeText(world.first_area, world.last_area),
1389 world.used, world.free, world.free_with_map_data,
1390 world.free_without_map_data);
1393 out <<
"\nPer-area tables (a read past the last entry returns the data "
1394 "after the table)\n";
1395 out << absl::StrFormat(
" %-24s %-19s %5s %-8s %-8s %s\n",
"table",
1396 "location",
"bytes",
"covers",
"read for",
1398 for (
const auto& table : report.
tables) {
1399 const std::string location =
1401 ? absl::StrFormat(
"%s 0x%06X", SnesText(table.snes), table.pc)
1402 : absl::StrFormat(
"%s RAM", SnesText(table.snes));
1403 out << absl::StrFormat(
" %-24s %-19s %5d %-8s %-8s %s\n", table.id,
1404 location, table.entry_bytes,
1405 RangeText(table.first_area, table.last_area),
1406 RangeText(table.applies_first, table.applies_last),
1407 table.past_end.empty() ?
"-" : table.past_end);
1410 out << absl::StrFormat(
1411 " %d of %d tables are read for $A0 but stop before it.\n",
1412 static_cast<int>(stopping.size()),
1413 static_cast<int>(report.
tables.size()));
1415 out <<
"\nAreas (map: data/blank/=dup/invalid/none; miss = tables read for "
1416 "the ID that have no entry for it)\n";
1417 out << absl::StrFormat(
1418 " %-4s %-7s %-4s %-6s %-5s %-7s %5s %4s %4s %4s %4s "
1420 "id",
"world",
"used",
"parent",
"size",
"map",
"tile32",
"ent",
"hole",
1421 "exit",
"spr",
"miss",
"evidence");
1422 for (
const auto& area : report.
areas) {
1423 std::string parent =
"--";
1424 if (area.parent.has_value()) {
1425 parent = AreaText(*area.parent);
1427 std::string evidence = absl::StrJoin(area.evidence,
"; ");
1428 if (!area.name.empty()) {
1429 evidence = evidence.empty()
1430 ? absl::StrCat(
"[", area.name,
"]")
1431 : absl::StrCat(
"[", area.name,
"] ", evidence);
1433 out << absl::StrFormat(
1434 " %-4s %-7s %-4s %-6s %-5s %-7s %5s %4d %4d %4d %4d %4d %s\n",
1438 area.unique_tile32 >= 0 ? absl::StrCat(area.unique_tile32) :
"-",
1439 area.entrances, area.holes, area.exits, area.sprites,
1440 static_cast<int>(area.uncovered_tables.size()),
1441 evidence.empty() ?
"-" : evidence);
1444 if (tail_check !=
nullptr) {
1446 out <<
"\nTailMapExpansion check "
1447 "(assets/patches/Overworld/TailMapExpansion.asm)\n";
1448 out << absl::StrFormat(
" Verdict: %s (ROM compatible: %s)\n",
1451 out << absl::StrFormat(
" Marker $28:A3FF: 0x%02X (%s)\n", c.
marker_value,
1454 out << absl::StrFormat(
1455 " ZS code ends at %s; %d bytes free before $28:A3FF\n",
1458 out <<
" ZS code end: not found (not a ZS v3 ROM)\n";
1460 out <<
" Target regions:\n";
1461 for (
const auto& region : c.
regions) {
1463 if (!region.symbol.empty()) {
1464 absl::StrAppend(&extra,
" symbol ", region.symbol);
1466 if (!region.owner.empty()) {
1467 absl::StrAppend(&extra,
" owner ", region.owner);
1469 out << absl::StrFormat(
1470 " %-10s %s PC 0x%06X %4d B %s non-zero %3d [%s]%s\n",
1471 region.id, SnesText(region.snes), region.pc, region.length,
1472 region.in_rom ?
"in ROM " :
"past end", region.nonzero_bytes,
1473 region.first_bytes, extra);
1475 out <<
" Blank streams (decoded as the game would read them after the "
1477 for (
const auto& stream : c.
streams) {
1478 out << absl::StrFormat(
1479 " %-10s %s %3d B written $FF in stream: %-3s decoded %d B, "
1480 "stopped at %s %s\n",
1481 stream.id, SnesText(stream.snes), stream.stream_bytes,
1482 stream.terminated_within_stream ?
"yes" :
"no", stream.decoded_bytes,
1483 stream.stopped_at, stream.valid ?
"valid" :
"INVALID");
1485 out <<
" Patched operands (LDA.l table,X):\n";
1486 for (
const auto& operand : c.
operands) {
1487 out << absl::StrFormat(
1488 " %s PC 0x%06X opcode 0x%02X now %s vanilla %s patch %s "
1490 SnesText(operand.snes), operand.pc, operand.opcode,
1491 SnesText(operand.current), SnesText(operand.vanilla),
1492 SnesText(operand.patched), operand.state);
1494 out <<
" Blockers:\n";
1496 for (
const auto& finding : c.
findings) {
1497 if (finding.blocking) {
1499 out << absl::StrFormat(
" [%s] %s: %s\n",
1507 out <<
" Warnings:\n";
1509 for (
const auto& finding : c.
findings) {
1510 if (!finding.blocking) {
1512 out << absl::StrFormat(
" [%s] %s: %s\n",
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
std::string AreaText(int area)
constexpr uint32_t kMap32LowPc
constexpr uint32_t kPoolOverlayPc
std::string HexBytes(const uint8_t *data, size_t count)
uint16_t ReadWord(const Rom &rom, uint32_t pc)
constexpr uint32_t kAreaSizePc
uint32_t LoRomSnesToPc(uint32_t snes)
constexpr int kMap32PerScreen
std::vector< AreaTableInfo > ZsCustomV3Tables()
std::vector< AreaTableInfo > VanillaTables()
AllocationAreaSize DecodeAreaSize(uint8_t value, bool zs_v3)
bool IsZsV3(uint8_t asm_version)
constexpr uint32_t kMap32HighPc
ordered_json TableJson(const AreaTableInfo &table)
std::string MapColumn(const AreaAllocation &area)
constexpr std::array< int, 9 > kVanillaCodeOverlayIds
constexpr uint32_t kAsmVersionPc
std::string JoinAreas(const std::vector< int > &areas)
constexpr uint32_t kZsFunctionSpacePc
AreaTableInfo RamTable(const char *id, const char *label, const char *group, uint32_t snes, int entry_bytes, int first_area, int last_area, std::string past_end, std::string note="")
AreaTableInfo RomTable(const char *id, const char *label, const char *group, uint32_t pc, int entry_bytes, int first_area, int last_area, std::string past_end, std::string note="")
int CountSpriteList(const Rom &rom, uint16_t pointer, bool *valid)
constexpr uint32_t kVanillaParentPc
TailStreamCheck CheckBlankStream(const Rom &rom, const char *id, uint32_t snes, const std::vector< uint8_t > &planned, bool applied)
TailCheckRegion MakeRegion(const Rom &rom, const TailCheckAnnotator &annotator, const char *id, const char *label, uint32_t snes, int length)
bool InRom(const Rom &rom, uint32_t pc, size_t length)
std::string EvidenceCount(int count, const char *singular, const char *plural)
constexpr std::array< uint32_t, 3 > kPoolSpritePointerPcs
ordered_json TailJson(const TailMapExpansionCheck &check)
constexpr std::array< int, 8 > kZsCodeOverlayIds
void ScanAreaMap(const Rom &rom, uint32_t high_table_pc, uint32_t low_table_pc, int index, AreaAllocation *area)
uint8_t ReadAsmVersion(const Rom &rom)
constexpr int kMaxSpritesPerList
constexpr uint32_t kOverworldSpriteBankPc
uint32_t LoRomPcToSnes(uint32_t pc)
std::string SnesText(uint32_t snes)
ordered_json StreamJson(const MapStreamScan &scan)
nlohmann::ordered_json ordered_json
int VanillaSpecialParent(int area)
constexpr uint32_t kPoolParentPc
ordered_json AreaJson(const AreaAllocation &area)
uint32_t ReadLong(const Rom &rom, uint32_t pc)
Zelda 3 specific classes and functions.
std::vector< AreaTableInfo > OverworldAreaTableCatalogue(uint8_t asm_version)
absl::StatusOr< std::vector< OverworldEntrance > > LoadEntrances(Rom *rom)
constexpr int kAllocationAreaCount
const char * AllocationAreaSizeName(AllocationAreaSize size)
absl::Status TailMapExpansionApplyGuard(const Rom &rom)
TailMapExpansionCheck CheckTailMapExpansion(const Rom &rom, const TailMapExpansionPlan &plan, const TailCheckAnnotator &annotator)
const char * AllocationWorldName(AllocationWorld world)
const char * TailCheckScopeName(TailCheckScope scope)
absl::StatusOr< std::vector< OverworldExit > > LoadExits(Rom *rom)
std::string FormatOverworldAllocationText(const OverworldAllocationReport &report, const TailMapExpansionCheck *tail_check)
const char * TailCheckVerdictName(TailCheckVerdict verdict)
constexpr int kSpecialTailFirstArea
std::string OverworldAllocationToJson(const OverworldAllocationReport &report, const TailMapExpansionCheck *tail_check)
AllocationWorld AllocationWorldForArea(int area)
MapStreamScan ScanMapStream(const uint8_t *data, size_t size, uint32_t pc, size_t max_output)
absl::StatusOr< std::vector< OverworldEntrance > > LoadHoles(Rom *rom)
absl::StatusOr< OverworldAllocationReport > BuildOverworldAllocationReport(Rom *rom, const OverworldAllocationOptions &options)
Read-only map of overworld area IDs $00-$BF: which IDs are used, which per-area tables cover each ID,...
std::vector< std::string > evidence
std::optional< int > duplicate_of
std::optional< int > parent
std::optional< int > parent_past_end
std::vector< std::string > uncovered_tables
std::string map_pointer_source
One table the game or yaze indexes with an overworld area ID.
bool Misses(int area) const
The game reads this table for area but has no entry for it.
std::vector< uint8_t > decoded
std::map< int, std::string > area_names
std::vector< WorldAllocationSummary > summary
std::vector< AreaAllocation > areas
std::vector< std::string > TablesStoppingBeforeTail() const
std::vector< AreaTableInfo > tables
std::function< std::string(uint32_t)> symbol
Returns a symbol for a SNES address (empty when none).
std::function< std::optional< std::pair< std::string, bool > >(uint32_t)> ownership
std::vector< TailStreamCheck > streams
std::vector< TailCheckRegion > regions
std::string BlockingSummary() const
One line per blocking finding, for error messages.
std::vector< TailCheckOperand > operands
int free_bytes_before_marker
std::vector< std::string > tables_stopping_before_tail
uint32_t zs_code_end_snes
std::vector< TailCheckFinding > findings
bool yaze_saves_relocated_tables
std::vector< uint8_t > blank_high
std::array< uint32_t, 8 > operand_snes
std::array< uint32_t, 8 > operand_patched
std::vector< uint8_t > blank_low
std::array< uint32_t, 8 > operand_vanilla
bool terminated_within_stream
int free_without_map_data