- druids get the shift cloud back in both directions, by the reference's own mechanism: every shapeshift aura add *or* remove arms a one-slot pending-morph latch that the model-swap rebuild drains and replays — which is why shifting out (a client-side cancel the server sends no visual for) puffs at all, and why a GM morph or a revive correctly shows nothing; the first fix that merely let one-shot effects survive the swap's teardown produced the right pixels for the wrong reason and is superseded - the honor system lands whole: the Honor tab on the character window, the honor page on inspect, and thirteen missing Lua bindings over a real wire model — session/yesterday/this-week/last-week kills, lifetime HKs and DKs, rank progress and badge art - the load-bearing find: your current rank and your highest-ever rank are two separately-streamed fields, and the one we already had was the wrong one for anyone who ever de-ranked; a binary cross-check corrected five display details after the build, down to the rank title never being gendered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
862 lines
53 KiB
Rust
862 lines
53 KiB
Rust
//! The item/container/PLAYER named accessors over [`ObjectFields`] — inventory and equipment
|
||
//! slots, purse/XP, the quest log, skills, buyback, reputation-adjacent player bytes. Split from
|
||
//! the one descriptor map (`mod.rs` holds the indices + codec).
|
||
|
||
use super::*;
|
||
|
||
impl ObjectFields {
|
||
/// `OBJECT_FIELD_ENTRY` (3) — the object's template entry (an item object's item id, a
|
||
/// GameObject's template, a creature's `creature_template` row). The one field that names WHAT
|
||
/// a streamed object is.
|
||
///
|
||
/// **For a PET this is the only route to its template**, and that is load-bearing: a
|
||
/// `HIGHGUID_PET` guid carries a *pet number* in the slot a creature's entry occupies
|
||
/// ([`crate::guid::pet_number`]), so [`crate::guid::entry`] refuses it — but vmangos still
|
||
/// writes the real `cinfo->entry` here (`Creature::InitEntry`, `Creature.cpp:376`,
|
||
/// `SetEntry(entry) // normal entry always`). Decision 1062's creature-family lookup goes
|
||
/// through this field for exactly that reason.
|
||
pub fn object_entry(&self) -> Option<u32> {
|
||
self.get_u32(3)
|
||
}
|
||
/// `ITEM_FIELD_STACK_COUNT` — an item object's stack size.
|
||
pub fn item_stack_count(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_ITEM_STACK_COUNT)
|
||
}
|
||
/// `ITEM_FIELD_SPELL_CHARGES + i` — this *instance*'s remaining charges for its `i`th spell
|
||
/// block (`i < 5`), signed: negative = a consumable-on-empty counter, `0` = spent. Distinct
|
||
/// from the TEMPLATE's `ItemSpellEntry::charges`, which says how many a fresh copy carries.
|
||
/// Field 16 = `FIELD_ITEM_STACK_COUNT`(14) + DURATION(15) + 1 — the chain between the tested
|
||
/// STACK_COUNT and FLAGS(21) anchors, and the client's own `[item+0x114]+0x28` (wow-re
|
||
/// `action-item-slot.md` §8.2, the mode-`0x20` search filter that reads exactly this).
|
||
pub fn item_spell_charges(&self, i: u8) -> Option<i32> {
|
||
(i < 5)
|
||
.then(|| self.get_u32(FIELD_ITEM_STACK_COUNT + 2 + u16::from(i)))?
|
||
.map(|v| v as i32)
|
||
}
|
||
/// `ITEM_FIELD_FLAGS` (field 21 — wire index; the client's item-relative index 15 + the 6
|
||
/// object fields, VERIFIED wow-re inventory-alert-law): bit `0x08` = wrapped (a gift — never
|
||
/// alerts, never reads broken), bit `0x10` = force-red (status 4 regardless of durability;
|
||
/// the bit's game meaning is unnamed in the binary, the behavior byte-verified).
|
||
pub fn item_flags(&self) -> Option<u32> {
|
||
self.get_u32(21)
|
||
}
|
||
/// `ITEM_FIELD_CREATOR` (field 10, guid — vmangos `UpdateFields_1_12_1.h` OBJECT_END+0x4) —
|
||
/// the item's author/crafter. On a mail-made permanent letter this is the letter's SENDER
|
||
/// (vmangos `HandleMailCreateTextItem` stamps it) — the reader window's "From," line.
|
||
pub fn item_creator(&self) -> Option<u64> {
|
||
self.get_guid(10).filter(|&g| g != 0)
|
||
}
|
||
/// `ITEM_FIELD_ITEM_TEXT_ID` (field 45, OWNER_ONLY — vmangos `UpdateFields_1_12_1.h`
|
||
/// OBJECT_END+0x27) — the item *instance*'s letter text (a mail-made permanent copy). Nonzero
|
||
/// = right-click reads it (`CMSG_ITEM_TEXT_QUERY`) instead of using it.
|
||
pub fn item_text_id(&self) -> Option<u32> {
|
||
self.get_u32(45)
|
||
}
|
||
/// `ITEM_FIELD_ENCHANTMENT + 3*slot` — the enchant id in one of an item OBJECT's **7**
|
||
/// enchantment slots (`slot < 7`; PERM = 0, TEMP = 1, then the five random-property slots).
|
||
/// `None` = empty / not sent.
|
||
///
|
||
/// This is the item's own array, and it is the FULL one: vmangos
|
||
/// `UpdateFields_1_12_1.h` gives `ITEM_FIELD_ENCHANTMENT = OBJECT_END + 0x10, Size: 21,
|
||
/// PUBLIC` = 7 slots × 3 fields (id, duration, charges — `Item.h:117-119`), so field
|
||
/// `22 + 3*slot`. Distinct from [`Self::player_visible_item_enchant`], the 2-slot
|
||
/// (PERM, TEMP) copy broadcast on a *unit's* descriptor for everyone else to render from:
|
||
/// an item object streams only to its owner, so seven slots are readable for our own gear
|
||
/// and two for anybody else's.
|
||
///
|
||
/// **Signed**, because the sign is load-bearing at exactly one consumer and nowhere else: the
|
||
/// tooltip resolves the `SpellItemEnchantment` row from `abs(id)` and uses the sign only to
|
||
/// paint the line pure-red instead of green (wow-re `tooltip-content-law.md` §E3).
|
||
pub fn item_enchant(&self, slot: u8) -> Option<i32> {
|
||
(slot < 7)
|
||
.then(|| self.get_u32(FIELD_ITEM_ENCHANTMENT + 3 * u16::from(slot)))?
|
||
.map(|id| id as i32)
|
||
.filter(|&id| id != 0)
|
||
}
|
||
/// `ITEM_FIELD_ENCHANTMENT + 3*slot + 2` — that slot's remaining enchant **charges**; `0` =
|
||
/// none. The tooltip appends `" (N Charges)"` to the slot's line when it is nonzero.
|
||
///
|
||
/// The triple's MIDDLE field (the duration) is deliberately not exposed: the reference's
|
||
/// tooltip never reads it. A temporary enchant's countdown comes from
|
||
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` instead (`items::Items::enchant_remaining_ms`).
|
||
pub fn item_enchant_charges(&self, slot: u8) -> u32 {
|
||
(slot < 7)
|
||
.then(|| self.get_u32(FIELD_ITEM_ENCHANTMENT + 3 * u16::from(slot) + 2))
|
||
.flatten()
|
||
.unwrap_or(0)
|
||
}
|
||
/// `ITEM_FIELD_DURABILITY` — an item's current durability (field 46: chain-locked between the
|
||
/// tested STACK_COUNT 14 and CONTAINER_NUM_SLOTS 48 = ITEM_END anchors).
|
||
pub fn item_durability(&self) -> Option<u32> {
|
||
self.get_u32(46)
|
||
}
|
||
/// `ITEM_FIELD_MAXDURABILITY` — the item's durability ceiling (0 = has no durability).
|
||
pub fn item_max_durability(&self) -> Option<u32> {
|
||
self.get_u32(47)
|
||
}
|
||
/// `CONTAINER_FIELD_NUM_SLOTS` — a bag object's capacity.
|
||
pub fn container_num_slots(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_CONTAINER_NUM_SLOTS)
|
||
}
|
||
/// `CONTAINER_FIELD_SLOT_1 + 2i` — the item guid in a bag's slot `i` (< 36). `Some(0)` = the
|
||
/// server said "empty"; `None` = the field wasn't sent.
|
||
pub fn container_slot(&self, i: u8) -> Option<u64> {
|
||
(i < 36).then(|| self.get_guid(FIELD_CONTAINER_SLOT_1 + 2 * u16::from(i)))?
|
||
}
|
||
/// `PLAYER_VISIBLE_ITEM_<slot>_0` — the **public** item entry visibly worn in equipment
|
||
/// slot `i` (what other clients render from; 12 fields per slot after the 2-field creator).
|
||
/// The cross-check for the private INV_SLOT guids — and the entry source for our own
|
||
/// equipment until item objects have a decode path.
|
||
pub fn player_visible_item_entry(&self, i: u8) -> Option<u32> {
|
||
(i < 19)
|
||
.then(|| {
|
||
self.get_u32(FIELD_PLAYER_VISIBLE_ITEM_1_CREATOR + 2 + 12 * u16::from(i))
|
||
.filter(|&e| e != 0)
|
||
})
|
||
.flatten()
|
||
}
|
||
/// `PLAYER_VISIBLE_ITEM_<slot>_0 + 1 + j` — the **broadcast enchant ids** on the item worn in
|
||
/// equipment slot `i`: that 8-field block is the item entry followed by 7 enchant slots, of
|
||
/// which 1.12 fills exactly two (vmangos `Player::SetVisibleItemSlot` loops
|
||
/// `MAX_INSPECTED_ENCHANTMENT_SLOT = 2` — PERM then TEMP). This is the only enchant feed a
|
||
/// client has for **any** unit, itself included: the reference reads a full CGItem's 7-slot
|
||
/// array (`item+0xc`, wow-re `item-visual-enchant.md` §3b), which on the wire is these.
|
||
/// Consumed by the weapon-glow lane (decision 0805); `None` = empty slot / field not sent.
|
||
pub fn player_visible_item_enchant(&self, i: u8, j: u8) -> Option<u32> {
|
||
(i < 19 && j < 7)
|
||
.then(|| {
|
||
self.get_u32(
|
||
FIELD_PLAYER_VISIBLE_ITEM_1_CREATOR + 3 + 12 * u16::from(i) + u16::from(j),
|
||
)
|
||
.filter(|&e| e != 0)
|
||
})
|
||
.flatten()
|
||
}
|
||
/// `PLAYER_FIELD_INV_SLOT_HEAD + 2i` — our equipment (0–18) and equipped-bag (19–22) guids.
|
||
pub fn player_inv_slot(&self, i: u8) -> Option<u64> {
|
||
(i < 23).then(|| self.get_guid(FIELD_PLAYER_INV_SLOT_HEAD + 2 * u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_PACK_SLOT_1 + 2i` — our backpack's 16 item guids.
|
||
pub fn player_pack_slot(&self, i: u8) -> Option<u64> {
|
||
(i < 16).then(|| self.get_guid(FIELD_PLAYER_PACK_SLOT_1 + 2 * u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_BANK_SLOT_1 + 2i` — our bank's 24 item guids.
|
||
pub fn player_bank_slot(&self, i: u8) -> Option<u64> {
|
||
(i < 24).then(|| self.get_guid(FIELD_PLAYER_BANK_SLOT_1 + 2 * u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_BANK_BAG_SLOT_1 + 2i` — the item guid of bank bag `i` (0–5, the 6 purchasable
|
||
/// bank bag slots); the bag's *contents* stream as an ordinary `CONTAINER_FIELD_SLOT_*` block
|
||
/// on the bag item itself, addressed by bag byte 63–68 (decision 0604).
|
||
pub fn player_bank_bag_slot(&self, i: u8) -> Option<u64> {
|
||
(i < 6).then(|| self.get_guid(FIELD_PLAYER_BANK_BAG_SLOT_1 + 2 * u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_KEYRING_SLOT_1 + 2i` — the keyring's item guids. The **field array is 32
|
||
/// guids** (`UpdateFields_1_12_1.h`: size 64 dwords; vmangos `MAX_KEYRING_SLOTS 32`) and the
|
||
/// client's inventory walker treats all 32 as its keyring band (absolute slots 81–112, mode
|
||
/// bit `0x40` — wow-re `action-item-slot.md` §8.2), but only the **first 16 are addressable
|
||
/// positions** on this wire (vmangos `KEYRING_SLOT_START 81`, `KEYRING_SLOT_END 97`, whose own
|
||
/// comment reads "32 slots (only 16 are visible/accessible in UI)"), and only as many of those
|
||
/// as the player's level unlocks are usable (4/8/12/16 — `ui_items::keyring_size`). Feeds the
|
||
/// keyring container (decision 0765) and the
|
||
/// GameObject lock chain's key-slot resolver, which scans the keyring alongside the bags
|
||
/// (wow-re `cursor-system.md` §8.4; decision 0752). `Some(0)` = empty.
|
||
pub fn player_keyring_slot(&self, i: u8) -> Option<u64> {
|
||
(i < 32).then(|| self.get_guid(FIELD_PLAYER_KEYRING_SLOT_1 + 2 * u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_VENDORBUYBACK_SLOT_1 + 2i` — the item guid parked in buyback slot `i` (0–11;
|
||
/// the wire's absolute inventory slot is `69 + i`). `Some(0)` = empty.
|
||
pub fn player_buyback_slot(&self, i: u8) -> Option<u64> {
|
||
(i < 12).then(|| self.get_guid(FIELD_PLAYER_VENDORBUYBACK_SLOT_1 + 2 * u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_BUYBACK_PRICE_1 + i` — what buying slot `i` back costs, in copper.
|
||
pub fn player_buyback_price(&self, i: u8) -> Option<u32> {
|
||
(i < 12).then(|| self.get_u32(FIELD_PLAYER_BUYBACK_PRICE_1 + u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_BUYBACK_TIMESTAMP_1 + i` — slot `i`'s expiry stamp; the client uses it only
|
||
/// to order the buyback list oldest→newest (wow-re `0x4fafd0`).
|
||
pub fn player_buyback_timestamp(&self, i: u8) -> Option<u32> {
|
||
(i < 12).then(|| self.get_u32(FIELD_PLAYER_BUYBACK_TIMESTAMP_1 + u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_COINAGE` — our purse in copper. `None` for a non-player unit (no PLAYER block)
|
||
/// or before the field has streamed in; the descriptor's zero-initialized default is 0 copper.
|
||
pub fn player_money(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_COINAGE)
|
||
}
|
||
/// `PLAYER_FARSIGHT` — the object our view is anchored to while a far-sight effect runs (Mind
|
||
/// Vision, Sentry Totem, the Ornate Spyglass), or `None` when the view is our own body.
|
||
///
|
||
/// PRIVATE, so like [`Self::player_xp`] it only ever streams for our own avatar — which is the
|
||
/// whole consumer: this answers "what does the camera orbit", never anything about another
|
||
/// player. `0` is the cleared state and reads as `None`, following the house convention for
|
||
/// guid fields ([`Self::unit_charmed_by`] and friends): a zero guid is absence, not an object.
|
||
pub fn player_farsight(&self) -> Option<u64> {
|
||
self.get_guid(FIELD_PLAYER_FARSIGHT).filter(|&g| g != 0)
|
||
}
|
||
/// `PLAYER_XP` — our current experience within the level (`UnitXP("player")`). `None` for a
|
||
/// non-player unit (no PLAYER block) or before the field has streamed; a PRIVATE field, so it
|
||
/// only ever arrives for our own avatar's descriptor.
|
||
pub fn player_xp(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_XP)
|
||
}
|
||
/// `PLAYER_NEXT_LEVEL_XP` — experience required to reach the next level (`UnitXPMax("player")`).
|
||
/// Same PRIVATE/own-player caveat as [`Self::player_xp`].
|
||
pub fn player_next_level_xp(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_NEXT_LEVEL_XP)
|
||
}
|
||
/// `PLAYER_FIELD_WATCHED_FACTION_INDEX` — the reputation-list slot whose bar rides the main
|
||
/// menu bar (`GetWatchedFactionInfo`), or `-1` for none. Same PRIVATE/own-player caveat as
|
||
/// [`Self::player_xp`].
|
||
///
|
||
/// **Signed, and `0` is not "none".** Slot 0 is the Bloodsail Buccaneers, so the only
|
||
/// no-faction value is `-1` — reading this field as unsigned would silently watch them.
|
||
pub fn player_watched_faction(&self) -> Option<i32> {
|
||
self.get_u32(FIELD_PLAYER_WATCHED_FACTION_INDEX)
|
||
.map(|v| v as i32)
|
||
}
|
||
/// `PLAYER_CHARACTER_POINTS1` — unspent talent points (`UnitCharacterPoints("player")`'s
|
||
/// first return; the talent frame's "N talent points" line, decision 0304). Same
|
||
/// PRIVATE/own-player caveat as [`Self::player_xp`].
|
||
pub fn player_talent_points(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_CHARACTER_POINTS1)
|
||
}
|
||
/// `PLAYER_CHARACTER_POINTS2` — free primary professions (`UnitCharacterPoints`'s second
|
||
/// return; the "You now have %d free professions." chat line). Same caveat as above.
|
||
pub fn player_free_professions(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_CHARACTER_POINTS2)
|
||
}
|
||
/// `PLAYER_TRACK_CREATURES` — the active creature-tracking bitmask (bit `1 << (creatureType −
|
||
/// 1)` per active TRACK_CREATURES aura): the minimap red-dot predicate's mask. PRIVATE — self
|
||
/// only; absent (no PLAYER block, or nothing ever tracked) reads as no tracking.
|
||
pub fn player_track_creatures(&self) -> u32 {
|
||
self.get_u32(FIELD_PLAYER_TRACK_CREATURES).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_TRACK_RESOURCES` — the active resource-tracking bitmask (bit `1 << (lockType − 1)`
|
||
/// per active TRACK_RESOURCES aura, e.g. Find Minerals → the Mining `LockType.dbc` bit): the
|
||
/// minimap gold-dot predicate's mask. Same PRIVATE/self-only caveat as above.
|
||
pub fn player_track_resources(&self) -> u32 {
|
||
self.get_u32(FIELD_PLAYER_TRACK_RESOURCES).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_FIELD_BYTES` byte 0 bit `0x2` — track-stealthed active (vmangos
|
||
/// `PLAYER_FIELD_BYTE_TRACK_STEALTHED`, set by the TRACK_STEALTHED(151) aura handler): the
|
||
/// tracking classifier's stealth always-show clause — with it set, any CREEP-flagged unit
|
||
/// red-dots (`0x5ed210` @ the `+0x1028` read, wow-re `track-predicates.md`). Same
|
||
/// PRIVATE/self-only caveat as the tracking masks.
|
||
pub fn player_track_stealthed(&self) -> bool {
|
||
self.get_u32(FIELD_PLAYER_FIELD_BYTES).unwrap_or(0) & 0x2 != 0
|
||
}
|
||
/// `PLAYER_QUEST_LOG_<slot+1>_*` — one decoded quest-log slot (0-based, `slot <
|
||
/// PLAYER_QUEST_LOG_SLOTS`). `None` when the slot's id field never streamed (or the index is out
|
||
/// of range); `Some` with `quest_id == 0` is a slot the server has explicitly cleared (an
|
||
/// abandon/turn-in leaves this). The count-state and timer fields default to `0` when absent —
|
||
/// the server writes them with the id on every occupy (vmangos `Player.h:1094-1099`
|
||
/// `SetQuestSlot`), so an absent pair only ever accompanies an untouched slot.
|
||
pub fn player_quest_log(&self, slot: u8) -> Option<QuestLogSlot> {
|
||
if slot >= PLAYER_QUEST_LOG_SLOTS {
|
||
return None;
|
||
}
|
||
let base = FIELD_PLAYER_QUEST_LOG_1_1 + 3 * u16::from(slot);
|
||
let quest_id = self.get_u32(base)?;
|
||
let count_state = self.get_u32(base + 1).unwrap_or(0);
|
||
let timer = self.get_u32(base + 2).unwrap_or(0);
|
||
let mut counters = [0u8; 4];
|
||
for (i, c) in counters.iter_mut().enumerate() {
|
||
*c = ((count_state >> (6 * i)) & 0x3F) as u8;
|
||
}
|
||
Some(QuestLogSlot {
|
||
quest_id,
|
||
counters,
|
||
state: (count_state >> 24) as u8,
|
||
timer,
|
||
})
|
||
}
|
||
/// `PLAYER_EXPLORED_ZONES_1 + i` — the discovery bitset's `i`-th u32 (absent slots read 0:
|
||
/// a fresh descriptor means nothing explored yet). The full set is
|
||
/// [`PLAYER_EXPLORED_ZONES_SLOTS`] slots; bit `n` of the whole = AreaTable exploreFlag `n`.
|
||
pub fn player_explored_zone_slot(&self, i: u16) -> u32 {
|
||
if i >= PLAYER_EXPLORED_ZONES_SLOTS {
|
||
return 0;
|
||
}
|
||
self.get_u32(FIELD_PLAYER_EXPLORED_ZONES_1 + i).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_BLOCK_PERCENTAGE` — our chance to block, already a percent (2.62 = "2.62%"). The
|
||
/// spell tooltip's chance-to-X line (law §3-CHANCE) reads exactly this and its three
|
||
/// neighbours; the character sheet reads them too. `None` for a non-player unit.
|
||
pub fn player_block_percentage(&self) -> Option<f32> {
|
||
self.get_f32(FIELD_PLAYER_BLOCK_PERCENTAGE)
|
||
}
|
||
/// `PLAYER_DODGE_PERCENTAGE` — see [`Self::player_block_percentage`].
|
||
pub fn player_dodge_percentage(&self) -> Option<f32> {
|
||
self.get_f32(FIELD_PLAYER_DODGE_PERCENTAGE)
|
||
}
|
||
/// `PLAYER_PARRY_PERCENTAGE` — see [`Self::player_block_percentage`].
|
||
pub fn player_parry_percentage(&self) -> Option<f32> {
|
||
self.get_f32(FIELD_PLAYER_PARRY_PERCENTAGE)
|
||
}
|
||
/// `PLAYER_CRIT_PERCENTAGE` — melee crit; see [`Self::player_block_percentage`]. (The ranged
|
||
/// twin sits one slot above and has no consumer yet.)
|
||
pub fn player_crit_percentage(&self) -> Option<f32> {
|
||
self.get_f32(FIELD_PLAYER_CRIT_PERCENTAGE)
|
||
}
|
||
/// `PLAYER_FIELD_POSSTAT0 + i` — stat `i`'s **positive** buff delta: the sum of every gear,
|
||
/// enchant and aura contribution the server split out of `UNIT_FIELD_STAT0 + i`. **INT on the
|
||
/// wire**, whatever the server keeps internally — see the field's doc comment. `i >= 5` reads
|
||
/// `None`.
|
||
pub fn player_posstat(&self, i: u8) -> Option<i32> {
|
||
(i < 5).then(|| self.get_i32(FIELD_PLAYER_POSSTAT0 + u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_NEGSTAT0 + i` — stat `i`'s **negative** buff delta; negative-or-zero, read
|
||
/// signed because only one of the two server host architectures can carry the sign at all (the
|
||
/// field's doc comment). `i >= 5` reads `None`.
|
||
pub fn player_negstat(&self, i: u8) -> Option<i32> {
|
||
(i < 5).then(|| self.get_i32(FIELD_PLAYER_NEGSTAT0 + u16::from(i)))?
|
||
}
|
||
/// `PLAYER_FIELD_RESISTANCEBUFFMODSPOSITIVE + school` — the positive resistance buff for
|
||
/// `school` (0..6), INT on the wire. `school >= 7` reads `None`.
|
||
pub fn player_resistance_buff_pos(&self, school: u8) -> Option<i32> {
|
||
(school < 7)
|
||
.then(|| self.get_i32(FIELD_PLAYER_RESISTANCEBUFFMODSPOSITIVE + u16::from(school)))?
|
||
}
|
||
/// `PLAYER_FIELD_RESISTANCEBUFFMODSNEGATIVE + school` — negative-or-zero (see the field's doc
|
||
/// comment). `school >= 7` reads `None`.
|
||
pub fn player_resistance_buff_neg(&self, school: u8) -> Option<i32> {
|
||
(school < 7)
|
||
.then(|| self.get_i32(FIELD_PLAYER_RESISTANCEBUFFMODSNEGATIVE + u16::from(school)))?
|
||
}
|
||
/// `PLAYER_FIELD_MOD_DAMAGE_DONE_POS + school` — positive damage-done bonus (`[0]` = physical).
|
||
/// `school >= 7` reads `None`.
|
||
pub fn player_mod_damage_done_pos(&self, school: u8) -> Option<i32> {
|
||
(school < 7).then(|| self.get_i32(FIELD_PLAYER_MOD_DAMAGE_DONE_POS + u16::from(school)))?
|
||
}
|
||
/// `PLAYER_FIELD_MOD_DAMAGE_DONE_NEG + school` — negative-or-zero (see the field's doc
|
||
/// comment). `school >= 7` reads `None`.
|
||
pub fn player_mod_damage_done_neg(&self, school: u8) -> Option<i32> {
|
||
(school < 7).then(|| self.get_i32(FIELD_PLAYER_MOD_DAMAGE_DONE_NEG + u16::from(school)))?
|
||
}
|
||
/// `PLAYER_FIELD_MOD_DAMAGE_DONE_PCT + school` — the damage-done percent multiplier (a true
|
||
/// float; see the field's doc comment — decision 0208's flagged question, now closed).
|
||
/// `school >= 7` reads `None`; an in-range-but-absent read (no aura has touched this school
|
||
/// yet) is the caller's job to default to `1.0` — the wire only sends fields that changed.
|
||
pub fn player_mod_damage_done_pct(&self, school: u8) -> Option<f32> {
|
||
(school < 7).then(|| self.get_f32(FIELD_PLAYER_MOD_DAMAGE_DONE_PCT + u16::from(school)))?
|
||
}
|
||
/// `PLAYER_SKILL_INFO_<slot+1>_*` — one decoded skill slot (`slot < `[`PLAYER_SKILL_SLOTS`]).
|
||
/// `None` when the id dword never streamed (or `slot` is out of range); the value/max and
|
||
/// bonus dwords default to `0` when absent (mirroring [`Self::player_quest_log`]'s tolerance —
|
||
/// vmangos writes id and value together on `SetSkill`, so an absent pair only ever accompanies
|
||
/// a slot whose snapshot hasn't landed yet).
|
||
pub fn player_skill(&self, slot: u8) -> Option<PlayerSkillSlot> {
|
||
if slot >= PLAYER_SKILL_SLOTS {
|
||
return None;
|
||
}
|
||
let base = FIELD_PLAYER_SKILL_INFO_1_1 + 3 * u16::from(slot);
|
||
let (skill_id, step) = self.get_u16_pair(base)?;
|
||
let (value, max) = self.get_u16_pair(base + 1).unwrap_or((0, 0));
|
||
let (temp_bonus, perm_bonus) = self.get_u16_pair(base + 2).unwrap_or((0, 0));
|
||
Some(PlayerSkillSlot {
|
||
skill_id,
|
||
step,
|
||
value,
|
||
max,
|
||
temp_bonus: temp_bonus as i16,
|
||
perm_bonus: perm_bonus as i16,
|
||
})
|
||
}
|
||
/// `PLAYER_AMMO_ID` — the equipped ammo pouch/quiver's item id (arrows/bullets), or `None`
|
||
/// before the field streams. `0` = no ammo selected.
|
||
pub fn player_ammo_id(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_AMMO_ID)
|
||
}
|
||
/// `PLAYER_BYTES` — packed skinColor (byte 0) / faceType (byte 1) / hairStyle (byte 2) / hairColor
|
||
/// (byte 3). The character compositor's customization input (RF-0056).
|
||
pub fn player_bytes(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_BYTES)
|
||
}
|
||
/// `PLAYER_BYTES_2` — byte 0 = facialHair, byte 1 = unk, byte 2 = bankBagSlots (see
|
||
/// [`Self::player_bank_bag_slots_purchased`]), byte 3 = restState (see
|
||
/// [`Self::player_rest_state`]).
|
||
pub fn player_bytes_2(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_BYTES_2)
|
||
}
|
||
/// `PLAYER_BYTES_2` **byte 3** — the rest state the client's `GetRestState()` maps to its
|
||
/// (id, name, multiplier) triple: `1` = rested, `2` = normal (vmangos `REST_STATE_*`,
|
||
/// `Player.h:673`). The server writes it with hysteresis off the rest pool — rested when the
|
||
/// pool exceeds 10, normal when it falls to ≤1 (`SetRestBonus`) — so the byte, not the pool,
|
||
/// is the state authority.
|
||
///
|
||
/// **Absent reads `0`, not "normal"** — the struct's own absent-field law, and it is the
|
||
/// truth: vmangos always writes this word (`Player::Create` and `LoadFromDB` seed byte 3 with
|
||
/// `REST_STATE_NORMAL` and byte 1 with `0xEE`, so the field is never zero and never masked out
|
||
/// of a create block), so on a created store this IS the wire's byte. `0` is what the client's
|
||
/// own zero-initialized descriptor holds *before* the create lands, and it is a state
|
||
/// `Exhaustion.dbc` cannot map — `GetRestState()` answers the binary's `(nil, nil, nil)` fail
|
||
/// path for it, which the reference's own unguarded `exhaustionStateID >= 3` cannot survive.
|
||
/// The reference never meets that because FrameXML is up and the descriptor has landed before
|
||
/// any handler runs; we hold the same guarantee at the feed's own gate
|
||
/// (`ui_script::ingame_ui_pending`, 1348). Do not fabricate a `2` here — that hides an
|
||
/// ordering bug behind a value.
|
||
pub fn player_rest_state(&self) -> Option<u8> {
|
||
self.player_bytes_2().map(|b| (b >> 24) as u8)
|
||
}
|
||
/// `PLAYER_REST_STATE_EXPERIENCE` (field 1175) — the rested-XP pool, in **base kill-XP
|
||
/// units**: the server drains it 1:1 against a kill's base XP while granting +100%
|
||
/// (vmangos `GetXPRestBonus`), so the doubled-XP span the exhaustion tick marks is 2× this
|
||
/// value. `None` before the field streams (PRIVATE — self only).
|
||
pub fn player_rest_state_experience(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_REST_STATE_EXPERIENCE)
|
||
}
|
||
/// `PLAYER_BYTES_3` **byte 1** — the inebriation byte (0..255) drunkenness renders from. The
|
||
/// server packs `gender | (drunk & 0xFFFE)` into the field's low u16 and decays it 256 per
|
||
/// 10 s (vmangos `Player::SetDrunkValue` / `HandleDrunkReduce`), so this byte is
|
||
/// `drunk_u16 >> 8` — the value the reference clamps to 100 and scales ×0.01 into its drunk
|
||
/// fraction (`[[unit+0xe68]+0x1d]`, wow-re `drunk_fraction_5e2a90`). `None` when never sent
|
||
/// on a bare delta; a create snapshot reads absent as `Some(0)` (sober).
|
||
pub fn player_drunk_byte(&self) -> Option<u8> {
|
||
self.get_u32(FIELD_PLAYER_BYTES_3).map(|v| (v >> 8) as u8)
|
||
}
|
||
/// `PLAYER_FLAGS` (field 190) — the player state flags. `0` when absent (the descriptor's
|
||
/// zero default — a create block skips zero fields).
|
||
pub fn player_flags(&self) -> u32 {
|
||
self.get_u32(FIELD_PLAYER_FLAGS).unwrap_or(0)
|
||
}
|
||
/// Whether the player is a released **ghost** — `PLAYER_FLAGS_GHOST (0x10)`, set/cleared by
|
||
/// the ghost aura 8326 at release/resurrect (decision 0308 §1). NB a ghost's health is **1**
|
||
/// on the wire, so `unit_is_dead` is FALSE for a ghost: the three predicates are
|
||
/// dead = health 0 · ghost = this bit · dead-or-ghost = either (the real client's
|
||
/// UnitIsDead/UnitIsGhost/UnitIsDeadOrGhost trio).
|
||
pub fn player_is_ghost(&self) -> bool {
|
||
self.player_flags() & 0x10 != 0
|
||
}
|
||
/// Whether this player has asked for their **helm** not to be drawn —
|
||
/// `PLAYER_FLAGS_HIDE_HELM (0x400)` (vmangos `Player.h:325`), flipped by `CMSG_TOGGLE_HELM`
|
||
/// and persisted per character through the char-enum record's `CHARACTER_FLAG_HIDE_HELM`.
|
||
///
|
||
/// `PLAYER_FLAGS` is `UF_FLAG_PUBLIC`, which is the whole point of reading it *here* rather
|
||
/// than off a local setting: the preference streams for every player in range, so a remote
|
||
/// player's hidden helm hides on our screen too, and ours hides on theirs (decision 1472).
|
||
/// Creatures have no `PLAYER_FLAGS` and read `false`.
|
||
pub fn player_hides_helm(&self) -> bool {
|
||
self.player_flags() & 0x400 != 0
|
||
}
|
||
/// Whether this player has asked for their **cloak** not to be drawn —
|
||
/// `PLAYER_FLAGS_HIDE_CLOAK (0x800)`, [`Self::player_hides_helm`]'s twin in every respect.
|
||
pub fn player_hides_cloak(&self) -> bool {
|
||
self.player_flags() & 0x800 != 0
|
||
}
|
||
/// Whether the player is inside a rest area (inn/city) — `PLAYER_FLAGS_RESTING (0x20)`
|
||
/// (vmangos `Player.h:320`), set/cleared by the area-trigger/zone rest checks
|
||
/// (`SetRestType`). The real client's `IsResting()` and the player frame's flashing zzz
|
||
/// state icon read exactly this bit.
|
||
pub fn player_is_resting(&self) -> bool {
|
||
self.player_flags() & 0x20 != 0
|
||
}
|
||
/// `PLAYER_DUEL_ARBITER` (field 188, GUID) — the duel-flag GameObject of the duel this player
|
||
/// is in, `0` when they are in none. Set on both duellists by `Spell::EffectDuel`, cleared by
|
||
/// `Player::DuelComplete`; **PUBLIC**, so it streams for every player we can see and not just
|
||
/// ourselves — which is what lets the client resolve a duel relationship it is not part of.
|
||
/// Paired with [`Self::player_duel_team`] it is the whole client-side duel law (0633).
|
||
pub fn player_duel_arbiter(&self) -> u64 {
|
||
self.get_guid(FIELD_PLAYER_DUEL_ARBITER).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_DUEL_TEAM` (field 196) — `0` until the countdown finishes, then `1` for the
|
||
/// challenger and `2` for the challenged (`Player::UpdateDuelFlag`). Non-zero on **both**
|
||
/// sides is what says the duel is actually under way: the arbiter is set from the moment of
|
||
/// the challenge, so hostility keys off the pair, never the arbiter alone (0633).
|
||
pub fn player_duel_team(&self) -> u32 {
|
||
self.get_u32(FIELD_PLAYER_DUEL_TEAM).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_GUILDID` (field 191) — the id of the guild this player belongs to, `0` for a
|
||
/// guildless one (and for the absent field, which is the same thing: the descriptor skips
|
||
/// zeroes). **PUBLIC**, so it arrives for every visible player, not only for us.
|
||
///
|
||
/// It names a guild but does not describe one: the name and the rank names come only from
|
||
/// `SMSG_GUILD_QUERY_RESPONSE`, keyed by this id (decision 1257). The real client's own
|
||
/// `GetGuildInfo 0x4c9330` reads exactly this field and answers nil on both the `0` and the
|
||
/// cache-miss legs (`0x4c93a9`, `0x4c93d7`).
|
||
pub fn player_guild_id(&self) -> u32 {
|
||
self.get_u32(FIELD_PLAYER_GUILDID).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_GUILDRANK` (field 192) — this player's rank within [`Self::player_guild_id`]'s
|
||
/// guild, **0-based with `0` = guild master** (vmangos promotes with `rank--`). Meaningless
|
||
/// while the guild id is `0`. Indexes both [`crate::messages::GuildQueryResponse::rank_names`]
|
||
/// and [`crate::messages::GuildRoster::rank_rights`].
|
||
///
|
||
/// The reference indexes its cached rank-name array with this **unbounded**
|
||
/// (`0x4c93e4 mov edx,[ecx+0x10]` / `shl edx,0x6`); a consumer of ours must bound it against
|
||
/// the ten slots that array really has.
|
||
pub fn player_guild_rank(&self) -> u32 {
|
||
self.get_u32(FIELD_PLAYER_GUILDRANK).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_FIELD_BYTES` byte 0 bit `0x08` — `PLAYER_FIELD_BYTE_RELEASE_TIMER`, "Display time
|
||
/// till auto release spirit" (the header's own comment): set at death iff the map is
|
||
/// non-instanceable, i.e. exactly when the server will force-release after 6:00. The client
|
||
/// mirrors that constant for the DEATH popup's countdown (byte-VERIFIED — wow-re death-ui.md
|
||
/// §1: the client arms `now + 360000 ms` at its own alive→dead edge; bit clear ⇒
|
||
/// `GetReleaseTimeRemaining() == -1`).
|
||
pub fn player_release_timer_running(&self) -> bool {
|
||
self.get_u32(FIELD_PLAYER_FIELD_BYTES).unwrap_or(0) & 0x08 != 0
|
||
}
|
||
/// `PLAYER_FIELD_BYTES` byte 1 — the player's **combo points** (0..5; vmangos
|
||
/// `PLAYER_FIELD_BYTES_OFFSET_COMBO_POINTS`, written by `Player::SetComboPoints`). PRIVATE, so
|
||
/// it streams only for our own player — the only consumer. The client reads this byte at
|
||
/// `[player+0xe68]+0x1029` in the usable walk's leg 5 (the combo-point gate, decision 0869):
|
||
/// the same dword whose byte 3 is [`Self::player_honor_rank`]'s byte-verified `+0x102b`, off
|
||
/// the player-block base the duel-arbiter comment pins at `[player+0xe68]`.
|
||
///
|
||
/// **The byte is not rogue/druid-only — the *display* is** (decision 0875). vmangos writes one
|
||
/// here when a *warrior's* victim **dodges** (`Unit.cpp` `PROC_EX_DODGE` →
|
||
/// `AddComboPoints(target, 1)` plus a 4 s `REACTIVE_OVERPOWER` timer whose expiry
|
||
/// `ClearComboPoints`es it) — the server's chosen marker for "Overpower is up", and the only
|
||
/// way that window reaches the client. Leg 5 reads this byte with no class test, which is what
|
||
/// greys Overpower; the Lua `GetComboPoints 0x51a190` reads the *same* byte behind a
|
||
/// rogue-or-druid class gate, which is why a warrior never sees a combo dot light up for it.
|
||
/// `None` before the field streams.
|
||
pub fn player_combo_points(&self) -> Option<u8> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_BYTES)
|
||
.map(|b| ((b >> 8) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_FIELD_COMBO_TARGET` (field 714, GUID) — the unit the banked
|
||
/// [`Self::player_combo_points`] sit on, written by `Player::SetComboPoints` in the same breath
|
||
/// as the byte and zeroed with it. PRIVATE, so it only ever streams for our own player.
|
||
///
|
||
/// Combo points are **per target**, and the client enforces that in the *display*, not on the
|
||
/// wire: `GetComboPoints 0x51a190` reads this pair at `[player+0xe68]+0x838/+0x83c`, compares
|
||
/// it against the current-target GUID global (`0xb4e2d8/dc`) and pushes `0` on a mismatch
|
||
/// (decision 0875) — so re-targeting empties the dots without the count moving, and selecting
|
||
/// the original unit again refills them. `0` when nothing is banked.
|
||
pub fn player_combo_target(&self) -> u64 {
|
||
self.get_guid(FIELD_PLAYER_FIELD_COMBO_TARGET).unwrap_or(0)
|
||
}
|
||
/// `PLAYER_FIELD_BYTES` byte 2 — the **action-bar toggle** byte: which of the four extra bars
|
||
/// the player has switched on (vmangos `Player.h:360-363`
|
||
/// `PLAYER_FIELD_BYTES_OFFSET_ACTION_BARS = 2`, written by `HandleSetActionBarTogglesOpcode`'s
|
||
/// `SetByteValue(PLAYER_FIELD_BYTES, 2, packet.actionBar)`). PRIVATE — the field is only
|
||
/// *allocated* on our own player (self 1282 dwords vs 486 for any other player), which is why
|
||
/// the reference binding resolves the local GUID itself instead of taking a unit token.
|
||
///
|
||
/// Byte-VERIFIED (wow-re `system/ui/scratch/action-bar-toggles.md` §4): `GetActionBarToggles
|
||
/// 0x4e7660` reads it at `[[player+0xe68]+0x102a]`, and `0x2f0 + 0x1028 = 0x1318 = 1222·4`
|
||
/// closes the arithmetic against index 1222 — the same player-block base that puts
|
||
/// [`Self::player_combo_points`] at `+0x1029` and [`Self::player_honor_rank`] at `+0x102b`.
|
||
/// (vmangos's inline comment on the field reads `// 0x4C0`, six low; its own arithmetic
|
||
/// `UNIT_END + 0x40A` and the binary both say `0x4C6`. The comment is stale — do not copy it.)
|
||
///
|
||
/// **This is the ONLY source of the value: the client never writes the byte.** Displacement
|
||
/// `0x102a` occurs exactly once image-wide and it is that read; `SetActionBarToggles` stores
|
||
/// nothing outside its own stack frame and only posts `CMSG_SET_ACTIONBAR_TOGGLES`
|
||
/// ([`crate::messages::set_actionbar_toggles`]), so the cell moves solely through the generic
|
||
/// `SMSG_UPDATE_OBJECT` value-apply — and nothing is notified when it does (§4.2). A purely
|
||
/// field-driven UI therefore lags a round trip; the reference keeps its optimistic copy in Lua
|
||
/// and re-reads this exactly once, at `PLAYER_ENTERING_WORLD`.
|
||
///
|
||
/// Only bits `0x01`..`0x08` are meaningful to the client — the binding tests four and can set
|
||
/// four (§2/§5); the high nibble is never read and is destroyed by the next `Set`. Bit→bar is a
|
||
/// FrameXML convention, not the descriptor's, so it is not named here. `None` before the field
|
||
/// streams.
|
||
pub fn player_action_bar_toggles(&self) -> Option<u8> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_BYTES)
|
||
.map(|b| ((b >> 16) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_FIELD_BYTES` byte 3 — the player's **highest lifetime honor rank** (vmangos
|
||
/// `PLAYER_FIELD_BYTES_OFFSET_HIGHEST_HONOR_RANK`, written by `HonorMgr`): what the client's
|
||
/// item-usable gate compares an item's `RequiredHonorRank` against (`0x5ea930` reads the
|
||
/// values byte at `+0x102b`). `None` before the field streams; rank 0 = never ranked.
|
||
///
|
||
/// **Not [`Self::player_pvp_rank`]** — that is the *current* rank, a different byte of a
|
||
/// different (and PUBLIC) field, and the two diverge the moment a rank is lost: vmangos only
|
||
/// ever raises this one (`HonorMgr.cpp:894-895`, `SetHighestRank` iff the new visual rank is
|
||
/// strictly greater), so a demoted player keeps their old title here while the current rank
|
||
/// falls. The Honor pane shows the current rank; an item requirement reads this one.
|
||
pub fn player_honor_rank(&self) -> Option<u8> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_BYTES)
|
||
.map(|b| ((b >> 24) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES_3` byte 3 — the player's **current** honor rank (vmangos
|
||
/// `PLAYER_BYTES_3_OFFSET_HONOR_RANK`, written by `HonorMgr::Update`, `HonorMgr.cpp:900`).
|
||
/// The *internal* rank — `HonorRankInfo::rank`, whose own comment reads "internal range
|
||
/// [0..18]": 0 = unranked, 1..4 the negative ranks (Pariah…Dishonored), 5 up the positive ones
|
||
/// (Scout/Private…). It indexes the `PVP_RANK_<rank>_<team>` GlobalStrings directly; the
|
||
/// number the UI *draws* is the **visual** rank (`rank > 4 ? rank - 4 : -rank`,
|
||
/// `HonorMgr.cpp:991`), which is a derivation, not a field — do it at the display edge, not
|
||
/// here, and only once.
|
||
///
|
||
/// **This field is PUBLIC** (vmangos `UpdateFields_1_12_1.h:125`), the only honor value that
|
||
/// is: it streams for *every visible player*, not just for us. That is what makes a foreign
|
||
/// unit's rank answerable at all — the reference inspect pane's `UnitPVPRank("target")` reads
|
||
/// this byte off the target's own descriptor, with no packet involved. Everything else on the
|
||
/// Honor tab is PRIVATE and has to be requested
|
||
/// ([`crate::messages::read_inspect_honor_stats`]).
|
||
///
|
||
/// Contrast [`Self::player_honor_rank`], the *highest lifetime* rank in a PRIVATE field's
|
||
/// byte 3; [`Self::player_drunk_byte`] reads byte 1 of this same dword. `None` before the
|
||
/// field streams.
|
||
pub fn player_pvp_rank(&self) -> Option<u8> {
|
||
self.get_u32(FIELD_PLAYER_BYTES_3)
|
||
.map(|b| ((b >> 24) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES_3` **byte 2** — the **city-protector title**
|
||
/// (`PLAYER_BYTES_3_OFFSET_CITY_PROTECTOR_TITLE`, vmangos `Player.h:354`, whose own comment
|
||
/// says the value is a **race id**). PUBLIC, like [`Self::player_pvp_rank`] beside it in the
|
||
/// same dword.
|
||
///
|
||
/// Its one consumer is `UnitPVPName`, which appends `"\n"` plus the `PVP_MEDAL<n>`
|
||
/// GlobalString when the byte is non-zero (wow-re `system/ui/scratch/honor-panel-law.md`,
|
||
/// `0x609370` leg C) — the "Protector of Stormwind" line under a city defender's name.
|
||
///
|
||
/// **vmangos never writes it**, so it is 0 for every character on this server and the medal
|
||
/// line never renders. Decoded anyway: the field is real, the binding that reads it is real,
|
||
/// and a decoder that answered `None` here would make a live divergence look like an unbuilt
|
||
/// feature the day a server does write it. `None` before the field streams.
|
||
pub fn player_pvp_medal(&self) -> Option<u8> {
|
||
self.get_u32(FIELD_PLAYER_BYTES_3)
|
||
.map(|b| ((b >> 16) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_FIELD_SESSION_KILLS` — **today's** `(honorable, dishonorable)` kills, low half then
|
||
/// high half. The one honor kill counter vmangos genuinely writes as two shorts
|
||
/// (`HonorMgr.cpp:913-914`, `SetUInt16Value(…, 0, todayHK)` / `(…, 1, todayDK)`), so both
|
||
/// halves are live here. PRIVATE — self only; `None` before the field streams.
|
||
///
|
||
/// Despite the name the server recomputes it from *today's* honor-CP rows, not from
|
||
/// session start (`HonorMgr::Update`'s `itr.date == today` arm) — the reference's
|
||
/// `GetPVPSessionStats()` pair.
|
||
pub fn player_session_kills(&self) -> Option<(u16, u16)> {
|
||
self.get_u16_pair(FIELD_PLAYER_FIELD_SESSION_KILLS)
|
||
}
|
||
/// `PLAYER_FIELD_YESTERDAY_KILLS` — `(honorable, dishonorable)` kills for yesterday.
|
||
///
|
||
/// **The dishonorable half is always `0` from this server, and that is not a decode bug.**
|
||
/// The descriptor type really is TWO_SHORT and the real client really does read halves, but
|
||
/// vmangos writes the field as a whole dword (`HonorMgr.cpp:917`,
|
||
/// `SetUInt32Value(PLAYER_FIELD_YESTERDAY_KILLS, yesterdayKills)`) — so the high half never
|
||
/// receives the DK count. We decode the wire faithfully rather than collapsing the field to a
|
||
/// `u32`: the shape belongs to the descriptor, and a different server (or a fixed vmangos)
|
||
/// would fill it. Same for [`Self::player_last_week_kills`] and
|
||
/// [`Self::player_this_week_kills`]. PRIVATE — self only; `None` before the field streams.
|
||
pub fn player_yesterday_kills(&self) -> Option<(u16, u16)> {
|
||
self.get_u16_pair(FIELD_PLAYER_FIELD_YESTERDAY_KILLS)
|
||
}
|
||
/// `PLAYER_FIELD_LAST_WEEK_KILLS` — `(honorable, dishonorable)` kills for last week. The
|
||
/// high half is always `0` from vmangos (`HonorMgr.cpp:929` writes the whole dword); see
|
||
/// [`Self::player_yesterday_kills`]. PRIVATE — self only.
|
||
pub fn player_last_week_kills(&self) -> Option<(u16, u16)> {
|
||
self.get_u16_pair(FIELD_PLAYER_FIELD_LAST_WEEK_KILLS)
|
||
}
|
||
/// `PLAYER_FIELD_THIS_WEEK_KILLS` — `(honorable, dishonorable)` kills for the current week.
|
||
/// The high half is always `0` from vmangos (`HonorMgr.cpp:924` writes the whole dword); see
|
||
/// [`Self::player_yesterday_kills`]. PRIVATE — self only.
|
||
pub fn player_this_week_kills(&self) -> Option<(u16, u16)> {
|
||
self.get_u16_pair(FIELD_PLAYER_FIELD_THIS_WEEK_KILLS)
|
||
}
|
||
/// `PLAYER_FIELD_THIS_WEEK_CONTRIBUTION` — honor points earned this week (vmangos's
|
||
/// "contribution": the summed CP of the week's honor rows, floored at 0 —
|
||
/// `HonorMgr.cpp:925`). PRIVATE — self only; `None` before the field streams.
|
||
pub fn player_this_week_contribution(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_THIS_WEEK_CONTRIBUTION)
|
||
}
|
||
/// `PLAYER_FIELD_YESTERDAY_CONTRIBUTION` — honor points earned yesterday
|
||
/// (`HonorMgr.cpp:918`). PRIVATE — self only.
|
||
pub fn player_yesterday_contribution(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_YESTERDAY_CONTRIBUTION)
|
||
}
|
||
/// `PLAYER_FIELD_LAST_WEEK_CONTRIBUTION` — honor points earned last week
|
||
/// (`HonorMgr.cpp:930`). This is the figure the weekly rank calculation actually consumed.
|
||
/// PRIVATE — self only.
|
||
pub fn player_last_week_contribution(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_LAST_WEEK_CONTRIBUTION)
|
||
}
|
||
/// `PLAYER_FIELD_LAST_WEEK_RANK` — last week's **standing**, i.e. our numeric position on the
|
||
/// server-wide honor ladder for that week (vmangos `m_standing`, `HonorMgr.cpp:931`), NOT an
|
||
/// honor rank. `0` means "unranked that week". PRIVATE — self only.
|
||
pub fn player_last_week_rank(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_LAST_WEEK_RANK)
|
||
}
|
||
/// `PLAYER_FIELD_LIFETIME_HONORBALE_KILLS` (vmangos's spelling) — the lifetime honorable-kill
|
||
/// total (`HonorMgr.cpp:934`). PRIVATE — self only; `None` before the field streams.
|
||
pub fn player_lifetime_honorable_kills(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_LIFETIME_HONORABLE_KILLS)
|
||
}
|
||
/// `PLAYER_FIELD_LIFETIME_DISHONORBALE_KILLS` (vmangos's spelling) — the lifetime
|
||
/// dishonorable-kill total (`HonorMgr.cpp:935`). PRIVATE — self only.
|
||
pub fn player_lifetime_dishonorable_kills(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_LIFETIME_DISHONORABLE_KILLS)
|
||
}
|
||
/// `PLAYER_FIELD_BYTES2` byte 0 — the **honor rank progress bar**
|
||
/// (`PLAYER_FIELD_BYTES_2_OFFSET_HONOR_RANK_BAR`, vmangos `HonorMgr.cpp:905-909`):
|
||
///
|
||
/// ```text
|
||
/// honorBar = uint32(|rankPoints|);
|
||
/// honorBar = uint8(((honorBar - rank.minRP) / (rank.maxRP - rank.minRP))
|
||
/// * (rank.positive ? 255 : -255));
|
||
/// ```
|
||
///
|
||
/// so the byte is **how far through the CURRENT rank we are, scaled 0..255** — not an absolute
|
||
/// honor total, and it resets each time the rank changes (`minRP`/`maxRP` are that rank's own
|
||
/// bounds, `HonorMgr::CalculateRankInfo`). A *negative* rank multiplies by `-255`, and the
|
||
/// C++ cast of a negative float to `uint8` wraps rather than clamping, so the byte a demoted
|
||
/// player carries is the wrapped product. We carry the byte the wire sent; interpreting the
|
||
/// negative-rank case is the display edge's problem, not the decoder's.
|
||
///
|
||
/// The field only exists from client 1.6.1 on (vmangos guards the write with
|
||
/// `SUPPORTED_CLIENT_BUILD >= CLIENT_BUILD_1_6_1`), which is live for build 5875. PRIVATE —
|
||
/// self only; `None` before the field streams.
|
||
pub fn player_honor_rank_bar(&self) -> Option<u8> {
|
||
self.get_u32(FIELD_PLAYER_FIELD_BYTES2)
|
||
.map(|b| (b & 0xff) as u8)
|
||
}
|
||
/// `UNIT_FIELD_BYTES_0` byte 0 — the unit's race. `None` when the field wasn't sent.
|
||
pub fn unit_race(&self) -> Option<u8> {
|
||
self.unit_bytes_0().map(|b| (b & 0xff) as u8)
|
||
}
|
||
/// `UNIT_FIELD_BYTES_0` byte 1 — the unit's class.
|
||
pub fn unit_class(&self) -> Option<u8> {
|
||
self.unit_bytes_0().map(|b| ((b >> 8) & 0xff) as u8)
|
||
}
|
||
/// `UNIT_FIELD_BYTES_0` byte 2 — the unit's gender/sex.
|
||
pub fn unit_gender(&self) -> Option<u8> {
|
||
self.unit_bytes_0().map(|b| ((b >> 16) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES` byte 0 — skinColor. `None` for a non-player unit (no PLAYER block).
|
||
pub fn player_skin(&self) -> Option<u8> {
|
||
self.player_bytes().map(|b| (b & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES` byte 1 — faceType.
|
||
pub fn player_face(&self) -> Option<u8> {
|
||
self.player_bytes().map(|b| ((b >> 8) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES` byte 2 — hairStyle.
|
||
pub fn player_hair_style(&self) -> Option<u8> {
|
||
self.player_bytes().map(|b| ((b >> 16) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES` byte 3 — hairColor.
|
||
pub fn player_hair_color(&self) -> Option<u8> {
|
||
self.player_bytes().map(|b| ((b >> 24) & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES_2` byte 0 — facialHair.
|
||
pub fn player_facial_hair(&self) -> Option<u8> {
|
||
self.player_bytes_2().map(|b| (b & 0xff) as u8)
|
||
}
|
||
/// `PLAYER_BYTES_2` byte 2 — `PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS` (vmangos `Player.h:347`):
|
||
/// how many of the 6 purchasable bank bag slots are bought. Byte **2**, not byte 1 — the
|
||
/// server writes byte 0 facialHair, byte 1 unused, byte 2 bankBagSlots, byte 3 restState. The
|
||
/// only success signal for `CMSG_BUY_BANK_SLOT` (decision 0604): the server sends no packet on
|
||
/// success, only this descriptor delta (`PLAYERBANKBAGSLOTS_CHANGED` on the reference client).
|
||
pub fn player_bank_bag_slots_purchased(&self) -> Option<u8> {
|
||
self.player_bytes_2().map(|b| ((b >> 16) & 0xff) as u8)
|
||
}
|
||
/// `OBJECT_FIELD_CREATED_BY` (the GameObject block's creator guid, fields 6-7 in 1.12): the
|
||
/// summoning player of a spell-spawned object — the fishing bobber's owner. `None` when absent
|
||
/// or zero (a world-spawned GO has no creator). The reference's ONE reader on the interaction
|
||
/// path is the faction resolver `0x5f7fd0`, which prefers the creator's reaction over
|
||
/// `GAMEOBJECT_FACTION` — the bobber's ownership *gate* is NOT this field but the player's own
|
||
/// `UNIT_FIELD_CHANNEL_OBJECT` (wow-re `fishing-bobber-interaction.md` §2, the byte-proven
|
||
/// negative: `0x5f6710` never reads CREATED_BY).
|
||
pub fn gameobject_created_by(&self) -> Option<u64> {
|
||
self.get_guid(FIELD_GAMEOBJECT_CREATED_BY)
|
||
.filter(|&g| g != 0)
|
||
}
|
||
/// `GAMEOBJECT_DISPLAYID`.
|
||
pub fn gameobject_displayid(&self) -> Option<i32> {
|
||
self.get_i32(FIELD_GAMEOBJECT_DISPLAYID)
|
||
}
|
||
/// `GAMEOBJECT_STATE` — 0 active (door open/triggered), 1 ready (closed/armed), 2 active-alt
|
||
/// (vmangos `GOState`, `OBJECT_END + 0x8` in 1.12). Drives the open/close sound slots.
|
||
pub fn gameobject_state(&self) -> Option<u32> {
|
||
self.get_u32(FIELD_GAMEOBJECT_STATE)
|
||
}
|
||
/// `GAMEOBJECT_ROTATION` — the spawn's local-rotation quaternion `(x, y, z, w)`. `None` when
|
||
/// the wire carried no component at all (vmangos omits zero fields from the create mask, and
|
||
/// an identity-ish quat still sends its nonzero `w`/`z`); absent components within a
|
||
/// partially-sent quat read as the create-block zero default, so a pure-yaw spawn
|
||
/// (`x = y = 0`, the live-data norm) round-trips exactly. This is the field a GameObject is
|
||
/// *placed* by — the reference builds its render matrix from this quaternion and never from
|
||
/// `GAMEOBJECT_FACING` (decision 1459) — and the basis a type-11 lift rotates its keyframe
|
||
/// offsets through.
|
||
pub fn gameobject_rotation(&self) -> Option<[f32; 4]> {
|
||
let any_sent = (0..4u16).any(|i| self.contains(FIELD_GAMEOBJECT_ROTATION + i));
|
||
any_sent.then(|| {
|
||
[
|
||
self.get_f32(FIELD_GAMEOBJECT_ROTATION).unwrap_or(0.0),
|
||
self.get_f32(FIELD_GAMEOBJECT_ROTATION + 1).unwrap_or(0.0),
|
||
self.get_f32(FIELD_GAMEOBJECT_ROTATION + 2).unwrap_or(0.0),
|
||
self.get_f32(FIELD_GAMEOBJECT_ROTATION + 3).unwrap_or(0.0),
|
||
]
|
||
})
|
||
}
|
||
/// GameObject position + facing (`GAMEOBJECT_POS_*` / `GAMEOBJECT_FACING`). On a create block this
|
||
/// is `Some` **even when none of the three were sent** — the created-store zero-fill
|
||
/// (`get_f32`/`get_u32`'s `created.then_some(0)`) can't tell "genuinely at the origin" from "never
|
||
/// carried a position at all" (a transport GameObject, whose stationary spot rides the movement
|
||
/// block's `HAS_POSITION` slot instead — decision 0438 "the wire"). [`Self::gameobject_pos_sent`]
|
||
/// answers that distinction; the create-placement fallback (`events/decode.rs`) gates on it.
|
||
pub fn gameobject_position(&self) -> Option<(Vector3d, f32)> {
|
||
Some((
|
||
Vector3d {
|
||
x: self.get_f32(FIELD_GAMEOBJECT_POS_X)?,
|
||
y: self.get_f32(FIELD_GAMEOBJECT_POS_Y)?,
|
||
z: self.get_f32(FIELD_GAMEOBJECT_POS_Z)?,
|
||
},
|
||
self.get_f32(FIELD_GAMEOBJECT_FACING).unwrap_or(0.0),
|
||
))
|
||
}
|
||
/// Whether the wire genuinely carried **any** `GAMEOBJECT_POS_*` field — a raw presence probe on purpose
|
||
/// (like `get_guid`'s, bypassing the create block's absent-is-zero fold) so a consumer can tell
|
||
/// "this GO's create never carried a position" from "it did, and it's `0.0` on some axis". A normal
|
||
/// GameObject sends at least one non-zero axis virtually always; a boat/zeppelin/elevator sends
|
||
/// **none** (vmangos never sets `GAMEOBJECT_POS_*` on a transport).
|
||
pub fn gameobject_pos_sent(&self) -> bool {
|
||
self.contains(FIELD_GAMEOBJECT_POS_X)
|
||
|| self.contains(FIELD_GAMEOBJECT_POS_Y)
|
||
|| self.contains(FIELD_GAMEOBJECT_POS_Z)
|
||
}
|
||
/// `GAMEOBJECT_TYPE_ID` — the GameObject sub-type (DOOR/CHEST/SPELL_FOCUS/…). **Absent ⇒ `0` =
|
||
/// DOOR**, not "unknown": vmangos omits a zero-valued field from the create mask (the same reason an
|
||
/// all-zero `PLAYER_BYTES` is never sent), and `GAMEOBJECT_TYPE_DOOR` *is* 0 — so a door sends no type
|
||
/// field at all, and the absent value is its default. Reading it as `None` silently mis-handled every
|
||
/// door (no interact cursor, no open/close animation), so this defaults to the wire default.
|
||
pub fn gameobject_type_id(&self) -> i32 {
|
||
self.get_i32(FIELD_GAMEOBJECT_TYPE_ID).unwrap_or(0)
|
||
}
|
||
/// `GAMEOBJECT_FLAGS` (`GameObjectFlags`) — static interactability bits: `0x1` IN_USE (busy),
|
||
/// `0x4` INTERACT_COND (usable only when the per-player `GAMEOBJECT_DYN_FLAGS` activate bit is set —
|
||
/// the quest gate), `0x10` NO_INTERACT. Read by the cursor/interact usability gate (decision 0243).
|
||
pub fn gameobject_flags(&self) -> u32 {
|
||
self.get_u32(FIELD_GAMEOBJECT_FLAGS).unwrap_or(0)
|
||
}
|
||
/// `GAMEOBJECT_FACTION` — the object's `FactionTemplate.dbc` id, or `0` for "no faction"
|
||
/// (which the reference resolves as NEUTRAL). The highlightable predicate's faction term reads
|
||
/// exactly this (`[go+0x110]+0x38`, decision 0764): a GameObject whose template is **hostile to
|
||
/// the player** is not highlightable, so it shows no cursor, no tooltip and no hover brighten.
|
||
pub fn gameobject_faction(&self) -> u32 {
|
||
self.get_u32(FIELD_GAMEOBJECT_FACTION).unwrap_or(0)
|
||
}
|
||
/// `GAMEOBJECT_DYN_FLAGS` — the **per-player** dynamic flags; bit `0x1` = `GO_DYNFLAG_LO_ACTIVATE`
|
||
/// (the sparkle / "usable for me now"), which the server sets from `GameObject::ActivateToQuest`.
|
||
/// Consulted only when `GAMEOBJECT_FLAGS & INTERACT_COND` is set (decision 0243).
|
||
pub fn gameobject_dynamic_flags(&self) -> u32 {
|
||
self.get_u32(FIELD_GAMEOBJECT_DYN_FLAGS).unwrap_or(0)
|
||
}
|
||
/// `GAMEOBJECT_LEVEL` — the object's level (vmangos `UpdateFields_1_12_1.h`: `OBJECT_END + 0x10`
|
||
/// = 22). The lock-refusal toast's required-rank fallback reads it: a skill lock whose
|
||
/// `Skill[0]` is 0 displays `level × 5` instead (wow-re cursor-system.md §8.8, `0x5f3490`).
|
||
/// `0` when absent (the create-block zero default).
|
||
pub fn gameobject_level(&self) -> u32 {
|
||
self.get_u32(FIELD_GAMEOBJECT_LEVEL).unwrap_or(0)
|
||
}
|
||
|
||
/// The object's class from the type field (`OBJECT_FIELD_TYPE`), used for `Values`/`Movement`
|
||
/// updates that carry no separate `TypeId`. Unused by the create path (which has a `TypeId`).
|
||
#[allow(dead_code)]
|
||
pub fn object_type(&self) -> Option<ObjectType> {
|
||
self.get_u32(FIELD_OBJECT_TYPE).map(|bits| {
|
||
if bits & 0x10 != 0 {
|
||
ObjectType::Player
|
||
} else if bits & 0x08 != 0 {
|
||
ObjectType::Unit
|
||
} else if bits & 0x20 != 0 {
|
||
ObjectType::GameObject
|
||
} else {
|
||
ObjectType::Object
|
||
}
|
||
})
|
||
}
|
||
}
|