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"
41 0x9C, 0x9D, 0x9E, 0x9F};
43 0x93, 0x94, 0x95, 0x96, 0x97, 0x9C, 0x9D, 0x9E, 0x9F};
46 return ((snes & 0x7F0000) >> 1) | (snes & 0x7FFF);
50 return ((pc << 1) & 0x7F0000) | (pc & 0x7FFF) | 0x8000;
54 return absl::StrFormat(
"$%02X:%04X", (snes >> 16) & 0xFF, snes & 0xFFFF);
57std::string HexByte(
int value) {
58 return absl::StrFormat(
"0x%02X", value);
62 return absl::StrFormat(
"$%02X", area);
65std::string RangeText(
int first,
int last) {
66 return absl::StrFormat(
"$%02X-$%02X", first, last);
70 return asm_version != 0xFF && asm_version != 0x00 && asm_version >= 3;
77bool InRom(
const Rom& rom, uint32_t pc,
size_t length) {
78 return static_cast<size_t>(pc) + length <= rom.
size();
82 const uint8_t* d = rom.
data();
83 return d[pc] | (d[pc + 1] << 8) | (d[pc + 2] << 16);
86uint16_t ReadWord(
const Rom& rom, uint32_t pc) {
87 const uint8_t* d = rom.
data();
88 return static_cast<uint16_t
>(d[pc] | (d[pc + 1] << 8));
91std::string HexBytes(
const uint8_t* data,
size_t count) {
93 for (
size_t i = 0; i < count; ++i) {
97 out += absl::StrFormat(
"%02X", data[i]);
103 uint32_t pc,
int entry_bytes,
int first_area,
104 int last_area, std::string past_end,
105 std::string note =
"") {
116 table.
past_end = std::move(past_end);
117 table.
note = std::move(note);
122 uint32_t snes,
int entry_bytes,
int first_area,
123 int last_area, std::string past_end,
124 std::string note =
"") {
135 table.
past_end = std::move(past_end);
136 table.
note = std::move(note);
141 std::vector<AreaTableInfo> t;
143 t.push_back(
RomTable(
"map32_high",
"Map32 pointers (high bytes)",
"map",
145 "Map32 pointers (low bytes) at $02:FB2D",
146 "TailMapExpansion.asm copies this table to $28:A400 "
147 "with 192 entries"));
148 t.push_back(
RomTable(
"map32_low",
"Map32 pointers (low bytes)",
"map",
150 "code (LoadSubscreenOverlay) at $02:FD0D",
151 "TailMapExpansion.asm copies this table to $28:A640 "
152 "with 192 entries"));
154 0xBF,
"Map32 pointers (high bytes) at $02:F94D",
155 "0 small, 1 large, 2 wide, 3 tall"));
157 0x00, 0x9F,
"Pool ByScreen1 at $28:8A38"));
160 "area_gfx",
"Area graphics ($0AA2, GFX0AA2ValsOW)",
"gfx", 0x7C9C, 1,
161 0x00, 0x9F,
"the old palette table at $00:FD3C (unused after ZS v3)"));
163 "palette_set",
"Area palette set (OverworldPalettesScreenToSet_New)",
164 "gfx", 0x4C635, 1, 0x00, 0xBF,
"",
"yaze writes entries $00-$9F only"));
165 t.push_back(
RomTable(
"pool_bg_color",
"BG color",
"gfx", 0x140000, 2, 0x00,
166 0x9F,
"Pool enable flags at $28:8140"));
167 t.push_back(
RomTable(
"pool_main_palette",
"Main palette",
"gfx", 0x140160, 1,
168 0x00, 0x9F,
"Pool mosaic table at $28:8200"));
169 t.push_back(
RomTable(
"pool_mosaic",
"Mosaic",
"gfx", 0x140200, 1, 0x00, 0x9F,
170 "Pool animated GFX table at $28:82A0"));
171 t.push_back(
RomTable(
"pool_animated_gfx",
"Animated GFX",
"gfx", 0x1402A0, 1,
172 0x00, 0x9F,
"Pool subscreen overlay table at $28:8340"));
173 t.push_back(
RomTable(
"pool_subscreen_overlay",
"Subscreen overlay",
"gfx",
175 "Pool tile GFX groups at $28:8480"));
176 t.push_back(
RomTable(
"pool_tile_gfx_groups",
"Tile GFX groups (8 sheets)",
177 "gfx", 0x140480, 8, 0x00, 0x9F,
178 "Pool default GFX groups at $28:8980, then the parent "
180 t.push_back(
RomTable(
"sw_sprite_gfx",
"Special-world sprite GFX set",
"gfx",
181 0x166E1, 1, 0x80, 0x9F,
182 "special-world sprite palettes at $02:E701"));
184 "sw_sprite_palette",
"Special-world sprite palette",
"gfx", 0x16701, 1,
186 "Sprite_LoadGfxProperties copies 0x40 bytes to $7E:FDC0; nothing "
187 "checks that entries past the authored ones hold palette data"));
188 t.push_back(
RomTable(
"sw_entry_gfx",
189 "Special-exit entry GFX ($0AA2, LoadSpecialOverworld)",
190 "gfx", 0x16821, 1, 0x80, 0x8F,
191 "palette_prop_a at $02:E831, then palette_prop_b",
192 "read only when entering through a special exit"));
194 t.push_back(
RomTable(
"pool_by_screen_1",
"Transition ByScreen1 (right)",
195 "transition", 0x140A38, 2, 0x00, 0x9F,
196 "Pool ByScreen2 at $28:8B78"));
197 t.push_back(
RomTable(
"pool_by_screen_2",
"Transition ByScreen2 (left)",
198 "transition", 0x140B78, 2, 0x00, 0x9F,
199 "Pool ByScreen3 at $28:8CB8"));
200 t.push_back(
RomTable(
"pool_by_screen_3",
"Transition ByScreen3 (down)",
201 "transition", 0x140CB8, 2, 0x00, 0x9F,
202 "Pool ByScreen4 at $28:8DF8"));
203 t.push_back(
RomTable(
"pool_by_screen_4",
"Transition ByScreen4 (up)",
204 "transition", 0x140DF8, 2, 0x00, 0x9F,
205 "Pool transition position Y at $28:8F38"));
206 t.push_back(
RomTable(
"pool_trans_pos_y",
"Transition position Y",
207 "transition", 0x140F38, 2, 0x00, 0x9F,
208 "Pool transition position X at $28:9078"));
209 t.push_back(
RomTable(
"pool_trans_pos_x",
"Transition position X",
210 "transition", 0x141078, 2, 0x00, 0x9F,
211 "Pool transition target north at $28:91B8"));
212 t.push_back(
RomTable(
"pool_trans_target_north",
"Transition target north",
213 "transition", 0x1411B8, 2, 0x00, 0x9F,
214 "Pool transition target west at $28:92F8"));
215 t.push_back(
RomTable(
"pool_trans_target_west",
"Transition target west",
216 "transition", 0x1412F8, 2, 0x00, 0x9F,
217 "Pool sprite pointers (phase 0) at $28:9438"));
219 t.push_back(
RomTable(
"pool_sprites_phase_0",
"Sprite list pointers, phase 0",
221 "Pool sprite pointers (phase 1) at $28:9578"));
222 t.push_back(
RomTable(
"pool_sprites_phase_1",
"Sprite list pointers, phase 1",
224 "Pool sprite pointers (phase 2) at $28:96B8"));
225 t.push_back(
RomTable(
"pool_sprites_phase_2",
"Sprite list pointers, phase 2",
227 "Pool sign messages at $28:97F8 (read as bank $09 "
228 "sprite pointers)"));
230 t.push_back(
RomTable(
"pool_sign_message",
"Sign message",
"text", 0x1417F8, 2,
232 "8 bytes of padding, then ZS code at $28:9940"));
234 t.push_back(
RomTable(
"music_lw",
"Music, light world (per game state)",
235 "music", 0x14303, 1, 0x00, 0x3F,
236 "the next game state's copy (+$40)",
237 "4 copies, one per game state"));
238 t.push_back(
RomTable(
"music_dw_sw",
"Music, dark and special world",
"music",
239 0x14403, 1, 0x40, 0x9F,
240 "code (AdjustOverworldAmbiance) at $02:C463",
241 "copied to WRAM $7F:5B40+ with the light-world table"));
243 t.push_back(
RomTable(
"bomb_doors",
"Bombable door locations",
"items",
244 0x12644, 2, 0x00, 0x9F,
245 "hidden item pointers at $02:A784"));
246 t.push_back(
RomTable(
"hidden_items",
"Hidden item pointers",
"items", 0x12784,
248 "data after the table at "
251 t.push_back(
RamTable(
"area_flags",
"Overworld area flags (SRAM)",
"sram",
252 0x7EF280, 1, 0x00, 0x7F,
253 "$7E:F300+, save data past the 0x80-byte area block",
254 "overlay and bomb-door bits are set for every area "
260 std::vector<AreaTableInfo> t;
261 t.push_back(
RomTable(
"map32_high",
"Map32 pointers (high bytes)",
"map",
263 "Map32 pointers (low bytes) at $02:FB2D"));
264 t.push_back(
RomTable(
"map32_low",
"Map32 pointers (low bytes)",
"map",
266 "code (LoadSubscreenOverlay) at $02:FD0D"));
267 t.push_back(
RomTable(
"area_size",
"Area size (vanilla: 0 large, 1 small)",
269 "Map32 pointers (high bytes) at $02:F94D"));
270 t.push_back(
RomTable(
"parent",
"Parent area (Overworld_ActualScreenID)",
272 "OverworldScreenTilemapChange at $02:A62C",
273 "dark world reads the light-world entry and adds $40; "
274 "special-world parents are fixed in code"));
275 t.push_back(
RomTable(
"area_gfx",
"Area graphics ($0AA2, GFXAA2ValsOW)",
"gfx",
276 0x7C9C, 1, 0x00, 0x7F,
277 "the area palette table at $00:FD1C"));
278 t.push_back(
RomTable(
"area_palette",
279 "Area palette (OverworldPalettesScreen"
281 "gfx", 0x7D1C, 1, 0x00, 0x87,
282 "code (ResetStarTileGraphics) at $00:FDA4"));
283 t.push_back(
RomTable(
"sw_sprite_gfx",
"Special-world sprite GFX ($0AA3)",
284 "gfx", 0x16811, 1, 0x80, 0x8F,
285 "special-world area GFX at $02:E821"));
286 t.push_back(
RomTable(
"sw_entry_gfx",
"Special-world area GFX ($0AA2)",
"gfx",
287 0x16821, 1, 0x80, 0x8F,
"palette_prop_a at $02:E831"));
288 t.push_back(
RomTable(
"sw_palette_a",
"Special-world palette (prop a)",
"gfx",
289 0x16831, 1, 0x80, 0x8F,
"palette_prop_b at $02:E841"));
290 t.push_back(
RomTable(
"sw_palette_b",
"Special-world palette (prop b)",
"gfx",
291 0x16841, 1, 0x80, 0x8F,
292 "code (LoadSpecialOverworld) at $02:E851"));
293 t.push_back(
RomTable(
"sign_message",
"Sign message (Overworld_SignText)",
294 "text", 0x3F51D, 2, 0x00, 0x8F,
295 "data after the table at $07:F63D"));
296 t.push_back(
RomTable(
"music_lw",
"Music, light world (per game state)",
297 "music", 0x14303, 1, 0x00, 0x3F,
298 "the next game state's copy (+$40)",
299 "4 copies, one per game state"));
300 t.push_back(
RomTable(
"music_dw_sw",
"Music, dark and special world",
"music",
301 0x14403, 1, 0x40, 0x9F,
302 "code (AdjustOverworldAmbiance) at $02:C463"));
303 t.push_back(
RomTable(
"hidden_items",
"Hidden item pointers",
"items", 0xDC2F9,
304 2, 0x00, 0x7F,
"hidden item data"));
305 t.push_back(
RamTable(
"area_flags",
"Overworld area flags (SRAM)",
"sram",
306 0x7EF280, 1, 0x00, 0x7F,
307 "$7E:F300+, save data past the 0x80-byte area block"));
330 if (area == 0x81 || area == 0x82 || area == 0x89 || area == 0x8A) {
338 const uint32_t high_entry = high_table_pc + 3 * index;
339 const uint32_t low_entry = low_table_pc + 3 * index;
340 if (!
InRom(rom, high_entry, 3) || !
InRom(rom, low_entry, 3)) {
345 auto scan = [&rom](uint32_t snes) {
348 if (pc < rom.
size()) {
352 result.
error =
"pointer is past the end of the ROM";
364 std::set<uint16_t> ids;
366 ids.insert(
static_cast<uint16_t
>((area->
high.
decoded[i] << 8) |
377 if (pointer < 0x8000) {
382 while (pc < rom.
size()) {
383 if (rom.
data()[pc] == 0xFF) {
395std::string
EvidenceCount(
int count,
const char* singular,
const char* plural) {
396 return absl::StrFormat(
"%d %s", count, count == 1 ? singular : plural);
400 std::vector<std::string> parts;
401 parts.reserve(areas.size());
402 for (
int area : areas) {
405 return absl::StrJoin(parts,
",");
444 std::vector<AreaTableInfo> tables =
445 IsZsV3(asm_version) ? ZsCustomV3Tables() : VanillaTables();
446 for (
auto& table : tables) {
447 if (table.id.rfind(
"sw_", 0) == 0) {
448 table.applies_first = 0x80;
449 }
else if (table.id ==
"music_lw") {
450 table.applies_last = 0x3F;
451 }
else if (table.id ==
"music_dw_sw") {
452 table.applies_first = 0x40;
462 scan.
snes = LoRomPcToSnes(pc);
465 scan.
error =
"stream starts past the end of the data";
469 std::vector<uint8_t>& out = scan.
decoded;
470 auto need = [&](
size_t count) {
471 return src + count <= size;
475 scan.
error =
"ran past the end of the data before $FF";
478 const uint8_t header = data[src++];
479 if (header == 0xFF) {
483 int command = header >> 5;
487 scan.
error =
"ran past the end of the data in a long header";
490 command = (header >> 2) & 7;
491 length = ((header & 0x03) << 8) | data[src++];
493 length = header & 0x1F;
503 out.insert(out.end(), data + src, data + src + length);
511 out.insert(out.end(), length, data[src++]);
518 const uint8_t a = data[src];
519 const uint8_t b = data[src + 1];
521 for (
int i = 0; i < length; ++i) {
522 out.push_back((i & 1) ? b : a);
531 uint8_t value = data[src++];
532 for (
int i = 0; i < length; ++i) {
533 out.push_back(value++);
542 size_t offset = (
static_cast<size_t>(data[src]) << 8) | data[src + 1];
544 for (
int i = 0; i < length; ++i) {
545 if (offset >= out.size()) {
546 scan.
error =
"copy offset points past the decoded bytes";
550 out.push_back(out[offset++]);
556 if (scan.
error.empty()) {
557 scan.
error =
"ran past the end of the data inside a command";
561 if (out.size() > max_output) {
562 scan.
error = absl::StrFormat(
"decoded more than %d bytes without $FF",
563 static_cast<int>(max_output));
574 std::vector<std::string> ids;
575 for (
const auto& table :
tables) {
577 ids.push_back(table.id);
585 if (rom ==
nullptr || !rom->
is_loaded()) {
586 return absl::FailedPreconditionError(
"ROM not loaded");
588 if (rom->
size() < 0x80000) {
589 return absl::FailedPreconditionError(
590 absl::StrFormat(
"ROM is %d bytes; an ALTTP ROM is at least 512 KB",
591 static_cast<int>(rom->
size())));
595 report.
rom_name = std::filesystem::path(rom->
filename()).filename().string();
599 report.
catalogue = zs_v3 ?
"zscustom_v3" :
"vanilla";
602 const uint32_t marker_pc = LoRomSnesToPc(plan.
marker_snes);
604 marker_pc < rom->
size() &&
613 area.
name = it->second;
615 for (
const auto& table : report.
tables) {
616 if (table.Misses(
id)) {
623 const uint32_t pc = kPoolParentPc + id;
624 if (pc < rom->size()) {
631 }
else if (
id < 0x80) {
632 const int lw =
id & 0x3F;
633 const uint8_t parent = rom->
data()[kVanillaParentPc + lw];
634 area.
parent =
id < 0x40 ? parent : parent + 0x40;
635 }
else if (
id <= 0x9F) {
636 area.
parent = VanillaSpecialParent(
id);
641 if (zs_v3 ||
id < 0x80) {
642 area.
size = DecodeAreaSize(rom->
data()[kAreaSizePc +
id], zs_v3);
654 }
else if (
id <= 0x9F) {
656 ScanAreaMap(*rom, kMap32HighPc, kMap32LowPc,
id, &area);
659 for (
int lower = 0; lower < id; ++lower) {
673 for (
int id = 0;
id <= 0x9F; ++id) {
678 const uint16_t overlay = ReadWord(*rom, kPoolOverlayPc + 2 *
id);
680 report.
areas[overlay].overlay_users.push_back(
id);
683 for (
int overlay : kZsCodeOverlayIds) {
684 report.
areas[overlay].overlay_named_in_code =
true;
687 for (
int overlay : kVanillaCodeOverlayIds) {
688 report.
areas[overlay].overlay_named_in_code =
true;
695 for (
const auto& entrance : *entrances) {
698 if (entrance.map_pos_ != 0xFFFF &&
700 report.
areas[entrance.map_id_].entrances++;
704 if (
auto holes =
LoadHoles(rom); holes.ok()) {
705 for (
const auto& hole : *holes) {
707 report.
areas[hole.map_id_].holes++;
711 if (
auto exits =
LoadExits(rom); exits.ok()) {
712 for (
const auto& exit : *exits) {
714 report.
areas[exit.map_id_].exits++;
722 for (
int id = 0;
id <= 0x9F; ++id) {
723 for (uint32_t table_pc : kPoolSpritePointerPcs) {
724 const uint32_t entry = table_pc + 2 * id;
725 if (!InRom(*rom, entry, 2)) {
729 const int count = CountSpriteList(*rom, ReadWord(*rom, entry), &valid);
731 report.
areas[id].sprites += count;
738 for (
auto& area : report.
areas) {
739 if (area.unique_tile32 > 1) {
740 area.evidence.push_back(
741 absl::StrFormat(
"map data (%d unique tile32)", area.unique_tile32));
743 const bool multi_area =
744 area.parent.has_value() &&
747 area.evidence.push_back(absl::StrFormat(
"%s area, parent %s",
749 AreaText(*area.parent)));
751 if (area.entrances > 0) {
752 area.evidence.push_back(
753 EvidenceCount(area.entrances,
"entrance",
"entrances"));
755 if (area.holes > 0) {
756 area.evidence.push_back(EvidenceCount(area.holes,
"hole",
"holes"));
758 if (area.exits > 0) {
759 area.evidence.push_back(EvidenceCount(area.exits,
"exit",
"exits"));
761 if (area.sprites > 0) {
762 area.evidence.push_back(EvidenceCount(area.sprites,
"sprite",
"sprites"));
764 if (!area.overlay_users.empty()) {
765 area.evidence.push_back(
766 absl::StrCat(
"subscreen overlay of ", JoinAreas(area.overlay_users)));
768 if (area.overlay_named_in_code) {
769 area.evidence.push_back(
"overlay ID named in code");
771 area.used = !area.evidence.empty();
774 const std::array<std::pair<const char*, std::pair<int, int>>, 4> worlds = {
775 {{
"light", {0x00, 0x3F}},
776 {
"dark", {0x40, 0x7F}},
777 {
"special", {0x80, 0x9F}},
778 {
"special_tail", {0xA0, 0xBF}}}};
779 for (
const auto& [key, range] : worlds) {
784 for (
int id = range.first;
id <= range.second; ++id) {
797 report.
summary.push_back(summary);
813 return "already_applied";
831 std::vector<std::string> lines;
832 for (
const auto& finding :
findings) {
833 if (finding.blocking) {
834 lines.push_back(absl::StrFormat(
838 return absl::StrJoin(lines,
"\n");
844 const char*
id,
const char* label, uint32_t snes,
848 region.
label = label;
853 if (region.
pc < rom.
size()) {
854 const size_t available = std::min<size_t>(length, rom.
size() - region.
pc);
855 const uint8_t* data = rom.
data() + region.
pc;
856 for (
size_t i = 0; i < available; ++i) {
861 region.
first_bytes = HexBytes(data, std::min<size_t>(available, 16));
864 if (
auto owner = annotator.
ownership(snes); owner.has_value()) {
865 region.
owner = owner->first;
875 const std::vector<uint8_t>& planned,
884 std::vector<uint8_t> image;
885 const size_t window = 0x10000;
886 if (pc < rom.
size()) {
887 const size_t available = std::min<size_t>(window, rom.
size() - pc);
888 image.assign(rom.
data() + pc, rom.
data() + pc + available);
892 if (image.size() < planned.size()) {
893 image.resize(planned.size(), 0x00);
895 std::copy(planned.begin(), planned.end(), image.begin());
919 const uint32_t marker_pc = LoRomSnesToPc(plan.
marker_snes);
920 if (marker_pc < rom.
size()) {
924 marker_pc < rom.
size() &&
928 check.
regions.push_back(MakeRegion(rom, annotator,
"marker",
930 check.
regions.push_back(MakeRegion(rom, annotator,
"high_table",
931 "Map32 pointers, high (192 entries)",
933 check.
regions.push_back(MakeRegion(rom, annotator,
"low_table",
934 "Map32 pointers, low (192 entries)",
936 check.
regions.push_back(MakeRegion(
937 rom, annotator,
"blank_high",
"Blank map stream, high bytes",
940 MakeRegion(rom, annotator,
"blank_low",
"Blank map stream, low bytes",
946 check.
streams.push_back(CheckBlankStream(rom,
"blank_low",
953 operand.
pc = LoRomSnesToPc(operand.
snes);
956 operand.
in_rom = operand.
pc >= 1 && InRom(rom, operand.
pc, 3);
958 operand.
state =
"missing";
961 operand.
current = ReadLong(rom, operand.
pc);
963 operand.
state =
"vanilla";
965 operand.
state =
"patched";
967 operand.
state =
"other";
975 for (uint32_t pc = marker_pc; pc > kZsFunctionSpacePc; --pc) {
976 if (rom.
data()[pc - 1] != 0) {
986 for (
const auto& table : tables) {
992 auto add = [&check](
TailCheckScope scope,
const char* id, std::string message,
994 check.
findings.push_back({scope, id, std::move(message), blocking});
1001 " is incompatible with the old tail-map table layout; "
1002 "the $EA marker cannot identify expansion in this format"),
1007 absl::StrFormat(
"ZSCustomOverworld v3 is required; this ROM reports "
1008 "ASM version 0x%02X at $28:8145",
1013 for (
const auto& region : check.
regions) {
1014 if (region.nonzero_bytes > 0) {
1015 std::string message = absl::StrFormat(
1016 "the patch would overwrite %d non-zero byte%s at %s (%s)",
1017 region.nonzero_bytes, region.nonzero_bytes == 1 ?
"" :
"s",
1018 SnesText(region.snes), region.label);
1019 if (!region.symbol.empty()) {
1020 absl::StrAppend(&message,
", symbol ", region.symbol);
1027 check.
regions.back().length)) {
1030 "the ROM is %d bytes; the patch writes up to PC "
1031 "0x%06X, so asar would expand it",
1037 for (
const auto& region : check.
regions) {
1041 if (
auto owner = annotator.
ownership(region.snes);
1042 owner.has_value() && owner->second) {
1044 absl::StrFormat(
"hack manifest: %s (%s) is %s", SnesText(region.snes),
1045 region.label, owner->first),
1049 for (
const auto& operand : check.
operands) {
1050 if (operand.state ==
"other") {
1052 absl::StrFormat(
"operand at %s holds %s, not the vanilla %s; "
1053 "another patch changed the map loader",
1054 SnesText(operand.snes), SnesText(operand.current),
1055 SnesText(operand.vanilla)),
1057 }
else if (operand.state ==
"patched" && !check.
marker_present) {
1059 absl::StrFormat(
"operand at %s already points to %s but the marker "
1061 SnesText(operand.snes), SnesText(operand.current),
1064 }
else if (operand.state ==
"missing") {
1066 absl::StrFormat(
"operand at %s is past the end of the ROM",
1067 SnesText(operand.snes)),
1071 if (
auto owner = annotator.
ownership(operand.snes);
1072 owner.has_value() && owner->second) {
1074 absl::StrFormat(
"hack manifest: operand at %s is %s",
1075 SnesText(operand.snes), owner->first),
1082 absl::StrFormat(
"ZS code ends at %s, %d bytes before the marker at "
1083 "%s; asar does not reserve %s-%s, so growing ZS code "
1084 "would run into the relocated tables",
1093 for (
const auto& stream : check.
streams) {
1097 std::string message;
1098 if (!stream.terminated_within_stream && stream.stream_bytes > 0) {
1099 message = absl::StrFormat(
1100 "blank map stream at %s is %d bytes with no $FF terminator; "
1101 "decoding reads past it and stops at %s after %d decoded bytes",
1102 SnesText(stream.snes), stream.stream_bytes, stream.stopped_at,
1103 stream.decoded_bytes);
1104 if (!stream.error.empty()) {
1105 absl::StrAppend(&message,
" (", stream.error,
")");
1108 message = absl::StrFormat(
1109 "blank map stream at %s decodes to %d bytes; a map needs %d",
1110 SnesText(stream.snes), stream.decoded_bytes, kMap32PerScreen);
1114 std::vector<std::string> unpatched_tables;
1116 if (
id !=
"map32_high" &&
id !=
"map32_low") {
1117 unpatched_tables.push_back(
id);
1120 if (!unpatched_tables.empty()) {
1123 "the patch extends only the map32 pointers; %d other per-area "
1124 "tables stop before $A0, so a tail area reads the data after "
1125 "each of them (%s)",
1126 static_cast<int>(unpatched_tables.size()),
1127 absl::StrJoin(unpatched_tables,
", ")),
1134 "after the patch the game reads map pointers from $28:A400/$28:A640, "
1135 "but yaze's overworld save writes 0x1794D/0x17B2D, so the next save "
1136 "leaves maps $00-$9F stale in game",
1141 absl::StrFormat(
"yaze loads and saves %d maps ($00-$%02X); maps "
1142 "$A0-$BF cannot be edited",
1147 bool blocking =
false;
1148 bool rom_blocking =
false;
1149 for (
const auto& finding : check.
findings) {
1150 blocking = blocking || finding.blocking;
1151 rom_blocking = rom_blocking ||
1167 return absl::OkStatus();
1169 return absl::FailedPreconditionError(absl::StrCat(
1170 "Refusing to apply TailMapExpansion.asm: the compatibility check says "
1173 "\nRun `z3ed overworld-allocation --rom <rom> --tail-check` for the "
1186 j[
"label"] = table.
label;
1187 j[
"group"] = table.
group;
1190 ? absl::StrFormat(
"%s (PC 0x%06X)",
SnesText(table.
snes), table.
pc)
1197 if (!table.
note.empty()) {
1198 j[
"note"] = table.
note;
1206 j[
"pc"] = absl::StrFormat(
"0x%06X", scan.
pc);
1210 if (!scan.
error.empty()) {
1211 j[
"error"] = scan.
error;
1218 j[
"id"] = HexByte(area.
area);
1220 if (!area.
name.empty()) {
1221 j[
"name"] = area.
name;
1223 j[
"used"] = area.
used;
1244 j[
"holes"] = area.
holes;
1245 j[
"exits"] = area.
exits;
1256 j[
"marker"] = {{
"address",
"$28:A3FF"},
1260 for (
const auto& region : check.
regions) {
1262 r[
"id"] = region.id;
1263 r[
"label"] = region.label;
1264 r[
"address"] =
SnesText(region.snes);
1265 r[
"pc"] = absl::StrFormat(
"0x%06X", region.pc);
1266 r[
"length"] = region.length;
1267 r[
"in_rom"] = region.in_rom;
1268 r[
"nonzero_bytes"] = region.nonzero_bytes;
1269 r[
"first_bytes"] = region.first_bytes;
1270 if (!region.owner.empty()) {
1271 r[
"owner"] = region.owner;
1273 if (!region.symbol.empty()) {
1274 r[
"symbol"] = region.symbol;
1276 regions.push_back(r);
1278 j[
"regions"] = regions;
1280 for (
const auto& stream : check.
streams) {
1282 s[
"id"] = stream.id;
1283 s[
"address"] =
SnesText(stream.snes);
1284 s[
"stream_bytes"] = stream.stream_bytes;
1285 s[
"terminated"] = stream.terminated;
1286 s[
"terminated_within_stream"] = stream.terminated_within_stream;
1287 s[
"decoded_bytes"] = stream.decoded_bytes;
1288 s[
"consumed_bytes"] = stream.consumed_bytes;
1289 s[
"stopped_at"] = stream.stopped_at;
1290 if (!stream.error.empty()) {
1291 s[
"error"] = stream.error;
1293 s[
"valid"] = stream.valid;
1294 streams.push_back(s);
1296 j[
"blank_streams"] = streams;
1298 for (
const auto& operand : check.
operands) {
1300 o[
"address"] =
SnesText(operand.snes);
1301 o[
"pc"] = absl::StrFormat(
"0x%06X", operand.pc);
1302 o[
"opcode"] = HexByte(operand.opcode);
1303 o[
"current"] =
SnesText(operand.current);
1304 o[
"vanilla"] =
SnesText(operand.vanilla);
1305 o[
"patched"] =
SnesText(operand.patched);
1306 o[
"state"] = operand.state;
1307 operands.push_back(o);
1309 j[
"operands"] = operands;
1314 j[
"zs_code_end"] =
nullptr;
1315 j[
"free_bytes_before_marker"] =
nullptr;
1320 for (
const auto& finding : check.
findings) {
1323 {
"message", finding.message}};
1324 (finding.blocking ? blockers : warnings).push_back(f);
1326 j[
"blockers"] = blockers;
1327 j[
"warnings"] = warnings;
1354 ordered_json summary = ordered_json::array();
1355 for (
const auto& world : report.
summary) {
1356 summary.push_back({{
"world", world.key},
1357 {
"range", RangeText(world.first_area, world.last_area)},
1358 {
"used", world.used},
1359 {
"free", world.free},
1360 {
"free_with_map_data", world.free_with_map_data},
1361 {
"free_without_map_data", world.free_without_map_data}});
1363 j[
"summary"] = summary;
1365 ordered_json tables = ordered_json::array();
1366 for (
const auto& table : report.
tables) {
1367 tables.push_back(TableJson(table));
1369 j[
"tables"] = tables;
1370 ordered_json areas = ordered_json::array();
1371 for (
const auto& area : report.
areas) {
1372 areas.push_back(AreaJson(area));
1375 if (tail_check !=
nullptr) {
1376 j[
"tail_check"] = TailJson(*tail_check);
1384 std::ostringstream out;
1385 out << absl::StrFormat(
"Overworld allocation: %s (%d bytes)\n",
1387 static_cast<int>(report.
rom_size));
1388 out << absl::StrFormat(
1389 "Layout: ASM version 0x%02X, %s tables; TailMapExpansion marker %s\n\n",
1394 out << absl::StrFormat(
" %-13s %-8s %5s %5s %9s %11s\n",
"world",
"range",
1395 "used",
"free",
"free+map",
"free-no-map");
1396 for (
const auto& world : report.
summary) {
1397 out << absl::StrFormat(
" %-13s %-8s %5d %5d %9d %11d\n", world.key,
1398 RangeText(world.first_area, world.last_area),
1399 world.used, world.free, world.free_with_map_data,
1400 world.free_without_map_data);
1403 out <<
"\nPer-area tables (a read past the last entry returns the data "
1404 "after the table)\n";
1405 out << absl::StrFormat(
" %-24s %-19s %5s %-8s %-8s %s\n",
"table",
1406 "location",
"bytes",
"covers",
"read for",
1408 for (
const auto& table : report.
tables) {
1409 const std::string location =
1411 ? absl::StrFormat(
"%s 0x%06X", SnesText(table.snes), table.pc)
1412 : absl::StrFormat(
"%s RAM", SnesText(table.snes));
1413 out << absl::StrFormat(
" %-24s %-19s %5d %-8s %-8s %s\n", table.id,
1414 location, table.entry_bytes,
1415 RangeText(table.first_area, table.last_area),
1416 RangeText(table.applies_first, table.applies_last),
1417 table.past_end.empty() ?
"-" : table.past_end);
1420 out << absl::StrFormat(
1421 " %d of %d tables are read for $A0 but stop before it.\n",
1422 static_cast<int>(stopping.size()),
1423 static_cast<int>(report.
tables.size()));
1425 out <<
"\nAreas (map: data/blank/=dup/invalid/none; miss = tables read for "
1426 "the ID that have no entry for it)\n";
1427 out << absl::StrFormat(
1428 " %-4s %-7s %-4s %-6s %-5s %-7s %5s %4s %4s %4s %4s "
1430 "id",
"world",
"used",
"parent",
"size",
"map",
"tile32",
"ent",
"hole",
1431 "exit",
"spr",
"miss",
"evidence");
1432 for (
const auto& area : report.
areas) {
1433 std::string parent =
"--";
1434 if (area.parent.has_value()) {
1435 parent = AreaText(*area.parent);
1437 std::string evidence = absl::StrJoin(area.evidence,
"; ");
1438 if (!area.name.empty()) {
1439 evidence = evidence.empty()
1440 ? absl::StrCat(
"[", area.name,
"]")
1441 : absl::StrCat(
"[", area.name,
"] ", evidence);
1443 out << absl::StrFormat(
1444 " %-4s %-7s %-4s %-6s %-5s %-7s %5s %4d %4d %4d %4d %4d %s\n",
1448 area.unique_tile32 >= 0 ? absl::StrCat(area.unique_tile32) :
"-",
1449 area.entrances, area.holes, area.exits, area.sprites,
1450 static_cast<int>(area.uncovered_tables.size()),
1451 evidence.empty() ?
"-" : evidence);
1454 if (tail_check !=
nullptr) {
1456 out <<
"\nTailMapExpansion check "
1457 "(assets/patches/Overworld/TailMapExpansion.asm)\n";
1458 out << absl::StrFormat(
" Verdict: %s (ROM compatible: %s)\n",
1461 out << absl::StrFormat(
" Marker $28:A3FF: 0x%02X (%s)\n", c.
marker_value,
1464 out << absl::StrFormat(
1465 " ZS code ends at %s; %d bytes free before $28:A3FF\n",
1468 out <<
" ZS code end: not found (not a ZS v3 ROM)\n";
1470 out <<
" Target regions:\n";
1471 for (
const auto& region : c.
regions) {
1473 if (!region.symbol.empty()) {
1474 absl::StrAppend(&extra,
" symbol ", region.symbol);
1476 if (!region.owner.empty()) {
1477 absl::StrAppend(&extra,
" owner ", region.owner);
1479 out << absl::StrFormat(
1480 " %-10s %s PC 0x%06X %4d B %s non-zero %3d [%s]%s\n",
1481 region.id, SnesText(region.snes), region.pc, region.length,
1482 region.in_rom ?
"in ROM " :
"past end", region.nonzero_bytes,
1483 region.first_bytes, extra);
1485 out <<
" Blank streams (decoded as the game would read them after the "
1487 for (
const auto& stream : c.
streams) {
1488 out << absl::StrFormat(
1489 " %-10s %s %3d B written $FF in stream: %-3s decoded %d B, "
1490 "stopped at %s %s\n",
1491 stream.id, SnesText(stream.snes), stream.stream_bytes,
1492 stream.terminated_within_stream ?
"yes" :
"no", stream.decoded_bytes,
1493 stream.stopped_at, stream.valid ?
"valid" :
"INVALID");
1495 out <<
" Patched operands (LDA.l table,X):\n";
1496 for (
const auto& operand : c.
operands) {
1497 out << absl::StrFormat(
1498 " %s PC 0x%06X opcode 0x%02X now %s vanilla %s patch %s "
1500 SnesText(operand.snes), operand.pc, operand.opcode,
1501 SnesText(operand.current), SnesText(operand.vanilla),
1502 SnesText(operand.patched), operand.state);
1504 out <<
" Blockers:\n";
1506 for (
const auto& finding : c.
findings) {
1507 if (finding.blocking) {
1509 out << absl::StrFormat(
" [%s] %s: %s\n",
1517 out <<
" Warnings:\n";
1519 for (
const auto& finding : c.
findings) {
1520 if (!finding.blocking) {
1522 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
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)
LayoutDialect DetectLayout(const Rom &rom)
bool CanUseTailExpansion(const Rom &rom)
const char * LayoutName(LayoutDialect dialect)
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