From cae59cdc111e1a338f6d806191beb6216484e0c1 Mon Sep 17 00:00:00 2001 From: grom Date: Fri, 23 Oct 2026 19:27:36 -0600 Subject: [PATCH] app+protocol: fire CHAT_MSG_HARDCORE, the realm's own chat type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `reference_ui`'s `UNPRODUCED` carried `CHAT_MSG_HARDCORE` as a chain event nothing here fires: the window registered for it and benilla routed no such message, so the realm's hardcore-death broadcasts simply never arrived. Three of grom's addons listen for it too. The missing piece was one number, and the realm's patched `WoW.exe` has it in the PE section it names `.tdata`, beside the string `"HARDCORE"` at `0xd06000`. Two detours do the whole job: `0x49f7aa`, in the chat type-name lookup, jumps to `0xd06020`, which sets the name pointer to that string and the type to `$0x5e`; and `0x49b193`, inside the `CHAT_MSG_*` fire helper `0x49b0b0` whose format string this file already cites, jumps to `0xd06040`, which tests the type against `$0x5e` and swaps in the event-name index `0x21f`. So wire type `0x5E` is `ChatEventKind::Hardcore`. Nothing in 1.12 claims it — its last type is `CHAT_MSG_BATTLEGROUND_LEADER` at `0x5D` and its `CHAT_MSG_BG_SYSTEM_*` block sits at `0x52`-`0x54` — so there is no collision to arbitrate, and the test pins that adjacency along with the decode, since a wrong id would look exactly like the silence it replaces. The chain's `ChatFrame.xml` registers the event and its `GlobalStrings.lua` has the `[Hardcore] %s: ` format, so the window needed nothing from us but the fire; it takes the window's default colour, as `FILTERED` does, the realm adding no `ChatTypeInfo` row. Not covered: sending. The ruRU locale carries `SLASH_HARDCORE1 = "/h"` and `CHAT_HARDCORE_SEND` where enUS has only the receive format, so the send side needs its own look. 269 data-gated failures, identical to the baseline, all nine gates green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/ui_chat/event.rs | 41 +++++++++++++++++++ crates/benilla-app/src/ui_chat/tests.rs | 2 +- .../benilla-app/src/ui_script/reference_ui.rs | 6 --- crates/benilla-protocol/src/messages/chat.rs | 12 ++++++ crates/benilla-protocol/src/messages/mod.rs | 6 +-- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/crates/benilla-app/src/ui_chat/event.rs b/crates/benilla-app/src/ui_chat/event.rs index 2d42e241..0fe5a40c 100644 --- a/crates/benilla-app/src/ui_chat/event.rs +++ b/crates/benilla-app/src/ui_chat/event.rs @@ -41,6 +41,12 @@ pub(crate) enum ChatEventKind { RaidLeader, RaidWarning, RaidBossEmote, + /// `CHAT_MSG_HARDCORE` (`0x5E`), the realm's hardcore-death broadcast channel — beyond 1.12, + /// which has no type past `0x5D` ([`benilla_protocol::messages::CHAT_MSG_HARDCORE`] carries + /// where the id was read). The chain's own `ChatFrame.xml` registers the event and its + /// `GlobalStrings.lua` has the `[Hardcore] %s: ` format, so the window needs nothing from us + /// but the fire. + Hardcore, /// `CHAT_MSG_FILTERED` (`0x5B`), the server's "your message was filtered" notice; `arg2` is the /// addressee the stock frame formats `CHAT_FILTERED` over (`ChatFrame.lua:1406`). Filtered, @@ -225,6 +231,7 @@ impl ChatEventKind { K::RaidWarning, K::RaidBossEmote, K::Filtered, + K::Hardcore, K::Battleground, K::BattlegroundLeader, K::BgSystemNeutral, @@ -409,6 +416,7 @@ pub(crate) fn event_name(kind: ChatEventKind) -> &'static str { K::RaidWarning => "CHAT_MSG_RAID_WARNING", K::RaidBossEmote => "CHAT_MSG_RAID_BOSS_EMOTE", K::Filtered => "CHAT_MSG_FILTERED", + K::Hardcore => "CHAT_MSG_HARDCORE", K::Battleground => "CHAT_MSG_BATTLEGROUND", K::BattlegroundLeader => "CHAT_MSG_BATTLEGROUND_LEADER", K::BgSystemNeutral => "CHAT_MSG_BG_SYSTEM_NEUTRAL", @@ -546,6 +554,9 @@ pub(crate) fn default_color(kind: ChatEventKind) -> [u8; 3] { // `ChatTypeInfo["FILTERED"]` has no colour (`ChatFrame.lua:112`), so the line takes the // window's default, white; no bubble or edit box header shows this kind. K::Filtered => [255, 255, 255], + // The realm adds no `ChatTypeInfo["HARDCORE"]` colour of its own, so the line takes the + // window's default the way `FILTERED` does. + K::Hardcore => [255, 255, 255], K::Skill => [85, 85, 255], K::Loot => [0, 170, 0], K::Money => [255, 255, 0], @@ -643,6 +654,7 @@ pub(crate) fn kind_of_wire(chat_type: u8) -> Option { m::CHAT_MSG_RAID_WARNING => K::RaidWarning, m::CHAT_MSG_RAID_BOSS_EMOTE => K::RaidBossEmote, m::CHAT_MSG_FILTERED => K::Filtered, + m::CHAT_MSG_HARDCORE => K::Hardcore, m::CHAT_MSG_BATTLEGROUND => K::Battleground, m::CHAT_MSG_BATTLEGROUND_LEADER => K::BattlegroundLeader, m::CHAT_MSG_BG_SYSTEM_NEUTRAL => K::BgSystemNeutral, @@ -685,3 +697,32 @@ pub(crate) fn language_name(id: u32) -> &'static str { _ => "", // 0, Universal, and unknown ids: no header } } + +#[cfg(test)] +mod hardcore_tests { + use super::{event_name, kind_of_wire, ChatEventKind as K}; + + /// The realm's hardcore channel, end to end through the decode: wire type `0x5E` is a kind, + /// that kind names the event the chain's `ChatFrame.xml` registers, and nothing in 1.12's own + /// range moved to make room for it. + /// + /// Pinned because the id came off two detours in the realm's patched `WoW.exe` and nothing + /// else states it: `reference/1.12-globals.tsv` has no `CHAT_MSG_HARDCORE`, so a wrong id + /// would simply mean a channel that never arrives, which is what it looked like before. + #[test] + fn the_realms_hardcore_chat_type_decodes_to_its_event() { + assert_eq!( + kind_of_wire(benilla_protocol::messages::CHAT_MSG_HARDCORE), + Some(K::Hardcore) + ); + assert_eq!(event_name(K::Hardcore), "CHAT_MSG_HARDCORE"); + assert_eq!(benilla_protocol::messages::CHAT_MSG_HARDCORE, 0x5E); + // 1.12's own types stop at BATTLEGROUND_LEADER, and its BG_SYSTEM block is far below, so + // `0x5E` was free; a decode for `0x5F` is still nobody's. + assert_eq!( + benilla_protocol::messages::CHAT_MSG_BATTLEGROUND_LEADER + 1, + benilla_protocol::messages::CHAT_MSG_HARDCORE + ); + assert_eq!(kind_of_wire(0x5F), None); + } +} diff --git a/crates/benilla-app/src/ui_chat/tests.rs b/crates/benilla-app/src/ui_chat/tests.rs index dc3a8ca9..0934bc40 100644 --- a/crates/benilla-app/src/ui_chat/tests.rs +++ b/crates/benilla-app/src/ui_chat/tests.rs @@ -1107,7 +1107,7 @@ fn every_kind_is_in_all() { seen.sort_unstable(); seen.dedup(); assert_eq!(seen.len(), before, "a kind is listed twice in ALL"); - assert_eq!(before, 93, "93 kinds — update this when the kind set grows"); + assert_eq!(before, 94, "94 kinds — update this when the kind set grows"); } #[test] diff --git a/crates/benilla-app/src/ui_script/reference_ui.rs b/crates/benilla-app/src/ui_script/reference_ui.rs index 0f25a93b..001d51b2 100644 --- a/crates/benilla-app/src/ui_script/reference_ui.rs +++ b/crates/benilla-app/src/ui_script/reference_ui.rs @@ -2250,12 +2250,6 @@ mod tests { // Registered by a chain file we load and fired by nothing here; each says why. const UNPRODUCED: &[(&str, &str)] = &[ - ( - "CHAT_MSG_HARDCORE", - "ChatFrame.xml — the realm's own hardcore-death broadcast channel. Its chat type \ - has no producer here: benilla routes no such server message, so the window \ - simply never receives one. Costs the death announcements, nothing functional", - ), ( "MODIFIER_STATE_CHANGED", "AIO's Dep_LibWindow-1.1 — a 2.x event for live modifier-key tracking, used to \ diff --git a/crates/benilla-protocol/src/messages/chat.rs b/crates/benilla-protocol/src/messages/chat.rs index f5c56342..afc6ae91 100644 --- a/crates/benilla-protocol/src/messages/chat.rs +++ b/crates/benilla-protocol/src/messages/chat.rs @@ -40,6 +40,18 @@ pub const CHAT_MSG_FILTERED: u8 = 0x5B; pub const CHAT_MSG_BATTLEGROUND: u8 = 0x5C; pub const CHAT_MSG_BATTLEGROUND_LEADER: u8 = 0x5D; +/// Beyond 1.12, whose last type is `0x5D`: the realm's own hardcore-death broadcast channel, +/// `[Hardcore] %s: ` (`CHAT_HARDCORE_GET` in its `GlobalStrings.lua`). +/// +/// The id is read off the realm's patched `WoW.exe`, which carries this in a PE section it names +/// `.tdata` alongside the string `"HARDCORE"` at `0xd06000`. Two detours do it: `0x49f7aa`, in the +/// type-name lookup, jumps to `0xd06020`, which sets the name pointer to that string and the type +/// to `$0x5e` before returning; and `0x49b193`, inside the `CHAT_MSG_*` fire helper `0x49b0b0`, +/// jumps to `0xd06040`, which tests the type against `$0x5e` and swaps in the event-name index +/// `0x21f`. Nothing in 1.12 claims `0x5E` — its `CHAT_MSG_BG_SYSTEM_*` sit at `0x52`-`0x54` — so +/// there is no collision to arbitrate. +pub const CHAT_MSG_HARDCORE: u8 = 0x5E; + /// The only chat types the 1.12 client runs its `$`-macro expander over; the rest reach the frame /// verbatim. This is the wire gate (`0x49d5e4-0x49d606`); the re-expansion after a name query /// (`0x49cf36`) omits `0x5A`. diff --git a/crates/benilla-protocol/src/messages/mod.rs b/crates/benilla-protocol/src/messages/mod.rs index e0814a6a..99721921 100644 --- a/crates/benilla-protocol/src/messages/mod.rs +++ b/crates/benilla-protocol/src/messages/mod.rs @@ -83,9 +83,9 @@ pub use chat::{ chat_tag, ChatMessage, CHAT_MSG_AFK, CHAT_MSG_BATTLEGROUND, CHAT_MSG_BATTLEGROUND_LEADER, CHAT_MSG_BG_SYSTEM_ALLIANCE, CHAT_MSG_BG_SYSTEM_HORDE, CHAT_MSG_BG_SYSTEM_NEUTRAL, CHAT_MSG_CHANNEL, CHAT_MSG_DND, CHAT_MSG_EMOTE, CHAT_MSG_FILTERED, CHAT_MSG_GUILD, - CHAT_MSG_IGNORED, CHAT_MSG_MONSTER_EMOTE, CHAT_MSG_MONSTER_SAY, CHAT_MSG_MONSTER_WHISPER, - CHAT_MSG_MONSTER_YELL, CHAT_MSG_OFFICER, CHAT_MSG_PARTY, CHAT_MSG_RAID, - CHAT_MSG_RAID_BOSS_EMOTE, CHAT_MSG_RAID_BOSS_WHISPER, CHAT_MSG_RAID_LEADER, + CHAT_MSG_HARDCORE, CHAT_MSG_IGNORED, CHAT_MSG_MONSTER_EMOTE, CHAT_MSG_MONSTER_SAY, + CHAT_MSG_MONSTER_WHISPER, CHAT_MSG_MONSTER_YELL, CHAT_MSG_OFFICER, CHAT_MSG_PARTY, + CHAT_MSG_RAID, CHAT_MSG_RAID_BOSS_EMOTE, CHAT_MSG_RAID_BOSS_WHISPER, CHAT_MSG_RAID_LEADER, CHAT_MSG_RAID_WARNING, CHAT_MSG_SAY, CHAT_MSG_SYSTEM, CHAT_MSG_WHISPER, CHAT_MSG_WHISPER_INFORM, CHAT_MSG_YELL, MACRO_EXPANDED_TYPES, };