feat(wasm): dm2-wasm browser bindings for the parser crates #314

Merged
jqueuniet merged 2 commits from feat/wasm-bindings into main 2026-09-03 13:45:22 +02:00
Owner

Closes #301 — a new dm2-wasm crate that exposes the DM2 parser crates to the browser through wasm-bindgen.

The premise held (and then some)

#301 guessed a WASM target should be "mostly free" because the parsers are pure-Rust and &[u8]-driven. Verified: all eight parser crates compile to wasm32-unknown-unknown unchanged, and no library code path touches std::fs / time / threads / rng (every such call is confined to #[cfg(test)] or src/bin/). So this PR is a thin marshalling layer, not a port.

Structure — three layers

  • core — pure fn(&[u8]) -> Result<_, WasmError> functions. No #[wasm_bindgen], no JS types, so they are exercised directly by native cargo test over real fixtures.
  • error — one WasmError unifying every wrapped crate's error via #[from]; its Display is the JS-facing message. Kept wasm-free so native builds don't pull in JsError.
  • wasm#[wasm_bindgen] passthroughs (camelCase js_names) that map WasmError -> JsError, plus a console_error_panic_hook start shim so Rust panics surface readably in the browser console.

API surface (this slice: round-trip + all reads)

Group Exports
Saves saveToJson / saveFromJson (byte-exact self-contained round-trip), saveInfoJson (lossy gameplay view)
Dungeon dungeonToJson / dungeonFromJson (byte-exact self-contained round-trip), dungeonSkprojectJson (lossy interop)
Graphics graphicsHeaderJson, graphicsDecodePng (read-only)
Music musicDetect, musicHmpToSmf, musicRenderModWav, musicSndToWav (read-only)
FTL ftlInfoJson, ftlRoundtripOk (read-only)

The save/dungeon round-trip reuses the self-contained lossless JSON from #308/#309 — no external files, which is exactly the web-friendly form #307 identified. Byte order for dungeon/saves is auto-detected inside the crate by mirroring the CLI's try-both-endians pattern against public predicates.

Proof

  • 8 native tests green: save + dungeon round-trip byte-exact through the JSON text; graphics decode yields PNG magic; HMP→SMF starts MThd; MOD→WAV header valid; 19 FTL files round-tripped byte-exact (sega-cd-us + amiga-eu).
  • cargo build -p dm2-wasm --target wasm32-unknown-unknown succeeds; native cargo build/test still pass (wasm-bindgen compiles inertly off-target); fmt clean; no new clippy warning naming a crate file.
  • wasm-pack build crates/dm2-wasm --target web is the documented package build (crate README carries the JS API table + a round-trip usage example).

