From 4ebc2a9f232a1f97dfdfbe054885eb532bb9f21b Mon Sep 17 00:00:00 2001 From: Batuhan Date: Sat, 26 Sep 2026 10:51:49 +0300 Subject: [PATCH] /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) Co-Authored-By: Sam --- crates/benilla-app/src/ui_chat/away.rs | 30 ---------- crates/benilla-app/src/ui_chat/input/mod.rs | 41 +++++++------- crates/benilla-app/src/ui_chat/tests.rs | 61 +++++++++++++++++++++ 3 files changed, 80 insertions(+), 52 deletions(-) diff --git a/crates/benilla-app/src/ui_chat/away.rs b/crates/benilla-app/src/ui_chat/away.rs index 62d9c193..bcc9f15f 100644 --- a/crates/benilla-app/src/ui_chat/away.rs +++ b/crates/benilla-app/src/ui_chat/away.rs @@ -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!( diff --git a/crates/benilla-app/src/ui_chat/input/mod.rs b/crates/benilla-app/src/ui_chat/input/mod.rs index 98045303..12736c23 100644 --- a/crates/benilla-app/src/ui_chat/input/mod.rs +++ b/crates/benilla-app/src/ui_chat/input/mod.rs @@ -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, diff --git a/crates/benilla-app/src/ui_chat/tests.rs b/crates/benilla-app/src/ui_chat/tests.rs index 16331236..9eaa0046 100644 --- a/crates/benilla-app/src/ui_chat/tests.rs +++ b/crates/benilla-app/src/ui_chat/tests.rs @@ -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::(); + world.init_resource::(); + world.init_resource::(); + world.init_resource::(); + // The five `GlobalStrings.lua` keys the away lines read, at their enUS values. + world + .non_send_resource::() + .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::().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::().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.