Import cartridge .sav files with verified structure, checksums and clock (Closes #44)
Savegames → Import backup now reads an English Crystal .sav (from an emulator, a cartridge dump or the 3DS Virtual Console's sav.dat) and converts it into a native save after a preview. - world/cartridge-save.ts: picks the copy TryLoadSaveFile would load (check values and checksums of the main copy, else the backup), reads the saved WRAM block, the numbered boxes, mail backups, Hall of Fame and Battle Tower by PRET labels (SRAM, SAVE_BLOCK in the profile, checked by check:reference with the SAV constants), engine flags via EngineFlags, scenes via MapScenes, and the map's objects and masks. - Clock: an emulator MBC3 footer plus the time since it was written, FixDays/FixTime with wStart*, mapped to the minute and weekday offsets; without one, the device's time and the saved wCurDay. Day countdowns move with the day difference. - Refused with the reason: unported maps, the Bug-Catching Contest, a Battle Tower challenge, characters outside the naming screen. - Names keep <PK>/<MN> as ᴾᴷ/ᴹᴺ (one character, one tile); trainer names accept the naming screen's alphabet; boxed Eggs validate; native states up to 256 kB load (a full PC exceeded 64 kB). - Tests: the committed debug saves, layout offsets against the symbols, backup fallback, clock footers, day counters, refusals, slot session and the Savegames flow in Chromium and WebKit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87b05b45f4
commit
592ca43a40
20 files changed
+1821
-40
No files matched your search
@@ -70,9 +70,13 @@ const CODES: Record<string, number> = {
|
||||
"♀": 0xf5,
|
||||
"…": ELLIPSIS_TILE,
|
||||
};
|
||||
/** Text as tile numbers, one character per tile. */
|
||||
/** The naming screen's <PK> and <MN>, written as two small capitals in
|
||||
* native names (see ../party/pokemon). */
|
||||
const GLYPHS: Record<string, number> = { "ᴾᴷ": 0xe1, "ᴹᴺ": 0xe2 };
|
||||
/** Text as tile numbers, one character (or <PK>/<MN> pair) per tile. */
|
||||
export function textTiles(text: string): number[] {
|
||||
return [...text].map((character) => {
|
||||
return (text.match(/ᴾᴷ|ᴹᴺ|[\s\S]/gu) ?? []).map((character) => {
|
||||
if (GLYPHS[character] !== undefined) return GLYPHS[character];
|
||||
const code = character.charCodeAt(0);
|
||||
if (character >= "A" && character <= "Z") return 0x80 + code - 65;
|
||||
if (character >= "a" && character <= "z") return 0xa0 + code - 97;
|
||||
|
||||
@@ -1565,6 +1565,155 @@ export const WRAM = {
|
||||
wBreedMon1Nickname: 0xdef6,
|
||||
wBreedMon2Nickname: 0xdf2f,
|
||||
} as const;
|
||||
/** The work RAM of the saved game block (wPlayerData to wPokemonDataEnd,
|
||||
* which sGameData copies byte for byte), plus wOptions and wCrystalData,
|
||||
* keyed by PRET labels: what a cartridge save import reads. */
|
||||
export const SAVE_BLOCK = {
|
||||
wOptions: 0xcfcc,
|
||||
wTextboxFrame: 0xcfce,
|
||||
wGBPrinterBrightness: 0xcfd0,
|
||||
wOptions2: 0xcfd1,
|
||||
wCrystalData: 0xd472,
|
||||
wPlayerGender: 0xd472,
|
||||
wPlayerData: 0xd47b,
|
||||
wPlayerID: 0xd47b,
|
||||
wPlayerName: 0xd47d,
|
||||
wRivalName: 0xd493,
|
||||
wStartDay: 0xd4b6,
|
||||
wDST: 0xd4c2,
|
||||
wGameTimeCap: 0xd4c3,
|
||||
wGameTimeHours: 0xd4c4,
|
||||
wGameTimeMinutes: 0xd4c6,
|
||||
wGameTimeSeconds: 0xd4c7,
|
||||
wGameTimeFrames: 0xd4c8,
|
||||
wCurDay: 0xd4cb,
|
||||
wPlayerStruct: 0xd4d6,
|
||||
wPlayerDirection: 0xd4de,
|
||||
wPlayerMapX: 0xd4e6,
|
||||
wObject1Struct: 0xd4fe,
|
||||
wMapObjects: 0xd71e,
|
||||
wObjectMasks: 0xd81e,
|
||||
wVariableSprites: 0xd82e,
|
||||
wStatusFlags: 0xd84c,
|
||||
wMoney: 0xd84e,
|
||||
wMomsMoney: 0xd851,
|
||||
wCoins: 0xd855,
|
||||
wTMsHMs: 0xd859,
|
||||
wNumItems: 0xd892,
|
||||
wNumKeyItems: 0xd8bc,
|
||||
wNumBalls: 0xd8d7,
|
||||
wNumPCItems: 0xd8f1,
|
||||
wWhichRegisteredItem: 0xd95b,
|
||||
wRegisteredItem: 0xd95c,
|
||||
wPlayerState: 0xd95d,
|
||||
wHallOfFameCount: 0xd95e,
|
||||
wTradeFlags: 0xd960,
|
||||
wEventFlags: 0xda72,
|
||||
wCurBox: 0xdb72,
|
||||
wBoxNames: 0xdb75,
|
||||
wDecoBed: 0xdc0f,
|
||||
wDecoCarpet: 0xdc10,
|
||||
wDecoPlant: 0xdc11,
|
||||
wDecoPoster: 0xdc12,
|
||||
wDecoConsole: 0xdc13,
|
||||
wDecoLeftOrnament: 0xdc14,
|
||||
wDecoRightOrnament: 0xdc15,
|
||||
wDecoBigDoll: 0xdc16,
|
||||
wWhichMomItem: 0xdc17,
|
||||
wMomItemTriggerBalance: 0xdc19,
|
||||
wDailyResetTimer: 0xdc1c,
|
||||
wFruitTreeFlags: 0xdc27,
|
||||
wLuckyNumberDayTimer: 0xdc2d,
|
||||
wSpecialPhoneCallID: 0xdc31,
|
||||
wBlueCardBalance: 0xdc4b,
|
||||
wStepCount: 0xdc73,
|
||||
wPoisonStepCount: 0xdc74,
|
||||
wHappinessStepCount: 0xdc77,
|
||||
wPhoneList: 0xdc7c,
|
||||
wRepelEffect: 0xdca1,
|
||||
wBikeStep: 0xdca2,
|
||||
wDigWarpNumber: 0xdca9,
|
||||
wBackupWarpNumber: 0xdcac,
|
||||
wLastSpawnMapGroup: 0xdcb2,
|
||||
wMapGroup: 0xdcb5,
|
||||
wYCoord: 0xdcb7,
|
||||
wXCoord: 0xdcb8,
|
||||
wPartyCount: 0xdcd7,
|
||||
wPartySpecies: 0xdcd8,
|
||||
wPartyMons: 0xdcdf,
|
||||
wPartyMonOTs: 0xddff,
|
||||
wPartyMonNicknames: 0xde41,
|
||||
wPokedexCaught: 0xde99,
|
||||
wPokedexSeen: 0xdeb9,
|
||||
wUnownDex: 0xded9,
|
||||
wFirstUnownSeen: 0xdef4,
|
||||
wDayCareMan: 0xdef5,
|
||||
wBreedMon1Nickname: 0xdef6,
|
||||
wBreedMon1OT: 0xdf01,
|
||||
wBreedMon1: 0xdf0c,
|
||||
wDayCareLady: 0xdf2c,
|
||||
wStepsToEgg: 0xdf2d,
|
||||
wBreedMotherOrNonDitto: 0xdf2e,
|
||||
wBreedMon2Nickname: 0xdf2f,
|
||||
wBreedMon2OT: 0xdf3a,
|
||||
wBreedMon2: 0xdf45,
|
||||
wEggMonNickname: 0xdf65,
|
||||
wEggMonOT: 0xdf70,
|
||||
wEggMon: 0xdf7b,
|
||||
wRoamMon1: 0xdfcf,
|
||||
wRoamMons_CurMapNumber: 0xdfe4,
|
||||
wBestMagikarpLengthFeet: 0xdfe8,
|
||||
wMagikarpRecordHoldersName: 0xdfea,
|
||||
wPokemonDataEnd: 0xdff5,
|
||||
} as const;
|
||||
/** Cartridge SRAM as offsets into a 32 KiB .sav file (bank * $2000 +
|
||||
* address - $a000), keyed by PRET labels (ram/sram.asm). */
|
||||
export const SRAM = {
|
||||
sPartyMailBackup: 0x071a,
|
||||
sMailboxCountBackup: 0x0a0b,
|
||||
sMailboxesBackup: 0x0a0c,
|
||||
sMysteryGiftItem: 0x0be2,
|
||||
sBackupMysteryGiftItem: 0x0be4,
|
||||
sRTCStatusFlags: 0x0c60,
|
||||
sLuckyNumberDay: 0x0c68,
|
||||
sLuckyIDNumber: 0x0c69,
|
||||
sBackupOptions: 0x1200,
|
||||
sBackupCheckValue1: 0x1208,
|
||||
sBackupGameData: 0x1209,
|
||||
sBackupChecksum: 0x1f0d,
|
||||
sBackupCheckValue2: 0x1f0f,
|
||||
sOptions: 0x2000,
|
||||
sCheckValue1: 0x2008,
|
||||
sGameData: 0x2009,
|
||||
sGameDataEnd: 0x2b83,
|
||||
sChecksum: 0x2d0d,
|
||||
sCheckValue2: 0x2d0f,
|
||||
sLinkBattleStats: 0x3260,
|
||||
sLinkBattleStatsEnd: 0x32c0,
|
||||
sHallOfFame: 0x32c0,
|
||||
sCrystalData: 0x3e3d,
|
||||
sBattleTowerChallengeState: 0x3e45,
|
||||
sNrOfBeatenBattleTowerTrainers: 0x3e46,
|
||||
sBTChoiceOfLevelGroup: 0x3e47,
|
||||
sBTTrainers: 0x3e48,
|
||||
sBattleTowerSaveFileFlags: 0x3e4f,
|
||||
sBattleTowerReward: 0x3e50,
|
||||
sBTMonOfTrainers: 0x3e51,
|
||||
sBox1: 0x4000,
|
||||
sBox2: 0x4450,
|
||||
sBox3: 0x48a0,
|
||||
sBox4: 0x4cf0,
|
||||
sBox5: 0x5140,
|
||||
sBox6: 0x5590,
|
||||
sBox7: 0x59e0,
|
||||
sBox8: 0x6000,
|
||||
sBox9: 0x6450,
|
||||
sBox10: 0x68a0,
|
||||
sBox11: 0x6cf0,
|
||||
sBox12: 0x7140,
|
||||
sBox13: 0x7590,
|
||||
sBox14: 0x79e0,
|
||||
} as const;
|
||||
|
||||
export const MAP_GROUP_COUNT = 26;
|
||||
export const NUM_POKEMON = 251;
|
||||
|
||||
Reference in new issue
Block a user