//! The melee swing refusal: what the client does with the server's answers to a //! `CMSG_ATTACKSWING` it will not honour. //! //! Six opcodes, four arms: `0x145` NOTINRANGE and `0x146` BADFACING latch a message; `0x148` //! DEADTARGET, `0x149` CANT_ATTACK and `0x14e` `SMSG_CANCEL_COMBAT` (handler `0x5e7dd0`, whose //! body is arm 4's byte for byte) stop the attack silently; `0x147` NOTSTANDING is never //! registered, and vmangos never sends it. //! //! The server sends a refusal only on its edge (vmangos `Unit.cpp:406-472`), so the repeat the //! player sees is the client's own latch and cooldown: //! //! ```text //! 0x625a8a (0x145 NOTINRANGE) ecx = 1 ; call 0x5ecdb0 ; latch code 1 //! 0x625aa1 (0x146 BADFACING) ecx = 2 ; call 0x5ecdb0 ; latch code 2 //! 0x625ab8 (0x148 DEADTARGET / 0x149 CANT_ATTACK) ; no message, StopAttack only //! //! 0x5ecdb0 [0xc4d758] = code ; [0xc4c270] = OsGetAsyncTimeMs() ; the code + "show at" //! //! 0x5ec950 the local player's tick: //! this is the active player (0x468550) else out //! [this+0xc48] != 0 (0x47bf60) else out ; an attack target is set //! now - [0xc4c270] >= 0 (5ec999 js) else out //! [0xc4d758] == 1 -> DisplayError(0xd8) | == 2 -> DisplayError(0xd7) | else out //! [0xc4c270] = now + 0xfa0 ; re-arm, 4000 ms //! ``` //! //! So the first line shows on the next tick and repeats every 4 s while the latch stands. A //! landed swing clears it: `SMSG_ATTACKERSTATEUPDATE`'s arm calls `0x5ea800` (at `0x6259b6`) when //! the attacker is the active player and the victim resolves, and its first act is `0x5ecdb0(0)`. //! A second refusal rewrites the deadline to now, defeating the cooldown, and the re-arm precedes //! the print (`0x5ec9bc`, `0x5ec9c2`), so it happens even when the string is missing. Both edges //! ride one message ([`SwingRefusalEdge`]) so they are consumed in packet order. use std::time::Duration; use benilla_protocol::messages::AttackSwingError; use bevy::prelude::*; use crate::creature_anim::{AttackSeam, Engaged}; use crate::net::SelfPlayer; use crate::ui_action::{UiError, UiErrorKeys}; /// The reference's re-arm interval (`0x5ec9b6`). const REPEAT: Duration = Duration::from_millis(0xfa0); /// One edge of the swing-refusal seam, off the net drain in packet order. #[derive(Message, Clone, Copy, Debug)] pub(crate) enum SwingRefusalEdge { /// The server refused our swing (`0x145`/`0x146`/`0x148`/`0x149`). Refused(AttackSwingError), /// `SMSG_CANCEL_COMBAT` (`0x14e`): the server forced our attack to stop (`Unit::CombatStop`, /// `Unit::StopAttackFaction`, `Unit::InterruptAttacksOnMe`, a resisted feign death). CombatCancelled, /// One of our swings landed on a victim we can see (`0x6259b6`). The write site applies both /// gates: the attacker is the local player (`0x5fa6d0`) and the victim resolves as a unit /// (`0x468460(8)`); benilla checks presence only, which is equivalent since the victim is /// always a unit or player. Landed, } /// The latched refusal: the reference's `[0xc4d758]` (the code) and `[0xc4c270]` (the absolute /// time the next line is due). `None` is code 0, whose timestamp the tick never reads. #[derive(Resource, Default)] struct SwingRefusal(Option<(Latched, Duration)>); /// The two refusals that latch; arm 4 writes no latch. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Latched { /// Code 1: `DisplayError(0xd8)`, `ERR_BADATTACKPOS`. NotInRange, /// Code 2: `DisplayError(0xd7)`, `ERR_BADATTACKFACING`. BadFacing, } impl Latched { /// The `GlobalStrings.lua` key the reference's error id resolves to. const fn key(self) -> &'static str { match self { Self::NotInRange => "ERR_BADATTACKPOS", Self::BadFacing => "ERR_BADATTACKFACING", } } } impl SwingRefusal { /// `0x5ecdb0(code)`: latch the refusal, due now. fn latch(&mut self, code: Latched, now: Duration) { self.0 = Some((code, now)); } /// `0x5ecdb0(0)`. fn clear(&mut self) { self.0 = None; } } /// Consume this frame's edges in packet order: latch a refusal, clear on a landing, and stop the /// attack for the silent arm. fn apply_swing_refusals( mut edges: MessageReader, time: Res