yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
lyra_layout_descriptor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstddef>
6#include <cstdint>
7#include <cstring>
8#include <string>
9#include <utility>
10#include <vector>
11
12namespace yaze::rom {
13namespace {
14
15constexpr size_t kDescriptorPc = 0x147000;
16constexpr size_t kDescriptorSize = 1024;
17constexpr size_t kHeaderSize = 32;
18constexpr size_t kEntrySize = 16;
19constexpr size_t kRevision1EntryCount = 35;
20constexpr size_t kEntryCount = 36;
21constexpr size_t kRevision1UsedSize =
23constexpr char kMagic[] = "OOSOW192";
24
25struct EntrySpec {
26 uint16_t id;
27 uint16_t kind;
28 uint16_t count;
29 uint16_t stride;
30};
31
32// Revision-1's directory order, domains and widths. Addresses are read from
33// the descriptor and then checked against the audited allocation windows.
34constexpr std::array<EntrySpec, kEntryCount> kSpecs = {{
35 {1, 1, 192, 2}, {2, 1, 192, 1}, {3, 1, 192, 1},
36 {4, 1, 192, 1}, {5, 1, 192, 2}, {6, 1, 192, 8},
37 {7, 1, 192, 1}, {8, 1, 192, 2}, {9, 1, 192, 2},
38 {10, 1, 192, 2}, {11, 1, 192, 2}, {12, 1, 192, 2},
39 {13, 1, 192, 2}, {14, 1, 192, 2}, {15, 1, 192, 2},
40 {16, 1, 192, 2}, {17, 1, 192, 2}, {18, 1, 192, 2},
41 {19, 1, 192, 2}, {20, 1, 192, 3}, {21, 1, 192, 3},
42 {22, 1, 192, 2}, {23, 1, 192, 2}, {24, 1, 64, 1},
43 {25, 1, 64, 1}, {26, 1, 192, 1}, {27, 1, 64, 1},
44 {28, 1, 128, 1}, {29, 1, 192, 1}, {30, 1, 192, 1},
45 {31, 1, 192, 1}, {100, 2, 16384, 1}, {200, 3, 32368, 1},
46 {201, 3, 16671, 1}, {202, 3, 32768, 1}, {203, 4, 2, 512},
47}};
48
49constexpr std::array<std::pair<uint32_t, uint32_t>, 3> kStreamWindows = {{
50 {0x58000, 0x5FE70},
51 {0x60000, 0x6411F},
52 {0x130000, 0x138000},
53}};
54
55uint16_t U16(const uint8_t* p) {
56 return static_cast<uint16_t>(p[0] | (p[1] << 8));
57}
58
59uint32_t U24(const uint8_t* p) {
60 return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) |
61 (static_cast<uint32_t>(p[2]) << 16);
62}
63
64uint32_t U32(const uint8_t* p) {
65 return U24(p) | (static_cast<uint32_t>(p[3]) << 24);
66}
67
68uint32_t Crc32(const uint8_t* p, size_t length) {
69 uint32_t crc = 0xFFFFFFFF;
70 for (size_t i = 0; i < length; ++i) {
71 crc ^= p[i];
72 for (int bit = 0; bit < 8; ++bit) {
73 crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320U : 0U);
74 }
75 }
76 return crc ^ 0xFFFFFFFF;
77}
78
79bool ToPc(uint32_t snes, uint32_t* pc) {
80 const uint32_t bank = snes >> 16;
81 if (bank > 0x3F || (snes & 0x8000) == 0) {
82 return false;
83 }
84 *pc = bank * 0x8000 + (snes & 0x7FFF);
85 return true;
86}
87
88LyraLayoutInspection Invalid(std::string error, uint16_t revision = 0,
89 uint16_t capacity = 0, uint16_t active = 0) {
92 result.revision = revision;
93 result.capacity = capacity;
94 result.active_count = active;
95 result.write_supported = false;
96 result.error = std::move(error);
97 return result;
98}
99
100bool InStreamWindow(uint32_t pc) {
101 for (const auto& [begin, end] : kStreamWindows) {
102 if (pc >= begin && pc < end) {
103 return true;
104 }
105 }
106 return false;
107}
108
109bool StreamPointerPc(size_t size, uint32_t snes, uint32_t* pc) {
110 // Existing stream tables use $8B/$8C/$A6 cartridge mirrors. Only bit 23
111 // is normalized; every result must still land in a declared stream window.
112 return ToPc(snes & 0x7FFFFF, pc) && *pc < size && InStreamWindow(*pc);
113}
114
115bool DecodeStream(const uint8_t* image, size_t size, uint32_t start,
116 std::array<uint8_t, 256>* decoded,
117 size_t* consumed_end = nullptr) {
118 size_t limit = std::min<size_t>(size, ((start / 0x8000) + 1) * 0x8000);
119 for (const auto& [begin, end] : kStreamWindows) {
120 if (start >= begin && start < end) {
121 limit = std::min<size_t>(limit, end);
122 break;
123 }
124 }
125 size_t p = start;
126 size_t used = 0;
127 auto take = [&](uint8_t* byte) {
128 if (p >= limit)
129 return false;
130 *byte = image[p++];
131 return true;
132 };
133 while (p < limit) {
134 uint8_t command = 0;
135 if (!take(&command))
136 return false;
137 if (command == 0xFF) {
138 if (consumed_end != nullptr)
139 *consumed_end = p;
140 return used == decoded->size();
141 }
142 unsigned kind = 0;
143 unsigned count = 0;
144 if (command >= 0xE0) {
145 uint8_t low = 0;
146 if (!take(&low))
147 return false;
148 kind = (command >> 2) & 7;
149 count = ((command & 3) << 8) + low + 1;
150 } else {
151 kind = command >> 5;
152 count = (command & 31) + 1;
153 }
154 if (used + count > decoded->size())
155 return false;
156 uint8_t a = 0;
157 uint8_t b = 0;
158 if (kind == 0) {
159 for (unsigned i = 0; i < count; ++i) {
160 if (!take(&(*decoded)[used++]))
161 return false;
162 }
163 } else if (kind == 1) {
164 if (!take(&a))
165 return false;
166 std::fill_n(decoded->begin() + used, count, a);
167 used += count;
168 } else if (kind == 2) {
169 if (!take(&a) || !take(&b))
170 return false;
171 for (unsigned i = 0; i < count; ++i) {
172 (*decoded)[used++] = i % 2 ? b : a;
173 }
174 } else if (kind == 3) {
175 if (!take(&a))
176 return false;
177 for (unsigned i = 0; i < count; ++i) {
178 (*decoded)[used++] = static_cast<uint8_t>(a + i);
179 }
180 } else {
181 if (!take(&a) || !take(&b))
182 return false;
183 const unsigned offset = (a << 8) | b;
184 for (unsigned i = 0; i < count; ++i) {
185 if (offset + i >= used)
186 return false;
187 (*decoded)[used++] = (*decoded)[offset + i];
188 }
189 }
190 }
191 return false;
192}
193
194bool CheckSpritePointer(const uint8_t* image, size_t size, uint16_t word) {
195 if (word < 0xC000) {
196 return false;
197 }
198 size_t pc = 0x1E0000 + (word & 0x7FFF);
199 for (int record = 0; record <= 128; ++record) {
200 if (pc >= 0x1E8000 || pc >= size) {
201 return false;
202 }
203 if (image[pc] == 0xFF) {
204 return true;
205 }
206 if (record == 128 || pc + 3 > 0x1E8000) {
207 return false;
208 }
209 pc += 3;
210 }
211 return false;
212}
213
214} // namespace
215
216LyraLayoutInspection InspectLyraLayout(const uint8_t* image, size_t size) {
218 if (image == nullptr || size <= kDescriptorPc) {
219 return absent;
220 }
221 const uint8_t* d = image + kDescriptorPc;
222 const size_t available = std::min(kDescriptorSize, size - kDescriptorPc);
223 const bool signature_prefix =
224 available >= 5 && std::memcmp(d, "OOSOW", 5) == 0;
225 // A damaged magic still belongs to this format when its independent header
226 // and directory fingerprint survive. Nonblank bytes alone mean nothing.
227 const bool directory_fingerprint =
228 available >= kRevision1UsedSize && U16(d + 0x0A) == kHeaderSize &&
229 U16(d + 0x0E) == 192 && (U16(d + 0x10) == 160 || U16(d + 0x10) == 161) &&
230 (U16(d + 0x12) == kRevision1EntryCount || U16(d + 0x12) == kEntryCount) &&
231 U16(d + kHeaderSize) == 1 && U16(d + kHeaderSize + 2) == 1;
232 if (!signature_prefix && !directory_fingerprint) {
233 return absent;
234 }
235 if (available != kDescriptorSize) {
236 return Invalid("Truncated Lyra descriptor region");
237 }
238 if (std::memcmp(d, kMagic, 8) != 0) {
239 return Invalid("Unknown Lyra descriptor magic");
240 }
241
242 const uint16_t revision = U16(d + 8);
243 const uint16_t capacity = U16(d + 0x0E);
244 const uint16_t active = U16(d + 0x10);
245 auto bad = [&](std::string error) {
246 return Invalid(std::move(error), revision, capacity, active);
247 };
248 if (revision != 1 && revision != 2)
249 return bad("Unsupported Lyra ROM-format revision");
250 const size_t entry_count = revision == 1 ? kRevision1EntryCount : kEntryCount;
251 const size_t used_size = kHeaderSize + entry_count * kEntrySize;
252 if (U16(d + 0x0A) != kHeaderSize || U16(d + 0x0C) != used_size ||
253 U16(d + 0x12) != entry_count) {
254 return bad("Invalid Lyra descriptor length or directory count");
255 }
256 const bool offline_only = d[0x15] & 1;
257 const bool supported_revision2 =
258 revision == 2 &&
259 ((d[0x15] == 3 && active == 160) || (d[0x15] == 2 && active == 161));
260 if (capacity != 192 ||
261 (revision == 1 ? active != 160 : !supported_revision2)) {
262 return bad("Invalid Lyra capacity or active count");
263 }
264 if (d[0x14] != 0x3C || (revision == 1 && d[0x15] != 1) ||
265 U16(d + 0x16) != 384) {
266 return bad("Unsupported Lyra sprite bank, flags or phase stride");
267 }
268 if (U32(d + 0x18) != size) {
269 return bad("Lyra image length mismatch");
270 }
271 if (image[0x140145] != 3 || image[0x1423FF] == 0xEA) {
272 return bad("Conflicting ZS version or older $EA tail identity");
273 }
274 std::array<uint8_t, kDescriptorSize> checked;
275 std::copy_n(d, kDescriptorSize, checked.begin());
276 std::fill_n(checked.begin() + 0x1C, 4, 0);
277 if (Crc32(checked.data(), checked.size()) != U32(d + 0x1C)) {
278 return bad("Lyra descriptor CRC mismatch");
279 }
280 if (std::any_of(d + used_size, d + kDescriptorSize,
281 [](uint8_t b) { return b != 0; })) {
282 return bad("Nonzero Lyra descriptor padding");
283 }
284
287 result.revision = revision;
288 result.capacity = capacity;
289 result.active_count = active;
290 result.write_supported = false;
291 result.a0_terrain_write_supported = supported_revision2 && offline_only;
292 result.offline_only = offline_only;
293 result.entries.reserve(entry_count);
294 for (size_t i = 0; i < entry_count; ++i) {
295 const uint8_t* p = d + kHeaderSize + i * kEntrySize;
296 const auto& spec = kSpecs[i];
297 LyraLayoutEntry e{U16(p), U16(p + 2), U32(p + 4), 0,
298 U16(p + 8), U16(p + 10), U32(p + 12)};
299 if (e.id != spec.id || e.kind != spec.kind || e.count != spec.count ||
300 e.stride != spec.stride) {
301 return bad("Invalid Lyra directory entry");
302 }
303 if (!ToPc(e.snes_address, &e.pc_address)) {
304 return bad("Invalid Lyra directory bank or address");
305 }
306 const uint32_t length = static_cast<uint32_t>(e.count) * e.stride;
307 if ((e.snes_address & 0xFFFF) + length > 0x10000) {
308 return bad("Lyra table crosses bank");
309 }
310 if (e.pc_address > size || length > size - e.pc_address) {
311 return bad("Lyra directory outside image extent");
312 }
313 if (i < 31) {
314 if (i == 29 && e.snes_address != 0x02F88D) {
315 return bad("Invalid Lyra area-size table address");
316 }
317 if (i == 30 && e.snes_address != 0x09C635) {
318 return bad("Invalid Lyra palette-set table address");
319 }
320 if (i < 29 &&
321 (e.pc_address < 0x144000 || e.pc_address + length > 0x146840)) {
322 return bad("Lyra table outside owned bank-28 reservation");
323 }
324 if (i < 19 && (e.snes_address >> 16) != 0x28) {
325 return bad("Lyra Pool table bank mismatch");
326 }
327 if (i > 0 && i < 19 &&
328 result.entries.back().pc_address +
329 result.entries.back().count * result.entries.back().stride !=
330 e.pc_address) {
331 return bad("Lyra Pool table overlap or gap");
332 }
333 } else if (i == 31) {
334 if (e.pc_address != 0x1E4000) {
335 return bad("Lyra sprite list region changed");
336 }
337 } else if (i < 35 && e.pc_address != kStreamWindows[i - 32].first) {
338 return bad("Lyra stream region changed");
339 } else if (i == 35 && e.pc_address != kLyraA0HighStreamPc) {
340 return bad("Lyra A0 stream reservation changed");
341 }
342 for (const auto& prior : result.entries) {
343 const uint32_t prior_end = prior.pc_address + prior.count * prior.stride;
344 if (e.pc_address < prior_end &&
345 prior.pc_address < e.pc_address + length) {
346 // Entry 203 subdivides region 202. No other directory overlap is legal.
347 if (e.id == 203 && prior.id == 202 &&
348 e.pc_address >= prior.pc_address &&
349 e.pc_address + length <= prior_end) {
350 continue;
351 }
352 return bad("Lyra directory overlap");
353 }
354 }
355 result.entries.push_back(e);
356 }
357 if (result.entries[16].pc_address - result.entries[15].pc_address != 384 ||
358 result.entries[17].pc_address - result.entries[16].pc_address != 384) {
359 return bad("Lyra sprite phase stride mismatch");
360 }
361 for (const auto& e : result.entries) {
362 const size_t length = static_cast<size_t>(e.count) * e.stride;
363 if (Crc32(image + e.pc_address, length) != e.crc32) {
364 return bad("Lyra data CRC mismatch");
365 }
366 }
367 for (size_t map = 0; map < 192; ++map) {
368 std::array<uint8_t, 256> high{};
369 std::array<uint8_t, 256> low{};
370 for (size_t table : {19U, 20U}) {
371 const auto& e = result.entries[table];
372 uint32_t stream_pc = 0;
373 if (!StreamPointerPc(size, U24(image + e.pc_address + map * 3),
374 &stream_pc)) {
375 return bad("Lyra map pointer outside owned stream regions");
376 }
377 size_t consumed_end = 0;
378 if (!DecodeStream(image, size, stream_pc, table == 19 ? &high : &low,
379 &consumed_end)) {
380 return bad("Lyra compressed map stream invalid");
381 }
382 if (supported_revision2) {
383 const uint32_t slot_begin =
385 const uint32_t owned_end = kLyraA0LowStreamPc + kLyraA0StreamSlotSize;
386 if (map == 0xA0) {
387 if (stream_pc != slot_begin ||
388 consumed_end > slot_begin + kLyraA0StreamSlotSize) {
389 return bad("Lyra A0 stream exceeds its dedicated slot");
390 }
391 } else if (stream_pc < owned_end &&
392 consumed_end > kLyraA0HighStreamPc) {
393 return bad("Lyra A0 stream reservation overlaps another map");
394 }
395 }
396 }
397 for (size_t tile = 0; tile < high.size(); ++tile) {
398 if ((static_cast<uint16_t>(high[tile]) << 8 | low[tile]) >= 17728) {
399 return bad("Lyra Tile32 reference exceeds existing capacity");
400 }
401 }
402 for (size_t table : {15U, 16U, 17U}) {
403 const auto& e = result.entries[table];
404 if (!CheckSpritePointer(image, size,
405 U16(image + e.pc_address + map * 2))) {
406 return bad("Lyra sprite pointer outside list region or unterminated");
407 }
408 }
409 if (map >= active) {
410 const auto& parent = result.entries[6];
411 const auto& role = result.entries[28];
412 const auto& overlay = result.entries[4];
413 const auto& area_size = result.entries[29];
414 if (image[parent.pc_address + map] != map ||
415 image[role.pc_address + map] != 3 ||
416 U16(image + overlay.pc_address + map * 2) != 0x00FF ||
417 image[area_size.pc_address + map] != 0) {
418 return bad("Lyra inactive tail role or parent is invalid");
419 }
420 }
421 if (supported_revision2 && map == 0xA0 &&
422 (image[result.entries[6].pc_address + map] != map ||
423 image[result.entries[28].pc_address + map] != (offline_only ? 3 : 0) ||
424 U16(image + result.entries[4].pc_address + map * 2) != 0x00FF ||
425 image[result.entries[29].pc_address + map] != 0)) {
426 return bad("Lyra A0 terrain role, parent, overlay or size is invalid");
427 }
428 }
429 return result;
430}
431
432std::optional<std::span<const uint8_t>> LyraLayoutRecord(
433 const LyraLayoutInspection& layout, const uint8_t* image, size_t size,
434 uint16_t table_id, size_t index) {
435 if (layout.presence != LyraLayoutPresence::kValid || image == nullptr) {
436 return std::nullopt;
437 }
438 for (const auto& entry : layout.entries) {
439 if (entry.id != table_id) {
440 continue;
441 }
442 if (entry.kind != 1 || index >= entry.count) {
443 return std::nullopt;
444 }
445 const size_t start = entry.pc_address + index * entry.stride;
446 if (start > size || entry.stride > size - start) {
447 return std::nullopt;
448 }
449 return std::span<const uint8_t>(image + start, entry.stride);
450 }
451 return std::nullopt;
452}
453
454std::optional<std::array<uint16_t, 256>> LyraMapTile32(
455 const LyraLayoutInspection& layout, const uint8_t* image, size_t size,
456 size_t map_index) {
457 auto high_record = LyraLayoutRecord(layout, image, size, 20, map_index);
458 auto low_record = LyraLayoutRecord(layout, image, size, 21, map_index);
459 if (!high_record || !low_record || high_record->size() != 3 ||
460 low_record->size() != 3) {
461 return std::nullopt;
462 }
463 std::array<uint8_t, 256> high{};
464 std::array<uint8_t, 256> low{};
465 uint32_t high_pc = 0;
466 uint32_t low_pc = 0;
467 if (!StreamPointerPc(size, U24(high_record->data()), &high_pc) ||
468 !StreamPointerPc(size, U24(low_record->data()), &low_pc) ||
469 !DecodeStream(image, size, high_pc, &high) ||
470 !DecodeStream(image, size, low_pc, &low)) {
471 return std::nullopt;
472 }
473 std::array<uint16_t, 256> tiles{};
474 for (size_t i = 0; i < tiles.size(); ++i) {
475 tiles[i] = static_cast<uint16_t>((high[i] << 8) | low[i]);
476 if (tiles[i] >= 17728) {
477 return std::nullopt;
478 }
479 }
480 return tiles;
481}
482
483absl::StatusOr<std::vector<uint8_t>> SerializeLyraA0Terrain(
484 const std::vector<uint8_t>& baseline,
485 const std::array<uint16_t, 256>& tiles) {
486 const auto layout = InspectLyraLayout(baseline.data(), baseline.size());
487 if (layout.presence != LyraLayoutPresence::kValid ||
488 !layout.a0_terrain_write_supported) {
489 return absl::FailedPreconditionError(
490 "Revision-2 A0 terrain capability required for Save As");
491 }
492 if (std::any_of(tiles.begin(), tiles.end(), [](uint16_t tile) {
493 return tile != 0x2103 && tile != 0x20B2;
494 })) {
495 return absl::InvalidArgumentError(
496 "Lyra pilot terrain supports only existing Tile32 2103 and 20B2");
497 }
498 for (size_t y : {7U, 8U}) {
499 for (size_t x : {7U, 8U}) {
500 if (tiles[y * 16 + x] != 0x2103) {
501 return absl::FailedPreconditionError(
502 "Lyra A0 entry and return anchors must remain walkable");
503 }
504 }
505 }
506 auto staged = baseline;
507 // A literal block has bounded size: two-byte command + 256 bytes + FF.
508 // Preserve all unused slot bytes; never repack shared map-stream storage.
509 for (int plane = 0; plane < 2; ++plane) {
510 const size_t pc = plane == 0 ? kLyraA0HighStreamPc : kLyraA0LowStreamPc;
511 staged[pc] = 0xE0;
512 staged[pc + 1] = 0xFF;
513 for (size_t tile = 0; tile < tiles.size(); ++tile) {
514 staged[pc + 2 + tile] =
515 static_cast<uint8_t>(plane == 0 ? tiles[tile] >> 8 : tiles[tile]);
516 }
517 staged[pc + 258] = 0xFF;
518 }
519 auto put32 = [&](size_t pc, uint32_t value) {
520 for (size_t i = 0; i < 4; ++i)
521 staged[pc + i] = static_cast<uint8_t>(value >> (8 * i));
522 };
523 for (size_t index : {34U, 35U}) {
524 const auto& e = layout.entries[index];
525 put32(kDescriptorPc + kHeaderSize + index * kEntrySize + 12,
526 Crc32(staged.data() + e.pc_address, e.count * e.stride));
527 }
528 put32(kDescriptorPc + 28, 0);
529 put32(kDescriptorPc + 28,
530 Crc32(staged.data() + kDescriptorPc, kDescriptorSize));
531 for (size_t pc = 0; pc < baseline.size(); ++pc) {
532 const bool allowed =
533 (pc >= kLyraA0HighStreamPc && pc < kLyraA0HighStreamPc + 259) ||
534 (pc >= kLyraA0LowStreamPc && pc < kLyraA0LowStreamPc + 259) ||
535 (pc >= kDescriptorPc + 28 && pc < kDescriptorPc + 32) ||
536 (pc >= kDescriptorPc + 32 + 34 * 16 + 12 &&
537 pc < kDescriptorPc + 32 + 34 * 16 + 16) ||
538 (pc >= kDescriptorPc + 32 + 35 * 16 + 12 &&
539 pc < kDescriptorPc + 32 + 35 * 16 + 16);
540 if (!allowed && staged[pc] != baseline[pc]) {
541 return absl::InternalError(
542 "Lyra terrain staging changed an unowned byte");
543 }
544 }
545 const auto checked = InspectLyraLayout(staged.data(), staged.size());
546 if (checked.presence != LyraLayoutPresence::kValid ||
547 LyraMapTile32(checked, staged.data(), staged.size(), 0xA0) != tiles) {
548 return absl::InternalError("Lyra terrain staged image failed readback");
549 }
550 return staged;
551}
552
553} // namespace yaze::rom
bool StreamPointerPc(size_t size, uint32_t snes, uint32_t *pc)
bool CheckSpritePointer(const uint8_t *image, size_t size, uint16_t word)
constexpr std::array< std::pair< uint32_t, uint32_t >, 3 > kStreamWindows
constexpr std::array< EntrySpec, kEntryCount > kSpecs
bool DecodeStream(const uint8_t *image, size_t size, uint32_t start, std::array< uint8_t, 256 > *decoded, size_t *consumed_end=nullptr)
std::optional< std::span< const uint8_t > > LyraLayoutRecord(const LyraLayoutInspection &layout, const uint8_t *image, size_t size, uint16_t table_id, size_t index)
constexpr uint32_t kLyraA0LowStreamPc
constexpr uint32_t kLyraA0HighStreamPc
LyraLayoutInspection InspectLyraLayout(const uint8_t *image, size_t size)
std::optional< std::array< uint16_t, 256 > > LyraMapTile32(const LyraLayoutInspection &layout, const uint8_t *image, size_t size, size_t map_index)
absl::StatusOr< std::vector< uint8_t > > SerializeLyraA0Terrain(const std::vector< uint8_t > &baseline, const std::array< uint16_t, 256 > &tiles)
constexpr size_t kLyraA0StreamSlotSize
std::vector< LyraLayoutEntry > entries