/dnd clears a standing AFK first, as every chat type but AFK does (#186)

- fix: clear a standing AFK when /dnd is sent
- The AFK clear runs once, ahead of the type arms, and a test drives /afk then /dnd through the real drain

Fixes #185

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
This commit is contained in:
Batuhan
2026-09-26 14:51:49 +07:00
committed by GitHub
co-authored by Claude Opus 5.5 Sam
parent 2f35f63274
commit 4ebc2a9f23
3 changed files with 80 additions and 52 deletions
-30
View File
@@ -314,36 +314,6 @@ mod tests {
);
}
/// `/afk` then `/dnd` prints three lines: the auto-clear skips only type `0x14` (`0x49f4f6`),
/// so `/dnd` (`0x15`) clears AFK first.
#[test]
fn afk_then_dnd_prints_three_lines() {
let mut mirror = AfkMirror::default();
let mut out = Vec::new();
let first = afk_line("", mirror, &strings);
out.push(first.line.clone().unwrap());
mirror.0 = first.mirror.unwrap();
// `/dnd` takes the generic path first: the clear fires because the mirror is set.
let cleared = auto_clear_line(mirror, true, &strings).expect("the implicit clear fires");
out.push(cleared);
mirror.0 = 0;
// Then the DND arm: `0x49f3c7` sits above the type dispatch.
assert!(!mirror.is_afk(), "the clear really cleared it");
out.push(dnd_line("", false, &strings).line.unwrap());
assert_eq!(
out,
vec![
"You are now AFK: Away from Keyboard",
"You are no longer AFK.",
"You are now DND: Do not Disturb.",
],
"the reference capture, line for line"
);
}
#[test]
fn the_auto_clear_is_gated_both_ways() {
assert!(
+19 -22
View File
@@ -1398,11 +1398,25 @@ pub(super) fn drain_addon_chat_sends(
}
// ── The away commands, and the AFK clear the other sends carry ───────────────────────
//
// `SendChatMessage` (`0x49f1e0`): AFK and DND each have an arm ahead of the generic send,
// and every type but AFK (`0x14`) first clears a standing AFK. The system lines, the
// default text and the mirror are `super::away`'s.
// `SendChatMessage` (`0x49f1e0`): every type but AFK (`0x14`) first clears a standing AFK
// (`0x49f4f6 jne 0x49f3c7`), `/dnd` included; then AFK and DND each have an arm ahead of
// the generic send. The system lines, the default text and the mirror are `super::away`'s.
let strings = |key: &str| crate::ui_chat::combat::global_string(&script, key);
let wire = kind.wire();
if !matches!(wire, crate::net::ChatKind::Afk) {
if let Some(line) =
super::away::auto_clear_line(*mirror, auto_clear_afk(&cvars), &strings)
{
super::away::push_system(&mut chat_log, line);
mirror.0 = 0;
// The empty `0x14` that tells the server, ahead of the message's own packet.
let _ = commands.0.send(ClientCommand::Chat {
kind: crate::net::ChatKind::Afk,
target: None,
text: String::new(),
});
}
}
let text = match wire {
crate::net::ChatKind::Afk => {
let out = super::away::afk_line(&send.text, *mirror, &strings);
@@ -1415,31 +1429,14 @@ pub(super) fn drain_addon_chat_sends(
out.body
}
crate::net::ChatKind::Dnd => {
// DND has no mirror (`0x49f3f0`): the live descriptor bit. The reference's DND
// also clears a standing AFK first (`0x49f3d6`); this arm does not.
// DND has no mirror (`0x49f3f0`): the live descriptor bit.
let out = super::away::dnd_line(&send.text, is_dnd(&self_q), &strings);
if let Some(line) = out.line {
super::away::push_system(&mut chat_log, line);
}
out.body
}
// Clear a standing AFK first (`0x49f3d6` skips only type `0x14`), then send the
// line's own packet.
_ => {
if let Some(line) =
super::away::auto_clear_line(*mirror, auto_clear_afk(&cvars), &strings)
{
super::away::push_system(&mut chat_log, line);
mirror.0 = 0;
// The empty `0x14` that tells the server, alongside the message's own packet.
let _ = commands.0.send(ClientCommand::Chat {
kind: crate::net::ChatKind::Afk,
target: None,
text: String::new(),
});
}
send.text
}
_ => send.text,
};
let cmd = ClientCommand::Chat {
kind: wire,
+61
View File
@@ -2392,6 +2392,67 @@ fn the_chat_cache_restore_is_finished_before_player_login() {
let _ = std::fs::remove_dir_all(&tmp);
}
/// `/afk` then `/dnd` through the real drain: every type but AFK clears a standing AFK first
/// (`0x49f4f6 jne 0x49f3c7`), so `/dnd` (`0x15`) prints the clear and sends the empty `0x14` ahead
/// of its own line and packet, three lines in all.
#[test]
fn afk_then_dnd_clears_the_afk_first() {
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.init_resource::<super::away::AfkMirror>();
world.init_resource::<crate::cvars::Cvars>();
world.init_resource::<super::edit::ChannelState>();
// The five `GlobalStrings.lua` keys the away lines read, at their enUS values.
world
.non_send_resource::<benilla_ui::script::UiScript>()
.run(
r#"
MARKED_AFK_MESSAGE = "You are now AFK: %s"
CLEARED_AFK = "You are no longer AFK."
MARKED_DND = "You are now DND: %s."
DEFAULT_AFK_MESSAGE = "Away from Keyboard"
DEFAULT_DND_MESSAGE = "Do not Disturb"
SendChatMessage("", "AFK")
SendChatMessage("", "DND")
"#,
)
.expect("lua");
world
.run_system_once(super::input::drain_addon_chat_sends)
.expect("drain");
assert_eq!(
world.resource::<super::feed::ChatLog>().pending_lines(),
vec![
"You are now AFK: Away from Keyboard",
"You are no longer AFK.",
"You are now DND: Do not Disturb.",
]
);
let sent: Vec<(ChatKind, String)> = rx
.try_iter()
.map(|c| match c {
ClientCommand::Chat { kind, text, .. } => (kind, text),
other => panic!("unexpected command {other:?}"),
})
.collect();
assert_eq!(
sent,
vec![
(ChatKind::Afk, "Away from Keyboard".into()),
(ChatKind::Afk, String::new()),
(ChatKind::Dnd, "Do not Disturb".into()),
]
);
assert!(!world.resource::<super::away::AfkMirror>().is_afk());
}
/// `SendChatMessage`'s `CHANNEL` target through the real drain: `SStrToInt` into `0x49be50`
/// (`0x49f4d9`-`0x49f4ea`), so the packet carries the numbered slot's name, and a number naming
/// no confirmed slot, or a name, sends nothing at all.