SendChatMessage sends nothing for an empty line unless its type is AFK or DND, checked after the chat type as 1.12.1 does (#499) (#505)

`SendChatMessage` with an empty line now sends nothing unless the type is AFK or DND, as in 1.12.1, where `0x49f28d`-`0x49f2a1` end the call on an empty first byte for every other type, after the chat-type check (`0x49f276`, "Unknown chat type" at `0x49f285`) and before the language, the target checks, the AFK clear and the tutorial acknowledge. Before, an empty line went out as an empty packet, and an empty SAY cleared a standing AFK.

Fixes #499

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-28 15:09:27 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 6633a11d60
commit 829194ac1a
3 changed files with 102 additions and 5 deletions
+6 -1
View File
@@ -1343,7 +1343,7 @@ fn auto_clear_afk(cvars: &crate::cvars::Cvars) -> bool {
/// Turn `SendChatMessage` calls into sends. No slash grammar runs here: an addon sending
/// `"/dance"` says six characters. An unknown chat-type token prints a system line and sends
/// nothing.
/// nothing; an empty line of any other type but AFK and DND sends nothing and prints nothing.
pub(super) fn drain_addon_chat_sends(
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
commands: Res<NetCommands>,
@@ -1372,6 +1372,11 @@ pub(super) fn drain_addon_chat_sends(
));
continue;
};
// An empty line of any type but AFK and DND ends the call here, after the type check and
// ahead of the target checks, the AFK clear and the tutorial acknowledge (`0x49f2a1`).
if send.ends_at_empty_line() {
continue;
}
// A channel target is a slot number, sent as that slot's name; with no such slot the call
// ends here (`0x49f4ea`), ahead of the AFK clear and the tutorial acknowledge.
if kind == super::edit::SendType::Channel {
+78
View File
@@ -2581,3 +2581,81 @@ fn a_channel_send_carries_the_numbered_slots_name() {
]
);
}
/// `SendChatMessage`'s empty-line gate through the real drain (`0x49f28d`-`0x49f2a1`): an empty
/// line, or one whose first byte is NUL, of any type but AFK and DND sends nothing and leaves a
/// standing AFK set, since the gate is ahead of the AFK clear. The type check comes first, so an
/// unknown type still reports.
#[test]
fn an_empty_line_sends_nothing_but_afk_and_dnd() {
use super::edit::{ChannelSlot, ChannelState};
use crate::net::{ChatKind, ClientCommand, NetCommands};
use bevy::ecs::system::RunSystemOnce;
let mut world = bevy::prelude::World::new();
world.insert_non_send_resource(benilla_ui::script::UiScript::new().expect("VM"));
let (tx, rx) = crossbeam_channel::unbounded();
world.insert_resource(NetCommands(tx));
world.init_resource::<super::feed::ChatLog>();
world.insert_resource(super::away::AfkMirror(2));
world.init_resource::<crate::cvars::Cvars>();
world.insert_resource(ChannelState {
joined: vec![Some(ChannelSlot::joined("General - Elwynn Forest"))],
..Default::default()
});
let script = |world: &bevy::prelude::World, lua: &str| {
world
.non_send_resource::<benilla_ui::script::UiScript>()
.run(lua)
.expect("lua");
};
let sent = |rx: &crossbeam_channel::Receiver<ClientCommand>| -> Vec<(ChatKind, String)> {
rx.try_iter()
.map(|c| match c {
ClientCommand::Chat { kind, text, .. } => (kind, text),
other => panic!("unexpected command {other:?}"),
})
.collect()
};
script(
&world,
r#"
MARKED_AFK_MESSAGE = "You are now AFK: %s"
CLEARED_AFK = "You are no longer AFK."
DEFAULT_AFK_MESSAGE = "Away from Keyboard"
SendChatMessage("")
SendChatMessage("", "PARTY")
SendChatMessage("", "WHISPER", nil, "Bob")
SendChatMessage("", "CHANNEL", nil, 1)
SendChatMessage("\0hidden", "YELL")
SendChatMessage("", "BOGUS")
"#,
);
world
.run_system_once(super::input::drain_addon_chat_sends)
.expect("drain");
assert_eq!(sent(&rx), vec![], "no empty line reaches the wire");
assert_eq!(
world.resource::<super::feed::ChatLog>().pending_lines(),
vec!["Unknown chat type \"BOGUS\"."],
"no AFK clear; the type check precedes the gate"
);
assert!(world.resource::<super::away::AfkMirror>().is_afk());
// A line with text clears the standing AFK and says itself; an empty AFK marks it again.
script(
&world,
r#"SendChatMessage("hi") SendChatMessage("", "AFK")"#,
);
world
.run_system_once(super::input::drain_addon_chat_sends)
.expect("drain");
assert_eq!(
sent(&rx),
vec![
(ChatKind::Afk, String::new()),
(ChatKind::Say, "hi".into()),
(ChatKind::Afk, "Away from Keyboard".into()),
]
);
}
+18 -4
View File
@@ -44,6 +44,20 @@ pub struct ChatSend {
pub language: Option<u32>,
}
impl ChatSend {
/// The empty-line gate (`0x49f28d`-`0x49f2a1`): a line whose first byte is NUL ends the call,
/// before the language is read or anything is sent, unless its type is AFK (`0x14`) or DND
/// (`0x15`). The chat-type lookup and its "Unknown chat type" (`0x49f27f`) come first.
pub fn ends_at_empty_line(&self) -> bool {
ends_at_empty_line(&self.text, &self.chat_type)
}
}
/// [`ChatSend::ends_at_empty_line`] on the binding's arguments, before the line is built.
fn ends_at_empty_line(text: &str, chat_type: &str) -> bool {
text.bytes().next().unwrap_or(0) == 0 && !matches!(chat_type, "AFK" | "DND")
}
impl super::UiScript {
/// Push `Languages.dbc` as `(ID, Name_lang)` rows in file order, the table `SendChatMessage`
/// matches its language name against (`[0xc0db40]`, count `[0xc0db44]`).
@@ -131,10 +145,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let chat_type = chat_type
.unwrap_or_else(|| "SAY".into())
.to_ascii_uppercase();
// The language is read only past the empty-line gate (`0x49f28d`-`0x49f2a1`),
// which lets an empty line through for AFK and DND alone.
let reads_language =
!text.is_empty() || matches!(chat_type.as_str(), "AFK" | "DND");
// The language is read only past the empty-line gate. The line is queued
// anyway: the type check that precedes the gate is the app's, and the drain
// applies the gate after it.
let reads_language = !ends_at_empty_line(&text, &chat_type);
let language = match super::binding_abi::optional_string(lua, &language) {
Some(name) if reads_language => {
let model = lua.app_data_ref::<Model>().expect("model app_data");