218 if (image ==
nullptr || size <= kDescriptorPc) {
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;
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) {
235 if (available != kDescriptorSize) {
236 return Invalid(
"Truncated Lyra descriptor region");
238 if (std::memcmp(d, kMagic, 8) != 0) {
239 return Invalid(
"Unknown Lyra descriptor magic");
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);
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");
256 const bool offline_only = d[0x15] & 1;
257 const bool supported_revision2 =
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");
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");
268 if (U32(d + 0x18) != size) {
269 return bad(
"Lyra image length mismatch");
271 if (image[0x140145] != 3 || image[0x1423FF] == 0xEA) {
272 return bad(
"Conflicting ZS version or older $EA tail identity");
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");
280 if (std::any_of(d + used_size, d + kDescriptorSize,
281 [](uint8_t b) {
return b != 0; })) {
282 return bad(
"Nonzero Lyra descriptor padding");
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];
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");
303 if (!ToPc(e.snes_address, &e.pc_address)) {
304 return bad(
"Invalid Lyra directory bank or address");
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");
310 if (e.pc_address > size || length > size - e.pc_address) {
311 return bad(
"Lyra directory outside image extent");
314 if (i == 29 && e.snes_address != 0x02F88D) {
315 return bad(
"Invalid Lyra area-size table address");
317 if (i == 30 && e.snes_address != 0x09C635) {
318 return bad(
"Invalid Lyra palette-set table address");
321 (e.pc_address < 0x144000 || e.pc_address + length > 0x146840)) {
322 return bad(
"Lyra table outside owned bank-28 reservation");
324 if (i < 19 && (e.snes_address >> 16) != 0x28) {
325 return bad(
"Lyra Pool table bank mismatch");
327 if (i > 0 && i < 19 &&
328 result.
entries.back().pc_address +
331 return bad(
"Lyra Pool table overlap or gap");
333 }
else if (i == 31) {
334 if (e.pc_address != 0x1E4000) {
335 return bad(
"Lyra sprite list region changed");
337 }
else if (i < 35 && e.pc_address != kStreamWindows[i - 32].first) {
338 return bad(
"Lyra stream region changed");
340 return bad(
"Lyra A0 stream reservation changed");
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) {
347 if (e.id == 203 && prior.id == 202 &&
348 e.pc_address >= prior.pc_address &&
349 e.pc_address + length <= prior_end) {
352 return bad(
"Lyra directory overlap");
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");
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");
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),
375 return bad(
"Lyra map pointer outside owned stream regions");
377 size_t consumed_end = 0;
378 if (!DecodeStream(image, size, stream_pc, table == 19 ? &high : &low,
380 return bad(
"Lyra compressed map stream invalid");
382 if (supported_revision2) {
383 const uint32_t slot_begin =
387 if (stream_pc != slot_begin ||
389 return bad(
"Lyra A0 stream exceeds its dedicated slot");
391 }
else if (stream_pc < owned_end &&
393 return bad(
"Lyra A0 stream reservation overlaps another map");
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");
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");
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");
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");
459 if (!high_record || !low_record || high_record->size() != 3 ||
460 low_record->size() != 3) {
463 std::array<uint8_t, 256> high{};
464 std::array<uint8_t, 256> low{};
465 uint32_t high_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)) {
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) {
484 const std::vector<uint8_t>& baseline,
485 const std::array<uint16_t, 256>& tiles) {
488 !layout.a0_terrain_write_supported) {
489 return absl::FailedPreconditionError(
490 "Revision-2 A0 terrain capability required for Save As");
492 if (std::any_of(tiles.begin(), tiles.end(), [](uint16_t tile) {
493 return tile != 0x2103 && tile != 0x20B2;
495 return absl::InvalidArgumentError(
496 "Lyra pilot terrain supports only existing Tile32 2103 and 20B2");
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");
506 auto staged = baseline;
509 for (
int plane = 0; plane < 2; ++plane) {
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]);
517 staged[pc + 258] = 0xFF;
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));
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));
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) {
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");
547 LyraMapTile32(checked, staged.data(), staged.size(), 0xA0) != tiles) {
548 return absl::InternalError(
"Lyra terrain staged image failed readback");