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:
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user