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