diff --git a/crates/benilla-app/src/ui_chat/tests.rs b/crates/benilla-app/src/ui_chat/tests.rs index 27c39408..9d5e22f4 100644 --- a/crates/benilla-app/src/ui_chat/tests.rs +++ b/crates/benilla-app/src/ui_chat/tests.rs @@ -2288,31 +2288,9 @@ fn level_up_gains_are_matched_by_level_and_a_miss_is_not_an_absence() { ); } -/// Stock `ChatFrame_OnEvent` prints the whole level-up block from `PLAYER_LEVEL_UP` -/// (`ChatFrame.lua:1283-1323`); the app composes none of it. -#[test] -fn the_ding_block_is_printed_once() { - let _data = benilla_formats::wow_data_or_skip!(); - use benilla_protocol::messages::LevelUpInfo; - let mut s = chat_vm(); - let mut log = super::ChatLog::default(); - - let info = LevelUpInfo { - level: 10, - health: 22, - powers: [15, 0, 0, 0, 0], - stats: [1, 0, 0, 0, 0], - }; - // The packet's apply parks the gains and prints nothing. - log.push_level_up_gains(&info, 1); - let before = lines_in_window(&s); - assert_eq!( - lines_in_window(&s) - before, - 0, - "the app composes no ding line of its own" - ); - - // Tap the window's `AddMessage` to read the block itself. +/// Fires `PLAYER_LEVEL_UP` with the nine reference args and returns every line the stock handler +/// added to `ChatFrame1`. +fn ding_lines(s: &mut benilla_ui::script::UiScript, args: [i64; 9]) -> Vec { s.run( r#" DingLines = {} @@ -2325,23 +2303,27 @@ fn the_ding_block_is_printed_once() { ) .unwrap(); - // The event `ui_unit` fires, with the reference's nine arguments. - let args: Vec = [10i64, 22, 15, 1, 1, 0, 0, 0, 0] + let args = args .into_iter() .map(benilla_ui::script::ScriptValue::Int) .collect(); s.fire_event("PLAYER_LEVEL_UP", args); assert!(s.errors().is_empty(), "handler errors: {:?}", s.errors()); - assert_eq!( - lines_in_window(&s) - before, - 4, - "LEVEL_UP, the health/mana pair, CHAR_POINTS, and one STAT — once each" - ); - // The singular `LEVEL_UP_CHAR_POINTS` (`GetText`'s plural pick) and one `LEVEL_UP_STAT`. - let lines: Vec = (1..=4) + let n: usize = s.eval("return table.getn(DingLines)").unwrap(); + (1..=n) .map(|i| s.eval::(&format!("return DingLines[{i}]")).unwrap()) - .collect(); + .collect() +} + +/// Stock `ChatFrame_OnEvent` prints the whole level-up block from `PLAYER_LEVEL_UP` +/// (`ChatFrame.lua:1283-1323`), once; the app composes none of it. The singular +/// `LEVEL_UP_CHAR_POINTS` is `GetText`'s plural pick. +#[test] +fn the_ding_block_is_printed_once() { + let _data = benilla_formats::wow_data_or_skip!(); + let mut s = chat_vm(); + let lines = ding_lines(&mut s, [10, 22, 15, 1, 1, 0, 0, 0, 0]); assert_eq!( lines, [ @@ -2353,6 +2335,22 @@ fn the_ding_block_is_printed_once() { ); } +/// A loss reaches stock signed, as the reference's `%d` fire passes it (`0x5e413e`): the health +/// line prints it, and a lost stat prints no line (`if ( argN > 0 )`, `ChatFrame.lua:1302-1321`). +#[test] +fn a_negative_gain_prints_signed() { + let _data = benilla_formats::wow_data_or_skip!(); + let mut s = chat_vm(); + let lines = ding_lines(&mut s, [2, -2, 5, 0, -1, 0, 0, 0, 0]); + assert_eq!( + lines, + [ + "Congratulations, you have reached level 2!", + "You have gained -2 hit points and 5 mana.", + ] + ); +} + /// On `CHARACTER_POINTS_CHANGED` with `arg2 > 0`, stock `ChatFrame_OnEvent` prints /// `LEVEL_UP_SKILL_POINTS` (`ChatFrame.lua:1324-1334`). #[test] diff --git a/crates/benilla-app/src/ui_unit.rs b/crates/benilla-app/src/ui_unit.rs index 4fdaf27c..df12b421 100644 --- a/crates/benilla-app/src/ui_unit.rs +++ b/crates/benilla-app/src/ui_unit.rs @@ -1444,7 +1444,7 @@ fn feed_units( // `ChatFrame.lua:1283-1320` reads them; missing gains are zeros, since stock's // `if ( argN > 0 )` raises on nil. let (info, talent_points) = sink.chat.take_level_up_gains(level).unzip(); - let gain = |f: fn(&benilla_protocol::messages::LevelUpInfo) -> u32| { + let gain = |f: fn(&benilla_protocol::messages::LevelUpInfo) -> i32| { ScriptValue::Int(i64::from(info.as_ref().map_or(0, f))) }; script.fire_event( diff --git a/crates/benilla-protocol/src/messages/progression.rs b/crates/benilla-protocol/src/messages/progression.rs index 8ba7843c..82da9e0f 100644 --- a/crates/benilla-protocol/src/messages/progression.rs +++ b/crates/benilla-protocol/src/messages/progression.rs @@ -2,7 +2,7 @@ use std::io::{self, Read}; -use crate::wire::{read_f32_le, read_u32_le, read_u64_le, read_u8}; +use crate::wire::{read_f32_le, read_i32_le, read_u32_le, read_u64_le, read_u8}; /// `SMSG_LOG_XPGAIN`: an XP award; `victim` is 0 for non-kill XP (vmangos `Misc.cpp:512-522`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -92,27 +92,29 @@ pub fn talent_wipe_confirm(trainer_guid: u64) -> Vec { pub struct LevelUpInfo { /// The level just reached. pub level: u32, - /// Hit points gained. - pub health: u32, + /// Hit points gained, negative on a loss. + pub health: i32, /// Power gains: mana, rage, focus, energy, happiness. The server fills only mana, which is /// the reference chat line's mana argument. - pub powers: [u32; 5], + pub powers: [i32; 5], /// Stat gains in `SPELL_STAT0..4` order (str, agi, stam, int, spirit), one chat line per /// positive entry. - pub stats: [u32; 5], + pub stats: [i32; 5], } -/// Read `SMSG_LEVELUP_INFO`: `u32` level, health, 5 powers, 5 stats (`Misc.cpp:524-532`). +/// Read `SMSG_LEVELUP_INFO`: `u32` level, `i32` health, 5 powers, 5 stats (`Misc.cpp:524-532`). +/// The reference fires the gains as `%d` (`0x5e413e`), so a loss the server wraps into its `u32` +/// (vmangos `Player.cpp:3203-3215`) reads negative. pub(super) fn read_level_up_info(r: &mut impl Read) -> io::Result { let level = read_u32_le(r)?; - let health = read_u32_le(r)?; - let mut powers = [0u32; 5]; + let health = read_i32_le(r)?; + let mut powers = [0; 5]; for p in &mut powers { - *p = read_u32_le(r)?; + *p = read_i32_le(r)?; } - let mut stats = [0u32; 5]; + let mut stats = [0; 5]; for s in &mut stats { - *s = read_u32_le(r)?; + *s = read_i32_le(r)?; } Ok(LevelUpInfo { level, diff --git a/crates/benilla-protocol/tests/spells.rs b/crates/benilla-protocol/tests/spells.rs index 232d4a1f..2843eff8 100644 --- a/crates/benilla-protocol/tests/spells.rs +++ b/crates/benilla-protocol/tests/spells.rs @@ -1003,8 +1003,8 @@ fn combat_log_wire_golden() { other => panic!("exploration xp event, got {other:?}"), } - // SMSG_LEVELUP_INFO: twelve u32, level, healthGain, powerGains[5] (mana..happiness) and - // statGains[5] (str..spirit), no guid (vmangos `Misc.cpp:524-532`). + // SMSG_LEVELUP_INFO: u32 level, then the i32 gains, healthGain, powerGains[5] (mana..happiness) + // and statGains[5] (str..spirit), no guid (vmangos `Misc.cpp:524-532`). let mut body = Vec::new(); for v in [7u32, 22, 15, 0, 0, 0, 0, 1, 0, 1, 2, 1] { body.extend_from_slice(&v.to_le_bytes()); @@ -1029,6 +1029,34 @@ fn combat_log_wire_golden() { SessionEvent::LevelUp(l) => assert_eq!((l.level, l.health, l.powers[0]), (7, 22, 15)), other => panic!("level up event, got {other:?}"), } + + // A loss the server wraps into its u32 reads negative, as the reference's `%d` fire prints it. + let mut body = Vec::new(); + for v in [ + 7u32, 0xFFFFFFFE, 0xFFFFFFFD, 0, 0, 0, 0, 0xFFFFFFFE, 0, 0xFFFFFFFB, 0xFFFFFFFA, 0xFFFFFFF9, + ] { + body.extend_from_slice(&v.to_le_bytes()); + } + assert_eq!(body.len(), 48); + let packet = messages::parse_server(messages::opcode::SMSG_LEVELUP_INFO, &body).unwrap(); + match &packet { + ServerPacket::LevelUp(l) => { + assert_eq!( + l, + &LevelUpInfo { + level: 7, + health: -2, + powers: [-3, 0, 0, 0, 0], + stats: [-2, 0, -5, -6, -7], + } + ); + } + other => panic!("level up, got {}", other.name()), + } + match decode(packet).pop().unwrap() { + SessionEvent::LevelUp(l) => assert_eq!((l.level, l.health, l.powers[0]), (7, -2, -3)), + other => panic!("level up event, got {other:?}"), + } } /// Raw caster guid, u32 spellId, u32 count, raw guids (vmangos `Spell.cpp:4974-4997`); in the