The Documentation Was Wrong. So Was the Code.
A bugfix cycle run by reading the manual against the code, four WebGPU faults found with bots, bots that had not fought since July, and demo-recorded training that had never extracted a frame.
The Method
The manual was read against the code one engine part at a time: what the docs claim is confirmed, corrected or deleted, and whatever the reading turns up in the code is fixed as it is found. Tracked in docs/research/v0.9.24-QA.md.
375 files, 3456 tests, typecheck, build, gate:biome, 1000-tick fuzzWhat Reading The Docs Against The Code Found
freefly_collision info— the exact string the physics guide told you to type —
silently disabled collision and reported it as the state.
findByPattern"turned a glob into a regex by swapping*and?and leaving
everything else alone", so maps/*.bsp matched maps/wdm2Xbsp.
--execsplit on semicolons inside quotes, making the manual's own
sim replay "W+CROUCH 60; JUMP 1; W 30" impossible to run from a batch.
electron-builder.config.cjs—npm run electron:build"could never have
produced anything": empty module.exports in an ESM package, plus a nonexistent manifest path.
.env.examplelisted 3 variables. The code reads 18.- Weapons respawn in thirty seconds, not fifteen.
- The gametype count is five, not four.
bind/unbindhad been reporting "binding was planned" for the engine's whole
life.
And The Code It Found
The first-person weapon fill light was a world light.
From the moment you joined it brightened the map by about 38%.
Spectators never saw it, because it was on your gun. It now lights only the gun.
Bots in Deathmatch had not fought anyone since July. The server sends "spectator" for every player in a free-for-all; its own rules read that as "no team", and the bots read it as a team they all shared, counting every other player as an ally.
Demo-recorded bot training had never extracted a single frame from a real match. Demos now carry the command the server applied for every player, every frame, taken where commands are accepted — so nothing a validator threw out is recorded as something a person did. Training reads those instead of inferring them, because an air-strafe points wish direction and velocity different ways on purpose.
The old label scheme "said 0.5 whatever the person had done" on a neutral frame — which, as the changelog puts it, means:
most of these outputs are off most of the time, so a network answering 'no' to
every button scores well and has learned nothing.
A trained bot's decision no longer calls into TensorFlow at all; the forward pass runs from the weights directly, sixty times faster. Bots are excluded from training unless --include-bots. Three outputs that could not be labelled from a recording were dropped. The network sees the last three frames instead of one. Early stopping on held-out score, and --lr was being parsed and stored while both models compiled with a fixed step size.
WebGPU, Audited Properly
Four gameplay faults found by a first pass with bots, SSR and a reconnect: a frame lost at every map load (a shader-warmup light destroyed while a draw still used it), SSR not starting on WebGPU (a missing engine extension, then a Babylon WGSL shader bug), sky lights rebuilt on every map load, and reconnect stacking a second spectator overlay over disposed trails and damage numbers.
A reproduced cause of oversized WebGPU world shaders:
completing a glTF model import raised light caps on every scene material, including
existing BSP surfaces
Renderer QA gains a two-container pixel test with local Copy/Download JSON controls and a third case, rendererqacase=environment, which passes with identical pixels on WebGL2 and WebGPU on an RTX 4070 SUPER.
The UI Run
First screen rewritten — the rail lists renderer, packages, engine and lobby, the logo sits in a turning ring, "Click to start". The death screen names the killer instead of "Fragged by player_12". Every map item now comes to rest on the floor on load unless the map marks it hanging, which on Le Toxiquet meant red armor was hanging 80 units in the air. Esc is the lobby's gold rail and no longer says PAUSED. Tab works in menus. Four-page Settings with three video presets — Smooth, Balanced, Pretty. Leaderboard with 30-day, all-time and top-100 boards. Demos with Featured, Latest, Yours, and Highlights at 1, 2 and 5 minutes. r_bloom abc no longer turns bloom off. Android package renamed com.iwmengine.ganjaw → `net.washoi.game`.
A running room now takes players while it has room: a watcher in a 7/8 FFA pressed Join and nothing happened. A running room with free seats reads join, a full one reads watch, and J asks for a seat on the connection you are watching from without reloading the map.
The Stale Citations
docs/known_bugs.md held one entry, marked critical and reproducible every time, filed in March 2025 — before the networking audit that reworked the very mechanism it blames.
Every file.ts:123 citation in the research bug list:
was deleted rather than updated: all were wrong, and they rot again on the next
edit.
That is the whole reason this release exists, stated more plainly than a feature list ever would.
Known Unverified Items
Several renderer entries still call for real-device WebGPU gameplay and same-view visual confirmation, including intermittent white frames and reported texture and lift artifacts. The Android splash change remains unverified on a device.