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>
113 lines
13 KiB
Markdown
113 lines
13 KiB
Markdown
# Architecture
|
|
|
|
The default application is a native TypeScript game. It loads no CPU interpreter, Game Boy hardware model or WebAssembly emulator. The reference emulator has a separate entry point and build.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
ROM[User ROM] --> Gate[Exact revision validation]
|
|
Gate --> Assets[ROM reader and asset decoders]
|
|
Assets --> World[Native world rules]
|
|
Assets --> Scripts[Resumable event, movement and text interpreters]
|
|
Scripts --> World
|
|
Input[Touch and keyboard] --> Runtime[Native runtime]
|
|
Runtime --> World
|
|
World --> View[Canvas world and DOM dialogue]
|
|
Session[Application session] --> Runtime
|
|
Session --> Saves[Native save storage]
|
|
Reference[Separate reference emulator] -. local comparisons only .-> World
|
|
```
|
|
|
|
| Module | Responsibility |
|
|
| ------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
| `src/rom/validate.ts` | Exact cartridge identity, no networking |
|
|
| `src/game/rom` | Banked reads, LZ, maps/tilesets/sprites/palettes and pinned addresses |
|
|
| `src/game/world` | Overworld ticks, movement, collision, warps, connections, scenes, callbacks and save validation |
|
|
| `src/game/scripts` | The game's event, movement and text formats and translated specials, no GB instructions |
|
|
| `src/game/rendering` | Planar pixels, metatiles, connection strips, adaptive camera, sprites, emotes and minimap |
|
|
| `src/game/party` | Party Pokémon generation, stat formulas, bag pockets and their validation |
|
|
| `src/game/audio` | The sound engine on its original WRAM, the home sound routines, and the Web Audio synthesizer |
|
|
| `src/game/runtime.ts` | 30 Hz tick scheduling from real time, input routing and presentation updates |
|
|
| `src/input` | Abstract buttons, multiple sources and browser bindings |
|
|
| `src/app` | Runtime-independent load/play/pause/backup lifecycle |
|
|
| `src/storage` | Versioned envelope, save slots with atomic generations and checksums, the singleton migration |
|
|
| `src/ui`, `src/main.ts` | Accessible game tabs, UI composition and DOM dialogue, no ROM offsets |
|
|
| `src/platform` | Browser locks, the installable app (service worker, install, storage) and future native adapters |
|
|
| `src/reference`, `src/emulator` | Original-game reference, excluded from the native build |
|
|
| `reference/public` | Licensed emulator files for the reference build only |
|
|
|
|
`GameRuntime` is a lifecycle interface, not an emulator contract. Native state is validated JSON, schema version 3. It holds the map, position, facing, name/avatar, player ID and in-game tile count; event and engine flags and sprite variables; scene values, phone contacts, a queued special call, the rival's name, the game clock's minute offset, the weekday offset and DST flag, whether the introduction is still pending; positions of objects that scripts moved; and the party, bag, Pokédex sets, money, Mom's savings, registered item and room decoration slots. Older saves without decorations migrate to the original bed and Town Map poster. Decoration imports validate slot categories, ownership and unique doll placement. Early version-3 backups without a money field migrate to the starting balance. It contains no graphics, music, dialogue, GB memory or ROM bytes. Versions 1 and 2 migrate with new-game defaults. Party Pokémon are validated by recomputing their stats from the ROM. The backup envelope retains the historical `coreId` field; native backups use it for their runtime/schema ID.
|
|
|
|
Saves made during an unfinished walk or jump retain its source tile and completed step count, so loading cannot skip a landing event or map transition. Directional mats wait for exit input; doors, stairs and caves force an animated downward step after arrival.
|
|
|
|
Native and reference sessions use different IndexedDB databases and Web Locks. The native player keeps save slots (`storage/slots.ts`, `app/slot-session.ts`): each slot keeps its newest two records with checksums, written in one transaction, and loading falls back to the previous record when the newest is damaged. The singleton save of database version 1 becomes the first slot inside the version-2 upgrade. Imports are previewed in a scratch world and reject incompatible schemas and invalid positions before changing the world or any slot. A failed import, switch or new game restores the prior state. Emulator snapshots cannot be used as native saves. Cartridge saves are converted, not loaded: `world/cartridge-save.ts` checks the size, the check values and checksums of both copies (taking the backup when the main copy fails, as `TryLoadSaveFile` does), reads the saved WRAM block, the numbered PC boxes, the mail backups, the Hall of Fame and the Battle Tower by PRET addresses (`SAVE_BLOCK`, `SRAM` in the profile), translates engine flags through the ROM's `EngineFlags` and scenes through `MapScenes`, and turns the cartridge clock (an emulator's MBC3 footer plus `wStart*`, as `FixDays`/`FixTime` do) into the port's minute and weekday offsets, moving stored day counters by the same amount. The result goes through the same preview and `restore` checks as a native backup.
|
|
|
|
`GameWorld` advances in overworld ticks of two video frames, as the original loop does. The runtime converts elapsed time to ticks. Tile events follow `CheckTileEvent`: a completed step checks connections, then warps, then coordinate events for the current scene. Idle ticks run the current scene script. Map entry runs the NEWMAP, TILES and OBJECTS callbacks on a copy first, so an unsupported callback refuses the step instead of entering a half-initialized map. Objects spawn from event flags at map load, and later flag changes leave them in place until `appear`/`disappear` or the next load.
|
|
|
|
Scripts are generators that evaluate against a private draft and yield presentation effects: pages, yes/no or weekday choices, movement, emotes and pauses. The world presents each effect and resumes the script with the answer, or after the animation's ticks. Unknown commands, movement commands and specials abort. Aborting discards the draft and restores the positions the scene changed. The draft commits only when the script ends. A save made during a scene contains the committed state, and coordinate events rewind to the tile before the triggering step, so the scene replays without partial rewards or flags. Specials are translated natively and selected by the routine that the ROM's special table points to.
|
|
|
|
Asset caches and the ROM reader remain in memory for lazy extraction. Neither ROMs nor extracted assets enter IndexedDB, the build or a backend. Vite blocks serving the root ROM and private reference checkout during development.
|
|
|
|
The native viewport follows its available CSS dimensions and draws additional world tiles at a bounded pixel resolution. Beyond a map's edge it draws the connected map with the current tileset, as the original connection strips do, and the border block elsewhere. Resizing also redraws paused sessions. The local minimap caches terrain from the loaded map and updates its marker from the same world position used by the player; the Map tab shares that view. Below it, the Map tab draws the region atlas (`world/atlas.ts`): the ROM's Town Map with the player's landmark, the visited flypoints and the areas open in this port. Leaving Game pauses the session and releases held input; returning requires explicit play. The reference player retains its own fixed-screen styling.
|
|
|
|
`game/battle` now holds the native battle core and ROM-backed battle tables. Browser tests cover the overworld-to-battle flow using a test-only encounter fixture; Route 29 uses ROM encounter tables. Trainer battles share that core, with ROM trainer decoders, the trainer's send-out and switch AI, and explicit AI support checks. Map trainers (sight and talk) are started by `game/world` and run the ROM's own trainer scripts. Battles receive the script's draft party, bag and Pokédex, so a catch, a thrown ball or a nickname commits only with the script that started the battle. The world runs the sound engine twice per tick, as VBlank does; the runtime sends each frame's sound register writes to an AudioWorklet that synthesizes them (`game/audio/worklet.ts`), unlocked by the first user gesture and suspended while paused. Sound never changes game state; `waitsfx` and cries hold the script, frozen, while sound effects play, as the original's busy loops do. Introduce future modules with working features: `features/link` and `features/walking`. Party and bag models live in `game/party`; scripts change them only on their draft state. Rules must not import UI/browser modules. Transports and step providers must not write directly into world state.
|
|
|
|
|
|
The overworld extension keeps player travel mode, backup-warp data and Flash
|
|
state in version-3 saves, with defaults for older saves. Forced terrain and
|
|
turn-in-place timing run through the idle movement path. Dynamic warps and
|
|
elevators retain their return destination. Block mutations use a per-map working
|
|
copy and a revision counter, so rollback and the minimap see the same terrain.
|
|
Surf, Cut and Strength use the ROM scripts. Select runs the registered item
|
|
through the same module as the start menu. Start opens
|
|
`scripts/start-menu.ts`, a native run on a script draft like the introduction:
|
|
the party menu and pack change only the draft, which commits when the menu
|
|
closes, and an item or field move that queues a script (the Bicycle, Cut,
|
|
Surf …) starts it afterwards with the context the world checked (the
|
|
Pokémon, the block to cut, the surfing state or a Sweet Scent encounter).
|
|
The start menu, party menu and pocket cursors live in the world's memory,
|
|
not in saves, and are cleared after each battle. `scripts/evolution.ts`
|
|
translates `EvolveAfterBattle`, `LearnLevelMoves` and `LearnMove` over a
|
|
draft. After a won battle the world runs it as an interlude on the battle's
|
|
script draft, before the script receives the battle result; the start menu
|
|
runs it inline for Rare Candy and stones. The `evolve` effect is a timed
|
|
picture sequence that B may stop. The bedroom PC edits
|
|
decorations on the script draft and reloads the room when the menu commits.
|
|
`scripts/intro.ts` runs a new game's introduction as a native generator on a
|
|
script draft; the session starts it only when nothing is saved, and the name,
|
|
gender and clock offset commit when it ends. `GameWorld` reads a game clock:
|
|
the device time plus that offset.
|
|
`scripts/pc.ts` runs the Pokémon Center and house PCs on the same draft, so a
|
|
refused step leaves boxes and items unchanged. `party/storage.ts` holds the
|
|
boxes and the PC's items. A `memscript` effect queues Bill's call (like
|
|
`LoadMemScript`); the world keeps it in memory only and runs it before scene
|
|
scripts.
|
|
The wall/poster Town Map is a ROM-backed canvas presented by a script effect.
|
|
`scripts/pokegear.ts` runs the Pokégear inside the start menu run: each card
|
|
is a `pokegear` effect answered with a button, and a phone call runs the
|
|
callee's ROM script in the event interpreter on the same draft. The world
|
|
keeps the card on screen (`world.pokegear`) during the call's texts.
|
|
`scripts/mom.ts` runs `BankOfMom` as a special; its amount box is a
|
|
`mombank` effect answered with buttons in the same way, and so are the
|
|
pages of `scripts/trainer-card.ts` and the screen of `scripts/options.ts`,
|
|
whose option bytes live in the script state, and of `scripts/fly-map.ts`,
|
|
which Fly opens before the world queues its script. The world refreshes the card's play
|
|
time, colon and badge step every tick; `world/game-time.ts` is the play
|
|
time (`wGameTime`), which the world advances two frames per tick.
|
|
|
|
`newloadmap` stops a script as `StopScript` does: the `warp` effect carries
|
|
the offset after it, and once the world has entered the new map it starts
|
|
the rest of the script there with the same field-move context (Fly's
|
|
landing, Teleport's and Dig's return movements). A `fly` effect hides every
|
|
object while the party icon flies (`world.flyingIcon`, drawn by the
|
|
renderer).
|
|
|
|
`TilesetAnimator` translates the ROM animation command tables, advancing two
|
|
video frames per overworld tick. It owns mutable graphics separately from the
|
|
asset cache. The renderer records background color indices for grass sprite
|
|
priority and applies special tileset palettes. Animation comparisons cover
|
|
Johto, Ice Path, restless Ilex Forest and Tin Tower. The live forest engine
|
|
flag selects the Celebi animation; the later story remains gated. Palette
|
|
comparisons execute the original LoadMapPals routine across 192 cases.
|
|
Movement traces compare event flags and check destinations across map-entry
|
|
boundaries; controlled scenes also compare scripted pose visibility and lift.
|