# ReverseEngineerWasm_PLAN.md — active effort

Goal: understand the Flyff WASM enough to (a) read decrypted state from memory and/or
(b) reconstruct the outgoing-command path (build → encrypt → `ws.send`) so we can craft
move/select/attack packets. Vision sensing is unreliable; this is the path to reliable
control. **This file is the strategy; `reverseEngineeringWASM/wasm_overview.md` is the
growing function-by-function map we fill in as we analyse.**

> Cheaper alternative still on the table: pure **memory-reading (M7)** replaces vision
> without any protocol RE. Full RE here is what lets us *send* crafted packets.

## Verified facts (don't re-derive)
- WASM: `re/main-wasm32.wasm`, 8,562,306 bytes. **162 imported funcs** (indices 0–161),
  **11,581 defined funcs** (indices 162–11742), 9 func exports + memory + table.
  Stripped (no name section) → functions are known only by index.
- Build: **emscripten + embind/emval**. Outgoing `ws.send` is invoked from WASM through
  the embind invoker, NOT by writing plaintext into JS.
- **Captured send stack** (from `wstest.py`): `Hooked.ws.send` ← embind
  `methodCaller<(emscripten::val)=>void>` ← `__emval_invoke` ← **wasm[2778]** ←
  **[187]** ← **[291]** ← **[4307]**.
- **func 2778** = the send bridge: calls imports **6 `__emval_invoke`**,
  **7 `__emval_create_invoker`**, **5 `__emval_run_destructors`**. 118 lines. Called by
  **187** and **6963**. → It hands an already-built buffer (as an emval) to JS `ws.send`.
- **func 187** = generic embind invoke helper — **571 callers** → it is the common
  "invoke a JS val" path, not the cipher. The cipher/builder is upstream of it.
- **func 291** (987 lines, 29 callers) and **func 4307** (23,929 lines, a top-level
  dispatcher, 0 callers) are on the path toward the message builder.
- **Frame shapes** (from captured traffic):
  - Outgoing "move" = 64 bytes. Header `1f 37 00 00 00` (constant; likely opcode), then
    bytes 5–8 = per-message **counter/nonce/sequence** (change every msg), rest =
    **encrypted body**. Replaying an old frame did nothing (sequence rejected).
  - Incoming = `b8` + 4-byte LE length (=framelen−9) + 4 varying bytes + constant
    `fe af a0` + encrypted body.
