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:
co-authored by
Claude Opus 5.5
parent
6633a11d60
commit
829194ac1a
@@ -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 {
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user