Files
benilla-octo/crates/benilla-protocol/src/world/writer/chat.rs
T
Sam SungandClaude Opus 5.5 c6767d6a79 SendChatMessage resolves its language name against Languages.dbc, raising "Unknown language" on a miss, and the chat send carries that id, so a racial language picked in the chat menu is spoken (#164) (#498)
`SendChatMessage`'s language argument is spoken, as in 1.12.1: a racial language picked from the chat menu (`ChatFrame.lua:2404`, passed on by `ChatEdit_SendText` at `:1950-1954`) or named by an addon goes out in `CMSG_MESSAGECHAT`, where before every line went in the faction tongue.

- `0x49f1e0` starts from the speaker's default language (`0x5ec890`); when argument 3 passes `lua_isstring` (a string or a number) it is matched by name against every `Languages.dbc` row, case-insensitively (`0x49f8a0`, `SStrCmpI`), and a miss raises "Unknown language" (`0x49f2e1`). nil, an absent argument, a table or a boolean keep the default. A language the character has not learned is sent and left to the server, which drops it (vmangos `ChatHandler.cpp:175`).
- The packet builder writes the type, the language and a target for a whisper or a channel only, then the text (`0x49f6c3`-`0x49f72a`); fourteen per-type writers become one, with byte-exact goldens.
- Each new UI state is seeded with the language table.

Checked live against the local vmangos: a say, a yell, a whisper to self and a GM command round-trip through the new builder; a channel line reaches the server's channel lookup; an unknown language raises in Lua.

Fixes #164

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 13:09:44 +07:00

123 lines
4.7 KiB
Rust

//! The chat sends. A line speaks the language `SendChatMessage` named, else
//! [`WorldWriter::chat_language`]: vmangos drops `Universal` outside AFK and DND
//! (`ChatHandler.cpp:105`) and any tongue the character does not know (`:175`).
use anyhow::Result;
use crate::messages::{self, opcode};
use super::WorldWriter;
impl WorldWriter {
/// Send a `/say` line; dot-commands go out this way, parsed after the language gate.
pub fn send_chat(&mut self, message: &str) -> Result<()> {
self.send(
opcode::CMSG_MESSAGECHAT,
&messages::messagechat(messages::CHAT_TYPE_SAY, self.chat_language, message),
)
}
/// Send a chat line of any `ChatMsg` type, as the reference's generic builder writes it
/// (`0x49f6c3`-`0x49f72a`). `language` is the `Languages.dbc` id `SendChatMessage` resolved;
/// `None` is the character's own tongue, [`WorldWriter::chat_language`].
pub fn send_message_chat(
&mut self,
chat_type: u32,
language: Option<u32>,
target: Option<&str>,
message: &str,
) -> Result<()> {
let body = message_chat_body(self.chat_language, chat_type, language, target, message);
self.send(opcode::CMSG_MESSAGECHAT, &body)
}
/// Send an addon message (`SendAddonMessage`): `text` is the caller's composed `prefix` TAB
/// `message`, on the party, raid, guild or battleground lane. Its language,
/// [`messages::LANGUAGE_ADDON`], is the only mark of addon data (`0x49f920`); vmangos gates it
/// on `AddonChannel` and skips the language gate, flood control and sanitizing.
pub fn send_addon_message(&mut self, chat_type: u32, text: &str) -> Result<()> {
self.send(
opcode::CMSG_MESSAGECHAT,
&messages::messagechat(chat_type, messages::LANGUAGE_ADDON, text),
)
}
/// Tell the server we ignore `guid`; that player gets a `CHAT_MSG_IGNORED` notice.
pub fn chat_ignored(&mut self, guid: u64) -> Result<()> {
self.send(opcode::CMSG_CHAT_IGNORED, &messages::full_guid(guid))
}
/// Ask our played time (`CMSG_PLAYED_TIME`), answered by `SMSG_PLAYED_TIME`.
pub fn played_time(&mut self) -> Result<()> {
self.send(opcode::CMSG_PLAYED_TIME, &messages::played_time())
}
/// Roll `/random`; the server checks `min <= max <= 10000` and answers on the same opcode.
pub fn random_roll(&mut self, min: u32, max: u32) -> Result<()> {
self.send(opcode::MSG_RANDOM_ROLL, &messages::random_roll(min, max))
}
/// Perform a text emote (EmotesText id, target or 0), echoed to everyone in range, us included.
pub fn text_emote(&mut self, text_id: u32, target: u64) -> Result<()> {
self.send(
opcode::CMSG_TEXT_EMOTE,
&messages::text_emote(text_id, target),
)
}
}
/// The `CMSG_MESSAGECHAT` body: type, language (`0x49f6f9`), the target cstring only for a whisper
/// or a channel (`0x49f701`-`0x49f712`, types `6` and `0xe`), then the text. `speaker` is the
/// language a send that names none carries.
fn message_chat_body(
speaker: u32,
chat_type: u32,
language: Option<u32>,
target: Option<&str>,
message: &str,
) -> Vec<u8> {
let target = matches!(
chat_type,
messages::CHAT_TYPE_WHISPER | messages::CHAT_TYPE_CHANNEL
)
.then(|| target.unwrap_or_default());
messages::messagechat_kind(chat_type, language.unwrap_or(speaker), target, message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_named_language_lands_in_the_body() {
// A Night Elf's `SendChatMessage("hi", "SAY", "Darnassian")`: type 0, language 2.
assert_eq!(
message_chat_body(7, messages::CHAT_TYPE_SAY, Some(2), None, "hi"),
[0, 0, 0, 0, 2, 0, 0, 0, b'h', b'i', 0]
);
// No language named: the speaker's Common (7).
assert_eq!(
message_chat_body(7, messages::CHAT_TYPE_YELL, None, None, "hi"),
[5, 0, 0, 0, 7, 0, 0, 0, b'h', b'i', 0]
);
// A Tauren's Taur-ahe (3) whisper: the name before the text.
assert_eq!(
message_chat_body(1, messages::CHAT_TYPE_WHISPER, Some(3), Some("Bo"), "yo"),
[6, 0, 0, 0, 3, 0, 0, 0, b'B', b'o', 0, b'y', b'o', 0]
);
}
#[test]
fn only_a_whisper_or_a_channel_carries_a_target() {
assert_eq!(
message_chat_body(1, messages::CHAT_TYPE_PARTY, None, Some("Bo"), "x"),
[1, 0, 0, 0, 1, 0, 0, 0, b'x', 0]
);
assert_eq!(
message_chat_body(1, messages::CHAT_TYPE_CHANNEL, Some(14), None, "x"),
[0xe, 0, 0, 0, 14, 0, 0, 0, 0, b'x', 0],
"a channel with no name still writes the empty cstring"
);
}
}