Files
benilla-octo/crates/benilla-app/src/probe_shield.rs
T
Sam SungandClaude Opus 5.5 4f8350c9ed String literals and test names in benilla-app carry no record numbers (#368)
The strings pass for `benilla-app`, the last crate: string literals and test names lose the private record numbers and the workspace's words that a comment pass cannot touch.

Fixes #92. Fixes #36.

- **Messages.** About 230 assertion, panic, `expect` and log messages, printed tool labels, and comments inside embedded Lua fixtures drop their record numbers; the eight that named "the director" keep their fact in plain words. Stale claims in a few messages are corrected on the way (a cast-bar step, a font line, the every-shot recompute).
- **Names.** Seven test and helper names that carried a record number or "director" now say what they check. `git grep` found no other use of any of them.
- **Player files.** The key-bindings file's header and `config.toml`'s header each lose a record citation. Neither header is compared when the file is read back: the bindings reader skips every `#` line, and `config.toml`'s header is a TOML comment.
- **Fixtures.** Test fixtures use neutral character, account and frame names.
- **#36.** The quest-material test labels GameObject type 10 GOOBER (in 1.12, 3 is CHEST, 9 TEXT, 10 GOOBER); the code under test was already right, and the comments were fixed with the comment pass.
- **Verification.** With comments stripped and every literal's contents masked, each file is identical to main and keeps its literals' line counts, apart from the renamed functions and one rustfmt reflow. `cargo fmt --check` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 01:06:28 +07:00

387 lines
15 KiB
Rust

//! The probe shield: a probe body cannot die, and does not need GM mode to stay alive.
//!
//! GM mode (`Player::SetGameMaster`) re-templates the player to faction 35 ("Friendly", enemy
//! mask 0) and freezes the mirror timers, so nothing hostility-, damage- or breath-related can be
//! measured with it on. `.cheat god` (`Player::SetCheatGod` → `SetInvincibilityHpThreshold(1)`)
//! instead makes `Unit::DealDamage` stop at 1 hp, environmental damage included, and changes
//! nothing a client can see. The shield is always armed; GM mode also stays on by default, since
//! the shield does not stop aggro, and `WOW_GM=off` drops it safely when a run needs faithful
//! factions. A shielded body with GM off in a hostile camp survives pinned at 1 hp.
//!
//! - Not persisted: the threshold is a runtime `Unit` member, so it is re-sent on every world
//! entry, and a body parked somewhere hostile is unshielded for the few hundred ms before the
//! first line goes out. GM mode on by default keeps it from being whittled down there.
//! - Target-sensitive: `.cheat god on` acts on the current selection and answers "Player not
//! found!" for a creature, so the command always names the character.
//! - `.die` clears it silently (`HandleDieHelper` calls `SetCheatGod(false)`), so a death re-arms
//! it.
//!
//! Only a probe account (`probe<N>`) is ever sent anything. `WOW_GM=off` (or `gm:off` in
//! `WOW_RIG`) drops GM mode; `WOW_GOD=off` runs unshielded, reported on the preflight banner.
use bevy::prelude::*;
use crate::login::LoginIntent;
use crate::names::NameCache;
use crate::net::{EnteredWorldMessage, ObjectStore, SelfGuid, SelfPlayer, ServerSaidMessage};
/// `PLAYER_FLAGS_GM`: the body carries the faction-35 re-template.
const PLAYER_FLAGS_GM: u32 = 0x0000_0008;
/// Spacing between the shield's lines: two server-side flips inside one net drain merge to a
/// no-op, and `.gm off` must land after the shield is up.
const STEP_SECS: f32 = 0.8;
/// How long to wait for our own name, which makes the command selection-proof, before arming
/// without it.
const NAME_WAIT_SECS: f32 = 3.0;
/// How long the server gets to confirm before the body is reported unshielded; the round trip is
/// ~100-150 ms.
const CONFIRM_SECS: f32 = 5.0;
pub(crate) struct ProbeShieldPlugin;
impl Plugin for ProbeShieldPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ProbeShield>().add_systems(
Update,
drive_shield.after(benilla_world::schedule::WorldStage::Net),
);
}
}
/// What the preflight banner says about the shield.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub(crate) enum ShieldReport {
/// Not a probe account: the shield never sends anything.
#[default]
NotOurs,
/// `WOW_GOD=off`: deliberately unshielded. This body CAN die.
Disabled,
/// The command is out, the server has not answered yet.
Arming,
/// The server confirmed it; this body cannot die.
Armed,
/// The server refused or did not confirm inside [`CONFIRM_SECS`]; the body is mortal.
Unconfirmed,
}
/// The shield's state for this session.
#[derive(Resource)]
pub(crate) struct ProbeShield {
report: ShieldReport,
/// Queued lines and when the next one may go out.
steps: Vec<String>,
next_at: f32,
/// `Time::elapsed_secs` after which an unanswered `.cheat god on` is called unconfirmed.
confirm_by: f32,
/// Set at world entry, cleared once the batch is built.
arm_wanted: Option<f32>,
/// Our own death state last frame, so a death edge differs from arriving dead; `None` until
/// the first descriptor read of this world entry.
was_dead: Option<bool>,
}
impl Default for ProbeShield {
fn default() -> Self {
Self {
report: ShieldReport::NotOurs, // decided once we know whose account this is
steps: Vec::new(),
next_at: 0.0,
confirm_by: 0.0,
arm_wanted: None,
was_dead: None,
}
}
}
impl ProbeShield {
/// What the preflight banner should say.
pub(crate) fn report(&self) -> ShieldReport {
self.report
}
}
/// Whether `user` is a probe account, `probe` followed by digits: the identity an unattended run
/// logs in with (`docs/CONTRIBUTING.md`, "Running it unattended"). No other account is touched.
fn is_probe_account(user: &str) -> bool {
// Byte-wise: a `&str` slice at 5 would panic mid-char on non-ASCII input.
let Some((prefix, digits)) = user.as_bytes().split_at_checked(5) else {
return false;
};
prefix.eq_ignore_ascii_case(b"probe")
&& !digits.is_empty()
&& digits.iter().all(u8::is_ascii_digit)
}
/// Whether this run wants GM mode off: `WOW_GM=off`, or `gm:off` in `WOW_RIG`, so the shield
/// never sends `.gm` against the rig's own step. The preflight banner reads it too.
pub(crate) fn wants_gm_off() -> bool {
std::env::var("WOW_GM").is_ok_and(|v| v.eq_ignore_ascii_case("off"))
|| std::env::var("WOW_RIG").is_ok_and(|spec| rig_asks_for_gm_off(&spec))
}
/// Whether a `WOW_RIG` spec carries `gm:off`, matched as [`crate::capture::probe_rig`] matches
/// it: case-insensitive key, anything but `on`/`1` false.
fn rig_asks_for_gm_off(spec: &str) -> bool {
spec.split_whitespace().any(|tok| {
matches!(
tok.to_ascii_lowercase().split_once(':'),
Some(("gm", v)) if !matches!(v, "on" | "1")
)
})
}
/// Whether `WOW_GOD=off` asked for an unshielded run, read once.
fn disabled_by_env() -> bool {
static OFF: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
std::env::var("WOW_GOD").is_ok_and(|v| v.eq_ignore_ascii_case("off"))
});
*OFF
}
/// Classify one `CHAT_MSG_SYSTEM` line as an answer about god mode. The chat strings are
/// `mangos_string` 368/369 (`LANG_YOU_SET_GOD`, `LANG_YOUR_GOD_SET`); `LANG_CHEAT_GOD_ON/OFF`
/// (349/350) arrive as `SMSG_NOTIFICATION` and never reach here.
fn god_verdict(text: &str) -> Option<bool> {
let lower = text.to_ascii_lowercase();
if !lower.contains("god mode") {
return None;
}
// "You set god mode to on for X." / "Your god mode has been turned off by X."
if lower.contains(" to on") || lower.contains("turned on") {
Some(true)
} else if lower.contains(" to off") || lower.contains("turned off") {
Some(false)
} else {
None
}
}
/// Arm the shield on every world entry, confirm it against the server's own answer, and put it
/// back up whenever something drops it.
fn drive_shield(
mut shield: ResMut<ProbeShield>,
mut entered: MessageReader<EnteredWorldMessage>,
mut said: MessageReader<ServerSaidMessage>,
time: Res<Time>,
intent: Res<LoginIntent>,
names: Res<NameCache>,
self_guid: Res<SelfGuid>,
self_q: Query<&ObjectStore, With<SelfPlayer>>,
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
) {
let entered_world = entered.read().next().is_some();
let ours = intent.account().is_some_and(is_probe_account);
if !ours || disabled_by_env() {
// Still drain, so a later frame never sees a stale backlog.
said.read().for_each(drop);
shield.report = if ours {
ShieldReport::Disabled
} else {
ShieldReport::NotOurs
};
if entered_world && ours {
warn!(
"probe-shield: DISABLED by WOW_GOD=off — this probe character CAN die. That is the \
right setting for a death-arc test; unset it for everything else."
);
}
return;
}
if entered_world {
shield.arm_wanted = Some(time.elapsed_secs());
shield.was_dead = None;
shield.report = ShieldReport::Arming;
}
// The server's answer is the only evidence: the flag rides no descriptor field.
for msg in said.read() {
match god_verdict(&msg.text) {
Some(true) => {
if shield.report != ShieldReport::Armed {
info!(
"probe-shield: ARMED — this character cannot die (vmangos `.cheat god`: \
damage clamps at 1 hp instead of killing). Hostility, factions, aggro and \
environmental damage all stay faithful, unlike GM mode. It is NOT \
persisted, so it is re-armed on every world entry."
);
}
shield.report = ShieldReport::Armed;
}
Some(false) => {
warn!(
"probe-shield: the shield was turned OFF — putting it back up. If you meant to \
run unshielded, use WOW_GOD=off (which also stops this re-arm) rather than \
`.cheat god off`."
);
rearm(&mut shield, &time, &names, &self_guid);
}
None => {}
}
}
// A death proves the shield was down; `.die` clears it silently before killing.
if let Ok(store) = self_q.single() {
let dead = store.0.unit_is_dead() || store.0.player_is_ghost();
match shield.was_dead {
None => shield.was_dead = Some(dead), // first read of this entry: a state, not an edge
Some(false) if dead => {
shield.was_dead = Some(true);
warn!(
"probe-shield: THE CHARACTER DIED — so the shield was down. `.die` clears it \
silently by design; anything else means it never landed. Re-arming now (it \
does not revive: send `.revive`)."
);
rearm(&mut shield, &time, &names, &self_guid);
}
Some(_) => shield.was_dead = Some(dead),
}
}
// Build the batch once the body, and ideally our own name, is there to address.
if let Some(wanted_at) = shield.arm_wanted {
let Ok(store) = self_q.single() else { return };
let name = self_guid.0.and_then(|g| names.peek(g));
let waited = time.elapsed_secs() - wanted_at;
if name.is_none() && waited < NAME_WAIT_SECS {
return;
}
if name.is_none() {
warn!(
"probe-shield: our own name has not resolved after {NAME_WAIT_SECS:.0}s — arming \
without it. `.cheat god` re-targets to the current selection, so this can be \
refused with \"Player not found!\" if anything is targeted."
);
}
shield.arm_wanted = None;
shield.steps.push(god_line(name));
shield.confirm_by = time.elapsed_secs() + CONFIRM_SECS;
let gm_is_on = store.0.player_flags() & PLAYER_FLAGS_GM != 0;
match (gm_is_on, wants_gm_off()) {
// The default: GM mode stays on, so nothing aggros a parked body.
(false, false) => {
info!(
"probe-shield: GM mode is off — turning it back ON, so a parked body is not \
permanently mobbed. Readings taken now are faction-35 readings and are wrong \
for anything about hostility, colour, threat, aggro, damage or timers \
— set WOW_GM=off for those, which is safe: the shield holds."
);
shield.steps.push(".gm on".into());
}
(true, true) => {
info!(
"probe-shield: WOW_GM=off — turning GM mode OFF so factions, hostility and \
damage are faithful. Safe: the shield is what keeps this body alive now."
);
shield.steps.push(".gm off".into());
}
_ => {} // already as asked for
}
shield.next_at = time.elapsed_secs();
}
// Send the queued lines, one per `STEP_SECS`, god first.
if !shield.steps.is_empty() {
let Some(mut script) = script else { return };
while !shield.steps.is_empty() && time.elapsed_secs() >= shield.next_at {
let line = shield.steps.remove(0);
debug!("probe-shield: sending {line:?}");
script.push_chat_input(line);
shield.next_at = time.elapsed_secs() + STEP_SECS;
}
return;
}
// Nothing outstanding: a `.cheat god on` that was never answered did not land.
if shield.report == ShieldReport::Arming && time.elapsed_secs() > shield.confirm_by {
shield.report = ShieldReport::Unconfirmed;
warn!(
"probe-shield: NOT CONFIRMED — `.cheat god on` went out and the server never answered \
it. Treat this character as mortal. Check the account's GM level (`.cheat` needs \
SEC_GAMEMASTER 3; probe accounts are 6): \
SELECT gmlevel FROM realmd.account_access WHERE id = <account>."
);
}
}
/// The command, naming the character when known: the bare form acts on the current selection.
fn god_line(name: Option<&str>) -> String {
match name {
Some(n) => format!(".cheat god on {n}"),
None => ".cheat god on".into(),
}
}
/// Queue another `.cheat god on` and re-open the confirmation window.
fn rearm(shield: &mut ProbeShield, time: &Time, names: &NameCache, self_guid: &SelfGuid) {
let name = self_guid.0.and_then(|g| names.peek(g));
shield.steps.push(god_line(name));
shield.next_at = time.elapsed_secs();
shield.confirm_by = time.elapsed_secs() + CONFIRM_SECS;
shield.report = ShieldReport::Arming;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_probe_accounts_are_ours() {
for user in ["probe0", "probe7", "PROBE12"] {
assert!(is_probe_account(user), "{user} is a probe account");
}
for user in ["one", "two", "probe", "probeone", "aprobe1", "probe1x"] {
assert!(!is_probe_account(user), "{user} is NOT a probe account");
}
}
#[test]
fn the_servers_own_words_are_what_confirm_it() {
// vmangos 368, the answer to `.cheat god on <name>`.
assert_eq!(
god_verdict("You set god mode to on for |cffffffff|Hplayer:Probetwo|h[Probetwo]|h|r."),
Some(true)
);
assert_eq!(
god_verdict("You set god mode to off for [Probetwo]."),
Some(false)
);
// vmangos 369, when another GM does it to us.
assert_eq!(
god_verdict("Your god mode has been turned off by [Someone]."),
Some(false)
);
for other in [
"Player not found!",
"GM mode is OFF",
"There is no such command",
"Welcome to World of Warcraft!",
] {
assert_eq!(god_verdict(other), None, "{other}");
}
}
#[test]
fn the_rigs_own_gm_token_wins_over_the_default() {
assert!(rig_asks_for_gm_off("gnome mage 39 gm:off"));
assert!(rig_asks_for_gm_off("GM:0"));
for spec in [
"tauren druid 60 gm:on at:ThunderBluff",
"GM:1",
"60 gear:dps-preraid-bis",
"",
] {
assert!(!rig_asks_for_gm_off(spec), "{spec:?}");
}
}
#[test]
fn the_command_always_names_its_target() {
assert_eq!(god_line(Some("Probetwo")), ".cheat god on Probetwo");
assert_eq!(god_line(None), ".cheat god on");
}
}