Files
benilla-octo/crates/benilla-app/tests/world_api_wall.rs
T
SamandClaude Fable 5.1 c3555b4eea the cooldown goes stock, early-Z comes back, and lists reach the end
- a size-less `<Model>` pane's rect is its file's box in layout units: the reference's geometry overrides (`0x76d080`/`0x76d0d0`) answer the bbox extent whenever no size is authored, `bboxExtent · 768·√(a²+1) · layoutScale` FrameXML units (`1280 · extent` at 4:3), written into the pane's layout input when the file's facts land, re-derived on an aspect change, and ended for good by any authored size; `SetPosition` is the same unit, which is how `0x4a7b20` centres the arrow at `½·GetWidth, ½·GetHeight` through the override — so the map arrow's sprite arm and its hand-sized 33.6 px footprint go (its rect is `0.0262 × 0.0263` layout units, 33.5 × 33.7 FrameXML units at 4:3, 41.0 × 41.2 at 16:9), and the stock `ItemAnim` pane plays the item push unadapted (`ReplaceIconTexture` on the type-14 batch, the 1000 ms clamp, the stock `OnAnimFinished`), `ContainerFrameAdapters.xml` losing 0887's Lua card and its hand-measured 42.41 × 124.72 rect — the implicit one at 16:9 — and keeping the keyring fork and the three bag verbs (2015); and the cooldown is the reference's own model: the texture-transform law `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`, the raw quaternion component-lerped and never normalised, is one function (`uv_transform`) with the shader's UV fold as its twin — rotation and scale tracks bake per file sequence slot beside the translation, the mat-anim table gains an affine row `[cos − 1, sin, sx − 1, sy − 1]` on `anim_slots.z` whose identity is the zero row so every static batch reads exactly what it did, and a UI tile clones the twin material per animated batch and writes both rows every frame off the pane's play head, never the world clock; the stock `Cooldown.xml` runs off the chain on 2007's two handlers, the file's four quadrant quads turned by their own rotation tracks (`0 → −90°` over each quadrant's 250 ms window, clockwise from 12 o'clock) and its alpha tracks revealing the star for the flash; retired: the native `Cooldown` widget and `SetCooldown` (an Era API 1.12 never had — `CreateFrame("Cooldown")` is the reference's unknown-type refusal now), the loader's `Model → Cooldown` substitution, the app's hand-built pie and star, and our `Cooldown.xml`; `assets/ui` is eight files (2019)
- `UpdateMapHighlight` gets its zone-level exits (B360): at zone zoom ours returned the nil tail for every cursor position, so hovering Deadwind Pass from Duskwood showed no label where 1.12 shows "Deadwind Pass"; the reference's `0x4a7fa0` runs two lookups at zone level and answers a NAME only — first the current zone's exploration-gated overlay list (the builder at `0x4a6ad9` gating on the explored byte), each row's HitRect scaled by 1/1002 and 1/668 against the normalized cursor, edges inclusive, the first hit naming its Area1, then the continent grid re-windowed through the displayed zone's own rect, naming the neighbouring zone or city and nothing for the displayed zone itself — and only the name-only tail is reachable from there, fileName nil and the six coords zero, so the stock frame hides the highlight quad, as the director's own 1.12 shot shows; and `SetVerticalScroll` stores the offset as given (B370, B363): the reference's `0x786db0` compares the new offset against the old and writes it to `[+0x328]` verbatim, `0x787100` re-anchors the scroll child off it, and the range at `[+0x320]` is never read on that path — every clamp the reference exhibits is FrameXML's, through the scroll bar's `[min, max]`; ours clamped into `[0, GetVerticalScrollRange()]`, the child's overflow past the FRAME, which is 40 px short of where `FauxScrollFrame_Update` sends the bar on the 220-tall skills list (three rows lost) and 15 px short on the 287-tall who list (one); the setter fires `OnVerticalScroll` only on a change, as the bytes do, `BenillaScroll_Step` bounds its wheel in Lua where the reference's handler does, and the ScrollFrame kind's prose stops calling its offset unpinned (2017)
- overdraw's cheap half, the sky first: since 0588 every sky element wrote `frag_depth = 0.0`, which costs a pipeline its early-Z, so on an outdoor frame every full-screen dome fragment under the ground and the buildings was shaded in full (the gradient's `asin`, `atan2` and eight table lookups at dawn and dusk) and thrown away; one shared `sky_vertex.wgsl` — bevy's mesh vertex verbatim plus `out.position.z = 0.0` — is now the vertex stage of the gradient dome, the cloud dome, the star patches and the celestial discs and glares, and the model lane's `WOW_SKY_DEPTH` branch moves from its fragment to its vertex, so no sky fragment declares `frag_depth` and the rasterizer rejects the covered screen before the shader runs; every sky `specialize` zeroes the rasterizer bias constant the `sky_order` rung had been leaking into `DepthBiasState::constant`, harmless only while the fragment overwrote its depth, and the prose beside the pin follows it; then the retained `static_gx` pass draws BEFORE bevy's opaque pass (`StartMainPass → static_gx → MainOpaquePass`) — buildings and doodads are the frame's best occluders and a terrain fragment its dearest, six samples a pixel — so walls reject the terrain behind them instead of the reverse, the pass taking the first-use clear with it and the dev depth probe ordered after it; measured on 16 capture scenarios: 14 bit-identical, `northshire-dusk` 1 px and `water-night` 11 px, attributed to exact depth ties on static/terrain silhouettes and last-bit interpolation noise; a depth prepass and near-to-far bins are declined until a journal says where the time goes (2016)
- the grass cut goes per pixel because of a quarter-mip: `0x6813d0` sets `D3DSAMP_MIPMAPLODBIAS = +0.25` on stage 0 (`0x6813f4`) once per frame before the detail-doodad pass — capture-confirmed on all 29 doodad batches, and outside the `[0x6b1000, 0x6b3200)` scan 2004 had called complete; it decides the whole look because the `clutter_cut` probe finds 6 of 31 shipped detail atlases carry a BINARY alpha pyramid (mip 0 has DXT3's 16 levels, every level below exactly two, both starting zones among them), so `a × ramp` degenerates to a global on/off and every texel of a leaf crosses at the same depth — Elwynn's survivor coverage is flat at 47.6 % across the whole 52.5–61.1 yd band, then 0.0 %, while the probe's box-filtered control erodes 47.7 → 20.9 over the same band, so the art has the gradient the shipped pyramid threw away, and `clutter_size` puts the median tuft at 0.92 yd tall, 1.03 yd deep, a full yard for the boundary plane to sweep; the escape is bilinear: full alpha needs a 2×2 all-opaque texel neighbourhood, and those run out between mip 2 and 3 (179/56/10/0/0/0 on the dominant Elwynn cell), so the reference's λ ≈ 2.9–3.3 sits past the cliff and the tuft erodes continuously — 2.2 % of a tuft's pixels leave in the last half-yard with the bias, 11.6 % without, 24.2 % one mip sharper; wgpu has no sampler LOD bias, so the clutter lane re-samples base colour at `view.mip_bias + 0.25`, keeping the sampler every other model batch shares at the reference's 0; ruled out on the way: our BLP decoder (all nine uploaded levels byte-identical against the archive's 52 793 BLP2s), mip regeneration, the `has_mipmaps` byte's value 2 (a plain boolean, an authoring marker), anisotropy and geometry; what it does not fix is resolution-dependent, and so is the reference's own look — λ with the bias is 3.22 at the capture's 648p but 2.48 at 1080p and 1.48 at 2160p, so at a modern resolution the reference itself pops leaf by leaf, and scaling the bias with the viewport is one reversible line left for a look call (2021)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:35:31 +07:00

864 lines
47 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.
//! **The world API wall** — the doorway of `benilla-world`, counted from source on every
//! `cargo test`, so it can never be measured once and then quietly widen.
//!
//! Decision 1160 splits the world renderer out of `benilla-app` into `benilla-world`, and its
//! *secondary* falsifier is a number: if the engine's public vocabulary is much larger than a
//! designed API, the cut line is wrong. 1163 measured it — **214 distinct engine items named by
//! gameplay code** — and set the gate at **forty**, with the sort into DOWN / PUBLISH / CLOSE as
//! the work. 1164 is that sort.
//!
//! A number in a decision record is a number nobody re-measures. This is the same measurement as
//! a test, so every commit that closes a leak shows up as [`CEILING`] going down, and every commit
//! that opens a new one fails the gate the day it lands rather than three weeks later.
//!
//! **It has two lives, and it switches by itself.** Before the move, the boundary is a naming
//! convention: gameplay files reaching `crate::<engine module>::…`. After the move it is a real
//! crate, and the same count is `benilla_world::…` in `benilla-app`'s sources. The scan below
//! looks for `crates/benilla-world` and measures whichever world exists — so the ratchet survives
//! stage two instead of being deleted by it.
//!
//! What it deliberately does **not** count: `#[cfg(test)]` bodies (a test naming an internal is
//! not an API consumer), doc comments (a `[`crate::x::Y`]` link is prose), and the composition
//! root's plugin registrations are counted like anything else — 1164's plugin-group collapse has
//! to actually happen for them to stop counting.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
/// The engine's module set inside `benilla-app` — the cut line of 1160, written down.
///
/// This list is the pre-move definition of "the world". It is load-bearing in exactly one way: an
/// engine module missing from it makes its leaks invisible to this test. It goes away at stage
/// two, when `crates/benilla-world/` becomes the definition and the scan switches to the crate
/// path (see the module docs).
const ENGINE_ROOTS: &[&str] = &[
"art_scope",
"assets",
"billboard",
"clouds",
"clutter",
"collision",
"debug_panel",
"decal",
"dev_state",
"doodad_anim",
"entity_shade",
"exterior_cull",
"ffx_glow",
"instance_tint",
"interact",
"interior",
"lighting",
"liquid",
"map_proj",
"mesh_tag",
"model_fade",
"model_forms",
"model_render",
"modkeys",
"particles",
"perf",
"pipe_warm",
"ribbons",
"rig_anim",
"rig_palette",
"schedule",
"sky",
"sky_order",
"skybox",
"sun",
"surface",
"terrain",
"terrain_stream",
"view",
"water_fx",
"wdl",
"weather",
"wmo_portal",
"world_census",
"world_plugins",
"world_point",
"world_unit",
"world_map",
"zfill",
];
/// The instruments — `debug_panel`, `perf`, `pipe_warm`, `art_scope`. They live inside the engine
/// module set today (the world viewer boots them), but 1160 puts instruments at the *top* of the
/// stack, not the bottom: extracting them early would publish hundreds of internals as permanent
/// API shaped by what a debugger wanted to poke. So they are not part of the doorway being
/// measured, and gameplay reaching into them is a different problem with a different record.
const INSTRUMENT_ROOTS: &[&str] = &["art_scope", "debug_panel", "perf", "pipe_warm"];
/// The instruments that ended up **above** the engine — `benilla-app` modules that consume
/// `benilla-world` like any other caller.
///
/// 1164's rule is that an instrument's surface is not the engine's designed API: "1160 puts
/// instruments at the top of the stack, and their surface is a different record's problem".
/// 1167 corrected the *geography* — the compiler proved these three read the game and cannot live
/// inside the engine — but the rule it was attached to still holds, and for the reason 1163 gave:
/// an API whose target is set by what a debugger wanted to poke is the same failure as an API
/// shaped by whichever `use` statement got written first.
///
/// So they are excluded from the gated number and **counted separately**, because a rule that
/// hides a number is worse than no rule. `art_scope` is not here: it is registered by
/// `WorldPlugins` and lives inside the engine, so it never crosses.
const INSTRUMENT_CONSUMERS: &[&str] = &["debug_panel", "perf", "pipe_warm"];
/// Is this file one of the app-side instruments?
fn is_instrument_consumer(rel: &str) -> bool {
let root = rel.split(['/', '.']).next().unwrap_or("");
INSTRUMENT_CONSUMERS.contains(&root)
}
/// The gate. 1163 sets it at forty; this is the current standing count, ratcheted down by each
/// commit that lands a DOWN or a CLOSE from 1164's sort.
///
/// **Lower it when you close something.** The lower bound below exists so that a session which
/// closes twenty leaks cannot leave the ceiling standing at the old number, which would silently
/// hand the next twenty leaks a free pass.
///
/// It has been raised **twelve times**, and every reason is worth keeping. 138 → 139: `ground_fx`
/// moved into the engine, where 1164 says it belongs ("326 lines of pure render"), and the game
/// now names one entry point (`spawn_ground_fx_decal`) where before it named none — it was calling
/// a module in its own crate. A structural correction that costs one item and buys the lane's
/// ordering law back from its caller; `ModelEffects` absorbs the entry point when it is built.
///
/// **150 → 176: the crate
/// exists, and the count switched to the thing it was always a proxy for.** Two reasons it went up,
/// both of them the measurement getting more honest rather than the doorway widening:
///
/// - The module-name scan excluded the *instruments* (`debug_panel`, `perf`, `pipe_warm`) as
/// namers, on the assumption they travel with the engine. The compiler refuted that the moment
/// the move was attempted: all three read the game, so they stayed in `benilla-app` — and they
/// consume the engine API like any other consumer, which they always did.
/// - `crate::x::Y` had to be parsed out of a naming convention. `benilla_world::x::Y` is a real
/// path the compiler agrees with, so nothing hides behind a form the parser did not anticipate.
///
/// The number to work down is this one. It is also the last time it can be argued with: from here
/// every item on it is a `pub` in `benilla-world`, and closing one is a demotion the compiler
/// checks.
///
/// 149 → 150: `SPAWN_XY`, the
/// streamer's last-ditch focus, moved out of `lib.rs` and into `terrain_stream` where it is used —
/// an engine that cannot answer "where do I stream from" with no game attached is not an engine.
/// The two gameplay readers (the player's boot pose, the world viewer's start) name it across the
/// line now; it bought the one reverse crossing that neither wall could see.
///
/// 155 → 156:
/// `model_fade::ModelFade` — 1164's alpha inversion, in one component. The game declares how
/// translucent a root should be; the engine owns the write, the chain composition and the material
/// swap. One published name for two impossible dependencies, and it retires a gameplay writer of a
/// channel the reference gives one owner.
///
/// 153 → 155:
/// `world_unit::{WorldUnit, ViewerUnit}` — a body in the world and the viewer's own, replacing
/// five engine lanes' filters on the game's wire record. Two published markers for three
/// impossible dependencies (`net::NetEntity`, `net::SelfPlayer`, `entities::CollisionHeight`).
///
/// 152 → 153: `view::Viewer`
/// — the avatar as a *body* (where, how fast, which way, how tall) rather than as the game's
/// `Player` type. Three engine lanes were reading it behind the identical predicate. One published
/// resource for two impossible dependencies.
///
/// 148 → 152: `creature_anim`
/// split along 1163's line — the rig machinery (pose evaluation, world/palette composition, the
/// global-sequence channels) became `rig_anim` and the game kept the policy. The four are the real
/// "pose a rig" vocabulary (`RigPose`, `RigFrame`/`RigAnchor`, `PosePost`, `GlobalSeqDrive`), and
/// they belong beside `RigPalettes` and `RigSkin` in 1164's *place* face — a second program that
/// spawns a skinned model has to pose it. They bought three impossible dependencies.
///
/// 147 → 148: 1160's wire
/// (b). The engine stopped reading the game's four-variant session enum to learn whether a world
/// exists and started reading its own one-bit `schedule::WorldLive`, which the session writes.
///
/// 145 → 147: 1160's wire
/// (a) inverted. The streamer stopped reading `player::Player` and started reading
/// `terrain_stream::ViewFocus`, which the game writes; and `WorldLoadProgress` — the streamer's
/// own residency fact, which had been living in `loading_screen` — came home, so its two readers
/// (the bar, and the mover's post-snap hold) now name it across the line. Two published inputs
/// bought four reverse crossings, and the enforcer stopped needing a stub avatar to boot.
///
/// 143 → 145: two capture
/// fixtures that had been living inside engine modules (`water_fx::view`, the foam viewer, and
/// `particles::census`, the draw-address probe) moved out to `capture`, where they belong — and
/// an instrument on the game side naming engine internals is a forward crossing where an
/// instrument *inside* the engine was not counted at all. Same shape as the 160 → 162 raise
/// below: the doorway did not widen, the measurement got honest. It closed three reverse
/// crossings, including an engine plugin that had to name `capture` to register a fixture.
///
/// 142 → 143: the depth-bias
/// ladder came together in `sky_order` as one `Rung` type. That is a genuinely new forward
/// crossing — gameplay lanes now name an engine rung where before each kept its own constant —
/// and it bought three REVERSE crossings, the ones that cannot exist at all once the crate does.
/// One published rung against three impossible dependencies is the trade, and the ladder is
/// engine API by 1164's own spine (a second program orders against the engine's frame).
///
/// 133 → 154, and this is the one to read first: it is not a leak, it is the wall admitting it
/// had been lying. `expand` stopped walking at a brace, so `use benilla_world::interact::{A, B}`
/// — the ordinary Rust import — collapsed to the bare module path and was then discarded as a
/// prefix. Only items written out fully qualified at a *usage* site were ever counted. Every
/// number this test has printed since the crate landed was an undercount of the same kind, and
/// the honest surface is 154. Found by disagreement: a second scanner put the total one item
/// lower while naming items this one had never heard of, and neither error was visible from
/// either side alone. `WOW_API_DUMP=1` exists because of this.
///
/// And 160 → 162, the first one: moving
/// `StreamActivity` out of `perf` and `DebugState` out of `debug_panel` did not widen the doorway,
/// it stopped two engine facts hiding inside instrument modules this test deliberately does not
/// count. The items were always crossing; the measurement improved. That is the only kind of raise
/// that is not a retreat — a raise for a NEW leak is the failure this number exists to catch.
///
/// And 158 → 159: `vis_chain::VisChainOnly`, a PUBLISH by 1164's test (decision 1441). The
/// chain-only visibility idiom — keep `Visibility`+`InheritedVisibility` (hide-propagation),
/// remove `ViewVisibility` (the per-camera sweep row) — is an ENGINE law about bevy's visibility
/// pipeline, but half the never-rendering hierarchy nodes it applies to are spawned game-side
/// (net-object roots, held-item and spell-fx wrappers, transports). Keeping it engine-private
/// would mean every game spawner hand-rolling `.remove::<ViewVisibility>()` with its own copy of
/// the why — the exact drift a named idiom exists to prevent; the trait is the smallest honest
/// carrier of the law.
///
/// And 157 → 158: `collision::ColliderEpoch`, a PUBLISH by 1164's test (decision 1384). It is the
/// stamp on the world's collider set — "the geometry you last asked has changed" — and the whole
/// point of it is that a *cached* collision answer must not outlive the world it described. The
/// engine owns the fact (the streamer's attach queue is what changes the set), the game owns two of
/// the three parties: the creature ground clamp reads it, and the GameObject hull lane
/// (`entities::attach`) is the one collider insert outside the streamer's queue, so it must be able
/// to stamp. Keeping it engine-private would mean either a game-side collider that no cache can see
/// arrive — which is B197's bug with a different collider class — or an engine system reaching into
/// game components to invalidate them, which crosses this line the other way and worse.
///
/// And 156 → 157: `mat_anim_table::MatAnimTable`, a PUBLISH by 1164's test (decision 1381). The
/// mat-anim delta table replaced per-frame material mutation, and registration must happen where
/// materials are BUILT — which for WMO GameObject props (transport interiors) is a game-side
/// spawner (`entities::wmo_props`) that already threads the two registries this table serves
/// (`UvAnimMaterials`/`TintAnimMaterials`, published members of the same family). The resource
/// that allocates the slots is the smallest honest addition beside them; hiding it would mean
/// a game-side material registering without a slot and silently freezing at its seed.
///
/// And 155 → 156: `doodad_anim::DoodadAnimHost`, a PUBLISH by 1164's test (decision 1365). The
/// doodad joint collapse put placed doodads on the collapsed-rig lane (`RigPose`), which made
/// them visible to the game's animation-LOD gate — whose park marker the engine's own doodad
/// draw gate already owns, on a different law (the composed draw verdict + fade sphere, not the
/// unit frustum test). Two writers to one marker silently un-park hidden hosts, so the game's
/// gate must be able to say "this population is not mine" — and the engine component that IS
/// that population's name is the smallest honest way to say it. The instruments already named
/// it; this makes the one gameplay filter explicit rather than inventing a second marker to
/// carry the same fact.
///
/// And 154 → 155: `modkeys::SyntheticHold`, a PUBLISH by 1164's test rather than a leak. The
/// engine owns the macOS stuck-modifier reconciler, and that reconciler decides by polling the
/// **hardware** flag state — which by construction reads "up" for a key no hand is on. So a
/// synthesized press (the probe harness's `WOW_PROBE_KEY`) was released the frame after it was
/// made, and logged as a stuck-key correction: every chord binding was silently unreachable
/// headlessly on the platform we develop on. "Something is deliberately holding this key" has
/// exactly one reader — the reconciler, engine-side — and its writers are whoever synthesizes
/// input, which is game-side; a one-field resource is the smallest honest expression of that. The
/// alternative was ordering a game system against an engine system, which crosses this same line
/// *and* publishes a schedule point to do it.
/// And 159 → 160: `WorldLoadProgress::gx_pending`, a PUBLISH beside the two residency terms
/// already through this door (`colliders_pending`, `merge_pending`). The retained pass (1429)
/// moved the static world off the entity path, so a placement that has SPAWNED still draws
/// nothing until its region bakes — and the game reads residency for two decisions the engine
/// does not own: when the loading cover may lift, and when the post-snap physics hold may
/// release (decision 0737's split). Both were answering "is the world there" with a fact that
/// had stopped meaning it (decision 1498). The alternative was the game reaching into
/// `StaticGx` itself, which is a whole engine subsystem through the door instead of one
/// `usize` on the residency struct that exists to be read from outside.
/// And 160 → 161: `mac_quit::MacQuitPlugin`, a PUBLISH in the exact class of the two engine
/// plugins already through this door — `thread_qos::ThreadQosPlugin` and `bgwin::BgWinPlugin`.
/// All three correct a *host platform default* at launch, both composition roots name all three,
/// and they sit outside `WorldPlugins` because they are properties of the process and its window
/// rather than of the world. This one re-points macOS's `Cmd+Q` (winit's default menu wires it
/// straight to `terminate:`, which exits without ever running another frame) at the window close,
/// so the client's shutdown tail gets the frame it needs — decision 1528, where that gesture was
/// measured on the real client writing *nothing*: no saved variables, no addon files, no camera
/// pose. The alternative was hiding it inside `WorldPlugins` to keep the count flat, which is
/// gaming the instrument rather than paying for a name.
/// And 161 → 162: `particles::ViewThrottled`, a PUBLISH of the same shape as
/// `rig_anim::AnimParked` already through this door — a marker the ENGINE reads and the GAME
/// writes, which is the 1160 line drawn exactly where 1163 drew it. Freezing an emitter with the
/// view that stopped drawing it is machinery (the draw-set law lives in `particles::sim`);
/// deciding that a particular booth camera is being *paced* rather than put to sleep is policy,
/// and it is `portrait::gate_booth_cameras`'s, in the game. There is no engine-side way to tell
/// the two apart — a camera's `is_active` bit says only that it did not draw — which is precisely
/// the conflation that ran the body panes' item effects at half speed (decision 1559, B312). The
/// alternative was passing a `Res<PaneRate>`-shaped opinion down into the engine's own sim, which
/// puts the game's knob inside the renderer to keep a count flat.
/// And 162 → 163: `rig_rider::RigRider`, a PUBLISH of the same shape as `rig_anim::AnimParked`
/// and `particles::ViewThrottled` above — a component the ENGINE reads and the GAME writes.
/// Placing an attached model's vertices in its wearer's rig frame rather than in absolute f32
/// world space is machinery (the whole lane is `rig_rider` + the palette's row writer, and the
/// reason is arithmetic: at Elwynn's ~9481 yards the absolute sum lands on an 0.98 mm grid and is
/// rebuilt every frame). *Which* bone a helm hangs from, and what a sheath swap does to that, is
/// policy, and it is `entities::equipment`'s, in the game — the same M2 attachment table that
/// decides where a weapon is drawn versus stowed. There is no engine-side way to know it: the
/// engine sees a scene-graph child, and a child is not an attachment. The alternative was passing
/// the game's attach-slot table down into the renderer to keep a count flat (decision 1609).
/// And 163 → 164: `instance_tint::InstanceTintMirrors`, the twin of `rig_palette::RigPaletteMirrors`
/// already through this door and registered at exactly the same site — an off-world `wow_light`
/// buffer the engine must also fill with the per-instance tint region, because the shader reads
/// that region out of whichever buffer the draw binds. Uploading a region to the buffers that
/// carry it is machinery. *Which* off-world buffers carry it is policy, and it has to be: a
/// portrait bake must NOT (the reference builds a fresh CM2 with colour `(1,1,1)`, so a ghost's
/// portrait shows the living face — wow-re `ghost-death-visuals.md` §6, report B49, decision 1481)
/// while the glue scene MUST (it is the screen itself, and its character component is the very
/// instance the reference tints). The engine cannot tell those two render targets apart — both are
/// a camera writing to an image — and encoding "a bake standing in for a UI model widget" inside
/// the uploader is exactly the game opinion the wall exists to keep out. The alternative was
/// mirroring the region into every registered buffer to keep the count flat, which re-opens B49 in
/// the tint lane (decision 1731).
/// And 164 → 165: `ffx_glow::GlueFfx`, a PUBLISH of the same shape as `rig_anim::AnimParked` and
/// the two above — a resource the ENGINE reads and the GAME writes, and exactly ONE bit wide.
/// Running an FFX pass pair is machinery; *which* pair a screen installs is the screen's own, and
/// the reference says so by building two pairs (WorldFrame `0x481c46`, CGlueMgr `0x46a723`) and
/// selecting between them at two different sites. The world's selector is already inside the
/// engine's reach — `PLAYER_FLAGS_GHOST` on the live player — but the glue's is not and cannot be:
/// it is the ghost bit of the **selected roster row** (`0x472fd9 test dh,0x20`), a character
/// nobody is standing in, on a screen the engine has no concept of. The alternative was handing the
/// renderer a roster to read to keep a count flat (decision 1731). Note the direction of travel:
/// this replaced `FfxGlow::state_scale`, a float that let a bake inherit a player-state lane by
/// arithmetic — the enum that took its place makes report B49's invariant structural.
/// And 165 → 166: `collision::MoverTraceExclusions`, a PUBLISH in the same class as the two
/// one-bit resources above — a resource the ENGINE reads and the GAME writes. The reference's
/// world trace carries a per-trace mask, and the local mover's gains bit `0x8000` when the body it
/// drives is a player in ghost form; the GameObject collision-candidacy virtual `0x5f85f0` reads
/// it and drops every `GAMEOBJECT_TYPE_ID == 0` object, so a ghost walks through closed doors
/// (decision 1767). The engine cannot compute that set — "a DOOR GameObject, while the player is a
/// ghost" names two gameplay concepts, which is precisely what the wall pointing the other way
/// forbids an engine file from knowing. The alternatives were both worse: dropping or re-laning
/// the door's collider makes it stop existing for the particle snap, the precipitation probe, the
/// mouse pick and the creature conform, none of which the binary touches; and threading a filter
/// through `step`/`grounded_step`/`airborne_step` puts a gameplay argument into the movement
/// core's signature. An `EntityHashSet` that is empty on every living frame is the smallest honest
/// expression, and it makes the living player's query provably the one it always was.
/// And 166 -> 172: **not six crossings — one measurement correction.** Nothing moved through the
/// doorway; the scan was blind to part of every file. A `#[cfg(test)]` was cleared only on
/// `depth < d`, and a top-level test module is seen at depth 0, rises to 1 and returns to 0 —
/// never below it, so the first test module in a file switched the scan off for the rest of that
/// file. [`paths_in`] carries the fix and how it surfaced (2026-09-01, a change that only *moved*
/// a test module out of `net.rs` and pushed the count up by one with no new reference in its
/// diff). These six were named by gameplay code all along, below a test module, uncounted:
/// `lighting::WorldTime`, `art_scope::ArtScope`, `art_scope::ArtSlot`,
/// `billboard::billboard_joint_palette`, `rig_anim::finalize_rig_worlds`, `world_unit::ViewerUnit`.
/// Raising the number is what keeps the ratchet honest — a ceiling measured through a hole is not
/// a ceiling — and it is the ratchet's own instruction: a count that cannot see the whole file
/// cannot report a widening either, which is the one thing this test exists to do. The six are
/// **not** hereby blessed as PUBLISH: they are un-sorted 1164 work, now visible enough to sort.
/// And 172 → 173: `model_render::lazy::realize`, a PUBLISH. A built `WowModelMaterial` is no
/// longer an asset the moment a spawner builds it — the engine parks the value behind a
/// reserved handle and inserts the asset the first frame something visible is bound to it
/// (decision 1940: 9.9k materials, 20.7k buffers and 10.2k bind groups were alive at the
/// Stormwind auction house for 36 drawn entity batches, the variant set every spawner is handed
/// and never switches to). A game lane that CLONES a material before anything binds it — the
/// portrait booth relighting a part's twins onto its own light buffer, a spell kit deriving its
/// per-instance tinted copy from the shared steady — asks the engine to realize it first. That
/// is the whole of the crossing: one function, "make this handle's asset exist now", the
/// doorway's own rule about when a material is real. The alternative — eager materials for
/// the two lanes that read early — would put the store's law in the caller's hands.
/// And 173 → 174: `particles::render::EFFECT_DRAW_STATS`, an instrument PUBLISH — the effect
/// lane's own per-frame draw census (items in the transparent phase, draws after the merge),
/// the number the `FPS_PROBE` line prints as `fx=`. It is the reading that refuted the
/// additive-window regroup on the crowd rig (decision 1955): a lane whose merge walk is the
/// only place the count exists has to publish it or stay unmeasurable.
/// And 174 → 175: `dev_state::STILL_INPUTS_CHANGED`, an instrument PUBLISH — how many frames
/// each whole-scene input of the still-frame skips (1979) read as changed. Counted in the
/// world crate so the probe names ONE static instead of the four resources the skips read; the
/// reading is what says whether a gate ever engages.
/// And 175 → 176: `mat_anim_table::affine_row`, a PUBLISH. The mat-anim table's second row
/// kind (decision 2019): a texture transform's rotation and scale as deltas from the identity,
/// `[cos − 1, sin, sx − 1, sy − 1]`, so that row 0 — the pinned zero every static material
/// reads — IS the identity. That encoding is the table's own law (the same zero-is-identity
/// rule its translation rows and the tint table run under), and the shader's fold is written
/// against it; a lane that owns rows in the table — the UI model tiles, sampling the cooldown's
/// quadrant rotations off the pane's play head — has to write them in the table's encoding,
/// not one of its own. One function, "encode this affine the way the table reads it".
const CEILING: usize = 176;
/// How far under [`CEILING`] the real count may sit before this test asks for the ceiling to be
/// lowered. Slack, not tolerance: it keeps a single closure from failing the gate, while making it
/// impossible to bank a whole stage of work without writing the new number down.
const SLACK: usize = 10;
#[test]
fn the_world_api_doorway_stays_shut() {
let all = measure();
// Partition: an item only an instrument names is the instruments' surcharge, not the designed
// doorway (see `INSTRUMENT_CONSUMERS`). One a game module *also* names is the doorway's.
let (probes, surface): (BTreeMap<_, _>, BTreeMap<_, _>) = all
.into_iter()
.partition(|(_, files)| files.iter().all(|f| is_instrument_consumer(f)));
let n = surface.len();
// Always say the number: `cargo test -p benilla-app --test world_api_wall -- --nocapture` is
// the one-command answer to "where is the wall now", which is asked on every unit of 1164's
// sort and used to need a deliberately-failing ceiling to get.
eprintln!(
"world API surface: {n} items (ceiling {CEILING}, slack {SLACK}) \
+ {} named only by the instruments",
probes.len()
);
// `WOW_API_DUMP=1` prints the surface itself, most-named first. The count alone answers "is
// the wall holding"; it cannot answer "which item did that unit actually retire", and a unit
// that reads as net-zero is exactly when you need the list (a second scanner disagreeing by
// one is how this got added).
if std::env::var("WOW_API_DUMP").is_ok() {
let mut rows: Vec<_> = surface.iter().map(|(k, v)| (v.len(), k)).collect();
rows.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(b.1)));
eprintln!("{}", render(&rows));
let mut probe_rows: Vec<_> = probes.iter().map(|(k, v)| (v.len(), k)).collect();
probe_rows.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(b.1)));
eprintln!(
"--- named only by the instruments ---\n{}",
render(&probe_rows)
);
}
if n > CEILING {
let fresh: Vec<_> = surface.iter().map(|(k, v)| (v.len(), k)).collect();
panic!(
"world API surface is {n} items, ceiling is {CEILING}.\n\
Something new crossed the line. Either close it, or — if it is genuine engine API — \
raise the ceiling in this file WITH the justification, the way decision 1164 requires \
for every item in the PUBLISH bucket.\n\n{}",
render(&fresh)
);
}
assert!(
n + SLACK >= CEILING,
"world API surface is down to {n} items and the ceiling still says {CEILING}. \
Lower CEILING to {n} in this file so the next leak has to earn its place — the ratchet \
only holds if the number follows the work down."
);
}
/// **The wall that points the other way.** 1160 stage one's other half: an engine file naming a
/// gameplay item is a dependency `benilla-world` cannot express, so every one of these has to go
/// before the crate can exist at all. Ratcheted to zero, and then the crate graph keeps it there.
#[test]
fn the_engine_names_nothing_of_the_game() {
let surface = measure_back();
let n = surface.len();
eprintln!("engine→game surface: {n} items (target 0 — CLOSED)");
assert!(
n == 0,
"the engine names {n} gameplay items, and this wall is CLOSED — it reached zero and must \
stay there.\n\
An engine file reaching into the game is the dependency `benilla-world` cannot have: not \
a lint, a compile error waiting for the crate to exist. Invert it (the engine publishes \
the fact, the game reads it), move the caller, or move the thing named.\n\n{}",
render(
&surface
.iter()
.map(|(k, v)| (v.len(), k))
.collect::<Vec<_>>()
)
);
}
/// Every distinct engine item named by non-engine code, and the files that name it.
fn measure() -> BTreeMap<String, BTreeSet<String>> {
let app = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let split = app
.parent()
.is_some_and(|c| c.join("benilla-world").is_dir());
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let src = app.join("src");
let tests = test_module_files(&src);
for file in rs_files(&src) {
let rel = file
.strip_prefix(&src)
.unwrap()
.to_string_lossy()
.to_string();
// Pre-move, the caller's own module decides which side of the line it is on; post-move,
// every file in this crate is on the game side by construction.
if (!split && is_engine(&rel)) || tests.contains(&rel) {
continue;
}
let text = std::fs::read_to_string(&file).unwrap();
for (path, _) in paths_in(&text, split, true) {
out.entry(path).or_default().insert(rel.clone());
}
}
drop_module_paths(out)
}
/// Drop every entry that is only the **path to** another entry.
///
/// `use benilla_world::particles::buffer;` exists so the file can write `buffer::EffectQuads`
/// below, and that second form is the one worth counting. There is no syntactic way to tell a
/// module path from a free function — `particles::buffer` and `mesh_tag::spawn_tag` look
/// identical — but there is a structural one: a module path is a **strict prefix of something
/// else that was named**, and a function is a prefix of nothing.
///
/// This generalises a special case that only caught single-segment modules and so missed
/// `particles::buffer` by one level. A rule that needs a list of exceptions is usually the wrong
/// rule; this one needs none.
fn drop_module_paths(
all: BTreeMap<String, BTreeSet<String>>,
) -> BTreeMap<String, BTreeSet<String>> {
let keys: Vec<String> = all.keys().cloned().collect();
all.into_iter()
.filter(|(k, _)| {
let prefix = format!("{k}::");
!keys.iter().any(|other| other.starts_with(&prefix))
})
.collect()
}
/// Every **gameplay** item named by an ENGINE file — the wall that points the other way.
///
/// Empty by construction once the crate exists: `benilla-world` does not depend on `benilla-app`,
/// so a name that crosses this way is not a lint, it is a compile error. Before the move it is
/// only a convention, and this is what holds it.
fn measure_back() -> BTreeMap<String, BTreeSet<String>> {
let app = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
if app
.parent()
.is_some_and(|c| c.join("benilla-world").is_dir())
{
return BTreeMap::new(); // the crate graph is the enforcement now
}
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let src = app.join("src");
let tests = test_module_files(&src);
for file in rs_files(&src) {
let rel = file
.strip_prefix(&src)
.unwrap()
.to_string_lossy()
.to_string();
if tests.contains(&rel) {
continue;
}
// Instruments are allowed to see both sides — 1160 puts them at the top of the stack, so
// they are not part of the engine being extracted and their reads are a later record's.
let root = rel.split(['/', '.']).next().unwrap_or("");
if !is_engine(&rel) || INSTRUMENT_ROOTS.contains(&root) {
continue;
}
let text = std::fs::read_to_string(&file).unwrap();
for (path, _) in paths_in(&text, false, false) {
out.entry(path).or_default().insert(rel.clone());
}
}
out
}
/// Every source file that is a **test module in its own file** — declared `#[cfg(test)] mod x;`
/// somewhere, with its body in `x.rs` or `x/mod.rs` beside its parent.
///
/// 1164's counting rule says `#[cfg(test)]` bodies do not count: a test naming an internal is not
/// an API consumer. The scan honours that for an inline `#[cfg(test)] mod tests { … }` by tracking
/// brace depth, and used to miss it entirely when the same module lives in its own file — so a
/// test file inflated the wall with items no shipping code names. One item was hiding behind this
/// (`mesh_tag::alpha_of`, read by `aura_visual/tests.rs` to assert a packed alpha round-trips);
/// the rule, not the count, is why it is worth fixing.
fn test_module_files(src: &Path) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for file in rs_files(src) {
let text = std::fs::read_to_string(&file).unwrap();
// A module file's children live in a directory named after it — `a.rs`'s `mod b;` is
// `a/b.rs`; `a/mod.rs`'s is `a/b.rs` too.
let dir = match file.file_name().and_then(|n| n.to_str()) {
Some("mod.rs") | Some("lib.rs") | Some("main.rs") => {
file.parent().unwrap().to_path_buf()
}
_ => file.with_extension(""),
};
let mut armed = false;
for line in text.lines() {
let t = line.trim();
if t == "#[cfg(test)]" {
armed = true;
continue;
}
if armed {
if let Some(name) = t
.strip_suffix(';')
.and_then(|d| d.rsplit_once("mod "))
.map(|(_, n)| n)
{
for cand in [
dir.join(format!("{name}.rs")),
dir.join(name).join("mod.rs"),
] {
if cand.is_file() {
out.insert(
cand.strip_prefix(src)
.unwrap()
.to_string_lossy()
.to_string(),
);
}
}
}
armed = false;
}
}
}
out
}
fn is_engine(rel: &str) -> bool {
let root = rel.split(['/', '.']).next().unwrap_or("");
ENGINE_ROOTS.contains(&root)
}
/// Walk a source file and yield `(canonical item, line)` for every engine path it names in code.
///
/// Hand-rolled rather than a regex dependency, and deliberately literal: it follows `crate::root::`
/// (or `benilla_world::` once the crate exists), expands a `use` brace group so
/// `use crate::assets::{AssetSet, LockRecover}` counts as two items, and stops the path at its
/// first capitalised segment so `crate::assets::WorldAssets::get` is one item and not two.
fn paths_in(text: &str, split: bool, want_engine: bool) -> Vec<(String, usize)> {
let prefix = if split { "benilla_world::" } else { "crate::" };
let mut found = Vec::new();
let mut depth: i32 = 0;
// **A `#[cfg(test)]` guards exactly the item that follows it, and the scan has to find that
// item's END.** It did not, and the miss was total: the guard was cleared only on `depth < d`,
// but a top-level `#[cfg(test)] mod tests { … }` is seen at depth 0, rises to 1 inside and
// returns to 0 — never *below* it. So the first test module in a file switched this scan off
// for the whole rest of that file, and every engine item named below it went uncounted. The
// wall's whole job is to be a number nobody has to re-measure, and it was measuring a prefix.
//
// Found on 2026-09-01 by a change that only *moved* a test module out of `net.rs`: the count
// rose by one with no new reference anywhere in the diff, because deleting the module handed
// the scan back the rest of the file. An instrument that moves when the code does not is
// reporting on itself.
//
// The extent, for both shapes that exist here: `test_at` arms on the attribute line at depth
// `d`; if the guarded item opens a brace (`mod tests {`, `impl X {`) the depth rises and the
// item ends when it comes back to `d`; if it opens none (`#[cfg(test)] mod tests;`,
// `#[cfg(test)] pub(crate) use …;`) it ends at that statement's semicolon.
let mut test_at: Option<i32> = None;
let mut test_open = false;
for (n, line) in text.lines().enumerate() {
let t = line.trim_start();
if t.starts_with("#[cfg(test)]") {
test_at = Some(depth);
test_open = false;
}
let skip = test_at.is_some() || t.starts_with("//") || t.starts_with('*');
if !skip {
let mut rest = line;
while let Some(i) = rest.find(prefix) {
// A bare `xcrate::` is not our prefix; require a non-ident char before it.
let boundary = i == 0 || !is_ident(rest.as_bytes()[i - 1] as char);
let tail = &rest[i + prefix.len()..];
if boundary {
// A **crate-root** item — `crate::SPAWN_XY`, no module segment — is neither
// side's by module, and both walls were structurally blind to it: the parse
// below wants a lowercase module root, so an uppercase first segment fell out
// entirely. It belongs to the *game* by construction, because `lib.rs` is what
// stays behind in `benilla-app` when the engine moves — so an engine file
// naming one is a reverse crossing, and it took a hand-rolled grep over the
// engine file set to find the one that was hiding (`SPAWN_XY`).
let root_item = !split
&& ident(tail).is_some_and(|(r, a)| {
r.starts_with(char::is_uppercase) && !a.starts_with("::")
});
if root_item && !want_engine {
if let Some((r, _)) = ident(tail) {
found.push((format!("crate::{r}"), n + 1));
}
rest = &rest[i + prefix.len()..];
continue;
}
let (root, after) = if split {
(String::new(), tail)
} else {
match ident(tail) {
Some((r, a))
if a.starts_with("::") && !r.starts_with(char::is_uppercase) =>
{
(r, &a[2..])
}
_ => (String::new(), ""),
}
};
// Three sides, not two. An INSTRUMENT is neither: it is excluded from the
// doorway being measured (1160 puts instruments at the top of the stack), and
// an engine file naming one is not a reverse dependency either — every
// instrument module is registered by `WorldPlugins`, so it travels *with* the
// engine into the crate.
let instrument = !split && INSTRUMENT_ROOTS.contains(&root.as_str());
let engine = split || (!instrument && ENGINE_ROOTS.contains(&root.as_str()));
let keep = if want_engine {
engine
} else {
!engine && !instrument
};
if keep && !after.is_empty() {
for tail in expand(after) {
let full = if root.is_empty() {
tail
} else {
format!("{root}::{tail}")
};
let item = canon(&full);
// A bare module name is not an item — `use benilla_world::interact;`
// exists so the file can write `interact::WorldObject` below. Most of
// these are caught structurally by `drop_module_paths`, but not the
// ones whose member is only named where this scan does not look (a
// `#[cfg(test)]` block, a macro body), so the parse drops them too.
let bare_module =
!item.contains("::") && !item.starts_with(char::is_uppercase);
if !bare_module {
found.push((item, n + 1));
}
}
}
}
rest = &rest[i + prefix.len()..];
}
}
depth += line.matches('{').count() as i32 - line.matches('}').count() as i32;
if let Some(d) = test_at {
if depth > d {
test_open = true;
} else if test_open || t.trim_end().ends_with(';') {
test_at = None;
test_open = false;
}
}
}
found
}
fn is_ident(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
/// The leading identifier of `s`, and what follows it.
fn ident(s: &str) -> Option<(String, &str)> {
let end = s.find(|c: char| !is_ident(c)).unwrap_or(s.len());
(end > 0).then(|| (s[..end].to_string(), &s[end..]))
}
/// The dotted tails a path fragment names — one, or every leaf of a `use` brace group.
fn expand(s: &str) -> Vec<String> {
let s = s.trim_start();
if !s.starts_with('{') {
let mut out = String::new();
let mut rest = s;
while let Some((seg, after)) = ident(rest) {
if !out.is_empty() {
out.push_str("::");
}
out.push_str(&seg);
if !after.starts_with("::") {
break;
}
let tail = &after[2..];
// `interact::{WorldClick, WorldRightClick}` — a group hanging off a path we have
// already walked. Without this the walk stops dead at the brace and the whole group
// collapses to the bare module path, which `drop_module_paths` then discards: every
// item imported the ordinary Rust way was invisible to both walls. The brace arm
// below only ever saw a group that began the path, which in real code it never does.
if tail.trim_start().starts_with('{') {
let head = out;
return expand(tail)
.into_iter()
.map(|leaf| format!("{head}::{leaf}"))
.collect();
}
rest = tail;
}
return if out.is_empty() { vec![] } else { vec![out] };
}
let mut depth = 0usize;
let mut end = s.len();
for (i, c) in s.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = i;
break;
}
}
_ => {}
}
}
let mut out = Vec::new();
let mut depth = 0usize;
for part in split_top(&s[1..end], &mut depth) {
let part = part.trim();
let part = part.split(" as ").next().unwrap_or(part).trim();
if part.is_empty() || part == "self" {
continue;
}
match part.find('{') {
Some(b) => {
let head = part[..b].trim_end_matches(':');
for leaf in expand(&part[b..]) {
out.push(format!("{head}::{leaf}"));
}
}
None => out.extend(expand(part)),
}
}
out
}
/// Split on commas that are not inside a nested brace group.
fn split_top<'a>(s: &'a str, depth: &mut usize) -> Vec<&'a str> {
let (mut out, mut start) = (Vec::new(), 0);
for (i, c) in s.char_indices() {
match c {
'{' => *depth += 1,
'}' => *depth = depth.saturating_sub(1),
',' if *depth == 0 => {
out.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
out.push(&s[start..]);
out
}
/// A path folded to the symbol it names: everything up to and including the first capitalised
/// segment (a type), or the whole path when every segment is lowercase (a function).
fn canon(path: &str) -> String {
let mut out: Vec<&str> = Vec::new();
for seg in path.split("::") {
out.push(seg);
if seg.starts_with(char::is_uppercase) {
break;
}
}
out.join("::")
}
fn rs_files(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d).into_iter().flatten().flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
} else if p.extension().is_some_and(|x| x == "rs") {
out.push(p);
}
}
}
out.sort();
out
}
fn render(items: &[(usize, &String)]) -> String {
let mut v = items.to_vec();
v.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(b.1)));
v.iter()
.map(|(n, k)| format!(" {n:3} file(s) {k}"))
.collect::<Vec<_>>()
.join("\n")
}