Skip to content
Open
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
17 changes: 11 additions & 6 deletions crates/wow-packet/src/packets/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,8 @@ impl ClientPacket for ChatAddonMessageWhisper {

/// Server sends this to broadcast a chat message to nearby players.
///
/// C# ChatPkt.Write():
/// C++ ref: `WorldPackets::Chat::Chat::Write` in
/// `Server/Packets/ChatPackets.cpp`.
/// ```text
/// u8 slash_cmd (ChatMsg)
/// u32 language
Expand Down Expand Up @@ -814,7 +815,8 @@ impl ServerPacket for ChatRestricted {

/// CMSG_EMOTE — client clears its emote state (no body).
///
/// C# ref: `EmoteClient` in `ChatPackets.cs` — `Read()` is empty.
/// C++ ref: `WorldPackets::Chat::EmoteClient` in `Server/Packets/ChatPackets.h`;
/// `Read()` is empty.
pub struct EmoteClient;

impl ClientPacket for EmoteClient {
Expand All @@ -826,7 +828,8 @@ impl ClientPacket for EmoteClient {

/// CMSG_SEND_TEXT_EMOTE — player performs a text emote (/wave, /dance, etc.).
///
/// C# ref: `CTextEmote.Read()` in `ChatPackets.cs`.
/// C++ ref: `WorldPackets::Chat::CTextEmote::Read` in
/// `Server/Packets/ChatPackets.cpp`.
pub struct CTextEmote {
pub target: ObjectGuid,
pub emote_id: i32,
Expand Down Expand Up @@ -860,7 +863,8 @@ impl ClientPacket for CTextEmote {

/// SMSG_TEXT_EMOTE — broadcasts text emote to nearby players (chat text).
///
/// C# ref: `STextEmote.Write()` in `ChatPackets.cs`.
/// C++ ref: `WorldPackets::Chat::STextEmote::Write` in
/// `Server/Packets/ChatPackets.cpp`.
pub struct STextEmote {
pub source_guid: ObjectGuid,
pub source_account_guid: ObjectGuid,
Expand All @@ -883,8 +887,9 @@ impl ServerPacket for STextEmote {

/// SMSG_EMOTE — plays the emote animation on the unit.
///
/// C# ref: `EmoteMessage.Write()` in `ChatPackets.cs`.
/// Also sent by `Unit.HandleEmoteCommand()`.
/// C++ ref: `WorldPackets::Chat::Emote::Write` in
/// `Server/Packets/ChatPackets.cpp`. Also sent by
/// `Unit::HandleEmoteCommand`.
pub struct EmoteMessage {
pub guid: ObjectGuid,
pub emote_id: i32,
Expand Down
137 changes: 125 additions & 12 deletions crates/wow-world/src/handlers/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
//! Broadcast ranges use C++ `ListenRange.Say`, `ListenRange.TextEmote`, and
//! `ListenRange.Yell` from `World.cpp`.
//!
//! Reference: C# Game/Handlers/ChatHandler.cs, Game/Entities/Player/Player.cs
//! Reference: C++ `Handlers/ChatHandler.cpp`, `Entities/Player/Player.cpp`,
//! and `Server/Packets/ChatPackets.cpp`.

use tracing::debug;

Expand Down Expand Up @@ -912,6 +913,11 @@ impl WorldSession {
}
self.send_packet(&text_emote);
self.broadcast_raw_packet(text_emote.to_bytes(), text_emote_range);
// C++ then resolves `ObjectAccessor::GetUnit(*_player, packet.Target)` for
// `CriteriaType::DoEmote` and `CreatureAI::ReceiveEmote`. Rust has no
// live chat->criteria/CreatureAI bridge here yet; keep the C++ packet
// order, target GUID, and fanout gates above while leaving those
// post-broadcast side effects explicit.
if emote != EMOTE_ONESHOT_NONE_LIKE_CPP {
self.remove_auras_with_interrupt_flags_like_cpp(
SPELL_AURA_INTERRUPT_FLAG_ANIM_LIKE_CPP,
Expand Down Expand Up @@ -1204,7 +1210,7 @@ impl WorldSession {
}

/// Serialize `pkt` and broadcast its bytes to all players on the same map
/// within `range` yards (excluding the sender).
/// instance within `range` yards (excluding the sender).
fn broadcast_chat_packet(&self, pkt: &ChatPkt, range: f32) {
self.broadcast_raw_packet(pkt.to_bytes(), range);
}
Expand Down Expand Up @@ -1386,8 +1392,8 @@ impl WorldSession {
}
}

/// Send pre-serialised packet `bytes` to all players on the same map
/// within `range` yards, excluding this session's player.
/// Send pre-serialised packet `bytes` to all players in the same map
/// instance within `range` yards, excluding this session's player.
fn broadcast_raw_packet(&self, bytes: Vec<u8>, range: f32) {
let registry = match self.player_registry() {
Some(r) => r,
Expand All @@ -1397,6 +1403,14 @@ impl WorldSession {
let sender_guid = self.player_guid().unwrap_or(ObjectGuid::EMPTY);
let sender_pos = self.player_position_like_cpp();
let sender_map = self.player_map_id_like_cpp();
let sender_instance = registry
.get(&sender_guid)
.map(|info| info.instance_id)
.or_else(|| {
self.current_canonical_player_map_key_like_cpp()
.map(|key| key.instance_id)
})
.unwrap_or(0);
let range_sq = range * range;

for entry in registry.iter() {
Expand All @@ -1407,8 +1421,9 @@ impl WorldSession {

let info = entry.value();

// Must be on the same map.
if info.map_id != sender_map {
// Must be an in-world player on the same map instance.
if !info.is_in_world || info.map_id != sender_map || info.instance_id != sender_instance
{
continue;
}

Expand Down Expand Up @@ -1671,17 +1686,41 @@ mod tests {
}

fn text_emote_packet(emote_id: i32, sound_index: i32) -> wow_packet::WorldPacket {
text_emote_packet_with_visuals(emote_id, sound_index, &[], 0)
text_emote_packet_with_target_and_visuals(ObjectGuid::EMPTY, emote_id, sound_index, &[], 0)
}

fn text_emote_packet_with_visuals(
emote_id: i32,
sound_index: i32,
spell_visual_kit_ids: &[i32],
sequence_variation: i32,
) -> wow_packet::WorldPacket {
text_emote_packet_with_target_and_visuals(
ObjectGuid::EMPTY,
emote_id,
sound_index,
spell_visual_kit_ids,
sequence_variation,
)
}

fn text_emote_packet_with_target(
target: ObjectGuid,
emote_id: i32,
sound_index: i32,
) -> wow_packet::WorldPacket {
text_emote_packet_with_target_and_visuals(target, emote_id, sound_index, &[], 0)
}

fn text_emote_packet_with_target_and_visuals(
target: ObjectGuid,
emote_id: i32,
sound_index: i32,
spell_visual_kit_ids: &[i32],
sequence_variation: i32,
) -> wow_packet::WorldPacket {
let mut writer = wow_packet::WorldPacket::new_empty();
writer.write_packed_guid(&ObjectGuid::EMPTY);
writer.write_packed_guid(&target);
writer.write_int32(emote_id);
writer.write_int32(sound_index);
writer.write_int32(spell_visual_kit_ids.len() as i32);
Expand Down Expand Up @@ -1778,6 +1817,11 @@ mod tests {
}

fn text_emote_fields(bytes: &[u8]) -> (i32, i32) {
let (emote_id, sound_index, _) = text_emote_fields_with_target(bytes);
(emote_id, sound_index)
}

fn text_emote_fields_with_target(bytes: &[u8]) -> (i32, i32, ObjectGuid) {
let mut packet = wow_packet::WorldPacket::from_bytes(bytes);
assert_eq!(
packet.read_uint16().expect("text emote opcode"),
Expand All @@ -1787,9 +1831,9 @@ mod tests {
let _ = packet.read_packed_guid().expect("source account guid");
let emote_id = packet.read_int32().expect("text emote id");
let sound_index = packet.read_int32().expect("sound index");
let _ = packet.read_packed_guid().expect("target guid");
let target_guid = packet.read_packed_guid().expect("target guid");
assert!(packet.is_empty());
(emote_id, sound_index)
(emote_id, sound_index, target_guid)
}

fn set_emotes_text_entries(
Expand Down Expand Up @@ -2403,9 +2447,78 @@ mod tests {
}

#[tokio::test]
async fn send_text_emote_requires_cpp_emotes_text_entry_like_cpp() {
async fn send_text_emote_filters_visible_map_instance_like_cpp() {
let sender = ObjectGuid::create_player(1, 346);
let nearby = ObjectGuid::create_player(1, 347);
let target = ObjectGuid::create_player(1, 347);
let nearby = ObjectGuid::create_player(1, 348);
let other_instance = ObjectGuid::create_player(1, 349);
let not_in_world = ObjectGuid::create_player(1, 350);
let (mut session, player_registry, sender_rx) = session_for_chat_routing_like_cpp(sender);
let (target_tx, target_rx) = flume::bounded(8);
let (nearby_tx, nearby_rx) = flume::bounded(8);
let (other_instance_tx, other_instance_rx) = flume::bounded(8);
let (not_in_world_tx, not_in_world_rx) = flume::bounded(8);
session.set_chat_listen_ranges_like_cpp(ChatListenRangesLikeCpp {
say: 25.0,
text_emote: 40.0,
yell: 300.0,
});
player_registry.insert(
target,
broadcast_info_at(
target,
target_tx,
wow_core::Position::new(60.0, 0.0, 0.0, 0.0),
),
);
player_registry.insert(
nearby,
broadcast_info_at(
nearby,
nearby_tx,
wow_core::Position::new(20.0, 0.0, 0.0, 0.0),
),
);
let mut other_instance_info = broadcast_info_at(
other_instance,
other_instance_tx,
wow_core::Position::new(20.0, 0.0, 0.0, 0.0),
);
other_instance_info.instance_id = 1;
player_registry.insert(other_instance, other_instance_info);
let mut not_in_world_info = broadcast_info_at(
not_in_world,
not_in_world_tx,
wow_core::Position::new(20.0, 0.0, 0.0, 0.0),
);
not_in_world_info.is_in_world = false;
player_registry.insert(not_in_world, not_in_world_info);
set_emotes_text_entries(
&mut session,
[emotes_text_entry(66, EMOTE_STATE_SIT_LIKE_CPP as u16)],
);

session
.handle_text_emote(text_emote_packet_with_target(target, 66, 7))
.await;

assert_eq!(
text_emote_fields_with_target(&sender_rx.try_recv().expect("sender text emote")),
(66, 7, target)
);
assert_eq!(
text_emote_fields_with_target(&nearby_rx.try_recv().expect("nearby text emote")),
(66, 7, target)
);
assert!(target_rx.try_recv().is_err());
assert!(other_instance_rx.try_recv().is_err());
assert!(not_in_world_rx.try_recv().is_err());
}

#[tokio::test]
async fn send_text_emote_requires_cpp_emotes_text_entry_like_cpp() {
let sender = ObjectGuid::create_player(1, 351);
let nearby = ObjectGuid::create_player(1, 352);
let (mut session, player_registry, sender_rx) = session_for_chat_routing_like_cpp(sender);
let (nearby_tx, nearby_rx) = flume::bounded(8);
player_registry.insert(nearby, broadcast_info(nearby, nearby_tx));
Expand Down
Loading