dialogs come to the front, and the red line names what it needs

- dialogs carry the reference's toplevel bit, so the stack-split window opens above Bagnon instead of behind it
- the failure line fills its blank — "Requires Blacksmith Hammer", not "Requires" — and Opening casts stop printing a label the spell itself marks "no text"
- the pet bar's Attack/Follow/Stay tooltips name their keybinding, and typed text reaches the focused frame through the client's own delivery walk
- AtlasLoot parses again: our Lua keeps 5.0's compatibility semicolon

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-14 19:38:18 +07:00
co-authored by Claude Fable 5
parent 606a72bde5
commit 2c1ffca2ca
57 changed files with 2163 additions and 275 deletions
@@ -436,8 +436,11 @@ impl World {
spell_id,
success,
reason,
arg,
} => {
println!("SMSG_CAST_RESULT: spell {spell_id} success={success} reason={reason:?}");
println!(
"SMSG_CAST_RESULT: spell {spell_id} success={success} reason={reason:?} arg={arg:?}"
);
if self.dest_spell == Some(*spell_id) {
self.dest_verdict = Some((*spell_id, *success, *reason));
} else if self.cast_verdict.is_none() {
+5 -1
View File
@@ -467,11 +467,15 @@ pub enum SessionEvent {
new_spell_id: u32,
},
/// The server's verdict on our cast (`SMSG_CAST_RESULT`): `reason` is the failure code when
/// `success` is false (the client's error-text table keys on it; surfaced raw for now).
/// `success` is false (the client's error-text table keys on it), and `arg` the reason-specific
/// argument word that fills that message's `%s` — a `SpellFocusObject.dbc` id for
/// `REQUIRES_SPELL_FOCUS`, an `AreaTable.dbc` id for `REQUIRES_AREA`
/// ([`crate::messages::CastOutcome::Failed`]).
CastResult {
spell_id: u32,
success: bool,
reason: Option<u8>,
arg: Option<u32>,
},
/// The pet action bar's whole state (`SMSG_PET_SPELLS`, decision 0982) — or its **teardown**,
/// which is the same event carrying a zero `pet_guid`. Server-authoritative: this is not a
+6 -2
View File
@@ -80,7 +80,11 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
success: outcome == CastOutcome::Ok,
reason: match outcome {
CastOutcome::Ok => None,
CastOutcome::Failed { reason } => Some(reason),
CastOutcome::Failed { reason, .. } => Some(reason),
},
arg: match outcome {
CastOutcome::Ok => None,
CastOutcome::Failed { arg, .. } => arg,
},
}],
ServerPacket::PetSpells(spells) => vec![SessionEvent::PetSpells(Box::new(spells))],
@@ -92,7 +96,7 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
spell_id,
reason: match outcome {
CastOutcome::Ok => None,
CastOutcome::Failed { reason } => Some(reason),
CastOutcome::Failed { reason, .. } => Some(reason),
},
}],
ServerPacket::ItemQueryResponse { entry, info } => {
@@ -380,12 +380,16 @@ pub(super) fn read_pet_action_feedback(r: &mut impl Read) -> io::Result<u8> {
/// `SMSG_CAST_RESULT` — the same `SpellCastResult` vocabulary, decoded the same way (status `2` =
/// fail) so the one message table renders both. vmangos only ever sends the fail status here, but
/// reading it the shared way costs nothing and keeps the two decodes from drifting.
///
/// The body stops at the reason: unlike `CastResult`, `PetCastFailed::AppendBodyTo` writes **no
/// argument words** at all, so a pet failure never carries a `%s` fill.
pub(super) fn read_pet_cast_failed(r: &mut impl Read) -> io::Result<(u32, super::CastOutcome)> {
let spell_id = read_u32_le(r)?;
let status = read_u8(r)?;
let outcome = if status == 2 {
super::CastOutcome::Failed {
reason: read_u8(r)?,
arg: None,
}
} else {
super::CastOutcome::Ok
+38 -10
View File
@@ -28,24 +28,52 @@ use crate::wire::{
};
/// `SMSG_CAST_RESULT`'s verdict: `u32 spellId, u8 status` — status `0` (`SPELL_RESULT_STATUS_OKAY`)
/// ends the packet, status `2` (`SPELL_RESULT_STATUS_FAIL`) appends a `u8` failure reason
/// (vmangos `CastResult::AppendBodyTo`; some reasons append extra arg words we skip).
/// ends the packet, status `2` (`SPELL_RESULT_STATUS_FAIL`) appends a `u8` failure reason and then
/// up to two **reason-specific argument words** (vmangos `CastResult::AppendBodyTo`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CastOutcome {
Ok,
Failed { reason: u8 },
Failed {
reason: u8,
/// The failure's first argument word, when the server sent one — the `%s` source of the
/// client's argument-formatted messages (the arm table at `0x6e1d8e`): a
/// `SpellFocusObject.dbc` id for `REQUIRES_SPELL_FOCUS` (0x5e), an `AreaTable.dbc` id for
/// `REQUIRES_AREA` (0x5d), an `EquippedItemClass` for the 0x19–0x1b family, a
/// permanent-cooldown flag for `NOT_READY` (0x3c). `None` = the packet ended at the reason.
arg: Option<u32>,
},
}
/// Read `SMSG_CAST_RESULT` → `(spell_id, outcome)`. Reason-specific trailing args (a required
/// spell-focus id, an equip class) are left unread — the slice ends with the packet, nothing
/// follows in the stream.
pub(super) fn read_cast_result(r: &mut impl Read) -> io::Result<(u32, CastOutcome)> {
/// Read `SMSG_CAST_RESULT` → `(spell_id, outcome)`.
///
/// The trailing argument words are **positional, gated on the remaining body length** — and that
/// is the reference's own decode, not merely a convenient one. `CastResultHandler 0x6e7330`
/// (registered at `0x6e7155`) reads `u32 spellId`, `u8 status`, then on `status == 2` a
/// `u8 reason`, then each word only `if cursor < size` (`0x6e737a` and `0x6e738c`, comparing
/// CDataStore `+0x14` against `+0x10`) — never keyed off the reason value (wow-re
/// `system/spell/scratch/cast-fail-strings.md` §WIRE-ARGS, §5 cross-checked). It has to be: vmangos
/// writes `if (arg1 || arg2) << u32 arg1; if (arg2) << u32 arg2;`
/// (`Server/Packets/Spell.cpp`, `CastResult::AppendBodyTo`), so a reason whose argument is
/// legitimately 0 with no second argument behind it sends no word at all, and a reason-keyed decode
/// would desync on exactly that case.
///
/// **The reference's absent value is `-1`, not zero** (`0x6e736e or eax,0xffffffff` initializes both
/// slots before the gated reads). `None` here is that sentinel: every consumer treats it and an
/// out-of-range id the same way — decline the fill — so the two encodings are behaviourally
/// identical, and `Option` says which case it is at the type level.
///
/// The second word is read to keep the shape documented, then dropped: only the
/// `EQUIPPED_ITEM_CLASS*` family (0x19–0x1b) sends one, and that arm's `%s` fill reads the failing
/// spell's own `EquippedItemClass`/`EquippedItemSubClassMask` columns — the same two values the
/// server copied out of `SpellEntry` to build the packet (`Spell::SendCastResult`).
pub(super) fn read_cast_result(r: &mut &[u8]) -> io::Result<(u32, CastOutcome)> {
let spell_id = read_u32_le(r)?;
let status = read_u8(r)?;
let outcome = if status == 2 {
CastOutcome::Failed {
reason: read_u8(r)?,
}
let reason = read_u8(r)?;
let arg = (r.len() >= 4).then(|| read_u32_le(r)).transpose()?;
let _arg2 = (r.len() >= 4).then(|| read_u32_le(r)).transpose()?;
CastOutcome::Failed { reason, arg }
} else {
CastOutcome::Ok
};