- an incoming trade request is answered by the reference's eight-leg ladder, not by reflex — before, `CMSG_BEGIN_TRADE` went back the same frame it decoded, so any player in range could put a window on your screen mid-cinematic, mid-loading, while mind-controlled, at an auctioneer, or from your own ignore list; three legs answer with something other than "busy" (the ignore list sends `CMSG_IGNORE_TRADE`, two legs send no packet at all), Block Trades ships as the CVar and options row it is in 1.12 with its refusal on the chat line the catalog says, and the consent popup that was built came back out — 1.12 signals the trade dialog from nowhere, so asking first would be a divergence every player and addon feels, and the reference's answer to an unwanted trade is already in the ladder - the client's message catalog ships as a generated table instead of being read by eye: every refusal, notice and confirmation goes through one dispatcher taking only a message id into a 465-record registry, benilla names 163 of the keys, and the surface does not follow the key names — the byte-exact sweep settles 170 chat / 28 yellow / 267 red, none on the console arm, with the skill-up trio speaking CHAT_MSG_SKILL rather than system; a message's surface now comes from its record, not its call site, which found `ERR_SPELL_FAILED_NOTUNSHEATHED` painted red where the client paints it yellow (seventeen of our eighteen cast-fail overrides are red; that one is not, and nothing downstream could have known which id the dispatch chose) — and a new test walks the workspace's own source asserting every `ERR_` literal is a real catalog row, because an unresolvable key shows NOTHING and nobody files a bug about a toast they never saw - starting an attack stands you up — the stand sits after the reference's attackability cluster and before any range logic, so an attackable target out of reach stands you, a friendly NPC does not, and re-issuing an attack you are already making stands you too; the rest of the intended build is proven unnecessary rather than deferred: bare ground cannot stand you on any client (its two requirements are mutually exclusive in both CVar configurations), gossip is proven not to stand, and the loot leg is unproven and left alone - a kick returns you to the login screen again instead of the client reconnecting indefinitely and taking the account back off whoever kicked it: "nobody is here" is DECLARED now, never inferred — the login environment variables answer "log in without waiting for someone to type", which cannot answer "is anybody here?", so a run acts unattended only on `$WOW_UNATTENDED` (implied by a capture or rig run), with the doubt deliberately falling toward a loud probe timeout over a silent account fight; and the player-tests gate finally resolves the install that sourcing FrameXML off the chain has required, instead of failing red for its own reasons on any shell without `$WOW_DATA` exported Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
83 lines
3.8 KiB
Rust
83 lines
3.8 KiB
Rust
//! Every `ERR_*` key this workspace names must be a real row of the client's message catalog.
|
|
//!
|
|
//! benilla writes these keys as string literals at ~160 call sites, and each one used to arrive by
|
|
//! hand-tracing a `push <id>; call CGGameUI::DisplayError` in the binary. A typo, a key from a
|
|
//! later expansion, or a plausible-looking invention would all have been invisible: the string
|
|
//! simply would not resolve in `GlobalStrings.lua`, and the reference's own data-suppression rule
|
|
//! means benilla shows *nothing* for an unresolvable key. Silence is the failure mode, which is the
|
|
//! worst kind — nobody files a bug about a toast they never saw.
|
|
//!
|
|
//! Since decision 1770 the surface those keys are shown on is read from
|
|
//! [`benilla_ui::messages`] rather than carried beside them, so a key that is not in the catalog
|
|
//! also silently takes the fallback. This walk is what keeps that fallback unreachable: it is the
|
|
//! tripwire that turns "we believe these are message ids" into a checked claim.
|
|
//!
|
|
//! It reads source text rather than any registry the code keeps, deliberately — a registry could
|
|
//! only contain what somebody remembered to register.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::path::Path;
|
|
|
|
/// Not message ids, and not meant to be — both are fixtures for the unknown-key *fallback*, which
|
|
/// needs a key the client does not have in order to be tested at all: `ui_petition::lines`'
|
|
/// degrade-to-generic arm, and `benilla_ui::messages`' own proof that `kind_of` answers for a key
|
|
/// it has never heard of. Anything else appearing here is a defect, not an entry to add.
|
|
const NOT_A_MESSAGE: &[&str] = &["ERR_SOMETHING_UNCARVED", "ERR_NOT_A_REAL_MESSAGE"];
|
|
|
|
/// The generated table itself, which is the answer and not a question — walking it would make
|
|
/// every key trivially present and the check vacuous.
|
|
const GENERATED: &str = "catalog.rs";
|
|
|
|
fn walk(dir: &Path, into: &mut Vec<String>) {
|
|
for entry in std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display())) {
|
|
let path = entry.expect("dir entry").path();
|
|
if path.is_dir() {
|
|
walk(&path, into);
|
|
} else if path.extension().is_some_and(|e| e == "rs")
|
|
&& path.file_name().is_some_and(|n| n != GENERATED)
|
|
{
|
|
into.push(std::fs::read_to_string(&path).expect("read source"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Pull every `"ERR_…"` string literal out of a source file, the way the call sites write them.
|
|
fn err_keys(src: &str) -> impl Iterator<Item = String> + '_ {
|
|
src.match_indices("\"ERR_").filter_map(|(i, _)| {
|
|
let rest = &src[i + 1..];
|
|
let end = rest.find('"')?;
|
|
let key = &rest[..end];
|
|
key.chars()
|
|
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
|
|
.then(|| key.to_string())
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn every_error_key_in_the_source_is_a_catalog_row() {
|
|
let mut sources = Vec::new();
|
|
walk(Path::new("src"), &mut sources);
|
|
walk(Path::new("../benilla-ui/src"), &mut sources);
|
|
assert!(sources.len() > 100, "the walk found almost nothing to read");
|
|
|
|
let keys: BTreeSet<String> = sources.iter().flat_map(|s| err_keys(s)).collect();
|
|
// 163 at the time of writing — the point is only that the walk found the vocabulary and not
|
|
// a handful of files, so this is a floor and not a ledger to keep updated.
|
|
assert!(
|
|
keys.len() > 150,
|
|
"expected the hand-written `ERR_*` vocabulary, found {}",
|
|
keys.len()
|
|
);
|
|
|
|
let strays: Vec<&String> = keys
|
|
.iter()
|
|
.filter(|k| !NOT_A_MESSAGE.contains(&k.as_str()))
|
|
.filter(|k| benilla_ui::messages::by_key(k).is_none())
|
|
.collect();
|
|
assert!(
|
|
strays.is_empty(),
|
|
"these keys are not rows of the 5875 message catalog, so the client would show nothing \
|
|
for them: {strays:?}"
|
|
);
|
|
}
|