- a loot refusal is a message id, not a sentence: `loot_error_text` composed eight sentences for `SMSG_LOOT_RESPONSE`'s refusals and six of them said something 1.12 never says — "too far away to loot that." for a string that ends "that corpse.", "You can't loot that from there." for "You must be facing the corpse to loot it.", and an invented "Those pockets are already empty." for a code the client has no string for; it and the `LootErrors` queue are deleted, and the refusal is raised by GlobalStrings key into `UiErrorKeys` at the net bridge, the way `fish_verdict` two functions above already did, off the reference's own eleven-entry jump table at `0x5ebc64`; a composed string had no record to read a surface or a voice off, so all eleven — `MsgKind::Error`, the red `UIErrorsFrame` line — had been going into the chat window as yellow SYSTEM lines, and the four spoken in the player's own race and gender (`DIDNT_KILL`, `BAD_FACING`, `LOCKED`, `TOO_FAR`) were silent; a second bug fell out of reading the leg: codes 4–9, 11 and the default fall through a release tail at `0x5ebac2` that zeroes `[player+0x1d28]` — our `LootLatch` — while 10, 12, 13 and 14 jump past it, so a master looter refused a hand-off no longer stands up out of the kneel at a still-open corpse; the default arm's `ja 0x5ebabd` lands on code 7's arm, so every unnamed code prints `ERR_LOOT_DIDNT_KILL` and releases (2035); and the pet has its own two message tables, not the player's with a flag: `SMSG_PET_ACTION_FEEDBACK`'s handler `0x4bdb70` decrements the byte, bounds it at 3 and jump-tables it through `0x4bdbe8` to `ERR_PET_SPELL_DEAD`, `ERR_NO_ATTACK_TARGET`, `ERR_INVALID_ATTACK_TARGET`, `ERR_PET_SPELL_NOPATH` — one per vmangos `PetFeedback` value, where our two-entry map had printed "No path available for your pet" for a dead pet, "Out of range" for no target and nothing for 3 and 4; `SMSG_PET_CAST_FAILED` is `0x6e8eb0`, a separate 142-byte index table that overrides ten reasons, six of them to `ERR_PET_SPELL_*` rows with no other raise site in the client, so `CastFail` grows a `Caster` and picks its table from it while layers 1 and 3 stay shared, as the binary shares them; `0x6e8eb0` calls neither the combat-log formatter `0x62c360` nor the error sound `0x458a50`, so a pet's refused Growl no longer prints "You fail to cast Growl: …" as the player's own failure; and the source walk that exists to catch a non-catalog key was blind to both wrong keys because all 465 catalog keys begin `ERR_`, so the tripwire moves to the funnel — `Shown::keyed` warns on a key `by_key` cannot resolve (2033)
- the hunter-pet feedback cluster, five opcodes carved off the binary where four had sat name-table-only: `SMSG_PET_TAME_FAILURE` (`0x173`) is two GlobalStrings lookups in order — `0x6e6a20` maps `reason − 1` through the eleven-arm jump table at `0x6e6ac0` to a `PETTAME_*` key and hands the resolved string to `DisplayError(ERR_TAME_FAILED)`, anything outside `1..=11` taking `PETTAME_UNKNOWNERROR`, and Call Pet and Revive Pet raise it too; `SMSG_PET_NAME_INVALID` (`0x178`) raises `ERR_INVALID_PETNAME` from the shared dispatcher `0x5e38c0`'s arm `0x5e3e33` — 1066 had called a refused rename silent on a carve that gave `0x178` the handler `0x4bdc00`, which is `0x2AF SMSG_PET_BROKEN`'s and raises `ERR_PET_BROKEN`, body unread; `SMSG_PET_ACTION_SOUND` (`0x324`) is a bark, not a sound id — its two talk values reach the bark dispatcher `0x623a40` at states 1 and 2, i.e. `CreatureSoundData` columns 28 and 27, and the shipped DBC names them from the other side: exactly four of its 406 rows carry them, the Imp, Succubus, Doomguard and Voidwalker voices, as `A_<demon>_ORDER`/`A_<demon>_KILL`; `Latch::Voice` now carries the bark state as a number and every one-shot creature vocal goes through one `play_bark` that is `0x623a40` — aggro at 0, the pet barks at 1 and 2, the death cry at 4, which had been on a bare `play_kit` off the slot so a pet bark could talk over it — with the reference's `jle` priority rule, because under the old boolean slot a demon's voice would have lost every race against its own aggro flares and been inaudible; it is a warlock-demon surface end to end — vmangos only sends it for a summoned pet, at a 10% roll per order, and a hunter's wolf has neither the columns nor the packet; and the fifth opcode surfaced because the cross-check went looking for column 29's consumer: `SMSG_PET_DISMISS_SOUND` (`0x325`), handler `0x604140`, reads column 29 twice (`0x6041c4`/`0x6041dc`) off a row resolved fresh by `CreatureModelData` id rather than the cached `[unit+0xb40]` — which is why a census over the cache had called the column dead — and plays `A_<demon>_DISMISS` at the packet's point with `z + 1.0`, no unit and no latch; column 29 is loaded, the column nothing reads is 22 (zero on all 406 rows), and the dismiss handler is built although vmangos never sends the packet (2039)
- every UV scroll and animated tint in the world had been frozen for three days: 1940's deferred realization parks a freshly built `WowModelMaterial` behind a reserved handle until something view-visible binds it, and `doodad_anim::register_uv`/`register_tint` still reached for the store alone — `Assets::get_mut` answered `None` for every registration, the slot was freed, the registry stayed empty and the shader read the pinned-zero identity row for every batch, silently, because the `else` branch was a bare `return`; the fix is the address, not the timing: `Parked::value_mut`/`value`/`holds` reach a value wherever it currently lives and `lazy::with_material_mut`/`with_material`/`holds` publish that, so the one-shot build stamp lands in the parked value and rides realization in — realizing it to stamp it would have thrown away exactly what deferral buys; the tick's eviction on `Assets::contains` read a parked material as dead and asks `lazy::holds` now, the empty-registry early-out says so out loud when marked parts exist, and two instruments ship — `WOW_MATANIM_PROBE=<secs>` prints per animated part whether it is registered, whether its loop moves, what its table row holds and whether it is drawn, and `WOW_WORLDVIEW_AT=x,y[,z]` opens the server-less viewer anywhere; measured at the reported waterfall: 0 uv / 0 tint entries before, 87 uv / 6 tint around the Elwynn valley after, its two sheets on slots 1 and 2 with live rows −0.2122 and −0.4246 — the authored 1:2 ratio (2038); and the paper doll and the inspect pane are lit from the model's right: `CGLight+0x24` is the direction the light PROPAGATES — the `CharacterModelBase` ctor `0x505680` writes `(0, 1, 0)` and `0x71bce0` negates all three components before building the SH basis — so `model_pane_light()` hands the packer the ctor's vector as written where 0638 had negated it on the way in and lit both panes from the left; the ambient `(0.7, 0.7, 0.7)`, the diffuse `(0.8, 0.8, 0.64)` and the side-light analysis all hold, which is why the A/B that approved 0638 could not tell the two readings apart (2034)
- a unit tooltip whose name query has not answered titles `UNKNOWNOBJECT`, not a blank gold row: the reference's builder `0x529fe0` reads its NAME line through `CGUnit_C::GetUnitName 0x609210` — the same function `UnitName 0x517020` delegates to — and every miss on its three legs, the null creature record included, ends at the one `FrameScript_GetText("UNKNOWNOBJECT")` tail at `0x609324`, so `unknownobject` moves into `unit` proper and the plate and the verb share it, read out of the VM's `_G` with the binary's "Unknown Being" behind a missing global; the faction line stops waiting for the creature record — its entry gate `0x612610` returns 1 when there is none, so it resolves off `UNIT_FIELD_FACTIONTEMPLATE` and keeps only `HIDE_FACTION_TOOLTIP` for when the record lands; the reference's placeholder sticks for the whole hover (stock `GameTooltip.xml` registers `UPDATE_MOUSEOVER_UNIT` alone) and our line-change repaint, which replaces it when the answer lands, is kept (2040); a unit predicate answers the NUMBER 1 or nil, never a Lua boolean: `UnitExists`, `UnitIsDead`, `UnitIsGhost`, `UnitIsDeadOrGhost` and `UnitOnTaxi` pushed a boolean and twenty-three more open-coded the right shape in eighteen places, where the reference pushes through `lua_pushnumber 0x6f3810` or `lua_pushnil 0x6f37f0`; all twenty-eight now reach 1830's one push site `binding_abi::predicate` — via `unit::unit_predicate` for a `UnitState` field, directly otherwise — `UnitIsCharmed`'s `Number(1.0)` and `ClearTarget`'s hand-branched nil included, and `UnitOnTaxi` gains the reference's `Usage:` raise (gate `0x517a48`) where a nil token had raised mlua's type error; truthiness is why it survived (`if UnitIsDead(u)` reads the same either way, and `== nil` inverts), nine `eval::<bool>` tests were vacuous under mlua's coercion and `taxi.rs` had pinned `== false`/`== true` — the one shape the reference cannot emit — so a table-driven test covers all twenty-eight on both legs, and `UnitIsAFK`/`UnitIsDND` now say they are ours (zero occurrences image-wide) (2043); and the quest log's stock title width cap — `275 − 15 − tagWidth`, live off the chain since 1944 — ellipsizes on one line rather than wrapping into the 16-unit row: 1873 declined it reading an explicit width as a wrap width, but the title FontString declares `y="10"`, and a declared height arms both overflow regimes, so a 258 px title in a 206 px box paints as one line ending in three ASCII dots — the reference's own result, its ellipsis gate at `0x771ec0` being `boxW > 0 && (maxLines != 0 || boxH > 0)`, which only an auto-height FontString escapes; two tests pin the render law on the real FRIZQT face and the geometry the stock file hands it, including the cap's per-row-button stickiness, which is the reference's too (2036)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
321 lines
15 KiB
Rust
321 lines
15 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:?}"
|
|
);
|
|
}
|
|
|
|
/// **The claim this file's own doc has always made, finally checked** (decision 1821): a key the
|
|
/// source names must not only be a catalog row, it must **resolve to text**.
|
|
///
|
|
/// The two are not the same, and the difference is the failure this file exists to catch. A row's
|
|
/// `key` is a `GlobalStrings.lua` *lookup*, and the shipped file has no entry for a good many of
|
|
/// them — that absence is the reference's own data-suppression face, and a raise site that picks
|
|
/// one of those rows is silent for good. The walk above would pass it.
|
|
///
|
|
/// Worth closing now because 1821 moved four more windows' refusals onto keys (the vendor's two
|
|
/// tables, the banker's, the mailbox's), where a superseded or mistyped key costs a whole window's
|
|
/// worth of lines rather than one toast. Skips without client data.
|
|
#[test]
|
|
fn every_error_key_in_the_source_resolves_to_real_text() {
|
|
/// Keys whose row the shipped `GlobalStrings.lua` has **no string for**, so the reference
|
|
/// itself shows nothing when it raises them. Each is documented where it is raised —
|
|
/// `ui_items::equip_error` (errorId 362), `ui_action::cast_fail` (the pet-happiness arm), and
|
|
/// `net::apply::pet` (errorId 337, both pet handlers' NOPATH arm). A fourth entry here is a
|
|
/// defect until someone proves otherwise.
|
|
///
|
|
/// `ERR_PET_SPELL_NOPATH` is the one worth reading twice, because it is a trap: 5875 *does*
|
|
/// ship a `PET_SPELL_NOPATH` ("No path available for your pet"), one letter-group away and
|
|
/// perfectly plausible on screen — but it is not row 337's key and no code path in the client
|
|
/// raises it. benilla named it for months and showed the wrong string for the wrong byte
|
|
/// (decision 2033). Silence here is the reference's behaviour, not a gap.
|
|
const SILENT_IN_5875: &[&str] = &[
|
|
"ERR_CANT_BE_DISENCHANTED",
|
|
"ERR_NOT_HAPPY_ENOUGH",
|
|
"ERR_PET_SPELL_NOPATH",
|
|
];
|
|
|
|
let data = match benilla_formats::wow_data() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
|
|
let src = chain
|
|
.read_file("Interface\\FrameXML\\GlobalStrings.lua")
|
|
.expect("GlobalStrings.lua in the chain");
|
|
let vm = benilla_ui::script::UiScript::new().expect("VM");
|
|
vm.run(&String::from_utf8_lossy(&src)).expect("runs clean");
|
|
|
|
let mut sources = Vec::new();
|
|
walk(Path::new("src"), &mut sources);
|
|
walk(Path::new("../benilla-ui/src"), &mut sources);
|
|
let keys: BTreeSet<String> = sources.iter().flat_map(|s| err_keys(s)).collect();
|
|
|
|
let mut resolved = 0;
|
|
let silent: Vec<&String> = keys
|
|
.iter()
|
|
.filter(|k| !NOT_A_MESSAGE.contains(&k.as_str()))
|
|
.filter(|k| !SILENT_IN_5875.contains(&k.as_str()))
|
|
.filter(|k| {
|
|
let text: String = vm.lua().globals().get(k.as_str()).unwrap_or_default();
|
|
resolved += usize::from(!text.is_empty());
|
|
text.is_empty()
|
|
})
|
|
.collect();
|
|
assert!(
|
|
silent.is_empty(),
|
|
"these keys are catalog rows but resolve to NOTHING in the shipped GlobalStrings.lua, so \
|
|
every line raised through them is invisible: {silent:?}"
|
|
);
|
|
// A floor, not a ledger: the point is that the VM really loaded the file, so an empty `vm`
|
|
// cannot make the sweep above vacuously green.
|
|
assert!(
|
|
resolved > 150,
|
|
"only {resolved} keys resolved — did GlobalStrings load?"
|
|
);
|
|
}
|
|
|
|
/// **The other half of the same claim, on real data: a key that carries a VOICE line can actually
|
|
/// be spoken** (decision 1815).
|
|
///
|
|
/// 56 catalog rows put an error-speech id in `+0x0c` instead of a cue name, and the client says
|
|
/// those aloud in the player's own race and gender. The join runs
|
|
/// `key → MessageRecord::type_tag → VocalUISounds.dbc(race, line) → SoundEntries(sex)`, across
|
|
/// four tables and two crates, and every link is a lookup that can come back empty *silently* —
|
|
/// the same failure mode the walk above exists for, one layer deeper.
|
|
///
|
|
/// So: for every voice-tagged key this workspace raises, every playable race must have a row, and
|
|
/// that row must resolve real audio for both sexes. The two exceptions are exceptions **in the
|
|
/// shipped data**, named here so a regression cannot hide behind them. Skips without client data.
|
|
#[test]
|
|
fn every_voiced_key_the_source_raises_has_audio_for_every_playable_race() {
|
|
use benilla_formats::VOCAL_UI_LINES;
|
|
|
|
/// Lines the 5875 data ships no audio for, in any race: `0x07` (`ERR_FOOD_COOLDOWN`) and
|
|
/// `0x20` (`ERR_LOOT_BAD_FACING`). Both have a `VocalUISounds` row per race whose kit ids are
|
|
/// `-1` — the file's own "no line recorded" — so the client is silent for them by data.
|
|
const NO_AUDIO_IN_5875: &[u8] = &[0x07, 0x20];
|
|
/// Single `(race, sex)` gaps in the shipped file — one voice actor's line that never got
|
|
/// recorded. Listed rather than tolerated wholesale, so a *new* hole fails the test.
|
|
const RACE_SEX_GAPS: &[(u8, u32, u32)] = &[
|
|
(0x21, 3, 0), // ERR_LOOT_LOCKED, Dwarf male
|
|
(0x21, 4, 0), // ERR_LOOT_LOCKED, Night Elf male
|
|
(0x31, 2, 0), // ERR_MUST_EQUIP_ITEM, Orc male
|
|
];
|
|
|
|
let data = match benilla_formats::wow_data() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
|
|
let vocal = benilla_formats::load_vocal_ui_sounds(&mut chain).expect("VocalUISounds.dbc");
|
|
let kits = benilla_formats::load_sound_kit_catalog(&mut chain).expect("SoundEntries.dbc");
|
|
|
|
let mut sources = Vec::new();
|
|
walk(Path::new("src"), &mut sources);
|
|
walk(Path::new("../benilla-ui/src"), &mut sources);
|
|
let keys: BTreeSet<String> = sources.iter().flat_map(|s| err_keys(s)).collect();
|
|
|
|
let mut voiced = 0;
|
|
for key in &keys {
|
|
let Some(record) = benilla_ui::messages::by_key(key) else {
|
|
continue; // the walk above is what polices this
|
|
};
|
|
let tag = record.type_tag;
|
|
if usize::from(tag) >= VOCAL_UI_LINES {
|
|
continue; // a cue row, not a spoken one
|
|
}
|
|
voiced += 1;
|
|
if NO_AUDIO_IN_5875.contains(&tag) {
|
|
continue;
|
|
}
|
|
for race in 1..=8u32 {
|
|
let row = vocal
|
|
.rows()
|
|
.iter()
|
|
.find(|r| r.race == race && r.line == u32::from(tag))
|
|
.unwrap_or_else(|| panic!("{key} (line {tag:#04x}) has no row for race {race}"));
|
|
for sex in 0..2u32 {
|
|
if RACE_SEX_GAPS.contains(&(tag, race, sex)) {
|
|
continue;
|
|
}
|
|
let kit = row
|
|
.normal_kit(sex)
|
|
.and_then(|k| kits.get(k))
|
|
.unwrap_or_else(|| {
|
|
panic!("{key} (line {tag:#04x}) is silent for race {race} sex {sex}")
|
|
});
|
|
assert!(
|
|
!kit.files.is_empty(),
|
|
"{key}: kit {} has no files for race {race} sex {sex}",
|
|
kit.id
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// benilla raises a good share of the 45 distinct voiced lines — a floor, so a refactor that
|
|
// quietly stopped routing refusals through the catalog fails here instead of going silent.
|
|
assert!(
|
|
voiced >= 30,
|
|
"only {voiced} voice-tagged keys reached the catalog — the raise sites stopped naming them"
|
|
);
|
|
}
|
|
|
|
/// **Why benilla may hang a message's sound off its display, even though the reference does not.**
|
|
///
|
|
/// `CGGameUI::DisplayError 0x496720` sounds first (`0x49673d`) and only then guards on the row's
|
|
/// key (`0x4967bd`/`0x4967c5`) and, in the sink, on the resolved text (`0x4945b4`) — so *there*, a
|
|
/// message with no GlobalStrings string still makes its noise. benilla drops an empty line before
|
|
/// it reaches `show_messages`, so it would not. That is a divergence with **no case in 5875's
|
|
/// data**, and this is the measurement that says so: every row that sounds also has text.
|
|
///
|
|
/// If this ever fails, `sound::message` has to sound the row independently of the display rather
|
|
/// than alongside it. Skips without client data.
|
|
#[test]
|
|
fn every_sounding_catalog_row_also_has_text_to_show() {
|
|
let data = match benilla_formats::wow_data() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
|
|
let src = chain
|
|
.read_file("Interface\\FrameXML\\GlobalStrings.lua")
|
|
.expect("GlobalStrings.lua in the chain");
|
|
let vm = benilla_ui::script::UiScript::new().expect("VM");
|
|
vm.run(&String::from_utf8_lossy(&src)).expect("runs clean");
|
|
|
|
let mut sounding = 0;
|
|
for r in benilla_ui::messages::CATALOG {
|
|
let sounds = usize::from(r.type_tag) < benilla_formats::VOCAL_UI_LINES || r.sound.is_some();
|
|
if !sounds {
|
|
continue;
|
|
}
|
|
sounding += 1;
|
|
let text: String = vm.lua().globals().get(r.key).unwrap_or_default();
|
|
assert!(
|
|
!text.is_empty(),
|
|
"{} sounds (tag {:#04x}, cue {:?}) but has no 1.12 string — benilla would be silent \
|
|
where the reference is not",
|
|
r.key,
|
|
r.type_tag,
|
|
r.sound
|
|
);
|
|
}
|
|
assert_eq!(sounding, 86, "56 voice lines + 30 named cues");
|
|
}
|
|
|
|
/// **The non-`ERR_` half of the same tripwire, for the one family that has one** (decision 2039):
|
|
/// every string `pet_tame_failure_key` can return must be a real global of the 1.12 client.
|
|
///
|
|
/// The walk above collects `"ERR_…"` literals, which is every catalog row — and is therefore
|
|
/// blind to a raise site that names something else. `SMSG_PET_TAME_FAILURE` is precisely that
|
|
/// shape: the message it raises is `ERR_TAME_FAILED`, but the *text* comes from a second lookup
|
|
/// of a `PETTAME_*` key, and an invented one there would fail exactly the way this file's header
|
|
/// describes — silently, with no line shown and nobody to file it.
|
|
///
|
|
/// Checked against `reference/1.12-globals.tsv`, the running client's own `_G` (decision 1189),
|
|
/// rather than against the install: it is in-tree, so this holds on a machine with no client.
|
|
#[test]
|
|
fn every_pettame_key_is_a_real_1_12_global() {
|
|
let tsv = concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../reference/1.12-globals.tsv"
|
|
);
|
|
let text = std::fs::read_to_string(tsv).expect("the reference surface");
|
|
let globals: BTreeSet<&str> = text
|
|
.lines()
|
|
.filter(|l| !l.starts_with('#'))
|
|
.filter_map(|l| l.split('\t').next())
|
|
.collect();
|
|
|
|
// The whole reachable image of the table: 1..=11 index it, everything else is the default
|
|
// arm. `u8` is small enough to just walk.
|
|
let reachable: BTreeSet<&str> = (0..=u8::MAX)
|
|
.map(benilla_protocol::messages::pet_tame_failure_key)
|
|
.collect();
|
|
assert_eq!(reachable.len(), 12, "eleven arms plus the default");
|
|
for key in &reachable {
|
|
assert!(globals.contains(key), "{key} is not a 1.12 global");
|
|
}
|
|
// The message the fill goes into, and the two bodiless arms beside it.
|
|
for key in ["ERR_TAME_FAILED", "ERR_INVALID_PETNAME", "ERR_PET_BROKEN"] {
|
|
assert!(globals.contains(key), "{key} is not a 1.12 global");
|
|
}
|
|
}
|