the unit frames are the reference's own, and the water has motes in it

- the player, party, target and pet frames are the reference's own files (`BuffFrame`, `UnitFrame`, `CombatFeedback`, `PlayerFrame`, `PartyFrame`, `TargetFrame`, `PetFrame`, in the reference's toc order) — our 2741-line `UnitFrames.xml` and our `PartyFrame.xml` are deleted; four engine verbs built for them (`PartialPlayTime`, `NoPlayTime`, `UnitPlayerControlled`, `UnitIsCivilian`, each answering 1/nil), the whole-UI load bracketed in the counted sound-suppression scope `0x48fbf0` self-brackets (the reference fires a `PlaySound` during load and throws it away), and `GetBillingTimeRested` read off `SMSG_AUTH_RESPONSE`'s third billing field rather than hardcoded; the differences the stock files bring are pinned rather than papered — `PlayerFrame` never hides, an unresolved name is BLANK not "Unknown", the target frame has no numerals at all (a bar hover pops the unit tooltip instead)
- what the swap surfaced, each a hard break under the stock files: `UnitBuff`/`UnitDebuff` returned the Classic Era ten-value tuple, name first, where 1.12 returns two and three values TEXTURE first (`0x519500`/`0x5198f0`) — a raise twice per `UNIT_AURA` on any debuffed target, and all 184 corpus call sites bind the 1.12 shape; the power events are named per resource (`UNIT_MANA`/`RAGE`/`FOCUS`/`ENERGY`/`HAPPINESS` and their MAX twins) and we fired the Era pair, so no mana, rage or energy bar could live-update; `SetRaidTarget` is the engine verb and `SetRaidTargetIcon` the FrameXML toggle wrapper — we had registered the wrapper's name on the engine's body, so every raid mark in the unit popup raised
- the underwater drift cloud: the reference draws a fixed 4000-mote field whenever the camera eye is inside a liquid — not a particle system, nothing spawns, ages or dies; a population scattered once into a 30 yd camera-relative cube, advected by the camera's own motion plus a gust and wrapped, so the field stands still in the world; benilla had the murk and nothing in it; a dry frame commits no draw, and the submerged one is bounded by the reference's own 666-quad cap (633–644 of 4000 measured live, which is the expected survivor count of a uniform cube through the 90° cone — an independent check on population, distribution and cone at once)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 17:04:32 +07:00
co-authored by Claude Fable 5.1
parent 6bafea0919
commit 7e3a67a02e
51 changed files with 2337 additions and 3467 deletions
@@ -323,6 +323,42 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
// red/blue player legs, an NPC the reaction swatch. (The live gate is `UnitPlayerControlled`,
// which also covers pets/charmed creatures; we resolve only the player half of it — the extra
// reach needs a player-controlled flag we don't carry, and a player's own alt is the case here.)
// UnitIsCivilian(unit) → 1 if killing this unit would be a DISHONORABLE kill, else nil. This is
// `0x612550` itself handed to Lua — the same four-term predicate (`pvp` bit AND creature-query
// civilian flag AND hostile AND the kill would be grey) that the unit tooltip's green CIVILIAN
// line and `UnitPVPName`'s civilian arm already run through `is_civilian_kill`. That function's
// doc called itself "ONE home, two callers"; the stock target frame is the third, and it is the
// one that names the predicate — `TargetFrame_CheckDishonorableKill` (TargetFrame.lua:230) is
// its only FrameXML caller and the comment inside it reads "Is a dishonorable kill".
//
// Sharing the function is the point: a second copy would let the tooltip, the name and the
// target plate disagree about the same mob.
g.set(
"UnitIsCivilian",
lua.create_function(|lua, unit: Option<String>| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
let player_level = model.player_req.level;
Ok(match unit.and_then(|u| model.unit(&u)) {
Some(u) if super::is_civilian_kill(u, player_level) => Value::Integer(1),
_ => Value::Nil,
})
})?,
)?;
// UnitPlayerControlled(unit) → 1 if a PLAYER is driving this unit, else nil. `UNIT_FIELD_FLAGS`
// bit 3 (`UNIT_FLAG_PVP_ATTACKABLE 0x8`), which is wider than `UnitIsPlayer` below: a player's
// pet and a charmed creature answer 1 here and nil there. Stock `UnitFrame_OnEnter`
// (UnitFrame.lua:58) is the caller that made this a blocker rather than a nicety — it gates the
// player-options newbie tip on it, so a missing binding raised on the first unit-frame hover.
g.set(
"UnitPlayerControlled",
lua.create_function(|lua, unit: Option<String>| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(match unit.and_then(|u| model.unit(&u)) {
Some(u) if u.player_controlled => Value::Integer(1),
_ => Value::Nil,
})
})?,
)?;
g.set(
"UnitIsPlayer",
lua.create_function(|lua, token: Option<String>| {
@@ -902,6 +938,54 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
})
})?,
)?;
// PartialPlayTime() / NoPlayTime() → 1/nil: the two anti-addiction play-time regimes, read
// straight off `PLAYER_FLAGS` — `0x48eb70` is `shr eax,0xc; and al,1` (bit 12) and `0x48ebe0`
// its bit-13 neighbour (decision 1746, which carved both while establishing that bit 0x1000 is
// PARTIAL_PLAY_TIME on 5875 rather than the pre-1.6.1 CAN_SELF_RESURRECT). Same Lua-vanilla
// predicate shape as `IsResting` above: the NUMBER 1 or nil, never a boolean. Stock
// `PlayerFrame_UpdatePlaytime` (PlayerFrame.lua:244) is the only FrameXML caller of either.
g.set(
"PartialPlayTime",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(if model.partial_play_time {
Value::Integer(1)
} else {
Value::Nil
})
})?,
)?;
g.set(
"NoPlayTime",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(if model.no_play_time {
Value::Integer(1)
} else {
Value::Nil
})
})?,
)?;
// GetBillingTimeRested() → the account's rested billing MINUTES, as one number, always
// (decision 1820; binding `0x48ec50`, byte-VERIFIED). The body has no `jcc` at all and ends in
// an unconditional `mov eax,1`, and both callees are branchless and call-free — so it can never
// return nil and never return zero values, which is why this is not the `Value::Nil` shape its
// neighbours use. The engine applies NO conversion: `0x48ec5e` is a bare unsigned u32→double
// push, so "minutes" is the server's convention, corroborated by stock `PlayerFrame.lua:246`
// dividing by 60 against `REQUIRED_REST_HOURS = 5`.
//
// Against vmangos this reads 0, because the server hardcodes `uint32(0)` for the field
// (`World.cpp:331`). It is fed from the wire rather than pinned to a constant so a server that
// populates it is reported honestly. Note the reference only ever reaches this call from
// INSIDE `if PartialPlayTime() ... elseif NoPlayTime()`, so with those bits clear stock
// FrameXML never calls it — it has to exist for the file to load, and is off the live path.
g.set(
"GetBillingTimeRested",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(Value::Number(f64::from(model.billing_time_rested)))
})?,
)?;
// GetTimeToWellRested() → nil, always — `0x48d4b0`, byte-VERIFIED: the whole binding is 11
// bytes, `pushnil; return 1`. FrameXML's EXHAUST_TOOLTIP4 countdown branch is dead in 5875.
g.set(
+51 -6
View File
@@ -182,6 +182,15 @@ pub struct UnitState {
/// This token is a PLAYER character (guid family) — the unit tooltip's level line renders
/// "Race Class (Player)" instead of the creature type (decision 0276's verified law).
pub is_player: bool,
/// This token is **player-CONTROLLED** — `UNIT_FIELD_FLAGS` bit 3
/// (`UNIT_FLAG_PVP_ATTACKABLE 0x8`, behaviourally "player-controlled"; the same bit
/// `target::relations::ring_reaction`'s duel leg selects on). Wider than
/// [`Self::is_player`]: a player's pet and a charmed creature are player-controlled without
/// being players, which is exactly the distinction stock `UnitFrame_OnEnter`
/// (UnitFrame.lua:58) needs when it decides whether the hovered plate gets the
/// player-options newbie tip. `UnitIsPlayer`'s note used to call this reach a gap we did not
/// carry; the unit-frame migration is what closed it.
pub player_controlled: bool,
/// The unit's guild membership (`GetGuildInfo(unit)`, decision 1257). `None` = guildless, or
/// a creature, or a player whose `PLAYER_GUILDID` has not streamed yet. Filled from the
/// PUBLIC descriptor fields 191/192 joined against the app's guild-identity cache — see
@@ -221,12 +230,16 @@ pub struct UnitState {
pub faction_name: Option<String>,
/// Connection to the server is live (`UnitIsConnected`) — an offline party member's portrait
/// desaturates (decision 0434 §2/§3: the `GROUP_LIST` status byte's `0x01` online bit).
/// **v1 default gap, stated not hidden:** the zero value is `false`, so a snapshot built via
/// `..Default::default()` reports a unit disconnected unless the app sets this explicitly; the
/// per-frame feed must set it `true` for every *live* unit token it pushes (mirroring
/// [`Self::exists`]) — a pending app-side change, not yet wired as of this field's addition. A
/// token absent from the store (`UnitExists` false) still reports nil regardless, via the same
/// "no snapshot" path every other predicate here uses.
/// **The zero value is `false`**, so a snapshot built via `..Default::default()` reports a unit
/// DISCONNECTED — which matters more than it reads: stock `UnitFrameManaBar_Update`
/// (UnitFrame.lua:213) greys a disconnected unit's bar and never reaches the power colour at
/// all. A synthetic unit that means to be live must say so.
///
/// The app's own feed does (`ui_unit::snapshot`, "the stated `is_connected` gap, closed"), and
/// the party feed reads the roster status byte's `0x01` for its tokens. This doc used to call
/// that "a pending app-side change, not yet wired"; it has been wired since, and the note
/// outlived it. A token absent from the store (`UnitExists` false) still reports nil
/// regardless, via the same "no snapshot" path every other predicate here uses.
pub is_connected: bool,
/// Away-from-keyboard (`UnitIsAFK` — decision 0434 §2's status-byte `0x40` bit). The party
/// frame's name line and chat both key off it. App-resolved; `false` until the feed lands.
@@ -451,6 +464,38 @@ impl super::UiScript {
model.resting = resting;
}
/// Push the two **play-time** bits of `PLAYER_FLAGS` — 12 (`PartialPlayTime`) and 13
/// (`NoPlayTime`), decision 1746. Taken together for the same reason the rest trio is:
/// stock `PlayerFrame_UpdatePlaytime` (PlayerFrame.lua:244) tests them as an if/elseif pair
/// and a half-updated pair would paint the wrong one of the two icons.
pub fn set_play_time(&mut self, partial: bool, none: bool) {
let mut model = self.model_mut();
model.partial_play_time = partial;
model.no_play_time = none;
}
/// Push the account's **rested billing minutes**, from the `SMSG_AUTH_RESPONSE` that admitted
/// the session — what `GetBillingTimeRested()` returns (decision 1820). Set once at login: the
/// client parks it in a process-lifetime global and nothing else on the wire ever writes it.
pub fn set_billing_time_rested(&mut self, minutes: u32) {
self.model_mut().billing_time_rested = minutes;
}
/// Enter the **UI-load sound-suppression scope** — the reference's `0x458f50`, an `inc` on the
/// counted depth every name-keyed `PlaySound` reads. Counted, not a flag, exactly as the
/// client has it: nesting is legal and only the outermost exit re-enables sound.
pub fn push_sound_suppression(&self) {
let mut model = self.model_mut();
model.sound_suppression = model.sound_suppression.saturating_add(1);
}
/// Leave it — `0x458f60`'s `dec`. Saturating on the way down too: an unbalanced pop is a bug
/// in the caller, and silently wrapping to a permanently-muted UI would be the worse failure.
pub fn pop_sound_suppression(&self) {
let mut model = self.model_mut();
model.sound_suppression = model.sound_suppression.saturating_sub(1);
}
/// Push whether a cinematic is playing — what `InCinematic()` answers.
pub fn set_in_cinematic(&mut self, playing: bool) {
self.model_mut().in_cinematic = playing;