- WASM stats: code section ~8.1 MB; **16,399 `call_indirect`** (table dispatch, used by
  embind — static graph can't resolve these), **113,249 direct `call`**.

## Tooling now in place (`reverseEngineeringWASM/`)
- `full.wat` — complete disassembly (98 MB) via wabt (`disasm.js`).
- `functions/func_NNNNN.wat` — one file per defined function (zero-padded index).
- `callgraph.json` — per func `{type, lines, calls[], indirect, calledBy[], export}`.
  **Built mechanically** — edges are complete for direct calls (indirect not resolved).
- `import_index_to_name.json` — import index (0–161) → runtime name (e.g.
  `__emval_invoke`, `_glDrawElements`, `__embind_register_function`). Use to read
  `call N` where N<162.
- `imports.json` / `exports.json` / `import_names.json` — raw maps.
- Query helper: `python -c` over `callgraph.json` (see git history / examples below).

Regenerate after a game patch: `node --max-old-space-size=8192 disasm.js` then
`node --max-old-space-size=8192 split_and_graph.js` (indices/offsets shift per patch).

## Method — goal-directed, not exhaustive
11,581 funcs is too many to sweep. We **walk the call graph outward from the send
anchor** toward the message builder + cipher, recording each function as we go.

Division of labour:
- **Mechanical (scripts):** call/called-by edges, sizes, import resolution, finding
  candidate crypto funcs (tight loops over a buffer with xor/rotl/shl/add + a constant
  table in the data section), finding the elem/table section to resolve key indirect
  calls. Never ask an LLM for something a script computes exactly.
- **Semantic (agents):** *what a function does*. Each agent reads ONE `func_NNNNN.wat`
  plus its graph row + import names and returns a structured description (below). The
  orchestrator writes confirmed results into `wasm_overview.md` and picks the next batch
  from the newly revealed neighbours.

### Agent report schema (one per function)
```
- idx, type, size(lines), export
- purpose: one line, best hypothesis
- behavior: 2–4 sentences (control flow, what it reads/writes)
- memory: notable linear-memory offsets / global indices / constants touched
- crypto_signals: any xor/rotate/add loops, S-box/table lookups, sequence handling
- calls_meaning: for the important callees, what they appear to do (resolve imports!)
- send_path_relevance: how this relates to build→encrypt→ws.send (or "utility/unrelated")
- next: which callers/callees look worth analysing next, and why
- confidence: high/med/low
```

### Priority frontier (update as we learn)
1. The embind **send bridge** + its drivers: 2778, 2779, 2780, 187, 6963.
2. **Up the stack** toward the builder: 291, then 4307 (huge → analyse in slices / by
   its callees, not whole).
3. The **message builder**: the function that writes the `1f 37 00 00 00` header +
   sequence + body (search data section for the constant; find writers of that buffer).
4. The **cipher**: tight transform loop just before the buffer reaches the bridge.
5. **Incoming**: mirror decrypt function (or skip — prefer memory-reading for state).

## Phases
0. **Refresh** (per patch): re-download wasm/js, re-run disasm + graph, re-capture frames.
1. ✅ **Tooling**: disassemble, split, build call graph + import map.  ← DONE
2. **Map the send path** (current): agent-analyse the frontier, fill `wasm_overview.md`,
   follow callers upward to the builder.
3. **Find the plaintext buffer**: locate where the 64-byte frame is assembled
   pre-encrypt (DevTools dynamic confirm: break at the 2778 embind site, read the
   pointer arg, dump `HEAPU8` around it before/after the suspected encrypt call).
4. **Identify the cipher**: read the transform loop; recover key (data-section constant)
   + the per-message sequence rule.
5. **Replicate or reuse**: re-implement in JS/Python, OR call the WASM encrypt fn from JS
   with our plaintext / write into the buffer right before the existing send fires.
6. **Validate**: craft "move", confirm the character moves; then map select/attack opcodes.

## Memory-read sensor (M7) — findings + next RE step
Goal: read live HP/position/mobs from WASM memory (via `HEAPU8`, a page global) to replace vision.
Tools: `memread.py` (scan/diff/ratio-diff/track/chase/peek/gwatch/hp), `hpoverlay.py` (live overlay).
- **Player controller pointer @ mem 1230588** (stable): `controller = *(1230588)`.
- **Entity stats layout:** HP-ratio `f32`@entity+308 × maxHP `i32`@entity+316 = current HP.
  **Current HP is a 0–1 ratio, NOT the displayed integer** → value-scans for the HP number all fail.
- **Blocker:** the live player entity **reallocates** (address changes), so value-scanning can't
  track it; `controller+7572/+7792` are STATIC copies (frozen). Vision HP (`hp_fraction`) works.
- **Next (chosen):** RE the **live-entity pointer chain** — find how the game dereferences the live
  entity, anchored on code reading `f32 @ obj+308` / `i32 @ obj+316` (200 funcs touch both; narrow
  to one that divides +308 for the HP bar, or writes +308 on damage, or loads the entity from a
  global/registry). Then read HP/pos/mobs via that chain (immune to realloc) — and the same entity
  struct holds position + links to nearby mobs, so this unlocks all three at once.

## Caveats
- **Indirect calls** (embind) aren't in the static graph — confirm those edges
  dynamically in DevTools (break + step) or via the elem/table section.
- **Maintenance:** every patch shifts indices — anchor on the stable embind `ws.send`
  glue site, re-derive 2778, re-run the frontier.
- **Ban risk:** crafted/replayed packets are the most detectable — throwaway accounts only.
- **Effort:** large. Reading state from memory is the cheaper 80%.
