yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_allocation.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <set>
6#include <sstream>
7#include <utility>
8
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"
16
17namespace yaze::zelda3 {
18
19namespace {
20
21using ordered_json = nlohmann::ordered_json;
22
23constexpr uint32_t kAsmVersionPc = 0x140145; // ZS "ASM applied" byte.
24constexpr uint32_t kZsFunctionSpacePc = 0x141940; // $28:9940.
25constexpr int kMap32PerScreen = 256;
26constexpr uint32_t kMap32HighPc = 0x1794D;
27constexpr uint32_t kMap32LowPc = 0x17B2D;
28constexpr uint32_t kPoolParentPc = 0x140998;
29constexpr uint32_t kPoolOverlayPc = 0x140340;
30constexpr uint32_t kAreaSizePc = 0x1788D;
31constexpr uint32_t kVanillaParentPc = 0x125EC;
32constexpr std::array<uint32_t, 3> kPoolSpritePointerPcs = {0x141438, 0x141578,
33 0x1416B8};
34constexpr uint32_t kOverworldSpriteBankPc = 0x48000; // $09:8000.
35constexpr int kMaxSpritesPerList = 0x80;
36
37// Subscreen overlay IDs that ZSCustomOverworld (or vanilla) code loads or
38// tests by value, independent of any table entry.
39constexpr std::array<int, 8> kZsCodeOverlayIds = {0x93, 0x94, 0x95, 0x96,
40 0x9C, 0x9D, 0x9E, 0x9F};
41constexpr std::array<int, 9> kVanillaCodeOverlayIds = {
42 0x93, 0x94, 0x95, 0x96, 0x97, 0x9C, 0x9D, 0x9E, 0x9F};
43
44uint32_t LoRomSnesToPc(uint32_t snes) {
45 return ((snes & 0x7F0000) >> 1) | (snes & 0x7FFF);
46}
47
48uint32_t LoRomPcToSnes(uint32_t pc) {
49 return ((pc << 1) & 0x7F0000) | (pc & 0x7FFF) | 0x8000;
50}
51
52std::string SnesText(uint32_t snes) {
53 return absl::StrFormat("$%02X:%04X", (snes >> 16) & 0xFF, snes & 0xFFFF);
54}
55
56std::string HexByte(int value) {
57 return absl::StrFormat("0x%02X", value);
58}
59
60std::string AreaText(int area) {
61 return absl::StrFormat("$%02X", area);
62}
63
64std::string RangeText(int first, int last) {
65 return absl::StrFormat("$%02X-$%02X", first, last);
66}
67
68bool IsZsV3(uint8_t asm_version) {
69 return asm_version != 0xFF && asm_version != 0x00 && asm_version >= 3;
70}
71
72uint8_t ReadAsmVersion(const Rom& rom) {
73 return rom.size() > kAsmVersionPc ? rom.data()[kAsmVersionPc] : 0xFF;
74}
75
76bool InRom(const Rom& rom, uint32_t pc, size_t length) {
77 return static_cast<size_t>(pc) + length <= rom.size();
78}
79
80uint32_t ReadLong(const Rom& rom, uint32_t pc) {
81 const uint8_t* d = rom.data();
82 return d[pc] | (d[pc + 1] << 8) | (d[pc + 2] << 16);
83}
84
85uint16_t ReadWord(const Rom& rom, uint32_t pc) {
86 const uint8_t* d = rom.data();
87 return static_cast<uint16_t>(d[pc] | (d[pc + 1] << 8));
88}
89
90std::string HexBytes(const uint8_t* data, size_t count) {
91 std::string out;
92 for (size_t i = 0; i < count; ++i) {
93 if (i > 0) {
94 out += ' ';
95 }
96 out += absl::StrFormat("%02X", data[i]);
97 }
98 return out;
99}
100
101AreaTableInfo RomTable(const char* id, const char* label, const char* group,
102 uint32_t pc, int entry_bytes, int first_area,
103 int last_area, std::string past_end,
104 std::string note = "") {
105 AreaTableInfo table;
106 table.id = id;
107 table.label = label;
108 table.group = group;
109 table.in_rom = true;
110 table.pc = pc;
111 table.snes = LoRomPcToSnes(pc);
112 table.entry_bytes = entry_bytes;
113 table.first_area = first_area;
114 table.last_area = last_area;
115 table.past_end = std::move(past_end);
116 table.note = std::move(note);
117 return table;
118}
119
120AreaTableInfo RamTable(const char* id, const char* label, const char* group,
121 uint32_t snes, int entry_bytes, int first_area,
122 int last_area, std::string past_end,
123 std::string note = "") {
124 AreaTableInfo table;
125 table.id = id;
126 table.label = label;
127 table.group = group;
128 table.in_rom = false;
129 table.pc = 0;
130 table.snes = snes;
131 table.entry_bytes = entry_bytes;
132 table.first_area = first_area;
133 table.last_area = last_area;
134 table.past_end = std::move(past_end);
135 table.note = std::move(note);
136 return table;
137}
138
139std::vector<AreaTableInfo> ZsCustomV3Tables() {
140 std::vector<AreaTableInfo> t;
141 // Map data.
142 t.push_back(RomTable("map32_high", "Map32 pointers (high bytes)", "map",
143 kMap32HighPc, 3, 0x00, 0x9F,
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",
148 kMap32LowPc, 3, 0x00, 0x9F,
149 "code (LoadSubscreenOverlay) at $02:FD0D",
150 "TailMapExpansion.asm copies this table to $28:A640 "
151 "with 192 entries"));
152 t.push_back(RomTable("area_size", "Area size", "map", kAreaSizePc, 1, 0x00,
153 0xBF, "Map32 pointers (high bytes) at $02:F94D",
154 "0 small, 1 large, 2 wide, 3 tall"));
155 t.push_back(RomTable("pool_parent", "Parent area", "map", kPoolParentPc, 1,
156 0x00, 0x9F, "Pool ByScreen1 at $28:8A38"));
157 // Graphics and palettes.
158 t.push_back(RomTable(
159 "area_gfx", "Area graphics ($0AA2, GFX0AA2ValsOW)", "gfx", 0x7C9C, 1,
160 0x00, 0x9F, "the old palette table at $00:FD3C (unused after ZS v3)"));
161 t.push_back(RomTable(
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",
173 kPoolOverlayPc, 2, 0x00, 0x9F,
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 "
178 "table"));
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"));
182 t.push_back(RomTable(
183 "sw_sprite_palette", "Special-world sprite palette", "gfx", 0x16701, 1,
184 0x80, 0xBF, "",
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"));
192 // Transitions.
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"));
217 // Sprites.
218 t.push_back(RomTable("pool_sprites_phase_0", "Sprite list pointers, phase 0",
219 "sprites", kPoolSpritePointerPcs[0], 2, 0x00, 0x9F,
220 "Pool sprite pointers (phase 1) at $28:9578"));
221 t.push_back(RomTable("pool_sprites_phase_1", "Sprite list pointers, phase 1",
222 "sprites", kPoolSpritePointerPcs[1], 2, 0x00, 0x9F,
223 "Pool sprite pointers (phase 2) at $28:96B8"));
224 t.push_back(RomTable("pool_sprites_phase_2", "Sprite list pointers, phase 2",
225 "sprites", kPoolSpritePointerPcs[2], 2, 0x00, 0x9F,
226 "Pool sign messages at $28:97F8 (read as bank $09 "
227 "sprite pointers)"));
228 // Text.
229 t.push_back(RomTable("pool_sign_message", "Sign message", "text", 0x1417F8, 2,
230 0x00, 0x9F,
231 "8 bytes of padding, then ZS code at $28:9940"));
232 // Music.
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"));
241 // Items and doors.
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,
246 2, 0x00, 0x9F,
247 "data after the table at "
248 "$02:A8C4"));
249 // Save data.
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 "
254 "visited"));
255 return t;
256}
257
258std::vector<AreaTableInfo> VanillaTables() {
259 std::vector<AreaTableInfo> t;
260 t.push_back(RomTable("map32_high", "Map32 pointers (high bytes)", "map",
261 kMap32HighPc, 3, 0x00, 0x9F,
262 "Map32 pointers (low bytes) at $02:FB2D"));
263 t.push_back(RomTable("map32_low", "Map32 pointers (low bytes)", "map",
264 kMap32LowPc, 3, 0x00, 0x9F,
265 "code (LoadSubscreenOverlay) at $02:FD0D"));
266 t.push_back(RomTable("area_size", "Area size (vanilla: 0 large, 1 small)",
267 "map", kAreaSizePc, 1, 0x00, 0xBF,
268 "Map32 pointers (high bytes) at $02:F94D"));
269 t.push_back(RomTable("parent", "Parent area (Overworld_ActualScreenID)",
270 "map", kVanillaParentPc, 1, 0x00, 0x7F,
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"
279 "ToSet)",
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"));
307 return t;
308}
309
310AllocationAreaSize DecodeAreaSize(uint8_t value, bool zs_v3) {
311 if (zs_v3) {
312 switch (value) {
313 case 1:
315 case 2:
317 case 3:
319 default:
321 }
322 }
324}
325
326// Vanilla special-world parents are fixed in code: Zora's Domain
327// ($81/$82/$89/$8A) is one large area, every other ID is its own parent.
328int VanillaSpecialParent(int area) {
329 if (area == 0x81 || area == 0x82 || area == 0x89 || area == 0x8A) {
330 return 0x81;
331 }
332 return area;
333}
334
335void ScanAreaMap(const Rom& rom, uint32_t high_table_pc, uint32_t low_table_pc,
336 int index, AreaAllocation* area) {
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)) {
340 return;
341 }
342 area->high.snes = ReadLong(rom, high_entry);
343 area->low.snes = ReadLong(rom, low_entry);
344 auto scan = [&rom](uint32_t snes) {
345 const uint32_t pc = LoRomSnesToPc(snes);
346 MapStreamScan result;
347 if (pc < rom.size()) {
348 result = ScanMapStream(rom.data(), rom.size(), pc);
349 } else {
350 result.pc = pc;
351 result.error = "pointer is past the end of the ROM";
352 }
353 result.snes = snes;
354 return result;
355 };
356 area->high = scan(area->high.snes);
357 area->low = scan(area->low.snes);
358 area->map_pointer_valid = area->high.in_rom && area->low.in_rom &&
359 area->high.terminated && area->low.terminated &&
362 if (area->map_pointer_valid) {
363 std::set<uint16_t> ids;
364 for (int i = 0; i < kMap32PerScreen; ++i) {
365 ids.insert(static_cast<uint16_t>((area->high.decoded[i] << 8) |
366 area->low.decoded[i]));
367 }
368 area->unique_tile32 = static_cast<int>(ids.size());
369 }
370 area->high.decoded.clear();
371 area->low.decoded.clear();
372}
373
374int CountSpriteList(const Rom& rom, uint16_t pointer, bool* valid) {
375 *valid = false;
376 if (pointer < 0x8000) {
377 return 0;
378 }
379 uint32_t pc = kOverworldSpriteBankPc + (pointer & 0x7FFF);
380 int count = 0;
381 while (pc < rom.size()) {
382 if (rom.data()[pc] == 0xFF) {
383 *valid = true;
384 return count;
385 }
386 if (++count > kMaxSpritesPerList) {
387 return count;
388 }
389 pc += 3;
390 }
391 return count;
392}
393
394std::string EvidenceCount(int count, const char* singular, const char* plural) {
395 return absl::StrFormat("%d %s", count, count == 1 ? singular : plural);
396}
397
398std::string JoinAreas(const std::vector<int>& areas) {
399 std::vector<std::string> parts;
400 parts.reserve(areas.size());
401 for (int area : areas) {
402 parts.push_back(AreaText(area));
403 }
404 return absl::StrJoin(parts, ",");
405}
406
407} // namespace
408
410 switch (world) {
412 return "light";
414 return "dark";
416 return "special";
417 }
418 return "special";
419}
420
422 switch (size) {
424 return "small";
426 return "large";
428 return "wide";
430 return "tall";
431 }
432 return "small";
433}
434
436 if (area >= 0x80) {
438 }
439 return area >= 0x40 ? AllocationWorld::kDark : AllocationWorld::kLight;
440}
441
442std::vector<AreaTableInfo> OverworldAreaTableCatalogue(uint8_t asm_version) {
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; // Indexed with the area ID minus $80.
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;
452 }
453 }
454 return tables;
455}
456
457MapStreamScan ScanMapStream(const uint8_t* data, size_t size, uint32_t pc,
458 size_t max_output) {
459 MapStreamScan scan;
460 scan.pc = pc;
461 scan.snes = LoRomPcToSnes(pc);
462 scan.in_rom = pc < size;
463 if (!scan.in_rom) {
464 scan.error = "stream starts past the end of the data";
465 return scan;
466 }
467 size_t src = pc;
468 std::vector<uint8_t>& out = scan.decoded;
469 auto need = [&](size_t count) {
470 return src + count <= size;
471 };
472 while (true) {
473 if (!need(1)) {
474 scan.error = "ran past the end of the data before $FF";
475 break;
476 }
477 const uint8_t header = data[src++];
478 if (header == 0xFF) {
479 scan.terminated = true;
480 break;
481 }
482 int command = header >> 5;
483 int length = 0;
484 if (command == 7) {
485 if (!need(1)) {
486 scan.error = "ran past the end of the data in a long header";
487 break;
488 }
489 command = (header >> 2) & 7;
490 length = ((header & 0x03) << 8) | data[src++];
491 } else {
492 length = header & 0x1F;
493 }
494 ++length;
495 bool ok = true;
496 switch (command) {
497 case 0: // Direct copy.
498 if (!need(length)) {
499 ok = false;
500 break;
501 }
502 out.insert(out.end(), data + src, data + src + length);
503 src += length;
504 break;
505 case 1: // Byte fill.
506 if (!need(1)) {
507 ok = false;
508 break;
509 }
510 out.insert(out.end(), length, data[src++]);
511 break;
512 case 2: { // Word fill.
513 if (!need(2)) {
514 ok = false;
515 break;
516 }
517 const uint8_t a = data[src];
518 const uint8_t b = data[src + 1];
519 src += 2;
520 for (int i = 0; i < length; ++i) {
521 out.push_back((i & 1) ? b : a);
522 }
523 break;
524 }
525 case 3: { // Increasing fill.
526 if (!need(1)) {
527 ok = false;
528 break;
529 }
530 uint8_t value = data[src++];
531 for (int i = 0; i < length; ++i) {
532 out.push_back(value++);
533 }
534 break;
535 }
536 default: { // Copy from output, big-endian offset (map streams).
537 if (!need(2)) {
538 ok = false;
539 break;
540 }
541 size_t offset = (static_cast<size_t>(data[src]) << 8) | data[src + 1];
542 src += 2;
543 for (int i = 0; i < length; ++i) {
544 if (offset >= out.size()) {
545 scan.error = "copy offset points past the decoded bytes";
546 ok = false;
547 break;
548 }
549 out.push_back(out[offset++]);
550 }
551 break;
552 }
553 }
554 if (!ok) {
555 if (scan.error.empty()) {
556 scan.error = "ran past the end of the data inside a command";
557 }
558 break;
559 }
560 if (out.size() > max_output) {
561 scan.error = absl::StrFormat("decoded more than %d bytes without $FF",
562 static_cast<int>(max_output));
563 break;
564 }
565 }
566 scan.compressed_bytes = static_cast<int>(src - pc);
567 scan.decoded_bytes = static_cast<int>(out.size());
568 return scan;
569}
570
572 const {
573 std::vector<std::string> ids;
574 for (const auto& table : tables) {
575 if (table.Misses(kSpecialTailFirstArea)) {
576 ids.push_back(table.id);
577 }
578 }
579 return ids;
580}
581
582absl::StatusOr<OverworldAllocationReport> BuildOverworldAllocationReport(
583 Rom* rom, const OverworldAllocationOptions& options) {
584 if (rom == nullptr || !rom->is_loaded()) {
585 return absl::FailedPreconditionError("ROM not loaded");
586 }
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())));
591 }
592
594 report.rom_name = std::filesystem::path(rom->filename()).filename().string();
595 report.rom_size = rom->size();
596 report.asm_version = ReadAsmVersion(*rom);
597 const bool zs_v3 = IsZsV3(report.asm_version);
598 report.catalogue = zs_v3 ? "zscustom_v3" : "vanilla";
600 const TailMapExpansionPlan plan;
601 const uint32_t marker_pc = LoRomSnesToPc(plan.marker_snes);
602 report.tail_marker_present =
603 marker_pc < rom->size() && rom->data()[marker_pc] == plan.marker_value;
604
605 report.areas.resize(kAllocationAreaCount);
606 for (int id = 0; id < kAllocationAreaCount; ++id) {
607 AreaAllocation& area = report.areas[id];
608 area.area = id;
609 area.world = AllocationWorldForArea(id);
610 if (auto it = options.area_names.find(id); it != options.area_names.end()) {
611 area.name = it->second;
612 }
613 for (const auto& table : report.tables) {
614 if (table.Misses(id)) {
615 area.uncovered_tables.push_back(table.id);
616 }
617 }
618
619 // Parent.
620 if (zs_v3) {
621 const uint32_t pc = kPoolParentPc + id;
622 if (pc < rom->size()) {
623 if (id <= 0x9F) {
624 area.parent = rom->data()[pc];
625 } else {
626 area.parent_past_end = rom->data()[pc];
627 }
628 }
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);
635 }
636
637 // Size. Vanilla fixes special-world sizes in code (only Zora's Domain
638 // is large); ZS v3 reads all 0xC0 entries of the table.
639 if (zs_v3 || id < 0x80) {
640 area.size = DecodeAreaSize(rom->data()[kAreaSizePc + id], zs_v3);
641 } else {
642 area.size = VanillaSpecialParent(id) == 0x81 ? AllocationAreaSize::kLarge
644 }
645
646 // Map data. Once the tail marker is present the game reads the
647 // relocated 192-entry tables; otherwise only $00-$9F have pointers.
648 if (report.tail_marker_present) {
649 area.map_pointer_source = "tail_table";
650 ScanAreaMap(*rom, LoRomSnesToPc(plan.high_table_snes),
651 LoRomSnesToPc(plan.low_table_snes), id, &area);
652 } else if (id <= 0x9F) {
653 area.map_pointer_source = "vanilla_table";
654 ScanAreaMap(*rom, kMap32HighPc, kMap32LowPc, id, &area);
655 }
656 if (area.map_pointer_source != "none") {
657 for (int lower = 0; lower < id; ++lower) {
658 const AreaAllocation& other = report.areas[lower];
659 if (other.map_pointer_source != "none" &&
660 other.high.snes == area.high.snes &&
661 other.low.snes == area.low.snes) {
662 area.duplicate_of = lower;
663 break;
664 }
665 }
666 }
667 }
668
669 // Subscreen overlay users (the game reads the parent's entry).
670 if (zs_v3) {
671 for (int id = 0; id <= 0x9F; ++id) {
672 const AreaAllocation& area = report.areas[id];
673 if (area.parent.has_value() && *area.parent != id) {
674 continue;
675 }
676 const uint16_t overlay = ReadWord(*rom, kPoolOverlayPc + 2 * id);
677 if (overlay >= 0x80 && overlay < kAllocationAreaCount) {
678 report.areas[overlay].overlay_users.push_back(id);
679 }
680 }
681 for (int overlay : kZsCodeOverlayIds) {
682 report.areas[overlay].overlay_named_in_code = true;
683 }
684 } else {
685 for (int overlay : kVanillaCodeOverlayIds) {
686 report.areas[overlay].overlay_named_in_code = true;
687 }
688 }
689
690 // Entities.
691 if (options.scan_entities) {
692 if (auto entrances = LoadEntrances(rom); entrances.ok()) {
693 for (const auto& entrance : *entrances) {
694 // LoadEntrances passes its deleted flag into is_hole_, so test the
695 // position: $FFFF marks an unused slot.
696 if (entrance.map_pos_ != 0xFFFF &&
697 entrance.map_id_ < kAllocationAreaCount) {
698 report.areas[entrance.map_id_].entrances++;
699 }
700 }
701 }
702 if (auto holes = LoadHoles(rom); holes.ok()) {
703 for (const auto& hole : *holes) {
704 if (hole.map_id_ < kAllocationAreaCount) {
705 report.areas[hole.map_id_].holes++;
706 }
707 }
708 }
709 if (auto exits = LoadExits(rom); exits.ok()) {
710 for (const auto& exit : *exits) {
711 if (!exit.deleted_ && exit.map_id_ < kAllocationAreaCount) {
712 report.areas[exit.map_id_].exits++;
713 }
714 }
715 }
716 }
717
718 // Overworld sprites (ZS v3 Pool pointer tables, bank $09 lists).
719 if (options.scan_sprites && zs_v3) {
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)) {
724 continue;
725 }
726 bool valid = false;
727 const int count = CountSpriteList(*rom, ReadWord(*rom, entry), &valid);
728 if (valid) {
729 report.areas[id].sprites += count;
730 }
731 }
732 }
733 }
734
735 // Evidence and summary.
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));
740 }
741 const bool multi_area =
742 area.parent.has_value() &&
743 (*area.parent != area.area || area.size != AllocationAreaSize::kSmall);
744 if (multi_area) {
745 area.evidence.push_back(absl::StrFormat("%s area, parent %s",
746 AllocationAreaSizeName(area.size),
747 AreaText(*area.parent)));
748 }
749 if (area.entrances > 0) {
750 area.evidence.push_back(
751 EvidenceCount(area.entrances, "entrance", "entrances"));
752 }
753 if (area.holes > 0) {
754 area.evidence.push_back(EvidenceCount(area.holes, "hole", "holes"));
755 }
756 if (area.exits > 0) {
757 area.evidence.push_back(EvidenceCount(area.exits, "exit", "exits"));
758 }
759 if (area.sprites > 0) {
760 area.evidence.push_back(EvidenceCount(area.sprites, "sprite", "sprites"));
761 }
762 if (!area.overlay_users.empty()) {
763 area.evidence.push_back(
764 absl::StrCat("subscreen overlay of ", JoinAreas(area.overlay_users)));
765 }
766 if (area.overlay_named_in_code) {
767 area.evidence.push_back("overlay ID named in code");
768 }
769 area.used = !area.evidence.empty();
770 }
771
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) {
779 summary.key = key;
780 summary.first_area = range.first;
781 summary.last_area = range.second;
782 for (int id = range.first; id <= range.second; ++id) {
783 const AreaAllocation& area = report.areas[id];
784 if (area.used) {
785 summary.used++;
786 } else {
787 summary.free++;
788 if (area.map_pointer_valid) {
789 summary.free_with_map_data++;
790 } else {
791 summary.free_without_map_data++;
792 }
793 }
794 }
795 report.summary.push_back(summary);
796 }
797 return report;
798}
799
800// ---------------------------------------------------------------------------
801// TailMapExpansion check
802// ---------------------------------------------------------------------------
803
805 switch (verdict) {
807 return "safe";
809 return "unsafe";
811 return "already_applied";
812 }
813 return "unsafe";
814}
815
817 switch (scope) {
819 return "rom";
821 return "patch";
823 return "yaze";
824 }
825 return "rom";
826}
827
829 std::vector<std::string> lines;
830 for (const auto& finding : findings) {
831 if (finding.blocking) {
832 lines.push_back(absl::StrFormat(
833 "[%s] %s", TailCheckScopeName(finding.scope), finding.message));
834 }
835 }
836 return absl::StrJoin(lines, "\n");
837}
838
839namespace {
840
842 const char* id, const char* label, uint32_t snes,
843 int length) {
844 TailCheckRegion region;
845 region.id = id;
846 region.label = label;
847 region.snes = snes;
848 region.pc = LoRomSnesToPc(snes);
849 region.length = length;
850 region.in_rom = InRom(rom, region.pc, length);
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) {
855 if (data[i] != 0) {
856 region.nonzero_bytes++;
857 }
858 }
859 region.first_bytes = HexBytes(data, std::min<size_t>(available, 16));
860 }
861 if (annotator.ownership) {
862 if (auto owner = annotator.ownership(snes); owner.has_value()) {
863 region.owner = owner->first;
864 }
865 }
866 if (annotator.symbol) {
867 region.symbol = annotator.symbol(snes);
868 }
869 return region;
870}
871
872TailStreamCheck CheckBlankStream(const Rom& rom, const char* id, uint32_t snes,
873 const std::vector<uint8_t>& planned,
874 bool applied) {
875 TailStreamCheck check;
876 check.id = id;
877 check.snes = snes;
878 const uint32_t pc = LoRomSnesToPc(snes);
879 // Decode the stream as the game would see it after the patch: the planned
880 // bytes, followed by whatever the ROM holds after them. Once applied, the
881 // ROM already holds the stream.
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);
887 }
888 if (!applied) {
889 check.stream_bytes = static_cast<int>(planned.size());
890 if (image.size() < planned.size()) {
891 image.resize(planned.size(), 0x00);
892 }
893 std::copy(planned.begin(), planned.end(), image.begin());
894 }
895 const MapStreamScan scan = ScanMapStream(image.data(), image.size(), 0);
896 check.terminated = scan.terminated;
897 check.decoded_bytes = scan.decoded_bytes;
898 check.consumed_bytes = scan.compressed_bytes;
899 check.error = scan.error;
902 scan.terminated &&
903 (applied || scan.compressed_bytes <= check.stream_bytes);
904 check.valid =
906 return check;
907}
908
909} // namespace
910
912 const Rom& rom, const TailMapExpansionPlan& plan,
913 const TailCheckAnnotator& annotator) {
915 check.rom_size = rom.size();
916 check.asm_version = ReadAsmVersion(rom);
917 const uint32_t marker_pc = LoRomSnesToPc(plan.marker_snes);
918 if (marker_pc < rom.size()) {
919 check.marker_value = rom.data()[marker_pc];
920 }
921 check.marker_present =
922 marker_pc < rom.size() && check.marker_value == plan.marker_value;
923 const int table_bytes = plan.table_entries * 3;
924
925 check.regions.push_back(MakeRegion(rom, annotator, "marker",
926 "Detection marker", plan.marker_snes, 1));
927 check.regions.push_back(MakeRegion(rom, annotator, "high_table",
928 "Map32 pointers, high (192 entries)",
929 plan.high_table_snes, table_bytes));
930 check.regions.push_back(MakeRegion(rom, annotator, "low_table",
931 "Map32 pointers, low (192 entries)",
932 plan.low_table_snes, table_bytes));
933 check.regions.push_back(MakeRegion(
934 rom, annotator, "blank_high", "Blank map stream, high bytes",
935 plan.blank_high_snes, static_cast<int>(plan.blank_high.size())));
936 check.regions.push_back(
937 MakeRegion(rom, annotator, "blank_low", "Blank map stream, low bytes",
938 plan.blank_low_snes, static_cast<int>(plan.blank_low.size())));
939
940 check.streams.push_back(
941 CheckBlankStream(rom, "blank_high", plan.blank_high_snes, plan.blank_high,
942 check.marker_present));
943 check.streams.push_back(CheckBlankStream(rom, "blank_low",
944 plan.blank_low_snes, plan.blank_low,
945 check.marker_present));
946
947 for (size_t i = 0; i < plan.operand_snes.size(); ++i) {
948 TailCheckOperand operand;
949 operand.snes = plan.operand_snes[i];
950 operand.pc = LoRomSnesToPc(operand.snes);
951 operand.vanilla = plan.operand_vanilla[i];
952 operand.patched = plan.operand_patched[i];
953 operand.in_rom = operand.pc >= 1 && InRom(rom, operand.pc, 3);
954 if (!operand.in_rom) {
955 operand.state = "missing";
956 } else {
957 operand.opcode = rom.data()[operand.pc - 1];
958 operand.current = ReadLong(rom, operand.pc);
959 if (operand.current == operand.vanilla) {
960 operand.state = "vanilla";
961 } else if (operand.current == operand.patched) {
962 operand.state = "patched";
963 } else {
964 operand.state = "other";
965 }
966 }
967 check.operands.push_back(operand);
968 }
969
970 // End of the ZS function space ($28:9940 up to the marker).
971 if (IsZsV3(check.asm_version) && marker_pc <= rom.size()) {
972 for (uint32_t pc = marker_pc; pc > kZsFunctionSpacePc; --pc) {
973 if (rom.data()[pc - 1] != 0) {
974 check.zs_code_found = true;
975 check.zs_code_end_snes = LoRomPcToSnes(pc - 1);
976 check.free_bytes_before_marker = static_cast<int>(marker_pc - pc);
977 break;
978 }
979 }
980 }
981
982 const auto tables = OverworldAreaTableCatalogue(check.asm_version);
983 for (const auto& table : tables) {
984 if (table.Misses(kSpecialTailFirstArea)) {
985 check.tables_stopping_before_tail.push_back(table.id);
986 }
987 }
988
989 auto add = [&check](TailCheckScope scope, const char* id, std::string message,
990 bool blocking) {
991 check.findings.push_back({scope, id, std::move(message), blocking});
992 };
993
994 // ROM findings.
995 if (!IsZsV3(check.asm_version)) {
996 add(TailCheckScope::kRom, "zscustom_v3_required",
997 absl::StrFormat("ZSCustomOverworld v3 is required; this ROM reports "
998 "ASM version 0x%02X at $28:8145",
999 check.asm_version),
1000 true);
1001 }
1002 if (!check.marker_present) {
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);
1011 }
1012 add(TailCheckScope::kRom, "target_in_use", message, true);
1013 }
1014 }
1015 if (!check.regions.empty() &&
1016 check.rom_size < static_cast<size_t>(check.regions.back().pc +
1017 check.regions.back().length)) {
1018 add(TailCheckScope::kRom, "rom_too_small",
1019 absl::StrFormat(
1020 "the ROM is %d bytes; the patch writes up to PC "
1021 "0x%06X, so asar would expand it",
1022 static_cast<int>(check.rom_size),
1023 check.regions.back().pc + check.regions.back().length),
1024 false);
1025 }
1026 }
1027 for (const auto& region : check.regions) {
1028 if (!annotator.ownership) {
1029 break;
1030 }
1031 if (auto owner = annotator.ownership(region.snes);
1032 owner.has_value() && owner->second) {
1033 add(TailCheckScope::kRom, "target_owned",
1034 absl::StrFormat("hack manifest: %s (%s) is %s", SnesText(region.snes),
1035 region.label, owner->first),
1036 true);
1037 }
1038 }
1039 for (const auto& operand : check.operands) {
1040 if (operand.state == "other") {
1041 add(TailCheckScope::kRom, "operand_changed",
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)),
1046 true);
1047 } else if (operand.state == "patched" && !check.marker_present) {
1048 add(TailCheckScope::kRom, "operand_without_marker",
1049 absl::StrFormat("operand at %s already points to %s but the marker "
1050 "at %s is missing",
1051 SnesText(operand.snes), SnesText(operand.current),
1052 SnesText(plan.marker_snes)),
1053 true);
1054 } else if (operand.state == "missing") {
1055 add(TailCheckScope::kRom, "operand_missing",
1056 absl::StrFormat("operand at %s is past the end of the ROM",
1057 SnesText(operand.snes)),
1058 true);
1059 }
1060 if (annotator.ownership) {
1061 if (auto owner = annotator.ownership(operand.snes);
1062 owner.has_value() && owner->second) {
1063 add(TailCheckScope::kRom, "operand_owned",
1064 absl::StrFormat("hack manifest: operand at %s is %s",
1065 SnesText(operand.snes), owner->first),
1066 true);
1067 }
1068 }
1069 }
1070 if (check.zs_code_found && check.free_bytes_before_marker < 0x200) {
1071 add(TailCheckScope::kRom, "marker_near_zs_code",
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",
1075 SnesText(check.zs_code_end_snes),
1077 SnesText(plan.marker_snes), SnesText(plan.marker_snes),
1078 SnesText(plan.low_table_snes + table_bytes - 1)),
1079 false);
1080 }
1081
1082 // Patch findings.
1083 for (const auto& stream : check.streams) {
1084 if (stream.valid) {
1085 continue;
1086 }
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, ")");
1096 }
1097 } else {
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);
1101 }
1102 add(TailCheckScope::kPatch, "blank_stream_invalid", message, true);
1103 }
1104 std::vector<std::string> unpatched_tables;
1105 for (const auto& id : check.tables_stopping_before_tail) {
1106 if (id != "map32_high" && id != "map32_low") {
1107 unpatched_tables.push_back(id);
1108 }
1109 }
1110 if (!unpatched_tables.empty()) {
1111 add(TailCheckScope::kPatch, "tail_tables_missing",
1112 absl::StrFormat(
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, ", ")),
1118 true);
1119 }
1120
1121 // yaze findings.
1122 if (!plan.yaze_saves_relocated_tables) {
1123 add(TailCheckScope::kYaze, "pointer_save_desync",
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",
1127 true);
1128 }
1129 if (plan.yaze_map_count < plan.table_entries) {
1130 add(TailCheckScope::kYaze, "tail_maps_not_editable",
1131 absl::StrFormat("yaze loads and saves %d maps ($00-$%02X); maps "
1132 "$A0-$BF cannot be edited",
1133 plan.yaze_map_count, plan.yaze_map_count - 1),
1134 false);
1135 }
1136
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 ||
1142 (finding.blocking && finding.scope == TailCheckScope::kRom);
1143 }
1144 check.rom_compatible = !rom_blocking;
1145 if (check.marker_present) {
1147 } else {
1148 check.verdict =
1150 }
1151 return check;
1152}
1153
1154absl::Status TailMapExpansionApplyGuard(const Rom& rom) {
1156 if (check.verdict != TailCheckVerdict::kUnsafe) {
1157 return absl::OkStatus();
1158 }
1159 return absl::FailedPreconditionError(absl::StrCat(
1160 "Refusing to apply TailMapExpansion.asm: the compatibility check says "
1161 "unsafe.\n",
1162 check.BlockingSummary(),
1163 "\nRun `z3ed overworld-allocation --rom <rom> --tail-check` for the "
1164 "full report."));
1165}
1166
1167// ---------------------------------------------------------------------------
1168// Output
1169// ---------------------------------------------------------------------------
1170
1171namespace {
1172
1174 ordered_json j;
1175 j["id"] = table.id;
1176 j["label"] = table.label;
1177 j["group"] = table.group;
1178 j["location"] =
1179 table.in_rom
1180 ? absl::StrFormat("%s (PC 0x%06X)", SnesText(table.snes), table.pc)
1181 : absl::StrFormat("%s (RAM)", SnesText(table.snes));
1182 j["entry_bytes"] = table.entry_bytes;
1183 j["covers"] = RangeText(table.first_area, table.last_area);
1184 j["read_for"] = RangeText(table.applies_first, table.applies_last);
1185 j["misses_tail"] = table.Misses(kSpecialTailFirstArea);
1186 j["past_end"] = table.past_end;
1187 if (!table.note.empty()) {
1188 j["note"] = table.note;
1189 }
1190 return j;
1191}
1192
1194 ordered_json j;
1195 j["pointer"] = SnesText(scan.snes);
1196 j["pc"] = absl::StrFormat("0x%06X", scan.pc);
1197 j["terminated"] = scan.terminated;
1198 j["compressed_bytes"] = scan.compressed_bytes;
1199 j["decoded_bytes"] = scan.decoded_bytes;
1200 if (!scan.error.empty()) {
1201 j["error"] = scan.error;
1202 }
1203 return j;
1204}
1205
1207 ordered_json j;
1208 j["id"] = HexByte(area.area);
1209 j["world"] = AllocationWorldName(area.world);
1210 if (!area.name.empty()) {
1211 j["name"] = area.name;
1212 }
1213 j["used"] = area.used;
1214 j["evidence"] = area.evidence;
1215 j["parent"] = area.parent.has_value() ? ordered_json(HexByte(*area.parent))
1216 : ordered_json(nullptr);
1217 if (area.parent_past_end.has_value()) {
1218 j["parent_read_past_table"] = HexByte(*area.parent_past_end);
1219 }
1220 j["size"] = AllocationAreaSizeName(area.size);
1221 ordered_json map;
1222 map["source"] = area.map_pointer_source;
1223 map["valid"] = area.map_pointer_valid;
1224 if (area.map_pointer_source != "none") {
1225 map["high"] = StreamJson(area.high);
1226 map["low"] = StreamJson(area.low);
1227 map["unique_tile32"] = area.unique_tile32;
1228 map["duplicate_of"] = area.duplicate_of.has_value()
1229 ? ordered_json(HexByte(*area.duplicate_of))
1230 : ordered_json(nullptr);
1231 }
1232 j["map"] = map;
1233 j["entrances"] = area.entrances;
1234 j["holes"] = area.holes;
1235 j["exits"] = area.exits;
1236 j["sprites"] = area.sprites;
1237 j["uncovered_tables"] = area.uncovered_tables;
1238 return j;
1239}
1240
1242 ordered_json j;
1243 j["verdict"] = TailCheckVerdictName(check.verdict);
1244 j["rom_compatible"] = check.rom_compatible;
1245 j["asm_version"] = HexByte(check.asm_version);
1246 j["marker"] = {{"address", "$28:A3FF"},
1247 {"value", HexByte(check.marker_value)},
1248 {"present", check.marker_present}};
1249 ordered_json regions = ordered_json::array();
1250 for (const auto& region : check.regions) {
1251 ordered_json r;
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;
1262 }
1263 if (!region.symbol.empty()) {
1264 r["symbol"] = region.symbol;
1265 }
1266 regions.push_back(r);
1267 }
1268 j["regions"] = regions;
1269 ordered_json streams = ordered_json::array();
1270 for (const auto& stream : check.streams) {
1271 ordered_json s;
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;
1282 }
1283 s["valid"] = stream.valid;
1284 streams.push_back(s);
1285 }
1286 j["blank_streams"] = streams;
1287 ordered_json operands = ordered_json::array();
1288 for (const auto& operand : check.operands) {
1289 ordered_json o;
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);
1298 }
1299 j["operands"] = operands;
1300 if (check.zs_code_found) {
1301 j["zs_code_end"] = SnesText(check.zs_code_end_snes);
1302 j["free_bytes_before_marker"] = check.free_bytes_before_marker;
1303 } else {
1304 j["zs_code_end"] = nullptr;
1305 j["free_bytes_before_marker"] = nullptr;
1306 }
1307 j["tables_stopping_before_tail"] = check.tables_stopping_before_tail;
1308 ordered_json blockers = ordered_json::array();
1309 ordered_json warnings = ordered_json::array();
1310 for (const auto& finding : check.findings) {
1311 ordered_json f = {{"scope", TailCheckScopeName(finding.scope)},
1312 {"id", finding.id},
1313 {"message", finding.message}};
1314 (finding.blocking ? blockers : warnings).push_back(f);
1315 }
1316 j["blockers"] = blockers;
1317 j["warnings"] = warnings;
1318 return j;
1319}
1320
1321std::string MapColumn(const AreaAllocation& area) {
1322 if (area.map_pointer_source == "none") {
1323 return "none";
1324 }
1325 if (!area.map_pointer_valid) {
1326 return "invalid";
1327 }
1328 if (area.duplicate_of.has_value()) {
1329 return absl::StrCat("=", AreaText(*area.duplicate_of));
1330 }
1331 return area.unique_tile32 > 1 ? "data" : "blank";
1332}
1333
1334} // namespace
1335
1337 const TailMapExpansionCheck* tail_check) {
1338 ordered_json j;
1339 j["rom"] = report.rom_name;
1340 j["rom_size"] = report.rom_size;
1341 j["asm_version"] = HexByte(report.asm_version);
1342 j["catalogue"] = report.catalogue;
1343 j["tail_marker_present"] = report.tail_marker_present;
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}});
1352 }
1353 j["summary"] = summary;
1354 j["tables_stopping_before_tail"] = report.TablesStoppingBeforeTail();
1355 ordered_json tables = ordered_json::array();
1356 for (const auto& table : report.tables) {
1357 tables.push_back(TableJson(table));
1358 }
1359 j["tables"] = tables;
1360 ordered_json areas = ordered_json::array();
1361 for (const auto& area : report.areas) {
1362 areas.push_back(AreaJson(area));
1363 }
1364 j["areas"] = areas;
1365 if (tail_check != nullptr) {
1366 j["tail_check"] = TailJson(*tail_check);
1367 }
1368 return j.dump(2);
1369}
1370
1372 const OverworldAllocationReport& report,
1373 const TailMapExpansionCheck* tail_check) {
1374 std::ostringstream out;
1375 out << absl::StrFormat("Overworld allocation: %s (%d bytes)\n",
1376 report.rom_name.empty() ? "<memory>" : report.rom_name,
1377 static_cast<int>(report.rom_size));
1378 out << absl::StrFormat(
1379 "Layout: ASM version 0x%02X, %s tables; TailMapExpansion marker %s\n\n",
1380 report.asm_version, report.catalogue,
1381 report.tail_marker_present ? "present" : "absent");
1382
1383 out << "Summary\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);
1391 }
1392
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",
1397 "past the end");
1398 for (const auto& table : report.tables) {
1399 const std::string location =
1400 table.in_rom
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);
1408 }
1409 const auto stopping = report.TablesStoppingBeforeTail();
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()));
1414
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 "
1419 "%4s %s\n",
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);
1426 }
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);
1432 }
1433 out << absl::StrFormat(
1434 " %-4s %-7s %-4s %-6s %-5s %-7s %5s %4d %4d %4d %4d %4d %s\n",
1435 AreaText(area.area), AllocationWorldName(area.world),
1436 area.used ? "yes" : "no", parent, AllocationAreaSizeName(area.size),
1437 MapColumn(area),
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);
1442 }
1443
1444 if (tail_check != nullptr) {
1445 const TailMapExpansionCheck& c = *tail_check;
1446 out << "\nTailMapExpansion check "
1447 "(assets/patches/Overworld/TailMapExpansion.asm)\n";
1448 out << absl::StrFormat(" Verdict: %s (ROM compatible: %s)\n",
1450 c.rom_compatible ? "yes" : "no");
1451 out << absl::StrFormat(" Marker $28:A3FF: 0x%02X (%s)\n", c.marker_value,
1452 c.marker_present ? "present" : "absent");
1453 if (c.zs_code_found) {
1454 out << absl::StrFormat(
1455 " ZS code ends at %s; %d bytes free before $28:A3FF\n",
1457 } else {
1458 out << " ZS code end: not found (not a ZS v3 ROM)\n";
1459 }
1460 out << " Target regions:\n";
1461 for (const auto& region : c.regions) {
1462 std::string extra;
1463 if (!region.symbol.empty()) {
1464 absl::StrAppend(&extra, " symbol ", region.symbol);
1465 }
1466 if (!region.owner.empty()) {
1467 absl::StrAppend(&extra, " owner ", region.owner);
1468 }
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);
1474 }
1475 out << " Blank streams (decoded as the game would read them after the "
1476 "patch):\n";
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");
1484 }
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 "
1489 "%s\n",
1490 SnesText(operand.snes), operand.pc, operand.opcode,
1491 SnesText(operand.current), SnesText(operand.vanilla),
1492 SnesText(operand.patched), operand.state);
1493 }
1494 out << " Blockers:\n";
1495 bool any = false;
1496 for (const auto& finding : c.findings) {
1497 if (finding.blocking) {
1498 any = true;
1499 out << absl::StrFormat(" [%s] %s: %s\n",
1500 TailCheckScopeName(finding.scope), finding.id,
1501 finding.message);
1502 }
1503 }
1504 if (!any) {
1505 out << " none\n";
1506 }
1507 out << " Warnings:\n";
1508 any = false;
1509 for (const auto& finding : c.findings) {
1510 if (!finding.blocking) {
1511 any = true;
1512 out << absl::StrFormat(" [%s] %s: %s\n",
1513 TailCheckScopeName(finding.scope), finding.id,
1514 finding.message);
1515 }
1516 }
1517 if (!any) {
1518 out << " none\n";
1519 }
1520 }
1521 return out.str();
1522}
1523
1524} // 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:49
auto filename() const
Definition rom.h:207
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
std::string HexBytes(const uint8_t *data, size_t count)
AllocationAreaSize DecodeAreaSize(uint8_t value, bool zs_v3)
std::string JoinAreas(const std::vector< int > &areas)
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)
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)
ordered_json TailJson(const TailMapExpansionCheck &check)
void ScanAreaMap(const Rom &rom, uint32_t high_table_pc, uint32_t low_table_pc, int index, AreaAllocation *area)
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::vector< std::string > uncovered_tables
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::vector< WorldAllocationSummary > summary
std::vector< std::string > TablesStoppingBeforeTail() const
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
std::vector< std::string > tables_stopping_before_tail
std::vector< TailCheckFinding > findings
std::array< uint32_t, 8 > operand_patched
std::array< uint32_t, 8 > operand_vanilla