Scope / follow-ups

  • Graphics serialization policy (#307) is deferred — graphics is read-only decode here (header summary + image→PNG). A lossless graphics JSON would be its own (currently unfiled) effort.
  • The full field-driven save reconstruct (SaveDump::full, which needs dungeon.dat + graphics.dat as inputs) is not exposed yet — the self-contained round-trip covers the browser edit flow without extra uploads. Easy to add later if a consumer wants field-level record-stream edits.
  • Unblocks the browser viewer/editor side of #303 (save editor) and #305 (dungeon editor).

Note for reviewers: the .idea/dm2-tools.iml source-folder entry the IDE auto-generates for the new crate was intentionally left out of the commit.

https://claude.ai/code/session_01D3ec7SFejDHAwmCwbapMVV

Closes #301 — a new `dm2-wasm` crate that exposes the DM2 parser crates to the browser through wasm-bindgen. ## The premise held (and then some) #301 guessed a WASM target should be "mostly free" because the parsers are pure-Rust and `&[u8]`-driven. Verified: **all eight parser crates compile to `wasm32-unknown-unknown` unchanged**, and no library code path touches `std::fs` / time / threads / rng (every such call is confined to `#[cfg(test)]` or `src/bin/`). So this PR is a thin marshalling layer, not a port. ## Structure — three layers - **`core`** — pure `fn(&[u8]) -> Result<_, WasmError>` functions. No `#[wasm_bindgen]`, no JS types, so they are exercised directly by native `cargo test` over real fixtures. - **`error`** — one `WasmError` unifying every wrapped crate's error via `#[from]`; its `Display` is the JS-facing message. Kept wasm-free so native builds don't pull in `JsError`. - **`wasm`** — `#[wasm_bindgen]` passthroughs (camelCase `js_name`s) that map `WasmError -> JsError`, plus a `console_error_panic_hook` start shim so Rust panics surface readably in the browser console. ## API surface (this slice: round-trip + all reads) | Group | Exports | | --- | --- | | Saves | `saveToJson` / `saveFromJson` (byte-exact self-contained round-trip), `saveInfoJson` (lossy gameplay view) | | Dungeon | `dungeonToJson` / `dungeonFromJson` (byte-exact self-contained round-trip), `dungeonSkprojectJson` (lossy interop) | | Graphics | `graphicsHeaderJson`, `graphicsDecodePng` (read-only) | | Music | `musicDetect`, `musicHmpToSmf`, `musicRenderModWav`, `musicSndToWav` (read-only) | | FTL | `ftlInfoJson`, `ftlRoundtripOk` (read-only) | The save/dungeon round-trip reuses the self-contained lossless JSON from #308/#309 — no external files, which is exactly the web-friendly form #307 identified. Byte order for dungeon/saves is auto-detected inside the crate by mirroring the CLI's try-both-endians pattern against public predicates. ## Proof - **8 native tests green**: save + dungeon round-trip byte-exact through the JSON *text*; graphics decode yields PNG magic; HMP→SMF starts `MThd`; MOD→WAV header valid; **19 FTL files round-tripped byte-exact** (sega-cd-us + amiga-eu). - `cargo build -p dm2-wasm --target wasm32-unknown-unknown` succeeds; native `cargo build`/`test` still pass (wasm-bindgen compiles inertly off-target); fmt clean; no new clippy warning naming a crate file. - `wasm-pack build crates/dm2-wasm --target web` is the documented package build (crate README carries the JS API table + a round-trip usage example). ## Scope / follow-ups - **Graphics *serialization* policy (#307) is deferred** — graphics is read-only decode here (header summary + image→PNG). A lossless graphics JSON would be its own (currently unfiled) effort. - The full field-driven save reconstruct (`SaveDump::full`, which needs `dungeon.dat` + `graphics.dat` as inputs) is not exposed yet — the self-contained round-trip covers the browser edit flow without extra uploads. Easy to add later if a consumer wants field-level record-stream edits. - Unblocks the browser viewer/editor side of #303 (save editor) and #305 (dungeon editor). Note for reviewers: the `.idea/dm2-tools.iml` source-folder entry the IDE auto-generates for the new crate was intentionally left out of the commit. https://claude.ai/code/session_01D3ec7SFejDHAwmCwbapMVV
feat(wasm): dm2-wasm browser bindings for the parser crates
All checks were successful
CI / msrv (1.90) (pull_request) Successful in 41s
CI / audit (pull_request) Successful in 22s
CI / stable (pull_request) Successful in 1m8s
00f24d46d1
Adds a new `dm2-wasm` crate exposing the DM2 parsers to the browser via
wasm-bindgen (#301). All parser crates already compile to
`wasm32-unknown-unknown` unchanged (pure `&[u8]`-driven, no fs/time/threads/
rng in any library path), so this is a thin marshalling layer, not a port.

Structure — three layers:
- `core`: pure `fn(&[u8]) -> Result<_, WasmError>` functions, fully covered
  by native `cargo test` over real fixtures.
- `error`: one `WasmError` unifying every wrapped crate's error; its Display
  is the JS-facing message. Kept wasm-free so native builds don't pull JsError.
- `wasm`: `#[wasm_bindgen]` passthroughs (camelCase js_names) mapping
  `WasmError -> JsError`, plus a `console_error_panic_hook` start shim.

API surface (this slice: round-trip + all reads):
- saves/dungeon: byte-exact self-contained JSON round-trip (to/from) plus the
  lossy gameplay-only and skproject read views.
- graphics/music/ftl: read-only decoders (header summary + image->PNG;
  detect + HMP->SMF + MOD->WAV + snd->WAV; FTL container info + round-trip).

Byte order for dungeon/saves is auto-detected inside the crate by mirroring
the CLI try-both-endians pattern against public predicates.

Proof: 8 native tests green (save + dungeon round-trips byte-exact through
the JSON text; graphics PNG magic; HMP->SMF; MOD->WAV header; 19 FTL files
round-tripped byte-exact). `cargo build -p dm2-wasm --target
wasm32-unknown-unknown` succeeds; fmt clean; no new clippy warning on crate
files. `wasm-pack build --target web` produces the browser package (see
crate README); graphics *serialization* policy (#307) is deferred — graphics
is read-only here.

Claude-Session: https://claude.ai/code/session_01D3ec7SFejDHAwmCwbapMVV
chore(ide): register dm2-wasm source folders in .idea module
All checks were successful
CI / audit (push) Successful in 21s
CI / stable (push) Successful in 1m6s
CI / msrv (1.90) (pull_request) Successful in 40s
CI / audit (pull_request) Successful in 21s
CI / stable (pull_request) Successful in 1m6s
CI / msrv (1.90) (push) Successful in 42s
8356d9ea16
jqueuniet deleted branch feat/wasm-bindings 2026-09-03 13:45:22 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jqueuniet/dm2-tools!314
No description provided.