Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions gateway/src/codec/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub struct EntityView {
/// gear-folded value so the operator sees real worn armor on relog (auras self-correct via the on_aura
/// relay). Matches the module's combat `effective_armor`, so the sheet equals the mitigation value.
pub effective_armor: u32,
/// Holy, Fire, Nature, Frost, Shadow and Arcane resistance in descriptor order.
pub magic_resistances: [u32; 6],
/// Persisted hearthstone / bind-point from `game_character.home_*`. Zero-initialised for
/// creature rows (which never build a `SMSG_BINDPOINTUPDATE`).
pub home_map: u32,
Expand Down Expand Up @@ -362,6 +364,12 @@ pub fn build_create_object(
// them the instant they insert. Part of the full CREATE mask (OBJECT_FIELD_TYPE belongs here),
// so no dirty_reset concern.
.set_unit_normal_resistance(entity.effective_armor as i32)
.set_unit_holy_resistance(entity.magic_resistances[0] as i32)
.set_unit_fire_resistance(entity.magic_resistances[1] as i32)
.set_unit_nature_resistance(entity.magic_resistances[2] as i32)
.set_unit_frost_resistance(entity.magic_resistances[3] as i32)
.set_unit_shadow_resistance(entity.magic_resistances[4] as i32)
.set_unit_arcane_resistance(entity.magic_resistances[5] as i32)
// Weapon damage for the character sheet's Main-Hand panel. gtker's UpdatePlayer has no
// default for these float/int fields, so leaving them unset makes the 5875 client read
// uninitialized floats → "-1.#IND" (NaN) in the damage/DPS tooltip. Send the derived values.
Expand Down
37 changes: 25 additions & 12 deletions gateway/src/codec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,13 @@ fn health_values_update_is_health_only_no_object_type() {

#[test]
fn resistance_values_is_unit_only_no_object_type() {
// Live armor (Demon Skin / gear): the EFFECTIVE-armor relay must be a Unit VALUES mask carrying ONLY
// UNIT_FIELD_RESISTANCES[0] (descriptor index 155 → bit 27 of mask block 4 → word 0x08000000), never
// Live armor and magic resistance must use a Unit VALUES mask carrying only
// UNIT_FIELD_RESISTANCES[0..=6], never
// re-sending OBJECT_FIELD_TYPE (bit 2, value 0x09 = OBJECT|UNIT) whose re-send strips the PLAYER bit
// → 5875 null+0x110 crash. Same dirty_reset discipline as build_health_values.
let guid = (0xF130u64 << 48) | (620u64 << 24) | 1;
let armor = 0x1234u32; // 4660 — a recognizable LE pattern distinct from any mask word
let msg = build_resistance_values(guid, armor);
let resistances = [0x1234u32, 1, 2, 3, 4, 5, 6];
let msg = build_resistance_values(guid, resistances);
match &msg.objects[0] {
Object::Values { guid1, mask1 } => {
assert_eq!(guid1.guid(), guid);
Expand All @@ -250,23 +250,28 @@ fn resistance_values_is_unit_only_no_object_type() {
}
let mut buf = Vec::new();
msg.write_unencrypted_server(&mut buf).unwrap();
// The update mask is the LAST thing written: block_count=5 (descriptor 155 lives in block 4), then 5
// mask words (only block 4 set = 0x08000000 = bit 27), then the armor value LE. A leaked
// The update mask is the last thing written: descriptors 155..=161 span blocks 4 and 5. A leaked
// OBJECT_FIELD_TYPE would set bit 2 in block 0 and add a 0x09 value word ahead of the armor word, so
// this exact-tail assertion fails in that case (guarding the crash regression).
assert!(
buf.ends_with(&[
0x05, // block_count = 5 (the mask spans blocks 0..=4)
0x06, // block_count = 6 (the mask spans blocks 0..=5)
0, 0, 0, 0, // block 0 (NO OBJECT_FIELD_TYPE bit 2)
0, 0, 0, 0, // block 1
0, 0, 0, 0, // block 2
0, 0, 0, 0, // block 3
0x00, 0x00, 0x00,
0x08, // block 4 = bit 27 only (descriptor 155 = UNIT_FIELD_RESISTANCES[0])
0x00, 0x00, 0x00, 0xf8, // block 4 = descriptors 155..=159
0x03, 0x00, 0x00, 0x00, // block 5 = descriptors 160..=161
0x34, 0x12, 0x00, 0x00, // armor value 0x1234 LE
1, 0, 0, 0, // holy
2, 0, 0, 0, // fire
3, 0, 0, 0, // nature
4, 0, 0, 0, // frost
5, 0, 0, 0, // shadow
6, 0, 0, 0, // arcane
]),
"resistance VALUES must carry only index 155 (no OBJECT_FIELD_TYPE); tail was {:02x?}",
&buf[buf.len().saturating_sub(28)..]
"resistance VALUES must carry only indices 155..=161; tail was {:02x?}",
&buf[buf.len().saturating_sub(56)..]
);
// Belt-and-suspenders: the OBJECT_FIELD_TYPE value word (0x09 = OBJECT|UNIT) must be absent anywhere.
let contains = |needle: &[u8]| buf.windows(needle.len()).any(|w| w == needle);
Expand Down Expand Up @@ -1098,7 +1103,9 @@ fn warrior_create_carries_skills_and_serializes() {
// without Fury the client throws SpellBookFrame.lua:342 "invalid spell slot". The existing
// CREATE tests only pattern-match, so SERIALIZE here to actually run the writer over the new
// PLAYER_SKILL_INFO words (a malformed skill mask would otherwise slip past the suite).
let msg = build_create_object(&warrior_entity(), CreateKind::SelfPlayer, &[], &[]).unwrap();
let mut entity = warrior_entity();
entity.magic_resistances = [1, 2, 3, 4, 5, 6];
let msg = build_create_object(&entity, CreateKind::SelfPlayer, &[], &[]).unwrap();
let mut buf = Vec::new();
msg.write_unencrypted_server(&mut buf).unwrap();
assert!(!buf.is_empty());
Expand All @@ -1123,6 +1130,12 @@ fn warrior_create_carries_skills_and_serializes() {
// EFFECTIVE armor (UNIT_FIELD_RESISTANCES[0]) is read from `entity.effective_armor` (base +
// worn gear, folded at the self-login call site; the fixture has no gear → effective == base).
assert_eq!(p.unit_normal_resistance(), Some(e.effective_armor as i32));
assert_eq!(p.unit_holy_resistance(), Some(1));
assert_eq!(p.unit_fire_resistance(), Some(2));
assert_eq!(p.unit_nature_resistance(), Some(3));
assert_eq!(p.unit_frost_resistance(), Some(4));
assert_eq!(p.unit_shadow_resistance(), Some(5));
assert_eq!(p.unit_arcane_resistance(), Some(6));
}
other => panic!("expected a Player CreateObject2, got {other:?}"),
}
Expand Down
45 changes: 24 additions & 21 deletions gateway/src/codec/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,24 +303,18 @@ pub fn build_max_vitals_values(
})
}

/// Build a VALUES partial-update carrying `UNIT_FIELD_RESISTANCES[0]` — the "Armor" slot (descriptor
/// index 155, read by the paperdoll via `UnitArmor`) — so the character sheet's Armor readout moves LIVE
/// when an `A_MOD_RESISTANCE(armor)` aura applies/expires (e.g. Demon Skin) or armor gear is
/// equipped/unequipped. `armor` is the gateway-computed EFFECTIVE armor (base + armor auras + gear; see
/// `stdb::armor::effective_armor`), which equals the module's combat `effective_armor` — so the sheet
/// shows exactly what physical mitigation uses. UNIT mask (armor is a UNIT field, like max-vitals) + the
/// same `dirty_reset` discipline as `build_health_values` so the wire carries ONLY index 155 and never
/// re-sends OBJECT_FIELD_TYPE (the 5875 null+0x110 crash trap).
/// Build a VALUES partial-update carrying armor WITH its green "(+N)" split:
/// `UNIT_FIELD_RESISTANCES[0]` = the effective TOTAL and `PLAYER_FIELD_RESISTANCEBUFFMODSPOSITIVE[0]`
/// (index 1187, PLAYER block) = the positive AURA portion the paperdoll colors green.
/// TWO wrong guesses preceded this (live-found white armor both rounds): the buff-mods fields are
/// NOT unit fields in 1.12 — unit-space 162 is UNIT_FIELD_BASE_MANA — they live in the PLAYER
/// block, where gtker HAS a typed int setter. Player mask (self-only push; only your own sheet
/// reads it), same dirty_reset discipline as every partial VALUES.
pub fn build_armor_values(guid: u64, total: u32, pos_buff: u32) -> SMSG_UPDATE_OBJECT {
/// Build a self-only VALUES update for all seven effective resistances. Armor also carries its positive
/// aura split in `PLAYER_FIELD_RESISTANCEBUFFMODSPOSITIVE[0]` so the paperdoll colors that term green.
/// This uses the same `dirty_reset` path as every partial VALUES update.
pub fn build_armor_values(guid: u64, total: [u32; 7], pos_buff: u32) -> SMSG_UPDATE_OBJECT {
player_values(guid, |p| {
p.set_unit_normal_resistance(total as i32);
p.set_unit_normal_resistance(total[0] as i32);
p.set_unit_holy_resistance(total[1] as i32);
p.set_unit_fire_resistance(total[2] as i32);
p.set_unit_nature_resistance(total[3] as i32);
p.set_unit_frost_resistance(total[4] as i32);
p.set_unit_shadow_resistance(total[5] as i32);
p.set_unit_arcane_resistance(total[6] as i32);
p.set_player_field_resistancebuffmodspositive(pos_buff as i32);
})
}
Expand Down Expand Up @@ -396,9 +390,15 @@ pub fn build_sheet_stats_values(guid: u64, s: &SheetStatsValues) -> SMSG_UPDATE_
})
}

pub fn build_resistance_values(guid: u64, armor: u32) -> SMSG_UPDATE_OBJECT {
pub fn build_resistance_values(guid: u64, total: [u32; 7]) -> SMSG_UPDATE_OBJECT {
unit_values(guid, |unit| {
unit.set_unit_normal_resistance(armor as i32);
unit.set_unit_normal_resistance(total[0] as i32);
unit.set_unit_holy_resistance(total[1] as i32);
unit.set_unit_fire_resistance(total[2] as i32);
unit.set_unit_nature_resistance(total[3] as i32);
unit.set_unit_frost_resistance(total[4] as i32);
unit.set_unit_shadow_resistance(total[5] as i32);
unit.set_unit_arcane_resistance(total[6] as i32);
})
}

Expand Down Expand Up @@ -640,8 +640,11 @@ mod lint_tests {
("power", build_power_values(g, 0, 55)),
("target", build_target_values(g, 0xF130_0000_0000_0001)),
("max_vitals", build_max_vitals_values(g, 100, 0, 200)),
("armor", build_armor_values(g, 60, 20)),
("resistance", build_resistance_values(g, 40)),
("armor", build_armor_values(g, [60, 0, 0, 0, 0, 0, 0], 20)),
(
"resistance",
build_resistance_values(g, [40, 0, 0, 0, 0, 0, 0]),
),
("coinage", build_coinage_values(g, 900)),
("ammo", build_player_ammo_id_values(g, 2512)),
(
Expand Down
Loading