channel lines get their warm color, and a /wave gets its sentence

- a channel line renders in the channel family's color, not the grey notice row the client consults and throws away
- a text emote composes its sentence — gender and target picked from the game's own tables — so a /wave is a line in chat, not just an animation
- a FontString's `font=` resolves a font object by name and gates the whole block, `<Gradient>` tints the art it sits on, and buttons' state fonts read `font=` too — Bagnon's white boxes and bare stack counts are fixed
- the money frame sizes its coins from the digits themselves, so Bagnon's gold stops opening cramped

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-14 19:25:00 +07:00
co-authored by Claude Fable 5
parent a8718ad62c
commit a075a2ef44
23 changed files with 1495 additions and 129 deletions
+7 -2
View File
@@ -437,8 +437,13 @@ pub enum SessionEvent {
instant: bool,
},
/// A nearby unit's chat emote (`SMSG_TEXT_EMOTE`): the `EmotesText.dbc` id; the guid names
/// the performer (voice race/sex resolve from its descriptor).
TextEmote { guid: u64, text_emote: u32 },
/// the performer (voice race/sex resolve from its descriptor). `target_name` is the wire's
/// target display name, empty when untargeted — the sentence-form selector, decision 1274.
TextEmote {
guid: u64,
text_emote: u32,
target_name: String,
},
/// A unit's anim emote (`SMSG_EMOTE`): the `Emotes.dbc` id (drives the anim + its
/// `EventSoundID`).
Emote { guid: u64, emote_id: u32 },
+10 -2
View File
@@ -45,8 +45,16 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
sound_id,
instant,
}],
ServerPacket::TextEmote { guid, text_emote } => {
vec![SessionEvent::TextEmote { guid, text_emote }]
ServerPacket::TextEmote {
guid,
text_emote,
target_name,
} => {
vec![SessionEvent::TextEmote {
guid,
text_emote,
target_name,
}]
}
ServerPacket::Emote { guid, emote_id } => vec![SessionEvent::Emote { guid, emote_id }],
ServerPacket::InitialSpells {
+10 -2
View File
@@ -330,11 +330,19 @@ pub enum ServerPacket {
},
/// `SMSG_TEXT_EMOTE` — a nearby unit performed a chat emote (`/wave`). Payload VERIFIED
/// vmangos `EmoteChatBuilder`: `u64 guid, u32 textEmote (EmotesText.dbc), u32 emoteNum,
/// u32 namelen, char name[namelen]` (the target's name; namelen ≥ 1). The name is parsed
/// past and dropped (the chat line renders from it later; audio keys on ids).
/// u32 namelen, char name[namelen]` (the target's name; namelen ≥ 1, a lone NUL when there was
/// no target).
///
/// **`target_name` is load-bearing and used to be dropped here** (decision 1274 / B156): it is
/// what selects the emote's sentence FORM — untargeted vs targeted vs "…at you" — in
/// `benilla_formats::EmoteTextCatalog`, and it is the raw wire string, never round-tripped
/// through a name cache, exactly as the reference passes it. `emoteNum` really is
/// dropped: the receive side reads it off the wire and never consumes it (wow-re
/// `ui/scratch/text-emote-composition.md` §3).
TextEmote {
guid: u64,
text_emote: u32,
target_name: String,
},
/// `SMSG_EMOTE` — a unit plays an anim emote. Payload VERIFIED vmangos
/// `Unit::HandleEmote`: `u32 emoteId (Emotes.dbc), u64 guid`.
+16 -3
View File
@@ -314,12 +314,25 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
opcode::SMSG_TEXT_EMOTE => {
let guid = read_u64_le(&mut r)?;
let text_emote = read_u32_le(&mut r)?;
// Read and dropped on purpose — `emoteNum` selects neither the sentence nor the voice
// kit on the receive side (wow-re `ui/scratch/text-emote-composition.md` §3).
let _emote_num = read_u32_le(&mut r)?;
let namelen = read_u32_le(&mut r)?;
// The target's name, length-prefixed **including its NUL** (vmangos writes a lone
// `0x00` and `namelen == 1` when there was no target). Trimmed at the first NUL, so an
// untargeted emote arrives as the empty string — which is precisely the "untargeted"
// bit of the sentence-form selector, not a missing value.
let namelen = read_u32_le(&mut r)? as usize;
let mut name = Vec::with_capacity(namelen.min(64));
for _ in 0..namelen {
read_u8(&mut r)?; // the target's name — chat consumes it later; audio keys on ids
name.push(read_u8(&mut r)?);
}
let end = name.iter().position(|&b| b == 0).unwrap_or(name.len());
let target_name = String::from_utf8_lossy(&name[..end]).into_owned();
ServerPacket::TextEmote {
guid,
text_emote,
target_name,
}
ServerPacket::TextEmote { guid, text_emote }
}
opcode::SMSG_EMOTE => {
let emote_id = read_u32_le(&mut r)?;