The browser host
What the WebAssembly bundle exports to this site: the play runtime behind the play page, the standalone minigames page, the viewer pages and the ROM patcher. Nothing is uploaded - the disc image a visitor supplies is read in their browser.
At a glance
- Crates
legaia-web-play(the play runtime, overlegaia-web-minigames, the minigames runtime) andlegaia-web-patcher(the ROM patcher) underlegaia-web-viewer(the inspectors and thecdylibthe site loads): one wasm bundle, four products- Session
legaia_engine_session::BootSessionover the page’s audio output - the type the native window runs- Frame
tick_frame()calls the session’s tick in the native frame order and returns the scene a door just walked into- Draw side
- Resolved against the same scene resources the host built at scene entry; nothing is decoded twice
- Parity
- Disc-gated tests pair this host against the native window; a source-derived gate fails when a screen reaches one host only
Module map
web-play holds the play runtime, web-minigames the minigames runtime, web-patcher the ROM patcher, and web-viewer the inspectors; web-viewer depends on web-play and web-patcher, and web-play on web-minigames, never the reverse. wasm-bindgen collects the exports of every crate the bundle links, so which crate an export lives in is invisible to the site’s JavaScript.Simulation and draw are split on purpose
runtimeis the simulation: load a disc, enter a field scene, set the pad word, set the camera azimuth so the D-pad remaps camera-relative, tick a frame, read state. It runs the ported field VM, free movement against the walkability grid, the NPC motion VMs and the dialogue runner.playis what the page draws: the assembled map, the lead’s mesh posed from the world’s live pose frame, and the scene’s placed NPCs at their live positions.
The page hands each animation frame’s sim ticks to play_frame_ticks (runtime/frame_ticks.rs), which gives each tick to the owner BootSession::claim_tick names - the field, the pause menu, a shop or name entry - in the order the native window’s sim_tick uses. Menus, shops and name entry therefore step on the sim tick, not the display frame, and the whole frame body is compared state by state against the native window in engine-shell/tests/host_frame_parity.rs.
The map and NPC layers make the native window’s exact resolver calls. NPC clip playback runs in simulation-tick time - one clip player per placed slot, advanced once per tick - so cadence does not depend on the display’s refresh rate and scripted actors perform their cued beats.
What each play module carries
| Module | Covers |
|---|---|
play_dialog | the retail dialog reading box |
play_cutscene / play_fmv | the opening prologue chain and the movie beats |
play_camera | the follow camera and the debug orbit |
play_menu | the retail pause menu |
play_battle, play_battle_render, play_battle_fx | live battles: rules tick, the 3D scene, effects |
play_shop | field merchants, the inn prompt, banners |
play_sfx / play_bgm / play_xa | sound effects, scene music, voice clips |
play_minigames, play_fishing | the in-world minigames reached through the scene’s own door warps |
boot_title, play_name_entry | the boot title screen and name entry |
session_save, cards | session saves and the two memory-card ports |
play_dev_menu | the retail developer menu, behind a session-only checkbox that is never persisted |
play_hotkeys | the native window’s option / cheat / camera keys, through the shared engine-session::hotkeys table |
Keeping two hosts in step
The play page and the native window are two framebuffers over one engine, and the failure that costs most is quiet: a screen is wired natively and the web host drifts behind with nothing in any diff to say so.
Every screen’s geometry is a draw-list builder in engine-ui, and a host has that screen exactly when it calls the builder. So the feature surface can be derived from source, and a gate fails when a builder reaches the native window but not this crate. One-host cases live in a waiver file, each with a reason, validated in both directions so it cannot decay into a stale list.
A screen can be fully wired and connected to nothing. The gate answers “does this host call the builder”; it cannot answer “does this host feed it the same model”. The options screen was drawn on both hosts with the gate green while one of them rebuilt its session from defaults on every open and dropped the result on close.
| Area | State against the native window |
|---|---|
| Seru-trade shop screens | closed: one shared composition |
| Options screen | closed: edits persist across reloads in localStorage, the browser twin of the native window’s legaia-options.toml |
| Inn prompt | closed |
| Load / Save rows | closed at the model: this host browses two memory-card ports, the native window writes save files, both from one summary |
| In-world minigames | closed: both hosts enter them from the scene’s door warps |
| Field music, effects, voice | closed |
The other surfaces
- Viewer pages - assembled full-scene maps, the field-NPC catalog, the Tactical Arts and equipment viewers, summon and battle-stage previews - read the same format crates the extractor does.
- NPC conversations on the NPCs page are each actor’s interaction script walked down every arm by the field kernel: pages of text, Yes / No menus and where each option leads, the story-flag and inventory tests that pick a branch, the side effects, and what the next talk runs. Text is decoded from the visitor’s disc in the browser.
- Scene export builds a textured
.glbof a scene in the browser. - The ROM patcher runs the randomizer and mod pipeline client-side over the visitor’s disc and hands back a patch file. See ROM patcher.
- The translation workbench is the in-browser language-pack editor.
See also
Every module family with its exports, the parity tests, the texture-replacement registry and the long tail of helper modules are in docs/subsystems/browser-host.md.