Files
benilla-octo/crates/benilla-app/src/dev.rs
T
SamandClaude Fable 5 6b980b9efb the player build: one binary that carries its content
- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module
- the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder
- the glue screens get a capture shutter, and the engine stops depending on the protocol crate

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

249 lines
16 KiB
Rust

//! **The dev/player seam** — the two instrument groups, and the one rule that governs them
//! (decision 0026 set the target, 1173 mandated the build, 1174 is what got built).
//!
//! ```text
//! Dev may see anything; nothing may depend on dev.
//! ```
//!
//! That is the whole boundary, and it is one-directional on purpose. An instrument's job *is* to
//! see everything — 1160 measured 266 references from the instruments into 63 of the app's 142
//! modules — so a crate wall here would mean publishing hundreds of internals as permanent public
//! API shaped by whatever a panel wanted to poke at. A `cfg` seam has the opposite property: the
//! instruments keep reaching in exactly as far as they like, and only the *direction* is
//! constrained.
//!
//! **The mechanism is not this comment.** It is `cargo build -p benilla --no-default-features` in
//! `scripts/gates.sh`. 0026 wrote this same rule down in June 2026 with no failing build behind
//! it, and by August there were 24 references from non-dev code into the instruments across 12
//! files (1173) — not through carelessness, but because a rule with no failing test is a wish.
//! When gameplay needs a fact an instrument happens to know, the fact goes to
//! [`crate::run_mode`] (always present, player-faithful default) and the instrument writes it
//! there. It does not get a `use` line from gameplay.
//!
//! **What is deliberately NOT in here:** `benilla_world::dev_state` (the always-present config
//! layer — its defaults *are* the player behaviour, so it ships), `pipe_warm` (a player on macOS
//! eats every synchronous pipeline stall without it — 0837/1116), and `art_scope` (within-map art
//! residency: engine, and it travels with `WorldPlugins` — 0729).
use bevy::prelude::*;
/// `WOW_CAPTURE=list` — the harness scenario names `scripts/visual.sh` reads, printed before any
/// window or asset setup. Answers nothing in a player build, which has no scenarios.
pub(crate) fn print_scenario_names() {
#[cfg(feature = "dev")]
crate::capture::print_scenario_names();
}
/// `WOW_HOVER_LOG_REPORT=<csv>` — re-read a recorded run and print its report, no window, no game.
/// New analysis lands on runs already captured (see `hover_log`).
pub(crate) fn report_recorded_hover_log(_path: &str) {
#[cfg(feature = "dev")]
crate::hover_log::report_recorded_file(_path);
}
/// **The instruments.** One group, added where the debug panel has always sat — first among them,
/// because `PerfPlugin` needs the egui plugin and context it sets up.
pub(crate) struct DevToolsPlugin;
impl Plugin for DevToolsPlugin {
#[cfg_attr(not(feature = "dev"), allow(unused_variables))]
fn build(&self, app: &mut App) {
#[cfg(feature = "dev")]
{
app.add_plugins(crate::debug_panel::DebugPanelPlugin)
.add_plugins(crate::perf::PerfPlugin)
// `WOW_FX_CENSUS=1`: where this frame's particle draws are addressed, and whether
// the view they name is switched on (decision 0775). An instrument, and one that
// reads the portrait booths — so it belongs on this side of the line.
.add_plugins(crate::capture::fx_draw_census_plugin)
// The hover-cost recorder (`WOW_HOVER_LOG`) and the asset-churn meter
// (`WOW_ASSET_CHURN`) — both no-ops without their variable.
.add_plugins(crate::hover_log::HoverLogPlugin)
.add_plugins(crate::asset_churn::AssetChurnPlugin)
// The session preflight (decision 0649): one banner per world entry naming the body
// we logged into, and loud warnings for the states — dead/ghost, GM mode,
// server-blocked movement — that silently invalidate a session's readings. Never
// env-gated; a warning nobody switches on isn't one.
.add_plugins(crate::preflight::PreflightPlugin)
// The probe shield (decision 0677): a body on a probe account is put into vmangos's
// `.cheat god` on every world entry — damage clamps at 1 hp instead of killing —
// and GM mode is turned OFF, because the shield replaces the only reason it was
// ever on. Inert on any other account.
.add_plugins(crate::probe_shield::ProbeShieldPlugin);
}
}
}
/// **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.
pub(crate) struct DevProbesPlugin;
impl Plugin for DevProbesPlugin {
#[cfg_attr(not(feature = "dev"), allow(unused_variables))]
fn build(&self, app: &mut App) {
#[cfg(feature = "dev")]
{
// The capture harness drives one deterministic screenshot then exits — added last so it observes
// the fully-built app. Inert unless `$WOW_CAPTURE` is set.
if crate::run_mode::scenario_active() {
app.add_plugins(crate::capture::CapturePlugin);
}
// The LIVE probe shot (orthogonal to the harness): `WOW_LIVE_SHOT=<png>` on a NORMAL connected
// run writes one screenshot `WOW_LIVE_SHOT_AT` seconds (default 12) after startup and keeps
// running — the agent-side instrument for seeing a live server scene (NPCs, GameObjects, event
// spawns) without a scenario. Pair with `WOW_USER`/`WOW_CHAR` + an outer `timeout`.
if std::env::var("WOW_LIVE_SHOT").is_ok() {
app.add_plugins(crate::capture::LiveShotPlugin);
}
// The probe RIG (decision 0651): `WOW_RIG="tauren druid 60 gear:heal-preraid-bis"` finds-or-
// creates that body on this slot's probe account, logs in as it, and applies level/spells/gear/
// spec/place — the one verb that replaces the hand-assembled GM recipe every session used to
// re-derive (see `capture::ProbeRigPlugin`).
if std::env::var("WOW_RIG").is_ok() {
app.add_plugins(crate::capture::ProbeRigPlugin);
}
// Any scripted probe keeps its window un-occludable: a fully covered macOS window drops to
// ~1 fps drawables, and every probe schedule is wall-clock — a throttled run doesn't measure
// slowly, it runs the wrong script (see `capture::ProbeFocusPlugin`, decision 0906).
// (`WOW_LIVE_FPS` is in the list because an occluded SETTLE phase streams the world at ~1 fps
// and under-warms the scene before sampling even starts — the assertion has to be live from
// the first tick, not at the uncap.)
if [
"WOW_PROBE",
"WOW_PROBE_CHAT",
"WOW_PROBE_KEY",
"WOW_PROBE_LUA",
"WOW_RIG",
"WOW_LIVE_FPS",
]
.iter()
.any(|k| std::env::var(k).is_ok())
{
app.add_plugins(crate::capture::ProbeFocusPlugin);
}
// The probe-chat one-shot: `WOW_PROBE_CHAT=".go xyz …"` sends GM/chat lines once in-world —
// the "park the probe character anywhere" instrument (see `capture::ProbeChatPlugin`).
if std::env::var("WOW_PROBE_CHAT").is_ok() {
app.add_plugins(crate::capture::ProbeChatPlugin);
}
// The probe-lua one-shot: `WOW_PROBE_LUA="CastSpell(…)"` runs a chunk in the live UI VM once
// in-world — the "press the button headlessly" instrument (see `capture::ProbeLuaPlugin`).
if std::env::var("WOW_PROBE_LUA").is_ok() {
app.add_plugins(crate::capture::ProbeLuaPlugin);
}
// The probe-key taps: `WOW_PROBE_KEY="Space@14"` presses keys once in-world — the "press
// space headlessly" instrument for input-gated behavior (see `capture::ProbeKeyPlugin`).
if std::env::var("WOW_PROBE_KEY").is_ok() {
app.add_plugins(crate::capture::ProbeKeyPlugin);
}
// The probe self-termination: `WOW_PROBE_EXIT_AT=<secs>` bounds any scripted live probe's
// lifetime — its own knob, not a rider on the Lua probe (see `capture::ProbeExitPlugin`).
if std::env::var("WOW_PROBE_EXIT_AT").is_ok() {
app.add_plugins(crate::capture::ProbeExitPlugin);
}
// The ray pick: `WOW_PICK="<x>,<y>"` names every surface along the ray through a screenshot
// pixel, nearest first — "what is at the spot `benilla-visual hotspot` flagged, and what is
// right behind it" (see `capture::PickProbePlugin`).
if std::env::var("WOW_PICK").is_ok() {
app.add_plugins(crate::capture::PickProbePlugin);
}
// The render-phase census: `WOW_PHASE=<uniqueId>` reports, per frame, which phase each of one
// placement's batches landed in and where in the draw order — the one thing every scene-side
// instrument is blind to, namely whether a surface was submitted at all (see
// `capture::PhaseProbePlugin`).
if std::env::var("WOW_PHASE").is_ok() {
app.add_plugins(crate::capture::PhaseProbePlugin);
}
// The depth readback: `WOW_DEPTH="<x>,<y>"` reports what depth actually won each named pixel,
// per frame, as a distance in yards — the link past submission that decides the pixel. Pair it
// with `WOW_PICK` at the same pixels to turn "what won" into "whose it was" (see
// `capture::DepthProbePlugin`).
// `WOW_DEPTH_QUADS=<bone>…` is the same readback taken at a particle quad's OWN pixels — the
// moving-subject form, which no hand-written pixel list can hold (see `capture::depth_probe`).
if std::env::var("WOW_DEPTH").is_ok() || std::env::var("WOW_DEPTH_QUADS").is_ok() {
app.add_plugins(crate::capture::DepthProbePlugin);
}
// The bevy_ui node census — "who owns this rectangle" for UI outside the FrameXML quad pass
// (see `capture::NodeProbePlugin`).
if std::env::var("WOW_NODE_PROBE").is_ok() {
app.add_plugins(crate::capture::NodeProbePlugin);
}
// The mid-run window resize: `WOW_PROBE_RESIZE="<secs>:<W>x<H>"` — the headless fullscreen-
// toggle stand-in for resize-reactive layout (see `capture::ProbeResizePlugin`).
if std::env::var("WOW_PROBE_RESIZE").is_ok() {
app.add_plugins(crate::capture::ProbeResizePlugin);
}
// The particle census: `WOW_PARTICLE_CENSUS=<secs>` prints per-emitter live counts once —
// the trace-comparable coverage number (see `capture::ParticleCensusPlugin`).
if std::env::var("WOW_PARTICLE_CENSUS").is_ok() {
app.add_plugins(crate::capture::ParticleCensusPlugin);
}
// The entity census: `WOW_ENTITY_CENSUS=<secs>` prints per-archetype entity counts once —
// what the resident entity count is made of (see `capture::EntityCensusPlugin`).
if std::env::var("WOW_ENTITY_CENSUS").is_ok() {
app.add_plugins(crate::capture::EntityCensusPlugin);
}
// The melee live probe: `WOW_PROBE=melee` auto-fights the nearest enemy so the dbg-trace
// sink can record the combat-text timeline (see `capture::ProbeMeleePlugin`).
if std::env::var("WOW_PROBE").as_deref() == Ok("melee") {
app.add_plugins(crate::capture::ProbeMeleePlugin);
}
// The partner live probe: `WOW_PROBE=partner` auto-accepts group invites — the party arc's
// second-client instrument (decision 0434; see `capture::ProbePartnerPlugin`).
if std::env::var("WOW_PROBE").as_deref() == Ok("partner") {
app.add_plugins(crate::capture::ProbePartnerPlugin);
}
// The sea-crossing live probe: `WOW_PROBE=crossing` boards a cross-continent boat and reports
// the map seam surviving — decision 0455's instrument (see `capture::ProbeCrossingPlugin`).
if std::env::var("WOW_PROBE").as_deref() == Ok("crossing") {
app.add_plugins(crate::capture::ProbeCrossingPlugin);
}
// The taxi-flight live probe: `WOW_PROBE=taxi` opens the flight-master menu on the real wire
// and rides Stormwind → Sentinel Hill to a measured verdict — decision 0484's end-to-end
// instrument (see `capture::ProbeTaxiPlugin`).
if std::env::var("WOW_PROBE").as_deref() == Ok("taxi") {
app.add_plugins(crate::capture::ProbeTaxiPlugin);
}
// The mail-arc live probe: `WOW_PROBE_MAIL=1` GM-mails the probe's own character, opens the
// Goldshire mailbox on the real wire, and drives the inbox/take/send/delete surface through
// the live Lua VM — decisions 0544/0548's end-to-end instrument (see `capture::ProbeMailPlugin`).
if std::env::var("WOW_PROBE_MAIL").is_ok() {
app.add_plugins(crate::capture::ProbeMailPlugin);
}
// The bank-arc live probe: `WOW_PROBE_BANK=1` GM-hops to a pure banker, drives the whole
// six-opcode bank wire (activate/deposit/withdraw/buy-slot/refusal) — decision 0604's
// end-to-end instrument (see `capture::ProbeBankPlugin`).
if std::env::var("WOW_PROBE_BANK").is_ok() {
app.add_plugins(crate::capture::ProbeBankPlugin);
}
// The cast-cancel live probe: `WOW_PROBE=castcancel` hearths and presses W mid-cast — the
// local self-cancel's end-to-end timing instrument (see `capture::ProbeCastCancelPlugin`).
if std::env::var("WOW_PROBE").as_deref() == Ok("castcancel") {
app.add_plugins(crate::capture::ProbeCastCancelPlugin);
}
// The char-create live probe: `WOW_PROBE_CHARCREATE="<name>[,race,class,gender,…]"` creates (and
// cleans up) a character at select to verify the char-create/delete wire (decision 0423 phase 1;
// see `capture::ProbeCharCreatePlugin`).
if std::env::var("WOW_PROBE_CHARCREATE").is_ok() {
app.add_plugins(crate::capture::ProbeCharCreatePlugin);
}
// The live FPS probe: `WOW_LIVE_FPS=<frames>` samples frame times on a NORMAL connected run
// and exits — the harness probe's numbers with the live world in (see `capture::LiveFpsPlugin`).
if std::env::var("WOW_LIVE_FPS").is_ok() {
app.add_plugins(crate::capture::LiveFpsPlugin);
}
// The two scripted probe drivers that lived in `player/` until decision 1174 — the mouse-turn
// (`WOW_PROBE_LOOK`, decision 0621) and the camera park (`WOW_PROBE_CAM`, decision 0653). Added
// unconditionally because each plugin's own `from_env` is its gate, so the variable's name is
// spelled in exactly one place; both order themselves before `player::PlayerControlSet`.
app.add_plugins(crate::capture::ProbeLookPlugin);
app.add_plugins(crate::capture::ProbeCamPlugin);
// The FPS journal: `WOW_FPS_JOURNAL=<csv>` appends per-second position + frame-time rows on a
// director-driven run — "where does it dip" as coordinates (see `perf::FpsJournalPlugin`).
if std::env::var("WOW_FPS_JOURNAL").is_ok() {
app.add_plugins(crate::perf::FpsJournalPlugin);
}
}
}
}