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:
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user