Gen 2 (GSC) is the one mainline dex that records which Unown letters
you've caught — a 26-byte ordered list right after the Seen flag array
(seen + 0x20). Parse it, map letters to the app's Form Dex slugs
('unown' for A, 'unown-b'…'unown-z'), and tick them via a new
markFormsCaught() on the formTracking store. A garbage/out-of-range list
is ignored rather than guessed at.
The import dialog and result line now surface "· N Unown letters".
Verified: 6 new unit tests (synthetic Gen 2 SRAM → slugs, empty list,
garbage rejection; markFormsCaught behaviour) + a real end-to-end import
of a synthetic Crystal save writing {unown, unown-c, unown-o, unown-z}
to pdx.formTracking.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
100 lines
3.4 KiB
JavaScript
100 lines
3.4 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { parseSaveDex } from '../src/lib/savedex.js';
|
|
|
|
// --- Synthetic Gen 1 (R/B/Y) SRAM -------------------------------------
|
|
// 32 KB, with the owned/seen bitfields and the byte checksum the parser
|
|
// verifies. Offsets mirror src/lib/savedex.js `G1`.
|
|
const G1 = {
|
|
owned: 0x25a3,
|
|
seen: 0x25b6,
|
|
species: 151,
|
|
ckByte: 0x3523,
|
|
ckFrom: 0x2598,
|
|
ckTo: 0x3522,
|
|
};
|
|
|
|
function setBits(bytes, start, ids) {
|
|
for (const n of ids) bytes[start + ((n - 1) >> 3)] |= 1 << ((n - 1) & 7);
|
|
}
|
|
|
|
function makeGen1Save({ caught, seen }) {
|
|
const bytes = new Uint8Array(0x8000);
|
|
setBits(bytes, G1.owned, caught);
|
|
setBits(bytes, G1.seen, seen);
|
|
let sum = 0;
|
|
for (let i = G1.ckFrom; i <= G1.ckTo; i++) sum = (sum + bytes[i]) >>> 0;
|
|
bytes[G1.ckByte] = ~sum & 0xff;
|
|
return bytes.buffer;
|
|
}
|
|
|
|
describe('parseSaveDex — Gen 1', () => {
|
|
it('reads owned/seen and verifies the checksum', () => {
|
|
const caught = [1, 25, 151];
|
|
const seen = [1, 4, 25, 151];
|
|
const dex = parseSaveDex(makeGen1Save({ caught, seen }));
|
|
|
|
expect(dex.gen).toBe(1);
|
|
expect(dex.game).toBe('Red / Blue / Yellow');
|
|
expect(dex.versionGroups).toEqual(['red-blue', 'yellow']);
|
|
expect(dex.caught).toEqual(caught);
|
|
expect(dex.seen).toEqual(seen);
|
|
expect(dex.checksumOk).toBe(true);
|
|
expect(dex.nationalUnlocked).toBe(true);
|
|
});
|
|
|
|
it('still parses (checksumOk false) when the checksum byte is wrong', () => {
|
|
const buf = makeGen1Save({ caught: [1], seen: [1, 2] });
|
|
new Uint8Array(buf)[G1.ckByte] ^= 0xff;
|
|
const dex = parseSaveDex(buf);
|
|
expect(dex.gen).toBe(1);
|
|
expect(dex.caught).toEqual([1]);
|
|
expect(dex.checksumOk).toBe(false);
|
|
});
|
|
|
|
it('rejects a file that is too small', () => {
|
|
expect(() => parseSaveDex(new Uint8Array(1024).buffer)).toThrow(/too small/i);
|
|
});
|
|
|
|
it('rejects a large file with no recognisable Pokédex', () => {
|
|
expect(() => parseSaveDex(new Uint8Array(0x20000).buffer)).toThrow();
|
|
});
|
|
});
|
|
|
|
// --- Synthetic Gen 2 (Crystal) SRAM ---------------------------------
|
|
// Offsets mirror src/lib/savedex.js `G2.cr`.
|
|
const G2 = { owned: 0x2a27, seen: 0x2a47, unown: 0x2a67, ck: 0x2d0d, ckFrom: 0x2009, ckTo: 0x2b82 };
|
|
|
|
function makeGen2Save({ caught, seen, unown = [] }) {
|
|
const bytes = new Uint8Array(0x8000);
|
|
setBits(bytes, G2.owned, caught);
|
|
setBits(bytes, G2.seen, seen);
|
|
unown.forEach((letter, i) => (bytes[G2.unown + i] = letter)); // 1 = A … 26 = Z
|
|
let sum = 0;
|
|
for (let i = G2.ckFrom; i <= G2.ckTo; i++) sum = (sum + bytes[i]) & 0xffffffff;
|
|
bytes[G2.ck] = sum & 0xff;
|
|
bytes[G2.ck + 1] = (sum >> 8) & 0xff;
|
|
return bytes.buffer;
|
|
}
|
|
|
|
describe('parseSaveDex — Gen 2 Unown Report', () => {
|
|
it('turns the caught-letter list into Form Dex slugs', () => {
|
|
const dex = parseSaveDex(
|
|
makeGen2Save({ caught: [1, 25], seen: [1, 25, 201], unown: [3, 1, 20] }), // C, A, T
|
|
);
|
|
expect(dex.gen).toBe(2);
|
|
expect(dex.game).toBe('Crystal');
|
|
expect(dex.checksumOk).toBe(true);
|
|
expect([...dex.formCaught].sort()).toEqual(['unown', 'unown-c', 'unown-t']);
|
|
});
|
|
|
|
it('no Unown caught → empty list', () => {
|
|
const dex = parseSaveDex(makeGen2Save({ caught: [1], seen: [1], unown: [] }));
|
|
expect(dex.formCaught).toEqual([]);
|
|
});
|
|
|
|
it('ignores a garbage list rather than ticking wrong boxes', () => {
|
|
const dex = parseSaveDex(makeGen2Save({ caught: [1], seen: [1], unown: [3, 99, 5] }));
|
|
expect(dex.formCaught).toEqual([]);
|
|
});
|
|
});
|