Files
benilla-octo/crates/benilla-protocol/src/messages/update_object/fields/unit.rs
T
SamandClaude Fable 5 ac2ed34735 the combat log speaks, and the swim line follows your native form
- the client synthesises its combat-log chat lines: 44 chat types over 34 GlobalString families from 7 packets, the reference's own four hops (classify both endpoints → chat type → string key → format), text resolved out of the player's own GlobalStrings — so damage meters and Quiver's TranqAnnouncer have something to parse; wow-re's byte round corrected six gates (owner is charmed-by then created-by, "party" is your subgroup, friend-or-foe is CanAttack, the range gate, the miss defaults, the damage-shield chat type)
- the collision prism follows the NATIVE display, not the rendered one: a druid in bear form keeps the druid's swim, splash and wade lines and a morph keeps the human's — worst divergence on shipped data 0.72 yd (tauren moonkin), the sign flipping by form
- the creature swim gate is on both legs: a unit that loses its flag mid-water is driven out of swim at any depth, as the reference drops a charmed creature to the seabed on the next tick

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

534 lines
31 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 unit/object/GameObject named accessors over [`ObjectFields`] — the UNIT_* family every
//! world entity carries (health/power/level/auras/combat stats), plus the object scale and the
//! GameObject block. Split from the one descriptor map (`mod.rs` holds the indices + codec).
use super::*;
/// `UNIT_DYNAMIC_FLAGS` bit 5 — `UNIT_DYNFLAG_DEAD` (vmangos `SharedDefines.h:1158`). The client
/// reads it as "this body is a corpse" wherever it reads health for display; the server sets it for
/// **feign death** and for `CREATURE_FLAG_EXTRA_APPEAR_DEAD` spawns, in both cases with health left
/// intact. Every consumer goes through [`ObjectFields::unit_reads_dead`] (decision 1022).
const UNIT_DYNFLAG_DEAD: u32 = 0x20;
/// `UNIT_STAND_STATE_DEAD` (vmangos `UnitDefines.h:109`) — the third leg of the client's
/// reads-dead predicate `0x605f90`. vmangos never writes it, so it is inert against our server.
///
/// The client reads it through the virtual `[vtbl+0xa4]`, and the **two classes install different
/// functions there** — `CGUnit_C` `0x60be50`, `CGPlayer_C` `0x5ed570` (memoised at `[this+0x1d68]`
/// for the local player) — but both return the same byte, `[descr+0x210]` = `UNIT_FIELD_BYTES_1`
/// byte 0, which is exactly what [`ObjectFields::unit_stand_state`] reads (wow-re
/// `feign-death-dyndead.md` §1).
const STAND_STATE_DEAD: u8 = 7;
/// The raw→display divisor per power type — the reference's `0x6e7130`, a lookup into the table at
/// `0x86f978`: **`{1, 10, 1, 1, 1000}`** for MANA / RAGE / FOCUS / ENERGY / HAPPINESS, with `1` for
/// any out-of-range type (`test ecx,ecx; jl → 1`). Rage is stored ×10 on the wire and pet happiness
/// ×1000 (vmangos `GetCreatePowers`: `POWER_RAGE → 1000`, `POWER_HAPPINESS → 1050000`), so the real
/// client's 100 rage and 1050 happiness are this divide, not a different field.
///
/// Applied by `UnitMana`/`UnitManaMax` only — the raw accessors stay raw, and the pet happiness
/// bucket thresholds are on the RAW scale, which is why both forms have to exist (decision 1034).
fn power_display_scale(ty: u8) -> u32 {
match ty {
1 => 10, // rage
4 => 1000, // happiness
_ => 1,
}
}
/// Which field [`ObjectFields::unit_owner`] falls back to when `CHARMEDBY` is clear — the one
/// thing the client's two owner-guid readers do differently. Named rather than defaulted because
/// picking the wrong one is invisible until a totem or a guardian behaves like a pet.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OwnerFallback {
/// `UNIT_FIELD_CREATEDBY` — what `0x5ee5a0` ("resolve my own pet") uses.
CreatedBy,
/// `UNIT_FIELD_SUMMONEDBY` — what the `PET_ATTACK_*` callback `0x5ff580` uses.
SummonedBy,
}
impl ObjectFields {
/// `OBJECT_FIELD_SCALE_X` — the per-object size multiplier.
pub fn object_scale_x(&self) -> Option<f32> {
self.get_f32(FIELD_OBJECT_SCALE_X)
}
/// `UNIT_FIELD_DISPLAYID` (units + players).
pub fn unit_displayid(&self) -> Option<i32> {
self.get_i32(FIELD_UNIT_DISPLAYID)
}
/// `UNIT_FIELD_NATIVEDISPLAYID` (units + players) — the unit's **unshifted** appearance, which
/// no transform touches. The collision prism is derived from *this*, not from
/// [`Self::unit_displayid`]: a druid in bear form keeps the druid's swim/splash/foam lines
/// (decision 1574). `None` when absent — the caller falls back to the render display.
pub fn unit_native_displayid(&self) -> Option<i32> {
self.get_i32(FIELD_UNIT_NATIVEDISPLAYID)
}
/// `UNIT_FIELD_MOUNTDISPLAYID` — the `CreatureDisplayInfo` id of the mount this unit rides;
/// `0` (or absent) = not mounted. The one and only mounted signal on the wire (decision 0441).
pub fn unit_mount_display_id(&self) -> u32 {
self.get_u32(FIELD_UNIT_MOUNTDISPLAYID).unwrap_or(0)
}
/// `UNIT_FIELD_TARGET` — the guid this unit is attacking/interacting with (its "look at" target),
/// or `None` when it has none (an absent field or an explicit `0`). Drives the idle face-target turn.
pub fn unit_target(&self) -> Option<u64> {
self.get_guid(FIELD_UNIT_TARGET).filter(|&g| g != 0)
}
/// `UNIT_FIELD_CHANNEL_OBJECT` — the object a channel is aimed at (may be the caster itself),
/// or `None` when not channeling (an absent field or an explicit `0`).
pub fn unit_channel_object(&self) -> Option<u64> {
self.get_guid(FIELD_UNIT_CHANNEL_OBJECT).filter(|&g| g != 0)
}
/// `UNIT_CHANNEL_SPELL` — the spell id a unit is channeling; `0` when not channeling or absent.
pub fn unit_channel_spell(&self) -> u32 {
self.get_u32(FIELD_UNIT_CHANNEL_SPELL).unwrap_or(0)
}
/// `UNIT_FIELD_SUMMON` — the guid of the unit this one has summoned, or `None`. On our own
/// descriptor that is **our pet**, and so the anchor of the `"pet"` unit token (decision 0982).
/// The inverse of [`Self::unit_summoned_by`].
pub fn unit_summon(&self) -> Option<u64> {
self.get_guid(FIELD_UNIT_SUMMON).filter(|&g| g != 0)
}
/// `UNIT_FIELD_SUMMONEDBY` — the summoner's guid (pet/guardian/totem), or `None` (absent or
/// explicit 0 — an unowned unit).
pub fn unit_summoned_by(&self) -> Option<u64> {
self.get_guid(FIELD_UNIT_SUMMONEDBY).filter(|&g| g != 0)
}
/// `UNIT_FIELD_CHARMEDBY` — whoever is charming this unit, or `None` when nobody is.
pub fn unit_charmed_by(&self) -> Option<u64> {
self.get_guid(FIELD_UNIT_CHARMEDBY).filter(|&g| g != 0)
}
/// The unit's **owner** as the client's own two owner-guid readers compute it:
/// `charmedBy` when set, else the caller's fallback field.
///
/// Both readers are `(charmedBy != 0) ? charmedBy : <fallback>` and **they disagree on the
/// fallback**, which is why this takes it rather than picking one. `0x5ee5a0` — "resolve my own
/// pet" — falls back to `CREATEDBY` (`0x5ee62f`: `fields+0x10` else `fields+0x20`); the
/// `PET_ATTACK_*` field-change callback `0x5ff580` falls back to `SUMMONEDBY` (`0x5ff769`:
/// `+0x10` else `+0x18`). Reading one where the reference reads the other silently changes
/// which units count as yours (wow-re `object-layer/scratch/pet-command-validators.md` §1, §4).
pub fn unit_owner(&self, fallback: OwnerFallback) -> Option<u64> {
self.unit_charmed_by().or_else(|| match fallback {
OwnerFallback::CreatedBy => self.unit_created_by(),
OwnerFallback::SummonedBy => self.unit_summoned_by(),
})
}
/// `UNIT_FIELD_CREATEDBY` — the creator's guid (totems and created objects), or `None`.
pub fn unit_created_by(&self) -> Option<u64> {
self.get_guid(FIELD_UNIT_CREATEDBY).filter(|&g| g != 0)
}
/// `UNIT_CREATED_BY_SPELL` — the spell that summoned this unit, `None` when zero (nothing
/// summoned it). The reference's feed-pet validator `0x6ea1e0` requires it non-zero before it
/// will accept a unit as a feedable pet — the first of that function's three gates.
pub fn unit_created_by_spell(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_CREATED_BY_SPELL)
.filter(|&s| s != 0)
}
/// `UNIT_FIELD_PETNUMBER` — non-zero iff this unit is somebody's **pet or charm**.
///
/// Not a count and not an index anyone displays: the client reads it as a pure boolean, as the
/// **second gate of the rank getter** `0x605620` (decision 0782). A creature with a cached
/// template but a non-zero pet number reports classification rank `0` — so an enslaved elite
/// loses its dragon border, its ELITE tooltip word and (at rank 3) its world-boss skull, all
/// three from this one read.
///
/// What lands here is the server's call, and vmangos is narrower than the name suggests:
/// `CharmInfo::SetPetNumber(n, statWindow)` writes `n` only when `statWindow`, else **0**. So a
/// permanent pet (`Pet::IsPermanentPetFor`), a charm/mind-control apply and a player's summon
/// carry a non-zero number, while **guardians and totems carry 0** and therefore keep whatever
/// rank their template gives them.
pub fn unit_is_pet_or_charm(&self) -> bool {
self.get_u32(FIELD_UNIT_PETNUMBER).unwrap_or(0) != 0
}
/// `UNIT_FIELD_PET_NAME_TIMESTAMP` — when this pet's name was last set. `None` when the field
/// has never been sent (a create omits zeros), which reads the same as "never renamed".
///
/// The **only** thing to do with the value is compare it with the last one seen: a pet's name
/// lives in a query cache keyed by pet number, and this changing is what says that cache has
/// gone stale (see the constant).
pub fn unit_pet_name_timestamp(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_PET_NAME_TIMESTAMP)
}
/// `UNIT_FIELD_HEALTH` — current hit points (0 ⇒ dead).
pub fn unit_health(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_HEALTH)
}
/// `UNIT_FIELD_MAXHEALTH` — full hit points.
pub fn unit_max_health(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_MAXHEALTH)
}
/// Whether the unit is **really dead** — health 0 on a real unit, the RAW field predicate.
/// Absent health reads as **zero** — a create block only carries *non-zero* fields (vmangos
/// `_SetCreateBits`), so an already-dead corpse streams in with `MAXHEALTH` but **no** `HEALTH`
/// at all; the real client's descriptor array is zero-initialized, so absent *is* 0 to it. The
/// `MAXHEALTH > 0` guard keeps a unit whose snapshot hasn't arrived (empty store) from reading
/// as dead.
///
/// This is the predicate for everything that keys on the body actually being a corpse — the
/// death arc and its release/ghost UI, loot and skinning, the selection's death-clear edge. For
/// **how the unit reads to the eye** — which a feigning hunter shares with a corpse — use
/// [`Self::unit_reads_dead`] (decision 1022).
pub fn unit_is_dead(&self) -> bool {
self.unit_max_health().unwrap_or(0) > 0 && self.unit_health().unwrap_or(0) == 0
}
/// Whether the unit **reads as dead** to the client — the reference's own one predicate
/// `0x605f90`, byte-exact: really dead (`[descr+0x40] <= 0`), **or** carrying
/// `UNIT_DYNFLAG_DEAD` (`0x605f9d`: `[descr+0x224] >> 5 & 1` — **feign death**), **or** stand
/// state 7 `UNIT_STAND_STATE_DEAD` (`0x605faa`: `vtbl+0xa4` == 7).
///
/// Feign death is exactly this bit and nothing else: vmangos `Unit::SetFeignDeath`
/// (`Unit.cpp:9491`/`9500`) sets and clears it while leaving `UNIT_FIELD_HEALTH` alone, and
/// `Object::BuildValuesUpdate` only rewrites `TRACK_UNIT`/`TAPPED` per viewer — so the flag
/// reaches **every** viewer, the feigner included. The client then reads dead everywhere it
/// matters: `UnitHealth`/`UnitMana` answer 0 ([`Self::unit_shown_health`],
/// [`Self::unit_shown_power`]), `UnitIsDead 0x517ac0` answers true, the animation chain's top
/// resolver `0x5fcff0` claims and holds the corpse pose, and the plate/loop-sound gates drop.
///
/// The stand-state leg is the reference's; vmangos never writes stand state 7, so against our
/// server the triple and the narrower `UnitIsDead 0x517ac0` pair (health ≤ 0 ∨ dynflag)
/// coincide.
pub fn unit_reads_dead(&self) -> bool {
self.unit_is_dead()
|| self.unit_dynamic_flags() & UNIT_DYNFLAG_DEAD != 0
|| self.unit_stand_state() == STAND_STATE_DEAD
}
/// Health **as the UI reads it** — the `UnitHealth 0x5174d0` law, byte-exact: a unit carrying
/// `UNIT_DYNFLAG_DEAD` answers **0** (`0x51753c`–`0x517551`), everything else answers the raw
/// field clamped at zero (`0x517553`–`0x517560`: `setle`/`dec`/`and`). So a feigning hunter's
/// health bar empties for himself and for everyone watching.
///
/// Deliberately NOT mirrored on the max: `UnitHealthMax 0x5175b0` reads `[descr+0x58]` with no
/// gate at all, which is what makes the bar render `0/max` (empty) rather than vanish.
pub fn unit_shown_health(&self) -> Option<u32> {
if self.unit_dynamic_flags() & UNIT_DYNFLAG_DEAD != 0 {
return Some(0);
}
self.unit_health()
}
/// Power **as the UI reads it** — the `UnitMana 0x517670` law, both halves (decision 1034):
/// the `UNIT_DYNFLAG_DEAD` gate (`0x5176d6`–`0x5176e6`) ahead of the power-slot read, and then
/// the **raw→display divide** by [`power_display_scale`] (`0x517704 call 0x6e7130; 0x51770f
/// div ecx`). Rage rides the wire ×10 and pet happiness ×1000; without the divide a warrior's
/// bar reads `0/1000` and a pet's `…/1050000`.
///
/// `UnitManaMax 0x5177e0` divides too (`0x517864`) but has **no** dead gate, so the bar empties
/// against a real maximum — see [`Self::unit_shown_max_power`].
///
/// Not transcribed: the `powerType == POWER_HEALTH (-2)` substitution at `0x5176f7`. The
/// reference means to read HEALTH there, but `0x5176e8 movzx` zero-extends the byte while
/// `0x5176ec cmp edx,0xfffffffe` sign-extends its imm8, so the equality can never hold and the
/// leg is **dead code as compiled in 5875** (wow-re `feign-death-dyndead.md` §3, both §5
/// workers independently). Rendering it would be reproducing a bug, not the behaviour.
pub fn unit_shown_power(&self, ty: u8) -> Option<u32> {
if self.unit_dynamic_flags() & UNIT_DYNFLAG_DEAD != 0 {
return (ty < 5).then_some(0);
}
Some(self.unit_power(ty)? / power_display_scale(ty))
}
/// Maximum power **as the UI reads it** — `UnitManaMax 0x5177e0`: the same
/// [`power_display_scale`] divide (`0x517864`), and deliberately **no** dead gate.
pub fn unit_shown_max_power(&self, ty: u8) -> Option<u32> {
Some(self.unit_max_power(ty)? / power_display_scale(ty))
}
/// `UNIT_FIELD_LEVEL` — the unit's level.
pub fn unit_level(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_LEVEL)
}
/// One aura slot (0-based), or `None` when the slot is empty or `slot >= UNIT_AURA_SLOTS`.
///
/// Occupancy is the **client's own liveness test**: at least one effect-index bit set in the
/// `AURAFLAGS` nibble (`flags & 0x0E`, decision 0257) — *not* "has a spell id". A slot the server
/// clears can keep a stale spell id after its flags nibble goes to zero; gating on the id alone
/// would surface that husk as a phantom aura the real client hides (`flags & 0x0E` is precisely
/// why it doesn't). The `spell_id != 0` guard stays as a belt-and-braces check — for a live aura
/// (an applied effect implies a spell) the two always coincide.
pub fn unit_aura(&self, slot: u8) -> Option<UnitAuraSlot> {
if slot >= UNIT_AURA_SLOTS {
return None;
}
let flags = self.get_aura_nibble(slot);
if flags & AURA_FLAG_EFF_INDEX_MASK == 0 {
return None;
}
let spell_id = self
.get_u32(FIELD_UNIT_AURA + u16::from(slot))
.filter(|&id| id != 0)?;
Some(UnitAuraSlot {
slot,
spell_id,
flags,
level: self.get_aura_byte(FIELD_UNIT_AURALEVELS, slot),
// The wire byte is `stack - 1` and saturates at 254, so an occupied slot is always ≥ 1.
stacks: self
.get_aura_byte(FIELD_UNIT_AURAAPPLICATIONS, slot)
.saturating_add(1),
})
}
/// Every occupied aura slot, **ascending by slot index** — buffs (0–31) before debuffs (32–47).
///
/// This is the reference client's ordering law for `UnitBuff`/`UnitDebuff` on **another unit**:
/// they read that unit's `UNIT_FIELD_AURA` straight, ascending raw slot within the half (wow-re
/// `0x519500`/`0x5198f0`, VERIFIED — decision 0257).
///
/// It is **not** the order the player's own buff bar draws. For the local player the client
/// keeps a separate, densely packed cache (`0xbc6040`) in **insertion order**, which repacks on
/// removal — so a dropped buff slides the later ones along, and a new buff always lands at the
/// end rather than in whatever slot the server recycled. That order cannot be recovered from a
/// descriptor snapshot; it has to be *maintained* across updates (decision 0257).
pub fn unit_auras(&self) -> impl Iterator<Item = UnitAuraSlot> + '_ {
(0..UNIT_AURA_SLOTS).filter_map(|slot| self.unit_aura(slot))
}
/// The unit's active power type (`UNIT_FIELD_BYTES_0` byte 3): `0` mana, `1` rage, `2` focus,
/// `3` energy, `4` happiness. Absent (no bytes_0 on the wire yet) reads as mana, the descriptor's
/// zero-initialized default.
pub fn unit_power_type(&self) -> u8 {
(self.unit_bytes_0().unwrap_or(0) >> 24) as u8
}
/// `UNIT_FIELD_BYTES_1` byte 0 — the unit's stand state (VERIFIED vmangos
/// `UNIT_BYTES_1_OFFSET_STAND_STATE = 0`, written by `Unit::SetStandState`): 0 stand · 1 sit
/// (ground) · 2 sit-chair · 3 sleep · 4/5/6 chair low/med/high · 8 kneel. For a player it's
/// the echo of their own `CMSG_STANDSTATECHANGE`. `0` (standing) when the field is absent —
/// the descriptor's zero-initialized default.
pub fn unit_stand_state(&self) -> u8 {
(self.get_u32(FIELD_UNIT_BYTES_1).unwrap_or(0) & 0xff) as u8
}
/// `UNIT_FIELD_BYTES_1` byte 1 — a hunter pet's **loyalty level**, `1..=8`, and `0` for a unit
/// that has none (VERIFIED vmangos `UNIT_BYTES_1_OFFSET_PET_LOYALTY = 1`; byte-verified
/// client-side as `[unit+0x110]+0x211`, the byte `GetPetLoyalty 0x4be700` indexes
/// `PetLoyalty.dbc` with). `0` is the binding's **nil**, not level 1.
pub fn unit_loyalty_level(&self) -> u8 {
((self.get_u32(FIELD_UNIT_BYTES_1).unwrap_or(0) >> 8) & 0xff) as u8
}
/// `UNIT_FIELD_PETEXPERIENCE` / `UNIT_FIELD_PETNEXTLEVELEXP` — `GetPetExperience`'s
/// `(currXP, nextXP)`, in that order. Absent fields read `0`, which is the binding's own
/// gate-failure value: this pair is numbers on every path, never nil.
pub fn unit_pet_experience(&self) -> (u32, u32) {
(
self.get_u32(FIELD_UNIT_PETEXPERIENCE).unwrap_or(0),
self.get_u32(FIELD_UNIT_PETNEXTLEVELEXP).unwrap_or(0),
)
}
/// `UNIT_TRAINING_POINTS` split as the client splits it — `(total, spent)`, **high word
/// first**. The order is the load-bearing half: both are small numbers of the same magnitude,
/// so swapping them yields a pet that looks like it has spent everything or nothing, with no
/// error anywhere.
pub fn unit_training_points(&self) -> (u16, u16) {
let packed = self.get_u32(FIELD_UNIT_TRAINING_POINTS).unwrap_or(0);
((packed >> 16) as u16, (packed & 0xffff) as u16)
}
/// `UNIT_FIELD_BYTES_1` byte 3's `UNIT_VIS_FLAGS_CREEP (0x2)` — the sneaking visual, set by
/// `SPELL_AURA_MOD_STEALTH` (vmangos `SpellAuras.cpp:3610`,
/// `UNIT_BYTES_1_OFFSET_VIS_FLAG = 3`). The usable walk's only-stealthed leg tests exactly
/// this bit (wow-re §2a leg 7: `[+0x110]+0x213 & 2`); the tracking classifier's
/// stealth always-show clause tests it against our own track-stealthed bit
/// (`0x5ed210`, wow-re `track-predicates.md`).
pub fn unit_is_stealthed(&self) -> bool {
(self.get_u32(FIELD_UNIT_BYTES_1).unwrap_or(0) >> 24) & 0x2 != 0
}
/// `UNIT_FIELD_BYTES_1` byte 3's `UNIT_VIS_FLAGS_GHOST (0x1)` — set on ANY unit by the ghost
/// aura (vmangos `HandleAuraGhost`). Byte-VERIFIED (wow-re ghost-death-visuals.md): this flag
/// drives NO body render — its complete client effect is nameplate/marker suppression
/// (tested as `byte3 & 0x03`, ghost|creep, at `0x607101`/`0x60f62e`). The translucent
/// whitened BODY is the ghost aura's own state visual kit (spell 8326 → SpellVisual 886 →
/// kit 989: charproc-14 alpha 0.5 + charproc-1 tint 0xFF8CB9FD + `Spells\Ghost_state.mdx`) —
/// aura-driven, exactly like Stealth's 0.3 translucency. Consumers: the overhead-name and
/// V-plate gates.
pub fn unit_is_ghost_visual(&self) -> bool {
(self.get_u32(FIELD_UNIT_BYTES_1).unwrap_or(0) >> 24) & 0x1 != 0
}
/// `UNIT_FIELD_AURASTATE` ([`FIELD_UNIT_AURASTATE`]) — the aura-state bit set (defense 1,
/// healthless-20% 2, …); the usable walk tests `1 << (state-1)` against it.
pub fn unit_aura_state(&self) -> u32 {
self.get_u32(FIELD_UNIT_AURASTATE).unwrap_or(0)
}
/// `UNIT_FIELD_BYTES_1` byte 2 — the shapeshift form (VERIFIED vmangos `UpdateFields_1_12_1.h`
/// 0x8A + `UNIT_BYTES_1_OFFSET_SHAPESHIFT_FORM = 2`): 0 none, 17/18/19 = warrior
/// battle/defensive/berserker stance, druid forms in the low ids. Drives the action bar's
/// bonus-page pick, and the tracking dots' creature-type override (the `0x605570`
/// resolver's first branch — decision 0564).
pub fn unit_shapeshift_form(&self) -> u8 {
(self.get_u32(FIELD_UNIT_BYTES_1).unwrap_or(0) >> 16) as u8
}
/// `UNIT_FIELD_POWER1..5` — current power for slot `ty` (the [`Self::unit_power_type`] index).
/// Out-of-range slots and absent fields read `None`.
pub fn unit_power(&self, ty: u8) -> Option<u32> {
(ty < 5).then(|| self.get_u32(FIELD_UNIT_POWER1 + u16::from(ty)))?
}
/// `UNIT_FIELD_MAXPOWER1..5` — maximum power for slot `ty`.
pub fn unit_max_power(&self, ty: u8) -> Option<u32> {
(ty < 5).then(|| self.get_u32(FIELD_UNIT_MAXPOWER1 + u16::from(ty)))?
}
/// `UNIT_FIELD_BASE_MANA` (VERIFIED vmangos `UpdateFields_1_12_1.h` `OBJECT_END + 0x9C` = 162;
/// PRIVATE + OWNER_ONLY, so it rides only our own descriptor) — the base the percent-cost
/// spells (`ManaCostPercentage`) scale from. `None` when absent (a non-self unit).
pub fn unit_base_mana(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_BASE_MANA)
}
/// `UNIT_FIELD_FACTIONTEMPLATE` — the unit's `FactionTemplate.dbc` row id, the key the client's
/// whole reaction/hostility decode starts from (wow-re: descriptor `+0x74`, read by every
/// `UnitReaction`/`Can*` predicate). Flagged PUBLIC server-side, so it's always on the wire.
pub fn unit_faction_template(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_FACTIONTEMPLATE)
}
/// `UNIT_FIELD_FLAGS` — unit state flags; bit `0x0400_0000` = **skinnable** (the cursor's Skin
/// branch reads it — wow-re cursor RE, client `+0xa0`). Absent counts as 0 (create omits zeros).
pub fn unit_flags(&self) -> u32 {
self.get_u32(FIELD_UNIT_FLAGS).unwrap_or(0)
}
/// `UNIT_FIELD_COMBATREACH` — the melee-reach float the client's edge-to-edge attack range gate
/// reads (`rA + rB + 1.333`, cap 5.0 — wow-re cursor RE `0x6e3480`, client `+0x1f0`). Vanilla
/// default 1.5 when absent.
pub fn unit_combat_reach(&self) -> f32 {
self.get_f32(FIELD_UNIT_COMBATREACH).unwrap_or(1.5)
}
/// `UNIT_FIELD_BOUNDINGRADIUS` — the unit's horizontal bounding radius (yd). The water-foam
/// depth gate reads it (`max(2 × radius, 1.0)` — wow-re CWater0Ripple driver `0x5fa760`).
/// Absent counts as 0 — the gate's own 1.0-yd floor covers it, like the reference.
pub fn unit_bounding_radius(&self) -> f32 {
self.get_f32(FIELD_UNIT_BOUNDINGRADIUS).unwrap_or(0.0)
}
/// `UNIT_DYNAMIC_FLAGS` — per-viewer dynamic state; bit `0x1` = **lootable** (the cursor's Loot
/// branch — wow-re cursor RE, client `+0x224`), bit `0x2` = tracked (Hunter's Mark), bit `0x20`
/// = **dead-looking** ([`Self::unit_reads_dead`] — feign death). Absent counts as 0.
pub fn unit_dynamic_flags(&self) -> u32 {
self.get_u32(FIELD_UNIT_DYNAMIC_FLAGS).unwrap_or(0)
}
/// `UNIT_DYNAMIC_FLAGS` bit `0x1` — **lootable by me** (the server strips the bit per viewer
/// before it ships — vmangos's "hide lootable animation for unallowed players"). Gates the
/// loot cursor, the right-click loot route, and the corpse sparkle (wow-re
/// `loot-corpse-effect.md`).
pub fn unit_lootable(&self) -> bool {
self.unit_dynamic_flags() & 0x1 != 0
}
/// `UNIT_NPC_FLAGS` — the NPC service bits (vanilla values: gossip `0x1`, questgiver `0x2`,
/// vendor `0x4`, flightmaster `0x8`, trainer `0x10`, … repair `0x4000` — vmangos
/// `UnitDefines.h`). The cursor's service ladder switches on these, first bit wins (wow-re
/// cursor RE, client `+0x234`). Absent counts as 0.
pub fn unit_npc_flags(&self) -> u32 {
self.get_u32(FIELD_UNIT_NPC_FLAGS).unwrap_or(0)
}
/// `UNIT_NPC_EMOTESTATE` — the unit's looping state emote (`Emotes.dbc` id: dance, NPC
/// cooking/working flavor; `0`/absent = none). The anim layer loops its `AnimID` while the
/// unit stands; movement suppresses it (the field stays set server-side until cleared).
pub fn unit_emote_state(&self) -> u32 {
self.get_u32(FIELD_UNIT_NPC_EMOTESTATE).unwrap_or(0)
}
/// `UNIT_FIELD_BYTES_0` — packed race (byte 0) / class (byte 1) / gender (byte 2) / power type (3).
pub fn unit_bytes_0(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_BYTES_0)
}
/// `UNIT_VIRTUAL_ITEM_SLOT_DISPLAY + slot` — a creature's virtual-weapon **display id** for
/// `slot` (0 mainhand / 1 offhand / 2 ranged): the item's `DisplayInfoID` directly, no item guid
/// behind it (see the field's doc comment). `slot >= 3` reads `None`.
pub fn unit_virtual_item_display(&self, slot: u8) -> Option<u32> {
(slot < 3).then(|| self.get_u32(FIELD_UNIT_VIRTUAL_ITEM_SLOT_DISPLAY + u16::from(slot)))?
}
/// `UNIT_VIRTUAL_ITEM_INFO + 2*slot` — `slot`'s flavor bytes: `(class, subclass, material,
/// inventory_type)`. `slot >= 3` reads `None`.
pub fn unit_virtual_item_info(&self, slot: u8) -> Option<(u8, u8, u8, u8)> {
let v =
(slot < 3).then(|| self.get_u32(FIELD_UNIT_VIRTUAL_ITEM_INFO + 2 * u16::from(slot)))?;
v.map(|v| {
(
(v & 0xff) as u8,
((v >> 8) & 0xff) as u8,
((v >> 16) & 0xff) as u8,
((v >> 24) & 0xff) as u8,
)
})
}
/// `UNIT_VIRTUAL_ITEM_INFO + 2*slot + 1` byte 0 — `slot`'s virtual item Sheath. `slot >= 3`
/// reads `None`.
pub fn unit_virtual_item_sheath(&self, slot: u8) -> Option<u8> {
let v = (slot < 3)
.then(|| self.get_u32(FIELD_UNIT_VIRTUAL_ITEM_INFO + 2 * u16::from(slot) + 1))?;
v.map(|v| (v & 0xff) as u8)
}
/// `UNIT_FIELD_BYTES_2` byte 0 — the unit's sheath state: `0` unarmed/stowed, `1` melee drawn,
/// `2` ranged drawn. For a player this is purely what they last told the server via
/// `CMSG_SETSHEATHED`, not a server-computed fact.
pub fn unit_sheath_state(&self) -> Option<u8> {
self.get_u32(FIELD_UNIT_BYTES_2).map(|v| (v & 0xff) as u8)
}
/// `UNIT_FIELD_STAT0..4 + i` — the unit's raw (post-buff) primary stat `i` (0 Str, 1 Agi,
/// 2 Stam, 3 Int, 4 Spirit — the `Stats` enum order). `i >= 5` reads `None`.
pub fn unit_stat(&self, i: u8) -> Option<u32> {
(i < 5).then(|| self.get_u32(FIELD_UNIT_STAT0 + u16::from(i)))?
}
/// `UNIT_FIELD_RESISTANCES + i` — resistance `i` (0 = armor, 1..6 the magic schools). `i >= 7`
/// reads `None`.
pub fn unit_resistance(&self, i: u8) -> Option<i32> {
(i < 7).then(|| self.get_i32(FIELD_UNIT_RESISTANCES + u16::from(i)))?
}
/// `UNIT_FIELD_ATTACK_POWER` — melee attack power before the split pos/neg mods.
pub fn unit_attack_power(&self) -> Option<i32> {
self.get_i32(FIELD_UNIT_ATTACK_POWER)
}
/// `UNIT_FIELD_ATTACK_POWER_MODS` — `(positive, negative)` flat AP mods (the negative half
/// already negative-or-zero; see the field's doc comment for the verified packing/sign).
/// Absent reads `(0, 0)` — no mods applied yet.
pub fn unit_attack_power_mods(&self) -> (i16, i16) {
let (lo, hi) = self
.get_u16_pair(FIELD_UNIT_ATTACK_POWER_MODS)
.unwrap_or((0, 0));
(lo as i16, hi as i16)
}
/// `UNIT_FIELD_ATTACK_POWER_MULTIPLIER` — the percent AP bonus **minus 1.0** (`0.0` = no
/// bonus; vmangos writes `multiplier − 1.0`, see the field's doc comment).
pub fn unit_attack_power_multiplier(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_ATTACK_POWER_MULTIPLIER)
}
/// `UNIT_FIELD_RANGED_ATTACK_POWER` — ranged attack power before the split pos/neg mods.
pub fn unit_ranged_attack_power(&self) -> Option<i32> {
self.get_i32(FIELD_UNIT_RANGED_ATTACK_POWER)
}
/// `UNIT_FIELD_RANGED_ATTACK_POWER_MODS` — the ranged twin of
/// [`Self::unit_attack_power_mods`].
pub fn unit_ranged_attack_power_mods(&self) -> (i16, i16) {
let (lo, hi) = self
.get_u16_pair(FIELD_UNIT_RANGED_ATTACK_POWER_MODS)
.unwrap_or((0, 0));
(lo as i16, hi as i16)
}
/// `UNIT_FIELD_RANGED_ATTACK_POWER_MULTIPLIER` — the ranged twin of
/// [`Self::unit_attack_power_multiplier`].
pub fn unit_ranged_attack_power_multiplier(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_RANGED_ATTACK_POWER_MULTIPLIER)
}
/// `UNIT_FIELD_MINDAMAGE` — the mainhand weapon's minimum damage.
pub fn unit_min_damage(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_MINDAMAGE)
}
/// `UNIT_FIELD_MAXDAMAGE` — the mainhand weapon's maximum damage.
pub fn unit_max_damage(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_MAXDAMAGE)
}
/// `UNIT_FIELD_MINOFFHANDDAMAGE` — the offhand weapon's minimum damage (present whether or not
/// one's equipped; the paper-doll only shows it when an offhand item is worn).
pub fn unit_min_offhand_damage(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_MINOFFHANDDAMAGE)
}
/// `UNIT_FIELD_MAXOFFHANDDAMAGE` — the offhand weapon's maximum damage.
pub fn unit_max_offhand_damage(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_MAXOFFHANDDAMAGE)
}
/// `UNIT_FIELD_MINRANGEDDAMAGE` — the ranged weapon's minimum damage.
pub fn unit_min_ranged_damage(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_MINRANGEDDAMAGE)
}
/// `UNIT_FIELD_MAXRANGEDDAMAGE` — the ranged weapon's maximum damage.
pub fn unit_max_ranged_damage(&self) -> Option<f32> {
self.get_f32(FIELD_UNIT_MAXRANGEDDAMAGE)
}
/// `UNIT_FIELD_BASEATTACKTIME + i` — attack speed in ms (`i` 0 mainhand, 1 offhand). `i >= 2`
/// reads `None`.
pub fn unit_base_attack_time(&self, i: u8) -> Option<u32> {
(i < 2).then(|| self.get_u32(FIELD_UNIT_BASEATTACKTIME + u16::from(i)))?
}
/// `UNIT_FIELD_RANGEDATTACKTIME` — ranged weapon attack speed in ms.
pub fn unit_ranged_attack_time(&self) -> Option<u32> {
self.get_u32(FIELD_UNIT_RANGEDATTACKTIME)
}
}