- the grass fade is a plane across the view, not a ring around the player: `0x6b2b80` texgens the camera-space position through `T(0,0,−52.5)·Ry(π/2)·S(1/17.5)`, so the fade coordinate is view-space depth, `u = (z_eye − 52.5)/17.5` — we measured `distance(eye, fragment)`, a sphere, which meets the ground as a circle centred on the player that slides as they walk and bites a piece out of every tuft it crosses, and that was the hard border; the ramp is `0x6b2320`'s quantised 64×8 table, `alpha = 4·(63 − col)`, read bilinearly as `(254 − 256·u)/255` and capped at `252/255` — plateau to 52.64 yd, zero at 69.86 yd, near grass 98.8 % opaque and never 100 — where we had a clean 1 → 0 line; the product `tex0.a × ramp` is what the 128/255 `detailDoodadAlpha` cutout tests (both stages MODULATE, `[0xc7b2fc]`, confirmed verbatim in all four apitrace captures), so the tuft eroding out at 61.11 yd is the reference's own behaviour and the "fix" that would have removed it never landed; the pass owns its render state — blend mode 2, ALPHAREF 128, cull off, depth-write on, the per-slot draw `0x6b2d60` binding nothing but the texture — where we passed the batch's authored blend and cull through, and the `clutter_state` probe counted the cost: of 322 detail batches, 2 author Opaque (no cutout, their margin drawn as a solid card) and 5 single-sided; the build gate measures the chunk's box against the frustum corner ray derived from the live projection (≈1.31× at 16:9, ≈1.45× at 21:9), not its centre plus a 24 yd margin that popped whole chunks in on relief (2004) - a `<Model>` widget's animation state is Lua-observable and lives in the engine: a private scene clock its own `OnUpdate` advances by `trunc(elapsed·1000)` ms on visible frames only (`0x76d7f0`), an arm with an anchor the sampler re-reads every frame (`0x7121a0`), `OnUpdateModel` at the top of every paint (`0x76d1a0`) and `OnAnimFinished` from the completion callback (`0x76cdc0`); the file's sequences and bounds are facts the host hands over, `SetModel` seeds Stand now or when they land (`0x70ebd0`) with a queued arm replaying after the seed, and the XML `scale=` on a model pane is the model's scale (`CSimpleModel::LoadXML` `0x76cac0`), which the loader had never read (2007); and the pane renders its M2 — every visible pane with a file gets a cell of one render-target atlas at its device size through one orthographic camera, the root at `1280·modelScale·layoutScale` FrameXML units per model unit, the tile's `AnimationPlayer` paused and seeked to the engine's play head, the light black, particles in their own unit (`768·√(a²+1)` per model unit, neither scale), composited as one premultiplied quad at the callback rank; two corrections off the bytes: `OnUpdateModel` fires only with a model installed (`[widget+0x318] ≠ 0`) and `OnAnimFinished` fires once per arm for a looping sequence too (`0x719370`); the loader now applies a `<Model>`'s `file=` at all, which every XML-declared pane had lacked; retired: the minimap ping's byte-measured sprite, since `MiniMapPing` and `WorldMapPing` draw their own file on the pane's clock (2013) - the stance feed fires `UPDATE_SHAPESHIFT_FORMS` only for a list change: the reference fires it at `0x4b28ff`/`0x4b2e43` from learn, unlearn and rank-replace alone, while ours fired on any change of the pushed view, and every fire runs the stock `ShapeshiftBar_Update`, which shows `ShapeshiftBarMiddle` unconditionally past two forms and `Show()`s an already-shown frame — no `OnShow`, no manage pass — so a stance switch or the category-47 cooldown arming and expiring left the middle strip drawn alone over a raised bottom-left bar; castability now rides `SPELL_UPDATE_USABLE` (`0x4b31c0`), active/texture/cooldown are pushed silently and repainted on `PLAYER_AURAS_CHANGED` and `SPELL_UPDATE_COOLDOWN`, the feed ordered before both handler sets so they read the fresh list (2009); and the loot-roll frame waits for its item template (B371): `GroupLootFrame_OnShow` paints once and nothing repaints it, and we fired `START_LOOT_ROLL` on the frame the packet arrived, against a snapshot whose name, texture and quality were still `None` — the reference's `0x61b310` ends in the item-template cache lookup `0x55ba30`, a hit firing the event there and a miss firing it from the arrival callback `0x61b460`, so the feed holds a fresh roll's event until the template answers, with two releases that are ours: a negative answer (a frame that never opens cannot even be passed on) and a 2 s deadline paid out of the roll's minute; `arg2` stays the wire's countdown, as `0x61b430` pushes it (2010) - the FPS journal ships to players: it had registered inside the dev-only `DevToolsPlugin` and been armed by an environment variable, so nothing we shipped could have written a row on the one GPU-bound frame in the corpus, a Steam Deck's at 94 % busy; `/console fpsJournal 1` — a CVar of our own, persisted like every CVar — opens `benilla-config/Diagnostics/fps-journal.csv` and appends one row a second, `0` closes it mid-run, and `WOW_FPS_JOURNAL=<path>` stays as the harness lever; eight GPU columns appended, `gpu_ms` then `gpu_opaque, gpu_static, gpu_transp, gpu_glow, gpu_post, gpu_ui, gpu_other`, read off bevy's `RenderDiagnosticsPlugin`, which times passes where the device offers `TIMESTAMP_QUERY_INSIDE_PASSES` (Vulkan and DX12) and leaves the cells empty on an Apple GPU, empty being the honest value; `static_gx`, the four `ffx_glow_*` passes and `ui_gamma_decode` open diagnostic spans so a city's own draw is named in the split; a fresh file opens with the adapter, backend, driver and `gpu_spans yes|no` line; and a probe's `/console` line, which skips the stock `ChatEdit_ParseText` and had landed in a parser arm that knew only `reloadUI`, now forwards to `ConsoleExec` and writes a CVar the way the typed line does (2008) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Important
Issues and pull requests are closed here. benilla is a solo project developed in a private tree; this repo is its export, published as squashed snapshots, so a PR here has nothing to land on. The best way to contribute is to join the Discord and report the bugs you find. Questions and ideas are welcome in the same place.
benilla speaks the original 1.12.1 protocol, so it connects to any server the real client could, and reads its game data at runtime from your own 1.12.1 install. Every file format and the network protocol are implemented from scratch, with no original client code, no third-party WoW crates, and no bundled game assets.
What works
- Formats: readers for the full asset stack (MPQ patch chain, BLP, DBC, ADT/WDT/WDL, M2, WMO), wired into Bevy as an asset source.
- World: streamed terrain out to the horizon, portal-culled WMOs with interior lighting, doodads and ground clutter, swimmable liquids, sky and weather, and the client's own day/night lighting, fog and gamma passes.
- Models: GPU-skinned M2s with the full animation controller, a near feature-complete particle system, ribbons, and animated gameobjects from doors to lifts.
- Characters: customization end to end, the armor texture composite, weapons with sheathing and enchant glows, shapeshift forms, stealth and mounts.
- Movement: a WoW-feel controller, networked movement in both directions, the server-granted modes from slow fall to roots, a follow camera with collision, boats, zeppelins and taxi flights.
- Networking: SRP6 auth through world-session crypto, the object mirror into the ECS, and live wire coverage from movement and chat through spells, party, quests, mail, trade, vendors, bank, loot, the auction house and PvP honor.
- UI: a from-scratch FrameXML + Lua engine driving the built-in interface, from the login and
character screens through the full HUD, the classic windows (guild, macros and key bindings
included), chat, nameplates, floating combat text and tooltips; third-party addons load from
a
benilla-config/AddOns/folder beside the executable (partial: AtlasLoot and Bagnon run, see below). - Combat: melee on the faithful swing law, ranged and Auto Shot, casting with GCD and cooldowns, combo points, crowd control that really holds you, and the spell visual pipeline.
- Audio: music, ambience and SFX under the client's own selection and crossfade rules, with interior and underwater transitions and zone reverb.
Where it's going
benilla is done when a 1.12.1 player can do everything here that they could in the original client, it looks and feels the same, and it runs from a download on Windows, Linux and macOS. No dates; the order is what is likely, not a promise.
- Battlegrounds and meeting stones, then the long tail of small features that separates a working client from a finished one.
- Addons, options and performance, ongoing.
- The no-brainer fixes from VanillaFixes, SuperWoW and the like.
- Playable downloads for Windows, Linux and macOS. Linux first.
Not planned: other expansions or client versions, Warden (anticheat).
Running it
You need a 1.12.1 (build 5875) client install for game data, a vanilla server to connect to, and stable Rust. Any 1.12.1 core works; vmangos is what development runs against, and cMaNGOS and the rest speak the same protocol.
WOW_DATA=/path/to/WoW/Data cargo run --release -p benilla
The server defaults to localhost:3724, the stock realmd auth port. Point WOW_HOST
at any IP or hostname, appending the auth port if yours is remapped
(WOW_HOST=play.example.com:5000). Credentials go in at the login screen, or set WOW_USER /
WOW_PASS to skip it.
Early inspiration and file format guidance came from the wowemulation-dev community, and warcraft-rs in particular.
benilla is an independent fan project, not affiliated with or endorsed by Blizzard Entertainment.
It ships no Blizzard content — no art, models, sounds, maps, MPQ contents or FrameXML; you
provide your own legally obtained 1.12.1 client. The interface code under
crates/benilla-app/assets/ui/ is ours, written to the client's own layout and API names so that
the windows look right and 1.12.1 addons find the names they expect.
World of Warcraft is a trademark of Blizzard Entertainment, Inc. Our own code is licensed under MIT or Apache 2.0, at your option.