From 7fe4b37c2f1dbec397ca1e47510837c05c27580a Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 6 Sep 2026 19:41:57 +0700 Subject: [PATCH] guild lines are message ids, mail sends again, and the shadow fades in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the guild lines are message ids: `ui_guild/lines.rs` carried 34 English constants and two hand-rolled fill helpers and pushed composed text onto the chat log — 2045's largest ratchet row and the one file its rule had no route for, because `UiError`'s doc claimed the 1.12 formats use at most one `%s` and one `%d` and the type was built to match, while `ERR_GUILD_PROMOTE_SSS` is "%s has promoted %s to %s." — a wrong sentence in a doc had become a wrong type, and the wrong type had made a file's worth of drift structurally necessary; `CGGameUI::DisplayError` (`0x496720`) is cdecl varargs — `add esp,4` bare, `add esp,8` with one string, `SMSG_GUILD_INVITE` pushing two with the stack check exact at `0x30 + 0x60 = 0x90`, and `SMSG_GUILD_EVENT`'s emitter tail `0x5e745f` passing one, two or three off `strCount` — so `UiError` carries an ordered heterogeneous `Vec` (`S(String) | D(i64)`, since `ERR_USE_LOCKED_WITH_SPELL_KNOWN_SI` is String-then-Integer) behind four constructors naming the reference's call shapes (`key`, `s`, `strings`, `args`), twenty sites migrated; what the catalog knew and composed text could not carry: `ERR_GUILD_NAME_INVALID`/`ERR_GUILD_NAME_EXISTS_S` are kind 2 — the red line — and went to chat, `ERR_GUILD_CREATE_S`/`ERR_INVITED_TO_GUILD_SS` carry the LEVELUP cue and `ERR_GUILD_PERMISSIONS` (type_tag `0x3e`) is one the reference speaks (1815), all three silent; the default arm was missing outright — `0x5e745a` pushes `0x69` = `ERR_GUILD_INTERNAL` for `0x09` and every id past `0x0d`, catalog row 105 the whole time, so `TABARD_CHANGE` had been silent under a comment calling the key unsettled; a short `strCount` is passed SHORT (0 and ≥ 4 pass zero strings) so a starved specifier reads "…promoted Furor to %s.", not "…to ."; `/ginfo` deliberately stays off the queue — `GUILD_NAME_TEMPLATE`/`GUILD_INFO_TEMPLATE` are not catalog rows, `0x5e6fb0` resolves each token through the VM and emits chat itself, and a non-row key through `Shown::keyed` would turn a chat line red — so it waits on the guild feed and goes out unkeyed, the enUS month-day-year swap with it; the `_S` arity is asserted from the key names, 22 of 22; the ratchet row is deleted, 208 over 31 files remain (2054) - a unit's blob shadow asks the UNIT for its alpha: the lane gathered live `RenderFade`s off a unit's part entities and read "no part is fading" as 1.0 — true of a settled unit and exactly backwards for arrival, where a streamed unit holds `PendingAppearFade` with no `RenderFade` anywhere until the world is shown, so its parts were deliberately invisible while the shadow drew fully opaque; measured on a probe login: alpha 1.000 from t 0.61 to the arm at t 3.51, then 0, then the ramp — 2.9 s of dark oval under no creature, the symptom `model_render_alpha`'s own "pending ⇒ 0" law (0827) already named for emitters and ribbons; `model_fade::UnitRenderAlpha` is the one door, composed from the ROOT where `UnitAppearFade` tells pending from settled, published as one question rather than its three inputs so a caller cannot reassemble the mistake, and `DespawnFade::armed()` retires the `-1.0` sentinel from every caller; the world API wall goes 177 → 178; and wow-re settled what the shadow's alpha IS, four questions VERIFIED: it reads `CM2Model+0x180` through `0x710ca0`, not `+0x19c`; that slot carries the appear and despawn ramps, the self fade AND the CharProc-14 aura — `0x60d180` drives the same `StartAlphaFade` the appear fade does, so a stealthed unit's shadow now dims with its body — and not the per-batch M2 colour/weight tracks; and the reference draws nothing under a not-yet-shown object either (`+0x180 = 1.0 · 0.0`, written 30 bytes before the read), so this is fidelity, not taste; three module-doc claims corrected against it: the dead `0x683ea5` gate, the registration handoff (GameObjects and DynamicObjects never cast; units, players and corpses always do), and 1277's `InheritedVisibility` gate, which the reference lacks and stays ours on purpose (2055) - the mail stationery list was empty for the whole session, and every send with it: the five `dropping command — not connected` lines at login, once named, were `ItemQuery` for all five `Stationery.dbc` rows — `feed_mail` carried no run condition and asked from the login screen before the socket existed, the asks evaporated in the io thread, and `Items::template` had already latched all five in its ask-once `pending` set, cleared on disconnect and never on connect, so they were never re-asked and the stock `SendMailFrame_Reset`'s row-1 selection left every send silently unsent; the list now waits for a self player, and `release_ask_once_latches_on_enter` clears the pending sets at world enter, the counterpart the disconnect teardown never had — a redundant re-ask costs one packet, a wrong latch costs a dead feature nobody can see is dead; and three more log lines answer for themselves: a control character is answered before the shape — a newline is a line break the markup parser consumes, not a missing glyph — so the warn stays meaningful for a glyph a face genuinely lacks; the blend-check's one-frame run is the specialization tick catching up with a material swap, a real login showing four entries clear 26 ms later, so the warn waits for a run to SURVIVE the tick and a one-frame run is recorded at debug; and the audio starve line carries the wall span it covered beside the time it counted and the position it saw advance, because a 1.0 s window still closed 0.756 s of wall after the stream started on `Time` — if they disagree the caller's clock is still wrong, if they agree the starvation is real Co-Authored-By: Claude Fable 5.1 --- crates/benilla-app/src/blob_shadow.rs | 93 ++- crates/benilla-app/src/char_create/mod.rs | 18 +- crates/benilla-app/src/char_select/refresh.rs | 59 +- crates/benilla-app/src/cvars.rs | 97 +++- crates/benilla-app/src/entities/wmo_props.rs | 12 +- crates/benilla-app/src/glue_strings.rs | 171 +++++- crates/benilla-app/src/items.rs | 16 + crates/benilla-app/src/net.rs | 23 + crates/benilla-app/src/net/apply.rs | 10 +- crates/benilla-app/src/net/apply/quests.rs | 17 +- crates/benilla-app/src/net/io.rs | 14 +- crates/benilla-app/src/perf/blend_check.rs | 76 ++- crates/benilla-app/src/sound/glue.rs | 11 +- crates/benilla-app/src/sound/mixer.rs | 35 +- crates/benilla-app/src/sound/zone.rs | 6 +- crates/benilla-app/src/target/click.rs | 36 +- crates/benilla-app/src/ui_action/errors.rs | 116 +++- crates/benilla-app/src/ui_action/mod.rs | 2 +- crates/benilla-app/src/ui_auction/mod.rs | 7 +- crates/benilla-app/src/ui_guild/feed.rs | 52 +- crates/benilla-app/src/ui_guild/lines.rs | 547 +++++++++--------- crates/benilla-app/src/ui_guild/mod.rs | 50 +- crates/benilla-app/src/ui_mail/mod.rs | 10 + crates/benilla-app/src/ui_models/mod.rs | 14 +- crates/benilla-app/src/ui_party/mod.rs | 28 +- crates/benilla-app/src/ui_quest.rs | 10 +- crates/benilla-app/src/ui_quest_share.rs | 15 +- crates/benilla-app/src/ui_text/engine/mod.rs | 55 ++ crates/benilla-app/src/ui_trade.rs | 50 +- crates/benilla-app/tests/reference_strings.rs | 21 +- crates/benilla-app/tests/world_api_wall.rs | 12 +- crates/benilla-protocol/src/messages/guild.rs | 11 +- crates/benilla-world/src/entity_shade.rs | 36 +- crates/benilla-world/src/model_fade.rs | 153 ++++- crates/benilla-world/src/model_render.rs | 13 +- crates/benilla-world/src/model_render/lazy.rs | 138 ++++- crates/benilla-world/src/static_gx/bake.rs | 110 +++- crates/benilla-world/src/static_gx/mod.rs | 32 +- 38 files changed, 1614 insertions(+), 562 deletions(-) diff --git a/crates/benilla-app/src/blob_shadow.rs b/crates/benilla-app/src/blob_shadow.rs index 6070f668..f6ac814d 100644 --- a/crates/benilla-app/src/blob_shadow.rs +++ b/crates/benilla-app/src/blob_shadow.rs @@ -13,9 +13,13 @@ //! row `0x6812b1`–`0x6812ca` inside `0x681070`, which the driver `0x483460` calls at //! `0x48361d` (wow-re `water-frame-straddle.md` §1 + `unit-blob-shadow.md` Q1). `0x683dd0` is //! the **M2 node drain**, and the same loop body ticks each node's object first -//! (`0x48160c call [obj vt+0x38]` → the selection ring) and gates its shadow second +//! (`0x48160c call [obj vt+0x38]` → the selection ring) and draws its shadow second //! (`0x683ec3`) — so per unit the additive ring goes down and this modulate darkens it, never -//! the other way round (wow-re `decal-frame-slot.md`). So the shadow +//! the other way round (wow-re `decal-frame-slot.md`). That second step is **not** a gate: +//! both exits of the tick `0x481540` return 1, so `0x683ea5`'s `je` is dead — an earlier +//! reading of it as "the callback suppresses a hidden object's shadow" was wrong at the bytes +//! and is corrected in wow-re (2026-09-06). What the tick DOES do is write the alpha this draw +//! is about to read, 30 bytes later — which is the whole of §"Appearance" below. So the shadow //! lands after terrain and WMO and **before** everything else: the footprint decals //! (`0x483654`), the M2 opaque pass (`0x4836a6`), the water surfaces (phase 3, drawn *between* //! the two M2 transparent passes) and both of those passes. Every transparent in the world @@ -40,16 +44,46 @@ //! never appear). Full extents, no missing half/scale factor — the standing size IS the law. //! - **Appearance**: multiplicative darken — `GL_DST_COLOR/GL_ZERO` with the fade riding the //! combine, which is exactly the lane's `EffectBlend::Multiply` (`dst × lerp(1, src, α)`). -//! Vertex diffuse is **white** with α = the model's fade alpha (`[model+0x180]` — spawn/despawn -//! fades + the self first-person fade ride into the shadow); the darkness lives in the texture -//! RGB. Unlit, no fog, no depth write. The texture loads as the default `WorldArt` +//! Vertex diffuse is **white** with α = the model's **base** alpha — `CM2Model+0x180`, read +//! through the accessor `0x710ca0` (`fld [ecx+0x180]`) at `0x6d7fd6`, then clamped to `[0,1]`, +//! ×255, `__ftol`, packed `(a<<24)|0x00FFFFFF`. VERIFIED, and it is `+0x180` and not `+0x19c`: +//! over the whole draw every `0x19c` operand is the stack local `[ebp-0x19c]`, and `this` is +//! touched at exactly three `mov ecx,ebx; call` sites (box `0x711a20`, matrix `0x710600`, +//! alpha `0x710ca0`), which makes that list the complete field census. +//! +//! **What rides in, at the bytes** (wow-re, 2026-09-06): the 2 s appear ramp and the despawn +//! ramp (`obj+0xf4`, `0x613b1e`'s `0x7d0`), the self first-person fade, **and the CharProc-14 +//! aura transition** — stealth's `0.3`, ghost/invisibility's `0.5`. The aura is not a second +//! channel: `0x60d180` drives the SAME `StartAlphaFade` (`0x614f80`) the appear fade uses, and +//! `0x614a90` writes `model+0x180 = obj+0x100 · obj+0xf4` unconditionally given a model handle. +//! A stealthed unit's blob shadow dims with its body, on the same 1000 ms cubic. +//! +//! **What does not**: the M2 animated colour / texture-weight tracks. The body's per-batch +//! alpha is `+0x19c · M2Color[i].alpha · M2TextureWeight[j].value` (`0x707aea`–`0x707b33`); +//! this draw reads `+0x180` raw, so an asset-authored transparency track fades the body and +//! leaves its shadow at full strength. [`benilla_world::model_fade::UnitRenderAlpha`] is that +//! same product and stops at the same place. +//! +//! The darkness lives in the texture RGB. Unlit, no fog, no depth write. The texture loads as the default `WorldArt` //! `Rgba8Unorm`, so the modulate multiplies raw bytes in the gamma lane — the reference's own //! arithmetic (0161). //! - **Gating**: the reference's `shadowLOD` cvar {0,1} is the master toggle (default on) — we are //! always-on; `shadowBias` (default 0.1) is its depth-bias knob — [`SHADOW_DEPTH_BIAS`] plays -//! that role here. No dead/mount/kind test exists on the draw path; **which** objects register -//! for shadows is an open RE item (`HANDOFF(-> object-layer)`) — v1 policy: every Player/Unit -//! entity with a built animated model (GameObjects/doodads excluded). +//! that role here. No dead/mount/kind test exists on the draw path, and **which** objects +//! register is now settled (wow-re, 2026-09-06 — the old `HANDOFF(-> object-layer)` is closed): +//! `[node+0x90]` bit `0x400` is `NOT(arg bit1)` (`0x670e94`), and `0x613e10` takes that arg off +//! `OBJECT_FIELD_TYPE` — `0xb` GAMEOBJECT, `2` DYNAMICOBJECT, `0` otherwise. **GameObjects and +//! DynamicObjects never cast a blob shadow; units, players and corpses always do.** Which is +//! the v1 policy this lane already ran (every Player/Unit with a built animated model), now a +//! byte-fact rather than a guess. +//! +//! **One benilla divergence, deliberate.** `0x6d78f0`/`0x6d7920` read no visibility state at +//! all — the body's draw flag `[CM2Model+0x50]` comes from `ShouldRender` at `0x48174e`, which +//! is consumed at `0x48161d`, *after* the tick — so the reference can and does draw a shadow +//! under an undrawn body (a unit whose skin composite is still unbaked, `0x477860`). We gate on +//! `InheritedVisibility` anyway, because benilla's exterior-scene election is not a mechanism +//! the reference has and left Tanaris mobs' shadows on the Caverns of Time floor (decision +//! 1277). Keeping that gate is the 1277 call; it is named here so it reads as a choice. //! //! One shadow record per unit, its projected triangles rebuilt only when the inputs move //! ([`ShadowKey`]) and pushed onto the effect stream every shown frame — an idle unit costs a @@ -59,14 +93,12 @@ use benilla_assets::ModelAnimations; use benilla_protocol::EntityKind; -use bevy::ecs::entity::{EntityHashMap, EntityHashSet}; +use bevy::ecs::entity::EntityHashSet; use bevy::prelude::*; use crate::creature_anim::AnimData; use crate::net::{Embodied, NetEntity}; -use crate::player::CameraControl; use benilla_world::decal::{DecalFrame, WorldDecal}; -use benilla_world::model_fade::{fade_alpha, RenderFade}; use benilla_world::particles::buffer::{begin_effect_frame, EffectVertex}; use benilla_world::schedule::WorldStage; use benilla_world::view::WorldCamera; @@ -184,15 +216,11 @@ fn sync_shadows( fn update_shadows( time: Res