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
20 changes: 16 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A World of Warcraft 1.12.1 server. Game state lives in SpacetimeDB; the gateway

**B** [Base Snapshot](#packages) · [Baseline](#client-content) · [Bot Capacity Lease](#sharding-and-transfer) · [Bot Controller](#sharding-and-transfer) · [Bot Objective](#sharding-and-transfer) · [Bounded Map Slice](#realm-topology) · [Build Identity](#packages)

**C** [Cancellation](#auctions) · [Candidate](#sharding-and-transfer) · [Carrier](#procs) · [Cast Handle](#procs) · [Channel Membership](#chat) · [Channel Notice](#chat) · [Character](#accounts-characters-and-sessions) · [Chat Channel](#chat) · [Chat Flood Limiter](#chat) · [Chat Kind](#chat) · [Claim](#packages) · [Claim Conflict](#packages) · [Client Artifact](#client-content) · [Command Receipt](#sharding-and-transfer) · [Companion Order](#sharding-and-transfer) · [Compatibility Manifest](#realm-topology) · [Content Identity](#packages) · [Coordinator](#gateway-and-module) · [Counterparty](#procs) · [Creature-AI Family](#creature-ai)
**C** [Cancellation](#auctions) · [Candidate](#sharding-and-transfer) · [Carrier](#procs) · [Cast Handle](#procs) · [Cast Miss](#procs) · [Channel Membership](#chat) · [Channel Notice](#chat) · [Character](#accounts-characters-and-sessions) · [Chat Channel](#chat) · [Chat Flood Limiter](#chat) · [Chat Kind](#chat) · [Claim](#packages) · [Claim Conflict](#packages) · [Client Artifact](#client-content) · [Command Receipt](#sharding-and-transfer) · [Companion Order](#sharding-and-transfer) · [Compatibility Manifest](#realm-topology) · [Content Identity](#packages) · [Coordinator](#gateway-and-module) · [Counterparty](#procs) · [Creature-AI Family](#creature-ai)

**D** [Datascript](#packages) · [Delivery Delay](#mail) · [Dismount](#mounts) · [Distraction](#creature-ai) · [Dormant Character](#accounts-characters-and-sessions) · [Durable Read](#gateway-and-module) · [Durable Request](#gateway-and-module)

Expand All @@ -37,7 +37,7 @@ A World of Warcraft 1.12.1 server. Game state lives in SpacetimeDB; the gateway

**R** [Raid](#sharding-and-transfer) · [Raid Quest](#sharding-and-transfer) · [Raid Slot](#sharding-and-transfer) · [Random Property](#random-properties) · [Ranged Posture](#creature-ai) · [Rank Rights](#guilds) · [Ready Check](#sharding-and-transfer) · [Realm](#realm-topology) · [Realm Chat Line](#chat) · [Realm Clock](#world-clock-and-weather) · [Realm Presence](#chat) · [Realm-core](#realm-topology) · [Recorded Revision](#packages) · [Recovery Attempt](#sharding-and-transfer) · [Recovery Scan](#sharding-and-transfer) · [Reference Datascript](#packages) · [Reference Package](#packages) · [Refusal](#gateway-and-module) · [Relay](#gateway-and-module) · [Relay Definition](#realm-topology) · [Relay Run](#realm-topology) · [Returned Mail](#mail) · [Reward Letter](#mail) · [Roster Revision](#sharding-and-transfer) · [Roster Revision Relay](#sharding-and-transfer) · [Route Path](#procs) · [Route Step](#procs) · [Rule State](#creature-ai) · [Runtime Script](#runtime-scripts) · [Runtime Script Host](#runtime-scripts) · [Runtime Script Toolchain](#packages)

**S** [Script Answer](#runtime-scripts) · [Script Artifact](#packages) · [Script Diagnostic](#runtime-scripts) · [Script Directive](#packages) · [Script Identity](#packages) · [Seam](#working-method) · [Seeker](#meeting-stones) · [Self-Resurrection Option](#death-and-resurrection) · [Service Reconciliation](#realm-topology) · [Session](#accounts-characters-and-sessions) · [Session Expiry](#accounts-characters-and-sessions) · [SessionActor](#accounts-characters-and-sessions) · [Sessionless Action Consent](#sharding-and-transfer) · [Settlement](#auctions) · [Shard](#realm-topology) · [Shard Boundary](#sharding-and-transfer) · [Shard Map](#sharding-and-transfer) · [Signature](#guilds) · [Solo Target Claim](#sharding-and-transfer) · [Soulstone](#death-and-resurrection) · [Spatial Claim](#packages) · [Speaker Facts](#chat) · [Spec](#working-method) · [Speech](#chat) · [Spell Cast Event Kind](#procs) · [Staged Effect](#runtime-scripts) · [Standalone Supervisor](#realm-topology) · [Stat Kind](#random-properties) · [Stone Add](#meeting-stones) · [Store](#working-method) · [Strategy](#sharding-and-transfer) · [Subgroup](#sharding-and-transfer) · [Suffix](#random-properties)
**S** [Script Answer](#runtime-scripts) · [Script Artifact](#packages) · [Script Diagnostic](#runtime-scripts) · [Script Directive](#packages) · [Script Identity](#packages) · [Seam](#working-method) · [Seeker](#meeting-stones) · [Self-Resurrection Option](#death-and-resurrection) · [Service Reconciliation](#realm-topology) · [Session](#accounts-characters-and-sessions) · [Session Expiry](#accounts-characters-and-sessions) · [SessionActor](#accounts-characters-and-sessions) · [Sessionless Action Consent](#sharding-and-transfer) · [Settlement](#auctions) · [Shard](#realm-topology) · [Shard Boundary](#sharding-and-transfer) · [Shard Map](#sharding-and-transfer) · [Signature](#guilds) · [Solo Target Claim](#sharding-and-transfer) · [Soulstone](#death-and-resurrection) · [Spatial Claim](#packages) · [Speaker Facts](#chat) · [Spec](#working-method) · [Speech](#chat) · [Spell Cast Event Kind](#procs) · [Staged Effect](#runtime-scripts) · [Standalone Supervisor](#realm-topology) · [Stat Kind](#random-properties) · [Stone Add](#meeting-stones) · [Store](#working-method) · [Strategy](#sharding-and-transfer) · [Subgroup](#sharding-and-transfer) · [Suffix](#random-properties) · [Swing Error](#procs)

**T** [Tabard Designer](#guilds) · [Target Icon](#sharding-and-transfer) · [Ticket](#working-method) · [Tracer](#working-method) · [Trade Commit](#trading) · [Trade Session](#trading) · [Transfer](#sharding-and-transfer) · [Transfer Intent](#sharding-and-transfer) · [Transport Loss](#gateway-and-module) · [Triggered Cast](#procs) · [Trust Review](#packages)

Expand Down Expand Up @@ -649,10 +649,22 @@ _Avoid_: proc cast, internal cast, free cast

**Spell Cast Event Kind**:
The append-only code naming the wire signal in one `game_spell_cast_event` row. Zero means a row
from the old schema. New rows use 1 START, 2 GO, 3 INTERRUPT, 4 PUSHBACK, or 5 PROC_LOG. A Gateway
does not guess how to decode an unknown nonzero code.
from the old schema. New rows use 1 START, 2 GO, 3 INTERRUPT, 4 PUSHBACK, 5 PROC_LOG or
6 PERIODIC_LOG. A PERIODIC_LOG row is one periodic tick, addressed at the unit it landed on. A
Gateway does not guess how to decode an unknown nonzero code.
_Avoid_: cast flags, event flags, signal flags

**Cast Miss**:
The primary target's outcome when a damage spell's hit roll fails, recorded on the GO row. The
roll runs once per target, and a missed target takes none of the spell's effects. A physical spell
misses; a magic spell is resisted outright.
_Avoid_: spell miss flag, resist flag

**Swing Error**:
Why a player's due melee swing could not land: the target is out of reach or behind the player.
Stored on the engagement row and reported to the player once per change.
_Avoid_: swing failure, attack error

**Item Exchange**:
A complete plan of carried-item consumption and item grants. Capacity, uniqueness, Random Property,
and identity allocation Gates pass before any inventory row changes. Quest rewards commit as one
Expand Down
21 changes: 20 additions & 1 deletion docs/benilla-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,25 @@ race's opening cinematic. A second login in one World Session gets
answer. The Gateway drops account-data uploads and zone reports before decoding,
because the protocol library cannot read them safely.

## Combat feedback

A player's due melee swing that finds its target out of reach or behind the player records a
Swing Error. The Gateway sends `SMSG_ATTACKSWING_NOTINRANGE` or `SMSG_ATTACKSWING_BADFACING` once
when the error changes, and a landed swing clears it. A creature that starts an Engagement sends
`SMSG_AI_REACTION` (hostile) before `SMSG_ATTACKSTART`; a reconnect replays only the attack start.
The player who lands a melee killing blow gets `SMSG_PARTYKILLLOG`. A swing that ends a Duel is no
killing blow. vmangos sends the line to the tagger or a pet's owner, and to the party. The tag
is gone from the Coordinator cache when the relay runs, so those copies and spell kills are not
sent.

Each periodic damage, heal or energize tick sends `SMSG_PERIODICAURALOG`; a heal tick on a unit at
full health logs nothing. A damage spell rolls its hit once per target, and a missed target takes
none of its effects. The `SMSG_SPELL_GO` miss list names the target, as a miss for a physical spell
and a resist for a magic one. The caster of an instant cast already holds a GO, so it gets
`SMSG_SPELLLOGMISS` instead. A direct energize sends `SMSG_SPELLENERGIZELOG`. Bystanders of an
interrupted player cast get `SMSG_SPELL_FAILED_OTHER`. Mount refusals name the vanilla cast
failure: dead, in combat, indoors or in deep water.

## Missing gameplay and deliberate responses

These features remain unavailable. The responses below apply to valid requests
Expand All @@ -260,7 +279,7 @@ per World Session. Repeated requests do not add chat lines.
| Battleground spirit healers | System Message reporting unavailability. | Resurrection queues and timers. |
| GM tickets | System disabled, no current ticket, creation and update refused. Deletion reports the already-absent ticket. | Durable tickets and GM workflow. |
| Pet stables | Stable failure code 6 for listing, storing, retrieving, swapping and slot purchase. | Stable storage and purchases. |
| Pet controls | Specific System Messages for bar changes, autocast, aura cancellation, stop-attack, naming, abandonment and unlearning. | These operations and their durable state. Existing pet action commands and progression remain separate. |
| Pet controls | Specific System Messages for bar changes, autocast, aura cancellation, stop-attack, naming, abandonment and unlearning. A pet-bar spell cast gets `SMSG_PET_CAST_FAILED` with the unreported reason, then a System Message. A pet-info request is ignored; the pet bar arrives with the pet. | These operations and their durable state, and an owner-to-pet read to resend the pet bar. Existing pet action commands and progression remain separate. |
| Item containers and gifts | Inventory Refusal 39. | Item-container loot, wrapping and unwrapping. |
| Readable pages | A terminal page names the unavailable text service. | Page-text catalog import and page chains. |
| Reading items | Read failure with the close reason and a System Message, for an item with page text. An item without page text gets Inventory Refusal 23. | Page-text catalog import, then the read-OK reply. |
Expand Down
135 changes: 135 additions & 0 deletions gateway/src/codec/combat.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use wow_world_messages::vanilla::{AiReaction, SMSG_AI_REACTION, SMSG_SPELL_FAILED_OTHER};

/// The client manages the GCD from Spell.dbc; send this only for spell cooldowns.
pub fn build_spell_cooldown(
Expand Down Expand Up @@ -136,8 +137,20 @@ pub fn cast_failure_reason_for(es: &str) -> u8 {
const TARGETS_DEAD: u8 = 0x65;
const BAD_TARGETS: u8 = 0x0A;
const DONT_REPORT: u8 = 0x17;
const AFFECTING_COMBAT: u8 = 0x00;
const CASTER_DEAD: u8 = 0x13;
const ONLY_ABOVEWATER: u8 = 0x50;
const ONLY_OUTDOORS: u8 = 0x55;
if es.contains("not enough power") {
NO_POWER
} else if es.contains("cannot mount while dead") {
CASTER_DEAD
} else if es.contains("cannot mount while in combat") {
AFFECTING_COMBAT
} else if es.contains("cannot mount indoors") {
ONLY_OUTDOORS
} else if es.contains("cannot mount in deep water") {
ONLY_ABOVEWATER
} else if es.contains("cooldown") {
NOT_READY
} else if es.contains("out of range") || es.contains("another map") {
Expand Down Expand Up @@ -295,6 +308,115 @@ pub fn build_spell_go_outcome(
}
}

pub const SMSG_PERIODICAURALOG_OPCODE: u16 = 0x024E;
pub const SMSG_SPELLENERGIZELOG_OPCODE: u16 = 0x0151;
pub const SMSG_SPELLLOGMISS_OPCODE: u16 = 0x024B;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PeriodicTick {
Damage {
damage: u32,
school: u8,
absorbed: u32,
resisted: u32,
},
Heal {
amount: u32,
},
Energize {
power_type: u8,
amount: u32,
},
}

/// Packed target, packed caster, spell, count 1, aura type, then u32 fields. Raw: gtker writes the
/// damage school as a u8; the client reads a u32.
pub fn build_periodic_aura_log(
target_guid: u64,
caster_guid: u64,
spell_id: u32,
tick: PeriodicTick,
) -> Vec<u8> {
const SPELL_AURA_PERIODIC_DAMAGE: u32 = 3;
const SPELL_AURA_PERIODIC_HEAL: u32 = 8;
const SPELL_AURA_PERIODIC_ENERGIZE: u32 = 24;
let mut body = Vec::with_capacity(40);
write_packed_guid_u64(&mut body, target_guid);
write_packed_guid_u64(&mut body, caster_guid);
body.extend_from_slice(&spell_id.to_le_bytes());
body.extend_from_slice(&1u32.to_le_bytes());
let fields: Vec<u32> = match tick {
PeriodicTick::Damage {
damage,
school,
absorbed,
resisted,
} => vec![
SPELL_AURA_PERIODIC_DAMAGE,
damage,
u32::from(school),
absorbed,
resisted,
],
PeriodicTick::Heal { amount } => vec![SPELL_AURA_PERIODIC_HEAL, amount],
PeriodicTick::Energize { power_type, amount } => {
vec![SPELL_AURA_PERIODIC_ENERGIZE, u32::from(power_type), amount]
}
};
for field in fields {
body.extend_from_slice(&field.to_le_bytes());
}
body
}

/// Packed target, packed caster, spell, power type as u32, amount.
pub fn build_spell_energize_log(
target_guid: u64,
caster_guid: u64,
spell_id: u32,
power_type: u8,
amount: u32,
) -> Vec<u8> {
let mut body = Vec::with_capacity(30);
write_packed_guid_u64(&mut body, target_guid);
write_packed_guid_u64(&mut body, caster_guid);
body.extend_from_slice(&spell_id.to_le_bytes());
body.extend_from_slice(&u32::from(power_type).to_le_bytes());
body.extend_from_slice(&amount.to_le_bytes());
body
}

/// Spell, full caster guid, extended-info flag 0, count 1, full target guid, `SpellMissInfo` byte.
pub fn build_spell_log_miss(
caster_guid: u64,
spell_id: u32,
target_guid: u64,
miss_info: u8,
) -> Vec<u8> {
let mut body = Vec::with_capacity(26);
body.extend_from_slice(&spell_id.to_le_bytes());
body.extend_from_slice(&caster_guid.to_le_bytes());
body.push(0);
body.extend_from_slice(&1u32.to_le_bytes());
body.extend_from_slice(&target_guid.to_le_bytes());
body.push(miss_info);
body
}

pub fn build_spell_failed_other(caster_guid: u64, spell_id: u32) -> SMSG_SPELL_FAILED_OTHER {
SMSG_SPELL_FAILED_OTHER {
caster: Guid::new(caster_guid),
id: spell_id,
}
}

pub fn build_ai_reaction_hostile(creature_guid: u64) -> SMSG_AI_REACTION {
SMSG_AI_REACTION {
guid: Guid::new(creature_guid),
reaction: AiReaction::Hostile,
}
}

pub fn build_attack_start(attacker_guid: u64, victim_guid: u64) -> SMSG_ATTACKSTART {
SMSG_ATTACKSTART {
attacker: Guid::new(attacker_guid),
Expand Down Expand Up @@ -434,6 +556,19 @@ mod tests {
);
}

#[test]
fn mount_refusals_map_to_the_vanilla_cast_failures() {
// module/src/mount.rs `check_mount_cast` wording; gtker vanilla CastFailureReason values.
for (refusal, reason) in [
("you cannot mount while dead", 0x13),
("you cannot mount while in combat", 0x00),
("you cannot mount indoors", 0x55),
("you cannot mount in deep water", 0x50),
] {
assert_eq!(cast_failure_reason_for(refusal), reason, "{refusal}");
}
}

#[test]
fn spell_failure_carries_interrupted_for_the_caster() {
let caster = 0xF130_0000_0000_0005u64;
Expand Down
Loading
Loading