diff --git a/.claude/hooks/guard-harness-worktree.sh b/.claude/hooks/guard-harness-worktree.sh deleted file mode 100755 index 093b677c..00000000 --- a/.claude/hooks/guard-harness-worktree.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# PreToolUse(EnterWorktree | Agent) guard (docs/METHOD.md): -# refuse the HARNESS worktree tool. `scripts/wt.sh` is this repo's only worktree mechanism. -# -# Two doors, one room. `EnterWorktree` is the named tool; `Agent` with `isolation: "worktree"` -# is the same worktree by another door — the subagent is spawned into a fresh -# `.claude/worktrees/agent-` checkout that no pool verb knows about (decision 2287: one was -# found under the primary on 2026-09-16, created by a session whose method text still said to -# use it). For `Agent` the block is conditional on that one field; every other Agent call -# passes untouched. -# -# Why a guard for a tool that sounds like exactly the right thing: it IS the natural reach. The -# session reads "every session works in its own git worktree", claims a slot, and then meets a -# built-in tool named EnterWorktree — the name matches the rule word for word. The pull is -# strongest right after a correct `wt.sh claim`, because the Bash tool resets cwd to the primary -# between calls, so the printed slot path looks unusable and this looks like how you "enter" it. -# -# Both of its forms are wrong here, for different reasons: -# `name` -> creates a worktree under `.claude/worktrees/`. That is precisely what the pool -# replaced: ownerless agent worktrees, no sweeper, each paying a cold ~4.5-min dep -# build, ~60 GB of them at the 2026-07-17 disk incident (decisions 0192, 0433 — -# sweep step 2 still exists to reap the strays). -# `path` -> points at a pool slot with a second tracker that has its own lifecycle opinions, -# including removal prompts. A pool slot must never be `git worktree remove`d; its -# warm target/ is the entire point (0192), and 0520 is the incident from a session -# holding the wrong idea of which slot it was in. -# So the block is absolute and needs no allowlist — same shape as guard-primary-checkout.sh. -# There is no legitimate call here: the pool's verbs are Bash, which this never sees. -# -# Unscoped by design, unlike the primary-checkout guard. That one allows sibling repos because a -# dispatched RE agent legitimately edits in wow-5875-re; this one has no such case — wow-re runs -# its own isolated-worktree rule under its own law (docs/METHOD.md, "The cross-repo RE workflow"), and -# an agent reaching for the harness tool there is making the same mistake in someone else's tree. -input=$(cat) - -# A deliberate exception names itself, the WT_ALLOW_PRIMARY / WOW_ALLOW_ACCOUNT shape (0677). -[ -n "${WT_ALLOW_HARNESS_WORKTREE:-}" ] && exit 0 - -# No pool on this machine — a plain clone — means there is no pool to prefer: the harness tool is -# then an ordinary worktree, and nothing here has an opinion about it (scripts/wt.sh). -own=$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -primary=$(cd "${own:-/nonexistent}/.." 2>/dev/null && pwd) || exit 0 -pool_here() { [ -d "${WT_POOL_ROOT:-/Volumes/SanDisk/benilla-wt}" ] || [ -d "$(dirname "$1")/benilla-wt" ]; } -pool_here "$primary" || exit 0 - -tool=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null) -if [ "$tool" = "Agent" ]; then - isolation=$(printf '%s' "$input" | jq -r '.tool_input.isolation // empty' 2>/dev/null) - [ "$isolation" = "worktree" ] || exit 0 - door="Agent isolation: worktree" -else - door="EnterWorktree" -fi - -target=$(printf '%s' "$input" | jq -r '.tool_input.path // .tool_input.name // empty' 2>/dev/null) - -cat >&2 < # prints the slot path on stdout - -If you HAVE claimed one, you do not need this tool — you need the path. The Bash tool resets cwd -to the primary between calls; that is expected, not a problem to solve. Two habits cover it -(a dispatched agent inherits the session's slot and works there the same way — it never needs a -tree of its own; a diagnostic that genuinely needs a clean tree is the parent's to run, on a -synced slot): - - Bash cd /path/to/benilla-wt/pool-N && cargo test ... # every call, prefix it - Read/Edit /path/to/benilla-wt/pool-N/crates/... # absolute, under the slot - -The gates, the hooks and the runtime asset reads all follow the slot on their own. -EOF -exit 2 diff --git a/.claude/hooks/guard-primary-bash.sh b/.claude/hooks/guard-primary-bash.sh deleted file mode 100755 index 7252f8d4..00000000 --- a/.claude/hooks/guard-primary-bash.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash -# PreToolUse(Bash) guard: refuse to keep working when the PRIMARY checkout is dirty and this -# session holds no pool slot. Exit 2 blocks the call and hands stderr back to the agent. -# -# **This is `guard-primary-checkout.sh`'s hole, closed.** That hook is wired to `Edit|Write| -# NotebookEdit`, and it reasons — in a comment that was true when it was written — that "the three -# things that genuinely write to the primary are scripts, not tool calls ... and they run as Bash, -# which this never sees." A session editing through Bash (`sed -i`, a `python3 - </dev/null) -[ -n "$cmd" ] || exit 0 - -# The deliberate exception, named in the transcript either way. -[ -n "${WT_ALLOW_PRIMARY:-}" ] && exit 0 -case "$cmd" in *WT_ALLOW_PRIMARY=1*) exit 0 ;; esac - -# The remedy must never be blocked by the thing it remedies: `wt.sh claim` is the one command this -# message asks for, and `status`/`land`/`release` are how a session gets itself unstuck. -case "$cmd" in *wt.sh*) exit 0 ;; esac - -# Scoped by the shared git common-dir, like the sibling hooks: another repo (wow-5875-re) has its -# own, so a dispatched RE agent is never touched by benilla's rule. -own=$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -[ -n "$own" ] || exit 0 -primary=$(cd "$own/.." 2>/dev/null && pwd) || exit 0 -# No pool on this machine — a plain clone — means this checkout IS the working tree: nothing to -# guard. The pool is what makes the primary off-limits (scripts/wt.sh); without one, the rule has -# nothing to protect. -pool_here() { [ -d "${WT_POOL_ROOT:-/Volumes/SanDisk/benilla-wt}" ] || [ -d "$(dirname "$1")/benilla-wt" ]; } -pool_here "$primary" || exit 0 - -# Clean primary -> nothing to protect, and this is the overwhelmingly common case: one `git status` -# against a warm index, ~10 ms, on a tree whose only large directory (target/) is gitignored. -dirt=$(git -C "$primary" status --porcelain 2>/dev/null | head -6) -[ -n "$dirt" ] || exit 0 - -mine="${CLAUDE_CODE_SESSION_ID:-}" -[ -n "$mine" ] || exit 0 # a human shell or CI — not ours to police - -# Does this session already hold a slot? Then it has somewhere to be, and the dirt in the primary -# is somebody else's problem to answer for — blocking here would be a gate firing on innocent work, -# which is how a gate teaches "the gate is broken, work around it". -# -# **BOTH pool roots, because there are two** (decision 1726): slots are allocated on the external -# drive, and the old internal root beside the primary is draining but still live. This hook asked -# only the internal one — so from the day 1726 landed, a session holding an external slot answered -# "no slot" and was blocked on its NEXT Bash call after any neighbour dirtied the primary. That is -# a gate firing on exactly the innocent work the paragraph above says it must not, and it takes out -# every session at once rather than the one that made the mess. `WT_POOL_ROOT` is honoured for the -# same reason `wt.sh` honours it. -for pool in "${WT_POOL_ROOT:-/Volumes/SanDisk/benilla-wt}" "$(dirname "$primary")/benilla-wt"; do - for marker in "$pool"/pool-*/.wt-claimed; do - [ -e "$marker" ] || continue - [ "$(cut -f3 "$marker" 2>/dev/null)" = "$mine" ] && exit 0 - done -done - -cat >&2 < # prints a slot path - git -C $primary diff > /tmp/carry.patch # needs WT_ALLOW_PRIMARY=1 on this call - # …then apply it in the slot and reset the primary. - -If they are NOT yours, do not clean them up — they are another session's live work. Claim a slot -and work there; say so to the director if the primary stays dirty. - -If the pool is genuinely full, \`wt.sh claim\` says so and the answer is to STOP and ask the -director which slot to take over — never to carry on here. - -A deliberate exception is \`WT_ALLOW_PRIMARY=1 \`. -EOF -exit 2 diff --git a/.claude/hooks/guard-primary-checkout.sh b/.claude/hooks/guard-primary-checkout.sh deleted file mode 100755 index aa5d840d..00000000 --- a/.claude/hooks/guard-primary-checkout.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# PreToolUse(Edit|Write|NotebookEdit) guard (docs/METHOD.md): -# refuse to write into a checkout that is not this session's. Exit 2 blocks the call and hands -# stderr back to the agent, which then claims a slot and works there. -# -# TWO trees are not yours, and the file name only says the first (kept, deliberately: renaming it -# would churn `.claude/settings.json`, which every live session reads, for a naming tidy-up): -# -# 1 · the PRIMARY checkout — nobody's, and it stays clean (decision 0976, below); -# 2 · another SESSION's pool slot (decision 1042). -# -# It fires at the FIRST edit because that is where the fix is one `wt.sh claim` and nothing has been -# lost; unguarded, the cost lands on a BYSTANDER hours later, whose `wt.sh land` refuses on a dirty -# primary full of work they can neither see nor move. Why a hook and not more prose: decision 0976. -# -# The block is absolute and needs no allowlist — there is no legitimate agent edit here. The one -# thing that genuinely writes to the primary is a script, not a tool call (`wt.sh land` fast-forwards -# main), and it runs as Bash, which this never sees. `WT_ALLOW_PRIMARY=1` overrides, the `WOW_ALLOW_ACCOUNT` shape -# from 0677: structurally hard rather than merely forbidden, and a deliberate exception names itself. -# -# **"…which this never sees" WAS the hole, and it is why `guard-primary-bash.sh` exists.** That -# sentence assumed only scripts and humans reach the primary through Bash. An AGENT editing with -# `sed -i` or a `python3 - </dev/null) -[ -n "$path" ] || exit 0 # no target we can judge → allow - -# Absolutize against the tool's own cwd — a relative path from a session sitting IN the primary is -# exactly the case this exists to catch, and it is the one that arrives without a leading slash. -cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null) -cwd="${cwd:-$PWD}" -case "$path" in - /*) ;; - '~'/*) path="${HOME}${path#\~}" ;; - *) path="$cwd/$path" ;; -esac - -# A Write targets a file that does not exist yet, and may name directories that do not either, so -# resolve against the nearest existing ancestor rather than the path itself. -dir=$(dirname "$path") -while [ ! -d "$dir" ] && [ "$dir" != "/" ] && [ "$dir" != "." ]; do dir=$(dirname "$dir"); done -[ -d "$dir" ] || exit 0 - -# Same-REPO check first (this repo's worktrees all share one common-dir; other repos don't). -own=$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -there=$(git -C "$dir" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -[ -n "$own" ] && [ "$there" = "$own" ] || exit 0 - -# The primary checkout is the common-dir's parent — the same expression `wt.sh` computes `PRIMARY` -# with, so the two can never disagree about which tree is the shared one. A linked worktree's -# toplevel is its own slot path, and falls through to the slot check below. -primary=$(cd "$own/.." 2>/dev/null && pwd) || exit 0 -# No pool on this machine — a plain clone — means this checkout IS the working tree: nothing to -# guard. The pool is what makes the primary off-limits (scripts/wt.sh); without one, the rule has -# nothing to protect. -pool_here() { [ -d "${WT_POOL_ROOT:-/Volumes/SanDisk/benilla-wt}" ] || [ -d "$(dirname "$1")/benilla-wt" ]; } -pool_here "$primary" || exit 0 -top=$(git -C "$dir" rev-parse --path-format=absolute --show-toplevel 2>/dev/null) -[ -n "$top" ] || exit 0 - -if [ "$top" = "$primary" ]; then - [ -n "${WT_ALLOW_PRIMARY:-}" ] && exit 0 # deliberate exception, named in the transcript - - rel="${path#"$primary"/}" - cat >&2 < # prints the slot path; cd there and carry on - -If you already have a claimed slot, you are simply in the wrong directory — reissue this edit -against the slot path. A deliberate exception is \`WT_ALLOW_PRIMARY=1\`. -EOF - exit 2 -fi - -# ── Another session's slot (decision 1042) ─────────────────────────────────────────────────────── -# 1041 stopped a foreign session at `wt.sh land`. This stops it at the first EDIT, which is 0976's -# whole argument one level down: at the verb, the damage is already a split branch somebody has to -# unpick by hand; at the edit, the fix is one `wt.sh claim` and nothing has happened yet. On -# 2026-08-06 the gap between the two was an hour, a red `fmt --check` on files this session had -# never touched, and two decision records that reverted under it mid-write. -# -# **Agents are safe here, and that is a measured fact, not a hope**: a dispatched subagent reports -# the same `CLAUDE_CODE_SESSION_ID` as its parent (checked, 2026-08-06 — same value, same PID). So -# the bulk-edit dispatches docs/METHOD.md's "Who types what" depends on, which work in their session's -# own slot by design, pass this untouched. 1041 named that as the fact it was waiting on. -marker="$top/.wt-claimed" -[ -e "$marker" ] || exit 0 # free slot: unclaimed, no session to wrong -owner=$(cut -f3 "$marker" 2>/dev/null) -case "$owner" in "" | "-") exit 0 ;; esac # pre-1041 marker: no session recorded -mine="${CLAUDE_CODE_SESSION_ID:-}" -[ -n "$mine" ] || exit 0 # nothing to compare (human shell, CI) -[ "$owner" = "$mine" ] && exit 0 # yours — the overwhelmingly common case -[ -n "${WT_ALLOW_FOREIGN:-}" ] && exit 0 # deliberate exception, named in the transcript - -holder=$(cut -f1 "$marker" 2>/dev/null) -cat >&2 < # prints the slot path; cd there and carry on - -\`wt.sh status\` tags the slot you own \`(yours)\` — if nothing is tagged, you have not claimed one. -A deliberate exception (recovering a dead session's work, say) is \`WT_ALLOW_FOREIGN=1\`. -EOF -exit 2 diff --git a/.claude/hooks/guard-tree-wide-git.sh b/.claude/hooks/guard-tree-wide-git.sh deleted file mode 100755 index e74254df..00000000 --- a/.claude/hooks/guard-tree-wide-git.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash -# PreToolUse(Bash) guard (docs/METHOD.md "Who types what": *Parallel agents share one working tree — -# whole-tree git ops are forbidden*): refuse the handful of git verbs that sweep a tree — or the -# SHARED STASH STACK — rather than a path. -# -# Why a hook, on 2026-08-14: a session reaching for a one-command "revert my file, run the test, -# put it back" repro typed `git stash push -- -q`. The trailing `-q` parsed as a pathspec, -# the push failed — and the follow-up `git stash pop` then popped a DIFFERENT session's entry -# (`sess/death-arc`'s) into this worktree. It conflicted on docs/MAP.md and aborted, so nothing was -# lost; the near-miss is the point. **Stashes are per-REPOSITORY, not per-worktree** — every pool -# slot shares one stack — so `stash pop` is a whole-tree op wearing a local-looking name, and the -# blast radius is another session's uncommitted hours. The earlier incident docs/METHOD.md records (a -# six-agent fan-out losing hours to one agent's diagnostic `git stash`) is the same mechanism. -# -# The rule the prose already had, made structural — 0976's argument for the primary-checkout guard, -# one verb over: at the command, the fix is one different command and nothing has happened; after -# it, somebody unpicks a stash stack by hand and hopes the conflict preserved their work. -# -# **The block list is deliberately SHORT.** A gate that fires on legitimate work teaches "the gate -# is broken, work around it" (the 2026-07-20 incident), so -# this blocks only forms with no path-scoped reading: -# -# git stash [push|save|pop|apply|drop|clear|branch|create|store] — the shared stack -# git reset --hard — sweeps the tree -# git clean -f… — deletes untracked files -# git checkout . | git checkout -- . | git restore . | … — the sweep-everything forms -# -# Everything else passes, including the ones this repo's own work depends on: `git stash list`/ -# `show` (read-only), `git checkout HEAD -- ` (the correct way to do the repro above), -# `git checkout --detach`/`-B ` (what `wt.sh` itself runs), `git restore --source=X -- path`, -# and a soft/mixed `git reset` (which moves HEAD but leaves the working tree alone). -# -# Scoped by the shared git common-dir like every other hook here: a sibling repo (wow-5875-re) has -# its own rules and falls through untouched. Note the hook only ever sees the agent's own command -# text, so git run INSIDE our scripts (`wt.sh`) is never its business. -# -# `WT_ALLOW_TREE_OP=1` overrides, the `WOW_ALLOW_ACCOUNT`/`WT_ALLOW_PRIMARY` shape (0677, 0976): -# structurally hard rather than merely forbidden, and a deliberate exception names itself. -input=$(cat) -cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null) -[ -n "$cmd" ] || exit 0 -[ -n "${WT_ALLOW_TREE_OP:-}" ] && exit 0 # deliberate exception, named in the transcript - -# Per SEGMENT of a (possibly compound) command, so a `git stash` riding behind a `&&` is judged -# on its own rather than by the line's first verb. -verdict="" -while IFS= read -r seg; do - # Strip a leading `cd … &&`-style residue and normalise whitespace for matching. - s=$(printf '%s' "$seg" | sed 's/^[[:space:]]*//; s/[[:space:]]\{1,\}/ /g') - # Only `git …` invocations, with or without a leading `-C `. - case "$s" in - git\ *) ;; - *) continue ;; - esac - # Drop `-C ` and other pre-verb globals so the verb is the next token. - v=$(printf '%s' "$s" | sed -E 's/^git( +-[A-Za-z-]+( +[^ ]+)?)* +//') - case "$v" in - # ── the shared stash stack (read-only subcommands excepted) ────────────────────────────── - stash\ list* | stash\ show*) ;; - stash | stash\ *) - verdict="git stash — the stash stack is shared by EVERY worktree of this repo, so this can -capture, or restore, another live session's uncommitted work. It has already come within one -merge conflict of doing exactly that (2026-08-14). - -For the common case — 'revert one file, run something, put it back' — copy it aside and use the -path-scoped restore instead: - - cp \"\$SCRATCH/.mine\" - git checkout HEAD -- # path-scoped: touches nothing else, no shared state - - cp \"\$SCRATCH/.mine\" - -To park work properly, commit it on your session branch — that is what the branch is for." - break - ;; - # ── whole-tree resets / sweeps ─────────────────────────────────────────────────────────── - reset*--hard*) - verdict="git reset --hard — discards every uncommitted change in this worktree, including -work belonging to a dispatched agent running in this session's slot right now. - -Undo one path with \`git checkout HEAD -- \`; move HEAD without touching files with -\`git reset --soft\`." - break - ;; - clean\ *-*f* | clean\ -*f*) - verdict="git clean -f — deletes untracked files outright, and untracked is exactly what a -neighbouring agent's not-yet-added new file looks like. - -Delete the specific files you mean by name, or list first with \`git clean -n\`." - break - ;; - checkout\ . | checkout\ --\ . | checkout\ -- | restore\ . | restore\ --\ . | checkout\ -f | checkout\ -f\ *) - verdict="a pathless/sweep-everything checkout — it reverts the WHOLE tree, not the file you -are looking at. - -Name the path: \`git checkout HEAD -- \` (or \`git restore -- \`)." - break - ;; - esac -done < && …` is how every session writes one. -cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null) -cwd="${cwd:-$PWD}" -cd_dir=$(printf '%s' "$cmd" | tr ';&|' '\n' \ - | sed -n 's/^[[:space:]]*cd[[:space:]]\{1,\}\([^[:space:]]\{1,\}\)[[:space:]]*$/\1/p' | tail -n 1) -if [ -n "$cd_dir" ]; then - case "$cd_dir" in - /*) cwd="$cd_dir" ;; - '~'*) cwd="${HOME}${cd_dir#\~}" ;; - *) cwd="$cwd/$cd_dir" ;; - esac -fi -[ -d "$cwd" ] || cwd=$PWD -own=$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -there=$(git -C "$cwd" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -[ -n "$own" ] && [ "$there" = "$own" ] || exit 0 # another repo's rules are its own -primary=$(cd "$own/.." 2>/dev/null && pwd) || exit 0 -# No pool on this machine — a plain clone — means this checkout IS the working tree, shared with -# nobody: nothing to guard. The pool is what makes the tree shared (scripts/wt.sh). -pool_here() { [ -d "${WT_POOL_ROOT:-/Volumes/SanDisk/benilla-wt}" ] || [ -d "$(dirname "$1")/benilla-wt" ]; } -pool_here "$primary" || exit 0 - -cat >&2 </dev/null && pwd) || exit 0 # The roots to protect. `$WOW_DATA` first (the resolver's own order), then the `WoW` link every -# checkout and pool slot carries. Both physicalised, so the symlink and its target are one root. +# checkout carries. Both physicalised, so the symlink and its target are one root. own=$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) primary=$(cd "${own:-.}/.." 2>/dev/null && pwd) for root in "${WOW_DATA:-}" "$primary/WoW" "$cwd/WoW"; do diff --git a/.claude/hooks/on-stop.sh b/.claude/hooks/on-stop.sh deleted file mode 100755 index b2c7a5f8..00000000 --- a/.claude/hooks/on-stop.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# Stop hook (docs/METHOD.md): tidy pass at the end of every turn — stamp this session's slot -# claim so the pool can tell a live session from a dead one, and format Rust so the fmt gate is -# green by the time anything commits. Best-effort: never block the agent from stopping (always -# exit 0). Operates on the SESSION'S checkout (worktree-aware), guarded via the shared git common-dir -# so it never formats a sibling repo (all benilla worktrees share one common-dir; wow-5875-re doesn't). -# -# It also **never formats the primary checkout**, and says so. Formatting is a write, and the primary -# belongs to no session (docs/METHOD.md "Parallel sessions"); a session sitting there is the mistake -# `guard-primary-checkout.sh` blocks at edit time, and this is the one place that notices the same -# mistake arriving by any OTHER route — a `sed -i`, a `perl -pi`, a script — since it runs every -# turn regardless of which tools were used. Warn, never block: a Stop hook that refuses to stop is a -# trap, and this one is best-effort by design. -own=$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) - -# ── The claim HEARTBEAT ────────────────────────────────────────────────────────────────────────── -# Stamp this session's claim marker once a turn, so "is anyone still working in that slot?" is a -# fact we hold rather than a guess. Before this, the only activity signal a dead claim could be -# judged on was its TIP COMMIT age, and `wt.sh` says why that is a poor one: a live session looks -# stale the moment it lands, so the reap window had to be 24 h on BOTH clocks to be safe. The cost -# of that safety was the director's own complaint — close a session after its work lands and the -# slot sits unusable for a day, with nothing "obviously free" for the next session to take. -# -# Deliberately BEFORE the cwd checks below, and found by SESSION ID rather than by `$PWD`: a turn -# that happened to end in the scratchpad, or in the primary, is still a turn this session was alive -# for. Best-effort like everything else here — a failed touch must never keep a session from -# stopping. -mine="${CLAUDE_CODE_SESSION_ID:-}" -if [ -n "$own" ] && [ -n "$mine" ]; then - primary_dir=$(cd "$own/.." 2>/dev/null && pwd) - if [ -n "$primary_dir" ]; then - # BOTH pool roots (decision 1726): slots are allocated on the external drive and the old - # internal root is draining. This walked only the internal one, so from the day 1726 landed the - # heartbeat stopped stamping for every session in the new pool — and the heartbeat is the whole - # reason `wt.sh` can tell a live claim from a dead one. A claim that never restamps looks idle - # to `reap_idle_claims`, which is how a full `claim` takes over a slot somebody is working in. - for pool in "${WT_POOL_ROOT:-/Volumes/SanDisk/benilla-wt}" "$(dirname "$primary_dir")/benilla-wt"; do - for marker in "$pool"/pool-*/.wt-claimed; do - [ -e "$marker" ] || continue - [ "$(cut -f3 "$marker" 2>/dev/null)" = "$mine" ] || continue - touch "$marker" 2>/dev/null || true - break 2 - done - done - fi -fi - -root=$(git rev-parse --path-format=absolute --show-toplevel 2>/dev/null) -cur=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) -[ -n "$root" ] && [ -n "$own" ] && [ "$cur" = "$own" ] || exit 0 -primary=$(cd "$own/.." 2>/dev/null && pwd) -# No pool on this machine — a plain clone — means the checkout IS the working tree, and formatting -# it is the whole job. The pool is what makes the primary off-limits (scripts/wt.sh). -pool_here() { [ -d "${WT_POOL_ROOT:-/Volumes/SanDisk/benilla-wt}" ] || [ -d "$(dirname "$1")/benilla-wt" ]; } -if [ -n "$primary" ] && [ "$root" = "$primary" ] && [ -z "${WT_ALLOW_PRIMARY:-}" ] && pool_here "$primary"; then - echo "on-stop: this session is sitting in the PRIMARY checkout ($primary) — it belongs to no" >&2 - echo "on-stop: session and must stay clean. Not formatting it. Run './scripts/wt.sh claim '" >&2 - echo "on-stop: and work in the printed slot; move anything already changed here into that slot." >&2 - exit 0 -fi -cd "$root" || exit 0 -cargo fmt --all >/dev/null 2>&1 || true -exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 28b926b8..1ca735a6 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,57 +1,16 @@ { "autoMemoryEnabled": false, "hooks": { - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/on-stop.sh\"", - "timeout": 120 - } - ] - } - ], "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-tree-wide-git.sh\"", - "timeout": 10 - }, - { - "type": "command", - "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-primary-bash.sh\"", - "timeout": 10 - } - ] - }, { "matcher": "Edit|Write|NotebookEdit", "hooks": [ - { - "type": "command", - "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-primary-checkout.sh\"", - "timeout": 10 - }, { "type": "command", "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-wow-install.sh\"", "timeout": 10 } ] - }, - { - "matcher": "EnterWorktree|Agent", - "hooks": [ - { - "type": "command", - "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-harness-worktree.sh\"", - "timeout": 10 - } - ] } ] } diff --git a/.claude/skills/probe/SKILL.md b/.claude/skills/probe/SKILL.md index f6d12ff0..7610dcde 100644 --- a/.claude/skills/probe/SKILL.md +++ b/.claude/skills/probe/SKILL.md @@ -5,15 +5,19 @@ description: Run the client unattended against the local vmangos server — a li The rules are in `docs/METHOD.md` ("The local server"). This is the how. -**Identity.** `WOW_USER=probeN WOW_PASS=pprobeN WOW_CHAR=Probe` for `pool-N` -(pool-4: `probe4` / `pprobe4` / `Probefour`; pool-0: `Probezero`). The client refuses a login -about to land on a player's account or another slot's; `WOW_ALLOW_ACCOUNT=1` overrides. -Probe accounts hold the highest GM level, so every GM command works. +**Identity.** The account this checkout declares in `.probe-identity` at the tree root +(`WOW_USER=`, `WOW_PASS=`, `WOW_CHAR=`, one per line, never committed), or the same three in +the environment. A login kicks whoever holds the account, so a probe account is its own, with +the top GM level (the GM commands the probes use all work; `.modify money` is out of reach +even so), and from a checkout that declares one the client refuses the scripted login on any +other account; `WOW_ALLOW_ACCOUNT=1` overrides. `scripts/smoke.sh`, `cine.sh` and +`summon-live.sh` take the identity from `scripts/probe-identity.sh`, which applies this rule. **Every run:** `WOW_UNATTENDED=1`, which reconnects instead of parking on a dialog and exits -non-zero on a login it cannot pass. An agent adds `WOW_NOSOUND=1`. `scripts/leg.sh`, -`smoke.sh`, `cine.sh` and `summon-live.sh` pass both already; a capture (`WOW_CAPTURE`) and a -rig (`WOW_RIG`) imply them. +non-zero on a login it cannot pass. An agent adds `WOW_NOSOUND=1`. The three scripts pass +`WOW_UNATTENDED=1`; `smoke.sh` and `summon-live.sh` add `WOW_NOSOUND=1`, and `cine.sh` +silences through a throwaway config with `MasterVolume=0` because the music path is its +subject. A capture (`WOW_CAPTURE`) implies both; a rig (`WOW_RIG`) implies unattended only. **The body.** A probe cannot die: every world entry sends `.cheat god on`. `.die` clears it for a death test; `WOW_GOD=off` runs unshielded. GM mode is on by default and makes reaction diff --git a/.gitignore b/.gitignore index 533bc207..a66f64db 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,6 @@ # --- Blizzard-owned content: NEVER commit --- # The user's legally-installed 1.12.1 client. Read at runtime; not redistributed. /WoW -# The user's legally-installed Classic Era client (product wow_classic_era; the shared CASC Data -# dir sits beside the product dir). Source of the Era UI exports for decision 0068; not redistributed. -/WoW-era # Cached / extracted assets derived from the user's install. /extracted/ /out/ @@ -17,17 +14,18 @@ # --- benilla's own local state (decision 0954, moved out of the install by 1175) --- # The one folder benilla persists into: config.toml, macros/, bindings/, saved-variables.lua, -# camera/. A dev build resolves it to the primary checkout (so the worktree pool shares one); a +# camera/. A dev build resolves it to the main checkout (so linked worktrees share one); a # release build puts it beside the binary — which is why it is not called `benilla/`: that name # collides with the binary of the same name. Machine-local by definition — `crate::local_state` # is the only thing in the tree that may write it. /benilla-config -# Worktree-pool claim marker (scripts/wt.sh) — per-slot state, never content. -/.wt-claimed +# The account a scripted run from this checkout logs in as (scripts/probe-identity.sh and the +# client's account guard read it): WOW_USER=, WOW_PASS=, WOW_CHAR=, one per line. A credential. +/.probe-identity # Profiling output: `--features bevy/trace_chrome` drops a multi-GB flame chart in the cwd, which -# otherwise leaves the checkout "dirty" and blocks the worktree verbs (decision 0610). +# otherwise leaves the checkout "dirty" (decision 0610). trace-*.json # OS / editor cruft @@ -46,5 +44,5 @@ trace-*.json __pycache__/ # The vanilla addon corpus the UI engine's real-addon tests read (third-party content, decision -# 2329): a symlink `scripts/wt.sh` lays into every slot and the primary, beside `WoW`. +# 2329): a symlink beside `WoW`, or `BENILLA_ADDON_CORPUS`. /wow-addons-vanilla diff --git a/Cargo.toml b/Cargo.toml index 55960f07..432a4942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,9 +58,9 @@ sha1 = "0.10.6" # The one entry below is not a fork of a WoW crate at all. `lua-src` compiles the Lua the client's # addon VM runs on, and benilla's Lua has to accept EXACTLY the grammar the 1.12.1 client's Lua # accepts — no more and no less. The hunks — `third_party/lua-src/BENILLA.md` lists them — all byte-read out of the client's own parser and VM: -# three restore what 5.1 removed (the iterator-less generic-for `for k, v in someTable do`, which +# they restore what 5.1 removed (the iterator-less generic-for `for k, v in someTable do`, which # 183 of 218 corpus addons are reached by; nesting long strings; the constructor's compat -# semicolon), three delete what 5.1 added (`...` as an expression, `#`, `%`). Both directions are +# semicolon) and delete what 5.1 added (`...` as an expression, `#`, `%`). Both directions are # below the reach of any layer above the VM — an opcode and a grammar are not a library function, # a global, or a metamethod — and the corpus ASKS about the second direction directly: 170 # `loadstring` sites compile a vararg expression purely to find out which interpreter they are on. diff --git a/README.md b/README.md index 8b686ed9..8ff0b061 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,10 @@ Not planned: other expansions or client versions, Warden (anticheat). ## Running it You need a **1.12.1 (build 5875) client install** for game data, a vanilla server to connect to, -and stable Rust. Any 1.12.1 core works; [vmangos](https://github.com/vmangos/core) is what -development runs against, and cMaNGOS and the rest speak the same protocol. +stable Rust and a C compiler (the Lua is built from source; on macOS the Xcode command line +tools, on Linux the ALSA and udev development packages). Any 1.12.1 core works; +[vmangos](https://github.com/vmangos/core) is what development runs against, and cMaNGOS and +the rest speak the same protocol. ```sh WOW_DATA=/path/to/WoW/Data cargo run --release -p benilla diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml index 042f69b6..67ef8cc8 100644 --- a/crates/benilla-app/Cargo.toml +++ b/crates/benilla-app/Cargo.toml @@ -150,8 +150,8 @@ smithay-clipboard = "0.7" # macOS: drive a native NSCursor for a crisp hardware cursor. winit drives the cursor via AppKit # cursor-rects, which our continuous Metal render loop doesn't honor on mouse-move (custom cursor / -# hide revert to the arrow); we bypass it with NSCursor + NSWindow::disableCursorRects. See -# docs/knowledge/camera-control.md. +# hide revert to the arrow); we bypass it with NSCursor + NSWindow::disableCursorRects +# (`video::cursor`). [target.'cfg(target_os = "macos")'.dependencies] # The audio device layer (decision 1857, `sound::output::coreaudio`): the HAL output unit through # coreaudio-rs's `AudioUnit`, and the AudioObject property/listener API through the objc2 CoreAudio diff --git a/crates/benilla-app/src/capture/probe_auction.rs b/crates/benilla-app/src/capture/probe_auction.rs index dba573f4..2ab61641 100644 --- a/crates/benilla-app/src/capture/probe_auction.rs +++ b/crates/benilla-app/src/capture/probe_auction.rs @@ -14,7 +14,7 @@ //! is the step this probe exists for. The house id is checked to be in `1..=7` because it keys the //! deposit rate the sell pane quotes, and a zero would silently quote a free listing. //! -//! ## The auctioneer (live-DB verified this session, `/Users/sam/dev/vmangos-deploy` → `mangos`) +//! ## The auctioneer (live-DB verified against the local vmangos, `mangos`) //! //! Auctioneer Fitch, creature entry 8719, spawn guid 12696, map 0 (Stormwind, Trade District), //! pos `(-8821.53, 659.886, 97.4645)`. Her `creature_template.npc_flags` is **4096** exactly — @@ -57,8 +57,8 @@ //! WOW_PROBE_AUCTION=1 WOW_NOSOUND=1 WOW_USER=probe5 WOW_PASS=pprobe5 WOW_CHAR=Probefive \ //! cargo run -p benilla //! ``` -//! (the slot-keyed probe identity — `pool-N` → `WOW_USER=probeN WOW_PASS=pprobeN -//! WOW_CHAR=Probe`, docs/METHOD.md "The local vmangos server"; this worktree is `pool-5`). +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). //! Non-combat. An outer grep on `PROBE_AUCTION:` is the whole harness; the probe self-exits (the //! [`super::probes::ProbeExitPlugin`] pattern) once DONE. diff --git a/crates/benilla-app/src/capture/probe_bank.rs b/crates/benilla-app/src/capture/probe_bank.rs index d3a4f18b..e9268fa3 100644 --- a/crates/benilla-app/src/capture/probe_bank.rs +++ b/crates/benilla-app/src/capture/probe_bank.rs @@ -9,7 +9,7 @@ //! window is first-party, no Lua click surface to drive); the live Lua VM is only touched for the //! bonus refusal step's `UI_ERROR_MESSAGE` observation, the mail probe's own idiom. //! -//! ## The banker (live-DB verified this session, `/Users/sam/dev/vmangos-deploy` → `characters`/ +//! ## The banker (live-DB verified against the local vmangos, `characters`/ //! `mangos` DBs) //! //! Soleil Stonemantle, creature entry 5099, spawn guid 12629, map 0 (Ironforge, The Vault), pos @@ -36,7 +36,8 @@ //! WOW_DATA=WoW/Data WOW_USER=probe1 WOW_PASS=pprobe1 WOW_CHAR=Probeone \ //! WOW_PROBE_BANK=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — this worktree is `pool-1` → `probe1`/`pprobe1`/`Probeone`). +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). //! Non-combat; GM mode is left exactly as found. An outer `timeout` + grep on `PROBE_BANK:` is the //! whole harness; the probe self-exits ([`super::probes::ProbeExitPlugin`]'s pattern) once DONE. diff --git a/crates/benilla-app/src/capture/probe_bg.rs b/crates/benilla-app/src/capture/probe_bg.rs index d1f03d73..5629b993 100644 --- a/crates/benilla-app/src/capture/probe_bg.rs +++ b/crates/benilla-app/src/capture/probe_bg.rs @@ -93,8 +93,7 @@ //! assignment reads off it (0649, 0679). //! //! Non-combat — the probe never attacks and never stands anywhere contested; with one player in -//! the instance there is nobody to fight. Pair with the SLOT-KEYED probe identity -//! (`docs/METHOD.md`, "The local vmangos server"). +//! the instance there is nobody to fight. Pair with the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill). use bevy::ecs::system::NonSendMut; use bevy::prelude::*; diff --git a/crates/benilla-app/src/capture/probe_bg_queue.rs b/crates/benilla-app/src/capture/probe_bg_queue.rs index 67fc8c4b..f4181123 100644 --- a/crates/benilla-app/src/capture/probe_bg_queue.rs +++ b/crates/benilla-app/src/capture/probe_bg_queue.rs @@ -30,8 +30,7 @@ //! answers `none`. A body left queued is not harmful, only surprising: every later login on that //! account carries an extra `SMSG_BATTLEFIELD_STATUS` in its burst. //! -//! Non-combat. Pair with the SLOT-KEYED probe identity (`WOW_USER=probeN WOW_PASS=pprobeN -//! WOW_CHAR=Probe`; `docs/METHOD.md`, "The local vmangos server"), and `WOW_NOSOUND=1` when +//! Non-combat. Pair with the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill), and `WOW_NOSOUND=1` when //! it runs unattended. One `timeout`'d run plus a grep for `PROBE bgqueue:` is the whole harness. use bevy::prelude::*; diff --git a/crates/benilla-app/src/capture/probe_binder.rs b/crates/benilla-app/src/capture/probe_binder.rs index 734c7bb3..5afe9319 100644 --- a/crates/benilla-app/src/capture/probe_binder.rs +++ b/crates/benilla-app/src/capture/probe_binder.rs @@ -15,7 +15,7 @@ //! visible against a real server sending real bytes, which is what this probe puts in front of //! them. //! -//! ## The innkeeper (live-DB verified this session, `/Users/sam/dev/vmangos-deploy` → `mangos` DB) +//! ## The innkeeper (live-DB verified against the local vmangos, `mangos` DB) //! //! Innkeeper Keldamyr — `creature_template.entry = 6736`, spawn `creature.guid = 46343`, **map 1** //! (Teldrassil, Dolanaar), position `(9802.21, 982.608, 1313.98)`, @@ -77,8 +77,8 @@ //! WOW_DATA=WoW/Data WOW_USER=probe1 WOW_PASS=pprobe1 WOW_CHAR=Probeone \ //! WOW_PROBE_BINDER=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — this worktree is `pool-1` → `probe1`/`pprobe1`/`Probeone`; -//! docs/METHOD.md "The local vmangos server"). Non-combat, and GM mode is left exactly as found. An +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). Non-combat, and GM mode is left exactly as found. An //! outer `timeout` + grep on `PROBE_BINDER:` is the whole harness; the probe self-exits once DONE. //! //! Every step SKIPs with a note rather than FAILing for an environmental problem (the NPC never diff --git a/crates/benilla-app/src/capture/probe_book.rs b/crates/benilla-app/src/capture/probe_book.rs index 49e33002..f125bc10 100644 --- a/crates/benilla-app/src/capture/probe_book.rs +++ b/crates/benilla-app/src/capture/probe_book.rs @@ -1,7 +1,7 @@ //! The world-book live probe (`WOW_PROBE_BOOK=1`) — B240's instrument: what does having the item- //! text reader open actually cost per frame, on the real object the report names? //! -//! Goudy, 2026-08-09 (`#bugs` `1535810645956632686`): *"page text with html text absolutely +//! Reported on Discord, 2026-08-09: *"page text with html text absolutely //! annihilates performance"* — ~50% fps drop (62 → 36 fps, 16.0 → 28.0 ms) while the *Alliance //! Military Ranks* plaque's reader is up, recovering the moment it closes. That is a **frame-cost //! A/B**, and eyeballing an fps counter is exactly the way not to settle one (docs/METHOD.md's @@ -21,8 +21,8 @@ //! WOW_NOSOUND=1 WOW_USER=probe0 WOW_PASS=pprobe0 WOW_CHAR=Probezero \ //! WOW_PROBE_BOOK=1 cargo run -q -p benilla --release //! ``` -//! (the slot-keyed probe identity — `pool-N` → `probeN`/`pprobeN`/`Probe`, docs/METHOD.md -//! "The local vmangos server"). An outer `timeout` + a grep on `PROBE_BOOK:` is the whole harness; +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). An outer `timeout` + a grep on `PROBE_BOOK:` is the whole harness; //! the probe self-exits ([`super::probes::ProbeExitPlugin`]'s pattern) once DONE. use bevy::prelude::*; @@ -36,8 +36,8 @@ use crate::player::Player; use crate::ui_item_text::ItemTextOpen; use crate::ui_script::{UiCostWanted, UiFrameCost}; -/// Goudy's own reported standing position beside the plaque (his debug panel, screenshot -/// `1535810480424222760-1`). +/// The reporter's own standing position beside the plaque (their debug panel, in the report's +/// screenshot). const PLAQUE_AT: [f32; 3] = [-8760.2, 402.3, 103.9]; /// `GAMEOBJECT_TYPE_TEXT` — the strategy type a book/plaque carries (decision 1105). const GO_TYPE_TEXT: i32 = 9; diff --git a/crates/benilla-app/src/capture/probe_castcancel.rs b/crates/benilla-app/src/capture/probe_castcancel.rs index af50a283..ca97c042 100644 --- a/crates/benilla-app/src/capture/probe_castcancel.rs +++ b/crates/benilla-app/src/capture/probe_castcancel.rs @@ -14,8 +14,7 @@ //! measured off the timestamps, not eyeballed. Non-combat, safe unattended //! (docs/METHOD.md's rule bans unattended *combat* probes); a hearth that completes because the //! cancel FAILED just ports the probe character home — visible in the log as the missing cancel line. Pair with -//! the slot-keyed probe identity (`WOW_USER=probeN WOW_PASS=pprobeN WOW_CHAR=Probe`, -//! docs/METHOD.md) + `WOW_CAST_TRACE=1 WOW_PROBE_EXIT_AT=`. +//! the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill) + `WOW_CAST_TRACE=1 WOW_PROBE_EXIT_AT=`. use bevy::prelude::*; diff --git a/crates/benilla-app/src/capture/probe_charcreate.rs b/crates/benilla-app/src/capture/probe_charcreate.rs index 6c0314fd..dec32c6d 100644 --- a/crates/benilla-app/src/capture/probe_charcreate.rs +++ b/crates/benilla-app/src/capture/probe_charcreate.rs @@ -8,7 +8,8 @@ //! the `SMSG_CHAR_CREATE` result byte, and — unless `WOW_PROBE_CHARCREATE_KEEP=1` — deletes the //! character it just made ([`CharRequest::Delete`]) so the account isn't littered, logging that //! result too. Run it **without** `WOW_CHAR` (which would auto-enter the world and leave select), -//! against the slot-keyed probe account (`WOW_USER=probeN WOW_PASS=pprobeN`, N = your pool slot — docs/METHOD.md "The local vmangos server"); creating/deleting characters is a +//! against the checkout's own probe account (`.probe-identity`, or `WOW_USER`/`WOW_PASS` — the +//! `probe` skill); creating/deleting characters is a //! non-combat operation, safe to run headlessly. use benilla_protocol::{messages, CharAction, CharCreateReq}; diff --git a/crates/benilla-app/src/capture/probe_charter.rs b/crates/benilla-app/src/capture/probe_charter.rs index 24acb6ab..3aeca3c1 100644 --- a/crates/benilla-app/src/capture/probe_charter.rs +++ b/crates/benilla-app/src/capture/probe_charter.rs @@ -16,7 +16,7 @@ //! fork arm whose whole evidence is "no other arm accepts a charter". Those are only visible //! against real bytes. //! -//! ## The registrar (live-DB verified this session, `/Users/sam/dev/vmangos-deploy` → `mangos` DB) +//! ## The registrar (live-DB verified against the local vmangos, `mangos` DB) //! //! Aldwin Laughlin, the Stormwind guild registrar — `creature_template.entry = 4974`, spawn //! `creature.guid = 79681`, **map 0**, position `(-8885.25, 614.395, 95.2576)`, @@ -96,8 +96,7 @@ //! WOW_NOSOUND=1 WOW_USER=probe0 WOW_PASS=pprobe0 WOW_CHAR=Probezero \ //! WOW_PROBE_CHARTER=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — this worktree is `pool-0` → `probe0`/`pprobe0`/`Probezero`; -//! docs/METHOD.md "The local vmangos server". **Never the default `one` account** — a login on it kicks +//! (the checkout's probe identity, the `probe` skill. **Never a player's account** — a login on it kicks //! the director's live session.) `WOW_NOSOUND=1` because an unattended probe must not play zone //! music into the director's room; `caffeinate -dis` is **not** needed for a run this short — the //! whole sequence is a handful of round trips and finishes in well under a minute. diff --git a/crates/benilla-app/src/capture/probe_chest.rs b/crates/benilla-app/src/capture/probe_chest.rs index 34c9f380..aecacd2e 100644 --- a/crates/benilla-app/src/capture/probe_chest.rs +++ b/crates/benilla-app/src/capture/probe_chest.rs @@ -1,7 +1,7 @@ //! The chest live probe (`WOW_PROBE_CHEST=1`) — **B84's instrument**: does the player kneel at an //! open chest? //! -//! Goudy, 2026-07-26 (`#bugs` `1530708187173359646`): *"No looting/opening animation when using a +//! Reported on Discord, 2026-07-26: *"No looting/opening animation when using a //! chest"*, with a benilla/reference pair at the same Mirror Lake Orchard chest — the reference //! gnome kneeling over the open loot window, ours standing bolt upright. Decision 0515 had shipped //! the kneel keyed to the `CMSG_LOOT` send alone and recorded a chest's silence as *correct*; 1471 @@ -38,8 +38,8 @@ //! WOW_NOSOUND=1 WOW_USER=probe0 WOW_PASS=pprobe0 WOW_CHAR=Probezero \ //! WOW_PROBE_CHEST=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — `pool-N` → `probeN`/`pprobeN`/`Probe`, `docs/METHOD.md` -//! "The local vmangos server"). `WOW_PROBE_CHEST=,,[,]` aims it at a different +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). `WOW_PROBE_CHEST=,,[,]` aims it at a different //! object; the default is a live `Battered Chest` spawn (`gameobject.guid` 26756, template 2843) in //! the Mirror Lake stretch of Elwynn, the same corner of the world the report came from. A chest is //! a respawning spawn point: if someone emptied that one it will be missing for its respawn timer, diff --git a/crates/benilla-app/src/capture/probe_clam.rs b/crates/benilla-app/src/capture/probe_clam.rs index 66f52d8a..f24774f6 100644 --- a/crates/benilla-app/src/capture/probe_clam.rs +++ b/crates/benilla-app/src/capture/probe_clam.rs @@ -34,8 +34,8 @@ //! WOW_NOSOUND=1 WOW_USER=probe0 WOW_PASS=pprobe0 WOW_CHAR=Probezero \ //! WOW_PROBE_CLAM=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — `pool-N` → `probeN`/`pprobeN`/`Probe`, `docs/METHOD.md` -//! "The local vmangos server"). `WOW_PROBE_CLAM=` aims it at a different openable template; +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). `WOW_PROBE_CLAM=` aims it at a different openable template; //! the default is a Small Barnacled Clam (7973 — `Flags` LOOTABLE with `LockID = 0`, so it is //! openable the moment it exists: no key, no lockpicking). The probe `.additem`s its own copy and //! subtracts it again on the way out, so it leaves the character as it found it. diff --git a/crates/benilla-app/src/capture/probe_crossing.rs b/crates/benilla-app/src/capture/probe_crossing.rs index a5214dea..7c11e029 100644 --- a/crates/benilla-app/src/capture/probe_crossing.rs +++ b/crates/benilla-app/src/capture/probe_crossing.rs @@ -4,9 +4,8 @@ //! report the seam: aboard → map flip (TRANSFER_PENDING / NEW_WORLD riding branch, logged by //! the net layer) → still riding → arrived docked on the far continent. Every phase edge prints //! a `PROBE crossing:` line, so an outer `timeout`d run + grep is the whole harness. Non-combat. -//! Pair with the SLOT-KEYED probe identity (`WOW_USER=probeN WOW_PASS=pprobeN -//! WOW_CHAR=Probe` for a `pool-N` worktree — docs/METHOD.md "The local vmangos server"; -//! a shared account gets kicked by parallel sessions mid-ride). +//! Pair with the checkout's own probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR +//! — the `probe` skill; a shared account gets kicked by parallel sessions mid-ride). use bevy::prelude::*; diff --git a/crates/benilla-app/src/capture/probe_gm_ticket.rs b/crates/benilla-app/src/capture/probe_gm_ticket.rs index c7d193af..addb794a 100644 --- a/crates/benilla-app/src/capture/probe_gm_ticket.rs +++ b/crates/benilla-app/src/capture/probe_gm_ticket.rs @@ -93,8 +93,8 @@ //! WOW_NOSOUND=1 WOW_USER=probe3 WOW_PASS=pprobe3 WOW_CHAR=Probethree \ //! WOW_PROBE_GMTICKET=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — this worktree is `pool-3` → `probe3`/`pprobe3`/`Probethree`; -//! docs/METHOD.md "The local vmangos server"). Non-combat; the probe never drives the body and sends no +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). Non-combat; the probe never drives the body and sends no //! GM command, so GM mode and position are left exactly as found — but note that the body is not //! necessarily *still* when a run starts (see [`REST_EPS`]). An outer `timeout` + grep on //! `PROBE_GMTICKET:` is the whole harness; the probe self-exits once DONE. Nothing here is a @@ -163,11 +163,11 @@ const REST_TIMEOUT_SECS: f64 = 20.0; /// the packet, before the db-expect line stops being trustworthy (yd, WoW space). const STAMP_DRIFT_EPS: f32 = 0.05; -/// The operator's half of step 4, printed verbatim so it can be pasted. -const DB_QUERY: &str = "cd /Users/sam/dev/vmangos-deploy && docker compose exec -T database \ - mariadb -umangos -pmangos characters -e \"SELECT ticket_id, name, \ - ticket_type, map, position_x, position_y, position_z, closed_by, message \ - FROM gm_tickets ORDER BY ticket_id DESC LIMIT 1;\""; +/// The operator's half of step 4: the row to read back off the server's `characters` database, +/// printed verbatim so it can be pasted into whatever reaches that database. +const DB_QUERY: &str = "SELECT ticket_id, name, ticket_type, map, position_x, position_y, \ + position_z, closed_by, message FROM gm_tickets ORDER BY ticket_id DESC \ + LIMIT 1;"; pub(crate) struct ProbeGmTicketPlugin; @@ -563,7 +563,8 @@ fn gm_ticket_probe( warn!( "PROBE_GMTICKET: SKIP (4 db) — operator step, by design: the client never \ reads map/position back, so only the server's own row proves they landed. \ - After the run: {DB_QUERY} — and compare against the db-expect line(s)." + After the run, against the server's `characters` database: {DB_QUERY} — \ + and compare against the db-expect line(s)." ); let text = unique_text(" edited"); if !run_or_skip( diff --git a/crates/benilla-app/src/capture/probe_goquest.rs b/crates/benilla-app/src/capture/probe_goquest.rs index 60567897..ff1a5dcd 100644 --- a/crates/benilla-app/src/capture/probe_goquest.rs +++ b/crates/benilla-app/src/capture/probe_goquest.rs @@ -43,8 +43,8 @@ //! WOW_NOSOUND=1 WOW_USER=probe4 WOW_PASS=pprobe4 WOW_CHAR=Probefour \ //! WOW_PROBE_GOQUEST=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — `pool-N` → `probeN`/`pprobeN`/`Probe`, `docs/METHOD.md` -//! "The local vmangos server"). `WOW_PROBE_GOQUEST=,,[,]` aims it elsewhere; the +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). `WOW_PROBE_GOQUEST=,,[,]` aims it elsewhere; the //! default is the **Goldshire wanted poster** — `gameobject.guid` 26843, template 68 //! `Wanted Poster`, `type` 2 `GAMEOBJECT_TYPE_QUESTGIVER`, `flags` 4 `INTERACT_COND`, offering //! quest 176 `Wanted: "Hogger"` (`MinLevel` 5, `QuestLevel` 11). It is a single **unpooled** spawn diff --git a/crates/benilla-app/src/capture/probe_guard_poi.rs b/crates/benilla-app/src/capture/probe_guard_poi.rs index c4a87704..d756b023 100644 --- a/crates/benilla-app/src/capture/probe_guard_poi.rs +++ b/crates/benilla-app/src/capture/probe_guard_poi.rs @@ -10,8 +10,7 @@ //! — distance, view radius, and which of the two draws it takes — so the run says *what would be //! on screen*, not merely that a packet arrived. //! -//! Non-combat. Pair with the SLOT-KEYED probe identity (`WOW_USER=probeN WOW_PASS=pprobeN -//! WOW_CHAR=Probe`; `docs/METHOD.md`, "The local vmangos server"), and `WOW_NOSOUND=1` when +//! Non-combat. Pair with the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill), and `WOW_NOSOUND=1` when //! it runs unattended. One `timeout`'d run plus a grep for `PROBE guardpoi:` is the whole harness. use bevy::prelude::*; diff --git a/crates/benilla-app/src/capture/probe_mail.rs b/crates/benilla-app/src/capture/probe_mail.rs index 482b48df..ca3085f5 100644 --- a/crates/benilla-app/src/capture/probe_mail.rs +++ b/crates/benilla-app/src/capture/probe_mail.rs @@ -5,7 +5,7 @@ //! `PROBE_MAIL: DONE pass= fail=` summary. Modeled closely on [`super::probe_taxi`] (same //! phase-machine shape, same trace style, same self-terminating exit). //! -//! ## The GM `.send` syntax (verified `/Users/sam/wre/vmangos-src`, `Chat/Chat.cpp` + +//! ## The GM `.send` syntax (verified against the vmangos source, `Chat/Chat.cpp` + //! `Commands/MiscCommands.cpp`) //! //! `sendCommandTable[]` (Chat.cpp ~l.931): `.send mail "subject" "text"` is @@ -39,9 +39,8 @@ //! WOW_DATA= WOW_USER=probe2 WOW_PASS=pprobe2 WOW_CHAR=Probetwo \ //! WOW_PROBE_MAIL=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — `pool-N` → `WOW_USER=probeN WOW_PASS=pprobeN -//! WOW_CHAR=Probe`, docs/METHOD.md "The local vmangos server"; this worktree is `pool-2` → -//! `probe2`/`pprobe2`/`Probetwo`). `WOW_CHAR` doubles as the probe's own mail-send target — read +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). `WOW_CHAR` doubles as the probe's own mail-send target — read //! once at world-enter, never hardcoded. An outer `timeout` + grep on `PROBE_MAIL:` is the whole //! harness; the probe self-exits (the [`super::probes::ProbeExitPlugin`] pattern) once DONE. diff --git a/crates/benilla-app/src/capture/probe_melee.rs b/crates/benilla-app/src/capture/probe_melee.rs index 278eb973..e6625d42 100644 --- a/crates/benilla-app/src/capture/probe_melee.rs +++ b/crates/benilla-app/src/capture/probe_melee.rs @@ -2,7 +2,7 @@ //! once in-world, periodically fire the attack-nearest core ([`AttackNearestRequest`] — the //! action-bar attack's own no-selection path), so the probe character fights whatever is closest //! while the `benilla_assets::trace` sink (`WOW_MOVE_TRACE=`) records the swing→impact→spawn combat-text -//! timeline for pair-birth analysis. Pair with the slot-keyed probe identity (docs/METHOD.md) + an outer +//! timeline for pair-birth analysis. Pair with the checkout's probe identity (the `probe` skill) + an outer //! `timeout`. //! //! **Do NOT run this unattended** (director's rule, 2026-07-14, `docs/METHOD.md` "The local vmangos diff --git a/crates/benilla-app/src/capture/probe_model_camera.rs b/crates/benilla-app/src/capture/probe_model_camera.rs index 4c92a0ab..d0569db5 100644 --- a/crates/benilla-app/src/capture/probe_model_camera.rs +++ b/crates/benilla-app/src/capture/probe_model_camera.rs @@ -38,7 +38,7 @@ //! WOW_USER=probe5 WOW_PASS=pprobe5 WOW_CHAR=Probefive WOW_UNATTENDED=1 WOW_NOSOUND=1 \ //! WOW_PROBE_MODEL_CAMERA=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — docs/METHOD.md "The local vmangos server"). `WOW_TILE_TRACE=1` +//! (the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill)). `WOW_TILE_TRACE=1` //! alongside it prints the leg, the record, the eye and the matrix per pane per frame. use bevy::camera::visibility::RenderLayers; diff --git a/crates/benilla-app/src/capture/probe_partner.rs b/crates/benilla-app/src/capture/probe_partner.rs index 9a90707c..3bddef10 100644 --- a/crates/benilla-app/src/capture/probe_partner.rs +++ b/crates/benilla-app/src/capture/probe_partner.rs @@ -6,7 +6,7 @@ //! questions answerable at all: whether the opponent turns hostile, whether the arbiter flag //! plants. Non-combat (the unattended-combat ban, `docs/METHOD.md` "The local vmangos server", stands //! untouched: this probe answers a challenge but never swings, and a duel nobody strikes in -//! simply times out). Pair with the slot-keyed probe identity (`WOW_USER=probeN …`, docs/METHOD.md). +//! simply times out). Pair with the checkout's probe identity (the `probe` skill). use bevy::prelude::*; diff --git a/crates/benilla-app/src/capture/probe_rig.rs b/crates/benilla-app/src/capture/probe_rig.rs index d06b598a..12e96a4e 100644 --- a/crates/benilla-app/src/capture/probe_rig.rs +++ b/crates/benilla-app/src/capture/probe_rig.rs @@ -1,4 +1,4 @@ -//! The probe **rig** (`WOW_RIG=""`, decision 0651) — one command that puts this slot's probe +//! The probe **rig** (`WOW_RIG=""`, decision 0651) — one command that puts this checkout's probe //! account into a chosen **body** and hands the session a world that is ready to test. //! //! ## Why it exists @@ -17,7 +17,7 @@ //! ```text //! WOW_RIG="tauren druid 60 gear:heal-preraid-bis spec:heal-preraid-bis at:ThunderBluff" //! WOW_RIG="gnome mage 39 gear:dps-39-twink gm:off" -//! WOW_RIG="60 gear:dps-preraid-bis" # the slot's own Probe, no new character +//! WOW_RIG="60 gear:dps-preraid-bis" # the account's own probe body, no new character //! WOW_RIG="nightelf druid" # just a body of that shape, level 1 //! ``` //! @@ -26,14 +26,15 @@ //! **Race + class name a character, so the rig owns the pick.** vmangos has no class-change command //! (`.character race` exists; there is no `.character class`), so a different class *is* a different //! character — which makes creating one the honest primitive rather than a fallback. The name is -//! derived, never invented: `[f]`, e.g. Tauren Druid on `pool-1` → -//! `Taudruone`. Deterministic means the *next* session reuses the same body instead of littering a -//! second one, and the pattern is what makes eviction safe (below). Omit race+class and the rig -//! configures whatever `WOW_CHAR`/the slot default already logs in as. +//! derived, never invented: `[f]`, the word being this checkout's +//! (`run_mode::rig_suffix`), e.g. a Tauren Druid for `Probeone` → `Taudruone`. Deterministic means +//! the *next* session reuses the same body instead of littering a second one, and the pattern is +//! what makes eviction safe (below). Omit race+class and the rig configures whatever `WOW_CHAR` +//! already logs in as. //! //! **The roster is a cache with an eviction policy.** `CharactersPerRealm` is 10, and there are 40 //! valid race/class pairs, so a busy account fills. The rig does not hardcode the limit: it tries -//! the create, and only on `CHAR_CREATE_SERVER_LIMIT` evicts **the rig-named character on this slot +//! the create, and only on `CHAR_CREATE_SERVER_LIMIT` evicts **the rig-named character of this checkout //! that is cheapest to rebuild** — lowest level first, ties broken by oldest — then retries once. //! Nothing outside the rig's own naming pattern is ever deleted, so `Probe` and anything a human //! made are untouchable. @@ -44,7 +45,7 @@ //! It always revives a dead or ghost body, whether or not you asked — that is the 121× command, and //! there is no session that wants to keep testing on a corpse. //! -//! ## The GM verbs behind it (all verified against `/Users/sam/wre/vmangos-src`) +//! ## The GM verbs behind it (all verified against the vmangos source, github.com/vmangos/core) //! //! | rig token | command | needs | note | //! |---|---|---|---| @@ -225,7 +226,7 @@ impl RigSpec { fn describe(&self) -> String { let body = self .body - .map_or("this slot's probe character".into(), |(r, c, g)| { + .map_or("the account's probe character".into(), |(r, c, g)| { format!( "{} {} {}", if g == 1 { "female" } else { "male" }, @@ -360,13 +361,13 @@ fn drive_rig( messages::CHAR_CREATE_SUCCESS => rig.phase = RigPhase::AwaitRoster, messages::CHAR_CREATE_SERVER_LIMIT if !rig.evicted => { rig.evicted = true; - match crate::run_mode::slot_word().and_then(|slot| { - evictable(&rig.roster, rig_char_name(&rig.spec).as_deref(), slot) + match crate::run_mode::rig_suffix().and_then(|word| { + evictable(&rig.roster, rig_char_name(&rig.spec).as_deref(), &word) }) { Some((guid, name)) => { warn!( "rig: the account is full — evicting the cheapest rig character to rebuild, {name} \ - (guid {guid}). Only rig-named characters on this slot are ever \ + (guid {guid}). Only this checkout's rig-named characters are ever \ deleted; Probe and hand-made characters are never touched." ); let _ = pick.0.send(CharRequest::Delete(guid)); @@ -518,7 +519,7 @@ fn build_steps(spec: &RigSpec, dead: bool) -> Vec { /// `game_tele` name. The facing is load-bearing exactly when a motion probe walks from the /// pin: `W` follows the BODY's facing, and each probe character keeps whatever facing it last /// had — an unpinned facing sent probe0 and probe4 down different routes from the same point, -/// which invalidated a cross-slot A/B before 1462's sitting caught it. +/// which invalidated a cross-checkout A/B before 1462's sitting caught it. fn parse_point(at: &str) -> Option<(i32, f32, f32, f32, Option)> { let mut parts = at.split(',').map(str::trim); let map = parts.next()?.parse().ok()?; @@ -536,9 +537,9 @@ fn parse_point(at: &str) -> Option<(i32, f32, f32, f32, Option)> { fn rig_char_name(spec: &RigSpec) -> Option { let (race, class, gender) = spec.body?; - let slot = crate::run_mode::slot_word()?; + let word = crate::run_mode::rig_suffix()?; let name = format!( - "{}{}{slot}{}", + "{}{}{word}{}", race_code(race)?, class_code(class)?, if gender == 1 { "f" } else { "" } @@ -550,45 +551,45 @@ fn rig_char_name(spec: &RigSpec) -> Option { Some(chars.next()?.to_ascii_uppercase().to_string() + &chars.as_str().to_ascii_lowercase()) } -/// The rig-named character on this slot that costs least to lose: **lowest level first**, ties +/// The rig-named character of this checkout that costs least to lose: **lowest level first**, ties /// broken by oldest (the enum is ordered by `create_time` — vmangos `HandleCharEnumOpcode` — so an /// earlier index *is* older). Level is the proxy for invested setup: a level-1 body is 15 seconds to /// rebuild, a geared 60 is a minute and a talent tree. Evicting by age alone would throw away the /// most valuable body on the account first, which the live fill test made obvious. /// -/// Anything that is not a rig name for `slot` — `Probe`, a hand-made character, another slot's -/// leftovers — is invisible here, and that is what makes automatic deletion safe. +/// Anything that is not a rig name for `word` — the probe body itself, a hand-made character, +/// another checkout's leftovers — is invisible here, and that is what makes automatic deletion safe. /// -/// `slot` is a **parameter, not a `slot_word()` call inside**: reading the ambient slot here made -/// the eviction test pass only in the worktree it was written in (`pool-1`, whose rig names end -/// `…one`) and fail in every other slot — and in the primary checkout, where `slot_word()` is -/// `None` and nothing matches at all. A unit test's answer must not depend on which directory the -/// build happened in. +/// `word` is a **parameter, not a `rig_suffix()` call inside**: reading the ambient word here made +/// the eviction test pass only in the checkout it was written in (whose rig names end `…one`) and +/// fail in every other — and in a checkout with no declaration, where `rig_suffix()` is `None` and +/// nothing matches at all. A unit test's answer must not depend on which directory the build +/// happened in. fn evictable( roster: &[benilla_protocol::Character], want: Option<&str>, - slot: &str, + word: &str, ) -> Option<(u64, String)> { roster .iter() .enumerate() .filter(|(_, c)| { - !want.is_some_and(|w| c.name.eq_ignore_ascii_case(w)) && is_rig_name(&c.name, slot) + !want.is_some_and(|w| c.name.eq_ignore_ascii_case(w)) && is_rig_name(&c.name, word) }) .min_by_key(|(age, c)| (c.level, *age)) .map(|(_, c)| (c.guid, c.name.clone())) } -/// Whether a roster name was minted by [`rig_char_name`] for this slot: a known race code, a known -/// class code, this slot's word, and nothing but an optional `f` after it. -fn is_rig_name(name: &str, slot: &str) -> bool { +/// Whether a roster name was minted by [`rig_char_name`] for this checkout: a known race code, a +/// known class code, this checkout's word, and nothing but an optional `f` after it. +fn is_rig_name(name: &str, word: &str) -> bool { let lower = name.to_ascii_lowercase(); let Some(rest) = lower.get(3..).and_then(|r| r.get(3..)) else { return false; }; let has_codes = RACE_CODES.iter().any(|(_, c)| lower.starts_with(c)) && CLASS_CODES.iter().any(|(_, c)| lower[3..].starts_with(c)); - has_codes && (rest == slot || rest == format!("{slot}f")) + has_codes && (rest == word || rest == format!("{word}f")) } /// Race id → the 3-letter name code. `Nel`/`Und` rather than the DBC's own prefixes: unambiguous, @@ -770,7 +771,7 @@ mod tests { #[test] fn the_derived_name_is_deterministic_and_fits_the_server_limit() { - // `[f]`, normalized the way vmangos normalizes a player name. + // `[f]`, normalized the way vmangos normalizes a player name. let name = |spec: &str, slot: &str| { let s = RigSpec::parse(spec).unwrap(); let (race, class, gender) = s.body.unwrap(); @@ -835,9 +836,9 @@ mod tests { #[test] fn eviction_spends_the_body_that_is_cheapest_to_rebuild() { - // The slot is passed in, never read from the ambient build directory — these names are - // pool-1's, and taking the slot from `slot_word()` made this test pass in pool-1 and fail - // in every other worktree (and in the primary checkout, where it is `None`). + // The word is passed in, never read from the ambient checkout — these names are one + // checkout's, and taking the word from `rig_suffix()` made this test pass there and fail + // in every other checkout (and in one with no declaration, where it is `None`). let slot = "one"; // Roster order IS create order (vmangos enumerates by `create_time`). let roster = [ @@ -859,8 +860,8 @@ mod tests { ); // Nothing rig-named on this slot ⇒ nothing to evict; the caller errors rather than guessing. assert_eq!(evictable(&roster[..2], None, slot), None); - // And another slot's leftovers are invisible: the same roster, read as pool-3, evicts - // nothing. This is the assertion that would have caught the ambient-slot read. + // And another checkout's leftovers are invisible: the same roster, read with another word, + // evicts nothing. This is the assertion that would have caught the ambient read. assert_eq!(evictable(&roster, None, "three"), None); } diff --git a/crates/benilla-app/src/capture/probe_service.rs b/crates/benilla-app/src/capture/probe_service.rs index 952885a0..e351507a 100644 --- a/crates/benilla-app/src/capture/probe_service.rs +++ b/crates/benilla-app/src/capture/probe_service.rs @@ -21,7 +21,7 @@ //! | *any pure questgiver with an offer* | `0x02`, GOSSIP clear | Questgiver | quest frame | //! | Alma Jainrose (812) | `0x10` TRAINER only | Trainer | trainer frame | //! -//! The flag column is live-DB verified this session (`vmangos-deploy` → `mangos`, +//! The flag column is live-DB verified against the local vmangos (`mangos`, //! `SELECT entry, name, npc_flags FROM creature_template`), and reported again from the wire on //! every run — a template edit shows up as a note beside the reading rather than as a mystery FAIL. //! @@ -48,7 +48,7 @@ //! WOW_DATA=WoW/Data WOW_USER=probe4 WOW_PASS=pprobe4 WOW_CHAR=Probefour \ //! WOW_UNATTENDED=1 WOW_PROBE_SERVICE=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — docs/METHOD.md "The local vmangos server"). Non-combat, GM mode +//! (the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill)). Non-combat, GM mode //! left as found, nothing bought and nothing turned in: every leg opens a window and closes it //! client-side, which is what the reference's own close does (`ui_gossip`: there is no //! `CMSG_GOSSIP_CLOSE` in 1.12). @@ -215,7 +215,7 @@ struct ServiceProbe { /// NPC look identical in a SKIP line otherwise — and on a loaded machine the first is far more /// likely: at load 61 this probe logged `frame hitch: ~1010 ms` on repeat (the ~1 fps regime /// decisions 0713/0777/1355 name) and its 20-second scan window bought about twenty samples. - /// `leg.sh` answers the same problem with a load guard (1157); a probe that does one thing and + /// A leg runner answers the same problem with a load guard (1157); a probe that does one thing and /// exits is better served by reporting what it actually got. polls: u32, /// Latched once [`Phase::Done`] has fired its exit (never re-fire on a later frame). @@ -426,7 +426,7 @@ fn service_probe( // frames a second this leg never really looked (0713/0777/1355). true => " — THE OBSERVER WAS STARVED, so this SKIP says nothing about \ - the NPC. Re-run on an idle machine (`uptime`; leg.sh's guard \ + the NPC. Re-run on an idle machine (`uptime`; a leg runner's guard \ is load < 3).", false => " — the machine was keeping up, so the `.go` was refused, the \ diff --git a/crates/benilla-app/src/capture/probe_stone.rs b/crates/benilla-app/src/capture/probe_stone.rs index 98a5043d..4feeb960 100644 --- a/crates/benilla-app/src/capture/probe_stone.rs +++ b/crates/benilla-app/src/capture/probe_stone.rs @@ -44,8 +44,8 @@ //! WOW_NOSOUND=1 WOW_UNATTENDED=1 WOW_USER=probe3 WOW_PASS=pprobe3 WOW_CHAR=Probethree \ //! WOW_PROBE_STONE=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — `pool-N` → `probeN`/`pprobeN`/`Probe`, `docs/METHOD.md` -//! "The local vmangos server"). `WOW_PROBE_STONE=,,[,]` aims it elsewhere. +//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe` +//! skill). `WOW_PROBE_STONE=,,[,]` aims it elsewhere. //! //! **It leaves the probe body at the stone's minimum level**, deliberately and without putting it //! back: the level is the probe's own instrument (leg 2 needs one side of the band and leg 3 the diff --git a/crates/benilla-app/src/capture/probe_taxi.rs b/crates/benilla-app/src/capture/probe_taxi.rs index 82984ca0..881d5318 100644 --- a/crates/benilla-app/src/capture/probe_taxi.rs +++ b/crates/benilla-app/src/capture/probe_taxi.rs @@ -7,8 +7,7 @@ //! arrival distance to the destination node's DBC position, and measured flight duration vs the //! DBC prediction `Σ path-segment length ÷ 32 yd/s` (`PLAYER_FLIGHT_SPEED`) — timing measured, //! never eyeballed (decision 0404). An outer `timeout`d run + grep is the whole harness. -//! Non-combat. Pair with the SLOT-KEYED probe identity (`WOW_USER=probeN WOW_PASS=pprobeN -//! WOW_CHAR=Probe`; docs/METHOD.md "The local vmangos server"). Uses `.taxicheat on` so +//! Non-combat. Pair with the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill). Uses `.taxicheat on` so //! the fresh probe character can fly to an unvisited node (and so the SHOWTAXINODES mask //! exercises the full-network branch); the 110-copper fare is DB-seeded (see the Wait phase — //! `.modify money` outranks the probe account). diff --git a/crates/benilla-app/src/capture/probe_vendor_swap.rs b/crates/benilla-app/src/capture/probe_vendor_swap.rs index 89981a88..1f81789b 100644 --- a/crates/benilla-app/src/capture/probe_vendor_swap.rs +++ b/crates/benilla-app/src/capture/probe_vendor_swap.rs @@ -36,7 +36,7 @@ //! WOW_USER=probe1 WOW_PASS=pprobe1 WOW_CHAR=Probeone WOW_UNATTENDED=1 WOW_NOSOUND=1 \ //! WOW_PROBE_VENDOR_SWAP=1 cargo run -q -p benilla //! ``` -//! (the slot-keyed probe identity — docs/METHOD.md "The local vmangos server"). +//! (the checkout's probe identity (`.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR — the `probe` skill)). use bevy::prelude::*; diff --git a/crates/benilla-app/src/capture/probes/ground_census.rs b/crates/benilla-app/src/capture/probes/ground_census.rs index 6d3c677c..3e5c3cdd 100644 --- a/crates/benilla-app/src/capture/probes/ground_census.rs +++ b/crates/benilla-app/src/capture/probes/ground_census.rs @@ -22,7 +22,7 @@ //! - **`terrain`** is the MCNK height under it, which is what the clamp finds when a building's own //! collider has not attached yet. //! -//! Pair it with the slot-keyed probe identity and a `.go` to the reported spot: +//! Pair it with the checkout's probe identity (the `probe` skill) and a `.go` to the reported spot: //! //! ```text //! WOW_USER=probeN WOW_PASS=pprobeN WOW_CHAR=Probe WOW_NOSOUND=1 \ diff --git a/crates/benilla-app/src/capture/probes/lift_census.rs b/crates/benilla-app/src/capture/probes/lift_census.rs index 59fa089b..3e5f74df 100644 --- a/crates/benilla-app/src/capture/probes/lift_census.rs +++ b/crates/benilla-app/src/capture/probes/lift_census.rs @@ -31,7 +31,7 @@ //! - **`meshes`** counts render descendants (the whole subtree, not just direct children — a GO //! model hangs its submeshes under an anim host). //! -//! Pair it with the slot-keyed probe identity and a `.go` to the reported spot: +//! Pair it with the checkout's probe identity (the `probe` skill) and a `.go` to the reported spot: //! //! ```text //! WOW_USER=probeN WOW_PASS=pprobeN WOW_CHAR=Probe WOW_NOSOUND=1 \ diff --git a/crates/benilla-app/src/capture/probes/live_fps.rs b/crates/benilla-app/src/capture/probes/live_fps.rs index 55031b94..e55515a0 100644 --- a/crates/benilla-app/src/capture/probes/live_fps.rs +++ b/crates/benilla-app/src/capture/probes/live_fps.rs @@ -272,7 +272,7 @@ fn drive_live_fps( // settle window on top — a run still worldless this far past it is stranded on a // glue screen (dead server, refused login the marker arms didn't catch) and every // second more is the 1371 sitting's dead wall-clock again. `FATAL` is the marker - // leg.sh keys on. + // a leg runner keys on. if time.elapsed_secs() > probe.at + BOOT_DEADLINE_SECS { error!( "live-fps: FATAL — still not in world {:.0}s past the probe delay; a measurement run with no world is dead. exiting", diff --git a/crates/benilla-app/src/capture/probes/trail_census.rs b/crates/benilla-app/src/capture/probes/trail_census.rs index 172691c7..b3052da4 100644 --- a/crates/benilla-app/src/capture/probes/trail_census.rs +++ b/crates/benilla-app/src/capture/probes/trail_census.rs @@ -32,7 +32,7 @@ //! Off a transport `deck=` reads `-` and the deck columns are the world ones, which is the control: //! this census must not change on the ground, where a streak *should* smear behind a moving host. //! -//! Pair it with the slot-keyed probe identity and a `.go` onto the Thunder Bluff lift: +//! Pair it with the checkout's probe identity (the `probe` skill) and a `.go` onto the Thunder Bluff lift: //! //! ```text //! WOW_USER=probeN WOW_PASS=pprobeN WOW_CHAR=Probe WOW_NOSOUND=1 \ diff --git a/crates/benilla-app/src/capture/probes/visual_census.rs b/crates/benilla-app/src/capture/probes/visual_census.rs index 2b91ae03..a8da49ab 100644 --- a/crates/benilla-app/src/capture/probes/visual_census.rs +++ b/crates/benilla-app/src/capture/probes/visual_census.rs @@ -38,7 +38,7 @@ //! never to be confused with one that built nothing, which is the distinction the census exists //! to keep. //! -//! Pair it with the slot-keyed probe identity and a `.go` to the reported spot: +//! Pair it with the checkout's probe identity (the `probe` skill) and a `.go` to the reported spot: //! //! ```text //! WOW_USER=probeN WOW_PASS=pprobeN WOW_CHAR=Probe WOW_NOSOUND=1 \ diff --git a/crates/benilla-app/src/char_select/mod.rs b/crates/benilla-app/src/char_select/mod.rs index 3fa0ee1e..c64bf506 100644 --- a/crates/benilla-app/src/char_select/mod.rs +++ b/crates/benilla-app/src/char_select/mod.rs @@ -581,7 +581,7 @@ fn apply_roster_policy( warn!("char select: WOW_CHAR={name} not on this account — showing roster"); // A driverless run can never pick a different row itself — parked here it // burns its whole wall-clock. Same home as the login arm's verdict, so the - // marker leg.sh greps for is written in exactly one place (1371, 1769). + // marker a leg runner greps for is written in exactly one place (1371, 1769). if std::env::var_os("WOW_LOGIN_SMOKE").is_none() && crate::run_mode::fatal_when_driverless(&format!( "WOW_CHAR={name} is not on this account" diff --git a/crates/benilla-app/src/crash.rs b/crates/benilla-app/src/crash.rs index 7d898380..a8b67d96 100644 --- a/crates/benilla-app/src/crash.rs +++ b/crates/benilla-app/src/crash.rs @@ -49,7 +49,7 @@ pub(crate) fn install(build: BuildId) { // The standing test affordance — `WOW_CRASH_INJECT=`, one deliberate main-thread panic // mid-run — is `perf::crash_inject`, a dev root: this module ships to players and knows nothing // of the `dev` seam (run_mode's one-door rule, 1176), and an instrument's home is with the -// instruments. Its module doc carries the crosscheck incident that moved it there. +// instruments. Its module doc carries the cross-platform incident that moved it there. /// Re-entrancy latch: a panic *inside* the hook (a poisoned lock, a failed write) must not /// recurse into it. Never cleared on purpose — after one report the process is on its way out, diff --git a/crates/benilla-app/src/creature_anim/emote_anim.rs b/crates/benilla-app/src/creature_anim/emote_anim.rs index 99686147..594bf9b0 100644 --- a/crates/benilla-app/src/creature_anim/emote_anim.rs +++ b/crates/benilla-app/src/creature_anim/emote_anim.rs @@ -3,7 +3,7 @@ //! catalog ([`EmoteSounds`], promoted from `crate::sound`, decision 0070 slice 4) and hand off — //! after the **receive-side posture gate** ([`receive_eligible`]). //! -//! **Verified against vmangos** (`/Users/sam/wre/vmangos-src/src/game/Handlers/ChatHandler.cpp` +//! **Verified against vmangos** (vmangos `src/game/Handlers/ChatHandler.cpp` //! `HandleTextEmoteOpcode` + `src/game/Unit.cpp` `Unit::HandleEmote`/`HandleEmoteCommand`, //! 2026-07-03): a one-shot text emote (`/wave`, `Emotes.dbc EmoteType == 0`) sends **both** //! `SMSG_TEXT_EMOTE` (the chat line + voice — `EmoteKind::Text`) and `SMSG_EMOTE` diff --git a/crates/benilla-app/src/local_state.rs b/crates/benilla-app/src/local_state.rs index 6871af2a..f8eb84cb 100644 --- a/crates/benilla-app/src/local_state.rs +++ b/crates/benilla-app/src/local_state.rs @@ -26,7 +26,7 @@ //! 1. **`$BENILLA_HOME`** — explicit override (tests point it at a tempdir; a shared-config setup //! points it wherever it likes). //! 2. **`/benilla-config/`** — `#[cfg(feature = "dev")]` only, so dev runs across the -//! worktree pool keep one predictable place. Gated for the same reason the install resolver's +//! worktrees of the repo keep one predictable place. Gated for the same reason the install resolver's //! project-folder probe is: a shipped binary must not carry the build machine's source tree. //! (`.gitignore` carries `/benilla-config` for it.) //! 3. **`/benilla-config/`** — the release answer: your settings sit next to the program that @@ -80,11 +80,12 @@ pub(crate) fn home() -> Option { /// this binary was actually built in. /// /// This is the one place 1175's §4 needed a correction on contact with how we work. `benilla/` used -/// to hang off `$WOW_DATA`, and every pool slot symlinks `WoW` to the same install — so there has -/// always been exactly ONE settings folder no matter which of the eight slots built the binary. -/// Resolving to `CARGO_MANIFEST_DIR` instead would give eight, and the director's keybinds, macros -/// and camera pose would appear to reset whenever a session happened to claim a different slot. -/// That is a silent, recurring surprise, and it is not what "one predictable place" meant. +/// to hang off `$WOW_DATA`, and every worktree of the repo points `WoW` at the same install — so +/// there has always been exactly ONE settings folder no matter which worktree built the binary. +/// Resolving to `CARGO_MANIFEST_DIR` instead would give one per worktree, and the player's +/// keybinds, macros and camera pose would appear to reset whenever a session happened to build +/// in a different one. That is a silent, recurring surprise, and it is not what "one predictable +/// place" meant. /// /// A linked worktree's `.git` is a **file** reading `gitdir: /.git/worktrees/`, so /// the primary is derivable with no git binary and no build script: walk up to the common dir and @@ -556,7 +557,7 @@ mod tests { /// One test rather than a `#[cfg]`-ed pair, because the seam is `run_mode::dev_source_dir()`'s /// to know and nothing else's (1179): a player build has no source dir, so the state folder /// must sit beside the binary; a dev build has one, and must resolve to the PRIMARY checkout - /// so the eight pool slots keep sharing a single settings folder — exactly as they did when it + /// so every worktree keeps sharing a single settings folder — exactly as they did when it /// hung off the shared install. The dev half is asserted structurally (a `.git` *file* means a /// linked worktree, and then the answer must be somewhere else), so it says the same thing /// whether it runs in the primary or in a slot. @@ -586,7 +587,7 @@ mod tests { assert_ne!( h, here.join(STATE_DIR), - "a linked worktree must not get its own settings folder — the pool shares one" + "a linked worktree must not get its own settings folder — the worktrees share one" ); assert!( h.parent().unwrap().join(".git").is_dir(), diff --git a/crates/benilla-app/src/login/mod.rs b/crates/benilla-app/src/login/mod.rs index 234f5019..1cc1540a 100644 --- a/crates/benilla-app/src/login/mod.rs +++ b/crates/benilla-app/src/login/mod.rs @@ -4,7 +4,7 @@ //! Cinematics/TOS side of the reference screen is deliberately cut (the director's call). //! //! This module owns the **credential policy** — the 0193 §3 mirror for the IO thread's pre-logon -//! park: the env fast path (any of `WOW_USER`/`WOW_PASS`/`WOW_CHAR` explicitly set auto-submits +//! park: the env fast path (`WOW_USER` and `WOW_PASS` both set auto-submits //! with the old `one`/`pone` defaults, so every probe/smoke invocation keeps working), the //! pending-credentials resubmit (paced at the flat 3 s, app-side — the IO thread never sleeps), //! and the director's typed submit. A *refused* code (bad password) clears the intent and shows @@ -470,7 +470,7 @@ fn drive_policy( // nobody at the keyboard: a login failure no resubmit can change would leave it parked on a // dialog for its whole wall-clock, and every retry a runner grants it is spent the same way. // Those failures exit non-zero instead, on one greppable marker — "login: FATAL" — that - // leg.sh keys on (decision 1371). + // a leg runner keys on (decision 1371). // // **Nobody is here only if the run says so** (decision 1769). Whether the client may end the // run itself is [`crate::run_mode::fatal_when_driverless`]'s to answer; the two facts this @@ -479,20 +479,20 @@ fn drive_policy( let empty = GlueStrings::default(); let strings = strings.as_deref().unwrap_or(&empty); - // The env fast path, once (decision 0539 §3): any of WOW_USER/WOW_PASS/WOW_CHAR explicitly - // set → auto-submit env-with-defaults, so every probe/smoke/harness invocation keeps working. + // The env fast path, once (decision 0539 §3): WOW_USER and WOW_PASS both set → auto-submit + // them, so every probe/smoke/harness invocation keeps working. There is no default account. // The login smoke drives its own credentials instead. if !attempt.intent.env_read { attempt.intent.env_read = true; // Purely "are the credentials in the environment?" (decision 1769) — whether anybody is // here to *react* is a different fact with a different home, `run_mode::unattended`. if crate::run_mode::env_login() && std::env::var_os("WOW_LOGIN_SMOKE").is_none() { - let user = std::env::var("WOW_USER").unwrap_or_else(|_| "one".into()); - let pass = std::env::var("WOW_PASS").unwrap_or_else(|_| "pone".into()); + let user = std::env::var("WOW_USER").unwrap_or_default(); + let pass = std::env::var("WOW_PASS").unwrap_or_default(); // The account guard (decision 0649): a vmangos login KICKS whoever holds the account, - // so an unattended run from a pool slot must not authenticate as the director's `one` - // or a neighbouring slot's probe. Only the *automated* path is gated — a typed login - // is the director's own and is never second-guessed. + // so an unattended run authenticates as the account its checkout declares + // (`.probe-identity`) and nothing else. Only the *automated* path is gated — a typed + // login is the player's own and is never second-guessed. match crate::run_mode::account_guard(&user) { Ok(()) => { info!("login: env fast path — auto-submitting as {user}"); diff --git a/crates/benilla-app/src/perf/clock.rs b/crates/benilla-app/src/perf/clock.rs index 40502d7a..7a8ebe53 100644 --- a/crates/benilla-app/src/perf/clock.rs +++ b/crates/benilla-app/src/perf/clock.rs @@ -10,7 +10,7 @@ /// (`getrusage(RUSAGE_SELF)`). /// /// The perf probes report wall-clock frame time, which on this machine is not a usable regression -/// instrument: parallel session worktrees build on the same 14 cores, and two identical probe runs +/// instrument: parallel builds share the same cores, and two identical probe runs /// of the same pin came back 49.6 ms and 28.6 ms apart purely on machine load. CPU-per-frame moves /// with the work we actually do, not with who else is compiling — and it is the metric the Mac /// report is written in ("250 % CPU at 59 fps" against 1.12.1's "100 % at 160"), so a probe that diff --git a/crates/benilla-app/src/perf/crash_inject.rs b/crates/benilla-app/src/perf/crash_inject.rs index e2452935..eb9647ea 100644 --- a/crates/benilla-app/src/perf/crash_inject.rs +++ b/crates/benilla-app/src/perf/crash_inject.rs @@ -8,7 +8,7 @@ //! players and knows nothing of the `dev` seam, so its injector cannot sit beside it under a //! `cfg`. And it was first armed from inside the stall sampler, which is macOS-only //! (`/usr/bin/sample`) — so on Linux and Windows the injector was dead code, `-D warnings` made -//! that a red `scripts/crosscheck.sh` on both, and the macOS gates stayed green for six days. An +//! that a red cross-platform compile on both, and the macOS gates stayed green for six days. An //! instrument's cfg is `dev`, never a platform's; `PerfPlugin` arms this on every platform. use bevy::prelude::*; diff --git a/crates/benilla-app/src/perf/hud.rs b/crates/benilla-app/src/perf/hud.rs index 5290ad0f..33921f52 100644 --- a/crates/benilla-app/src/perf/hud.rs +++ b/crates/benilla-app/src/perf/hud.rs @@ -69,7 +69,7 @@ const HUD_REFRESH_SECS: f32 = 0.25; /// records the open gap: every campaign anchor was measured on a binary that draws this overlay, /// at a cost booked as "est 0.4–1.2 ms CPU + unquantified GPU" — an estimate, never a measurement, /// because nothing could turn the fixture off without also changing the binary. One env var makes -/// it an interleaved A/B on *one* binary instead (`scripts/leg.sh`), so the constant baked into +/// it an interleaved A/B on *one* binary instead (a leg runner), so the constant baked into /// every anchor becomes a number. The meters keep sampling either way: only the drawing stops, /// which is the half being priced — and with the default flipped, the *unmeasured* leg is now the /// one nobody is running. diff --git a/crates/benilla-app/src/perf/stall.rs b/crates/benilla-app/src/perf/stall.rs index 3f8b08c2..756d02a8 100644 --- a/crates/benilla-app/src/perf/stall.rs +++ b/crates/benilla-app/src/perf/stall.rs @@ -137,7 +137,7 @@ fn arm_injectors(app: &mut App) { } // The third standing injector — `crash::arm_injector`, a deliberate main-thread panic — is // armed by `PerfPlugin` itself, beside this plugin's registration, not from here: this - // module is macOS-only and the crash injector is not (its doc has the crosscheck incident). + // module is macOS-only and the crash injector is not (its doc has the cross-platform incident). } fn watchdog(dir: Option) { diff --git a/crates/benilla-app/src/portrait/mod.rs b/crates/benilla-app/src/portrait/mod.rs index 62402771..567a643a 100644 --- a/crates/benilla-app/src/portrait/mod.rs +++ b/crates/benilla-app/src/portrait/mod.rs @@ -1161,7 +1161,7 @@ fn feed_gx_aspect( /// 1607.** 1444 shipped it on; 1559 turned it off on the director's look-call (a full-rate doll /// reads as smoother); the 08-25 weak-GPU perf reports (B329) then measured what that costs — a /// body-pane booth's off-screen pass every frame, ~1.6 ms at 1600×900 and **7.6 ms at 4K** -/// (`sess/perfregress` A/B), paid on exactly the weak GPUs that reported. The director retested +/// (an A/B leg), paid on exactly the weak GPUs that reported. The director retested /// the 30 fps doll and it reads fine, so the cheaper default is back. Full-rate is one /// `/script SetCVar("boothHalfRate", 0)` away for anyone who wants the smoother cadence. /// diff --git a/crates/benilla-app/src/preflight.rs b/crates/benilla-app/src/preflight.rs index ca6cf3f6..cd64a307 100644 --- a/crates/benilla-app/src/preflight.rs +++ b/crates/benilla-app/src/preflight.rs @@ -48,8 +48,8 @@ //! establish — which is precisely the player build. The banner now registers beside the stamp //! itself, in `lib::run`, where it is always compiled. //! -//! The other half of decision 0649 — the pre-connect **account guard**, which keeps a session -//! running inside a worktree pool slot from logging in as somebody else's account — lives in +//! The other half of decision 0649 — the pre-connect **account guard**, which keeps a scripted +//! run from logging in as an account its checkout did not declare — lives in //! [`crate::run_mode`] now, not here: it is consulted by the login policy, and 1174's seam does not //! let gameplay call an instrument. Its reasoning went with it. @@ -435,7 +435,7 @@ fn findings( ShieldReport::Arming | ShieldReport::Armed => "This is the default. Re-run with WOW_GM=off for those readings — safe, \ because the probe shield (decision 0677) keeps the body alive without it.", - // Not a probe body — the director's own account, or a bystander. `WOW_GM` would be + // Not a probe body — a player's account, or a plain test account. `WOW_GM` would be // inert here (the shield only ever commands `probe`, 0677), and saying otherwise // sends the reader after a switch that does nothing. The state is also PERSISTED: // vmangos saves it in `characters.extra_flags` bit 0 and `GM.LoginState = 2` diff --git a/crates/benilla-app/src/probe_shield.rs b/crates/benilla-app/src/probe_shield.rs index b0de6a8b..a98b267c 100644 --- a/crates/benilla-app/src/probe_shield.rs +++ b/crates/benilla-app/src/probe_shield.rs @@ -159,9 +159,9 @@ impl ProbeShield { } } -/// Whether `user` is a probe account — `probe` followed by digits, the slot-keyed identity every -/// unattended session logs in with (docs/METHOD.md "The local vmangos server"). Nothing else is ours to -/// modify: `one` is the director's, and a bystander test account is not a probe. +/// Whether `user` is a probe account — `probe` followed by digits, the identity every unattended +/// run logs in with (the `probe` skill). Nothing else is ours to +/// modify: a player's account is theirs, and a plain test account is not a probe. fn is_probe_account(user: &str) -> bool { // Byte-wise (a &str slice at 5 would panic mid-char on non-ASCII input; bytes can't). let Some((prefix, digits)) = user.as_bytes().split_at_checked(5) else { @@ -405,7 +405,7 @@ mod tests { for user in ["probe0", "probe7", "PROBE12"] { assert!(is_probe_account(user), "{user} is a probe account"); } - // The director's account, and bystanders: never touched. + // A player's account, and plain test accounts: never touched. for user in ["one", "two", "probe", "probeone", "aprobe1", "probe1x"] { assert!(!is_probe_account(user), "{user} is NOT a probe account"); } diff --git a/crates/benilla-app/src/run_mode.rs b/crates/benilla-app/src/run_mode.rs index 44fae46f..53790811 100644 --- a/crates/benilla-app/src/run_mode.rs +++ b/crates/benilla-app/src/run_mode.rs @@ -69,21 +69,19 @@ pub(crate) fn capture_ui_opted_in() -> bool { false } -/// **Do the credentials come from the environment?** (`$WOW_USER` / `$WOW_PASS` / `$WOW_CHAR` — -/// the login screen's env fast path.) +/// **Do the credentials come from the environment?** (`$WOW_USER` and `$WOW_PASS`, both — the +/// login screen's env fast path.) /// -/// The player answer is `false`, and it is the default: with none of these set the client opens at -/// the login screen and waits for somebody to type. Setting any of them says *"log in without -/// waiting for me to type"* — and **that is the only thing it says.** It is not a claim about who -/// is in the room; [`unattended`] is the fact for that, and keeping the two apart is decision 1769. -/// -/// One environment fact, several readers, no shared symbol — the module doc's pattern, same as -/// [`scenario_active`], and the reason the roster's `WOW_CHAR` fast path asks here instead of -/// re-reading the three names. +/// The player answer is `false`, and it is the default: with neither set the client opens at the +/// login screen and waits for somebody to type. Setting both says *"log in without waiting for me +/// to type"* — and **that is the only thing it says.** It is not a claim about who is in the room; +/// [`unattended`] is the fact for that, and keeping the two apart is decision 1769. There is no +/// default account: one credential without the other is a typed login, and `$WOW_CHAR` alone is +/// the roster's fast path after one (`char_select`), not a login. pub(crate) fn env_login() -> bool { - ["WOW_USER", "WOW_PASS", "WOW_CHAR"] + ["WOW_USER", "WOW_PASS"] .iter() - .any(|k| std::env::var_os(k).is_some()) + .all(|k| std::env::var_os(k).is_some()) } /// **Is there nobody here?** (`$WOW_UNATTENDED`, or the two runs that are automated by @@ -107,8 +105,8 @@ pub(crate) fn env_login() -> bool { /// /// `WOW_CAPTURE` and `WOW_RIG` are folded in because they *cannot* be a person: a capture authors /// the camera and a rig drives the body, so a run that sets either has already said what it is and -/// cannot forget to. Everything else declares — `scripts/leg.sh`, `smoke.sh`, `cine.sh`, -/// `summon-live.sh` and the ad-hoc probe recipe in `docs/METHOD.md` all pass `WOW_UNATTENDED=1`. +/// cannot forget to. Everything else declares — `scripts/smoke.sh`, `cine.sh`, `summon-live.sh` +/// and the ad-hoc probe recipe in the `probe` skill all pass `WOW_UNATTENDED=1`. /// /// Read by whatever may act *instead of* a person: the lost-session verdict (decision 1262 — the /// session-loss readers never call this directly, [`crate::net::DisconnectedMessage::new`] asks @@ -124,14 +122,14 @@ pub(crate) fn unattended() -> bool { /// (decision 1371's `FATAL` marker, resting on 1769's fact.) /// /// `true` only for a run that has declared itself [`unattended`]: it exits non-zero on the one -/// greppable marker `scripts/leg.sh` keys on, rather than parking a driverless run on a dialog for +/// greppable marker a leg runner keys on, rather than parking a driverless run on a dialog for /// its whole wall-clock. Otherwise the dialog stays up for whoever is there. /// /// The `false` arm still speaks, when the credentials came from the environment: a probe that /// quietly parks for 300 s instead of failing in 5 is precisely what 1769's default trades away, /// so the log names the declaration that buys the old behaviour back. Both arms live here because /// three call sites — two in [`crate::login`], one in [`crate::char_select`] — must not drift -/// apart on either the verdict or the marker `leg.sh` greps for. +/// apart on either the verdict or the marker a leg runner greps for. pub(crate) fn fatal_when_driverless(why: &str) -> bool { if unattended() { error!("login: FATAL — {why}; exiting"); @@ -248,108 +246,101 @@ pub(crate) fn dev_source_dir() -> Option<&'static std::path::Path> { } /// The pre-connect **account guard**, consulted by the login policy's env fast path -/// ([`crate::login`]). Returns `Err(explanation)` when this build lives in a worktree pool slot and -/// the fast path is about to authenticate as an account that belongs to somebody else — the -/// director's `one` (a login KICKS their live session mid-play) or another slot's `probeN` (the -/// kicked client's 0065 teardown despawns every net entity, so a parallel session's probe reads a -/// unit-less world and prints garbage; it happened, docs/METHOD.md records it). +/// ([`crate::login`]). Returns `Err(explanation)` when this checkout declares the account its +/// scripted runs log in as ([`declared_identity`]) and the fast path is about to authenticate as +/// anything else: a login KICKS whoever holds the account — a player mid-session, or another +/// checkout's probe, whose kicked client (the 0065 teardown) despawns every net entity, so that +/// run's next sample reads a unit-less world and prints garbage. /// -/// Slot identity comes from the compiled-in manifest path, because that is what the pool guarantees -/// is unique per session: every slot has its own checkout and its own `target/`. Outside a pool slot -/// (the primary checkout, which is the director's) the guard is inert — it has no business having an -/// opinion about a login it cannot attribute. That is why this lives here and not in `preflight` +/// The declaration is a file, never the build directory: a path is nobody's identity. Without +/// one (a plain clone, a player build) the guard is inert — it has no business having an opinion +/// about a login it cannot attribute. That is why this lives here and not in `preflight` /// (decision 0649's home for it, until 1174 made `preflight` dev-only): gameplay's login policy /// calls it, and gameplay may not call an instrument. /// /// `WOW_ALLOW_ACCOUNT=1` is the escape hatch for the rare deliberate cross-account run; it turns the /// refusal into a warning rather than silence, because the kick still happens. pub(crate) fn account_guard(user: &str) -> Result<(), String> { - guard_for(compiled_slot(), user) + guard_for(declared_identity().as_ref(), user) } -/// The pool slot this binary was *compiled* in, or `None`. -/// -/// A player build is never in a pool slot, so 1174 read `env!("CARGO_MANIFEST_DIR")` -/// unconditionally on the grounds that the ladder collapses to `Ok(())` anyway. It does — but the -/// path is still a **string in the shipped binary**, naming the build machine's home directory, -/// which is the one thing decision 1175 exists to end (its falsifier is literally -/// `strings | grep -c '/Users/…'`). It costs one `cfg` to not ship it, and it is -/// what makes that falsifier readable: after this, the only source-tree paths left in a player -/// binary are debuginfo and panic metadata, never data the program acts on. +/// What `.probe-identity` at the project root declares: the account a scripted run from this +/// checkout logs in as (`WOW_USER=`, `WOW_PASS=`, `WOW_CHAR=`, one per line; never committed — +/// `.gitignore` carries it). A machine that runs several checkouts against one server gives each +/// its own account this way, and `scripts/probe-identity.sh` reads the same file. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct DeclaredIdentity { + pub(crate) user: String, + pub(crate) character: String, +} + +/// The declaration, read off the project folder — `dev` only, the install resolver's own +/// project-folder rung: a player build has no source tree to name (decision 1175), and no +/// declared identity to keep. #[cfg(feature = "dev")] -fn compiled_slot() -> Option { - pool_slot(env!("CARGO_MANIFEST_DIR")) +pub(crate) fn declared_identity() -> Option { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2)?; + parse_identity(&std::fs::read_to_string(root.join(".probe-identity")).ok()?) } #[cfg(not(feature = "dev"))] -fn compiled_slot() -> Option { +pub(crate) fn declared_identity() -> Option { None } -/// [`account_guard`]'s decision, with the slot passed in so the ladder is testable from any -/// checkout (the real one reads a compile-time path that differs per worktree). -fn guard_for(slot: Option, user: &str) -> Result<(), String> { - let Some(slot) = slot else { +/// The file's three lines, as an identity — `None` unless the account and the character are +/// both there (the password is the scripts' business, never this crate's). +#[cfg_attr(not(feature = "dev"), allow(dead_code))] +fn parse_identity(text: &str) -> Option { + let field = |key: &str| { + text.lines() + .filter_map(|l| l.trim().strip_prefix(key)?.strip_prefix('=')) + .map(|v| v.trim().trim_matches('"').to_string()) + .find(|v| !v.is_empty()) + }; + Some(DeclaredIdentity { + user: field("WOW_USER")?, + character: field("WOW_CHAR")?, + }) +} + +/// [`account_guard`]'s decision, with the declaration passed in so it is testable without a file. +fn guard_for(declared: Option<&DeclaredIdentity>, user: &str) -> Result<(), String> { + let Some(id) = declared else { return Ok(()); }; - let mine = format!("probe{slot}"); - let user_lc = user.to_ascii_lowercase(); - if user_lc == mine { + if user.eq_ignore_ascii_case(&id.user) { return Ok(()); } - let whose = if user_lc == "one" { - "the DIRECTOR's account — logging in on it kicks them out of their live session mid-play" - } else if user_lc.starts_with("probe") && user_lc[5..].chars().all(|c| c.is_ascii_digit()) { - "ANOTHER worktree slot's probe account — logging in on it kicks that session's probe out \ - of the world, and its next sample reads a unit-less world" - } else { - return Ok(()); // a bystander account (`two`, a fresh test account): not ours to police - }; Err(format!( - "the env fast path is about to log in as `{user}` from pool-{slot}, and that is {whose}. \ - This slot's identity is WOW_USER=probe{slot} WOW_PASS=pprobe{slot} \ - WOW_CHAR=Probe{spelled} (docs/METHOD.md \"The local vmangos server\").", - spelled = spell_digit(slot) + "the env fast path is about to log in as `{user}`, and this checkout's .probe-identity \ + declares `{}` (WOW_CHAR={}). A login kicks whoever holds `{user}` — a player mid-session, \ + or another checkout's probe.", + id.user, id.character )) } -/// This slot's index as the word the probe identity spells it with (`pool-4` → `"four"`), or `None` -/// outside a pool slot. The one place anything else should ask "which session am I?" — the rig keys -/// its derived character names off it, and the preflight banner names it. -pub(crate) fn slot_word() -> Option<&'static str> { - compiled_slot().map(spell_digit) +/// The word a rig appends to the bodies it mints for this checkout (`[f]`), +/// so that several checkouts against one server never collide on a character name: the declared +/// character, lowercased, without a leading `probe` — a probe fleet names its bodies +/// `Probe`, and the word is the identity. `None` without a declaration; the rig then +/// configures the body it logged in as. +pub(crate) fn rig_suffix() -> Option { + Some(suffix_of(&declared_identity()?.character)) } -/// The pool slot index in a `…/benilla-wt/pool-/…` manifest path; `None` for the primary -/// checkout and any non-pool worktree. Dev-only, because [`compiled_slot`] is its only caller and -/// a player build must not name a manifest path at all (1175). -#[cfg(any(feature = "dev", test))] -fn pool_slot(manifest_dir: &str) -> Option { - let mut parts = manifest_dir.split('/'); - while let Some(part) = parts.next() { - if part == "benilla-wt" { - return parts.next()?.strip_prefix("pool-")?.parse().ok(); - } - } - None -} - -/// The probe character's name suffix for slot `n` — vmangos names carry no digits, so the pool -/// index is spelled (`pool-4` → `Probefour`). -fn spell_digit(n: u32) -> &'static str { - match n { - 0 => "zero", - 1 => "one", - 2 => "two", - 3 => "three", - 4 => "four", - 5 => "five", - 6 => "six", - 7 => "seven", - 8 => "eight", - 9 => "nine", - _ => "", - } +fn suffix_of(character: &str) -> String { + let name = character.to_ascii_lowercase(); + let word = name + .strip_prefix("probe") + .filter(|w| !w.is_empty()) + .unwrap_or(&name); + word.chars() + .filter(char::is_ascii_alphabetic) + .take(6) + .collect() } #[cfg(test)] @@ -479,44 +470,45 @@ mod tests { } #[test] - fn the_guard_only_polices_accounts_that_belong_to_someone() { - // Our own slot's probe: the whole point of the identity. - assert!(guard_for(Some(4), "probe4").is_ok()); - assert!(guard_for(Some(4), "PROBE4").is_ok()); // vmangos accounts are case-insensitive - // The director's account, and a neighbouring slot's probe: both kick a live session. - let director = guard_for(Some(4), "one").unwrap_err(); - assert!(director.contains("DIRECTOR") && director.contains("WOW_USER=probe4")); + fn the_fast_path_is_the_declared_account_or_refused() { + let id = parse_identity("WOW_USER=probe4\nWOW_PASS=pprobe4\nWOW_CHAR=Probefour\n").unwrap(); + // Our own account: the whole point of the declaration. + assert!(guard_for(Some(&id), "probe4").is_ok()); + assert!(guard_for(Some(&id), "PROBE4").is_ok()); // vmangos accounts are case-insensitive + + // Anyone else's account — a player's, another checkout's probe — kicks a live session. + let player = guard_for(Some(&id), "one").unwrap_err(); + assert!(player.contains("probe4") && player.contains("kicks")); // The override hint belongs to the caller that can act on it, not to the reason. - assert!(!director.contains("WOW_ALLOW_ACCOUNT")); - assert!(guard_for(Some(4), "probe7") - .unwrap_err() - .contains("ANOTHER worktree slot")); - // A bystander account is nobody's to police, and outside a pool slot we have no standing. - assert!(guard_for(Some(4), "two").is_ok()); + assert!(!player.contains("WOW_ALLOW_ACCOUNT")); + assert!(guard_for(Some(&id), "probe7").is_err()); + // No declaration, no standing. assert!(guard_for(None, "one").is_ok()); } #[test] - fn the_probe_character_name_spells_the_slot() { - // vmangos player names carry no digits, so `Probe4` cannot exist — the pool index is spelled. - assert!(guard_for(Some(0), "one") - .unwrap_err() - .contains("WOW_CHAR=Probezero")); - assert!(guard_for(Some(9), "one") - .unwrap_err() - .contains("WOW_CHAR=Probenine")); + fn the_declaration_needs_the_account_and_the_character() { + assert_eq!( + parse_identity("WOW_USER=probe4\nWOW_PASS=x\nWOW_CHAR=\"Probefour\"\n"), + Some(DeclaredIdentity { + user: "probe4".into(), + character: "Probefour".into() + }) + ); + assert!(parse_identity("WOW_USER=probe4\nWOW_PASS=x\n").is_none()); + assert!(parse_identity("WOW_USER=\nWOW_CHAR=Probefour\n").is_none()); + assert!(parse_identity("").is_none()); } #[test] - fn the_slot_is_read_off_the_manifest_path() { - assert_eq!( - pool_slot("/Users/sam/dev/benilla-wt/pool-7/crates/benilla"), - Some(7) - ); - assert_eq!(pool_slot("/Users/sam/dev/benilla-wow/crates/benilla"), None); - assert_eq!( - pool_slot("/Users/sam/dev/benilla-wow/.claude/worktrees/x/crates/benilla"), - None - ); + fn the_rig_word_is_the_declared_character_without_its_probe_prefix() { + // vmangos player names carry no digits, so a probe fleet spells its index: the word is + // what follows `Probe`, and the rig's `Taudru` keeps one body per checkout. + assert_eq!(suffix_of("Probefour"), "four"); + assert_eq!(suffix_of("Probezero"), "zero"); + // Any other name is its own word, cut to what a 12-character name has room for. + assert_eq!(suffix_of("Tester"), "tester"); + assert_eq!(suffix_of("Longcharname"), "longch"); + assert_eq!(suffix_of("Probe"), "probe"); } } diff --git a/crates/benilla-app/src/sound/output/mod.rs b/crates/benilla-app/src/sound/output/mod.rs index db7be143..90c4f302 100644 --- a/crates/benilla-app/src/sound/output/mod.rs +++ b/crates/benilla-app/src/sound/output/mod.rs @@ -801,7 +801,7 @@ mod tests { /// and nothing above it is, so a failure names the platform file and nothing else. /// /// It is the whole verification story off macOS, where the gates never compile this code at - /// all: `scripts/crosscheck.sh` type-checks Linux and Windows, and this runs the result. + /// all: a cross-platform compile type-checks Linux and Windows, and this runs the result. /// /// WOW_AUDIO_LIVE=1 cargo test -p benilla-app --lib sound::output:: -- --nocapture /// diff --git a/crates/benilla-app/src/sound/ui.rs b/crates/benilla-app/src/sound/ui.rs index 36f4fab9..998371bd 100644 --- a/crates/benilla-app/src/sound/ui.rs +++ b/crates/benilla-app/src/sound/ui.rs @@ -1,5 +1,5 @@ //! UI sounds — the app side of the Lua `PlaySound` seam (decision 0070 §4, the one deliberate -//! UI-crate overlap cut from the sound worktree), plus the **per-item item-gesture sounds** +//! UI-crate overlap cut when the sound crate was split), plus the **per-item item-gesture sounds** //! (decision 0091): the bag-drag pickup/put-down, and the loot-window pickup. //! //! Three triggers, all 2D SFX plays, all after the UI input pass so a click's sound plays the diff --git a/crates/benilla-app/src/ui_gm_ticket.rs b/crates/benilla-app/src/ui_gm_ticket.rs index 74eadd1c..2c69de84 100644 --- a/crates/benilla-app/src/ui_gm_ticket.rs +++ b/crates/benilla-app/src/ui_gm_ticket.rs @@ -34,7 +34,7 @@ //! //! ## What vmangos actually does, and what that costs us //! -//! Verified against `/Users/sam/wre/vmangos-src` (`Handlers/GMTicketHandler.cpp`, `GMTicketMgr.cpp`, +//! Verified against the vmangos source (`Handlers/GMTicketHandler.cpp`, `GMTicketMgr.cpp`, //! `Commands/TicketCommands.cpp`) — four behaviours a client author has to design around: //! //! 1. **Silence is a legal answer.** Create returns with no packet at all when the queue is off, diff --git a/crates/benilla-app/src/ui_script/content.rs b/crates/benilla-app/src/ui_script/content.rs index 9dc83d6d..1c969d10 100644 --- a/crates/benilla-app/src/ui_script/content.rs +++ b/crates/benilla-app/src/ui_script/content.rs @@ -71,7 +71,7 @@ fn read_source_tree(req: &str) -> Option { /// /// It exists because a number taken from the corpus harness is meaningless without it. In a dev /// build [`read`] prefers the SOURCE TREE, so editing an `assets/ui` file changes what a survey -/// loads **with no rebuild** — and this repo's worktrees are routinely shared by several agents at +/// loads **with no rebuild** — and a tree is routinely edited by several agents at /// once. Three separate measurements this arc were taken across a moving tree and one of them /// landed a wrong attribution in a decision record: 87/218 was credited to a single table when a /// controlled A/B later put it at 75, with the other twelve belonging to a neighbour's uncommitted diff --git a/crates/benilla-app/src/ui_script/frame_flag_gate.rs b/crates/benilla-app/src/ui_script/frame_flag_gate.rs index 17f64443..61da5842 100644 --- a/crates/benilla-app/src/ui_script/frame_flag_gate.rs +++ b/crates/benilla-app/src/ui_script/frame_flag_gate.rs @@ -1,7 +1,7 @@ //! **The reference's frame FLAGS are the test oracle** — the flag half of decision 0675's "the //! reference file is the test oracle", and the gate for the class decision 1739 closed. //! -//! `framexml_diff` guards the *numbers* a transcribed window carries. Nothing guarded its *flags*, +//! A window's numbers are the stock file's own now. Nothing guarded its *flags*, //! and they went quietly wrong at scale: on 2026-08-30 a sweep found **47 frames** the reference //! marks `toplevel` and ours did not (so no window ever came to the front), **23** the reference //! makes mouse-interactive and ours did not (so a click on a window's own background fell through @@ -71,11 +71,10 @@ //! can hide a divergence, never invent one — so nothing it misses turns into a false failure. It //! would be a `KNOWN` entry's reason if any frame still needed one. //! -//! The whole module skips cleanly with no install — `_extracted_framexml/` is a gitignored -//! Blizzard asset, like every other client-data test here. +//! The whole module skips cleanly with no install — the reference corpus is read off the +//! player's own patch chain, like every other client-data test here. -use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::collections::{HashMap, HashSet}; use benilla_ui::framexml::{self, Element, TopLevel}; @@ -232,34 +231,54 @@ const KNOWN: &[Known] = &[ // window is the reference's own now, scroll children and all. ]; -/// The extracted reference FrameXML directory, or `None` when the install isn't there. -fn reference_dir() -> Option { - let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../WoW/_extracted_framexml"); - dir.is_dir().then_some(dir) +/// Every reference FrameXML document, read off the player's own patch chain in the client's +/// load order: the files `FrameXML.toc` lists, and what each ``s, each once. `None` +/// without an install. Off the chain, never an extracted folder, so the gate is armed on any +/// machine with the install rather than on the one that happened to extract it. +fn reference_documents() -> Option> { + let data = benilla_formats::wow_data()?; + let chain = benilla_formats::open_chain(&data).ok()?; + let toc = chain.read("Interface\\FrameXML\\FrameXML.toc").ok()?; + let toc = benilla_ui::toc::Toc::parse(&benilla_ui::source::decode(&toc)); + let mut queue: Vec = toc + .files + .iter() + .filter(|f| f.to_ascii_lowercase().ends_with(".xml")) + .map(|f| format!("Interface\\FrameXML\\{f}")) + .collect(); + let mut seen = HashSet::new(); + let mut docs = Vec::new(); + while !queue.is_empty() { + let path = queue.remove(0); + if !seen.insert(path.to_ascii_lowercase()) { + continue; + } + let Ok(bytes) = chain.read(&path) else { + continue; + }; + // Blizzard ships a UTF-8 BOM on some of these and stray high bytes in comments; the parse + // is what matters, so read lossily rather than refusing the file. + let text = String::from_utf8_lossy(&bytes); + let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else { + continue; + }; + let dir = path.rsplit_once('\\').map(|(d, _)| d).unwrap_or(""); + for item in &doc.items { + if let TopLevel::Include(file) = item { + queue.push(format!("{dir}\\{file}")); + } + } + docs.push(doc); + } + Some(docs) } /// Every named *frame* element in the reference corpus, keyed by name — templates and instances /// alike, nested `` included, `$parent`-relative names excluded (they repeat across /// templates and name nothing on their own). fn reference_frames() -> Option> { - let dir = reference_dir()?; - let mut paths: Vec = std::fs::read_dir(&dir) - .ok()? - .flatten() - .map(|e| e.path()) - .filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("xml"))) - .collect(); - paths.sort(); - let mut out: HashMap = HashMap::new(); - for path in paths { - // Blizzard ships a UTF-8 BOM on some of these and stray high bytes in comments; the parse - // is what matters, so read lossily rather than refusing the file. - let bytes = std::fs::read(&path).ok()?; - let text = String::from_utf8_lossy(&bytes); - let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else { - continue; - }; + for doc in reference_documents()? { for item in &doc.items { if let TopLevel::Template(el) | TopLevel::Instance(el) = item { collect_named(el, &mut out); @@ -315,24 +334,8 @@ fn collect_nesting( /// The reference's nesting table, built over the same corpus [`reference_frames`] reads. fn reference_nesting() -> Option>> { - let dir = reference_dir()?; - let mut paths: Vec = std::fs::read_dir(&dir) - .ok()? - .flatten() - .map(|e| e.path()) - .filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("xml"))) - .collect(); - paths.sort(); - let mut out: HashMap> = HashMap::new(); - for path in paths { - let Ok(bytes) = std::fs::read(&path) else { - continue; - }; - let text = String::from_utf8_lossy(&bytes); - let Ok(doc) = framexml::parse(text.trim_start_matches('\u{feff}')) else { - continue; - }; + for doc in reference_documents()? { for item in &doc.items { if let TopLevel::Template(el) | TopLevel::Instance(el) = item { collect_nesting(el, None, &mut out); @@ -436,14 +439,8 @@ fn describe(frame: &str, flag: Flag, ours: &str, theirs: &str) -> String { /// `parent="UIParent"` back on `BlackoutWorld` and it names that. #[test] fn the_shipped_frames_carry_the_references_flags() { - // Two gates, because the corpus and the manifest are two assets: the extracted reference - // dir below, and the chain `load_default_ui` reads — under `WOW_DATA=` (1451) the first is - // still there and the second is not. let _data = benilla_formats::wow_data_or_skip!(); - let Some(reference) = reference_frames() else { - eprintln!("skipping: no extracted reference FrameXML (WoW/_extracted_framexml)"); - return; - }; + let reference = reference_frames().expect("the reference FrameXML off the player's chain"); let nesting = reference_nesting().expect("the same corpus the frames came from"); assert!( reference.len() > 500, diff --git a/crates/benilla-app/src/ui_script/framexml_diff.rs b/crates/benilla-app/src/ui_script/framexml_diff.rs deleted file mode 100644 index 00f957a4..00000000 --- a/crates/benilla-app/src/ui_script/framexml_diff.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! **The reference file is the test oracle** (decision 0675) — the shared half of it. -//! -//! A transcribed window's geometry errors are invisible to every behavioural test: the frames -//! load, the buttons click, the lists populate, and only the *look* is wrong. 0675 built the -//! detector for `FriendsFrame` after two rounds of the director's eye caught what a one-number -//! diff would have caught on its first run, and closed with "for the next transcribed window, -//! write the diff test first". This is that scraper, lifted out of `friends_tests` so the next -//! window costs a call rather than a copy (`QuestTimerFrame` is the first to pay the lower price). -//! -//! Deliberately narrow: it does not try to understand the XML, only to notice that a number moved. -//! Two properties make it a guard rather than a comfort, and both are the caller's to keep — -//! **known-benign differences are an explicit list, never a pattern**, so a new difference cannot -//! hide inside a tolerance; and **it must be verified to fail** (perturb one number by 1 px and -//! watch it name the element). - -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -/// The extracted reference FrameXML file of that name, or `None` when the install isn't there -/// (these tests skip cleanly without it — it is a gitignored Blizzard asset). -pub(super) fn reference(file: &str) -> Option { - let path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../WoW/_extracted_framexml") - .join(file); - path.is_file().then_some(path) -} - -/// Named element → the `` pairs inside it, up to the next named element. -/// -/// Hand-rolled rather than a regex: this crate has no regex dependency, and adding one for a scan -/// of two files is a worse trade than twenty lines of `find`. -/// -/// `$parent*` names repeat across templates (every row template has a `$parentText`), so a bare -/// name would compare one template's column against another's. Each is qualified by the template -/// it lives in — the nearest preceding real name. -fn scrape(text: &str) -> Vec<(String, Vec<(f32, f32)>)> { - const TAGS: &[&str] = &[ - "