Files
benilla-octo/crates/benilla-app/src/lib.rs
T
SamandClaude Fable 5 447e92aed5 three parked costs burned, two schedules go single-threaded
- the sun flare's terrain-occlusion march re-prices 2 grid cells a frame instead of 768 height queries, the retained pass's published regions go behind an Arc instead of two memcpys, and a unit's splash line is re-read only when its pose changed — −0.66 ms parked at Goldshire, pixels bit-identical
- PostUpdate and the render extract schedule run single-threaded by default — ~620 mostly no-op systems were paying the multi-threaded dispatch premium — −0.90 ms parked at Goldshire, −0.8 walking in Lower Blackrock Spire; WOW_MT_POSTUPDATE and WOW_MT_EXTRACT are the levers back
- GltfPlugin, AntiAliasPlugin and GilrsPlugin leave the boot — machinery for types nothing makes, 31 systems a frame fewer
- WOW_SCHED_CENSUS lists every schedule's systems with their executor-relevant flags, both worlds — the inventory the flips were priced against

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 18:59:48 +07:00

727 lines
40 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `benilla` — a from-scratch World of Warcraft 1.12.1 client on Bevy, talking to a local vmangos server.
//!
//! Opens the vanilla patch chain from wherever the install is (`$WOW_DATA`, the project folder on a
//! dev build, else beside the binary — decision 1175) and streams the world around the
//! player through Bevy's `AssetServer` (the `benilla-assets` `mpq://` pipeline): ADT terrain tiles within
//! `$WOW_TILE_RADIUS` — their splat-blended ground (tiling `$WOW_TEX_TILES`), doodads/WMOs, water, and
//! ground clutter — plus the avian colliders the character controller walks on. Lit by a time-of-day WoW
//! lighting model (`Light.dbc` sampled against the server clock) with a sky dome, sun/moon discs, and
//! distance fog; a faithful `EffectGlow` bloom on top.
//!
//! In parallel a background thread ([`net`]) logs in (`$WOW_USER`/`$WOW_PASS`/`$WOW_HOST`, default
//! `one`/`pone`/`localhost`), enters the world, and streams object updates. NPCs and GameObjects
//! render as their real models (resolved from the display id via the creature/GameObject catalogs); other
//! players stay cyan cubes, and our own avatar is blue until we take third-person control of it.
//! **The world is loaded when a character enters it, and released when they leave** — the glue
//! screens have no world behind them (decision 0777). With no server there is no world: the client
//! sits at the login screen, which is what the real one does. The scene harness (`$WOW_CAPTURE`)
//! boots straight in-world and is unaffected.
//!
//! Controls: WASD walks the avatar; right-drag turns it, left-drag orbits the camera (both
//! hide/freeze the cursor while held), scroll wheel zooms. Those are all **bindings** now (0997) —
//! rebindable, and nothing in the client squats on a bare key beside them (1043). The dev chord
//! (`Ctrl`+`Shift`) + `F` toggles free-fly, then WASD flies the camera with Space up / C down and
//! `Ctrl` boosts — a boost that exists only inside free-fly, itself behind the chord.
// **A player build's dead code is the seam working, not a defect.** With `--no-default-features`
// every symbol whose only caller is an instrument goes unused — the camera-park seam, the aim
// seam, the pool-slot identity, a dozen accessors the panel and the probes read. Warning about
// each one would bury a REAL warning in the one build nobody looks at daily, and the alternative
// (a `cfg` attribute per item) would spread seam knowledge back across the gameplay modules 1174
// spent its whole diff clearing it out of. Dev builds still warn normally.
#![cfg_attr(not(feature = "dev"), allow(dead_code))]
pub mod addon_harness;
mod area;
mod area_trigger;
#[cfg(feature = "dev")]
mod asset_churn;
mod aura_visual;
mod autocast_shine;
mod bindings;
mod blob_shadow;
mod bowstring;
#[cfg(feature = "dev")]
mod capture;
mod char_create;
mod char_select;
mod chat_bubble;
mod combat_text;
mod cooldowns;
mod creature_anim;
mod cursor;
mod cvars;
mod death;
#[cfg(feature = "dev")]
mod debug_panel;
/// **The dev/player seam** (decisions 0026/1173, built in 1174) — the group, and the boundary
/// rule, in one file. Always compiled; what it *holds* is not.
mod dev;
mod entities;
mod fishing_line;
mod footprints;
mod glue;
mod glue_strings;
mod go_anim;
mod go_templates;
#[cfg(feature = "dev")]
mod hover_log;
mod items;
mod loading_screen;
mod local_state;
mod login;
mod minimap;
mod nameplates;
mod names;
mod net;
mod npc_text;
mod pending_item_ops;
#[cfg(feature = "dev")]
mod perf;
mod pipe_warm;
mod player;
mod portrait;
#[cfg(feature = "dev")]
mod preflight;
#[cfg(feature = "dev")]
mod probe_shield;
mod quest_markers;
mod raid_marks;
mod run_mode;
mod shaders;
mod smart_rect;
mod sound;
mod target;
mod textinput;
mod transport;
mod ui_action;
mod ui_aura;
mod ui_bank;
mod ui_binder;
mod ui_cast;
mod ui_char;
mod ui_chat;
mod ui_craft;
mod ui_dressup;
mod ui_duel;
mod ui_follow;
mod ui_gamma;
mod ui_gossip;
mod ui_guild;
mod ui_hide;
mod ui_inspect;
mod ui_item_text;
mod ui_items;
mod ui_logout;
mod ui_loot;
mod ui_loot_roll;
mod ui_macro;
mod ui_mail;
mod ui_merchant;
mod ui_mirror;
mod ui_net;
mod ui_party;
mod ui_pass;
mod ui_pet;
mod ui_pet_book;
mod ui_pet_doll;
mod ui_pet_stats;
mod ui_quest;
mod ui_quest_log;
mod ui_reputation;
mod ui_saved;
mod ui_script;
mod ui_session;
mod ui_shapeshift;
mod ui_social;
mod ui_spellbook;
mod ui_talent;
mod ui_taxi;
mod ui_text;
mod ui_tooltip;
mod ui_trade;
mod ui_tradeskill;
mod ui_trainer;
mod ui_unit;
mod ui_world_map;
mod video;
mod vplates;
mod world_state;
use bevy::prelude::*;
use blob_shadow::BlobShadowPlugin;
use bowstring::BowstringPlugin;
use creature_anim::CreatureAnimPlugin;
use cursor::CursorPlugin;
use entities::EntitiesPlugin;
use fishing_line::FishingLinePlugin;
use footprints::FootprintsPlugin;
use loading_screen::LoadingScreenPlugin;
use net::NetPlugin;
use player::PlayerPlugin;
use portrait::PortraitPlugin;
use quest_markers::QuestMarkersPlugin;
use sound::SoundPlugin;
use target::TargetPlugin;
use textinput::TextInputPlugin;
use transport::TransportPlugin;
use ui_action::UiActionPlugin;
use ui_aura::UiAuraPlugin;
use ui_bank::UiBankPlugin;
use ui_binder::UiBinderPlugin;
use ui_cast::UiCastPlugin;
use ui_char::UiCharPlugin;
use ui_chat::UiChatPlugin;
use ui_craft::UiCraftPlugin;
use ui_duel::UiDuelPlugin;
use ui_follow::UiFollowPlugin;
use ui_gossip::UiGossipPlugin;
use ui_guild::UiGuildPlugin;
use ui_item_text::UiItemTextPlugin;
use ui_items::UiItemsPlugin;
use ui_logout::UiLogoutPlugin;
use ui_loot::UiLootPlugin;
use ui_loot_roll::UiLootRollPlugin;
use ui_mail::UiMailPlugin;
use ui_merchant::UiMerchantPlugin;
use ui_mirror::UiMirrorPlugin;
use ui_net::UiNetPlugin;
use ui_party::UiPartyPlugin;
use ui_pass::PlayerUiPlugin;
use ui_pet::UiPetPlugin;
use ui_pet_book::UiPetBookPlugin;
use ui_pet_doll::UiPetDollPlugin;
use ui_pet_stats::UiPetStatsPlugin;
use ui_quest::UiQuestPlugin;
use ui_quest_log::UiQuestLogPlugin;
use ui_saved::UiSavedPlugin;
use ui_script::UiScriptPlugin;
use ui_shapeshift::UiShapeshiftPlugin;
use ui_social::UiSocialPlugin;
use ui_spellbook::UiSpellbookPlugin;
use ui_talent::UiTalentPlugin;
use ui_taxi::UiTaxiPlugin;
use ui_text::UiTextPlugin;
use ui_tooltip::UiTooltipPlugin;
use ui_trade::UiTradePlugin;
use ui_tradeskill::UiTradeSkillPlugin;
use ui_trainer::UiTrainerPlugin;
use ui_unit::UiUnitPlugin;
// The `benilla` launcher shim (the bin package) is this library's only caller: it stamps the
// build id at compile time and hands it into [`run`]. Re-exported so the shim needs no bevy
// dep of its own.
pub use benilla_world::build_id::BuildId;
/// The world viewer's entry point — the engine with no game attached (decision 1160).
/// Its shim (`benilla-worldview`) is this library's second caller; see [`worldview`].
pub use benilla_world::worldview::run as run_worldview;
pub use bevy::app::AppExit;
/// Build and run the client app. `build` is the launcher shim's compile-time git stamp
/// ([`build_id`]) — passed in as plain data so the sha lives in the shim's fingerprint, not
/// this crate's, and a commit stops recompiling the app (decision 0993).
pub fn run(build: BuildId) -> AppExit {
// `WOW_CAPTURE=list` just prints the harness scenario names (the source of truth `scripts/visual.sh`
// reads) and exits before any window/asset setup.
// `WOW_HOVER_LOG_REPORT=<csv>` re-reads a recorded run and prints its report, then exits —
// no window, no game. New analysis lands on runs already captured (see `hover_log`).
if let Ok(path) = std::env::var("WOW_HOVER_LOG_REPORT") {
dev::report_recorded_hover_log(&path);
return AppExit::Success;
}
if std::env::var("WOW_CAPTURE").as_deref() == Ok("list") {
dev::print_scenario_names();
return AppExit::Success;
}
let mut app = App::new();
// The stamp is plain data from here on — the panel footer and preflight banner read it back.
app.insert_resource(build);
// Pin Bevy's static-scene transform tracking ON (decision 1356). At the default threshold
// `mark_dirty_trees` re-decides per frame by counting every changed-Transform row and every
// tree row — two full scans of the very population the tracking exists to skip. This scene
// is provably static-heavy (terrain/WMO batches and parked units barely move), so the
// auto-tuner can only ever confirm what this line states. The guard it removes bites only
// when MOST rows move in one frame (a load burst into a near-empty world), where
// dirty-marking briefly costs more than it saves — a loading-band term, accepted knowingly.
app.insert_resource(bevy::transform::systems::StaticTransformOptimizations::enabled());
// The Update schedule runs on the SINGLE-THREADED executor (decision 1366). Not a tuning
// whim: ~60% of our per-frame Update systems are non-Send (mlua's UiScript, kira's audio
// handles) and serialize through the multi-threaded executor's one `local_thread_running`
// flag anyway, so the cross-thread dispatch machinery was pure overhead — measured
// −1.30 cpu_ms at the LBRS pin under 5-round grading with the wall tail (p95/p99/max)
// flat-to-better (the 1364 "fatter tail" reading did not reproduce; 1366 has the tables,
// both pins). `WOW_MT_UPDATE=1` is the A/B lever back to the multi-threaded executor.
if std::env::var_os("WOW_MT_UPDATE").is_none() {
app.edit_schedule(Update, |s| {
s.set_executor_kind(bevy::ecs::schedule::ExecutorKind::SingleThreaded);
});
}
// PostUpdate too (decision 1437, closing 1366's named open probe — which expected a
// REGRESSION here and was refuted by measurement). The census: 208 systems paying
// ~10 µs/system of MT dispatch parked (schedule self 2.09 ms/f in the 1435 band map) while
// the wide bands the 1366 expectation leaned on are exactly the ones later work gated or
// pinned (1429 idle-gates the animation par sweep, 1356 pins static-transform tracking).
// Graded −0.49 cpu_ms parked (all 5 rounds negative) and winning IN MOTION as part of the
// combo (−0.76/−0.80 across two 4-round LBRS walk sittings), parked tails flat, motion
// p99/max flipping sign wholesale between sittings on both executors — 1366's own
// noise-dominated shape. `WOW_MT_POSTUPDATE=1` is the lever back; the engagement line makes
// every leg log name the config it measured.
if std::env::var_os("WOW_MT_POSTUPDATE").is_none() {
app.edit_schedule(PostUpdate, |s| {
s.set_executor_kind(bevy::ecs::schedule::ExecutorKind::SingleThreaded);
});
println!(
"executor: PostUpdate -> single-threaded (1437 default; WOW_MT_POSTUPDATE=1 for MT)"
);
}
// …and one startup line says which build produced this log. Registered HERE, beside the stamp
// it prints, rather than in `preflight` where it sat until decision 1179: **which build is
// this** is the first thing a bug report from someone else's machine has to establish, and a
// player build is the one whose logs always come from someone else's machine. Gating it out
// with the instruments got the argument exactly backwards (`preflight`'s own module doc makes
// the case; 1174 moved the file without re-reading it).
app.add_systems(Startup, benilla_world::build_id::banner);
// Visual A/B harness (decision 0008): with `$WOW_CAPTURE` set, the app runs a deterministic,
// server-less capture (net off so no NPCs stream in nondeterministically) and exits. See `capture`.
let capturing = run_mode::scenario_active();
// Any instrumented run — captures AND the live-probe fleet — opens in the background so it
// never fights the director's screen (decision 0703; `WOW_BG` overrides). See `bgwin`.
let background = benilla_world::bgwin::background_run();
if capturing {
// Ground clutter scatters with per-run randomness, so disable it for byte-stable baselines
// — clutter isn't what the lighting rework validates, and the regression diff must not be
// masked by grass wobble. Set before plugins build so `ClutterConfig::from_env` reads it.
// It is not the only source of per-run drift, though it was long documented as such: the
// other is the frame clock itself, frozen in `capture` (decision 0723).
std::env::set_var("WOW_CLUTTER_DENSITY", "0");
// Third source of per-run drift, and the one the lighting matrix (decision 0746) hit: the
// anim-LOD park/wake gate (`creature_anim::lod::gate_rig_animation`). Whether a rig is
// parked, and which pose it wakes into, depends on when its model finished loading relative
// to the frustum/room evaluation — asset-load timing, which the frozen clock does not
// control. MEASURED: the seeded wolf lands in one of exactly two poses, the pair always
// MAE 4.123 apart, and three runs with the gate off are bit-identical.
//
// Off for captures, unless explicitly overridden. A still frame is meant to contain only
// rigs that are IN view, and a rig in view is one the gate should never park — so what a
// capture loses is the gate's own correctness, not the shot's subject. That loss is named,
// not free: a rig wrongly parked in frame stays invisible to the sweep, and the deeper
// question the measurement raises — why a woken rig does not converge on the absolute-clock
// pose the gate promises — is open in 0746 and worth its own hunt.
if std::env::var("WOW_NO_ANIM_LOD").is_err() {
std::env::set_var("WOW_NO_ANIM_LOD", "1");
}
}
// The `mpq://` asset source must be registered BEFORE `AssetPlugin` (inside `DefaultPlugins`)
// builds. Finds the install the one way anything does (`benilla_formats::wow_data`, decision
// 1175) — the same answer the `WorldAssets` foundation gets. On failure the source is simply
// absent and the AdtTile-pipeline loads fail gracefully (the terrain just doesn't appear).
match benilla_formats::wow_data() {
Some(data_dir) => {
if let Err(e) = benilla_assets::register_mpq_source(&mut app, &data_dir) {
eprintln!("benilla-assets: mpq:// source unavailable ({e:#})");
}
}
None => eprintln!(
"benilla: no WoW install found — looked in {:?}",
benilla_formats::candidates()
),
}
// NOTE: there is deliberately no `game://` asset source here (decision 1175). 1171 gave the
// game's five UI shaders their own source pointed at this crate's `assets/` — with the path
// baked from `CARGO_MANIFEST_DIR`, so it named the build machine's source tree and resolved
// to nothing anywhere else. The line 1171 drew survives: those five are still this crate's,
// now compiled in by `crate::shaders` and addressed `embedded://benilla_app/shaders/…`,
// because `embedded_asset!` is per-crate by construction. Only the directory is gone.
app.add_plugins(benilla_world::boot::tuned_default_plugins(Window {
title: "benilla".into(),
// UI-fixture captures shrink the window so the docked panel fills the frame — the capture
// is the look-pass instrument and the window is its subject. The action bar is the
// exception: it spans 1024px + 128px end caps along the screen bottom, so it gets a wide,
// short window instead of the tall default (else the caps crop). The vplates scenario pins
// the 1:1 gx window: at 1024×768 one gx unit = 1280 px, so the plate must land at the
// border texture's native 128×32 — directly diffable against the decoded BLP. Sized
// per-capture off WOW_CAPTURE.
resolution: if capturing && std::env::var("WOW_CAPTURE_UI").as_deref() == Ok("1") {
// `$WOW_WIN` overrides here too — the resolution-A/B instrument for UI scenarios (a
// scale-dependent text bug looks fine at the scenario's default size and truncates at
// fullscreen heights).
if let Some(win) = std::env::var("WOW_WIN").ok().and_then(|v| {
let (w, h) = v.split_once('x')?;
Some(UVec2::new(w.parse().ok()?, h.parse().ok()?))
}) {
win
} else {
match std::env::var("WOW_CAPTURE").as_deref() {
Ok("ui-actionbar") => UVec2::new(1300, 260),
Ok("vplates") => UVec2::new(1024, 768),
// The director's small-window shape: short enough that the action bar strip
// overlaps the chat edit box rows — the overlap is the subject.
Ok("ui-chatedit") => UVec2::new(566, 377),
// The fullscreen map's chrome is a centered 1024×768 block; a hair of margin
// shows the blackout doing its job.
Ok("ui-worldmap") => UVec2::new(1100, 800),
// The 920×724 era options window wants air on every side so the straddling
// right-edge tile and the hung close X stay in frame.
Ok("ui-options") | Ok("ui-options-audio") | Ok("ui-options-graphics") => {
UVec2::new(1200, 900)
}
_ => UVec2::new(640, 700),
}
}
.into()
} else {
// `$WOW_WIN=WxH` (logical px): override the world capture/window size — the
// resolution-A/B instrument. The FFXGlow blur geometry is byte-pinned in TEXELS, so its
// angular footprint shrinks as resolution grows and thin bright features (fence rails)
// self-amplify at 4K where the 1024-era reference diluted them; matching the era's
// pixel density (e.g. `WOW_WIN=512x288` on a 2× display → 1024×576 physical) isolates
// that term. Also the knob for any future era-resolution comparison.
std::env::var("WOW_WIN")
.ok()
.and_then(|v| {
let (w, h) = v.split_once('x')?;
Some(UVec2::new(w.parse().ok()?, h.parse().ok()?))
})
// A run that reads no pixels gets a SMALL window. It is held `AlwaysOnTop` for its
// whole life so it can never be occluded into the ~1 fps throttle
// (`capture::ProbeFocusPlugin`, decision 0906) — at the full default that meant
// every agent probe planted a screen-filling window over the director's work. Small
// + cornered (`ProbeFocusPlugin` parks it) is un-occludable AND out of the way;
// anything photographing pixels keeps the full size, and `WOW_WIN` overrides either
// way (decision 1148).
.unwrap_or(if benilla_world::bgwin::no_pixel_run() {
UVec2::new(640, 360)
} else {
UVec2::new(1600, 900)
})
.into()
},
// The boot present mode. `$WOW_NOVSYNC=1` uncaps presentation so a headless FPS-journal
// run measures true frame cost, not the vsync ceiling — the same uncap the capture probe
// flips mid-run, available from boot for non-capture probes (perf triage at the glue
// screens, where no capture scenario runs). Absent it, we boot synced and the player's
// `gxVSync` setting takes over from `Startup` on ([`crate::video`]).
present_mode: video::present_mode(!video::novsync_env()),
// An instrumented run must never fight the director's screen (decision 0703).
// Focused, it steals the keyboard — on 2026-07-19 a login-shot run swallowed
// their keystrokes out of another app and typed them into the account box,
// which is also how that capture lost the bare caret it was taken to measure.
// So every probe/capture/regression run (`bgwin`) opens unfocused; an ordinary
// `cargo run` is unaffected and focuses normally.
//
// A background run is BORN at `AlwaysOnBottom` (`kCGNormalWindowLevel - 1`)
// so it can never flash over their work on the way up — winit raises a new
// window twice before our first frame runs, and at the normal level that
// showed as ~half a second of probe window on top (measured). But it does not
// STAY there: that level is a cage, and 0703 leaving it on for the whole run
// is why an instrumented window could never be raised again however hard you
// clicked it. `BgWinPlugin` promotes it back to Normal the moment the launch
// settles (decision 0709), and owns the app-level half — winit's forced macOS
// app activation — as well.
focused: !background,
window_level: if background {
bevy::window::WindowLevel::AlwaysOnBottom
} else {
bevy::window::WindowLevel::Normal
},
..default()
}))
// The game's own WGSL, compiled into the binary (decision 1175) — before anything that could
// ask for one. The engine's seven register themselves inside `WorldPlugins` below.
.add_plugins(shaders::plugin)
.add_plugins(benilla_world::thread_qos::ThreadQosPlugin)
// The app-side half of background instrumented runs: undo winit's forced macOS app
// activation so a probe/capture launch never yanks focus off the director's screen. The
// window-side half (unfocused + always-on-bottom) is in the `Window` above. Decision 0703.
.add_plugins(benilla_world::bgwin::BgWinPlugin)
// **The engine, as one name** (decision 1164). Everything `benilla-world` will own,
// in the order both binaries used before this group existed — see `world_plugins.rs`
// for the two ordering edges inside it that are load-bearing, and for what is
// deliberately left out (`pipe_warm`).
.add_plugins(benilla_world::world_plugins::WorldPlugins)
// The instruments, which the engine group deliberately does not carry (1160: instruments at
// the top of the stack). The panel first — `PerfPlugin` needs the egui plugin/context it sets
// up. Toggles: the dev chord + D, and P.
// **The instruments** (decisions 1173/1174): the debug panel, the perf HUD, the object
// inspector, the hover-cost recorder, the asset-churn meter, the session preflight and the
// probe shield — one group, in the slot the panel has always held (it sets up the egui
// context the perf pill needs). `--no-default-features` compiles every one of them out; see
// `dev.rs` for what is in the group and the one rule that governs the boundary.
.add_plugins(dev::DevToolsPlugin)
.add_plugins(BowstringPlugin)
.add_plugins(FishingLinePlugin)
.add_plugins(QuestMarkersPlugin)
// Pipeline-compile counters + the live-compile tripwire (decision 0837: macOS builds every
// pipeline synchronously on the render thread, so a live compile is a felt stall).
.add_plugins(pipe_warm::plugin)
// Streamed world entities: cube assets + display catalogs at startup, sync each frame.
.add_plugins(EntitiesPlugin)
// Creature animation: pick Stand/Walk/Run from each creature's movement state each frame (Milestone C).
.add_plugins(CreatureAnimPlugin)
// The unit blob shadow: the dark ground oval under every unit, sized from the playing
// animation's box (the byte-verified law — wow-re unit-blob-shadow RE), on the same
// surface-decal projector as the selection ring.
.add_plugins(BlobShadowPlugin)
// Footprint decals (B212, decision 1006): the prints a walking unit leaves on snow/sand,
// spawn-once projections on the same decal projector, fading off the effect stream.
.add_plugins(FootprintsPlugin)
// GameObject animation (decision 0242): net-streamed GObjects (doors/chests) play an M2 sequence
// on GAMEOBJECT_STATE change — the state-machine sibling of the doodad idle loop above.
.add_plugins(go_anim::plugin)
// Avatar + camera + input.
.add_plugins(PlayerPlugin)
// The real client's hardware mouse cursor (native NSCursor on macOS).
.add_plugins(CursorPlugin)
// Net↔ECS bridge: spawns the world thread, exposes the snapshot + writer resources. In capture
// mode the IO thread is skipped (`connect: false`) so the scene is deterministic.
.add_plugins(NetPlugin {
connect: !capturing,
})
// The death arc (decision 0308): the wire-fed death stores + the root/water-walk ack messages.
.add_plugins(death::DeathPlugin)
// The shared glue vocabulary both pre-world screens stand on (decision 0465): the ADD-mode UI
// material, the client-data art set, the GlueStrings table.
.add_plugins(glue::GluePlugin)
// The glue layer (decision 0193): the ClientState machine + the character-select screen
// that answers the parked IO thread's pick. A world capture boots straight InWorld (no net,
// no picker); a glue capture boots onto the screen it photographs.
.add_plugins(char_select::CharSelectPlugin {
start: run_mode::start_state(),
})
// The login screen (decision 0539): the faithful AccountLogin glue + the credential policy
// that answers the IO thread's pre-logon park.
.add_plugins(login::LoginPlugin)
// The character-creation screen + its live preview booth (decision 0423).
.add_plugins(char_create::CharCreatePlugin)
// Audio: the delegated mixer + WoW's owned selection layer (decision 0070).
.add_plugins(SoundPlugin)
// Targeting: left-click a unit to select it (→ CMSG_SET_SELECTION) + draw its ground ring.
.add_plugins(TargetPlugin)
.add_plugins(TransportPlugin)
// Faithful world-load splash + progress bar on startup + cross-map teleport (the load latency
// streaming can't hide); per-map art via the Map.dbc→LoadingScreens.dbc→BLP chain.
.add_plugins(LoadingScreenPlugin)
// The player-UI quad pass (decision 0068 §2): its own composited-above-the-world,
// below-the-egui-dev-overlays camera + sorted-quad renderer. `$WOW_UI_DEMO=1` seeds a proof scene.
.add_plugins(PlayerUiPlugin)
// The HUD minimap (decision 0203 phase 1): fills the `<Minimap>` widget's extracted hole with
// the streamed tile window + mask + player arrow, and feeds the zone text.
.add_plugins(minimap::MinimapPlugin)
// The pet-bar / spellbook autocast shine, drawn on the append lane from the conversion's
// parked sites — zero per-frame script-layout traffic (decision 1383, B282).
.add_plugins(autocast_shine::AutocastShinePlugin)
// The shared AreaTable catalog + the ZONE_CHANGED event family / zone-text host globals
// behind GetZoneText & co. (the zone-entry splash arc, decision 0287).
.add_plugins(area::AreaPlugin)
// The `AreaTrigger.dbc` volumes + the per-frame containment check that reports walking into
// one (`CMSG_AREATRIGGER`) — the client's whole part in portals, instance entrances and
// explore objectives; the server owns what each trigger means.
.add_plugins(area_trigger::AreaTriggerPlugin)
.add_plugins(ui_world_map::WorldMapUiPlugin)
// The glyph atlas (client TTFs -> baked bitmap) `ui_script`'s extraction draws `FontString`
// regions through. Loads at Startup, after the asset chain opens (decision 0068 §2).
.add_plugins(UiTextPlugin)
// Unit-frame portraits: the token -> off-screen-baked-face bridge the UI extract samples for a
// `SetPortraitTexture`-bound region (the modern high-res 2D model bake).
.add_plugins(PortraitPlugin)
.add_plugins((TextInputPlugin, UiScriptPlugin))
// The video knobs the CVar host writes into (today: `gxVSync`). Before CvarPlugin so the
// resource exists when `load_config` applies the saved value at Startup.
.add_plugins(video::VideoPlugin)
// The CVar host (decision 0954): registration, knob sync, config.toml persistence. After
// UiScriptPlugin only for reading order — its systems gate on the VM existing anyway.
.add_plugins(cvars::CvarPlugin)
// The key-binding engine (decision 0997): the chord→command dispatch every rebindable input
// runs through, its persistence, and the Key Bindings window's capture seam.
.add_plugins(bindings::BindingsPlugin)
// The unit snapshot + event feed (decision 0068 §3): pushes ECS game state into the VM as the
// plain data the `Unit*` bindings read, and fires the matching WoW events.
.add_plugins(UiUnitPlugin)
.add_plugins(UiPartyPlugin)
// Duels (decision 0633): the wire session, the client-side countdown tick, the four Era
// events, and the accept/cancel/challenge intents.
.add_plugins(UiDuelPlugin)
// Setting your hearthstone (decision 1331): the innkeeper's SMSG_BINDER_CONFIRM question, the
// CONFIRM_BINDER dialog it raises, and the CMSG_BINDER_ACTIVATE its Accept sends — the only
// packet in the flow that actually binds anything.
.add_plugins(UiBinderPlugin)
// Auto-follow's UI seam: the popup's Follow row + `FollowUnit`/`FollowByName` inbound, and
// the AUTOFOLLOW_BEGIN/END pair that drives the centre-screen status line outbound.
.add_plugins(UiFollowPlugin)
// Leaving (decision 0674): the game menu's Logout/Exit Game — the request, the server's
// 20-second answer narrated as the CAMP/QUIT countdown, and the process exit.
.add_plugins(UiLogoutPlugin)
.add_plugins(UiSocialPlugin)
// Guilds (decision 1257): the identity/roster mirror behind the four guild windows, the
// membership verbs, and the `ERR_GUILD_*` lines. Right after the social session, whose
// FriendsFrame it shares a window with and whose ignore list its sign-on lines consult.
.add_plugins(UiGuildPlugin)
.add_plugins(UiTooltipPlugin)
// The character-window feed (decision 0208): the combat-stats/inventory snapshots + events
// the paper doll reads, and the paper-doll booth's yaw mirror.
.add_plugins(UiCharPlugin)
// The reputation-pane feed: the player's wire faction slots resolved against Faction.dbc into
// the pane's snapshot, plus the pane's three outbound verbs. Beside the character feed because
// it is the same window's other tab.
.add_plugins(ui_reputation::UiReputationPlugin)
// The inspect feed (decision 0631): another player's equipment off their PUBLIC visible-item
// entries, plus the "inspect" booth's unit + yaw. Right after the character feed it mirrors.
.add_plugins(ui_inspect::InspectUiPlugin)
// The dressing-room feed (decision 1060): the window's try-on intents → the player's own look
// with the tried-on items substituted in, plus the "dressup" booth's yaw. Beside the inspect
// feed, whose shape it shares (intents in, a booth look out).
.add_plugins(ui_dressup::DressUpUiPlugin)
.add_plugins(UiActionPlugin)
// The aura feed (decisions 0255/0257): the player's insertion-ordered buff/debuff cache + the
// self-only durations, pushed as the data the `UnitAura` bindings read; fires UNIT_AURA and
// drains the right-click cancels. After UiActionPlugin (shares its `Spells` catalog).
.add_plugins(UiAuraPlugin)
// The spellbook window feed (decision 0216 §8, slice 5): builds the book from
// PlayerActions.spells through the Spell.dbc/SkillLine.dbc join and drives
// SpellBookFrame.xml's snapshot + cast-drain seam — the spell SOURCE for the cursor payload
// arc (bags/doll/bars/book). After UiActionPlugin (shares its `Spells` resource + the cast
// tail `send_spell_cast`).
.add_plugins(UiSpellbookPlugin)
// The macro system (decision 0983): the icon chooser's catalog, the `benilla-config/macros/`
// files, `UPDATE_MACROS`, and the macro→bound-spell table the action bar's MACRO slots
// resolve their cooldown/usability through. After UiSpellbookPlugin — the bound spell is
// resolved against the book that feed pushes, by the same law `CastSpellByName` uses.
.add_plugins(ui_macro::UiMacroPlugin)
// The talent window feed (decision 0304): builds the class pages from Talent.dbc × the
// known-spell set + PLAYER_CHARACTER_POINTS, drives TalentFrame.xml through the engine's
// talent seam, and drains learn clicks into CMSG_LEARN_TALENT. After UiActionPlugin
// (shares its `Spells` catalog), beside the spellbook it mirrors.
.add_plugins(UiTalentPlugin)
// The stance/shapeshift bar feed (wow-re shapeshift-bar-api.md): builds the form list from
// PlayerActions.spells per the byte-verified admission/order, drives StanceBar.xml through
// the engine's shapeshift seam, and drains its clicks (cancel-if-active else cast). After
// UiActionPlugin (shares `Spells`, the `usable` walk, and the cast tail).
.add_plugins(UiShapeshiftPlugin)
// The pet action bar (decision 0982) — the stance bar's mirror image: server-authoritative,
// so this renders the ten packed words the last `SMSG_PET_SPELLS` delivered and sends
// intents back. After UiActionPlugin (shares `Spells` and the cooldown triple's clock).
.add_plugins(UiPetPlugin)
.add_plugins(UiPetBookPlugin)
// The pet's paper-doll stat block (happiness/loyalty/XP/training points). Its own plugin
// because it runs off descriptor fields and two DBC tables rather than off `SMSG_PET_SPELLS`.
.add_plugins(UiPetStatsPlugin)
// The pet paper doll's SHARED surface (decision 1057) — the combat-stats snapshot under the
// `"pet"` token and the page's model booth. Apart from the block above because these values
// pass through the character sheet's own bindings and events, with no hunter gate.
.add_plugins(UiPetDollPlugin)
// The connection-telemetry feed: the averaged ping RTT behind `GetNetStats()`, which the main
// bar's performance meter polls (decision 0658).
.add_plugins(UiNetPlugin)
.add_plugins(UiCastPlugin)
// The breath / fatigue bars (decision 0874): server-authoritative mirror timers off the
// wire into the transcribed MirrorTimer1/2/3 frames. Beside the cast bar it shares its
// feed→drain shape (and its art: the same UI-CastingBar-Border chrome).
.add_plugins(UiMirrorPlugin)
// Floating combat text (decision 0137 phase 2): the WORLDTEXTSTRING law — world-anchored
// damage numbers/outcome words projected into the UI quad pass each frame.
.add_plugins(combat_text::CombatTextPlugin)
// Overhead unit names (nameplates): world-billboard name text over players + NPCs.
.add_plugins(nameplates::NameplatesPlugin)
// Raid-target marker billboards (0434 §6): the mark icon over marked units, one line-pitch
// above the overhead name; plated units show the plate's raid child instead.
.add_plugins(raid_marks::RaidMarksPlugin)
// V-key nameplates (0167): the toggled health-bar plates, a 2-D overlay replacing the
// overhead name on plated units.
.add_plugins(vplates::VPlatesPlugin)
// Chat speech bubbles (0598): the over-the-head bubble a say/yell/party line spawns, the
// plates' 2-D overlay sibling — mutually exclusive with both the plate and the name.
.add_plugins(chat_bubble::ChatBubblePlugin)
// TOGGLEUI (`CTRL-Z`/`Cmd-Z`): the whole quad layer goes dark — frames, minimap, plates,
// bubbles, combat text — leaving the world and the cursor.
.add_plugins(ui_hide::UiHidePlugin)
.add_plugins(UiItemsPlugin)
// The gossip window (decision 0081): fills from the net drain's GossipState and drives
// GossipFrame.xml over the Era gossip API.
.add_plugins(UiGossipPlugin)
// The merchant window (decision 0081 phase 4): fills from the net drain's MerchantOpen and
// drives MerchantFrame.xml over the Era vendor API + the money display.
.add_plugins(UiMerchantPlugin)
// The bank window (decision 0604): the SHOW_BANK session (BankOpen) + the purchase row;
// the vault's slots ride the container feed as bags −1/5..=10.
.add_plugins(UiBankPlugin)
// The mail window (decision 0544 P1/P2): the client-side mailbox session (MailOpen), the
// NPC-session range guard, and MailFrame.xml over the Era mail API (inbox, open-letter,
// send tab).
.add_plugins(UiMailPlugin)
// Player-to-player trade (TradeFrame.xml): the two-sided trade window, driven server-side over
// the P0 wire; the partner's portrait rides the shared "npc" booth (decision 0592 P1).
.add_plugins(UiTradePlugin)
// The item-text reader (ItemTextFrame.xml): right-clicked bag letters (mail-made permanent
// copies) read in the reference reader window over the shared ask-once item-text cache.
.add_plugins(UiItemTextPlugin)
.add_plugins(UiSavedPlugin)
.add_plugins(UiTrainerPlugin)
// The taxi map (decision 0484 phases 1-2): the SMSG_SHOWTAXINODES-fed TaxiState resource, the
// NPC-session range guard, and the TaxiFrame.xml window feed/drain (catalogs, node
// projection/route computation, the activate send, the UnitOnTaxi ride flag).
.add_plugins(UiTaxiPlugin)
.add_plugins(UiTradeSkillPlugin)
.add_plugins(UiCraftPlugin)
// The loot window (decision 0084): fills from the net drain's LootState and drives
// LootFrame.xml over the Era loot API (coin + rows, paging).
.add_plugins(UiLootPlugin)
.add_plugins(UiLootRollPlugin)
// The questgiver window (decision 0088): fills from the net drain's QuestGiver and drives
// QuestFrame.xml's four sub-panels over the Era quest API.
.add_plugins(UiQuestPlugin)
// The quest-log window (decision 0088's deferred second slice): fills from the self player's
// PLAYER_QUEST_LOG descriptor slots + the SMSG_QUEST_QUERY_RESPONSE template cache, and drives
// QuestLogFrame.xml over the Era quest-log API.
.add_plugins(UiQuestLogPlugin)
.add_plugins(UiChatPlugin);
// Register benilla-assets' loaders AFTER `AssetPlugin` (they go into the live `AssetServer`).
benilla_assets::register_asset_loaders(&mut app);
// The render app's `ExtractSchedule` runs SINGLE-THREADED too (decision 1437, same grading
// as the PostUpdate flip above): bevy_render never sets an executor kind, so it ran the
// multi-threaded one by default — 0.34 ms/f of schedule self in the 1435 parked band map
// over 165 systems (census) with zero true non-Send members. Graded −0.26 parked alone,
// −0.90 parked as the combo. `WOW_MT_EXTRACT=1` is the lever back.
//
// The `Render` schedule stays MULTI-threaded, and not as a tuning judgment (1437, measured
// the hard way): under pipelined rendering the MT executor is ALSO bevy's non-Send→main-
// thread routing, and `bevy_render::view::window::create_surfaces` is non-Send precisely to
// ride it — on macOS a Metal layer can only be made on the UI thread, and the ST executor
// runs everything on the render thread (`get_metal_layer cannot be called in non-ui thread`,
// a startup panic). One system pins the schedule; its 1.06 ms/f self stays on the table
// until that coupling changes upstream.
//
// Lives HERE, not beside its siblings: the render sub-app only exists once the plugin chain
// has built (pipelining detaches it at cleanup, later still).
if std::env::var_os("WOW_MT_EXTRACT").is_none() {
if let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) {
render_app.edit_schedule(bevy::render::ExtractSchedule, |s| {
s.set_executor_kind(bevy::ecs::schedule::ExecutorKind::SingleThreaded);
});
println!(
"executor: ExtractSchedule -> single-threaded (1437 default; WOW_MT_EXTRACT=1 for MT)"
);
} else {
// A silently missing sub-app would flip nothing — say so instead of measuring a ghost.
eprintln!("executor: no render app — ExtractSchedule flip NOT applied");
}
}
// **The probe fleet** — the capture harness and every scripted live probe, each armed by its
// own environment variable and inert without it. Added last so they observe the fully-built
// app; compiled out entirely by `--no-default-features` (decisions 1173/1174), which is why
// this is one line and the twenty env checks behind it live in `dev.rs`.
app.add_plugins(dev::DevProbesPlugin);
// Return the app's own exit status instead of dropping it: a failed capture writes
// `AppExit::error()` (see `capture::drive_capture`), and discarding it made the process exit 0
// with no PNG on disk — which is how a sweep carried on around a missing shot (decision 0743).
app.run()
}