a fresh clone gets the whole project, and the maintainer's tooling moves out

What a clone lacked: the gates went green with a thousand skipped tests and said nothing, every
login script assumed a worktree-pool slot and its probe account, the client defaulted to the
maintainer's account, a docker path and a home directory were in the code, the temp files broke
on GNU coreutils, and two generators could only run against a sibling repo.

Now the test gates print how many data-gated tests skipped and why; a scripted run logs in as
the account its checkout declares in `.probe-identity` or as the three variables name, never a
default, and the client refuses a scripted login on any other account; the event and message
tables regenerate from vendored inputs, byte for byte; the docs state the real prerequisites.

Out of the tree, into the maintainer's own tooling: the worktree pool, its hooks, the perf leg
runner, the cross-platform compile and the Vulkan container leg — a way of working, and setups
not every machine has. One hook stays: no writes into the install.

Two tests that could never run are gone with their subject: the quest-timer geometry diff
compared a copy of the window that no longer exists (and joined a chain path onto a folder, so
it always skipped), and the FrameXML flag gate now reads the stock files off the player's own
patch chain instead of an extracted folder only one machine had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-23 17:27:14 +07:00
co-authored by Claude Fable 5.1
parent 64dc60f52f
commit aff5203393
106 changed files with 2145 additions and 3254 deletions
-81
View File
@@ -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-<id>` 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 <<EOF
BLOCKED — $door is not how this repo does worktrees${target:+ (asked for: $target)}.
\`scripts/wt.sh\` is the only worktree mechanism here (docs/METHOD.md, "Parallel sessions — one
worktree each"). The harness tool builds worktrees under \`.claude/worktrees/\` — ownerless, with
no sweeper and a cold ~4.5-min dependency build each; ~60 GB of those strays were the 2026-07-17
disk incident (decisions 0192/0433). Pointing it at a pool slot instead is no better: a slot must
never be removed, its warm target/ is the whole point.
If you have NOT claimed a slot yet:
./scripts/wt.sh claim <short-task-name> # 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
-105
View File
@@ -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 - <<PY` heredoc,
# `tee`) is not covered by a single line of it. On 2026-08-25 a session did exactly that: 434 lines
# across ten files, committed to main from the primary, while three neighbours were landing into the
# same checkout. Nothing fired. docs/METHOD.md said, and had said for months, that such a write is
# hook-blocked. It was not.
#
# **Why dirtiness and not the command.** The obvious fix is to scan the command for write verbs and
# resolve their arguments. That is shell parsing, and it is a losing game: the write that started
# this was a heredoc whose target path was computed inside Python, which no pattern can see. So this
# asks a question with no parsing in it at all — *is the primary dirty, and is this session the one
# with nowhere else to be?* It cannot be evaded, because it does not look at what you typed.
#
# The cost is that it fires on the command AFTER the first stray write rather than on the write
# itself. Measured against the incident it exists for, that is one command, not 434 lines: the
# session ran ~40 Bash calls over that work.
#
# **A dirty primary is never normal** (docs/METHOD.md hard rules; docs/METHOD.md "Parallel sessions"), which
# is what makes this a cheap question to ask. The primary belongs to no session and stays clean; a
# session that has claimed a slot is not answerable for dirt it did not make, so it passes.
#
# `WT_ALLOW_PRIMARY=1` overrides — in the hook's environment, or as an inline prefix on the command
# itself, because for a Bash tool call that is where a deliberate exception is actually typed.
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/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 <<EOF
BLOCKED — the PRIMARY checkout is dirty and this session has not claimed a slot.
primary: $primary
$(printf '%s\n' "$dirt" | sed 's/^/ /')
The primary belongs to no session and stays clean (docs/METHOD.md hard rules; docs/METHOD.md "Parallel
sessions — one worktree each"). Working here reverts other sessions' edits, jams the
whole-workspace gates with a neighbour's in-flight code, and blocks their \`wt.sh land\`.
If those changes are YOURS — you have been editing the primary, most likely through Bash, which
\`guard-primary-checkout.sh\` cannot see:
./scripts/wt.sh claim <short-task-name> # 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 <command>\`.
EOF
exit 2
-126
View File
@@ -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 - <<PY` heredoc does too, and none of it passes through this file: on
# 2026-08-25 a session wrote 434 lines into the primary and committed them to main without one line
# of this hook running. This one still catches the Edit/Write path at the FIRST write, which is the
# better moment; the Bash guard is the backstop that cannot be evaded by choice of tool.
#
# Scoped by the shared git common-dir, like `on-stop.sh`: a sibling repo
# (wow-5875-re) has its own, so a dispatched RE agent editing THERE is never touched by benilla's
# rule. Paths outside any git repo — the scratchpad, `~/.benilla` — are likewise none of our business.
input=$(cat)
# Edit/Write carry `file_path`; NotebookEdit carries `notebook_path`.
path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty' 2>/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 <<EOF
BLOCKED — that write targets the PRIMARY checkout ($primary/$rel).
The primary belongs to no session and stays clean (docs/METHOD.md hard rules; docs/METHOD.md "Parallel
sessions — one worktree each"). Editing it silently reverts other sessions' work, jams the
whole-workspace gates with a neighbour's in-flight code, and blocks their \`wt.sh land\`.
Claim a slot and work there instead:
./scripts/wt.sh claim <short-task-name> # 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 <<EOF
BLOCKED — that write targets ANOTHER SESSION's worktree ($top).
that slot is claim '$holder', held by session $owner
you are session $mine
Two sessions in one slot revert each other's edits, redden each other's gates, and split each
other's commits at land (decisions 1041, 1042). Claim your own slot:
./scripts/wt.sh claim <short-task-name> # 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
-139
View File
@@ -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 -- <path> -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 -- <path>` (the correct way to do the repro above),
# `git checkout --detach`/`-B <branch>` (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 <dir>`.
case "$s" in
git\ *) ;;
*) continue ;;
esac
# Drop `-C <dir>` 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 <file> \"\$SCRATCH/<file>.mine\"
git checkout HEAD -- <file> # path-scoped: touches nothing else, no shared state
<run the thing>
cp \"\$SCRATCH/<file>.mine\" <file>
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 -- <path>\`; 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 -- <path>\` (or \`git restore -- <path>\`)."
break
;;
esac
done <<EOF
$(printf '%s' "$cmd" | tr ';&|\n' '\n\n\n\n')
EOF
[ -n "$verdict" ] || exit 0
# Same-REPO check LAST — it costs a git call, and the overwhelming majority of commands never
# reach it. The cwd resolution: a tree-wide op is judged
# by the tree it lands in, and a `cd <slot> && …` 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 <<EOF
BLOCKED — tree-wide git op in a SHARED working tree.
$verdict
(docs/METHOD.md, "Who types what": parallel agents share one working tree, so whole-tree git ops are
forbidden — diagnostics use path-scoped commands only. A genuinely deliberate exception is
WT_ALLOW_TREE_OP=1; if you need a clean tree of your own, take a worktree instead.)
EOF
exit 2
+1 -1
View File
@@ -51,7 +51,7 @@ while [ ! -d "$dir" ] && [ "$dir" != "/" ] && [ "$dir" != "." ]; do dir=$(dirnam
real=$(cd -P "$dir" 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
-63
View File
@@ -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 <name>'" >&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
-41
View File
@@ -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
}
]
}
]
}
+11 -7
View File
@@ -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<N spelled>` 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
+6 -8
View File
@@ -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
+2 -2
View File
@@ -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.
+4 -2
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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<N-spelled>`, 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.
+3 -2
View File
@@ -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.
+1 -2
View File
@@ -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::*;
@@ -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<N-spelled>`; `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::*;
@@ -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
+5 -5
View File
@@ -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<N-spelled>`, 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;
@@ -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<N-spelled>`,
//! docs/METHOD.md) + `WOW_CAST_TRACE=1 WOW_PROBE_EXIT_AT=<s>`.
//! 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=<s>`.
use bevy::prelude::*;
@@ -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};
@@ -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.
@@ -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<N-spelled>`, `docs/METHOD.md`
//! "The local vmangos server"). `WOW_PROBE_CHEST=<x>,<y>,<z>[,<map>]` 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=<x>,<y>,<z>[,<map>]` 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,
+2 -2
View File
@@ -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<N-spelled>`, `docs/METHOD.md`
//! "The local vmangos server"). `WOW_PROBE_CLAM=<entry>` 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=<entry>` 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.
@@ -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<N-spelled>` 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::*;
@@ -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(
@@ -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<N-spelled>`, `docs/METHOD.md`
//! "The local vmangos server"). `WOW_PROBE_GOQUEST=<x>,<y>,<z>[,<map>]` aims it elsewhere; the
//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe`
//! skill). `WOW_PROBE_GOQUEST=<x>,<y>,<z>[,<map>]` 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
@@ -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<N-spelled>`; `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::*;
+3 -4
View File
@@ -5,7 +5,7 @@
//! `PROBE_MAIL: DONE pass=<n> fail=<m>` 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 <name> "subject" "text"` is
@@ -39,9 +39,8 @@
//! WOW_DATA=<vanilla Data dir> 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<N-spelled>`, 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.
@@ -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=<path>`) 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
@@ -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;
@@ -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::*;
+36 -35
View File
@@ -1,4 +1,4 @@
//! The probe **rig** (`WOW_RIG="<spec>"`, decision 0651) — one command that puts this slot's probe
//! The probe **rig** (`WOW_RIG="<spec>"`, 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<N>, 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: `<Race3><Class3><slot-word>[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: `<Race3><Class3><word>[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<N>` 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<N> 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<String> {
/// `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<f32>)> {
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<f32>)> {
fn rig_char_name(spec: &RigSpec) -> Option<String> {
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<String> {
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<N>`, 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() {
// `<Race3><Class3><slot-word>[f]`, normalized the way vmangos normalizes a player name.
// `<Race3><Class3><word>[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);
}
@@ -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 \
@@ -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<N-spelled>`, `docs/METHOD.md`
//! "The local vmangos server"). `WOW_PROBE_STONE=<x>,<y>,<z>[,<map>]` aims it elsewhere.
//! (the checkout's probe identity — `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR; the `probe`
//! skill). `WOW_PROBE_STONE=<x>,<y>,<z>[,<map>]` 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
+1 -2
View File
@@ -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<N-spelled>`; 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).
@@ -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::*;
@@ -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<n> WOW_NOSOUND=1 \
@@ -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<n> WOW_NOSOUND=1 \
@@ -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",
@@ -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<n> WOW_NOSOUND=1 \
@@ -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<n> WOW_NOSOUND=1 \
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -49,7 +49,7 @@ pub(crate) fn install(build: BuildId) {
// The standing test affordance — `WOW_CRASH_INJECT=<at_secs>`, 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,
@@ -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`
+9 -8
View File
@@ -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. **`<project folder>/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. **`<exe dir>/benilla-config/`** — the release answer: your settings sit next to the program that
@@ -80,11 +80,12 @@ pub(crate) fn home() -> Option<PathBuf> {
/// 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: <primary>/.git/worktrees/<slot>`, 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(),
+9 -9
View File
@@ -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}");
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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::*;
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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<std::path::PathBuf>) {
+1 -1
View File
@@ -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.
///
+3 -3
View File
@@ -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<N>`, 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`
+4 -4
View File
@@ -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");
}
+115 -123
View File
@@ -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 <player binary> | 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<u32> {
pool_slot(env!("CARGO_MANIFEST_DIR"))
pub(crate) fn declared_identity() -> Option<DeclaredIdentity> {
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<u32> {
pub(crate) fn declared_identity() -> Option<DeclaredIdentity> {
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<u32>, 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<DeclaredIdentity> {
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 (`<Race3><Class3><word>[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<word>`, 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<String> {
Some(suffix_of(&declared_identity()?.character))
}
/// The pool slot index in a `…/benilla-wt/pool-<N>/…` 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<u32> {
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",
_ => "<n>",
}
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<word>` 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");
}
}
+1 -1
View File
@@ -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
///
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -71,7 +71,7 @@ fn read_source_tree(req: &str) -> Option<String> {
///
/// 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
@@ -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<PathBuf> {
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 `<Include>`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<Vec<framexml::ParsedDocument>> {
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<String> = 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 `<Frames>` included, `$parent`-relative names excluded (they repeat across
/// templates and name nothing on their own).
fn reference_frames() -> Option<HashMap<String, Element>> {
let dir = reference_dir()?;
let mut paths: Vec<PathBuf> = 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<String, Element> = 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<HashMap<String, Option<String>>> {
let dir = reference_dir()?;
let mut paths: Vec<PathBuf> = 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<String, Option<String>> = 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,
@@ -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<PathBuf> {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../WoW/_extracted_framexml")
.join(file);
path.is_file().then_some(path)
}
/// Named element → the `<AbsDimension>` 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] = &[
"<Button name=\"",
"<Frame name=\"",
"<EditBox name=\"",
"<ScrollFrame name=\"",
"<FontString name=\"",
"<Texture name=\"",
"<CheckButton name=\"",
];
// Every (offset, name) where a named element starts, in document order.
let mut marks: Vec<(usize, String)> = Vec::new();
for tag in TAGS {
let mut from = 0;
while let Some(hit) = text[from..].find(tag) {
let at = from + hit;
let name_start = at + tag.len();
let Some(len) = text[name_start..].find('"') else {
break;
};
marks.push((at, text[name_start..name_start + len].to_string()));
from = name_start + len;
}
}
marks.sort_by_key(|(at, _)| *at);
let mut owner = String::new();
marks
.iter()
.enumerate()
.map(|(i, (at, name))| {
let end = marks.get(i + 1).map_or(text.len(), |(next, _)| *next);
let key = if let Some(child) = name.strip_prefix("$parent") {
format!("{owner}/{child}")
} else {
owner = name.clone();
name.clone()
};
(key, dimensions(&text[*at..end]))
})
.collect()
}
/// Every `<AbsDimension x=".." y=".."/>` in `chunk`, in order.
fn dimensions(chunk: &str) -> Vec<(f32, f32)> {
const OPEN: &str = "<AbsDimension x=\"";
let mut out = Vec::new();
let mut rest = chunk;
while let Some(hit) = rest.find(OPEN) {
rest = &rest[hit + OPEN.len()..];
let Some((x, tail)) = rest.split_once('"') else {
break;
};
let Some(y_at) = tail.find("y=\"") else { break };
let Some((y, tail)) = tail[y_at + 3..].split_once('"') else {
break;
};
if let (Ok(x), Ok(y)) = (x.parse(), y.parse()) {
out.push((x, y));
}
rest = tail;
}
out
}
/// Assert every element benilla's `ours` file shares with the reference `reference` file carries
/// the reference's `<AbsDimension>` numbers.
///
/// - `ours` is a path under `assets/ui/`; names are matched to the reference's after stripping the
/// house `Benilla` prefix (a window whose frames keep the bare reference names — decision 0591
/// §3 — matches unchanged).
/// - `expected` lists the *deliberate* deviations by their REFERENCE name. Every entry earns a
/// comment at the call site naming its reason; that is what keeps this a guard.
/// - `min_compared` fails the test if fewer elements paired up than that, so a scrape or a naming
/// change that quietly stops comparing anything is caught rather than reported as a pass.
///
/// Elements ours has and the reference does not (our own template faces) are skipped, not failed.
pub(super) fn assert_geometry_matches(
ours: &str,
reference: &Path,
expected: &[&str],
min_compared: usize,
) {
assert_geometry_matches_text(
ours,
&std::fs::read_to_string(reference).unwrap(),
expected,
min_compared,
);
}
/// [`assert_geometry_matches`] against reference TEXT rather than a file on disk.
///
/// The split exists because not every reference file is *on* disk: `_extracted_framexml/` holds
/// FrameXML only, and a window whose specification is partly a `Blizzard_*` **addon** — the raid
/// grid is the first, decision 1549 — has to read its half straight out of the patch chain
/// (`benilla_formats::open_chain`, the same door `mpqx` uses). Reading it rather than requiring
/// someone to have extracted it first is what keeps that half of the diff ARMED instead of
/// skipped, and it writes nothing into the install.
pub(super) fn assert_geometry_matches_text(
ours: &str,
reference_text: &str,
expected: &[&str],
min_compared: usize,
) {
let ours_text = std::fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets/ui")
.join(ours),
)
.unwrap();
let theirs: HashMap<String, Vec<(f32, f32)>> = scrape(reference_text).into_iter().collect();
let mut compared = 0;
let mut drifted = Vec::new();
for (name, dims) in scrape(&ours_text) {
let key = name.replace("Benilla", "");
let Some(ref_dims) = theirs.get(&key) else {
continue; // ours alone (the local template faces) — nothing to compare against
};
compared += 1;
if dims != *ref_dims && !expected.contains(&key.as_str()) {
drifted.push(format!("{key}: ours {dims:?} != ref {ref_dims:?}"));
}
}
assert!(
compared >= min_compared,
"only {compared} elements of {ours} matched by name (expected >= {min_compared}) — the \
scrape or the naming broke"
);
assert!(
drifted.is_empty(),
"{ours} geometry differs from the reference FrameXML:\n {}",
drifted.join("\n ")
);
}
-4
View File
@@ -1179,10 +1179,6 @@ mod duel_tests;
#[cfg(test)]
mod enchant_confirm_tests;
/// The shared reference-geometry diff (decision 0675) every transcribed window's test calls.
#[cfg(test)]
mod framexml_diff;
/// Its FLAG twin (decision 1739): the whole-tree sweep for `toplevel`/mouse/`id` against the
/// reference, read off the loaded engine rather than off our XML.
#[cfg(test)]
@@ -181,30 +181,3 @@ fn a_world_entry_paints_a_quest_already_running() {
// gap reads 89 through the `−1`.
assert_eq!(row_text(&s, 1), "1 Min 29 Secs ");
}
/// **The reference file is the test oracle** (decision 0675). Scrapes both this window and the
/// extracted reference `QuestTimerFrame.xml` for `<AbsDimension>` pairs per named element and
/// asserts every shared element's numbers match — the guard that catches a transcription whose
/// frames all load, click and populate while every number is wrong.
///
/// Verified to fail: perturbing `QuestTimer1`'s TOP offset from -30 to -29 reports
/// `QuestTimer1: ours [(140.0, 16.0), (0.0, -29.0)] != ref [(140.0, 16.0), (0.0, -30.0)]`.
#[test]
fn the_window_geometry_matches_the_reference_framexml() {
let _data = benilla_formats::wow_data_or_skip!();
let Some(reference) =
super::framexml_diff::reference("Interface\\FrameXML\\QuestTimerFrame.xml")
else {
eprintln!("skipping: no extracted FrameXML");
return;
};
// No exemptions: this window's frames keep the reference's bare names (decision 0591 §3 — the
// manage pass and the ref's own row lookup both resolve by literal name), and every number in
// it is the reference's. If an entry ever needs to go here, it names its reason.
super::framexml_diff::assert_geometry_matches(
"Interface\\FrameXML\\QuestTimerFrame.xml",
&reference,
&[],
22,
);
}
+2 -2
View File
@@ -314,8 +314,8 @@ fn the_reported_html_page_draws_as_blocks_not_as_its_own_markup() {
);
}
/// **B342, on the reported page, through the real archives.** Goudy, 2026-08-27 (`#bugs`
/// `1542371921486811236`): *"html images in books are not scaled correctly"* — the Alliance crest
/// **B342, on the reported page, through the real archives.** Reported on Discord,
/// 2026-08-27: *"html images in books are not scaled correctly"* — the Alliance crest
/// on *A Treatise on Military Ranks* drawn several times the reference's size with the page's own
/// text over it, beside a 1.12.1 shot of the same page for comparison.
///
@@ -74,7 +74,6 @@ fn is_instrument(rel: &str) -> bool {
"/bin/",
"debug_panel",
"shape_gate",
"framexml_diff",
"resolve_bench",
]
.iter()
+3 -5
View File
@@ -21,9 +21,7 @@ use std::process::Command;
/// Why it exists: people run benilla from a clone of the public snapshot repo, and a report ("this
/// looks wrong", "it crashed here") is only actionable if we know *which* code they ran. A git sha
/// is the only id that can't drift — the crate version is a permanent `0.1.0` and there are no
/// releases to number. For a public clone the stamp is the **snapshot** sha, which maps back to the
/// private commit exactly (`pubsync` tags it `pub/<public-short-sha>`); for our own worktrees it is
/// the private commit itself.
/// releases to number. The stamp is the commit the binary was built from, on any checkout.
///
/// It emits four `rustc-env` vars. The three git ones come back **empty** when git can't answer (a
/// source zip with no `.git`, or no `git` on PATH), which the runtime side reports as an unknown
@@ -45,8 +43,8 @@ use std::process::Command;
/// A stamp is worthless if it can report a commit the binary isn't. The rerun triggers below are
/// therefore exactly the files that change when HEAD moves: `HEAD` itself (checkout, rebase,
/// detach) and the ref it names (a commit rewrites `refs/heads/<branch>`, not `HEAD`). Both are
/// resolved with `git rev-parse --git-path`, which applies the worktree rules — in a `wt.sh` pool
/// slot `.git` is a *file*, `HEAD` lives in `<primary>/.git/worktrees/<slot>/`, and `refs/heads/*`
/// resolved with `git rev-parse --git-path`, which applies the worktree rules — in a linked
/// worktree `.git` is a *file*, `HEAD` lives in `<main>/.git/worktrees/<name>/`, and `refs/heads/*`
/// stay in the common dir. Only paths that exist are emitted: cargo reruns a build script whose
/// watched path is *missing* on every build, and every one of those reruns relinks the app crate.
///
+15 -4
View File
@@ -163,6 +163,18 @@ pub fn skipped(what: &str, looked_in: &[PathBuf]) {
what,
looked_in,
);
// `$BENILLA_SKIP_LOG`: libtest swallows the line above, so a gate hands in a file and counts
// the skips after the run — a clone without the data sees how hollow its green is.
if let Some(log) = std::env::var_os("BENILLA_SKIP_LOG").filter(|p| !p.is_empty()) {
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log)
{
let _ = writeln!(f, "{what}");
}
}
}
/// [`skipped`] with the switch passed in, so the refusal is testable without touching the
@@ -183,15 +195,14 @@ fn skipped_under(required: bool, what: &str, looked_in: &[PathBuf]) {
///
/// Two rungs, the install's own shape: **`$BENILLA_ADDON_CORPUS`**, then
/// **`<project folder>/wow-addons-vanilla`** — `dev` only, the same `CARGO_MANIFEST_DIR` hop as
/// the install's rung 2. That folder is a gitignored symlink `scripts/wt.sh` lays into every pool
/// slot and the primary (`link_wow`), exactly as it lays `WoW`; the corpus itself lives beside the
/// primary checkout.
/// the install's rung 2. That folder is a gitignored symlink beside `WoW`, laid by whoever set the
/// checkout up; the corpus itself lives outside the tree.
///
/// **Why one rung and a link, not a walk up the tree.** Five test files carried their own copy of
/// this resolver, and every copy looked for a *sibling* of the checkout — the manifest's ancestors
/// two to four, joined with the folder name. That found the corpus from the primary and from the
/// old pool root beside it, and nothing once the pool moved to the external drive (2026-08-30):
/// from `/Volumes/SanDisk/benilla-wt/pool-N` the three hops name three folders that do not exist.
/// from a worktree on another volume the three hops name three folders that do not exist.
/// Every land gates in a slot, so thirty tests — a ratchet among them — skipped at every land for
/// three weeks. A resolver that looks *outside* the checkout answers according to where the
/// checkout happens to sit; the install learned this in 1175, and this is the same rule for the
+1 -1
View File
@@ -2,7 +2,7 @@
//! **WoW coordinates throughout** (the Bevy transform is the consumer's job).
//!
//! This transcribes the structural skeleton of vmangos's own transport-path builder and mover:
//! - `TransportMgr::GenerateWaypoints` (`/Users/sam/wre/vmangos-src/src/game/Transports/
//! - `TransportMgr::GenerateWaypoints` (vmangos `src/game/Transports/
//! TransportMgr.cpp:105-334`) — the keyframe list, the map-change/teleport skip dance, the
//! per-leg Catmull-Rom splines, the `DistSinceStop`/`DistUntilStop` modular walks, the
//! four-regime `TimeTo` trapezoid, and the `ArriveTime`/`DepartureTime` accumulation.
+1 -1
View File
@@ -213,7 +213,7 @@ fn decodes_a_blp_icon_to_png() {
.read_file("Interface/Icons/Spell_Holy_ArcaneIntellect.blp")
.expect("read spell icon BLP");
// Keyed by pid, like every other temp path in the workspace: eight pool slots run
// Keyed by pid, like every other temp path in the workspace: several worktrees run
// `cargo test --workspace` concurrently, and a FIXED shared name means one run's
// cleanup deletes the file another is about to read. That is not hypothetical — the
// taxi twin below reddened a land gate exactly that way (decision 1918).
@@ -24,9 +24,8 @@
//! pprobeM] [host]`. With one account it runs the CHANNEL case alone (the server echoes a channel
//! line back to its own sender, so one session is a complete round trip). With two it also runs
//! the PARTY case, which needs a real group: the first account invites the second, the second
//! accepts, and the addon line goes out over `/p`. **Use `probe8`/`probe9` for the second
//! identity** — those two accounts sit above `WT_MAX_SLOTS`, so no live session's slot owns them
//! (decision 0530's slot-keyed rule).
//! accepts, and the addon line goes out over `/p`. **The second identity is a probe account no
//! checkout declares** — a login kicks whoever holds the account (decision 0530).
//!
//! Server prerequisites, both checked by the probe's own output rather than assumed:
//! `AddonChannel = 1` (off ⇒ the server silently drops every addon send) and
@@ -63,8 +62,7 @@ fn probe_char_name(user: &str) -> Option<String> {
}
fn login(host: &str, user: &str, pass: &str) -> Result<(WorldSession, String)> {
let name =
probe_char_name(user).context("account must be a slot-keyed probeN (decision 0530)")?;
let name = probe_char_name(user).context("account must be a probeN (decision 0530)")?;
let logon = benilla_protocol::logon(host, user, pass)?;
let addr = logon
.realms
@@ -9,9 +9,9 @@
//! second `SMSG_SPELL_GO`) timestamps the server's cooldown end relative to the first GO.
//!
//! Run: `cargo run -p benilla-protocol --example cooldown_probe -- probeN pprobeN [host]` — the
//! slot-keyed probe account (docs/METHOD.md "The local vmangos server"; NEVER `one`, the director's
//! account — a probe login there kicks their live session). Two caveats: this is a COMBAT probe,
//! so it runs director-supervised (docs/METHOD.md: no unattended combat probes), and `.learn` is
//! probe account (the `probe` skill; never a player's account — a probe login there kicks their
//! live session). Two caveats: this is a COMBAT probe, so it runs supervised (docs/METHOD.md: no
//! unattended combat probes), and `.learn` is
//! SEC_DEVELOPER (5); probe accounts are gmlevel 6 (SEC_ADMINISTRATOR), so it lands. The vmangos
//! console for the run and restore it after.
@@ -43,10 +43,10 @@ fn main() -> Result<()> {
let mut args = std::env::args().skip(1);
let user = args
.next()
.context("usage: cooldown_probe -- <probeN> <pprobeN> [host] (slot-keyed account)")?;
.context("usage: cooldown_probe -- <probeN> <pprobeN> [host] (a probe account)")?;
let pass = args
.next()
.context("usage: cooldown_probe -- <probeN> <pprobeN> [host] (slot-keyed account)")?;
.context("usage: cooldown_probe -- <probeN> <pprobeN> [host] (a probe account)")?;
let host = args.next().unwrap_or_else(|| "localhost".into());
let logon = benilla_protocol::logon(&host, &user, &pass)?;
@@ -3,9 +3,9 @@
//! decode against the server DB's `creature_template.faction`.
//!
//! Run: `cargo run -p benilla-protocol --example faction_probe -- probeN pprobeN [host]`
//! — the slot-keyed probe account (docs/METHOD.md "The local vmangos server"). The account is a
//! REQUIRED arg with no default: a default was once `one`, the director's account, where a
//! probe login kicks their live session (decision 0530).
//! — a probe account (the `probe` skill). The account is a REQUIRED arg with no default: a
//! default is somebody's account, and a probe login there kicks their live session (decision
//! 0530).
use std::time::{Duration, Instant};
@@ -16,10 +16,10 @@ fn main() -> Result<()> {
let mut args = std::env::args().skip(1);
let user = args
.next()
.context("usage: faction_probe -- <probeN> <pprobeN> [host] (slot-keyed account)")?;
.context("usage: faction_probe -- <probeN> <pprobeN> [host] (a probe account)")?;
let pass = args
.next()
.context("usage: faction_probe -- <probeN> <pprobeN> [host] (slot-keyed account)")?;
.context("usage: faction_probe -- <probeN> <pprobeN> [host] (a probe account)")?;
let host = args.next().unwrap_or_else(|| "localhost".into());
let logon = benilla_protocol::logon(&host, &user, &pass)?;
@@ -2,8 +2,8 @@
//! HORDE character's say must echo back (the server accepted the tongue), a dot-command must
//! answer (it survived the pre-parse `KnowsLanguage` gate), and the split-writer path must carry
//! the tongue too. Run: `cargo run -p benilla-protocol --example horde_chat_probe -- probeN
//! pprobeN [host]` — the slot-keyed probe account (docs/METHOD.md "The local vmangos server"; it
//! once hardcoded the retired shared `three` identity, decision 0530). Creates the orc
//! pprobeN [host]` — a probe account (the `probe` skill; it once hardcoded a shared identity,
//! decision 0530). Creates the orc
//! `Orc<N-spelled>` on this account on first run (names are realm-unique, so the orc keys to
//! the slot too). Exists because a hardcoded-Common send silently ate every Horde character's
//! chat and commands (decision 0392) — this is the one-shot regression check for that whole path.
@@ -27,14 +27,12 @@ fn main() -> Result<()> {
let mut args = std::env::args().skip(1);
let user = args
.next()
.context("usage: horde_chat_probe -- <probeN> <pprobeN> [host] (slot-keyed account)")?;
.context("usage: horde_chat_probe -- <probeN> <pprobeN> [host] (a probe account)")?;
let pass = args
.next()
.context("usage: horde_chat_probe -- <probeN> <pprobeN> [host] (slot-keyed account)")?;
.context("usage: horde_chat_probe -- <probeN> <pprobeN> [host] (a probe account)")?;
let host = args.next().unwrap_or_else(|| "localhost".into());
let orc = orc_name(&user).context(
"account must be a slot-keyed probeN (docs/METHOD.md \"The local vmangos server\")",
)?;
let orc = orc_name(&user).context("account must be a probeN (the `probe` skill)")?;
let logon = benilla_protocol::logon(&host, &user, &pass)?;
let addr = logon
@@ -219,8 +219,8 @@ struct Cli {
/// `SMSG_UPDATE_WORLD_STATE` — what the NPC-text `$<n>w`/`$<n>e` tokens read): teleport to
/// Elwynn, hop to Stormwind to force a zone-change init, and `.debug send worldstate` one
/// synthetic pair to require back. Prints every state received. Needs a **SEC_DEVELOPER**
/// account: `.debug send worldstate` is gmlevel 5, under the 6 the slot-keyed probe
/// accounts carry (decision 0450), so the update leg needs a temporary grant.
/// account: `.debug send worldstate` is gmlevel 5, under the 6 the probe accounts
/// carry (decision 0450), so the update leg needs a temporary grant.
#[arg(long)]
worldstate: bool,
@@ -231,7 +231,7 @@ struct Cli {
/// the same server spline machinery as any creature (not a teleport / not a knockback). Prints
/// the self spline in full (facing kind, duration, waypoints, flying bit) so the ride + the
/// `CMSG_MOVE_SPLINE_DONE` ack it obliges can be built from the real numbers. Needs a GM warrior
/// (any slot-keyed probe account, gmlevel 6).
/// (any probe account, gmlevel 6).
#[arg(long)]
charge: bool,
@@ -125,7 +125,7 @@ impl Probe for Aura {
"--aura: spell 1126 never appeared in UNIT_FIELD_AURA. Either the descriptor field \
index is wrong, or the GM `.aura` command was refused — it needs gmlevel >= 4 \
(VERIFIED vmangos `Chat/Chat.cpp:1229`: SEC_BASIC_ADMIN, which is 4 in \
`shared/Common.h:142`), and the slot-keyed probe accounts are gmlevel 6, so this \
`shared/Common.h:142`), and the probe accounts are gmlevel 6, so this \
probe cannot run as `probeN` without a temporary grant (docs/METHOD.md, decision 0450's \
precedent for --worldstate).",
)?;
@@ -10,7 +10,7 @@
//!
//! **The update leg needs a temporary grant.** `.debug send worldstate` sits in
//! `debugSendCommandTable` at **SEC_DEVELOPER = 5** (vmangos `Chat.cpp:323`) — one above the
//! gmlevel 6 the slot-keyed probe accounts carry (decisions 0450/0651). Without it the server answers
//! gmlevel 6 the probe accounts carry (decisions 0450/0651). Without it the server answers
//! *"This command is not available to you."* in chat and sends no packet, and the probe fails on
//! the update leg while the init leg still passes.
//!
+1 -1
View File
@@ -34,7 +34,7 @@ use anyhow::{anyhow, Context, Result};
use benilla_srp::{NormalizedString, PublicKey, SrpClientChallenge, SESSION_KEY_LENGTH};
/// The realmd (auth/login) server port — the stock one a vmangos `realmd` listens on, which our
/// deploy maps straight through (`3724:3724` in the compose file at `vmangos-deploy`).
/// deploy maps straight through (`3724:3724` in a docker deploy's compose file).
pub const AUTH_PORT: u16 = 3724;
/// The 1.12.1 client build we present to the server.
pub const CLIENT_BUILD: u16 = 5875;
@@ -1,7 +1,7 @@
//! Provision a slot-keyed probe account for the local vmangos `realmd` DB (decision 0450: a
//! session in `pool-N` probes as `probeN`/`pprobeN`/`Probe<N-spelled>`). Growing the worktree
//! pool means provisioning the matching account first; this is that recipe as one command
//! (decision 0530 — the pool once grew without it, leaving a slot with no working identity).
//! Provision a probe account for the local vmangos `realmd` DB (decision 0450: a scripted run
//! logs in as `probeN`/`pprobeN`/`Probe<N-spelled>`, one account per checkout that runs probes).
//! A new checkout means provisioning its account first; this is that recipe as one command
//! (decision 0530 — a checkout once went without it, and had no working identity).
//!
//! No DB access here — it prints the SQL and the char-create probe line; run those yourself.
//!
+2 -2
View File
@@ -1,7 +1,7 @@
//! The 5875 message catalog, row for row.
//!
//! @generated by `scripts/gen-message-catalog.py` from wow-5875-re `re/ui/message-catalog.tsv`
//! (itself generated by that repo's `re/ui/extract-message-catalog.py` out of `WoW.exe`).
//! @generated by `scripts/gen-message-catalog.py` from `reference/1.12-message-catalog.tsv`
//! (wow-5875-re's `re/ui/message-catalog.tsv`, extracted out of `WoW.exe` there).
//! Do not edit by hand — regenerate.
use super::{MessageRecord, MsgKind};
+1 -1
View File
@@ -34,7 +34,7 @@
//! reference's `0xbc6040` insertion-ordered cache (`benilla::ui_aura`), a cache position is simply a
//! 0-based index into `model.auras["player"]`. No second structure exists, and none is needed.
//!
//! # Byte-verified signatures (`~/dev/wow-5875-re/WoW/WoW.exe`, `system/ui/ledger.tsv`)
//! # Byte-verified signatures (the 1.12.1 binary, wow-5875-re `system/ui/ledger.tsv`)
//!
//! Read off the disassembly, not off a wiki — a later client changed every one of these shapes, and a
//! wrong shape fails *silently*.
+3 -3
View File
@@ -4,9 +4,9 @@
//!
//! ## Why this exists
//!
//! 1.12's own FrameXML never touches the API — `grep -c FontObject` over
//! `wow-5875-re/WoW/_extracted_framexml/*.lua` is **0** — so the whole surface is addon-facing, and
//! the corpus (`/Users/sam/dev/wow-addons-vanilla`, 218 addons) is where the demand is:
//! 1.12's own FrameXML never touches the API — `grep -c FontObject` over the reference
//! FrameXML's own `.lua` is **0** — so the whole surface is addon-facing, and
//! the addon corpus (`benilla_formats::addon_corpus`, 218 addons) is where the demand is:
//!
//! | call shape | sites | note |
//! |---|---|---|
@@ -47,7 +47,7 @@ fn dispatch_bench() {
// A plain Lua table with a table `__index` — the floor this path could reach, and what the
// reference client's flat `{name, lua_CFunction}` probe costs in C.
// **The MINIMUM of [`ROUNDS`] runs, never the mean.** This machine builds while it measures —
// a neighbouring worktree's `cargo` saturates ten cores — and a mean of a contended loop reads
// another build on the same machine saturates ten cores — and a mean of a contended loop reads
// whatever else was running. The minimum is the closest thing to the uncontended cost that a
// shared machine can report, and it is what makes two rows of one run comparable at all.
let floor = |what: &str, chunk: &str| {
+1 -1
View File
@@ -105,7 +105,7 @@ pub fn background_run() -> bool {
/// `WOW_PROBE…`-named env that captures pixels has to be excluded by name here, since the bare
/// `WOW_PROBE` prefix would otherwise sweep it in.
const NO_PIXEL_ENV_PREFIXES: &[&str] = &[
// Paired with its entry above, and it has to be BOTH or neither: `leg.sh`, `cine.sh` and
// Paired with its entry above, and it has to be BOTH or neither: `cine.sh` and
// `summon-live.sh` all set `WOW_UNATTENDED` beside a `WOW_PROBE*`/`WOW_RIG` that IS no-pixel,
// so listing it only as a background env would make it the one non-no-pixel member of every
// such run and the all-of rule would grow all three back to the full 1600x900 that decision
+27 -8
View File
@@ -24,7 +24,8 @@ fork, and forks are welcome.
## How a change is judged
1. `scripts/gates.sh` is green: fmt, clippy with warnings denied, the workspace tests, the
player build.
doc-link and render-pass lints, the player build with its own tests, and the engine boot
checks.
2. The reference fact is stated: what 1.12.1 does, and where that is known from (the client's
behaviour you observed, a DBC field, a FrameXML line, a packet capture). The names and shapes
under `reference/` are the surface benilla tracks.
@@ -34,12 +35,30 @@ fork, and forks are welcome.
## Setting up
- A 1.12.1 install of your own: `WOW_DATA=<path>`, or a `WoW` link at the repo root. benilla
reads it and never writes into it.
- A server to test against: any 1.12.1 server. The project runs against a local vmangos
(`WOW_HOST`, `WOW_USER`, `WOW_PASS`).
- `cargo play` builds and runs the play profile. `scripts/check.sh` verifies a round of work;
`scripts/gates.sh` is what a pull request must pass.
- Bugs, questions and ideas go to the Discord linked from the README. Issues are off on purpose.
- **The toolchain.** Stable Rust (`rust-toolchain.toml` adds clippy and rustfmt) and a C
compiler, because the client's Lua is vendored and built from source. On macOS the Xcode
command line tools, which also supply libclang for the audio bindings; on Linux the ALSA and
udev development packages and pkg-config. `python3` runs two of the gates, and `jq` the hooks.
- **A 1.12.1 install of your own.** `WOW_DATA=<its Data folder>`, or a `WoW` link at the repo
root, which only a dev build sees: the player build looks for `Data/` or `WoW/Data/` beside
the binary. benilla reads the install and never writes into it. `WOW_DATA=` (set, empty)
means "no install", which is how the no-install boot is tested on a machine that has one.
- **Without the install, green is hollow.** About a thousand tests read the install and skip
when it is absent; six more read a corpus of vanilla addons (`BENILLA_ADDON_CORPUS=<a folder
of addons>`, or a `wow-addons-vanilla` link at the root), third-party content that is not in
this repo. `scripts/gates.sh` and `scripts/check.sh` print how many tests skipped and why.
Where the data is, `BENILLA_REQUIRE_DATA=1` turns a skip into a failure, and the gates set it
themselves when the install and the corpus both resolve.
- **A server to test against.** Any 1.12.1 server; `WOW_HOST` names it (default
`localhost:3724`). A scripted run has no default account: `WOW_USER`, `WOW_PASS` and
`WOW_CHAR` name a test account on your server whose login kicks nobody, all three, either in
the environment or in a `.probe-identity` file at the repo root (one per line, never
committed), and `scripts/smoke.sh` (the live login gate) refuses without them. The probes
drive the body with GM commands, so give that account the top GM level.
- **The loop.** `cargo play` builds and runs the play profile. `scripts/check.sh` verifies a
round of work; `scripts/gates.sh` is the full chain, and it opens a window for the engine boot
checks, so it needs a display. Work on a branch.
- Bugs, questions and ideas go to the Discord linked from the README. Issues are off on purpose,
and pull requests are not open yet.
Working with an AI agent is expected. The agent reads `AGENTS.md`, and the same rules bind it.
+17 -37
View File
@@ -36,8 +36,8 @@ comment naming the reference fact and why we differ. Anything else is a fork.
- **Never commit anything from the install.** Art, models, sounds, maps, data: the install is
read at runtime from a gitignored path, and everyone provides their own. The one exception is
interface code: FrameXML and GlueXML run off the player's own patch chain, and the copies
already under `assets/ui` stay until they retire.
interface code: FrameXML and GlueXML run off the player's own patch chain, and our own
counterparts under `assets/ui` stay until they retire.
- **The install is read-only.** benilla never writes into the WoW folder: no screenshot, log,
cache or scratch file. A hook blocks such a write at the tool call, and `scripts/smoke.sh`
fails a run that leaves the install changed.
@@ -69,45 +69,31 @@ comment naming the reference fact and why we differ. Anything else is a fork.
## Gates
- **The land gates**: `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets
-- -D warnings`, `cargo test --workspace`, the player build without the `dev` feature and the
engine boot checks, as `scripts/gates.sh`. `wt.sh land` runs it on the tree that lands; it is
memoized on the tree.
-- -D warnings`, `cargo test --workspace`, the doc-link and render-pass lints, the player
build without the `dev` feature with its own tests, and the engine boot checks, as
`scripts/gates.sh`. It runs on the tree that lands, before it lands; it is memoized on the
tree. The tests that read the install or the addon corpus skip where the data is absent, and
the gate prints the count; where the data is, a skip is a failure (`BENILLA_REQUIRE_DATA=1`).
- **Per round**: `scripts/check.sh`, scoped to the changed crates and their dependents. Run each
gate once per round; `tee` the output to a file and grep the file.
- **A clean run** is the fourth gate: `scripts/smoke.sh`, a live login and logout against the
local server. For wire work the gate is a live run with a trace (`WOW_MOVE_TRACE=<path>`,
tags filtered), never a capture: capture mode has no network.
- **Platform seams**: a `cfg(target_os)`, a `[target.'cfg(…)']` dependency, a `#[link]` or an
`extern "system"` is invisible to the macOS gates. `scripts/crosscheck.sh` compiles Linux and
Windows, and the land worker runs it when the diff touches a seam.
- **Vulkan**: `scripts/vkleg.sh` runs the client on Vulkan in a container before a release,
because Metal cannot ask what Vulkan validation refuses.
- Hooks enforce only two things: no writes into the install, and the worktree rule on a machine
with a pool. Everything else is a gate or discipline.
## Worktrees
On a machine with a pool, every session works in its own slot and `scripts/wt.sh` is the only
way to get one. `claim <name>` at the start; `land <name>` at the end, which rebases,
regenerates the map, runs the full gate chain, fast-forwards and pushes main and releases the
slot, one land at a time; `release` to abandon; `status` to see the pool. Never the primary
checkout, which belongs to no session and stays clean, and never the harness worktree tool or a
hand-rolled `git worktree add`. Work against the slot by path: prefix Bash calls with
`cd <slot> &&` and give edits absolute paths under it. No free slot means stop and ask which
one to take over, never fall back to the primary. Hooks enforce all of this.
Sessions share the machine, so a kill is scoped to a PID, never to a command pattern:
`pkill -f "cargo test"` takes out every neighbour's gates along with yours.
On a plain clone there is no pool: work on a branch and open a pull request.
`extern "system"` is invisible to the gates of the platform you are on. Say in the commit
which platforms you built.
- One hook: no writes into the install. Everything else is a gate or discipline.
## The local server
benilla talks to a local vmangos (build 5875); `WOW_HOST`, `WOW_USER`, `WOW_PASS` and `WOW_CHAR`
override the defaults. These bind every session and every agent:
benilla talks to any 1.12.1 server: `WOW_HOST` names it (default `localhost`), `WOW_USER` and
`WOW_PASS` together log in without typing, `WOW_CHAR` picks the character. There is no default
account. These bind every session and every agent:
- A probe logs in as its own probe account (`probeN` for `pool-N`), never as a player's: a login
kicks whoever holds the account.
- A scripted run logs in as the account its checkout declares in `.probe-identity` (the three
variables, one per line, never committed), or as the three variables name; never as a
player's, because a login kicks whoever holds the account, and the client refuses a scripted
login from a checkout on any account but the declared one.
- Every unattended run says so: `WOW_UNATTENDED=1`. An agent's run is silent: `WOW_NOSOUND=1`.
- No unattended combat probes.
- Anything about hostility, reaction colour, nameplates, threat, damage or breath runs with
@@ -115,9 +101,3 @@ override the defaults. These bind every session and every agent:
- Read the preflight banner before debugging anything else.
The mechanics are the `probe` skill.
## Agents in a shared tree
Parallel agents share one working tree: whole-tree git ops (`stash`, `reset --hard`,
`clean -f`, a pathless `checkout`) are forbidden, diagnostics use path-scoped commands, and an
agent never gets a tree of its own.
+428
View File
@@ -0,0 +1,428 @@
# FrameScript C->Lua event catalog — eventId -> name, extracted from WoW.exe 1.12.1.5875
# source: the event-name pointer array at .data 0xbe1198 (stride 4, 0x225=549 slots), RE 2026-06-21,
# BASE CORRECTED 2026-07-21 (was 0xbe11d8 -> every id was 16 too low; see
# system/object-layer/scratch/lootroll-chat-and-lifecycle.md §3). The base is VERIFIED at
# 0x48fea1-0x48feab: `mov edx,0x225; mov ecx,0xbe1198; call 0x703d90`, and 0x703d90 builds the
# runtime 16-byte-stride dispatch table [0xceef68] as table[i].name = strdup(nameArray[i])
# (0x703e06-0x703e2e), so the id passed to FrameScript_SignalEvent 0x703e50 / 0x703f50 IS this index.
# NB slots 0..15 carry no `mov imm32` name write in this scan (they are set from a register at
# 0x51acbf and neighbours); coverage is unchanged from the pre-correction extraction (383 rows).
0 UNIT_PET
2 UNIT_PET
16 UNIT_HEALTH
17 UNIT_MANA
18 UNIT_RAGE
19 UNIT_FOCUS
20 UNIT_ENERGY
21 UNIT_HAPPINESS
22 UNIT_MAXHEALTH
23 UNIT_MAXMANA
24 UNIT_MAXRAGE
25 UNIT_MAXFOCUS
26 UNIT_MAXENERGY
27 UNIT_MAXHAPPINESS
28 UNIT_LEVEL
29 UNIT_FACTION
30 UNIT_DISPLAYPOWER
40 UNIT_FLAGS
41 UNIT_AURA
107 UNIT_AURA
120 UNIT_ATTACK_SPEED
121 UNIT_ATTACK_SPEED
122 UNIT_RANGEDDAMAGE
128 UNIT_DAMAGE
129 UNIT_DAMAGE
130 UNIT_DAMAGE
131 UNIT_DAMAGE
132 UNIT_LOYALTY
135 UNIT_PET_EXPERIENCE
136 UNIT_PET_EXPERIENCE
137 UNIT_DYNAMIC_FLAGS
143 UNIT_PET_TRAINING_POINTS
144 UNIT_STATS
145 UNIT_STATS
146 UNIT_STATS
147 UNIT_STATS
148 UNIT_STATS
149 UNIT_RESISTANCES
150 UNIT_RESISTANCES
151 UNIT_RESISTANCES
152 UNIT_RESISTANCES
153 UNIT_RESISTANCES
154 UNIT_RESISTANCES
155 UNIT_RESISTANCES
159 UNIT_ATTACK_POWER
160 UNIT_ATTACK_POWER
161 UNIT_ATTACK_POWER
162 UNIT_RANGED_ATTACK_POWER
163 UNIT_RANGED_ATTACK_POWER
164 UNIT_RANGED_ATTACK_POWER
165 UNIT_RANGEDDAMAGE
166 UNIT_RANGEDDAMAGE
167 UNIT_MANA
174 UNIT_MANA
182 UNIT_COMBAT
183 UNIT_NAME_UPDATE
184 UNIT_PORTRAIT_UPDATE
185 UNIT_MODEL_CHANGED
186 UNIT_INVENTORY_CHANGED
187 UNIT_CLASSIFICATION_CHANGED
188 ITEM_LOCK_CHANGED
189 PLAYER_XP_UPDATE
190 PLAYER_REGEN_DISABLED
191 PLAYER_REGEN_ENABLED
192 PLAYER_AURAS_CHANGED
193 PLAYER_ENTER_COMBAT
194 PLAYER_LEAVE_COMBAT
195 PLAYER_TARGET_CHANGED
196 PLAYER_CONTROL_LOST
197 PLAYER_CONTROL_GAINED
198 PLAYER_FARSIGHT_FOCUS_CHANGED
199 PLAYER_LEVEL_UP
200 PLAYER_MONEY
201 PLAYER_DAMAGE_DONE_MODS
202 PLAYER_COMBO_POINTS
203 ZONE_CHANGED
204 ZONE_CHANGED_INDOORS
205 ZONE_CHANGED_NEW_AREA
206 MINIMAP_ZONE_CHANGED
207 MINIMAP_UPDATE_ZOOM
208 SCREENSHOT_SUCCEEDED
209 SCREENSHOT_FAILED
210 ACTIONBAR_SHOWGRID
211 ACTIONBAR_HIDEGRID
212 ACTIONBAR_PAGE_CHANGED
213 ACTIONBAR_SLOT_CHANGED
214 ACTIONBAR_UPDATE_STATE
215 ACTIONBAR_UPDATE_USABLE
216 ACTIONBAR_UPDATE_COOLDOWN
217 UPDATE_BONUS_ACTIONBAR
218 PARTY_MEMBERS_CHANGED
219 PARTY_LEADER_CHANGED
220 PARTY_MEMBER_ENABLE
221 PARTY_MEMBER_DISABLE
222 PARTY_LOOT_METHOD_CHANGED
223 SYSMSG
224 UI_ERROR_MESSAGE
225 UI_INFO_MESSAGE
226 UPDATE_CHAT_COLOR
227 CHAT_MSG_ADDON
228 CHAT_MSG_SAY
229 CHAT_MSG_PARTY
230 CHAT_MSG_RAID
231 CHAT_MSG_GUILD
232 CHAT_MSG_OFFICER
233 CHAT_MSG_YELL
234 CHAT_MSG_WHISPER
235 CHAT_MSG_WHISPER_INFORM
236 CHAT_MSG_EMOTE
237 CHAT_MSG_TEXT_EMOTE
238 CHAT_MSG_SYSTEM
239 CHAT_MSG_MONSTER_SAY
240 CHAT_MSG_MONSTER_YELL
241 CHAT_MSG_MONSTER_WHISPER
242 CHAT_MSG_MONSTER_EMOTE
243 CHAT_MSG_CHANNEL
244 CHAT_MSG_CHANNEL_JOIN
245 CHAT_MSG_CHANNEL_LEAVE
246 CHAT_MSG_CHANNEL_LIST
247 CHAT_MSG_CHANNEL_NOTICE
248 CHAT_MSG_CHANNEL_NOTICE_USER
249 CHAT_MSG_AFK
250 CHAT_MSG_DND
251 CHAT_MSG_COMBAT_LOG
252 CHAT_MSG_IGNORED
253 CHAT_MSG_SKILL
254 CHAT_MSG_LOOT
255 CHAT_MSG_MONEY
256 CHAT_MSG_RAID_LEADER
257 CHAT_MSG_RAID_WARNING
258 LANGUAGE_LIST_CHANGED
259 TIME_PLAYED_MSG
260 SPELLS_CHANGED
261 CURRENT_SPELL_CAST_CHANGED
262 SPELL_UPDATE_COOLDOWN
263 SPELL_UPDATE_USABLE
264 CHARACTER_POINTS_CHANGED
265 SKILL_LINES_CHANGED
266 ITEM_PUSH
267 LOOT_OPENED
268 LOOT_SLOT_CLEARED
269 LOOT_CLOSED
270 PLAYER_LOGIN
271 PLAYER_LOGOUT
272 PLAYER_ENTERING_WORLD
273 PLAYER_LEAVING_WORLD
274 PLAYER_ALIVE
275 PLAYER_DEAD
276 PLAYER_CAMPING
277 PLAYER_QUITING
278 LOGOUT_CANCEL
279 RESURRECT_REQUEST
280 PARTY_INVITE_REQUEST
281 PARTY_INVITE_CANCEL
282 GUILD_INVITE_REQUEST
283 GUILD_INVITE_CANCEL
284 GUILD_MOTD
285 TRADE_REQUEST
286 TRADE_REQUEST_CANCEL
287 LOOT_BIND_CONFIRM
288 EQUIP_BIND_CONFIRM
289 AUTOEQUIP_BIND_CONFIRM
290 USE_BIND_CONFIRM
291 DELETE_ITEM_CONFIRM
292 CURSOR_UPDATE
293 ITEM_TEXT_BEGIN
294 ITEM_TEXT_TRANSLATION
295 ITEM_TEXT_READY
296 ITEM_TEXT_CLOSED
297 GOSSIP_SHOW
298 GOSSIP_ENTER_CODE
299 GOSSIP_CLOSED
300 QUEST_GREETING
301 QUEST_DETAIL
302 QUEST_PROGRESS
303 QUEST_COMPLETE
304 QUEST_FINISHED
305 QUEST_ITEM_UPDATE
306 TAXIMAP_OPENED
307 TAXIMAP_CLOSED
308 QUEST_LOG_UPDATE
309 TRAINER_SHOW
310 TRAINER_UPDATE
311 TRAINER_CLOSED
312 CVAR_UPDATE
313 TRADE_SKILL_SHOW
314 TRADE_SKILL_UPDATE
315 TRADE_SKILL_CLOSE
316 MERCHANT_SHOW
317 MERCHANT_UPDATE
318 MERCHANT_CLOSED
319 TRADE_SHOW
320 TRADE_CLOSED
321 TRADE_UPDATE
322 TRADE_ACCEPT_UPDATE
323 TRADE_TARGET_ITEM_CHANGED
324 TRADE_PLAYER_ITEM_CHANGED
325 TRADE_MONEY_CHANGED
326 PLAYER_TRADE_MONEY
327 BAG_OPEN
328 BAG_UPDATE
329 BAG_CLOSED
330 BAG_UPDATE_COOLDOWN
331 LOCALPLAYER_PET_RENAMED
332 UNIT_ATTACK
333 UNIT_DEFENSE
334 PET_ATTACK_START
335 PET_ATTACK_STOP
336 UPDATE_MOUSEOVER_UNIT
337 SPELLCAST_START
338 SPELLCAST_STOP
339 SPELLCAST_FAILED
340 SPELLCAST_INTERRUPTED
341 SPELLCAST_DELAYED
342 SPELLCAST_CHANNEL_START
343 SPELLCAST_CHANNEL_UPDATE
344 SPELLCAST_CHANNEL_STOP
345 PLAYER_GUILD_UPDATE
346 QUEST_ACCEPT_CONFIRM
347 PLAYERBANKSLOTS_CHANGED
348 BANKFRAME_OPENED
349 BANKFRAME_CLOSED
350 PLAYERBANKBAGSLOTS_CHANGED
351 FRIENDLIST_UPDATE
352 IGNORELIST_UPDATE
353 PET_BAR_UPDATE
354 PET_BAR_UPDATE_COOLDOWN
355 PET_BAR_SHOWGRID
356 PET_BAR_HIDEGRID
357 MINIMAP_PING
358 CHAT_MSG_COMBAT_MISC_INFO
359 CRAFT_SHOW
360 CRAFT_UPDATE
361 CRAFT_CLOSE
362 MIRROR_TIMER_START
363 MIRROR_TIMER_PAUSE
364 MIRROR_TIMER_STOP
365 WORLD_MAP_UPDATE
366 WORLD_MAP_NAME_UPDATE
367 AUTOFOLLOW_BEGIN
368 AUTOFOLLOW_END
370 CINEMATIC_START
371 CINEMATIC_STOP
372 UPDATE_FACTION
373 CLOSE_WORLD_MAP
374 OPEN_TABARD_FRAME
375 CLOSE_TABARD_FRAME
376 TABARD_CANSAVE_CHANGED
377 SHOW_COMPARE_TOOLTIP
378 GUILD_REGISTRAR_SHOW
379 GUILD_REGISTRAR_CLOSED
380 DUEL_REQUESTED
381 DUEL_OUTOFBOUNDS
382 DUEL_INBOUNDS
383 DUEL_FINISHED
384 TUTORIAL_TRIGGER
385 PET_DISMISS_START
386 UPDATE_BINDINGS
387 UPDATE_SHAPESHIFT_FORMS
388 WHO_LIST_UPDATE
389 UPDATE_LFG
390 PETITION_SHOW
391 PETITION_CLOSED
392 EXECUTE_CHAT_LINE
393 UPDATE_MACROS
394 UPDATE_TICKET
395 UPDATE_CHAT_WINDOWS
396 CONFIRM_XP_LOSS
397 CORPSE_IN_RANGE
398 CORPSE_IN_INSTANCE
399 CORPSE_OUT_OF_RANGE
400 UPDATE_GM_STATUS
401 PLAYER_UNGHOST
402 BIND_ENCHANT
403 REPLACE_ENCHANT
404 TRADE_REPLACE_ENCHANT
405 PLAYER_UPDATE_RESTING
406 UPDATE_EXHAUSTION
407 PLAYER_FLAGS_CHANGED
408 GUILD_ROSTER_UPDATE
409 GM_PLAYER_INFO
410 MAIL_SHOW
411 MAIL_CLOSED
412 SEND_MAIL_MONEY_CHANGED
413 SEND_MAIL_COD_CHANGED
414 MAIL_SEND_INFO_UPDATE
415 MAIL_SEND_SUCCESS
416 MAIL_INBOX_UPDATE
417 BATTLEFIELDS_SHOW
418 BATTLEFIELDS_CLOSED
419 UPDATE_BATTLEFIELD_STATUS
420 UPDATE_BATTLEFIELD_SCORE
421 AUCTION_HOUSE_SHOW
422 AUCTION_HOUSE_CLOSED
423 NEW_AUCTION_UPDATE
424 AUCTION_ITEM_LIST_UPDATE
425 AUCTION_OWNED_LIST_UPDATE
426 AUCTION_BIDDER_LIST_UPDATE
427 PET_UI_UPDATE
428 PET_UI_CLOSE
429 ADDON_LOADED
430 VARIABLES_LOADED
431 MACRO_ACTION_FORBIDDEN
432 ADDON_ACTION_FORBIDDEN
433 MEMORY_EXHAUSTED
434 MEMORY_RECOVERED
435 START_AUTOREPEAT_SPELL
436 STOP_AUTOREPEAT_SPELL
437 PET_STABLE_SHOW
438 PET_STABLE_UPDATE
439 PET_STABLE_UPDATE_PAPERDOLL
440 PET_STABLE_CLOSED
441 CHAT_MSG_COMBAT_SELF_HITS
442 CHAT_MSG_COMBAT_SELF_MISSES
443 CHAT_MSG_COMBAT_PET_HITS
444 CHAT_MSG_COMBAT_PET_MISSES
445 CHAT_MSG_COMBAT_PARTY_HITS
446 CHAT_MSG_COMBAT_PARTY_MISSES
447 CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS
448 CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES
449 CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS
450 CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES
451 CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS
452 CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES
453 CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS
454 CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES
455 CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS
456 CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES
457 CHAT_MSG_COMBAT_FRIENDLY_DEATH
458 CHAT_MSG_COMBAT_HOSTILE_DEATH
459 CHAT_MSG_COMBAT_XP_GAIN
460 CHAT_MSG_COMBAT_HONOR_GAIN
461 CHAT_MSG_SPELL_SELF_DAMAGE
462 CHAT_MSG_SPELL_SELF_BUFF
463 CHAT_MSG_SPELL_PET_DAMAGE
464 CHAT_MSG_SPELL_PET_BUFF
465 CHAT_MSG_SPELL_PARTY_DAMAGE
466 CHAT_MSG_SPELL_PARTY_BUFF
467 CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE
468 CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF
469 CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE
470 CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF
471 CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE
472 CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF
473 CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE
474 CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF
475 CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE
476 CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF
477 CHAT_MSG_SPELL_TRADESKILLS
478 CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF
479 CHAT_MSG_SPELL_DAMAGESHIELDS_ON_OTHERS
480 CHAT_MSG_SPELL_AURA_GONE_SELF
481 CHAT_MSG_SPELL_AURA_GONE_PARTY
482 CHAT_MSG_SPELL_AURA_GONE_OTHER
483 CHAT_MSG_SPELL_ITEM_ENCHANTMENTS
484 CHAT_MSG_SPELL_BREAK_AURA
485 CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE
486 CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS
487 CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE
488 CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS
489 CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE
490 CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS
491 CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE
492 CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS
493 CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE
494 CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS
495 CHAT_MSG_SPELL_FAILED_LOCALPLAYER
496 CHAT_MSG_BG_SYSTEM_NEUTRAL
497 CHAT_MSG_BG_SYSTEM_ALLIANCE
498 CHAT_MSG_BG_SYSTEM_HORDE
499 RAID_ROSTER_UPDATE
500 UPDATE_PENDING_MAIL
501 UPDATE_INVENTORY_ALERTS
502 UPDATE_TRADESKILL_RECAST
503 OPEN_MASTER_LOOT_LIST
504 UPDATE_MASTER_LOOT_LIST
505 START_LOOT_ROLL
506 CANCEL_LOOT_ROLL
507 CONFIRM_LOOT_ROLL
508 INSTANCE_BOOT_START
509 INSTANCE_BOOT_STOP
510 LEARNED_SPELL_IN_TAB
511 DISPLAY_SIZE_CHANGED
512 CONFIRM_TALENT_WIPE
513 CONFIRM_BINDER
514 MAIL_FAILED
515 CLOSE_INBOX_ITEM
516 CONFIRM_SUMMON
517 BILLING_NAG_DIALOG
518 IGR_BILLING_NAG_DIALOG
519 MEETINGSTONE_CHANGED
520 PLAYER_SKINNED
521 TABARD_SAVE_PENDING
522 UNIT_QUEST_LOG_CHANGED
523 PLAYER_PVP_KILLS_CHANGED
524 PLAYER_PVP_RANK_CHANGED
525 INSPECT_HONOR_UPDATE
526 UPDATE_WORLD_STATES
527 AREA_SPIRIT_HEALER_IN_RANGE
528 AREA_SPIRIT_HEALER_OUT_OF_RANGE
529 CONFIRM_PET_UNLEARN
530 PLAYTIME_CHANGED
531 UPDATE_LFG_TYPES
532 UPDATE_LFG_LIST
533 CHAT_MSG_COMBAT_FACTION_CHANGE
534 START_MINIGAME
535 MINIGAME_UPDATE
536 READY_CHECK
537 RAID_TARGET_UPDATE
538 GMSURVEY_DISPLAY
539 UPDATE_INSTANCE_INFO
541 CHAT_MSG_RAID_BOSS_EMOTE
542 COMBAT_TEXT_UPDATE
543 LOTTERY_SHOW
544 CHAT_MSG_FILTERED
545 QUEST_WATCH_UPDATE
546 CHAT_MSG_BATTLEGROUND
547 CHAT_MSG_BATTLEGROUND_LEADER
548 LOTTERY_ITEM_UPDATE
1 # FrameScript C->Lua event catalog — eventId -> name, extracted from WoW.exe 1.12.1.5875
2 # source: the event-name pointer array at .data 0xbe1198 (stride 4, 0x225=549 slots), RE 2026-06-21,
3 # BASE CORRECTED 2026-07-21 (was 0xbe11d8 -> every id was 16 too low; see
4 # system/object-layer/scratch/lootroll-chat-and-lifecycle.md §3). The base is VERIFIED at
5 # 0x48fea1-0x48feab: `mov edx,0x225; mov ecx,0xbe1198; call 0x703d90`, and 0x703d90 builds the
6 # runtime 16-byte-stride dispatch table [0xceef68] as table[i].name = strdup(nameArray[i])
7 # (0x703e06-0x703e2e), so the id passed to FrameScript_SignalEvent 0x703e50 / 0x703f50 IS this index.
8 # NB slots 0..15 carry no `mov imm32` name write in this scan (they are set from a register at
9 # 0x51acbf and neighbours); coverage is unchanged from the pre-correction extraction (383 rows).
10 0 UNIT_PET
11 2 UNIT_PET
12 16 UNIT_HEALTH
13 17 UNIT_MANA
14 18 UNIT_RAGE
15 19 UNIT_FOCUS
16 20 UNIT_ENERGY
17 21 UNIT_HAPPINESS
18 22 UNIT_MAXHEALTH
19 23 UNIT_MAXMANA
20 24 UNIT_MAXRAGE
21 25 UNIT_MAXFOCUS
22 26 UNIT_MAXENERGY
23 27 UNIT_MAXHAPPINESS
24 28 UNIT_LEVEL
25 29 UNIT_FACTION
26 30 UNIT_DISPLAYPOWER
27 40 UNIT_FLAGS
28 41 UNIT_AURA
29 107 UNIT_AURA
30 120 UNIT_ATTACK_SPEED
31 121 UNIT_ATTACK_SPEED
32 122 UNIT_RANGEDDAMAGE
33 128 UNIT_DAMAGE
34 129 UNIT_DAMAGE
35 130 UNIT_DAMAGE
36 131 UNIT_DAMAGE
37 132 UNIT_LOYALTY
38 135 UNIT_PET_EXPERIENCE
39 136 UNIT_PET_EXPERIENCE
40 137 UNIT_DYNAMIC_FLAGS
41 143 UNIT_PET_TRAINING_POINTS
42 144 UNIT_STATS
43 145 UNIT_STATS
44 146 UNIT_STATS
45 147 UNIT_STATS
46 148 UNIT_STATS
47 149 UNIT_RESISTANCES
48 150 UNIT_RESISTANCES
49 151 UNIT_RESISTANCES
50 152 UNIT_RESISTANCES
51 153 UNIT_RESISTANCES
52 154 UNIT_RESISTANCES
53 155 UNIT_RESISTANCES
54 159 UNIT_ATTACK_POWER
55 160 UNIT_ATTACK_POWER
56 161 UNIT_ATTACK_POWER
57 162 UNIT_RANGED_ATTACK_POWER
58 163 UNIT_RANGED_ATTACK_POWER
59 164 UNIT_RANGED_ATTACK_POWER
60 165 UNIT_RANGEDDAMAGE
61 166 UNIT_RANGEDDAMAGE
62 167 UNIT_MANA
63 174 UNIT_MANA
64 182 UNIT_COMBAT
65 183 UNIT_NAME_UPDATE
66 184 UNIT_PORTRAIT_UPDATE
67 185 UNIT_MODEL_CHANGED
68 186 UNIT_INVENTORY_CHANGED
69 187 UNIT_CLASSIFICATION_CHANGED
70 188 ITEM_LOCK_CHANGED
71 189 PLAYER_XP_UPDATE
72 190 PLAYER_REGEN_DISABLED
73 191 PLAYER_REGEN_ENABLED
74 192 PLAYER_AURAS_CHANGED
75 193 PLAYER_ENTER_COMBAT
76 194 PLAYER_LEAVE_COMBAT
77 195 PLAYER_TARGET_CHANGED
78 196 PLAYER_CONTROL_LOST
79 197 PLAYER_CONTROL_GAINED
80 198 PLAYER_FARSIGHT_FOCUS_CHANGED
81 199 PLAYER_LEVEL_UP
82 200 PLAYER_MONEY
83 201 PLAYER_DAMAGE_DONE_MODS
84 202 PLAYER_COMBO_POINTS
85 203 ZONE_CHANGED
86 204 ZONE_CHANGED_INDOORS
87 205 ZONE_CHANGED_NEW_AREA
88 206 MINIMAP_ZONE_CHANGED
89 207 MINIMAP_UPDATE_ZOOM
90 208 SCREENSHOT_SUCCEEDED
91 209 SCREENSHOT_FAILED
92 210 ACTIONBAR_SHOWGRID
93 211 ACTIONBAR_HIDEGRID
94 212 ACTIONBAR_PAGE_CHANGED
95 213 ACTIONBAR_SLOT_CHANGED
96 214 ACTIONBAR_UPDATE_STATE
97 215 ACTIONBAR_UPDATE_USABLE
98 216 ACTIONBAR_UPDATE_COOLDOWN
99 217 UPDATE_BONUS_ACTIONBAR
100 218 PARTY_MEMBERS_CHANGED
101 219 PARTY_LEADER_CHANGED
102 220 PARTY_MEMBER_ENABLE
103 221 PARTY_MEMBER_DISABLE
104 222 PARTY_LOOT_METHOD_CHANGED
105 223 SYSMSG
106 224 UI_ERROR_MESSAGE
107 225 UI_INFO_MESSAGE
108 226 UPDATE_CHAT_COLOR
109 227 CHAT_MSG_ADDON
110 228 CHAT_MSG_SAY
111 229 CHAT_MSG_PARTY
112 230 CHAT_MSG_RAID
113 231 CHAT_MSG_GUILD
114 232 CHAT_MSG_OFFICER
115 233 CHAT_MSG_YELL
116 234 CHAT_MSG_WHISPER
117 235 CHAT_MSG_WHISPER_INFORM
118 236 CHAT_MSG_EMOTE
119 237 CHAT_MSG_TEXT_EMOTE
120 238 CHAT_MSG_SYSTEM
121 239 CHAT_MSG_MONSTER_SAY
122 240 CHAT_MSG_MONSTER_YELL
123 241 CHAT_MSG_MONSTER_WHISPER
124 242 CHAT_MSG_MONSTER_EMOTE
125 243 CHAT_MSG_CHANNEL
126 244 CHAT_MSG_CHANNEL_JOIN
127 245 CHAT_MSG_CHANNEL_LEAVE
128 246 CHAT_MSG_CHANNEL_LIST
129 247 CHAT_MSG_CHANNEL_NOTICE
130 248 CHAT_MSG_CHANNEL_NOTICE_USER
131 249 CHAT_MSG_AFK
132 250 CHAT_MSG_DND
133 251 CHAT_MSG_COMBAT_LOG
134 252 CHAT_MSG_IGNORED
135 253 CHAT_MSG_SKILL
136 254 CHAT_MSG_LOOT
137 255 CHAT_MSG_MONEY
138 256 CHAT_MSG_RAID_LEADER
139 257 CHAT_MSG_RAID_WARNING
140 258 LANGUAGE_LIST_CHANGED
141 259 TIME_PLAYED_MSG
142 260 SPELLS_CHANGED
143 261 CURRENT_SPELL_CAST_CHANGED
144 262 SPELL_UPDATE_COOLDOWN
145 263 SPELL_UPDATE_USABLE
146 264 CHARACTER_POINTS_CHANGED
147 265 SKILL_LINES_CHANGED
148 266 ITEM_PUSH
149 267 LOOT_OPENED
150 268 LOOT_SLOT_CLEARED
151 269 LOOT_CLOSED
152 270 PLAYER_LOGIN
153 271 PLAYER_LOGOUT
154 272 PLAYER_ENTERING_WORLD
155 273 PLAYER_LEAVING_WORLD
156 274 PLAYER_ALIVE
157 275 PLAYER_DEAD
158 276 PLAYER_CAMPING
159 277 PLAYER_QUITING
160 278 LOGOUT_CANCEL
161 279 RESURRECT_REQUEST
162 280 PARTY_INVITE_REQUEST
163 281 PARTY_INVITE_CANCEL
164 282 GUILD_INVITE_REQUEST
165 283 GUILD_INVITE_CANCEL
166 284 GUILD_MOTD
167 285 TRADE_REQUEST
168 286 TRADE_REQUEST_CANCEL
169 287 LOOT_BIND_CONFIRM
170 288 EQUIP_BIND_CONFIRM
171 289 AUTOEQUIP_BIND_CONFIRM
172 290 USE_BIND_CONFIRM
173 291 DELETE_ITEM_CONFIRM
174 292 CURSOR_UPDATE
175 293 ITEM_TEXT_BEGIN
176 294 ITEM_TEXT_TRANSLATION
177 295 ITEM_TEXT_READY
178 296 ITEM_TEXT_CLOSED
179 297 GOSSIP_SHOW
180 298 GOSSIP_ENTER_CODE
181 299 GOSSIP_CLOSED
182 300 QUEST_GREETING
183 301 QUEST_DETAIL
184 302 QUEST_PROGRESS
185 303 QUEST_COMPLETE
186 304 QUEST_FINISHED
187 305 QUEST_ITEM_UPDATE
188 306 TAXIMAP_OPENED
189 307 TAXIMAP_CLOSED
190 308 QUEST_LOG_UPDATE
191 309 TRAINER_SHOW
192 310 TRAINER_UPDATE
193 311 TRAINER_CLOSED
194 312 CVAR_UPDATE
195 313 TRADE_SKILL_SHOW
196 314 TRADE_SKILL_UPDATE
197 315 TRADE_SKILL_CLOSE
198 316 MERCHANT_SHOW
199 317 MERCHANT_UPDATE
200 318 MERCHANT_CLOSED
201 319 TRADE_SHOW
202 320 TRADE_CLOSED
203 321 TRADE_UPDATE
204 322 TRADE_ACCEPT_UPDATE
205 323 TRADE_TARGET_ITEM_CHANGED
206 324 TRADE_PLAYER_ITEM_CHANGED
207 325 TRADE_MONEY_CHANGED
208 326 PLAYER_TRADE_MONEY
209 327 BAG_OPEN
210 328 BAG_UPDATE
211 329 BAG_CLOSED
212 330 BAG_UPDATE_COOLDOWN
213 331 LOCALPLAYER_PET_RENAMED
214 332 UNIT_ATTACK
215 333 UNIT_DEFENSE
216 334 PET_ATTACK_START
217 335 PET_ATTACK_STOP
218 336 UPDATE_MOUSEOVER_UNIT
219 337 SPELLCAST_START
220 338 SPELLCAST_STOP
221 339 SPELLCAST_FAILED
222 340 SPELLCAST_INTERRUPTED
223 341 SPELLCAST_DELAYED
224 342 SPELLCAST_CHANNEL_START
225 343 SPELLCAST_CHANNEL_UPDATE
226 344 SPELLCAST_CHANNEL_STOP
227 345 PLAYER_GUILD_UPDATE
228 346 QUEST_ACCEPT_CONFIRM
229 347 PLAYERBANKSLOTS_CHANGED
230 348 BANKFRAME_OPENED
231 349 BANKFRAME_CLOSED
232 350 PLAYERBANKBAGSLOTS_CHANGED
233 351 FRIENDLIST_UPDATE
234 352 IGNORELIST_UPDATE
235 353 PET_BAR_UPDATE
236 354 PET_BAR_UPDATE_COOLDOWN
237 355 PET_BAR_SHOWGRID
238 356 PET_BAR_HIDEGRID
239 357 MINIMAP_PING
240 358 CHAT_MSG_COMBAT_MISC_INFO
241 359 CRAFT_SHOW
242 360 CRAFT_UPDATE
243 361 CRAFT_CLOSE
244 362 MIRROR_TIMER_START
245 363 MIRROR_TIMER_PAUSE
246 364 MIRROR_TIMER_STOP
247 365 WORLD_MAP_UPDATE
248 366 WORLD_MAP_NAME_UPDATE
249 367 AUTOFOLLOW_BEGIN
250 368 AUTOFOLLOW_END
251 370 CINEMATIC_START
252 371 CINEMATIC_STOP
253 372 UPDATE_FACTION
254 373 CLOSE_WORLD_MAP
255 374 OPEN_TABARD_FRAME
256 375 CLOSE_TABARD_FRAME
257 376 TABARD_CANSAVE_CHANGED
258 377 SHOW_COMPARE_TOOLTIP
259 378 GUILD_REGISTRAR_SHOW
260 379 GUILD_REGISTRAR_CLOSED
261 380 DUEL_REQUESTED
262 381 DUEL_OUTOFBOUNDS
263 382 DUEL_INBOUNDS
264 383 DUEL_FINISHED
265 384 TUTORIAL_TRIGGER
266 385 PET_DISMISS_START
267 386 UPDATE_BINDINGS
268 387 UPDATE_SHAPESHIFT_FORMS
269 388 WHO_LIST_UPDATE
270 389 UPDATE_LFG
271 390 PETITION_SHOW
272 391 PETITION_CLOSED
273 392 EXECUTE_CHAT_LINE
274 393 UPDATE_MACROS
275 394 UPDATE_TICKET
276 395 UPDATE_CHAT_WINDOWS
277 396 CONFIRM_XP_LOSS
278 397 CORPSE_IN_RANGE
279 398 CORPSE_IN_INSTANCE
280 399 CORPSE_OUT_OF_RANGE
281 400 UPDATE_GM_STATUS
282 401 PLAYER_UNGHOST
283 402 BIND_ENCHANT
284 403 REPLACE_ENCHANT
285 404 TRADE_REPLACE_ENCHANT
286 405 PLAYER_UPDATE_RESTING
287 406 UPDATE_EXHAUSTION
288 407 PLAYER_FLAGS_CHANGED
289 408 GUILD_ROSTER_UPDATE
290 409 GM_PLAYER_INFO
291 410 MAIL_SHOW
292 411 MAIL_CLOSED
293 412 SEND_MAIL_MONEY_CHANGED
294 413 SEND_MAIL_COD_CHANGED
295 414 MAIL_SEND_INFO_UPDATE
296 415 MAIL_SEND_SUCCESS
297 416 MAIL_INBOX_UPDATE
298 417 BATTLEFIELDS_SHOW
299 418 BATTLEFIELDS_CLOSED
300 419 UPDATE_BATTLEFIELD_STATUS
301 420 UPDATE_BATTLEFIELD_SCORE
302 421 AUCTION_HOUSE_SHOW
303 422 AUCTION_HOUSE_CLOSED
304 423 NEW_AUCTION_UPDATE
305 424 AUCTION_ITEM_LIST_UPDATE
306 425 AUCTION_OWNED_LIST_UPDATE
307 426 AUCTION_BIDDER_LIST_UPDATE
308 427 PET_UI_UPDATE
309 428 PET_UI_CLOSE
310 429 ADDON_LOADED
311 430 VARIABLES_LOADED
312 431 MACRO_ACTION_FORBIDDEN
313 432 ADDON_ACTION_FORBIDDEN
314 433 MEMORY_EXHAUSTED
315 434 MEMORY_RECOVERED
316 435 START_AUTOREPEAT_SPELL
317 436 STOP_AUTOREPEAT_SPELL
318 437 PET_STABLE_SHOW
319 438 PET_STABLE_UPDATE
320 439 PET_STABLE_UPDATE_PAPERDOLL
321 440 PET_STABLE_CLOSED
322 441 CHAT_MSG_COMBAT_SELF_HITS
323 442 CHAT_MSG_COMBAT_SELF_MISSES
324 443 CHAT_MSG_COMBAT_PET_HITS
325 444 CHAT_MSG_COMBAT_PET_MISSES
326 445 CHAT_MSG_COMBAT_PARTY_HITS
327 446 CHAT_MSG_COMBAT_PARTY_MISSES
328 447 CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS
329 448 CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES
330 449 CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS
331 450 CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES
332 451 CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS
333 452 CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES
334 453 CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS
335 454 CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES
336 455 CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS
337 456 CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES
338 457 CHAT_MSG_COMBAT_FRIENDLY_DEATH
339 458 CHAT_MSG_COMBAT_HOSTILE_DEATH
340 459 CHAT_MSG_COMBAT_XP_GAIN
341 460 CHAT_MSG_COMBAT_HONOR_GAIN
342 461 CHAT_MSG_SPELL_SELF_DAMAGE
343 462 CHAT_MSG_SPELL_SELF_BUFF
344 463 CHAT_MSG_SPELL_PET_DAMAGE
345 464 CHAT_MSG_SPELL_PET_BUFF
346 465 CHAT_MSG_SPELL_PARTY_DAMAGE
347 466 CHAT_MSG_SPELL_PARTY_BUFF
348 467 CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE
349 468 CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF
350 469 CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE
351 470 CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF
352 471 CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE
353 472 CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF
354 473 CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE
355 474 CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF
356 475 CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE
357 476 CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF
358 477 CHAT_MSG_SPELL_TRADESKILLS
359 478 CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF
360 479 CHAT_MSG_SPELL_DAMAGESHIELDS_ON_OTHERS
361 480 CHAT_MSG_SPELL_AURA_GONE_SELF
362 481 CHAT_MSG_SPELL_AURA_GONE_PARTY
363 482 CHAT_MSG_SPELL_AURA_GONE_OTHER
364 483 CHAT_MSG_SPELL_ITEM_ENCHANTMENTS
365 484 CHAT_MSG_SPELL_BREAK_AURA
366 485 CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE
367 486 CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS
368 487 CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE
369 488 CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS
370 489 CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE
371 490 CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS
372 491 CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE
373 492 CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS
374 493 CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE
375 494 CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS
376 495 CHAT_MSG_SPELL_FAILED_LOCALPLAYER
377 496 CHAT_MSG_BG_SYSTEM_NEUTRAL
378 497 CHAT_MSG_BG_SYSTEM_ALLIANCE
379 498 CHAT_MSG_BG_SYSTEM_HORDE
380 499 RAID_ROSTER_UPDATE
381 500 UPDATE_PENDING_MAIL
382 501 UPDATE_INVENTORY_ALERTS
383 502 UPDATE_TRADESKILL_RECAST
384 503 OPEN_MASTER_LOOT_LIST
385 504 UPDATE_MASTER_LOOT_LIST
386 505 START_LOOT_ROLL
387 506 CANCEL_LOOT_ROLL
388 507 CONFIRM_LOOT_ROLL
389 508 INSTANCE_BOOT_START
390 509 INSTANCE_BOOT_STOP
391 510 LEARNED_SPELL_IN_TAB
392 511 DISPLAY_SIZE_CHANGED
393 512 CONFIRM_TALENT_WIPE
394 513 CONFIRM_BINDER
395 514 MAIL_FAILED
396 515 CLOSE_INBOX_ITEM
397 516 CONFIRM_SUMMON
398 517 BILLING_NAG_DIALOG
399 518 IGR_BILLING_NAG_DIALOG
400 519 MEETINGSTONE_CHANGED
401 520 PLAYER_SKINNED
402 521 TABARD_SAVE_PENDING
403 522 UNIT_QUEST_LOG_CHANGED
404 523 PLAYER_PVP_KILLS_CHANGED
405 524 PLAYER_PVP_RANK_CHANGED
406 525 INSPECT_HONOR_UPDATE
407 526 UPDATE_WORLD_STATES
408 527 AREA_SPIRIT_HEALER_IN_RANGE
409 528 AREA_SPIRIT_HEALER_OUT_OF_RANGE
410 529 CONFIRM_PET_UNLEARN
411 530 PLAYTIME_CHANGED
412 531 UPDATE_LFG_TYPES
413 532 UPDATE_LFG_LIST
414 533 CHAT_MSG_COMBAT_FACTION_CHANGE
415 534 START_MINIGAME
416 535 MINIGAME_UPDATE
417 536 READY_CHECK
418 537 RAID_TARGET_UPDATE
419 538 GMSURVEY_DISPLAY
420 539 UPDATE_INSTANCE_INFO
421 541 CHAT_MSG_RAID_BOSS_EMOTE
422 542 COMBAT_TEXT_UPDATE
423 543 LOTTERY_SHOW
424 544 CHAT_MSG_FILTERED
425 545 QUEST_WATCH_UPDATE
426 546 CHAT_MSG_BATTLEGROUND
427 547 CHAT_MSG_BATTLEGROUND_LEADER
428 548 LOTTERY_ITEM_UPDATE
+533
View File
@@ -0,0 +1,533 @@
# C->Lua event fire sites — every call AND TAIL-JMP to FrameScript_SignalEvent (0x703e50, ecx=eventId)
# (the 85 `jmp 0x703e50` sites were absent until 2026-08-21 — the census had been written over `call`
# alone, so a tail-jmp fire read as "this event has no fire site here"; see re/audit/re-sweep-status.md)
# and SignalEvent2 (0x703f50, [esp]=eventId + printf-style arg_format). eventId resolved via event-catalog.tsv.
# eventId "dyn" = computed (e.g. a UnitField update loop); blank name = eventId has no entry in the name table.
#
# CAVEAT (2026-09-04, staticpopup round): the `arg_format` column is the FORMAT STRING the site pushes,
# which is a declaration, NOT a census of what the caller actually pushed. The marshaller 0x704f10
# consumes one vararg per directive (%f eight bytes, everything else four), so a site whose format
# declares more than its caller pushed hands Lua undefined values. `re/audit/signalevent2-fmt-args.py`
# audits all 149 sites (0 unadjudicated) and finds exactly THREE such sites -- read that column with
# these in mind: 0x496230 TRADE_REPLACE_ENCHANT "%s%s%d" declares 3 and pushes 2; 0x5e4527 and
# 0x5e7960 UPDATE_TICKET "%d%s%f%f%f%d%d" declare 10 dwords and push 7 (their sibling 0x5e45e2 pushes
# the full 12). Every other site's format matches its caller. See method.md §6.
#
# CAVEAT (2026-09-03, found while carving `UpdateSpells` for benilla; the answer was already on `main`
# in system/glue/): the `name` column resolves every id through event-catalog.tsv, which is the
# IN-GAME table (0xbe1198, 549 entries). 0x703d90 is called TWICE with different populations --
# 0x46a88f with (0xb41e70, 26) for the GLUE phase and 0x48feab with (0xbe1198, 549) for the world --
# and it rebuilds [0xceef68] wholesale from whichever it is handed, so THE SAME INTEGER NAMES A
# DIFFERENT EVENT IN THE TWO PHASES and 0x703e50 bounds-checks nothing. 22 rows here sit in a
# `Source\Glue\` TU (0x46aa34; 0x46bc9d-0x46c52e inside 0x46b930; 0x46e73d; 0x47461e) and their ids
# belong to the GLUE space: name them from system/glue/scratch/glue-event-name-map.md, never from
# event-catalog.tsv. 0x46aa34 was the one row where that produced a WRONG name (id 22 read as
# UNIT_MAXHEALTH; it is FRAMES_LOADED -- glue-event-name-map.md's own control, with GlueParent.lua:94
# as the independent oracle); it is blanked below. The other 21 were already blank, because those ids
# are NULL slots in the in-game table. Owed: the glue node names them in its own artifact.
#
# CAVEAT 2 (2026-09-03, carving UNIT_FLAGS for benilla): AN EVENT WITH NO ROW HERE IS NOT AN EVENT
# WITH NO PRODUCER. This census is over the two signal helpers' own call/jmp sites, and the whole
# per-field UNIT_* family reaches them through a WRAPPER: 0x515e50(&guid, id) fans out over the unit
# tokens and fires 0x703f50(id, "%s", token) at 0x515e7a -- the "?" row below. Its 38 callers carry
# the id in edx, and one of them, 0x51bd5b in the UpdateField watch callback 0x51bd50, computes it as
# (registered field byte offset >> 2). So ids 0x00-0xb5 -- every named unit descriptor field,
# UNIT_FLAGS 40 included -- are produced there and appear nowhere in this file.
# SCOPE WIDENED 2026-09-07 (PLAYER_FLAGS round, benilla dispatch): "0x00-0xb5" is the COMPUTED-id
# subset only, and reading it as the whole absent set is a trap -- most 0x515e50 sites carry a
# LITERAL id in edx, and those are absent from this file too, at any magnitude. The literal set is
# {0x10 0x16 0x1c 0x1d 0x1e 0x29 0xb7 0xb8 0xb9 0xba 0xbb 0x159 0x197 0x20a}. Notably 0x197 = 407
# PLAYER_FLAGS_CHANGED (0x5eea35 mov edx,0x197 / 0x5eea3d call 0x515e50, inside the PLAYER_FLAGS
# delta handler 0x5ee990) sits OUTSIDE 0x00-0xb5, so a reader checking 407 against the range above
# would wrongly conclude it has no producer. It has one; it is simply not a SignalEvent call site.
# See system/object-layer/scratch/player-flags-delta-arms.md. Mechanism, the
# bounded four-way census that makes 0x51bd50 the SOLE producer of 40, and the fire condition
# (a repe cmpsb of the watched span against the object's shadow, at 0x4655bb) are in
# system/ui/scratch/unit-field-event-bridge.md. Same shape for 0x5de4b0 (id = its userdata,
# arg1 hardcoded "player") and 0x49b199 (id from the table at 0x8062e0).
fire_site_va dispatcher eventId name arg_format
0x4018df SignalEvent2 259 TIME_PLAYED_MSG %d%d
0x46aa34 SignalEvent 22
0x46b04d SignalEvent2 3 %s
0x46b0ac SignalEvent2 3 %s%s
0x46b0d2 SignalEvent2 3 %s
0x46b1a4 SignalEvent2 3 %s%s
0x46b1e7 SignalEvent2 3 %s%s
0x46b26c SignalEvent2 3 %s%s
0x46b2c4 SignalEvent2 3 %s%s
0x46b31e SignalEvent2 3 %s%s
0x46b388 SignalEvent2 23 UNIT_MAXMANA %s
0x46b3c0 SignalEvent2 3 %s%s
0x46b48b SignalEvent 5
0x46b4a8 SignalEvent2 23 UNIT_MAXMANA %s
0x46b4cd SignalEvent2 23 UNIT_MAXMANA %s
0x46b5a5 SignalEvent2 3 %s%s
0x46b5c2 SignalEvent2 23 UNIT_MAXMANA %s
0x46b848 SignalEvent2 0 UNIT_PET %s
0x46b8af SignalEvent2 3 %s%s
0x46b8fa SignalEvent2 3 %s%s
0x46b964 SignalEvent2 21 UNIT_HAPPINESS %s
0x46bac8 SignalEvent 5
0x46bb15 SignalEvent2 25 UNIT_MAXFOCUS %d%d%d%d%d%d%d%d%d%d
0x46bc9d SignalEvent2 3 %s%s
0x46bdfc SignalEvent2 4 %s%s
0x46be81 SignalEvent2 4 %s%s
0x46be96 SignalEvent2 4 %s
0x46bee7 SignalEvent2 3 %s%s
0x46bf6f SignalEvent2 3 %s%s%s
0x46bf91 SignalEvent 5
0x46c04a SignalEvent2 4 %s
0x46c08f SignalEvent 5
0x46c0b4 SignalEvent2 ? %s
0x46c0dd SignalEvent 5
0x46c103 SignalEvent2 4 %s
0x46c129 SignalEvent 5
0x46c133 SignalEvent 12
0x46c15e SignalEvent2 4 %s
0x46c183 SignalEvent2 3 %s%s
0x46c1a2 SignalEvent 13
0x46c298 SignalEvent2 3 %s%s
0x46c52e SignalEvent 15
0x46c5b0 SignalEvent2 3 %s%s
0x46c62e SignalEvent 2 UNIT_PET
0x46c68f SignalEvent2 3 %s%s
0x46e73d SignalEvent 9
0x46e9cb SignalEvent 9
0x46ea1a SignalEvent 9
0x46eb44 SignalEvent2 18 UNIT_RAGE %d%d
0x46ec26 SignalEvent 9
0x46ec42 SignalEvent2 18 UNIT_RAGE %d%d
0x47120c SignalEvent2 3 %s%s
0x47256d SignalEvent 7
0x4725e1 SignalEvent 7
0x472732 SignalEvent 7
0x47276c SignalEvent2 8 %d
0x4735d2 SignalEvent2 23 UNIT_MAXMANA %s
0x47461e SignalEvent 5
0x474b39 SignalEvent2 3 %s%s
0x474b94 SignalEvent 17 UNIT_MANA
0x474c81 SignalEvent 16 UNIT_HEALTH
0x474cb3 SignalEvent2 3 %s%s
0x488cd5 SignalEvent2 312 CVAR_UPDATE %s%s
0x48edda SignalEvent 370 CINEMATIC_START
0x48f095 SignalEvent 371 CINEMATIC_STOP
0x48f3eb SignalEvent2 279 RESURRECT_REQUEST %s
0x48f412 SignalEvent2 520 PLAYER_SKINNED %d
0x48f42b SignalEvent2 280 PARTY_INVITE_REQUEST %s
0x48f445 SignalEvent 281 PARTY_INVITE_CANCEL
0x48f45c SignalEvent2 282 GUILD_INVITE_REQUEST %s%s
0x48f475 SignalEvent 283 GUILD_INVITE_CANCEL
0x48fa17 SignalEvent 526 UPDATE_WORLD_STATES
0x490959 SignalEvent 270 PLAYER_LOGIN
0x49096a SignalEvent 272 PLAYER_ENTERING_WORLD
0x490b4d SignalEvent 273 PLAYER_LEAVING_WORLD
0x490b77 SignalEvent 371 CINEMATIC_STOP
0x490c2a SignalEvent 271 PLAYER_LOGOUT
0x49156a SignalEvent 430 VARIABLES_LOADED
0x491be8 SignalEvent2 266 ITEM_PUSH %d%s
0x49211a SignalEvent dyn
0x492124 SignalEvent 399 CORPSE_OUT_OF_RANGE
0x4922d1 SignalEvent 527 AREA_SPIRIT_HEALER_IN_RANGE
0x4929b3 SignalEvent 336 UPDATE_MOUSEOVER_UNIT
0x492f5c SignalEvent 511 DISPLAY_SIZE_CHANGED
0x4930c7 SignalEvent 209 SCREENSHOT_FAILED
0x49389c SignalEvent 195 PLAYER_TARGET_CHANGED
0x493a73 SignalEvent 195 PLAYER_TARGET_CHANGED
0x4945ca SignalEvent2 ? %s
0x49468b SignalEvent2 182 UNIT_COMBAT %s%s%s%d%d
0x49493f SignalEvent 205 ZONE_CHANGED_NEW_AREA
0x49495f SignalEvent ?
0x4949cb SignalEvent 206 MINIMAP_ZONE_CHANGED
0x4949e7 SignalEvent 206 MINIMAP_ZONE_CHANGED
0x494add SignalEvent 431 MACRO_ACTION_FORBIDDEN
0x494afb SignalEvent2 432 ADDON_ACTION_FORBIDDEN %s
0x494c54 SignalEvent 292 CURSOR_UPDATE
0x494d10 SignalEvent 200 PLAYER_MONEY
0x49515e SignalEvent 292 CURSOR_UPDATE
0x495248 SignalEvent 200 PLAYER_MONEY
0x4952cc SignalEvent 292 CURSOR_UPDATE
0x4953c0 SignalEvent2 291 DELETE_ITEM_CONFIRM %s%d
0x495415 SignalEvent 188 ITEM_LOCK_CHANGED
0x495455 SignalEvent 188 ITEM_LOCK_CHANGED
0x49557d SignalEvent 188 ITEM_LOCK_CHANGED
0x49563d SignalEvent2 433 MEMORY_EXHAUSTED %d
0x495656 SignalEvent 434 MEMORY_RECOVERED
0x4956e9 SignalEvent 518 IGR_BILLING_NAG_DIALOG
0x495700 SignalEvent2 517 BILLING_NAG_DIALOG %d
0x495741 SignalEvent 518 IGR_BILLING_NAG_DIALOG
0x49581e SignalEvent2 517 BILLING_NAG_DIALOG %d
0x49587d SignalEvent 275 PLAYER_DEAD
0x495893 SignalEvent 274 PLAYER_ALIVE
0x4958f7 SignalEvent 197 PLAYER_CONTROL_GAINED
0x49590b SignalEvent 196 PLAYER_CONTROL_LOST
0x4960a3 SignalEvent 402 BIND_ENCHANT
0x4960e4 SignalEvent2 403 REPLACE_ENCHANT %s%s
0x496230 SignalEvent2 404 TRADE_REPLACE_ENCHANT %s%s%d
0x49630d SignalEvent dyn
0x496359 SignalEvent 508 INSTANCE_BOOT_START
0x49636e SignalEvent 509 INSTANCE_BOOT_STOP
0x4963ce SignalEvent 516 CONFIRM_SUMMON
0x4996be SignalEvent 395 UPDATE_CHAT_WINDOWS
0x49971b SignalEvent2 226 UPDATE_CHAT_COLOR %s%f%f%f
0x499793 SignalEvent2 226 UPDATE_CHAT_COLOR %s%f%f%f
0x499939 SignalEvent 395 UPDATE_CHAT_WINDOWS
0x499993 SignalEvent2 226 UPDATE_CHAT_COLOR %s%f%f%f
0x499a03 SignalEvent2 226 UPDATE_CHAT_COLOR %s%f%f%f
0x49a95f SignalEvent2 227 CHAT_MSG_ADDON %s%s%s%s
0x49b199 SignalEvent2 ? %s%s%s%s%s%s%d%d%s%d
0x49b975 SignalEvent 258 LANGUAGE_LIST_CHANGED
0x49bddc SignalEvent 526 UPDATE_WORLD_STATES
0x49e1a7 SignalEvent 539 UPDATE_INSTANCE_INFO
0x49eb21 SignalEvent 395 UPDATE_CHAT_WINDOWS
0x49eb5f SignalEvent 395 UPDATE_CHAT_WINDOWS
0x4a00a9 SignalEvent 395 UPDATE_CHAT_WINDOWS
0x4a098b SignalEvent2 226 UPDATE_CHAT_COLOR %s%f%f%f
0x4a6ce4 SignalEvent 365 WORLD_MAP_UPDATE
0x4a7835 SignalEvent 373 CLOSE_WORLD_MAP
0x4aa15b SignalEvent 418 BATTLEFIELDS_CLOSED
0x4aa308 SignalEvent 420 UPDATE_BATTLEFIELD_SCORE
0x4aa7d1 SignalEvent 417 BATTLEFIELDS_SHOW
0x4aab05 SignalEvent 419 UPDATE_BATTLEFIELD_STATUS
0x4ac195 SignalEvent 366 WORLD_MAP_NAME_UPDATE
0x4acd42 SignalEvent 410 MAIL_SHOW
0x4acda3 SignalEvent 411 MAIL_CLOSED
0x4ace19 SignalEvent 412 SEND_MAIL_MONEY_CHANGED
0x4ace23 SignalEvent 413 SEND_MAIL_COD_CHANGED
0x4ace2d SignalEvent 415 MAIL_SEND_SUCCESS
0x4ad02a SignalEvent 416 MAIL_INBOX_UPDATE
0x4ad16a SignalEvent 514 MAIL_FAILED
0x4ad5d6 SignalEvent 416 MAIL_INBOX_UPDATE
0x4ad605 SignalEvent 500 UPDATE_PENDING_MAIL
0x4ad66b SignalEvent 500 UPDATE_PENDING_MAIL
0x4ad772 SignalEvent2 515 CLOSE_INBOX_ITEM %d
0x4ad7a3 SignalEvent 416 MAIL_INBOX_UPDATE
0x4ad87d SignalEvent2 515 CLOSE_INBOX_ITEM %d
0x4ad8ae SignalEvent 416 MAIL_INBOX_UPDATE
0x4ad8fc SignalEvent 416 MAIL_INBOX_UPDATE
0x4ad966 SignalEvent 416 MAIL_INBOX_UPDATE
0x4adc44 SignalEvent 412 SEND_MAIL_MONEY_CHANGED
0x4adc88 SignalEvent 413 SEND_MAIL_COD_CHANGED
0x4add9d SignalEvent 416 MAIL_INBOX_UPDATE
0x4adeba SignalEvent 500 UPDATE_PENDING_MAIL
0x4ae0de SignalEvent 414 MAIL_SEND_INFO_UPDATE
0x4ae71f SignalEvent 414 MAIL_SEND_INFO_UPDATE
0x4ae98d SignalEvent 414 MAIL_SEND_INFO_UPDATE
0x4aeaf8 SignalEvent 416 MAIL_INBOX_UPDATE
0x4af08f SignalEvent 416 MAIL_INBOX_UPDATE
0x4af0af SignalEvent 416 MAIL_INBOX_UPDATE
0x4af34f SignalEvent 416 MAIL_INBOX_UPDATE
0x4af5bf SignalEvent 416 MAIL_INBOX_UPDATE
0x4b2904 SignalEvent 387 UPDATE_SHAPESHIFT_FORMS
0x4b2b92 SignalEvent2 510 LEARNED_SPELL_IN_TAB %d
0x4b2e4e SignalEvent 387 UPDATE_SHAPESHIFT_FORMS
0x4b302f SignalEvent 260 SPELLS_CHANGED
0x4b31ba SignalEvent 262 SPELL_UPDATE_COOLDOWN
0x4b3246 SignalEvent 263 SPELL_UPDATE_USABLE
0x4b325a SignalEvent 261 CURRENT_SPELL_CAST_CHANGED
0x4b54ad SignalEvent2 384 TUTORIAL_TRIGGER %d
0x4b5890 SignalEvent2 384 TUTORIAL_TRIGGER %d
0x4b649d SignalEvent 386 UPDATE_BINDINGS
0x4b805b SignalEvent 386 UPDATE_BINDINGS
0x4ba1a2 SignalEvent 499 RAID_ROSTER_UPDATE
0x4ba348 SignalEvent 537 RAID_TARGET_UPDATE
0x4ba53a SignalEvent 536 READY_CHECK
0x4ba57b SignalEvent 499 RAID_ROSTER_UPDATE
0x4ba5db SignalEvent 537 RAID_TARGET_UPDATE
0x4babef SignalEvent 499 RAID_ROSTER_UPDATE
0x4bada6 SignalEvent 499 RAID_ROSTER_UPDATE
0x4badb0 SignalEvent 218 PARTY_MEMBERS_CHANGED
0x4bc85b SignalEvent2 2 UNIT_PET %s
0x4bc907 SignalEvent 428 PET_UI_CLOSE
0x4bc911 SignalEvent 353 PET_BAR_UPDATE
0x4bc956 SignalEvent 353 PET_BAR_UPDATE
0x4bc979 SignalEvent 353 PET_BAR_UPDATE
0x4bcb6d SignalEvent 353 PET_BAR_UPDATE
0x4bcc81 SignalEvent 353 PET_BAR_UPDATE
0x4bcdcb SignalEvent 353 PET_BAR_UPDATE
0x4bce75 SignalEvent 355 PET_BAR_SHOWGRID
0x4bce85 SignalEvent 356 PET_BAR_HIDEGRID
0x4bce95 SignalEvent 354 PET_BAR_UPDATE_COOLDOWN
0x4bd438 SignalEvent 353 PET_BAR_UPDATE
0x4bd6b4 SignalEvent 353 PET_BAR_UPDATE
0x4bd722 SignalEvent2 385 PET_DISMISS_START %d
0x4bdb54 SignalEvent 353 PET_BAR_UPDATE
0x4bf260 SignalEvent2 322 TRADE_ACCEPT_UPDATE %d%d
0x4bf29f SignalEvent2 322 TRADE_ACCEPT_UPDATE %d%d
0x4bf414 SignalEvent2 323 TRADE_TARGET_ITEM_CHANGED %d
0x4bf452 SignalEvent2 323 TRADE_TARGET_ITEM_CHANGED %d
0x4bf487 SignalEvent2 324 TRADE_PLAYER_ITEM_CHANGED %d
0x4bf4ab SignalEvent 326 PLAYER_TRADE_MONEY
0x4bf4b5 SignalEvent 200 PLAYER_MONEY
0x4bf4d6 SignalEvent 325 TRADE_MONEY_CHANGED
0x4bf522 SignalEvent 320 TRADE_CLOSED
0x4bf630 SignalEvent 326 PLAYER_TRADE_MONEY
0x4bf63a SignalEvent 200 PLAYER_MONEY
0x4bf644 SignalEvent 319 TRADE_SHOW
0x4bf705 SignalEvent 319 TRADE_SHOW
0x4bf832 SignalEvent 286 TRADE_REQUEST_CANCEL
0x4bfaef SignalEvent2 324 TRADE_PLAYER_ITEM_CHANGED %d
0x4bfbea SignalEvent 326 PLAYER_TRADE_MONEY
0x4bfbf5 SignalEvent 200 PLAYER_MONEY
0x4bfc3f SignalEvent2 324 TRADE_PLAYER_ITEM_CHANGED %d
0x4bfdce SignalEvent 326 PLAYER_TRADE_MONEY
0x4c034f SignalEvent 321 TRADE_UPDATE
0x4c1dac SignalEvent 269 LOOT_CLOSED
0x4c1ee6 SignalEvent 267 LOOT_OPENED
0x4c1f77 SignalEvent2 268 LOOT_SLOT_CLEARED %d
0x4c1fe0 SignalEvent 267 LOOT_OPENED
0x4c21d4 SignalEvent2 287 LOOT_BIND_CONFIRM %u
0x4c2240 SignalEvent 267 LOOT_OPENED
0x4c276f SignalEvent2 268 LOOT_SLOT_CLEARED %d
0x4c2862 SignalEvent2 268 LOOT_SLOT_CLEARED %d
0x4c28d1 SignalEvent 503 OPEN_MASTER_LOOT_LIST
0x4c290f SignalEvent2 287 LOOT_BIND_CONFIRM %u
0x4c2b37 SignalEvent 267 LOOT_OPENED
0x4c2fbf SignalEvent 504 UPDATE_MASTER_LOOT_LIST
0x4c3e48 SignalEvent 543 LOTTERY_SHOW
0x4c4123 SignalEvent 548 LOTTERY_ITEM_UPDATE
0x4c4cd5 SignalEvent 534 START_MINIGAME
0x4c4cdf SignalEvent 535 MINIGAME_UPDATE
0x4c4d0a SignalEvent 535 MINIGAME_UPDATE
0x4c6f56 SignalEvent 525 INSPECT_HONOR_UPDATE
0x4c7098 SignalEvent2 264 CHARACTER_POINTS_CHANGED %d%d
0x4c7110 SignalEvent2 186 UNIT_INVENTORY_CHANGED %s
0x4c728d SignalEvent2 347 PLAYERBANKSLOTS_CHANGED %s
0x4c72e0 SignalEvent2 186 UNIT_INVENTORY_CHANGED %s
0x4c8085 SignalEvent 501 UPDATE_INVENTORY_ALERTS
0x4c8509 SignalEvent2 186 UNIT_INVENTORY_CHANGED %s
0x4ca38f SignalEvent 519 MEETINGSTONE_CHANGED
0x4cac9b SignalEvent 437 PET_STABLE_SHOW
0x4cad88 SignalEvent 438 PET_STABLE_UPDATE
0x4cae3a SignalEvent 440 PET_STABLE_CLOSED
0x4cb3bf SignalEvent 438 PET_STABLE_UPDATE
0x4cba5f SignalEvent 439 PET_STABLE_UPDATE_PAPERDOLL
0x4cc5d0 SignalEvent 424 AUCTION_ITEM_LIST_UPDATE
0x4cc703 SignalEvent 424 AUCTION_ITEM_LIST_UPDATE
0x4cc769 SignalEvent 426 AUCTION_BIDDER_LIST_UPDATE
0x4cd09e SignalEvent 426 AUCTION_BIDDER_LIST_UPDATE
0x4cd143 SignalEvent 424 AUCTION_ITEM_LIST_UPDATE
0x4cd301 SignalEvent 425 AUCTION_OWNED_LIST_UPDATE
0x4cd3a8 SignalEvent 424 AUCTION_ITEM_LIST_UPDATE
0x4cd5e6 SignalEvent 421 AUCTION_HOUSE_SHOW
0x4cd602 SignalEvent 422 AUCTION_HOUSE_CLOSED
0x4cd678 SignalEvent 423 NEW_AUCTION_UPDATE
0x4cd8cf SignalEvent 424 AUCTION_ITEM_LIST_UPDATE
0x4cdcef SignalEvent 425 AUCTION_OWNED_LIST_UPDATE
0x4cdddf SignalEvent 426 AUCTION_BIDDER_LIST_UPDATE
0x4ce03e SignalEvent 424 AUCTION_ITEM_LIST_UPDATE
0x4ce09e SignalEvent 425 AUCTION_OWNED_LIST_UPDATE
0x4ce101 SignalEvent 426 AUCTION_BIDDER_LIST_UPDATE
0x4ce2ff SignalEvent 423 NEW_AUCTION_UPDATE
0x4ce576 SignalEvent 423 NEW_AUCTION_UPDATE
0x4cf29f SignalEvent 424 AUCTION_ITEM_LIST_UPDATE
0x4cf2bf SignalEvent 425 AUCTION_OWNED_LIST_UPDATE
0x4cf2df SignalEvent 426 AUCTION_BIDDER_LIST_UPDATE
0x4d117a SignalEvent2 408 GUILD_ROSTER_UPDATE %d
0x4d1188 SignalEvent 408 GUILD_ROSTER_UPDATE
0x4d2ff2 SignalEvent 265 SKILL_LINES_CHANGED
0x4d346f SignalEvent 265 SKILL_LINES_CHANGED
0x4d3598 SignalEvent 265 SKILL_LINES_CHANGED
0x4d3e3a SignalEvent 265 SKILL_LINES_CHANGED
0x4d4a88 SignalEvent2 380 DUEL_REQUESTED %s
0x4d4aa5 SignalEvent 381 DUEL_OUTOFBOUNDS
0x4d4ac5 SignalEvent 382 DUEL_INBOUNDS
0x4d4b6a SignalEvent 383 DUEL_FINISHED
0x4d569e SignalEvent 372 UPDATE_FACTION
0x4d56f6 SignalEvent 372 UPDATE_FACTION
0x4d5984 SignalEvent 372 UPDATE_FACTION
0x4d5db0 SignalEvent 372 UPDATE_FACTION
0x4d7468 SignalEvent 311 TRAINER_CLOSED
0x4d7b73 SignalEvent 309 TRAINER_SHOW
0x4d83f9 SignalEvent 310 TRAINER_UPDATE
0x4d8ca0 SignalEvent 310 TRAINER_UPDATE
0x4d8cc0 SignalEvent 310 TRAINER_UPDATE
0x4d8ce0 SignalEvent 310 TRAINER_UPDATE
0x4dba96 SignalEvent 306 TAXIMAP_OPENED
0x4dc15f SignalEvent 307 TAXIMAP_CLOSED
0x4de534 SignalEvent 308 QUEST_LOG_UPDATE
0x4de816 SignalEvent 308 QUEST_LOG_UPDATE
0x4ded98 SignalEvent 308 QUEST_LOG_UPDATE
0x4dedba SignalEvent 308 QUEST_LOG_UPDATE
0x4dedd6 SignalEvent 308 QUEST_LOG_UPDATE
0x4def20 SignalEvent 308 QUEST_LOG_UPDATE
0x4df28f SignalEvent 308 QUEST_LOG_UPDATE
0x4df840 SignalEvent 308 QUEST_LOG_UPDATE
0x4e07ef SignalEvent 308 QUEST_LOG_UPDATE
0x4e081f SignalEvent 308 QUEST_LOG_UPDATE
0x4e083f SignalEvent 308 QUEST_LOG_UPDATE
0x4e1559 SignalEvent 308 QUEST_LOG_UPDATE
0x4e1fdc SignalEvent 299 GOSSIP_CLOSED
0x4e22b5 SignalEvent 297 GOSSIP_SHOW
0x4e237e SignalEvent2 298 GOSSIP_ENTER_CODE %d
0x4e332d SignalEvent 296 ITEM_TEXT_CLOSED
0x4e33a2 SignalEvent 296 ITEM_TEXT_CLOSED
0x4e34a4 SignalEvent 293 ITEM_TEXT_BEGIN
0x4e367c SignalEvent 295 ITEM_TEXT_READY
0x4e3783 SignalEvent 295 ITEM_TEXT_READY
0x4e3860 SignalEvent 295 ITEM_TEXT_READY
0x4e437f SignalEvent 192 PLAYER_AURAS_CHANGED
0x4e501d SignalEvent 217 UPDATE_BONUS_ACTIONBAR
0x4e58c5 SignalEvent 210 ACTIONBAR_SHOWGRID
0x4e58d5 SignalEvent 211 ACTIONBAR_HIDEGRID
0x4e5998 SignalEvent2 213 ACTIONBAR_SLOT_CHANGED %d
0x4e5bf5 SignalEvent 214 ACTIONBAR_UPDATE_STATE
0x4e5c53 SignalEvent 215 ACTIONBAR_UPDATE_USABLE
0x4e5c65 SignalEvent 216 ACTIONBAR_UPDATE_COOLDOWN
0x4e7655 SignalEvent 212 ACTIONBAR_PAGE_CHANGED
0x4e82a9 SignalEvent 219 PARTY_LEADER_CHANGED
0x4e82c2 SignalEvent 219 PARTY_LEADER_CHANGED
0x4e8495 SignalEvent 218 PARTY_MEMBERS_CHANGED
0x4e85c5 SignalEvent 218 PARTY_MEMBERS_CHANGED
0x4e86b6 SignalEvent2 220 PARTY_MEMBER_ENABLE %d
0x4e87fa SignalEvent 222 PARTY_LOOT_METHOD_CHANGED
0x4e8b0e SignalEvent 531 UPDATE_LFG_TYPES
0x4e8c70 SignalEvent 531 UPDATE_LFG_TYPES
0x4e8d46 SignalEvent 532 UPDATE_LFG_LIST
0x4e8f45 SignalEvent 532 UPDATE_LFG_LIST
0x4ee48d SignalEvent2 357 MINIMAP_PING %s%f%f
0x4f0dfb SignalEvent 393 UPDATE_MACROS
0x4f153c SignalEvent2 392 EXECUTE_CHAT_LINE %s
0x4f402c SignalEvent 391 PETITION_CLOSED
0x4f41b4 SignalEvent 390 PETITION_SHOW
0x4f430c SignalEvent 390 PETITION_SHOW
0x4f433f SignalEvent 390 PETITION_SHOW
0x4f43ad SignalEvent 390 PETITION_SHOW
0x4f5003 SignalEvent 378 GUILD_REGISTRAR_SHOW
0x4f503a SignalEvent 379 GUILD_REGISTRAR_CLOSED
0x4f588d SignalEvent 374 OPEN_TABARD_FRAME
0x4f58f8 SignalEvent 375 CLOSE_TABARD_FRAME
0x4f5fef SignalEvent 361 CRAFT_CLOSE
0x4f6017 SignalEvent 361 CRAFT_CLOSE
0x4f6040 SignalEvent 359 CRAFT_SHOW
0x4f661a SignalEvent 360 CRAFT_UPDATE
0x4f6bf0 SignalEvent 360 CRAFT_UPDATE
0x4f8495 SignalEvent 350 PLAYERBANKBAGSLOTS_CHANGED
0x4f84df SignalEvent 349 BANKFRAME_CLOSED
0x4f8522 SignalEvent 348 BANKFRAME_OPENED
0x4f8e8a SignalEvent2 328 BAG_UPDATE %d
0x4f92b5 SignalEvent2 329 BAG_CLOSED %d
0x4f9343 SignalEvent2 328 BAG_UPDATE %d
0x4f93bf SignalEvent2 328 BAG_UPDATE %d
0x4f93d5 SignalEvent 330 BAG_UPDATE_COOLDOWN
0x4f9434 SignalEvent2 327 BAG_OPEN %d
0x4f9469 SignalEvent2 327 BAG_OPEN %d
0x4facaa SignalEvent 317 MERCHANT_UPDATE
0x4fad92 SignalEvent 316 MERCHANT_SHOW
0x4fae2c SignalEvent 318 MERCHANT_CLOSED
0x4faea5 SignalEvent 317 MERCHANT_UPDATE
0x4faebd SignalEvent 317 MERCHANT_UPDATE
0x4fb115 SignalEvent 317 MERCHANT_UPDATE
0x4fb575 SignalEvent 317 MERCHANT_UPDATE
0x4fc884 SignalEvent 315 TRADE_SKILL_CLOSE
0x4fc930 SignalEvent 315 TRADE_SKILL_CLOSE
0x4fc99f SignalEvent 313 TRADE_SKILL_SHOW
0x4fd164 SignalEvent 314 TRADE_SKILL_UPDATE
0x4fd725 SignalEvent 314 TRADE_SKILL_UPDATE
0x4fd745 SignalEvent 314 TRADE_SKILL_UPDATE
0x4fd765 SignalEvent 314 TRADE_SKILL_UPDATE
0x4fd781 SignalEvent 502 UPDATE_TRADESKILL_RECAST
0x4fd7a8 SignalEvent 502 UPDATE_TRADESKILL_RECAST
0x4fd7e1 SignalEvent 502 UPDATE_TRADESKILL_RECAST
0x500ee5 SignalEvent 300 QUEST_GREETING
0x50105d SignalEvent 303 QUEST_COMPLETE
0x501120 SignalEvent 302 QUEST_PROGRESS
0x501217 SignalEvent 304 QUEST_FINISHED
0x501903 SignalEvent 305 QUEST_ITEM_UPDATE
0x5019fe SignalEvent2 346 QUEST_ACCEPT_CONFIRM %s%s
0x515e7a SignalEvent2 ? %s
0x51f5ad SignalEvent2 429 ADDON_LOADED %s
0x5aae97 SignalEvent 278 LOGOUT_CANCEL
0x5aaf17 SignalEvent ?
0x5aaf72 SignalEvent 278 LOGOUT_CANCEL
0x5ab2f5 SignalEvent 6
0x5ad9f2 SignalEvent 388 WHO_LIST_UPDATE
0x5ae141 SignalEvent 388 WHO_LIST_UPDATE
0x5ae867 SignalEvent 351 FRIENDLIST_UPDATE
0x5ae92d SignalEvent 352 IGNORELIST_UPDATE
0x5d48a8 SignalEvent 200 PLAYER_MONEY
0x5d859a SignalEvent 188 ITEM_LOCK_CHANGED
0x5d9222 SignalEvent 290 USE_BIND_CONFIRM
0x5d94b9 SignalEvent 188 ITEM_LOCK_CHANGED
0x5dd15e SignalEvent2 545 QUEST_WATCH_UPDATE %d
0x5ddd6e SignalEvent 347 PLAYERBANKSLOTS_CHANGED
0x5ddf4b SignalEvent 200 PLAYER_MONEY
0x5ddffa SignalEvent 202 PLAYER_COMBO_POINTS
0x5de14e SignalEvent 198 PLAYER_FARSIGHT_FOCUS_CHANGED
0x5de167 SignalEvent 198 PLAYER_FARSIGHT_FOCUS_CHANGED
0x5de4c1 SignalEvent2 ? %s
0x5de4e8 SignalEvent 406 UPDATE_EXHAUSTION
0x5dea2a SignalEvent 401 PLAYER_UNGHOST
0x5deb20 SignalEvent2 213 ACTIONBAR_SLOT_CHANGED %d
0x5deecb SignalEvent2 162 UNIT_RANGED_ATTACK_POWER %s
0x5df837 SignalEvent2 396 CONFIRM_XP_LOSS %d
0x5dfa3f SignalEvent2 512 CONFIRM_TALENT_WIPE %d
0x5dfc5f SignalEvent2 529 CONFIRM_PET_UNLEARN %d
0x5dfec3 SignalEvent2 513 CONFIRM_BINDER %s
0x5e05fc SignalEvent 521 TABARD_SAVE_PENDING
0x5e0907 SignalEvent 376 TABARD_CANSAVE_CHANGED
0x5e0ee4 SignalEvent2 288 EQUIP_BIND_CONFIRM %d
0x5e16c6 SignalEvent2 289 AUTOEQUIP_BIND_CONFIRM %d
0x5e27b5 SignalEvent 345 PLAYER_GUILD_UPDATE
0x5e2b38 SignalEvent2 28 UNIT_LEVEL %s
0x5e3b20 SignalEvent 267 LOOT_OPENED
0x5e413e SignalEvent2 199 PLAYER_LEVEL_UP %d%d%d%d%d%d%d%d%d
0x5e4527 SignalEvent2 394 UPDATE_TICKET %d%s%f%f%f%d%d
0x5e45e2 SignalEvent2 394 UPDATE_TICKET %d%s%f%f%f%d%d
0x5e461d SignalEvent2 394 UPDATE_TICKET %d%s
0x5e4689 SignalEvent2 400 UPDATE_GM_STATUS %d
0x5e5e7b SignalEvent2 545 QUEST_WATCH_UPDATE %d
0x5e6c66 SignalEvent 218 PARTY_MEMBERS_CHANGED
0x5e7172 SignalEvent 521 TABARD_SAVE_PENDING
0x5e728d SignalEvent2 284 GUILD_MOTD %s
0x5e7960 SignalEvent2 394 UPDATE_TICKET %d%s%f%f%f%d%d
0x5e797b SignalEvent 538 GMSURVEY_DISPLAY
0x5e79cc SignalEvent2 364 MIRROR_TIMER_STOP %s
0x5e7a11 SignalEvent2 363 MIRROR_TIMER_PAUSE %s%d
0x5e7a9a SignalEvent2 362 MIRROR_TIMER_START %s%d%d%d%d%s
0x5e9b78 SignalEvent2 221 PARTY_MEMBER_DISABLE %d
0x5e9ba5 SignalEvent2 364 MIRROR_TIMER_STOP %s
0x5ec68b SignalEvent 296 ITEM_TEXT_CLOSED
0x5ec6c5 SignalEvent2 294 ITEM_TEXT_TRANSLATION %f
0x5ed97f SignalEvent2 332 UNIT_ATTACK %s
0x5ed99f SignalEvent2 332 UNIT_ATTACK %s
0x5ed9b3 SignalEvent2 333 UNIT_DEFENSE %s
0x5ed9c7 SignalEvent2 162 UNIT_RANGED_ATTACK_POWER %s
0x5eeaf7 SignalEvent 405 PLAYER_UPDATE_RESTING
0x5eeb6f SignalEvent 530 PLAYTIME_CHANGED
0x5ef037 SignalEvent 391 PETITION_CLOSED
0x5ef16b SignalEvent 379 GUILD_REGISTRAR_CLOSED
0x5ef29c SignalEvent 390 PETITION_SHOW
0x5fefb8 SignalEvent 274 PLAYER_ALIVE
0x5ff533 SignalEvent 191 PLAYER_REGEN_ENABLED
0x5ff79f SignalEvent 335 PET_ATTACK_STOP
0x604b17 SignalEvent 331 LOCALPLAYER_PET_RENAMED
0x604c1b SignalEvent 427 PET_UI_UPDATE
0x60fb84 SignalEvent 368 AUTOFOLLOW_END
0x610071 SignalEvent2 367 AUTOFOLLOW_BEGIN %s
0x61b2c7 SignalEvent2 506 CANCEL_LOOT_ROLL %d
0x61b452 SignalEvent2 505 START_LOOT_ROLL %d%d
0x61b6f3 SignalEvent2 506 CANCEL_LOOT_ROLL %d
0x61b7ac SignalEvent2 505 START_LOOT_ROLL %d%d
0x61bc12 SignalEvent2 506 CANCEL_LOOT_ROLL %d
0x61be8b SignalEvent2 507 CONFIRM_LOOT_ROLL %d%d
0x61bf2a SignalEvent2 506 CANCEL_LOOT_ROLL %d
0x6256ff SignalEvent 193 PLAYER_ENTER_COMBAT
0x625778 SignalEvent 194 PLAYER_LEAVE_COMBAT
0x6268d3 SignalEvent2 542 COMBAT_TEXT_UPDATE %s%s%s
0x62694d SignalEvent2 542 COMBAT_TEXT_UPDATE %s%s%s
0x62699c SignalEvent2 542 COMBAT_TEXT_UPDATE %s%s
0x6269c3 SignalEvent2 542 COMBAT_TEXT_UPDATE %s%s
0x6269e2 SignalEvent2 542 COMBAT_TEXT_UPDATE %s
0x6d902a SignalEvent 207 MINIMAP_UPDATE_ZOOM
0x6d95b8 SignalEvent 207 MINIMAP_UPDATE_ZOOM
0x6e224a SignalEvent 340 SPELLCAST_INTERRUPTED
0x6e4a50 SignalEvent 338 SPELLCAST_STOP
0x6e5952 SignalEvent 435 START_AUTOREPEAT_SPELL
0x6e70c3 SignalEvent 528 AREA_SPIRIT_HEALER_OUT_OF_RANGE
0x6e74d0 SignalEvent 338 SPELLCAST_STOP
0x6e7532 SignalEvent2 341 SPELLCAST_DELAYED %d
0x6e75d0 SignalEvent2 342 SPELLCAST_CHANNEL_START %d%s
0x6e7611 SignalEvent2 343 SPELLCAST_CHANNEL_UPDATE %d
0x6e7627 SignalEvent 344 SPELLCAST_CHANNEL_STOP
0x6e7a53 SignalEvent2 337 SPELLCAST_START %s%d
0x6ea170 SignalEvent 436 STOP_AUTOREPEAT_SPELL
Can't render this file because it contains an unexpected character in line 3 and column 38.
+470
View File
@@ -0,0 +1,470 @@
# build 5875 CGGameUI message catalog (the 0xb4b498 table indexed by 0x496720)
# 465 rows, ids 0x0..0x1d0; consumer bound `cmp id,0x1d1; jge` at 0x49672c
# kind 0 -> 0x49a870(text, chan); 1 -> UI_INFO_MESSAGE 0xe1; 2 -> UI_ERROR_MESSAGE 0xe0; 3 -> console 0x63cd00
# regenerate (in wow-5875-re): python3 re/ui/extract-message-catalog.py <WoW.exe> > re/ui/message-catalog.tsv
id_hex id_dec row_va key kind sound type_tag chan
0x0 0 0xb4b498 ERR_INV_FULL 2 NONE 0x0 10
0x1 1 0xb4b4ac ERR_BANK_FULL 2 NONE 0x44 10
0x2 2 0xb4b4c0 ERR_CANT_EQUIP_LEVEL_I 2 NONE 0x2 10
0x3 3 0xb4b4d4 ERR_CANT_EQUIP_SKILL 2 NONE 0x29 10
0x4 4 0xb4b4e8 ERR_CANT_EQUIP_EVER 2 NONE 0x3 10
0x5 5 0xb4b4fc ERR_CANT_EQUIP_RANK 2 NONE 0x44 10
0x6 6 0xb4b510 ERR_CANT_EQUIP_REPUTATION 2 NONE 0x44 10
0x7 7 0xb4b524 ERR_PROFICIENCY_NEEDED 2 NONE 0x30 10
0x8 8 0xb4b538 ERR_WRONG_SLOT 2 NONE 0x1b 10
0x9 9 0xb4b54c ERR_BAG_FULL 2 NONE 0x1d 10
0xa 10 0xb4b560 ERR_INTERNAL_BAG_ERROR 2 NONE 0x44 10
0xb 11 0xb4b574 ERR_DESTROY_NONEMPTY_BAG 2 NONE 0x44 10
0xc 12 0xb4b588 ERR_BAG_IN_BAG 2 NONE 0x1a 10
0xd 13 0xb4b59c ERR_TOO_MANY_SPECIAL_BAGS 2 NONE 0x44 10
0xe 14 0xb4b5b0 ERR_TRADE_EQUIPPED_BAG 2 NONE 0x44 10
0xf 15 0xb4b5c4 ERR_AMMO_ONLY 2 NONE 0x1c 10
0x10 16 0xb4b5d8 ERR_NO_SLOT_AVAILABLE 2 NONE 0x44 10
0x11 17 0xb4b5ec ERR_WRONG_BAG_TYPE 2 NONE 0x44 10
0x12 18 0xb4b600 ERR_ITEM_MAX_COUNT 2 NONE 0x1e 10
0x13 19 0xb4b614 ERR_NOT_EQUIPPABLE 2 NONE 0x2c 10
0x14 20 0xb4b628 ERR_CANT_STACK 2 NONE 0x44 10
0x15 21 0xb4b63c ERR_CANT_SWAP 2 NONE 0x44 10
0x16 22 0xb4b650 ERR_SLOT_EMPTY 2 NONE 0x44 10
0x17 23 0xb4b664 ERR_ITEM_NOT_FOUND 2 NONE 0x44 10
0x18 24 0xb4b678 ERR_TOO_FEW_TO_SPLIT 2 NONE 0x44 10
0x19 25 0xb4b68c ERR_SPLIT_FAILED 2 NONE 0x44 10
0x1a 26 0xb4b6a0 ERR_NOT_A_BAG 2 NONE 0x19 10
0x1b 27 0xb4b6b4 ERR_NOT_OWNER 2 NONE 0x44 10
0x1c 28 0xb4b6c8 ERR_ONLY_ONE_QUIVER 2 NONE 0x44 10
0x1d 29 0xb4b6dc ERR_NO_BANK_SLOT 2 NONE 0x44 10
0x1e 30 0xb4b6f0 ERR_NO_BANK_HERE 2 NONE 0x44 10
0x1f 31 0xb4b704 ERR_ITEM_LOCKED 2 NONE 0x3d 10
0x20 32 0xb4b718 ERR_2HANDED_EQUIPPED 2 NONE 0x2a 10
0x21 33 0xb4b72c ERR_VENDOR_NOT_INTERESTED 2 NONE 0x44 10
0x22 34 0xb4b740 ERR_VENDOR_HATES_YOU 2 NONE 0x44 10
0x23 35 0xb4b754 ERR_VENDOR_SOLD_OUT 2 NONE 0x44 10
0x24 36 0xb4b768 ERR_VENDOR_TOO_FAR 2 NONE 0x44 10
0x25 37 0xb4b77c ERR_NOT_ENOUGH_MONEY 2 NONE 0x28 10
0x26 38 0xb4b790 ERR_RECEIVE_ITEM_S 0 ITEMGENERICSOUND 0x44 10
0x27 39 0xb4b7a4 ERR_DROP_BOUND_ITEM 2 NONE 0x4 10
0x28 40 0xb4b7b8 ERR_TRADE_BOUND_ITEM 2 NONE 0x3b 10
0x29 41 0xb4b7cc ERR_TRADE_QUEST_ITEM 2 NONE 0x3b 10
0x2a 42 0xb4b7e0 ERR_TRADE_GROUND_ITEM 2 NONE 0x44 10
0x2b 43 0xb4b7f4 ERR_TRADE_BAG 2 NONE 0x44 10
0x2c 44 0xb4b808 ERR_SPELL_FAILED_S 2 NONE 0x44 10
0x2d 45 0xb4b81c ERR_ITEM_COOLDOWN 2 NONE 0x5 10
0x2e 46 0xb4b830 ERR_POTION_COOLDOWN 2 NONE 0x2f 10
0x2f 47 0xb4b844 ERR_FOOD_COOLDOWN 2 NONE 0x7 10
0x30 48 0xb4b858 ERR_SPELL_COOLDOWN 2 NONE 0xc 10
0x31 49 0xb4b86c ERR_ABILITY_COOLDOWN 2 NONE 0x32 10
0x32 50 0xb4b880 ERR_SPELL_ALREADY_KNOWN_S 2 NONE 0xd 10
0x33 51 0xb4b894 ERR_PET_SPELL_ALREADY_KNOWN_S 2 NONE 0x44 10
0x34 52 0xb4b8a8 ERR_PROFICIENCY_GAINED_S 0 NONE 0x44 23
0x35 53 0xb4b8bc ERR_SKILL_GAINED_S 0 NONE 0x44 23
0x36 54 0xb4b8d0 ERR_SKILL_UP_SI 0 NONE 0x44 23
0x37 55 0xb4b8e4 ERR_LEARN_SPELL_S 0 NONE 0x44 10
0x38 56 0xb4b8f8 ERR_LEARN_ABILITY_S 0 NONE 0x44 10
0x39 57 0xb4b90c ERR_LEARN_RECIPE_S 0 NONE 0x44 10
0x3a 58 0xb4b920 ERR_INVITE_PLAYER_S 0 NONE 0x44 10
0x3b 59 0xb4b934 ERR_INVITE_SELF 2 NONE 0x44 10
0x3c 60 0xb4b948 ERR_INVITED_TO_GROUP_S 0 NONE 0x44 10
0x3d 61 0xb4b95c ERR_ALREADY_IN_GROUP_S 0 NONE 0x14 10
0x3e 62 0xb4b970 ERR_PLAYER_BUSY_S 0 NONE 0x44 10
0x3f 63 0xb4b984 ERR_NEW_LEADER_S 0 NONE 0x44 10
0x40 64 0xb4b998 ERR_NEW_LEADER_YOU 0 NONE 0x44 10
0x41 65 0xb4b9ac ERR_LEFT_GROUP_S 0 NONE 0x44 10
0x42 66 0xb4b9c0 ERR_LEFT_GROUP_YOU 0 NONE 0x44 10
0x43 67 0xb4b9d4 ERR_GROUP_DISBANDED 0 NONE 0x44 10
0x44 68 0xb4b9e8 ERR_DECLINE_GROUP_S 0 igPlayerInviteDecline 0x44 10
0x45 69 0xb4b9fc ERR_JOINED_GROUP_S 0 NONE 0x44 10
0x46 70 0xb4ba10 ERR_UNINVITE_YOU 0 NONE 0x44 10
0x47 71 0xb4ba24 ERR_BAD_PLAYER_NAME_S 0 NONE 0x44 10
0x48 72 0xb4ba38 ERR_NOT_IN_GROUP 0 NONE 0x44 10
0x49 73 0xb4ba4c ERR_TARGET_NOT_IN_GROUP_S 0 NONE 0x44 10
0x4a 74 0xb4ba60 ERR_GROUP_FULL 0 NONE 0x8 10
0x4b 75 0xb4ba74 ERR_NOT_LEADER 0 NONE 0x44 10
0x4c 76 0xb4ba88 ERR_PLAYER_DIED_S 0 NONE 0x44 10
0x4d 77 0xb4ba9c ERR_GUILD_CREATE_S 0 LEVELUP 0x44 10
0x4e 78 0xb4bab0 ERR_GUILD_INVITE_S 0 NONE 0x44 10
0x4f 79 0xb4bac4 ERR_INVITED_TO_GUILD_SS 0 LEVELUP 0x44 10
0x50 80 0xb4bad8 ERR_ALREADY_IN_GUILD_S 0 NONE 0x44 10
0x51 81 0xb4baec ERR_ALREADY_INVITED_TO_GUILD_S 0 NONE 0x44 10
0x52 82 0xb4bb00 ERR_INVITED_TO_GUILD 0 NONE 0x44 10
0x53 83 0xb4bb14 ERR_ALREADY_IN_GUILD 0 NONE 0x44 10
0x54 84 0xb4bb28 ERR_GUILD_ACCEPT 0 NONE 0x44 10
0x55 85 0xb4bb3c ERR_GUILD_DECLINE_S 0 NONE 0x44 10
0x56 86 0xb4bb50 ERR_GUILD_PERMISSIONS 0 NONE 0x3e 10
0x57 87 0xb4bb64 ERR_GUILD_JOIN_S 0 NONE 0x44 10
0x58 88 0xb4bb78 ERR_GUILD_FOUNDER_S 0 NONE 0x44 10
0x59 89 0xb4bb8c ERR_GUILD_PROMOTE_SSS 0 NONE 0x44 10
0x5a 90 0xb4bba0 ERR_GUILD_DEMOTE_SSS 0 NONE 0x44 10
0x5b 91 0xb4bbb4 ERR_GUILD_QUIT_S 0 NONE 0x44 10
0x5c 92 0xb4bbc8 ERR_GUILD_LEAVE_S 0 NONE 0x44 10
0x5d 93 0xb4bbdc ERR_GUILD_REMOVE_SS 0 NONE 0x44 10
0x5e 94 0xb4bbf0 ERR_GUILD_REMOVE_SELF 0 NONE 0x44 10
0x5f 95 0xb4bc04 ERR_GUILD_DISBAND_S 0 NONE 0x44 10
0x60 96 0xb4bc18 ERR_GUILD_DISBAND_SELF 0 NONE 0x44 10
0x61 97 0xb4bc2c ERR_GUILD_LEADER_S 0 NONE 0x44 10
0x62 98 0xb4bc40 ERR_GUILD_LEADER_SELF 0 NONE 0x44 10
0x63 99 0xb4bc54 ERR_GUILD_PLAYER_NOT_FOUND_S 0 NONE 0x44 10
0x64 100 0xb4bc68 ERR_GUILD_PLAYER_NOT_IN_GUILD_S 0 NONE 0x44 10
0x65 101 0xb4bc7c ERR_GUILD_PLAYER_NOT_IN_GUILD 0 NONE 0x44 10
0x66 102 0xb4bc90 ERR_GUILD_CANT_PROMOTE_S 0 NONE 0x44 10
0x67 103 0xb4bca4 ERR_GUILD_CANT_DEMOTE_S 0 NONE 0x44 10
0x68 104 0xb4bcb8 ERR_GUILD_NOT_IN_A_GUILD 0 NONE 0x44 10
0x69 105 0xb4bccc ERR_GUILD_INTERNAL 0 NONE 0x44 10
0x6a 106 0xb4bce0 ERR_GUILD_LEADER_IS_S 0 NONE 0x44 10
0x6b 107 0xb4bcf4 ERR_GUILD_LEADER_CHANGED_SS 0 NONE 0x44 10
0x6c 108 0xb4bd08 ERR_GUILD_DISBANDED 0 NONE 0x44 10
0x6d 109 0xb4bd1c ERR_GUILD_NOT_ALLIED 0 NONE 0x44 10
0x6e 110 0xb4bd30 ERR_GUILD_LEADER_LEAVE 0 NONE 0x44 10
0x6f 111 0xb4bd44 ERR_GUILD_RANKS_LOCKED 0 NONE 0x44 10
0x70 112 0xb4bd58 ERR_GUILD_RANK_IN_USE 0 NONE 0x44 10
0x71 113 0xb4bd6c ERR_GUILD_RANK_TOO_HIGH_S 0 NONE 0x44 10
0x72 114 0xb4bd80 ERR_GUILD_RANK_TOO_LOW_S 0 NONE 0x44 10
0x73 115 0xb4bd94 ERR_GUILD_NAME_INVALID 2 NONE 0x44 10
0x74 116 0xb4bda8 ERR_GUILD_NAME_EXISTS_S 2 NONE 0x44 10
0x75 117 0xb4bdbc ERR_GUILD_ENTER_NAME 2 NONE 0x44 10
0x76 118 0xb4bdd0 ERR_GUILD_NAME_TOO_SHORT 2 NONE 0x44 10
0x77 119 0xb4bde4 ERR_GUILD_NAME_MIXED_LANGUAGES 2 NONE 0x44 10
0x78 120 0xb4bdf8 ERR_GUILD_NAME_PROFANE 2 NONE 0x44 10
0x79 121 0xb4be0c ERR_GUILD_NAME_RESERVED 2 NONE 0x44 10
0x7a 122 0xb4be20 ERR_GUILD_NAME_INVALID_SPACE 2 NONE 0x44 10
0x7b 123 0xb4be34 ERR_GUILD_NAME_NAME_CONSECUTIVE_SPACES 2 NONE 0x44 10
0x7c 124 0xb4be48 ERR_NO_GUILD_CHARTER 2 NONE 0x44 10
0x7d 125 0xb4be5c ERR_OUT_OF_RANGE 2 NONE 0xa 10
0x7e 126 0xb4be70 ERR_PLAYER_DEAD 2 NONE 0x44 10
0x7f 127 0xb4be84 ERR_CLIENT_LOCKED_OUT 2 NONE 0x44 10
0x80 128 0xb4be98 ERR_KILLED_BY_S 0 NONE 0x44 10
0x81 129 0xb4beac ERR_LOOT_LOCKED 2 NONE 0x21 10
0x82 130 0xb4bec0 ERR_LOOT_TOO_FAR 2 NONE 0x23 10
0x83 131 0xb4bed4 ERR_LOOT_DIDNT_KILL 2 NONE 0x1f 10
0x84 132 0xb4bee8 ERR_LOOT_BAD_FACING 2 NONE 0x20 10
0x85 133 0xb4befc ERR_LOOT_NOTSTANDING 2 NONE 0x44 10
0x86 134 0xb4bf10 ERR_LOOT_STUNNED 2 NONE 0x44 10
0x87 135 0xb4bf24 ERR_LOOT_NO_UI 2 NONE 0x44 10
0x88 136 0xb4bf38 ERR_LOOT_WHILE_INVULNERABLE 2 NONE 0x44 10
0x89 137 0xb4bf4c ERR_QUEST_ACCEPTED_S 0 QUESTADDED 0x44 10
0x8a 138 0xb4bf60 ERR_QUEST_COMPLETE_S 0 igQuestListComplete 0x44 10
0x8b 139 0xb4bf74 ERR_QUEST_FAILED_S 0 igQuestFailed 0x44 10
0x8c 140 0xb4bf88 ERR_QUEST_FAILED_BAG_FULL_S 0 igQuestFailed 0x44 10
0x8d 141 0xb4bf9c ERR_QUEST_FAILED_MAX_COUNT_S 0 igQuestFailed 0x44 10
0x8e 142 0xb4bfb0 ERR_QUEST_FAILED_LOW_LEVEL 0 igQuestFailed 0x44 10
0x8f 143 0xb4bfc4 ERR_QUEST_FAILED_MISSING_ITEMS 0 igQuestFailed 0x44 10
0x90 144 0xb4bfd8 ERR_QUEST_FAILED_WRONG_RACE 0 igQuestFailed 0x44 10
0x91 145 0xb4bfec ERR_QUEST_FAILED_NOT_ENOUGH_MONEY 0 igQuestFailed 0x44 10
0x92 146 0xb4c000 ERR_QUEST_ONLY_ONE_TIMED 0 igQuestFailed 0x44 10
0x93 147 0xb4c014 ERR_QUEST_NEED_PREREQS 0 igQuestFailed 0x44 10
0x94 148 0xb4c028 ERR_QUEST_ALREADY_ON 0 igQuestFailed 0x44 10
0x95 149 0xb4c03c ERR_QUEST_REWARD_EXP_I 0 NONE 0x44 10
0x96 150 0xb4c050 ERR_QUEST_REWARD_ITEM_S 0 NONE 0x44 10
0x97 151 0xb4c064 ERR_QUEST_REWARD_MONEY_S 0 NONE 0x44 10
0x98 152 0xb4c078 ERR_QUEST_MUST_CHOOSE 2 igQuestFailed 0x44 10
0x99 153 0xb4c08c ERR_QUEST_LOG_FULL 2 igQuestFailed 0x44 10
0x9a 154 0xb4c0a0 ERR_COMBAT_DAMAGE_SSI 0 NONE 0x44 10
0x9b 155 0xb4c0b4 ERR_INSPECT_S 0 NONE 0x44 10
0x9c 156 0xb4c0c8 ERR_CANT_USE_ITEM 2 NONE 0x33 10
0x9d 157 0xb4c0dc ERR_MUST_EQUIP_ITEM 2 NONE 0x31 10
0x9e 158 0xb4c0f0 ERR_PASSIVE_ABILITY 2 NONE 0x44 10
0x9f 159 0xb4c104 ERR_2HSKILLNOTFOUND 2 NONE 0x44 10
0xa0 160 0xb4c118 ERR_NO_ATTACK_TARGET 2 NONE 0x26 10
0xa1 161 0xb4c12c ERR_INVALID_ATTACK_TARGET 2 NONE 0xb 10
0xa2 162 0xb4c140 ERR_ATTACK_STUNNED 2 NONE 0x44 10
0xa3 163 0xb4c154 ERR_ATTACK_PACIFIED 2 NONE 0x44 10
0xa4 164 0xb4c168 ERR_ATTACK_MOUNTED 2 NONE 0x44 10
0xa5 165 0xb4c17c ERR_ATTACK_FLEEING 2 NONE 0x44 10
0xa6 166 0xb4c190 ERR_ATTACK_CONFUSED 2 NONE 0x44 10
0xa7 167 0xb4c1a4 ERR_ATTACK_CHARMED 2 NONE 0x44 10
0xa8 168 0xb4c1b8 ERR_ATTACK_DEAD 2 NONE 0x44 10
0xa9 169 0xb4c1cc ERR_ATTACK_PREVENTED_BY_MECHANIC_S 2 NONE 0x44 10
0xaa 170 0xb4c1e0 ERR_TAXISAMENODE 2 NONE 0x44 10
0xab 171 0xb4c1f4 ERR_TAXINOSUCHPATH 1 NONE 0x44 10
0xac 172 0xb4c208 ERR_TAXIUNSPECIFIEDSERVERERROR 2 NONE 0x44 10
0xad 173 0xb4c21c ERR_TAXINOTENOUGHMONEY 1 NONE 0x36 10
0xae 174 0xb4c230 ERR_TAXITOOFARAWAY 1 NONE 0x44 10
0xaf 175 0xb4c244 ERR_TAXINOVENDORNEARBY 1 NONE 0x44 10
0xb0 176 0xb4c258 ERR_TAXINOTVISITED 1 NONE 0x44 10
0xb1 177 0xb4c26c ERR_TAXIPLAYERBUSY 2 NONE 0x44 10
0xb2 178 0xb4c280 ERR_TAXIPLAYERALREADYMOUNTED 2 NONE 0x44 10
0xb3 179 0xb4c294 ERR_TAXIPLAYERSHAPESHIFTED 2 NONE 0x44 10
0xb4 180 0xb4c2a8 ERR_TAXIPLAYERMOVING 1 NONE 0x44 10
0xb5 181 0xb4c2bc ERR_TAXINOPATHS 2 NONE 0x44 10
0xb6 182 0xb4c2d0 ERR_TAXINOTSTANDING 1 NONE 0x44 10
0xb7 183 0xb4c2e4 ERR_NO_REPLY_TARGET 0 NONE 0x44 10
0xb8 184 0xb4c2f8 ERR_GENERIC_NO_TARGET 2 NONE 0x2d 10
0xb9 185 0xb4c30c ERR_INITIATE_TRADE_S 0 NONE 0x44 10
0xba 186 0xb4c320 ERR_TRADE_REQUEST_S 0 LEVELUP 0x44 10
0xbb 187 0xb4c334 ERR_TRADE_BLOCKED_S 0 NONE 0x44 10
0xbc 188 0xb4c348 ERR_TRADE_TARGET_DEAD 2 NONE 0x44 10
0xbd 189 0xb4c35c ERR_TRADE_TOO_FAR 2 NONE 0x44 10
0xbe 190 0xb4c370 ERR_TRADE_CANCELLED 1 NONE 0x44 10
0xbf 191 0xb4c384 ERR_TRADE_COMPLETE 1 NONE 0x44 10
0xc0 192 0xb4c398 ERR_TRADE_BAG_FULL 2 NONE 0x44 10
0xc1 193 0xb4c3ac ERR_TRADE_TARGET_BAG_FULL 2 NONE 0x44 10
0xc2 194 0xb4c3c0 ERR_TRADE_MAX_COUNT_EXCEEDED 2 NONE 0x44 10
0xc3 195 0xb4c3d4 ERR_TRADE_TARGET_MAX_COUNT_EXCEEDED 2 NONE 0x44 10
0xc4 196 0xb4c3e8 ERR_ALREADY_TRADING 2 NONE 0x44 10
0xc5 197 0xb4c3fc ERR_MOUNT_INVALIDMOUNTEE 2 NONE 0x44 10
0xc6 198 0xb4c410 ERR_MOUNT_TOOFARAWAY 2 NONE 0x44 10
0xc7 199 0xb4c424 ERR_MOUNT_ALREADYMOUNTED 2 NONE 0x44 10
0xc8 200 0xb4c438 ERR_MOUNT_NOTMOUNTABLE 2 NONE 0x44 10
0xc9 201 0xb4c44c ERR_MOUNT_NOTYOURPET 2 NONE 0x44 10
0xca 202 0xb4c460 ERR_MOUNT_OTHER 2 NONE 0x44 10
0xcb 203 0xb4c474 ERR_MOUNT_LOOTING 2 NONE 0x44 10
0xcc 204 0xb4c488 ERR_MOUNT_RACECANTMOUNT 2 NONE 0x44 10
0xcd 205 0xb4c49c ERR_MOUNT_SHAPESHIFTED 2 NONE 0x44 10
0xce 206 0xb4c4b0 ERR_DISMOUNT_NOPET 2 NONE 0x44 10
0xcf 207 0xb4c4c4 ERR_DISMOUNT_NOTMOUNTED 2 NONE 0x44 10
0xd0 208 0xb4c4d8 ERR_DISMOUNT_NOTYOURPET 2 NONE 0x44 10
0xd1 209 0xb4c4ec ERR_SPELL_FAILED_TOTEMS 2 NONE 0x44 10
0xd2 210 0xb4c500 ERR_SPELL_FAILED_REAGENTS 2 NONE 0x44 10
0xd3 211 0xb4c514 ERR_SPELL_FAILED_REAGENTS_GENERIC 2 NONE 0x44 10
0xd4 212 0xb4c528 ERR_SPELL_FAILED_EQUIPPED_ITEM 2 NONE 0x44 10
0xd5 213 0xb4c53c ERR_SPELL_FAILED_EQUIPPED_ITEM_CLASS_S 2 NONE 0x44 10
0xd6 214 0xb4c550 ERR_SPELL_FAILED_SHAPESHIFT_FORM_S 2 NONE 0x44 10
0xd7 215 0xb4c564 ERR_BADATTACKFACING 2 NONE 0x44 10
0xd8 216 0xb4c578 ERR_BADATTACKPOS 2 NONE 0x44 10
0xd9 217 0xb4c58c ERR_CHEST_IN_USE 2 NONE 0x34 10
0xda 218 0xb4c5a0 ERR_USE_CANT_OPEN 2 NONE 0x44 10
0xdb 219 0xb4c5b4 ERR_USE_LOCKED 2 NONE 0x3d 10
0xdc 220 0xb4c5c8 ERR_DOOR_LOCKED 2 NONE 0x3d 10
0xdd 221 0xb4c5dc ERR_BUTTON_LOCKED 2 NONE 0x3d 10
0xde 222 0xb4c5f0 ERR_USE_LOCKED_WITH_ITEM_S 2 NONE 0x44 10
0xdf 223 0xb4c604 ERR_USE_LOCKED_WITH_SPELL_S 2 NONE 0x44 10
0xe0 224 0xb4c618 ERR_USE_LOCKED_WITH_SPELL_KNOWN_SI 2 NONE 0x44 10
0xe1 225 0xb4c62c ERR_USE_TOO_FAR 2 NONE 0x39 10
0xe2 226 0xb4c640 ERR_USE_BAD_ANGLE 2 NONE 0x44 10
0xe3 227 0xb4c654 ERR_USE_OBJECT_MOVING 2 NONE 0x44 10
0xe4 228 0xb4c668 ERR_USE_SPELL_FOCUS 1 NONE 0x44 10
0xe5 229 0xb4c67c ERR_USE_DESTROYED 2 NONE 0x44 10
0xe6 230 0xb4c690 ERR_SET_LOOT_FREEFORALL 0 NONE 0x44 10
0xe7 231 0xb4c6a4 ERR_SET_LOOT_ROUNDROBIN 0 NONE 0x44 10
0xe8 232 0xb4c6b8 ERR_SET_LOOT_MASTER 0 NONE 0x44 10
0xe9 233 0xb4c6cc ERR_SET_LOOT_GROUP 0 NONE 0x44 10
0xea 234 0xb4c6e0 ERR_SET_LOOT_NBG 0 NONE 0x44 10
0xeb 235 0xb4c6f4 ERR_SET_LOOT_THRESHOLD_S 0 NONE 0x44 10
0xec 236 0xb4c708 ERR_NEW_LOOT_MASTER_S 0 NONE 0x44 10
0xed 237 0xb4c71c ERR_SPECIFY_MASTER_LOOTER 2 NONE 0x44 10
0xee 238 0xb4c730 ERR_TAME_FAILED 2 NONE 0x44 10
0xef 239 0xb4c744 ERR_CHAT_WHILE_DEAD 2 NONE 0x44 10
0xf0 240 0xb4c758 ERR_CHAT_WRONG_FACTION 0 NONE 0x44 10
0xf1 241 0xb4c76c ERR_CHAT_PLAYER_NOT_FOUND_S 0 NONE 0x44 10
0xf2 242 0xb4c780 ERR_NEWTAXIPATH 1 TaxiNodeDiscovered 0x44 10
0xf3 243 0xb4c794 ERR_NO_PET 2 NONE 0x44 10
0xf4 244 0xb4c7a8 ERR_NOTYOURPET 2 NONE 0x44 10
0xf5 245 0xb4c7bc ERR_PET_NOT_RENAMEABLE 2 NONE 0x44 10
0xf6 246 0xb4c7d0 ERR_NULL_PETNAME 2 NONE 0x44 10
0xf7 247 0xb4c7e4 ERR_INVALID_PETNAME 2 NONE 0x44 10
0xf8 248 0xb4c7f8 ERR_QUEST_OBJECTIVE_COMPLETE_S 1 NONE 0x44 10
0xf9 249 0xb4c80c ERR_QUEST_UNKNOWN_COMPLETE 1 NONE 0x44 10
0xfa 250 0xb4c820 ERR_QUEST_ADD_KILL_SII 1 NONE 0x44 10
0xfb 251 0xb4c834 ERR_QUEST_ADD_FOUND_SII 1 NONE 0x44 10
0xfc 252 0xb4c848 ERR_QUEST_ADD_ITEM_SII 1 NONE 0x44 10
0xfd 253 0xb4c85c ERR_CANNOTCREATEDIRECTORY 2 NONE 0x44 10
0xfe 254 0xb4c870 ERR_CANNOTCREATEFILE 2 NONE 0x44 10
0xff 255 0xb4c884 ERR_PLAYER_WRONG_FACTION 2 NONE 0x44 10
0x100 256 0xb4c898 ERR_BANKSLOT_FAILED_TOO_MANY 2 NONE 0x44 10
0x101 257 0xb4c8ac ERR_BANKSLOT_INSUFFICIENT_FUNDS 2 NONE 0x16 10
0x102 258 0xb4c8c0 ERR_BANKSLOT_NOTBANKER 2 NONE 0x44 10
0x103 259 0xb4c8d4 ERR_FRIEND_DB_ERROR 0 NONE 0x44 10
0x104 260 0xb4c8e8 ERR_FRIEND_LIST_FULL 0 NONE 0x44 10
0x105 261 0xb4c8fc ERR_FRIEND_ADDED_S 0 NONE 0x44 10
0x106 262 0xb4c910 ERR_FRIEND_ONLINE_SS 0 FRIENDJOINGAME 0x44 10
0x107 263 0xb4c924 ERR_FRIEND_OFFLINE_S 0 NONE 0x44 10
0x108 264 0xb4c938 ERR_FRIEND_NOT_FOUND 0 NONE 0x44 10
0x109 265 0xb4c94c ERR_FRIEND_WRONG_FACTION 0 NONE 0x44 10
0x10a 266 0xb4c960 ERR_FRIEND_REMOVED_S 0 NONE 0x44 10
0x10b 267 0xb4c974 ERR_FRIEND_ERROR 0 NONE 0x44 10
0x10c 268 0xb4c988 ERR_FRIEND_ALREADY_S 0 NONE 0x44 10
0x10d 269 0xb4c99c ERR_FRIEND_SELF 0 NONE 0x44 10
0x10e 270 0xb4c9b0 ERR_IGNORE_FULL 0 NONE 0x44 10
0x10f 271 0xb4c9c4 ERR_IGNORE_SELF 0 NONE 0x44 10
0x110 272 0xb4c9d8 ERR_IGNORE_NOT_FOUND 0 NONE 0x44 10
0x111 273 0xb4c9ec ERR_IGNORE_ALREADY_S 0 NONE 0x44 10
0x112 274 0xb4ca00 ERR_IGNORE_ADDED_S 0 NONE 0x44 10
0x113 275 0xb4ca14 ERR_IGNORE_REMOVED_S 0 NONE 0x44 10
0x114 276 0xb4ca28 ERR_IGNORE_AMBIGUOUS 0 NONE 0x44 10
0x115 277 0xb4ca3c ERR_ONLY_ONE_BOLT 2 NONE 0x44 10
0x116 278 0xb4ca50 ERR_ONLY_ONE_AMMO 2 NONE 0x44 10
0x117 279 0xb4ca64 ERR_SPELL_FAILED_EQUIPPED_SPECIFIC_ITEM 2 NONE 0x44 10
0x118 280 0xb4ca78 ERR_WRONG_BAG_TYPE_SUBCLASS 2 NONE 0x1c 10
0x119 281 0xb4ca8c ERR_CANT_WRAP_STACKABLE 2 NONE 0x44 10
0x11a 282 0xb4caa0 ERR_CANT_WRAP_EQUIPPED 2 NONE 0x44 10
0x11b 283 0xb4cab4 ERR_CANT_WRAP_WRAPPED 2 NONE 0x44 10
0x11c 284 0xb4cac8 ERR_CANT_WRAP_BOUND 2 NONE 0x44 10
0x11d 285 0xb4cadc ERR_CANT_WRAP_UNIQUE 2 NONE 0x44 10
0x11e 286 0xb4caf0 ERR_CANT_WRAP_BAGS 2 NONE 0x44 10
0x11f 287 0xb4cb04 ERR_OUT_OF_MANA 2 NONE 0xf 10
0x120 288 0xb4cb18 ERR_OUT_OF_RAGE 2 NONE 0x3f 10
0x121 289 0xb4cb2c ERR_OUT_OF_FOCUS 2 NONE 0x44 10
0x122 290 0xb4cb40 ERR_OUT_OF_ENERGY 2 NONE 0x40 10
0x123 291 0xb4cb54 ERR_OUT_OF_HEALTH 2 NONE 0x44 10
0x124 292 0xb4cb68 ERR_LOOT_GONE 2 NONE 0x44 10
0x125 293 0xb4cb7c ERR_MOUNT_FORCEDDISMOUNT 2 NONE 0x44 10
0x126 294 0xb4cb90 ERR_AUTOFOLLOW_TOO_FAR 2 NONE 0x44 10
0x127 295 0xb4cba4 ERR_UNIT_NOT_FOUND 2 NONE 0x44 10
0x128 296 0xb4cbb8 ERR_INVALID_FOLLOW_TARGET 2 NONE 0x44 10
0x129 297 0xb4cbcc ERR_INVALID_INSPECT_TARGET 2 NONE 0x44 10
0x12a 298 0xb4cbe0 ERR_GUILDEMBLEM_SUCCESS 2 NONE 0x44 10
0x12b 299 0xb4cbf4 ERR_GUILDEMBLEM_INVALID_TABARD_COLORS 2 NONE 0x44 10
0x12c 300 0xb4cc08 ERR_GUILDEMBLEM_NOGUILD 2 NONE 0x44 10
0x12d 301 0xb4cc1c ERR_GUILDEMBLEM_NOTGUILDMASTER 2 NONE 0x44 10
0x12e 302 0xb4cc30 ERR_GUILDEMBLEM_NOTENOUGHMONEY 2 NONE 0x28 10
0x12f 303 0xb4cc44 ERR_GUILDEMBLEM_INVALIDVENDOR 2 NONE 0x44 10
0x130 304 0xb4cc58 ERR_EMBLEMERROR_NOTABARDGEOSET 2 NONE 0x44 10
0x131 305 0xb4cc6c ERR_SPELL_OUT_OF_RANGE 2 NONE 0x2e 10
0x132 306 0xb4cc80 ERR_COMMAND_NEEDS_TARGET 0 NONE 0x44 10
0x133 307 0xb4cc94 ERR_NOAMMO_S 2 NONE 0x1 10
0x134 308 0xb4cca8 ERR_TOOBUSYTOFOLLOW 2 NONE 0x44 10
0x135 309 0xb4ccbc ERR_DUEL_REQUESTED 0 LEVELUP 0x44 10
0x136 310 0xb4ccd0 ERR_DUEL_CANCELLED 1 NONE 0x44 10
0x137 311 0xb4cce4 ERR_DEATHBINDALREADYBOUND 2 NONE 0x44 10
0x138 312 0xb4ccf8 ERR_DEATHBIND_SUCCESS_S 0 NONE 0x44 10
0x139 313 0xb4cd0c ERR_NOEMOTEWHILERUNNING 2 NONE 0x44 10
0x13a 314 0xb4cd20 ERR_ZONE_EXPLORED 1 NONE 0x44 10
0x13b 315 0xb4cd34 ERR_ZONE_EXPLORED_XP 0 NONE 0x44 10
0x13c 316 0xb4cd48 ERR_INVALID_ITEM_TARGET 2 NONE 0x33 10
0x13d 317 0xb4cd5c ERR_IGNORING_YOU_S 0 NONE 0x44 10
0x13e 318 0xb4cd70 ERR_FISH_NOT_HOOKED 1 NONE 0x44 10
0x13f 319 0xb4cd84 ERR_FISH_ESCAPED 1 NONE 0x44 10
0x140 320 0xb4cd98 ERR_SPELL_FAILED_NOTUNSHEATHED 1 NONE 0x44 10
0x141 321 0xb4cdac ERR_PETITION_OFFERED_S 0 NONE 0x44 10
0x142 322 0xb4cdc0 ERR_PETITION_SIGNED 0 igPlayerInviteAccept 0x44 10
0x143 323 0xb4cdd4 ERR_PETITION_SIGNED_S 0 igPlayerInviteAccept 0x44 10
0x144 324 0xb4cde8 ERR_PETITION_DECLINED_S 0 igPlayerInviteDecline 0x44 10
0x145 325 0xb4cdfc ERR_PETITION_ALREADY_SIGNED 2 igPlayerInviteDecline 0x44 10
0x146 326 0xb4ce10 ERR_PETITION_IN_GUILD 2 igPlayerInviteDecline 0x44 10
0x147 327 0xb4ce24 ERR_PETITION_CREATOR 2 igPlayerInviteDecline 0x44 10
0x148 328 0xb4ce38 ERR_PETITION_NOT_ENOUGH_SIGNATURES 2 igPlayerInviteDecline 0x44 10
0x149 329 0xb4ce4c ERR_PETITION_NOT_SAME_SERVER 2 igPlayerInviteDecline 0x44 10
0x14a 330 0xb4ce60 ERR_SPELL_UNLEARNED_S 0 NONE 0x44 10
0x14b 331 0xb4ce74 ERR_PET_SPELL_ROOTED 2 NONE 0x44 10
0x14c 332 0xb4ce88 ERR_PET_SPELL_AFFECTING_COMBAT 2 NONE 0x44 10
0x14d 333 0xb4ce9c ERR_PET_SPELL_OUT_OF_RANGE 2 NONE 0x44 10
0x14e 334 0xb4ceb0 ERR_PET_SPELL_NOT_BEHIND 2 NONE 0x44 10
0x14f 335 0xb4cec4 ERR_PET_SPELL_TARGETS_DEAD 2 NONE 0x44 10
0x150 336 0xb4ced8 ERR_PET_SPELL_DEAD 2 NONE 0x44 10
0x151 337 0xb4ceec ERR_PET_SPELL_NOPATH 2 NONE 0x44 10
0x152 338 0xb4cf00 ERR_ITEM_CANT_BE_DESTROYED 2 NONE 0x44 10
0x153 339 0xb4cf14 ERR_TICKET_ALREADY_EXISTS 2 NONE 0x44 10
0x154 340 0xb4cf28 ERR_TICKET_CREATE_ERROR 2 NONE 0x44 10
0x155 341 0xb4cf3c ERR_TICKET_UPDATE_ERROR 2 NONE 0x44 10
0x156 342 0xb4cf50 ERR_TICKET_DB_ERROR 2 NONE 0x44 10
0x157 343 0xb4cf64 ERR_TICKET_NO_TEXT 2 NONE 0x44 10
0x158 344 0xb4cf78 ERR_OBJECT_IS_BUSY 2 NONE 0x44 10
0x159 345 0xb4cf8c ERR_EXHAUSTION_WELLRESTED 0 NONE 0x44 10
0x15a 346 0xb4cfa0 ERR_EXHAUSTION_RESTED 0 NONE 0x44 10
0x15b 347 0xb4cfb4 ERR_EXHAUSTION_NORMAL 0 NONE 0x44 10
0x15c 348 0xb4cfc8 ERR_EXHAUSTION_TIRED 0 NONE 0x44 10
0x15d 349 0xb4cfdc ERR_EXHAUSTION_EXHAUSTED 0 NONE 0x44 10
0x15e 350 0xb4cff0 ERR_NO_ITEMS_WHILE_SHAPESHIFTED 2 NONE 0x44 10
0x15f 351 0xb4d004 ERR_CANT_INTERACT_SHAPESHIFTED 2 NONE 0x44 10
0x160 352 0xb4d018 ERR_MAIL_QUEST_ITEM 2 NONE 0x3b 10
0x161 353 0xb4d02c ERR_MAIL_BOUND_ITEM 2 NONE 0x3b 10
0x162 354 0xb4d040 ERR_MAIL_CONJURED_ITEM 2 NONE 0x44 10
0x163 355 0xb4d054 ERR_MAIL_BAG 2 NONE 0x44 10
0x164 356 0xb4d068 ERR_MAIL_TO_SELF 2 NONE 0x44 10
0x165 357 0xb4d07c ERR_MAIL_TARGET_NOT_FOUND 2 NONE 0x44 10
0x166 358 0xb4d090 ERR_MAIL_DATABASE_ERROR 2 NONE 0x44 10
0x167 359 0xb4d0a4 ERR_MAIL_SENT 1 NONE 0x44 10
0x168 360 0xb4d0b8 ERR_NOT_HAPPY_ENOUGH 2 NONE 0x44 10
0x169 361 0xb4d0cc ERR_USE_CANT_IMMUNE 2 NONE 0x44 10
0x16a 362 0xb4d0e0 ERR_CANT_BE_DISENCHANTED 2 NONE 0x44 10
0x16b 363 0xb4d0f4 ERR_CANT_USE_DISARMED 2 NONE 0x44 10
0x16c 364 0xb4d108 ERR_AUCTION_QUEST_ITEM 2 NONE 0x3b 10
0x16d 365 0xb4d11c ERR_AUCTION_BOUND_ITEM 2 NONE 0x3b 10
0x16e 366 0xb4d130 ERR_AUCTION_CONJURED_ITEM 2 NONE 0x44 10
0x16f 367 0xb4d144 ERR_AUCTION_WRAPPED_ITEM 2 NONE 0x44 10
0x170 368 0xb4d158 ERR_AUCTION_LOOT_ITEM 2 NONE 0x44 10
0x171 369 0xb4d16c ERR_AUCTION_BAG 2 NONE 0x44 10
0x172 370 0xb4d180 ERR_AUCTION_DATABASE_ERROR 2 NONE 0x44 10
0x173 371 0xb4d194 ERR_AUCTION_BID_OWN 2 NONE 0x44 10
0x174 372 0xb4d1a8 ERR_AUCTION_BID_INCREMENT 2 NONE 0x44 10
0x175 373 0xb4d1bc ERR_AUCTION_HIGHER_BID 2 NONE 0x44 10
0x176 374 0xb4d1d0 ERR_AUCTION_MIN_BID 2 NONE 0x44 10
0x177 375 0xb4d1e4 ERR_AUCTION_REPAIR_ITEM 2 NONE 0x44 10
0x178 376 0xb4d1f8 ERR_AUCTION_STARTED 0 NONE 0x44 10
0x179 377 0xb4d20c ERR_AUCTION_REMOVED 0 NONE 0x44 10
0x17a 378 0xb4d220 ERR_AUCTION_OUTBID_S 0 NONE 0x44 10
0x17b 379 0xb4d234 ERR_AUCTION_WON_S 0 NONE 0x44 10
0x17c 380 0xb4d248 ERR_AUCTION_SOLD_S 0 NONE 0x44 10
0x17d 381 0xb4d25c ERR_AUCTION_EXPIRED_S 0 NONE 0x44 10
0x17e 382 0xb4d270 ERR_AUCTION_REMOVED_S 0 NONE 0x44 10
0x17f 383 0xb4d284 ERR_AUCTION_BID_PLACED 0 NONE 0x44 10
0x180 384 0xb4d298 ERR_LOGOUT_FAILED 2 NONE 0x44 10
0x181 385 0xb4d2ac ERR_QUEST_PUSH_SUCCESS_S 0 NONE 0x44 10
0x182 386 0xb4d2c0 ERR_QUEST_PUSH_INVALID_S 0 NONE 0x44 10
0x183 387 0xb4d2d4 ERR_QUEST_PUSH_ACCEPTED_S 0 NONE 0x44 10
0x184 388 0xb4d2e8 ERR_QUEST_PUSH_DECLINED_S 0 NONE 0x44 10
0x185 389 0xb4d2fc ERR_QUEST_PUSH_TOO_FAR_S 0 NONE 0x44 10
0x186 390 0xb4d310 ERR_QUEST_PUSH_BUSY_S 0 NONE 0x44 10
0x187 391 0xb4d324 ERR_QUEST_PUSH_LOG_FULL_S 0 NONE 0x44 10
0x188 392 0xb4d338 ERR_QUEST_PUSH_ONQUEST_S 0 NONE 0x44 10
0x189 393 0xb4d34c ERR_QUEST_PUSH_ALREADY_DONE_S 0 NONE 0x44 10
0x18a 394 0xb4d360 ERR_RAID_GROUP_ONLY 2 NONE 0x44 10
0x18b 395 0xb4d374 ERR_RAID_GROUP_FULL 2 NONE 0x44 10
0x18c 396 0xb4d388 ERR_PVP_KILL_HONORABLE 1 NONE 0x44 10
0x18d 397 0xb4d39c ERR_PVP_KILL_DISHONORABLE 1 NONE 0x44 10
0x18e 398 0xb4d3b0 ERR_SPELL_FAILED_ALREADY_AT_FULL_HEALTH 2 NONE 0x44 10
0x18f 399 0xb4d3c4 ERR_SPELL_FAILED_ALREADY_AT_FULL_POWER_S 2 NONE 0x44 10
0x190 400 0xb4d3d8 ERR_AUTOLOOT_MONEY_S 0 NONE 0x44 10
0x191 401 0xb4d3ec ERR_GENERIC_STUNNED 2 NONE 0x44 10
0x192 402 0xb4d400 ERR_TARGET_STUNNED 2 NONE 0x44 10
0x193 403 0xb4d414 ERR_MUST_REPAIR_DURABILITY 2 NONE 0x44 10
0x194 404 0xb4d428 ERR_RAID_YOU_JOINED 0 NONE 0x44 10
0x195 405 0xb4d43c ERR_RAID_YOU_LEFT 0 NONE 0x44 10
0x196 406 0xb4d450 ERR_RAID_MEMBER_ADDED_S 0 NONE 0x44 10
0x197 407 0xb4d464 ERR_RAID_MEMBER_REMOVED_S 0 NONE 0x44 10
0x198 408 0xb4d478 ERR_CLICK_ON_ITEM_TO_FEED 2 NONE 0x44 10
0x199 409 0xb4d48c ERR_TOO_MANY_CHAT_CHANNELS 0 NONE 0x44 10
0x19a 410 0xb4d4a0 ERR_LOOT_ROLL_PENDING 2 NONE 0x44 10
0x19b 411 0xb4d4b4 ERR_LOOT_PLAYER_NOT_FOUND 2 NONE 0x44 10
0x19c 412 0xb4d4c8 ERR_NOT_IN_RAID 0 NONE 0x44 10
0x19d 413 0xb4d4dc ERR_LOGGING_OUT 2 NONE 0x44 10
0x19e 414 0xb4d4f0 ERR_TARGET_LOGGING_OUT 2 NONE 0x44 10
0x19f 415 0xb4d504 ERR_NOT_WHILE_MOUNTED 2 NONE 0x44 10
0x1a0 416 0xb4d518 ERR_NOT_WHILE_SHAPESHIFTED 2 NONE 0x44 10
0x1a1 417 0xb4d52c ERR_NOT_IN_COMBAT 2 NONE 0x44 10
0x1a2 418 0xb4d540 ERR_NOT_WHILE_DISARMED 2 NONE 0x44 10
0x1a3 419 0xb4d554 ERR_PET_BROKEN 2 NONE 0x44 10
0x1a4 420 0xb4d568 ERR_TALENT_WIPE_ERROR 2 NONE 0x44 10
0x1a5 421 0xb4d57c ERR_FEIGN_DEATH_RESISTED 2 NONE 0x44 10
0x1a6 422 0xb4d590 ERR_MEETING_STONE_IN_QUEUE_S 0 NONE 0x44 10
0x1a7 423 0xb4d5a4 ERR_MEETING_STONE_LEFT_QUEUE_S 0 NONE 0x44 10
0x1a8 424 0xb4d5b8 ERR_MEETING_STONE_OTHER_MEMBER_LEFT 0 NONE 0x44 10
0x1a9 425 0xb4d5cc ERR_MEETING_STONE_PARTY_KICKED_FROM_QUEUE 0 NONE 0x44 10
0x1aa 426 0xb4d5e0 ERR_MEETING_STONE_MEMBER_STILL_IN_QUEUE 0 NONE 0x44 10
0x1ab 427 0xb4d5f4 ERR_MEETING_STONE_SUCCESS 0 NONE 0x44 10
0x1ac 428 0xb4d608 ERR_MEETING_STONE_IN_PROGRESS 0 NONE 0x44 10
0x1ad 429 0xb4d61c ERR_MEETING_STONE_MEMBER_ADDED_S 0 NONE 0x44 10
0x1ae 430 0xb4d630 ERR_MEETING_STONE_GROUP_FULL 2 NONE 0x44 10
0x1af 431 0xb4d644 ERR_MEETING_STONE_NOT_LEADER 2 NONE 0x44 10
0x1b0 432 0xb4d658 ERR_MEETING_STONE_INVALID_LEVEL 2 NONE 0x44 10
0x1b1 433 0xb4d66c ERR_MEETING_STONE_MUST_BE_LEADER 2 NONE 0x44 10
0x1b2 434 0xb4d680 ERR_MEETING_STONE_NO_RAID_GROUP 2 NONE 0x44 10
0x1b3 435 0xb4d694 ERR_GUILDEMBLEM_SAME 2 NONE 0x44 10
0x1b4 436 0xb4d6a8 ERR_EQUIP_TRADE_ITEM 2 NONE 0x44 10
0x1b5 437 0xb4d6bc ERR_PVP_TOGGLE_ON 1 NONE 0x44 10
0x1b6 438 0xb4d6d0 ERR_PVP_TOGGLE_OFF 1 NONE 0x44 10
0x1b7 439 0xb4d6e4 ERR_GROUP_JOIN_BATTLEGROUND_DESERTERS 1 NONE 0x44 10
0x1b8 440 0xb4d6f8 ERR_GROUP_JOIN_BATTLEGROUND_S 1 NONE 0x44 10
0x1b9 441 0xb4d70c ERR_GROUP_JOIN_BATTLEGROUND_FAIL 2 NONE 0x44 10
0x1ba 442 0xb4d720 ERR_GROUP_JOIN_BATTLEGROUND_TOO_MANY 2 NONE 0x44 10
0x1bb 443 0xb4d734 ERR_INVALID_PROMOTION_CODE 2 NONE 0x44 10
0x1bc 444 0xb4d748 ERR_BG_PLAYER_JOINED_SS 0 NONE 0x44 10
0x1bd 445 0xb4d75c ERR_BG_PLAYER_LEFT_S 0 NONE 0x44 10
0x1be 446 0xb4d770 ERR_RESTRICTED_ACCOUNT 2 NONE 0x44 10
0x1bf 447 0xb4d784 ERR_PLAY_TIME_EXCEEDED 2 NONE 0x44 10
0x1c0 448 0xb4d798 ERR_APPROACHING_PARTIAL_PLAY_TIME 0 NONE 0x44 10
0x1c1 449 0xb4d7ac ERR_APPROACHING_NO_PLAY_TIME 0 NONE 0x44 10
0x1c2 450 0xb4d7c0 ERR_UNHEALTHY_TIME 0 NONE 0x44 10
0x1c3 451 0xb4d7d4 ERR_CHAT_RESTRICTED 0 NONE 0x44 10
0x1c4 452 0xb4d7e8 ERR_MAIL_REACHED_CAP 2 NONE 0x44 10
0x1c5 453 0xb4d7fc ERR_INVALID_RAID_TARGET 2 NONE 0x44 10
0x1c6 454 0xb4d810 ERR_RAID_LEADER_READY_CHECK_START_S 0 NONE 0x44 10
0x1c7 455 0xb4d824 ERR_READY_CHECK_IN_PROGRESS 2 NONE 0x44 10
0x1c8 456 0xb4d838 ERR_DUNGEON_DIFFICULTY_FAILED 0 NONE 0x44 10
0x1c9 457 0xb4d84c ERR_DUNGEON_DIFFICULTY_CHANGED_S 0 NONE 0x44 10
0x1ca 458 0xb4d860 ERR_TRADE_WRONG_REALM 2 NONE 0x44 10
0x1cb 459 0xb4d874 ERR_CHAT_PLAYER_AMBIGUOUS_S 0 NONE 0x44 10
0x1cc 460 0xb4d888 ERR_LOOT_CANT_LOOT_THAT_NOW 2 NONE 0x44 10
0x1cd 461 0xb4d89c ERR_LOOT_MASTER_INV_FULL 2 NONE 0x44 10
0x1ce 462 0xb4d8b0 ERR_LOOT_MASTER_UNIQUE_ITEM 2 NONE 0x44 10
0x1cf 463 0xb4d8c4 ERR_LOOT_MASTER_OTHER 2 NONE 0x44 10
0x1d0 464 0xb4d8d8 ERR_FILTERING_YOU_S 0 NONE 0x44 10
1 # build 5875 CGGameUI message catalog (the 0xb4b498 table indexed by 0x496720)
2 # 465 rows, ids 0x0..0x1d0; consumer bound `cmp id,0x1d1; jge` at 0x49672c
3 # kind 0 -> 0x49a870(text, chan); 1 -> UI_INFO_MESSAGE 0xe1; 2 -> UI_ERROR_MESSAGE 0xe0; 3 -> console 0x63cd00
4 # regenerate (in wow-5875-re): python3 re/ui/extract-message-catalog.py <WoW.exe> > re/ui/message-catalog.tsv
5 id_hex id_dec row_va key kind sound type_tag chan
6 0x0 0 0xb4b498 ERR_INV_FULL 2 NONE 0x0 10
7 0x1 1 0xb4b4ac ERR_BANK_FULL 2 NONE 0x44 10
8 0x2 2 0xb4b4c0 ERR_CANT_EQUIP_LEVEL_I 2 NONE 0x2 10
9 0x3 3 0xb4b4d4 ERR_CANT_EQUIP_SKILL 2 NONE 0x29 10
10 0x4 4 0xb4b4e8 ERR_CANT_EQUIP_EVER 2 NONE 0x3 10
11 0x5 5 0xb4b4fc ERR_CANT_EQUIP_RANK 2 NONE 0x44 10
12 0x6 6 0xb4b510 ERR_CANT_EQUIP_REPUTATION 2 NONE 0x44 10
13 0x7 7 0xb4b524 ERR_PROFICIENCY_NEEDED 2 NONE 0x30 10
14 0x8 8 0xb4b538 ERR_WRONG_SLOT 2 NONE 0x1b 10
15 0x9 9 0xb4b54c ERR_BAG_FULL 2 NONE 0x1d 10
16 0xa 10 0xb4b560 ERR_INTERNAL_BAG_ERROR 2 NONE 0x44 10
17 0xb 11 0xb4b574 ERR_DESTROY_NONEMPTY_BAG 2 NONE 0x44 10
18 0xc 12 0xb4b588 ERR_BAG_IN_BAG 2 NONE 0x1a 10
19 0xd 13 0xb4b59c ERR_TOO_MANY_SPECIAL_BAGS 2 NONE 0x44 10
20 0xe 14 0xb4b5b0 ERR_TRADE_EQUIPPED_BAG 2 NONE 0x44 10
21 0xf 15 0xb4b5c4 ERR_AMMO_ONLY 2 NONE 0x1c 10
22 0x10 16 0xb4b5d8 ERR_NO_SLOT_AVAILABLE 2 NONE 0x44 10
23 0x11 17 0xb4b5ec ERR_WRONG_BAG_TYPE 2 NONE 0x44 10
24 0x12 18 0xb4b600 ERR_ITEM_MAX_COUNT 2 NONE 0x1e 10
25 0x13 19 0xb4b614 ERR_NOT_EQUIPPABLE 2 NONE 0x2c 10
26 0x14 20 0xb4b628 ERR_CANT_STACK 2 NONE 0x44 10
27 0x15 21 0xb4b63c ERR_CANT_SWAP 2 NONE 0x44 10
28 0x16 22 0xb4b650 ERR_SLOT_EMPTY 2 NONE 0x44 10
29 0x17 23 0xb4b664 ERR_ITEM_NOT_FOUND 2 NONE 0x44 10
30 0x18 24 0xb4b678 ERR_TOO_FEW_TO_SPLIT 2 NONE 0x44 10
31 0x19 25 0xb4b68c ERR_SPLIT_FAILED 2 NONE 0x44 10
32 0x1a 26 0xb4b6a0 ERR_NOT_A_BAG 2 NONE 0x19 10
33 0x1b 27 0xb4b6b4 ERR_NOT_OWNER 2 NONE 0x44 10
34 0x1c 28 0xb4b6c8 ERR_ONLY_ONE_QUIVER 2 NONE 0x44 10
35 0x1d 29 0xb4b6dc ERR_NO_BANK_SLOT 2 NONE 0x44 10
36 0x1e 30 0xb4b6f0 ERR_NO_BANK_HERE 2 NONE 0x44 10
37 0x1f 31 0xb4b704 ERR_ITEM_LOCKED 2 NONE 0x3d 10
38 0x20 32 0xb4b718 ERR_2HANDED_EQUIPPED 2 NONE 0x2a 10
39 0x21 33 0xb4b72c ERR_VENDOR_NOT_INTERESTED 2 NONE 0x44 10
40 0x22 34 0xb4b740 ERR_VENDOR_HATES_YOU 2 NONE 0x44 10
41 0x23 35 0xb4b754 ERR_VENDOR_SOLD_OUT 2 NONE 0x44 10
42 0x24 36 0xb4b768 ERR_VENDOR_TOO_FAR 2 NONE 0x44 10
43 0x25 37 0xb4b77c ERR_NOT_ENOUGH_MONEY 2 NONE 0x28 10
44 0x26 38 0xb4b790 ERR_RECEIVE_ITEM_S 0 ITEMGENERICSOUND 0x44 10
45 0x27 39 0xb4b7a4 ERR_DROP_BOUND_ITEM 2 NONE 0x4 10
46 0x28 40 0xb4b7b8 ERR_TRADE_BOUND_ITEM 2 NONE 0x3b 10
47 0x29 41 0xb4b7cc ERR_TRADE_QUEST_ITEM 2 NONE 0x3b 10
48 0x2a 42 0xb4b7e0 ERR_TRADE_GROUND_ITEM 2 NONE 0x44 10
49 0x2b 43 0xb4b7f4 ERR_TRADE_BAG 2 NONE 0x44 10
50 0x2c 44 0xb4b808 ERR_SPELL_FAILED_S 2 NONE 0x44 10
51 0x2d 45 0xb4b81c ERR_ITEM_COOLDOWN 2 NONE 0x5 10
52 0x2e 46 0xb4b830 ERR_POTION_COOLDOWN 2 NONE 0x2f 10
53 0x2f 47 0xb4b844 ERR_FOOD_COOLDOWN 2 NONE 0x7 10
54 0x30 48 0xb4b858 ERR_SPELL_COOLDOWN 2 NONE 0xc 10
55 0x31 49 0xb4b86c ERR_ABILITY_COOLDOWN 2 NONE 0x32 10
56 0x32 50 0xb4b880 ERR_SPELL_ALREADY_KNOWN_S 2 NONE 0xd 10
57 0x33 51 0xb4b894 ERR_PET_SPELL_ALREADY_KNOWN_S 2 NONE 0x44 10
58 0x34 52 0xb4b8a8 ERR_PROFICIENCY_GAINED_S 0 NONE 0x44 23
59 0x35 53 0xb4b8bc ERR_SKILL_GAINED_S 0 NONE 0x44 23
60 0x36 54 0xb4b8d0 ERR_SKILL_UP_SI 0 NONE 0x44 23
61 0x37 55 0xb4b8e4 ERR_LEARN_SPELL_S 0 NONE 0x44 10
62 0x38 56 0xb4b8f8 ERR_LEARN_ABILITY_S 0 NONE 0x44 10
63 0x39 57 0xb4b90c ERR_LEARN_RECIPE_S 0 NONE 0x44 10
64 0x3a 58 0xb4b920 ERR_INVITE_PLAYER_S 0 NONE 0x44 10
65 0x3b 59 0xb4b934 ERR_INVITE_SELF 2 NONE 0x44 10
66 0x3c 60 0xb4b948 ERR_INVITED_TO_GROUP_S 0 NONE 0x44 10
67 0x3d 61 0xb4b95c ERR_ALREADY_IN_GROUP_S 0 NONE 0x14 10
68 0x3e 62 0xb4b970 ERR_PLAYER_BUSY_S 0 NONE 0x44 10
69 0x3f 63 0xb4b984 ERR_NEW_LEADER_S 0 NONE 0x44 10
70 0x40 64 0xb4b998 ERR_NEW_LEADER_YOU 0 NONE 0x44 10
71 0x41 65 0xb4b9ac ERR_LEFT_GROUP_S 0 NONE 0x44 10
72 0x42 66 0xb4b9c0 ERR_LEFT_GROUP_YOU 0 NONE 0x44 10
73 0x43 67 0xb4b9d4 ERR_GROUP_DISBANDED 0 NONE 0x44 10
74 0x44 68 0xb4b9e8 ERR_DECLINE_GROUP_S 0 igPlayerInviteDecline 0x44 10
75 0x45 69 0xb4b9fc ERR_JOINED_GROUP_S 0 NONE 0x44 10
76 0x46 70 0xb4ba10 ERR_UNINVITE_YOU 0 NONE 0x44 10
77 0x47 71 0xb4ba24 ERR_BAD_PLAYER_NAME_S 0 NONE 0x44 10
78 0x48 72 0xb4ba38 ERR_NOT_IN_GROUP 0 NONE 0x44 10
79 0x49 73 0xb4ba4c ERR_TARGET_NOT_IN_GROUP_S 0 NONE 0x44 10
80 0x4a 74 0xb4ba60 ERR_GROUP_FULL 0 NONE 0x8 10
81 0x4b 75 0xb4ba74 ERR_NOT_LEADER 0 NONE 0x44 10
82 0x4c 76 0xb4ba88 ERR_PLAYER_DIED_S 0 NONE 0x44 10
83 0x4d 77 0xb4ba9c ERR_GUILD_CREATE_S 0 LEVELUP 0x44 10
84 0x4e 78 0xb4bab0 ERR_GUILD_INVITE_S 0 NONE 0x44 10
85 0x4f 79 0xb4bac4 ERR_INVITED_TO_GUILD_SS 0 LEVELUP 0x44 10
86 0x50 80 0xb4bad8 ERR_ALREADY_IN_GUILD_S 0 NONE 0x44 10
87 0x51 81 0xb4baec ERR_ALREADY_INVITED_TO_GUILD_S 0 NONE 0x44 10
88 0x52 82 0xb4bb00 ERR_INVITED_TO_GUILD 0 NONE 0x44 10
89 0x53 83 0xb4bb14 ERR_ALREADY_IN_GUILD 0 NONE 0x44 10
90 0x54 84 0xb4bb28 ERR_GUILD_ACCEPT 0 NONE 0x44 10
91 0x55 85 0xb4bb3c ERR_GUILD_DECLINE_S 0 NONE 0x44 10
92 0x56 86 0xb4bb50 ERR_GUILD_PERMISSIONS 0 NONE 0x3e 10
93 0x57 87 0xb4bb64 ERR_GUILD_JOIN_S 0 NONE 0x44 10
94 0x58 88 0xb4bb78 ERR_GUILD_FOUNDER_S 0 NONE 0x44 10
95 0x59 89 0xb4bb8c ERR_GUILD_PROMOTE_SSS 0 NONE 0x44 10
96 0x5a 90 0xb4bba0 ERR_GUILD_DEMOTE_SSS 0 NONE 0x44 10
97 0x5b 91 0xb4bbb4 ERR_GUILD_QUIT_S 0 NONE 0x44 10
98 0x5c 92 0xb4bbc8 ERR_GUILD_LEAVE_S 0 NONE 0x44 10
99 0x5d 93 0xb4bbdc ERR_GUILD_REMOVE_SS 0 NONE 0x44 10
100 0x5e 94 0xb4bbf0 ERR_GUILD_REMOVE_SELF 0 NONE 0x44 10
101 0x5f 95 0xb4bc04 ERR_GUILD_DISBAND_S 0 NONE 0x44 10
102 0x60 96 0xb4bc18 ERR_GUILD_DISBAND_SELF 0 NONE 0x44 10
103 0x61 97 0xb4bc2c ERR_GUILD_LEADER_S 0 NONE 0x44 10
104 0x62 98 0xb4bc40 ERR_GUILD_LEADER_SELF 0 NONE 0x44 10
105 0x63 99 0xb4bc54 ERR_GUILD_PLAYER_NOT_FOUND_S 0 NONE 0x44 10
106 0x64 100 0xb4bc68 ERR_GUILD_PLAYER_NOT_IN_GUILD_S 0 NONE 0x44 10
107 0x65 101 0xb4bc7c ERR_GUILD_PLAYER_NOT_IN_GUILD 0 NONE 0x44 10
108 0x66 102 0xb4bc90 ERR_GUILD_CANT_PROMOTE_S 0 NONE 0x44 10
109 0x67 103 0xb4bca4 ERR_GUILD_CANT_DEMOTE_S 0 NONE 0x44 10
110 0x68 104 0xb4bcb8 ERR_GUILD_NOT_IN_A_GUILD 0 NONE 0x44 10
111 0x69 105 0xb4bccc ERR_GUILD_INTERNAL 0 NONE 0x44 10
112 0x6a 106 0xb4bce0 ERR_GUILD_LEADER_IS_S 0 NONE 0x44 10
113 0x6b 107 0xb4bcf4 ERR_GUILD_LEADER_CHANGED_SS 0 NONE 0x44 10
114 0x6c 108 0xb4bd08 ERR_GUILD_DISBANDED 0 NONE 0x44 10
115 0x6d 109 0xb4bd1c ERR_GUILD_NOT_ALLIED 0 NONE 0x44 10
116 0x6e 110 0xb4bd30 ERR_GUILD_LEADER_LEAVE 0 NONE 0x44 10
117 0x6f 111 0xb4bd44 ERR_GUILD_RANKS_LOCKED 0 NONE 0x44 10
118 0x70 112 0xb4bd58 ERR_GUILD_RANK_IN_USE 0 NONE 0x44 10
119 0x71 113 0xb4bd6c ERR_GUILD_RANK_TOO_HIGH_S 0 NONE 0x44 10
120 0x72 114 0xb4bd80 ERR_GUILD_RANK_TOO_LOW_S 0 NONE 0x44 10
121 0x73 115 0xb4bd94 ERR_GUILD_NAME_INVALID 2 NONE 0x44 10
122 0x74 116 0xb4bda8 ERR_GUILD_NAME_EXISTS_S 2 NONE 0x44 10
123 0x75 117 0xb4bdbc ERR_GUILD_ENTER_NAME 2 NONE 0x44 10
124 0x76 118 0xb4bdd0 ERR_GUILD_NAME_TOO_SHORT 2 NONE 0x44 10
125 0x77 119 0xb4bde4 ERR_GUILD_NAME_MIXED_LANGUAGES 2 NONE 0x44 10
126 0x78 120 0xb4bdf8 ERR_GUILD_NAME_PROFANE 2 NONE 0x44 10
127 0x79 121 0xb4be0c ERR_GUILD_NAME_RESERVED 2 NONE 0x44 10
128 0x7a 122 0xb4be20 ERR_GUILD_NAME_INVALID_SPACE 2 NONE 0x44 10
129 0x7b 123 0xb4be34 ERR_GUILD_NAME_NAME_CONSECUTIVE_SPACES 2 NONE 0x44 10
130 0x7c 124 0xb4be48 ERR_NO_GUILD_CHARTER 2 NONE 0x44 10
131 0x7d 125 0xb4be5c ERR_OUT_OF_RANGE 2 NONE 0xa 10
132 0x7e 126 0xb4be70 ERR_PLAYER_DEAD 2 NONE 0x44 10
133 0x7f 127 0xb4be84 ERR_CLIENT_LOCKED_OUT 2 NONE 0x44 10
134 0x80 128 0xb4be98 ERR_KILLED_BY_S 0 NONE 0x44 10
135 0x81 129 0xb4beac ERR_LOOT_LOCKED 2 NONE 0x21 10
136 0x82 130 0xb4bec0 ERR_LOOT_TOO_FAR 2 NONE 0x23 10
137 0x83 131 0xb4bed4 ERR_LOOT_DIDNT_KILL 2 NONE 0x1f 10
138 0x84 132 0xb4bee8 ERR_LOOT_BAD_FACING 2 NONE 0x20 10
139 0x85 133 0xb4befc ERR_LOOT_NOTSTANDING 2 NONE 0x44 10
140 0x86 134 0xb4bf10 ERR_LOOT_STUNNED 2 NONE 0x44 10
141 0x87 135 0xb4bf24 ERR_LOOT_NO_UI 2 NONE 0x44 10
142 0x88 136 0xb4bf38 ERR_LOOT_WHILE_INVULNERABLE 2 NONE 0x44 10
143 0x89 137 0xb4bf4c ERR_QUEST_ACCEPTED_S 0 QUESTADDED 0x44 10
144 0x8a 138 0xb4bf60 ERR_QUEST_COMPLETE_S 0 igQuestListComplete 0x44 10
145 0x8b 139 0xb4bf74 ERR_QUEST_FAILED_S 0 igQuestFailed 0x44 10
146 0x8c 140 0xb4bf88 ERR_QUEST_FAILED_BAG_FULL_S 0 igQuestFailed 0x44 10
147 0x8d 141 0xb4bf9c ERR_QUEST_FAILED_MAX_COUNT_S 0 igQuestFailed 0x44 10
148 0x8e 142 0xb4bfb0 ERR_QUEST_FAILED_LOW_LEVEL 0 igQuestFailed 0x44 10
149 0x8f 143 0xb4bfc4 ERR_QUEST_FAILED_MISSING_ITEMS 0 igQuestFailed 0x44 10
150 0x90 144 0xb4bfd8 ERR_QUEST_FAILED_WRONG_RACE 0 igQuestFailed 0x44 10
151 0x91 145 0xb4bfec ERR_QUEST_FAILED_NOT_ENOUGH_MONEY 0 igQuestFailed 0x44 10
152 0x92 146 0xb4c000 ERR_QUEST_ONLY_ONE_TIMED 0 igQuestFailed 0x44 10
153 0x93 147 0xb4c014 ERR_QUEST_NEED_PREREQS 0 igQuestFailed 0x44 10
154 0x94 148 0xb4c028 ERR_QUEST_ALREADY_ON 0 igQuestFailed 0x44 10
155 0x95 149 0xb4c03c ERR_QUEST_REWARD_EXP_I 0 NONE 0x44 10
156 0x96 150 0xb4c050 ERR_QUEST_REWARD_ITEM_S 0 NONE 0x44 10
157 0x97 151 0xb4c064 ERR_QUEST_REWARD_MONEY_S 0 NONE 0x44 10
158 0x98 152 0xb4c078 ERR_QUEST_MUST_CHOOSE 2 igQuestFailed 0x44 10
159 0x99 153 0xb4c08c ERR_QUEST_LOG_FULL 2 igQuestFailed 0x44 10
160 0x9a 154 0xb4c0a0 ERR_COMBAT_DAMAGE_SSI 0 NONE 0x44 10
161 0x9b 155 0xb4c0b4 ERR_INSPECT_S 0 NONE 0x44 10
162 0x9c 156 0xb4c0c8 ERR_CANT_USE_ITEM 2 NONE 0x33 10
163 0x9d 157 0xb4c0dc ERR_MUST_EQUIP_ITEM 2 NONE 0x31 10
164 0x9e 158 0xb4c0f0 ERR_PASSIVE_ABILITY 2 NONE 0x44 10
165 0x9f 159 0xb4c104 ERR_2HSKILLNOTFOUND 2 NONE 0x44 10
166 0xa0 160 0xb4c118 ERR_NO_ATTACK_TARGET 2 NONE 0x26 10
167 0xa1 161 0xb4c12c ERR_INVALID_ATTACK_TARGET 2 NONE 0xb 10
168 0xa2 162 0xb4c140 ERR_ATTACK_STUNNED 2 NONE 0x44 10
169 0xa3 163 0xb4c154 ERR_ATTACK_PACIFIED 2 NONE 0x44 10
170 0xa4 164 0xb4c168 ERR_ATTACK_MOUNTED 2 NONE 0x44 10
171 0xa5 165 0xb4c17c ERR_ATTACK_FLEEING 2 NONE 0x44 10
172 0xa6 166 0xb4c190 ERR_ATTACK_CONFUSED 2 NONE 0x44 10
173 0xa7 167 0xb4c1a4 ERR_ATTACK_CHARMED 2 NONE 0x44 10
174 0xa8 168 0xb4c1b8 ERR_ATTACK_DEAD 2 NONE 0x44 10
175 0xa9 169 0xb4c1cc ERR_ATTACK_PREVENTED_BY_MECHANIC_S 2 NONE 0x44 10
176 0xaa 170 0xb4c1e0 ERR_TAXISAMENODE 2 NONE 0x44 10
177 0xab 171 0xb4c1f4 ERR_TAXINOSUCHPATH 1 NONE 0x44 10
178 0xac 172 0xb4c208 ERR_TAXIUNSPECIFIEDSERVERERROR 2 NONE 0x44 10
179 0xad 173 0xb4c21c ERR_TAXINOTENOUGHMONEY 1 NONE 0x36 10
180 0xae 174 0xb4c230 ERR_TAXITOOFARAWAY 1 NONE 0x44 10
181 0xaf 175 0xb4c244 ERR_TAXINOVENDORNEARBY 1 NONE 0x44 10
182 0xb0 176 0xb4c258 ERR_TAXINOTVISITED 1 NONE 0x44 10
183 0xb1 177 0xb4c26c ERR_TAXIPLAYERBUSY 2 NONE 0x44 10
184 0xb2 178 0xb4c280 ERR_TAXIPLAYERALREADYMOUNTED 2 NONE 0x44 10
185 0xb3 179 0xb4c294 ERR_TAXIPLAYERSHAPESHIFTED 2 NONE 0x44 10
186 0xb4 180 0xb4c2a8 ERR_TAXIPLAYERMOVING 1 NONE 0x44 10
187 0xb5 181 0xb4c2bc ERR_TAXINOPATHS 2 NONE 0x44 10
188 0xb6 182 0xb4c2d0 ERR_TAXINOTSTANDING 1 NONE 0x44 10
189 0xb7 183 0xb4c2e4 ERR_NO_REPLY_TARGET 0 NONE 0x44 10
190 0xb8 184 0xb4c2f8 ERR_GENERIC_NO_TARGET 2 NONE 0x2d 10
191 0xb9 185 0xb4c30c ERR_INITIATE_TRADE_S 0 NONE 0x44 10
192 0xba 186 0xb4c320 ERR_TRADE_REQUEST_S 0 LEVELUP 0x44 10
193 0xbb 187 0xb4c334 ERR_TRADE_BLOCKED_S 0 NONE 0x44 10
194 0xbc 188 0xb4c348 ERR_TRADE_TARGET_DEAD 2 NONE 0x44 10
195 0xbd 189 0xb4c35c ERR_TRADE_TOO_FAR 2 NONE 0x44 10
196 0xbe 190 0xb4c370 ERR_TRADE_CANCELLED 1 NONE 0x44 10
197 0xbf 191 0xb4c384 ERR_TRADE_COMPLETE 1 NONE 0x44 10
198 0xc0 192 0xb4c398 ERR_TRADE_BAG_FULL 2 NONE 0x44 10
199 0xc1 193 0xb4c3ac ERR_TRADE_TARGET_BAG_FULL 2 NONE 0x44 10
200 0xc2 194 0xb4c3c0 ERR_TRADE_MAX_COUNT_EXCEEDED 2 NONE 0x44 10
201 0xc3 195 0xb4c3d4 ERR_TRADE_TARGET_MAX_COUNT_EXCEEDED 2 NONE 0x44 10
202 0xc4 196 0xb4c3e8 ERR_ALREADY_TRADING 2 NONE 0x44 10
203 0xc5 197 0xb4c3fc ERR_MOUNT_INVALIDMOUNTEE 2 NONE 0x44 10
204 0xc6 198 0xb4c410 ERR_MOUNT_TOOFARAWAY 2 NONE 0x44 10
205 0xc7 199 0xb4c424 ERR_MOUNT_ALREADYMOUNTED 2 NONE 0x44 10
206 0xc8 200 0xb4c438 ERR_MOUNT_NOTMOUNTABLE 2 NONE 0x44 10
207 0xc9 201 0xb4c44c ERR_MOUNT_NOTYOURPET 2 NONE 0x44 10
208 0xca 202 0xb4c460 ERR_MOUNT_OTHER 2 NONE 0x44 10
209 0xcb 203 0xb4c474 ERR_MOUNT_LOOTING 2 NONE 0x44 10
210 0xcc 204 0xb4c488 ERR_MOUNT_RACECANTMOUNT 2 NONE 0x44 10
211 0xcd 205 0xb4c49c ERR_MOUNT_SHAPESHIFTED 2 NONE 0x44 10
212 0xce 206 0xb4c4b0 ERR_DISMOUNT_NOPET 2 NONE 0x44 10
213 0xcf 207 0xb4c4c4 ERR_DISMOUNT_NOTMOUNTED 2 NONE 0x44 10
214 0xd0 208 0xb4c4d8 ERR_DISMOUNT_NOTYOURPET 2 NONE 0x44 10
215 0xd1 209 0xb4c4ec ERR_SPELL_FAILED_TOTEMS 2 NONE 0x44 10
216 0xd2 210 0xb4c500 ERR_SPELL_FAILED_REAGENTS 2 NONE 0x44 10
217 0xd3 211 0xb4c514 ERR_SPELL_FAILED_REAGENTS_GENERIC 2 NONE 0x44 10
218 0xd4 212 0xb4c528 ERR_SPELL_FAILED_EQUIPPED_ITEM 2 NONE 0x44 10
219 0xd5 213 0xb4c53c ERR_SPELL_FAILED_EQUIPPED_ITEM_CLASS_S 2 NONE 0x44 10
220 0xd6 214 0xb4c550 ERR_SPELL_FAILED_SHAPESHIFT_FORM_S 2 NONE 0x44 10
221 0xd7 215 0xb4c564 ERR_BADATTACKFACING 2 NONE 0x44 10
222 0xd8 216 0xb4c578 ERR_BADATTACKPOS 2 NONE 0x44 10
223 0xd9 217 0xb4c58c ERR_CHEST_IN_USE 2 NONE 0x34 10
224 0xda 218 0xb4c5a0 ERR_USE_CANT_OPEN 2 NONE 0x44 10
225 0xdb 219 0xb4c5b4 ERR_USE_LOCKED 2 NONE 0x3d 10
226 0xdc 220 0xb4c5c8 ERR_DOOR_LOCKED 2 NONE 0x3d 10
227 0xdd 221 0xb4c5dc ERR_BUTTON_LOCKED 2 NONE 0x3d 10
228 0xde 222 0xb4c5f0 ERR_USE_LOCKED_WITH_ITEM_S 2 NONE 0x44 10
229 0xdf 223 0xb4c604 ERR_USE_LOCKED_WITH_SPELL_S 2 NONE 0x44 10
230 0xe0 224 0xb4c618 ERR_USE_LOCKED_WITH_SPELL_KNOWN_SI 2 NONE 0x44 10
231 0xe1 225 0xb4c62c ERR_USE_TOO_FAR 2 NONE 0x39 10
232 0xe2 226 0xb4c640 ERR_USE_BAD_ANGLE 2 NONE 0x44 10
233 0xe3 227 0xb4c654 ERR_USE_OBJECT_MOVING 2 NONE 0x44 10
234 0xe4 228 0xb4c668 ERR_USE_SPELL_FOCUS 1 NONE 0x44 10
235 0xe5 229 0xb4c67c ERR_USE_DESTROYED 2 NONE 0x44 10
236 0xe6 230 0xb4c690 ERR_SET_LOOT_FREEFORALL 0 NONE 0x44 10
237 0xe7 231 0xb4c6a4 ERR_SET_LOOT_ROUNDROBIN 0 NONE 0x44 10
238 0xe8 232 0xb4c6b8 ERR_SET_LOOT_MASTER 0 NONE 0x44 10
239 0xe9 233 0xb4c6cc ERR_SET_LOOT_GROUP 0 NONE 0x44 10
240 0xea 234 0xb4c6e0 ERR_SET_LOOT_NBG 0 NONE 0x44 10
241 0xeb 235 0xb4c6f4 ERR_SET_LOOT_THRESHOLD_S 0 NONE 0x44 10
242 0xec 236 0xb4c708 ERR_NEW_LOOT_MASTER_S 0 NONE 0x44 10
243 0xed 237 0xb4c71c ERR_SPECIFY_MASTER_LOOTER 2 NONE 0x44 10
244 0xee 238 0xb4c730 ERR_TAME_FAILED 2 NONE 0x44 10
245 0xef 239 0xb4c744 ERR_CHAT_WHILE_DEAD 2 NONE 0x44 10
246 0xf0 240 0xb4c758 ERR_CHAT_WRONG_FACTION 0 NONE 0x44 10
247 0xf1 241 0xb4c76c ERR_CHAT_PLAYER_NOT_FOUND_S 0 NONE 0x44 10
248 0xf2 242 0xb4c780 ERR_NEWTAXIPATH 1 TaxiNodeDiscovered 0x44 10
249 0xf3 243 0xb4c794 ERR_NO_PET 2 NONE 0x44 10
250 0xf4 244 0xb4c7a8 ERR_NOTYOURPET 2 NONE 0x44 10
251 0xf5 245 0xb4c7bc ERR_PET_NOT_RENAMEABLE 2 NONE 0x44 10
252 0xf6 246 0xb4c7d0 ERR_NULL_PETNAME 2 NONE 0x44 10
253 0xf7 247 0xb4c7e4 ERR_INVALID_PETNAME 2 NONE 0x44 10
254 0xf8 248 0xb4c7f8 ERR_QUEST_OBJECTIVE_COMPLETE_S 1 NONE 0x44 10
255 0xf9 249 0xb4c80c ERR_QUEST_UNKNOWN_COMPLETE 1 NONE 0x44 10
256 0xfa 250 0xb4c820 ERR_QUEST_ADD_KILL_SII 1 NONE 0x44 10
257 0xfb 251 0xb4c834 ERR_QUEST_ADD_FOUND_SII 1 NONE 0x44 10
258 0xfc 252 0xb4c848 ERR_QUEST_ADD_ITEM_SII 1 NONE 0x44 10
259 0xfd 253 0xb4c85c ERR_CANNOTCREATEDIRECTORY 2 NONE 0x44 10
260 0xfe 254 0xb4c870 ERR_CANNOTCREATEFILE 2 NONE 0x44 10
261 0xff 255 0xb4c884 ERR_PLAYER_WRONG_FACTION 2 NONE 0x44 10
262 0x100 256 0xb4c898 ERR_BANKSLOT_FAILED_TOO_MANY 2 NONE 0x44 10
263 0x101 257 0xb4c8ac ERR_BANKSLOT_INSUFFICIENT_FUNDS 2 NONE 0x16 10
264 0x102 258 0xb4c8c0 ERR_BANKSLOT_NOTBANKER 2 NONE 0x44 10
265 0x103 259 0xb4c8d4 ERR_FRIEND_DB_ERROR 0 NONE 0x44 10
266 0x104 260 0xb4c8e8 ERR_FRIEND_LIST_FULL 0 NONE 0x44 10
267 0x105 261 0xb4c8fc ERR_FRIEND_ADDED_S 0 NONE 0x44 10
268 0x106 262 0xb4c910 ERR_FRIEND_ONLINE_SS 0 FRIENDJOINGAME 0x44 10
269 0x107 263 0xb4c924 ERR_FRIEND_OFFLINE_S 0 NONE 0x44 10
270 0x108 264 0xb4c938 ERR_FRIEND_NOT_FOUND 0 NONE 0x44 10
271 0x109 265 0xb4c94c ERR_FRIEND_WRONG_FACTION 0 NONE 0x44 10
272 0x10a 266 0xb4c960 ERR_FRIEND_REMOVED_S 0 NONE 0x44 10
273 0x10b 267 0xb4c974 ERR_FRIEND_ERROR 0 NONE 0x44 10
274 0x10c 268 0xb4c988 ERR_FRIEND_ALREADY_S 0 NONE 0x44 10
275 0x10d 269 0xb4c99c ERR_FRIEND_SELF 0 NONE 0x44 10
276 0x10e 270 0xb4c9b0 ERR_IGNORE_FULL 0 NONE 0x44 10
277 0x10f 271 0xb4c9c4 ERR_IGNORE_SELF 0 NONE 0x44 10
278 0x110 272 0xb4c9d8 ERR_IGNORE_NOT_FOUND 0 NONE 0x44 10
279 0x111 273 0xb4c9ec ERR_IGNORE_ALREADY_S 0 NONE 0x44 10
280 0x112 274 0xb4ca00 ERR_IGNORE_ADDED_S 0 NONE 0x44 10
281 0x113 275 0xb4ca14 ERR_IGNORE_REMOVED_S 0 NONE 0x44 10
282 0x114 276 0xb4ca28 ERR_IGNORE_AMBIGUOUS 0 NONE 0x44 10
283 0x115 277 0xb4ca3c ERR_ONLY_ONE_BOLT 2 NONE 0x44 10
284 0x116 278 0xb4ca50 ERR_ONLY_ONE_AMMO 2 NONE 0x44 10
285 0x117 279 0xb4ca64 ERR_SPELL_FAILED_EQUIPPED_SPECIFIC_ITEM 2 NONE 0x44 10
286 0x118 280 0xb4ca78 ERR_WRONG_BAG_TYPE_SUBCLASS 2 NONE 0x1c 10
287 0x119 281 0xb4ca8c ERR_CANT_WRAP_STACKABLE 2 NONE 0x44 10
288 0x11a 282 0xb4caa0 ERR_CANT_WRAP_EQUIPPED 2 NONE 0x44 10
289 0x11b 283 0xb4cab4 ERR_CANT_WRAP_WRAPPED 2 NONE 0x44 10
290 0x11c 284 0xb4cac8 ERR_CANT_WRAP_BOUND 2 NONE 0x44 10
291 0x11d 285 0xb4cadc ERR_CANT_WRAP_UNIQUE 2 NONE 0x44 10
292 0x11e 286 0xb4caf0 ERR_CANT_WRAP_BAGS 2 NONE 0x44 10
293 0x11f 287 0xb4cb04 ERR_OUT_OF_MANA 2 NONE 0xf 10
294 0x120 288 0xb4cb18 ERR_OUT_OF_RAGE 2 NONE 0x3f 10
295 0x121 289 0xb4cb2c ERR_OUT_OF_FOCUS 2 NONE 0x44 10
296 0x122 290 0xb4cb40 ERR_OUT_OF_ENERGY 2 NONE 0x40 10
297 0x123 291 0xb4cb54 ERR_OUT_OF_HEALTH 2 NONE 0x44 10
298 0x124 292 0xb4cb68 ERR_LOOT_GONE 2 NONE 0x44 10
299 0x125 293 0xb4cb7c ERR_MOUNT_FORCEDDISMOUNT 2 NONE 0x44 10
300 0x126 294 0xb4cb90 ERR_AUTOFOLLOW_TOO_FAR 2 NONE 0x44 10
301 0x127 295 0xb4cba4 ERR_UNIT_NOT_FOUND 2 NONE 0x44 10
302 0x128 296 0xb4cbb8 ERR_INVALID_FOLLOW_TARGET 2 NONE 0x44 10
303 0x129 297 0xb4cbcc ERR_INVALID_INSPECT_TARGET 2 NONE 0x44 10
304 0x12a 298 0xb4cbe0 ERR_GUILDEMBLEM_SUCCESS 2 NONE 0x44 10
305 0x12b 299 0xb4cbf4 ERR_GUILDEMBLEM_INVALID_TABARD_COLORS 2 NONE 0x44 10
306 0x12c 300 0xb4cc08 ERR_GUILDEMBLEM_NOGUILD 2 NONE 0x44 10
307 0x12d 301 0xb4cc1c ERR_GUILDEMBLEM_NOTGUILDMASTER 2 NONE 0x44 10
308 0x12e 302 0xb4cc30 ERR_GUILDEMBLEM_NOTENOUGHMONEY 2 NONE 0x28 10
309 0x12f 303 0xb4cc44 ERR_GUILDEMBLEM_INVALIDVENDOR 2 NONE 0x44 10
310 0x130 304 0xb4cc58 ERR_EMBLEMERROR_NOTABARDGEOSET 2 NONE 0x44 10
311 0x131 305 0xb4cc6c ERR_SPELL_OUT_OF_RANGE 2 NONE 0x2e 10
312 0x132 306 0xb4cc80 ERR_COMMAND_NEEDS_TARGET 0 NONE 0x44 10
313 0x133 307 0xb4cc94 ERR_NOAMMO_S 2 NONE 0x1 10
314 0x134 308 0xb4cca8 ERR_TOOBUSYTOFOLLOW 2 NONE 0x44 10
315 0x135 309 0xb4ccbc ERR_DUEL_REQUESTED 0 LEVELUP 0x44 10
316 0x136 310 0xb4ccd0 ERR_DUEL_CANCELLED 1 NONE 0x44 10
317 0x137 311 0xb4cce4 ERR_DEATHBINDALREADYBOUND 2 NONE 0x44 10
318 0x138 312 0xb4ccf8 ERR_DEATHBIND_SUCCESS_S 0 NONE 0x44 10
319 0x139 313 0xb4cd0c ERR_NOEMOTEWHILERUNNING 2 NONE 0x44 10
320 0x13a 314 0xb4cd20 ERR_ZONE_EXPLORED 1 NONE 0x44 10
321 0x13b 315 0xb4cd34 ERR_ZONE_EXPLORED_XP 0 NONE 0x44 10
322 0x13c 316 0xb4cd48 ERR_INVALID_ITEM_TARGET 2 NONE 0x33 10
323 0x13d 317 0xb4cd5c ERR_IGNORING_YOU_S 0 NONE 0x44 10
324 0x13e 318 0xb4cd70 ERR_FISH_NOT_HOOKED 1 NONE 0x44 10
325 0x13f 319 0xb4cd84 ERR_FISH_ESCAPED 1 NONE 0x44 10
326 0x140 320 0xb4cd98 ERR_SPELL_FAILED_NOTUNSHEATHED 1 NONE 0x44 10
327 0x141 321 0xb4cdac ERR_PETITION_OFFERED_S 0 NONE 0x44 10
328 0x142 322 0xb4cdc0 ERR_PETITION_SIGNED 0 igPlayerInviteAccept 0x44 10
329 0x143 323 0xb4cdd4 ERR_PETITION_SIGNED_S 0 igPlayerInviteAccept 0x44 10
330 0x144 324 0xb4cde8 ERR_PETITION_DECLINED_S 0 igPlayerInviteDecline 0x44 10
331 0x145 325 0xb4cdfc ERR_PETITION_ALREADY_SIGNED 2 igPlayerInviteDecline 0x44 10
332 0x146 326 0xb4ce10 ERR_PETITION_IN_GUILD 2 igPlayerInviteDecline 0x44 10
333 0x147 327 0xb4ce24 ERR_PETITION_CREATOR 2 igPlayerInviteDecline 0x44 10
334 0x148 328 0xb4ce38 ERR_PETITION_NOT_ENOUGH_SIGNATURES 2 igPlayerInviteDecline 0x44 10
335 0x149 329 0xb4ce4c ERR_PETITION_NOT_SAME_SERVER 2 igPlayerInviteDecline 0x44 10
336 0x14a 330 0xb4ce60 ERR_SPELL_UNLEARNED_S 0 NONE 0x44 10
337 0x14b 331 0xb4ce74 ERR_PET_SPELL_ROOTED 2 NONE 0x44 10
338 0x14c 332 0xb4ce88 ERR_PET_SPELL_AFFECTING_COMBAT 2 NONE 0x44 10
339 0x14d 333 0xb4ce9c ERR_PET_SPELL_OUT_OF_RANGE 2 NONE 0x44 10
340 0x14e 334 0xb4ceb0 ERR_PET_SPELL_NOT_BEHIND 2 NONE 0x44 10
341 0x14f 335 0xb4cec4 ERR_PET_SPELL_TARGETS_DEAD 2 NONE 0x44 10
342 0x150 336 0xb4ced8 ERR_PET_SPELL_DEAD 2 NONE 0x44 10
343 0x151 337 0xb4ceec ERR_PET_SPELL_NOPATH 2 NONE 0x44 10
344 0x152 338 0xb4cf00 ERR_ITEM_CANT_BE_DESTROYED 2 NONE 0x44 10
345 0x153 339 0xb4cf14 ERR_TICKET_ALREADY_EXISTS 2 NONE 0x44 10
346 0x154 340 0xb4cf28 ERR_TICKET_CREATE_ERROR 2 NONE 0x44 10
347 0x155 341 0xb4cf3c ERR_TICKET_UPDATE_ERROR 2 NONE 0x44 10
348 0x156 342 0xb4cf50 ERR_TICKET_DB_ERROR 2 NONE 0x44 10
349 0x157 343 0xb4cf64 ERR_TICKET_NO_TEXT 2 NONE 0x44 10
350 0x158 344 0xb4cf78 ERR_OBJECT_IS_BUSY 2 NONE 0x44 10
351 0x159 345 0xb4cf8c ERR_EXHAUSTION_WELLRESTED 0 NONE 0x44 10
352 0x15a 346 0xb4cfa0 ERR_EXHAUSTION_RESTED 0 NONE 0x44 10
353 0x15b 347 0xb4cfb4 ERR_EXHAUSTION_NORMAL 0 NONE 0x44 10
354 0x15c 348 0xb4cfc8 ERR_EXHAUSTION_TIRED 0 NONE 0x44 10
355 0x15d 349 0xb4cfdc ERR_EXHAUSTION_EXHAUSTED 0 NONE 0x44 10
356 0x15e 350 0xb4cff0 ERR_NO_ITEMS_WHILE_SHAPESHIFTED 2 NONE 0x44 10
357 0x15f 351 0xb4d004 ERR_CANT_INTERACT_SHAPESHIFTED 2 NONE 0x44 10
358 0x160 352 0xb4d018 ERR_MAIL_QUEST_ITEM 2 NONE 0x3b 10
359 0x161 353 0xb4d02c ERR_MAIL_BOUND_ITEM 2 NONE 0x3b 10
360 0x162 354 0xb4d040 ERR_MAIL_CONJURED_ITEM 2 NONE 0x44 10
361 0x163 355 0xb4d054 ERR_MAIL_BAG 2 NONE 0x44 10
362 0x164 356 0xb4d068 ERR_MAIL_TO_SELF 2 NONE 0x44 10
363 0x165 357 0xb4d07c ERR_MAIL_TARGET_NOT_FOUND 2 NONE 0x44 10
364 0x166 358 0xb4d090 ERR_MAIL_DATABASE_ERROR 2 NONE 0x44 10
365 0x167 359 0xb4d0a4 ERR_MAIL_SENT 1 NONE 0x44 10
366 0x168 360 0xb4d0b8 ERR_NOT_HAPPY_ENOUGH 2 NONE 0x44 10
367 0x169 361 0xb4d0cc ERR_USE_CANT_IMMUNE 2 NONE 0x44 10
368 0x16a 362 0xb4d0e0 ERR_CANT_BE_DISENCHANTED 2 NONE 0x44 10
369 0x16b 363 0xb4d0f4 ERR_CANT_USE_DISARMED 2 NONE 0x44 10
370 0x16c 364 0xb4d108 ERR_AUCTION_QUEST_ITEM 2 NONE 0x3b 10
371 0x16d 365 0xb4d11c ERR_AUCTION_BOUND_ITEM 2 NONE 0x3b 10
372 0x16e 366 0xb4d130 ERR_AUCTION_CONJURED_ITEM 2 NONE 0x44 10
373 0x16f 367 0xb4d144 ERR_AUCTION_WRAPPED_ITEM 2 NONE 0x44 10
374 0x170 368 0xb4d158 ERR_AUCTION_LOOT_ITEM 2 NONE 0x44 10
375 0x171 369 0xb4d16c ERR_AUCTION_BAG 2 NONE 0x44 10
376 0x172 370 0xb4d180 ERR_AUCTION_DATABASE_ERROR 2 NONE 0x44 10
377 0x173 371 0xb4d194 ERR_AUCTION_BID_OWN 2 NONE 0x44 10
378 0x174 372 0xb4d1a8 ERR_AUCTION_BID_INCREMENT 2 NONE 0x44 10
379 0x175 373 0xb4d1bc ERR_AUCTION_HIGHER_BID 2 NONE 0x44 10
380 0x176 374 0xb4d1d0 ERR_AUCTION_MIN_BID 2 NONE 0x44 10
381 0x177 375 0xb4d1e4 ERR_AUCTION_REPAIR_ITEM 2 NONE 0x44 10
382 0x178 376 0xb4d1f8 ERR_AUCTION_STARTED 0 NONE 0x44 10
383 0x179 377 0xb4d20c ERR_AUCTION_REMOVED 0 NONE 0x44 10
384 0x17a 378 0xb4d220 ERR_AUCTION_OUTBID_S 0 NONE 0x44 10
385 0x17b 379 0xb4d234 ERR_AUCTION_WON_S 0 NONE 0x44 10
386 0x17c 380 0xb4d248 ERR_AUCTION_SOLD_S 0 NONE 0x44 10
387 0x17d 381 0xb4d25c ERR_AUCTION_EXPIRED_S 0 NONE 0x44 10
388 0x17e 382 0xb4d270 ERR_AUCTION_REMOVED_S 0 NONE 0x44 10
389 0x17f 383 0xb4d284 ERR_AUCTION_BID_PLACED 0 NONE 0x44 10
390 0x180 384 0xb4d298 ERR_LOGOUT_FAILED 2 NONE 0x44 10
391 0x181 385 0xb4d2ac ERR_QUEST_PUSH_SUCCESS_S 0 NONE 0x44 10
392 0x182 386 0xb4d2c0 ERR_QUEST_PUSH_INVALID_S 0 NONE 0x44 10
393 0x183 387 0xb4d2d4 ERR_QUEST_PUSH_ACCEPTED_S 0 NONE 0x44 10
394 0x184 388 0xb4d2e8 ERR_QUEST_PUSH_DECLINED_S 0 NONE 0x44 10
395 0x185 389 0xb4d2fc ERR_QUEST_PUSH_TOO_FAR_S 0 NONE 0x44 10
396 0x186 390 0xb4d310 ERR_QUEST_PUSH_BUSY_S 0 NONE 0x44 10
397 0x187 391 0xb4d324 ERR_QUEST_PUSH_LOG_FULL_S 0 NONE 0x44 10
398 0x188 392 0xb4d338 ERR_QUEST_PUSH_ONQUEST_S 0 NONE 0x44 10
399 0x189 393 0xb4d34c ERR_QUEST_PUSH_ALREADY_DONE_S 0 NONE 0x44 10
400 0x18a 394 0xb4d360 ERR_RAID_GROUP_ONLY 2 NONE 0x44 10
401 0x18b 395 0xb4d374 ERR_RAID_GROUP_FULL 2 NONE 0x44 10
402 0x18c 396 0xb4d388 ERR_PVP_KILL_HONORABLE 1 NONE 0x44 10
403 0x18d 397 0xb4d39c ERR_PVP_KILL_DISHONORABLE 1 NONE 0x44 10
404 0x18e 398 0xb4d3b0 ERR_SPELL_FAILED_ALREADY_AT_FULL_HEALTH 2 NONE 0x44 10
405 0x18f 399 0xb4d3c4 ERR_SPELL_FAILED_ALREADY_AT_FULL_POWER_S 2 NONE 0x44 10
406 0x190 400 0xb4d3d8 ERR_AUTOLOOT_MONEY_S 0 NONE 0x44 10
407 0x191 401 0xb4d3ec ERR_GENERIC_STUNNED 2 NONE 0x44 10
408 0x192 402 0xb4d400 ERR_TARGET_STUNNED 2 NONE 0x44 10
409 0x193 403 0xb4d414 ERR_MUST_REPAIR_DURABILITY 2 NONE 0x44 10
410 0x194 404 0xb4d428 ERR_RAID_YOU_JOINED 0 NONE 0x44 10
411 0x195 405 0xb4d43c ERR_RAID_YOU_LEFT 0 NONE 0x44 10
412 0x196 406 0xb4d450 ERR_RAID_MEMBER_ADDED_S 0 NONE 0x44 10
413 0x197 407 0xb4d464 ERR_RAID_MEMBER_REMOVED_S 0 NONE 0x44 10
414 0x198 408 0xb4d478 ERR_CLICK_ON_ITEM_TO_FEED 2 NONE 0x44 10
415 0x199 409 0xb4d48c ERR_TOO_MANY_CHAT_CHANNELS 0 NONE 0x44 10
416 0x19a 410 0xb4d4a0 ERR_LOOT_ROLL_PENDING 2 NONE 0x44 10
417 0x19b 411 0xb4d4b4 ERR_LOOT_PLAYER_NOT_FOUND 2 NONE 0x44 10
418 0x19c 412 0xb4d4c8 ERR_NOT_IN_RAID 0 NONE 0x44 10
419 0x19d 413 0xb4d4dc ERR_LOGGING_OUT 2 NONE 0x44 10
420 0x19e 414 0xb4d4f0 ERR_TARGET_LOGGING_OUT 2 NONE 0x44 10
421 0x19f 415 0xb4d504 ERR_NOT_WHILE_MOUNTED 2 NONE 0x44 10
422 0x1a0 416 0xb4d518 ERR_NOT_WHILE_SHAPESHIFTED 2 NONE 0x44 10
423 0x1a1 417 0xb4d52c ERR_NOT_IN_COMBAT 2 NONE 0x44 10
424 0x1a2 418 0xb4d540 ERR_NOT_WHILE_DISARMED 2 NONE 0x44 10
425 0x1a3 419 0xb4d554 ERR_PET_BROKEN 2 NONE 0x44 10
426 0x1a4 420 0xb4d568 ERR_TALENT_WIPE_ERROR 2 NONE 0x44 10
427 0x1a5 421 0xb4d57c ERR_FEIGN_DEATH_RESISTED 2 NONE 0x44 10
428 0x1a6 422 0xb4d590 ERR_MEETING_STONE_IN_QUEUE_S 0 NONE 0x44 10
429 0x1a7 423 0xb4d5a4 ERR_MEETING_STONE_LEFT_QUEUE_S 0 NONE 0x44 10
430 0x1a8 424 0xb4d5b8 ERR_MEETING_STONE_OTHER_MEMBER_LEFT 0 NONE 0x44 10
431 0x1a9 425 0xb4d5cc ERR_MEETING_STONE_PARTY_KICKED_FROM_QUEUE 0 NONE 0x44 10
432 0x1aa 426 0xb4d5e0 ERR_MEETING_STONE_MEMBER_STILL_IN_QUEUE 0 NONE 0x44 10
433 0x1ab 427 0xb4d5f4 ERR_MEETING_STONE_SUCCESS 0 NONE 0x44 10
434 0x1ac 428 0xb4d608 ERR_MEETING_STONE_IN_PROGRESS 0 NONE 0x44 10
435 0x1ad 429 0xb4d61c ERR_MEETING_STONE_MEMBER_ADDED_S 0 NONE 0x44 10
436 0x1ae 430 0xb4d630 ERR_MEETING_STONE_GROUP_FULL 2 NONE 0x44 10
437 0x1af 431 0xb4d644 ERR_MEETING_STONE_NOT_LEADER 2 NONE 0x44 10
438 0x1b0 432 0xb4d658 ERR_MEETING_STONE_INVALID_LEVEL 2 NONE 0x44 10
439 0x1b1 433 0xb4d66c ERR_MEETING_STONE_MUST_BE_LEADER 2 NONE 0x44 10
440 0x1b2 434 0xb4d680 ERR_MEETING_STONE_NO_RAID_GROUP 2 NONE 0x44 10
441 0x1b3 435 0xb4d694 ERR_GUILDEMBLEM_SAME 2 NONE 0x44 10
442 0x1b4 436 0xb4d6a8 ERR_EQUIP_TRADE_ITEM 2 NONE 0x44 10
443 0x1b5 437 0xb4d6bc ERR_PVP_TOGGLE_ON 1 NONE 0x44 10
444 0x1b6 438 0xb4d6d0 ERR_PVP_TOGGLE_OFF 1 NONE 0x44 10
445 0x1b7 439 0xb4d6e4 ERR_GROUP_JOIN_BATTLEGROUND_DESERTERS 1 NONE 0x44 10
446 0x1b8 440 0xb4d6f8 ERR_GROUP_JOIN_BATTLEGROUND_S 1 NONE 0x44 10
447 0x1b9 441 0xb4d70c ERR_GROUP_JOIN_BATTLEGROUND_FAIL 2 NONE 0x44 10
448 0x1ba 442 0xb4d720 ERR_GROUP_JOIN_BATTLEGROUND_TOO_MANY 2 NONE 0x44 10
449 0x1bb 443 0xb4d734 ERR_INVALID_PROMOTION_CODE 2 NONE 0x44 10
450 0x1bc 444 0xb4d748 ERR_BG_PLAYER_JOINED_SS 0 NONE 0x44 10
451 0x1bd 445 0xb4d75c ERR_BG_PLAYER_LEFT_S 0 NONE 0x44 10
452 0x1be 446 0xb4d770 ERR_RESTRICTED_ACCOUNT 2 NONE 0x44 10
453 0x1bf 447 0xb4d784 ERR_PLAY_TIME_EXCEEDED 2 NONE 0x44 10
454 0x1c0 448 0xb4d798 ERR_APPROACHING_PARTIAL_PLAY_TIME 0 NONE 0x44 10
455 0x1c1 449 0xb4d7ac ERR_APPROACHING_NO_PLAY_TIME 0 NONE 0x44 10
456 0x1c2 450 0xb4d7c0 ERR_UNHEALTHY_TIME 0 NONE 0x44 10
457 0x1c3 451 0xb4d7d4 ERR_CHAT_RESTRICTED 0 NONE 0x44 10
458 0x1c4 452 0xb4d7e8 ERR_MAIL_REACHED_CAP 2 NONE 0x44 10
459 0x1c5 453 0xb4d7fc ERR_INVALID_RAID_TARGET 2 NONE 0x44 10
460 0x1c6 454 0xb4d810 ERR_RAID_LEADER_READY_CHECK_START_S 0 NONE 0x44 10
461 0x1c7 455 0xb4d824 ERR_READY_CHECK_IN_PROGRESS 2 NONE 0x44 10
462 0x1c8 456 0xb4d838 ERR_DUNGEON_DIFFICULTY_FAILED 0 NONE 0x44 10
463 0x1c9 457 0xb4d84c ERR_DUNGEON_DIFFICULTY_CHANGED_S 0 NONE 0x44 10
464 0x1ca 458 0xb4d860 ERR_TRADE_WRONG_REALM 2 NONE 0x44 10
465 0x1cb 459 0xb4d874 ERR_CHAT_PLAYER_AMBIGUOUS_S 0 NONE 0x44 10
466 0x1cc 460 0xb4d888 ERR_LOOT_CANT_LOOT_THAT_NOW 2 NONE 0x44 10
467 0x1cd 461 0xb4d89c ERR_LOOT_MASTER_INV_FULL 2 NONE 0x44 10
468 0x1ce 462 0xb4d8b0 ERR_LOOT_MASTER_UNIQUE_ITEM 2 NONE 0x44 10
469 0x1cf 463 0xb4d8c4 ERR_LOOT_MASTER_OTHER 2 NONE 0x44 10
470 0x1d0 464 0xb4d8d8 ERR_FILTERING_YOU_S 0 NONE 0x44 10
+1 -1
View File
@@ -50,7 +50,7 @@
# cfg-truncated / indirect-branch / iteration-cap = an edge the walk could not
# resolve; such a row's arity is not covered by the soundness claim above.
#
# regenerate: ~/wre/pyvenv/bin/python re/audit/binding-shapes.py > re/audit/binding-shapes.tsv
# regenerate (in wow-5875-re): python3 re/audit/binding-shapes.py > re/audit/binding-shapes.tsv
name fn pair_va table_va table_kind argc argc_conf arity arity_conf kinds kinds_conf notes
AbandonQuest 0x4dfe00 0x84b128 0x84b0f0 global 0 exact 0 exact () agree
AbandonSkill 0x4d3ab0 0x849d08 0x849cf8 global 1 exact 0 exact () agree
1 # Lua binding SHAPE table for WoW.exe 1.12.1 build 5875 — one row per registered
50 # cfg-truncated / indirect-branch / iteration-cap = an edge the walk could not
51 # resolve; such a row's arity is not covered by the soundness claim above.
52 #
53 # regenerate: ~/wre/pyvenv/bin/python re/audit/binding-shapes.py > re/audit/binding-shapes.tsv # regenerate (in wow-5875-re): python3 re/audit/binding-shapes.py > re/audit/binding-shapes.tsv
54 name
55 AbandonQuest
56 AbandonSkill
+25 -8
View File
@@ -5,7 +5,10 @@ against them on any day without a re-derivation. Not documentation, not state: d
generator beside it in `scripts/`.
Nothing here is Blizzard content. A catalogue of *names* is not the thing named — the same reason
`decisions/` may quote a byte address and `MAP.md` may name a file.
a comment may quote a byte address and `docs/MAP.md` may name a file. The `1.12-*.tsv` files are
the surface benilla's gates read; the three `1.12-event-*.tsv` / `1.12-message-catalog.tsv`
tables are the inputs two of the generators run from, vendored from wow-5875-re so that
regeneration needs no sibling checkout.
## `1.12-globals.tsv`
@@ -34,7 +37,7 @@ corpus:
| origin | count | meaning |
|---|---|---|
| `engine` | 1,104 | the C client provides it. **benilla implements these in Rust.** |
| `framexml` | 20,427 | the shipped UI defines it. **benilla transcribes these into `assets/ui/`.** |
| `framexml` | 20,427 | the shipped UI defines it. **The stock files run off the player's own install** (`ui_script::reference_ui`). |
| `lua` | 24 | Lua 5.0's own runtime. mlua gives us these. |
Of the `framexml` rows, **1,986 carry type `lod`**: defined by the shipped UI but absent from the
@@ -48,9 +51,9 @@ wholesale: a name defined in shipped Lua is FrameXML's, and hardcoding it in Rus
error even when it works.
**Regenerating** — `scripts/gen-reference-globals.py`, whose header carries the full method and
the traps. It needs the director's 1.12 install and the sibling RE repo, so it is a manual
regeneration like `genmap.sh`, not something CI can run. The artifact is stable: it describes a
client that shipped in 2006.
the traps. It needs a 1.12 install and the RE repo's runtime capture of the reference client's
`_G` (`WOW_RE=<the RE repo>`), so it is a manual regeneration like `genmap.sh`, not something
CI can run. The artifact is stable: it describes a client that shipped in 2006.
**Reading it** — `scripts/api-coverage.sh` is the instrument. It asks a real `UiScript::new()`
what benilla exposes and reports `have / missing / beyond-1.12`, always with 1178's split.
@@ -61,8 +64,20 @@ global that 1.12 does not have and nobody wrote down why.
wow-5875-re's binding-shape table (what each Lua verb takes and returns; decision 1842, gated by
`ui_script/shape_gate.rs`) and the event table with the arguments each producer pushes
(decision 2140, `scripts/gen-reference-events.py`, gated by `ui_script/event_shape_gate.rs`).
Each file's header carries its column contract.
(decision 2140, gated by `ui_script/event_shape_gate.rs`). Each file's header carries its column
contract.
**Regenerating** — `1.12-shapes.tsv` is vendored as wow-5875-re writes it.
`scripts/gen-reference-events.py` derives `1.12-events.tsv` from two more vendored tables,
`1.12-event-catalog.tsv` (event id → name, off the client's name-pointer array) and
`1.12-event-firesites.tsv` (every fire site with the format string it pushes), so it runs from
any clone and reproduces the committed file byte for byte.
## `1.12-message-catalog.tsv`
The client's message registry (`0xb4b498`: id, key, kind, sound, chat type), vendored from
wow-5875-re; `scripts/gen-message-catalog.py` re-shapes it into
`crates/benilla-ui/src/messages/catalog.rs`, the table `CGGameUI::DisplayError` indexes.
## `1.12-verb-events.tsv`
@@ -79,7 +94,9 @@ getter's fire is a cache-miss re-query, a state event benilla's feeds fire off t
site is attributed to a function against wow-re's verified extents (the ledgers' `size=`) plus the
disassembly's padding boundaries, and a site past its candidate's extent is left out rather than
mis-filed (decision 2257: four of the first table's rows were callbacks reached by pointer, or a
site past an extent). It reads the sibling RE repo's disassembly, so it is manual, like the others.
site past an extent). The fire sites and the shapes are the vendored tables above; the function
names, the ledgers and the disassembly are the RE repo's (`WOW_RE=<the RE repo>`), so this one
is a manual regeneration there.
**Reading it** — `crates/benilla-app/src/ui_script/verb_event_gate.rs` is the gate: for every
pair whose verb benilla registers, the module that registers the verb fires the event, or the
+1 -1
View File
@@ -18,7 +18,7 @@
# demand. The real-addon half of the question belongs to the vanilla addon harness (1188 phase 6).
#
# **The addon corpora are not 1.12 codebases, and that shapes what a grep over them means.**
# `~/dev/wow-addons-top20` and `~/dev/wow-addons-vanilla` hold addons that RUN on 1.12 — which is
# The vanilla addon corpus (`benilla_formats::addon_corpus`) holds addons that RUN on 1.12 — which is
# not the same as addons written for 1.12 and nothing else. Most of the big ones ship one codebase
# for several clients and pick a path at load: pfUI registers every module with a version list
# (`RegisterModule("loot", "vanilla:tbc", …)`, matched against `pfUI.expansion`), pfQuest keeps a
+13 -5
View File
@@ -12,7 +12,7 @@
# Scope rule — conservative by construction, unknown means FULL, never "probably fine":
# crates/<dir>/** → that crate (owner read off cargo metadata, not the dir name)
# assets/ui/** → benilla-app (ui_script's shipped-UI tests read it at runtime)
# *.md → no gate can change (same rule as wt.sh docs_only)
# *.md → no gate can change (gates.sh's docs-only rule)
# anything else → escalate: exec scripts/gates.sh (workspace manifests, .cargo/,
# rust-toolchain, scripts/, shaders outside crates, …)
# Change set = fork point vs main + staged + unstaged + untracked — the round's work, not the last
@@ -48,7 +48,7 @@ dirs=""
full_reason=""
while IFS= read -r f; do
case "$f" in
docs/* | *.md) ;; # provably gate-inert (wt.sh docs_only)
docs/* | *.md) ;; # provably gate-inert (gates.sh's docs-only rule)
assets/ui/*) dirs="$dirs benilla-app-DIR:crates/benilla-app" ;;
crates/*/*) d="${f#crates/}"; dirs="$dirs DIR:crates/${d%%/*}" ;;
*) full_reason="$f" ;;
@@ -109,8 +109,9 @@ for p in $pkgs; do pflags="$pflags -p $p"; done
echo "check: scope = $pkgs"
echo "check: (changed crates + everything that depends on them; full chain still runs at sync→land)"
log="$(mktemp -t benilla-check)"
trap 'rm -f "$log"' EXIT
log="$(mktemp "${TMPDIR:-/tmp}/benilla-check.XXXXXX")"
skips="$(mktemp "${TMPDIR:-/tmp}/benilla-check-skips.XXXXXX")"
trap 'rm -f "$log" "$skips"' EXIT
run() {
local name="$1"; shift
if ! "$@" >"$log" 2>&1; then
@@ -123,7 +124,14 @@ run() {
run fmt cargo fmt --all -- --check
run clippy cargo clippy $pflags --all-targets -- -D warnings
run test cargo test $pflags
run test env BENILLA_SKIP_LOG="$skips" cargo test $pflags
# The data-gated tests skip without the install or the addon corpus, and libtest swallows the
# line; the resolver logs each skip to $BENILLA_SKIP_LOG and this counts them (gates.sh says why).
if [ -s "$skips" ]; then
echo " test: $(wc -l <"$skips" | tr -d ' ') data-gated tests SKIPPED on this machine —"
sort "$skips" | uniq -c | sort -rn | sed 's/^ *\([0-9]*\) \(.*\)/ \1 × \2/'
echo " (they run where the data is: docs/CONTRIBUTING.md, \"Setting up\")"
fi
echo "CHECK GREEN (scoped: $pkgs)"
echo " (land still pays the full chain once: scripts/gates.sh — memoized, so an already-gated tree is free)"
+6 -12
View File
@@ -28,8 +28,8 @@
# scripts/cine.sh 81 --at "-8913,554,94,0"
# scripts/cine.sh 81 --keep # keep the raw log and trace paths, printed at the end
#
# The probe identity is SLOT-KEYED off this worktree's path (`docs/METHOD.md`): pool-N logs in as
# probeN. Never the director's account.
# The probe identity is the checkout's `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR
# (scripts/probe-identity.sh).
set -euo pipefail
cd "$(dirname "$0")/.."
@@ -47,15 +47,9 @@ while [ $# -gt 0 ]; do
esac
done
# Slot-keyed probe identity (docs/METHOD.md, "The local vmangos server").
slot="$(basename "$PWD")"
n="${slot#pool-}"
case "$n" in
0) word=zero ;; 1) word=one ;; 2) word=two ;; 3) word=three ;; 4) word=four ;;
5) word=five ;; 6) word=six ;; 7) word=seven ;; 8) word=eight ;; 9) word=nine ;;
*) echo "cine.sh: not in a wt.sh pool slot ($slot) — claim one first (docs/METHOD.md)" >&2; exit 2 ;;
esac
char="Probe$word"
. "$PWD/scripts/probe-identity.sh"
probe_identity cine.sh "$PWD" || exit 2
char="$PROBE_CHAR"
work="$(mktemp -d)"
cfg="$work/cfg"
@@ -79,7 +73,7 @@ echo "cine.sh: cinematic $id as $char, parked at $px $py $pz (map $pmap)"
echo "cine.sh: this takes about $((exit_at + 20)) s — the shot is played in full so the END is measured too"
env BENILLA_HOME="$cfg" WOW_UNATTENDED=1 \
WOW_USER="probe$n" WOW_PASS="pprobe$n" WOW_CHAR="$char" \
WOW_USER="$PROBE_USER" WOW_PASS="$PROBE_PASS" WOW_CHAR="$char" \
WOW_PROBE_CHAT=".go xyz $px $py $pz $pmap;.debug play cinematic $id" \
WOW_PROBE_CHAT_AT="$park_at" WOW_PROBE_CHAT_EVERY="$((play_at - park_at))" \
${key:+WOW_PROBE_KEY="$key@$key_at:3"} \
-174
View File
@@ -1,174 +0,0 @@
#!/usr/bin/env bash
# crosscheck — compile benilla for the platforms the gates cannot see.
#
# Why it exists (decision 1920, bug B356): `scripts/gates.sh` runs on the developer's Mac, so a
# file behind `cfg(not(target_os = "macos"))` is not merely unverified — it is INVISIBLE. Decision
# 1857 shipped a non-macOS audio device layer that opened nothing, every Linux and Windows build
# went silent in the 09-02 sync, and nothing in this repo could have said so, because nothing here
# had ever compiled `benilla-app` for another target. This is that missing compile.
#
# scripts/crosscheck.sh [linux|windows|all] (default: all)
#
# Run it whenever platform-conditional code changes — a `cfg(target_os)`, a `[target.'cfg(…)']`
# dependency, a `#[link]`, an `extern "system"`. It is NOT part of `gates.sh`: it costs a container
# and a second dependency graph, and most commits touch no platform seam. It is a land-time check
# for the ones that do.
#
# Scope is `-p benilla-app --lib`, deliberately: that is the crate every platform seam lives in and
# the one players run, and checking it pulls benilla-world/-assets/-formats/-ui/-protocol with it.
#
# What each arm needs, once:
# linux docker (colima is fine) — the container installs its own ALSA/udev headers.
# The source is rsynced to ~/.benilla-crosscheck/src first: a worktree on an external
# volume is not visible inside the VM, and $HOME is. The container is pinned to THIS
# machine's rustc version, not to `rust-toolchain.toml`'s `stable`: the image's stable
# ran ahead of the host's on the first run and reported eight `chunks_exact_to_as_chunks`
# findings across four crates — a newer clippy's opinion, nothing to do with Linux. The
# question this script asks is "does OUR toolchain build this for another platform".
# windows rustup target add x86_64-pc-windows-gnu + brew install mingw-w64
# (blake3 assembles for the target, so `cargo check` needs the cross assembler even
# though it never links. The *-gnu target is used rather than *-msvc for the same
# reason: `ml64.exe` has no substitute here, and nothing we compile differs between
# the two ABIs at the type level.)
#
# A missing prerequisite is a FAILURE, not a skip. The whole point is that a platform nobody
# compiled is a platform nobody supports; a green line for an arm that never ran would recreate
# exactly the hole this script closes.
set -uo pipefail
arm="${1:-all}"
here="$(cd "$(dirname "$0")/.." && pwd)"
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
[ -n "$root" ] && [ -f "$root/scripts/crosscheck.sh" ] || root="$here"
cd "$root" || exit 1
echo "cross-checking: $root"
# ── Green-stamp memoization (decision 2331; the shape of gates.sh's, 1822) ─────────────────────
# `wt.sh land` runs this script on the landing tree whenever the diff touches a platform seam, and
# a session that already ran it on the same tree should not pay the container twice. The key is
# what decided the verdict: the WORKING TREE's true content hash (a throwaway index, so `target/`
# and the install links stay ignored), the arm, this script, and rustc. CROSSCHECK_FORCE=1 re-runs.
stamp="target/.crosscheck-green"
tree_key() {
local tmpidx t
tmpidx="$(mktemp -t benilla-xcheck-idx)" && rm -f "$tmpidx" || return 1
t="$( (export GIT_INDEX_FILE="$tmpidx"
git read-tree HEAD && git add -A . && git write-tree) 2>/dev/null )"
rm -f "$tmpidx"
[ -n "$t" ] || return 1
printf '%s|%s|%s|%s' "$t" "$arm" "$(rustc -V 2>/dev/null)" \
"$(shasum "$root/scripts/crosscheck.sh" 2>/dev/null | cut -d' ' -f1)"
}
# A docs-only delta keeps the verdict, as in gates.sh (2049): a record or a stub landing on the
# stamped tree changes nothing any compiler reads.
docs_only_delta() { # $1 = stamped tree, $2 = current tree
git cat-file -e "$1^{tree}" 2>/dev/null && git cat-file -e "$2^{tree}" 2>/dev/null || return 1
[ -z "$(git diff-tree -r --name-only "$1" "$2" 2>/dev/null | grep -vE '^(docs/|[^/]*\.md$)' || true)" ]
}
key_start="$(tree_key 2>/dev/null || true)"
if [ "${CROSSCHECK_FORCE:-}" != "1" ] && [ -n "$key_start" ] && [ -f "$stamp" ]; then
old="$(cat "$stamp" 2>/dev/null)"
memo=""
if [ "$old" = "$key_start" ]; then
memo="this exact tree already passed"
elif [ "${old#*|}" = "${key_start#*|}" ] && docs_only_delta "${old%%|*}" "${key_start%%|*}"; then
memo="this tree differs from one that passed only in top-level *.md"
fi
if [ -n "$memo" ]; then
echo "CROSS-CHECK GREEN ($arm — memoized: $memo; CROSSCHECK_FORCE=1 re-runs)"
exit 0
fi
fi
WIN_TARGET=x86_64-pc-windows-gnu
LINUX_IMAGE=rust:1-bookworm
STAGE="$HOME/.benilla-crosscheck/src"
failed=0
note() { printf '\ncrosscheck: %s\n' "$*"; }
fail() { printf '\nCROSSCHECK FAILED: %s\n' "$*"; failed=1; }
run_windows() {
note "windows — cargo clippy --target $WIN_TARGET"
if ! rustup target list --installed 2>/dev/null | grep -qx "$WIN_TARGET"; then
fail "windows: target $WIN_TARGET is not installed (rustup target add $WIN_TARGET)"
return
fi
if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
fail "windows: no x86_64-w64-mingw32-gcc (brew install mingw-w64) — blake3 needs it"
return
fi
if cargo clippy -p benilla-app --lib --target "$WIN_TARGET" -- -D warnings; then
note "windows OK"
else
fail "windows: clippy -D warnings"
fi
}
run_linux() {
note "linux — cargo clippy in $LINUX_IMAGE"
if ! docker info >/dev/null 2>&1; then
fail "linux: docker is not running (colima start)"
return
fi
mkdir -p "$STAGE" || { fail "linux: cannot create $STAGE"; return; }
# Anchored excludes ('/target/', not 'target/'): an unanchored pattern also matches
# `crates/benilla-app/src/target/`, and the missing module then reports as four unrelated
# type errors deep in bevy. Cost the first run of this script twenty minutes.
if ! rsync -a --delete \
--exclude '/target/' --exclude '/.git' --exclude '/WoW' --exclude '/WoW-era' \
--exclude '/reference/' "$root"/ "$STAGE"/; then
fail "linux: rsync to $STAGE"
return
fi
local version
version="$(rustc -V | awk '{print $2}')"
[ -n "$version" ] || { fail "linux: could not read this machine's rustc version"; return; }
note "linux — pinned to rustc $version (this machine's)"
docker volume create benilla-xcheck-target >/dev/null 2>&1
docker volume create benilla-xcheck-cargo >/dev/null 2>&1
docker volume create benilla-xcheck-rustup >/dev/null 2>&1
if docker run --rm \
-v "$STAGE":/src \
-v benilla-xcheck-target:/xtarget \
-v benilla-xcheck-cargo:/usr/local/cargo/registry \
-v benilla-xcheck-rustup:/usr/local/rustup \
-w /src -e CARGO_TARGET_DIR=/xtarget -e CARGO_BUILD_JOBS="${CROSSCHECK_JOBS:-4}" \
-e RUSTUP_TOOLCHAIN="$version" \
"$LINUX_IMAGE" bash -c '
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null 2>&1
apt-get install -y -qq pkg-config libasound2-dev libudev-dev >/dev/null 2>&1
rustup toolchain install -c clippy --profile minimal "$RUSTUP_TOOLCHAIN"
rustc -V
cargo clippy -p benilla-app --lib -- -D warnings
'; then
note "linux OK"
else
fail "linux: clippy -D warnings"
fi
}
case "$arm" in
linux) run_linux ;;
windows) run_windows ;;
all) run_windows; run_linux ;;
*) echo "usage: $0 [linux|windows|all]"; exit 2 ;;
esac
if [ "$failed" -eq 0 ]; then
# Stamp only a tree nobody edited under the run (gates.sh's rule): a changed key is no stamp.
key_end="$(tree_key 2>/dev/null || true)"
if [ -n "$key_end" ] && [ "$key_end" = "$key_start" ]; then
mkdir -p target && printf '%s\n' "$key_end" > "$stamp" 2>/dev/null || true
fi
echo
echo "CROSS-CHECK GREEN ($arm)"
echo " (compiling is not sounding: WOW_AUDIO_LIVE=1 cargo test -p benilla-app --lib sound::output::"
echo " runs the output layer against the machine's real device — decision 1920)"
exit 0
fi
echo
echo "CROSS-CHECK RED"
exit 1
+32 -16
View File
@@ -9,10 +9,10 @@
# the social arc, then again at the 6a land). Run this instead of composing pipelines.
set -uo pipefail
# Gate the tree you are STANDING IN, not the one this file happens to live in. Every session works
# in its own worktree (docs/METHOD.md) and reaches for the gates by absolute path — `scripts/gates.sh`
# resolved from `$0`, which is the primary checkout. That silently gated the PRIMARY: fmt-red work
# in a worktree reported ALL GATES GREEN, because the primary is always clean.
# Gate the tree you are STANDING IN, not the one this file happens to live in. A machine with
# several checkouts reaches for the gates by absolute path — `scripts/gates.sh` resolved from
# `$0`, which is another checkout. That silently gated the wrong one: fmt-red work in one
# worktree reported ALL GATES GREEN, because the other was clean.
#
# So: the git toplevel of $PWD, as long as it is a checkout of THIS repo (it has this script) — a
# stray invocation from a sibling repo like wow-5875-re falls back rather than gating that repo with
@@ -26,7 +26,7 @@ echo "gating: $root"
# ── Green-stamp memoization (decision 1822) ──────────────────────────────────────────────────────
# A green chain stamps target/.gates-green with a key of exactly what determined the verdict: the
# WORKING TREE's true content hash (untracked files included — hashed through a throwaway index, the
# real one untouched, so `target/`, `WoW` and `.wt-claimed` stay excluded by the ignore rules), the
# real one untouched, so `target/` and `WoW` stay excluded by the ignore rules), the
# toolchain, this script itself, and where the install resolver points. Re-running on an unchanged
# tree is then instant instead of ~5–8 min — which is what a land after an already-gated sync, or a
# "once more to be sure", actually costs the machine. GATES_FORCE=1 runs the chain regardless.
@@ -35,7 +35,7 @@ echo "gating: $root"
stamp="target/.gates-green"
tree_key() {
local tmpidx t
tmpidx="$(mktemp -t benilla-gates-idx)" && rm -f "$tmpidx" || return 1
tmpidx="$(mktemp "${TMPDIR:-/tmp}/benilla-gates-idx.XXXXXX")" && rm -f "$tmpidx" || return 1
t="$( (export GIT_INDEX_FILE="$tmpidx"
git read-tree HEAD && git add -A . && git write-tree) 2>/dev/null )"
rm -f "$tmpidx"
@@ -46,11 +46,10 @@ tree_key() {
resolve_wow() { cargo run -q -p benilla-formats --example where 2>/dev/null || true; }
# **A docs-only delta keeps the verdict** (decision 2049). The stamped tree and the current one are
# both real tree objects (write-tree puts them in the object store), so git can say exactly what
# differs. If every differing path is a top-level `*.md` — wt.sh's
# `docs_only` rule (0979): nothing compiles, tests, formats or `include_str!`s those, checked — then
# the chain's outcome on this tree IS the stamped one, by construction rather than by optimism. This
# is what lets `wt.sh land`'s worker land a tree the session already gated after a rebase brought in
# after its own map regeneration, without paying the chain again.
# differs. If every differing path is under `docs/` or a top-level `*.md` — nothing compiles,
# tests, formats or `include_str!`s those, checked — then the chain's outcome on this tree IS the
# stamped one, by construction rather than by optimism. This is what lets a landing that only
# regenerated the map after a rebase go through without paying the chain again.
# A stamped tree that the object store has since pruned simply fails the check, and the chain runs.
docs_only_delta() { # $1 = stamped tree, $2 = current tree
git cat-file -e "$1^{tree}" 2>/dev/null && git cat-file -e "$2^{tree}" 2>/dev/null || return 1
@@ -75,8 +74,23 @@ if [ "${GATES_FORCE:-}" != "1" ] && [ -n "$key_start" ] && [ -f "$stamp" ]; then
fi
fi
log="$(mktemp -t benilla-gates)"
trap 'rm -f "$log"' EXIT
log="$(mktemp "${TMPDIR:-/tmp}/benilla-gates.XXXXXX")"
skips="$(mktemp "${TMPDIR:-/tmp}/benilla-gates-skips.XXXXXX")"
trap 'rm -f "$log" "$skips"' EXIT
# **How hollow was that green?** The data-gated tests pass without asserting on a machine that
# lacks the install or the addon corpus, and libtest swallows the skip line — so `install.rs`'s
# `skipped` appends one to `$BENILLA_SKIP_LOG` instead, and this reads the file back after each
# test rung. A clone without the data sees the number rather than a green it cannot weigh
# (docs/CONTRIBUTING.md, "Setting up"). Where the data is, `BENILLA_REQUIRE_DATA` has already
# made every skip a failure and the file stays empty.
report_skips() { # $1 = rung name
[ -s "$skips" ] || return 0
echo " $1: $(wc -l <"$skips" | tr -d ' ') data-gated tests SKIPPED on this machine —"
sort "$skips" | uniq -c | sort -rn | sed 's/^ *\([0-9]*\) \(.*\)/ \1 × \2/'
echo " (they run where the data is: docs/CONTRIBUTING.md, \"Setting up\")"
: >"$skips"
}
# ── Timing (decision 2265 §C1) ──────────────────────────────────────────────────────────────────
# Every gate's wall seconds, printed beside its verdict and appended to `target/.gates-timing`
@@ -118,7 +132,7 @@ run clippy cargo clippy --workspace --all-targets -- -D warnings
# after the pool moved drives. Where both resolve, `BENILLA_REQUIRE_DATA=1` turns a skip into the
# failure it is. Both, not either: with the install alone every corpus test would fail for the
# honest reason. Ask the resolver for the install (the same `where` the player-tests rung and the
# enforcer use); the corpus is the symlink `wt.sh link_wow` lays at the tree's root.
# enforcer use); the corpus is the `wow-addons-vanilla` link at the tree's root.
wow_data="$(resolve_wow)"
require_data=""
if [ -n "$wow_data" ] && [ -d "$root/wow-addons-vanilla" ]; then
@@ -128,7 +142,8 @@ else
"corpus=$([ -d "$root/wow-addons-vanilla" ] && echo yes || echo no)):" \
"data-gated tests skip silently on this machine"
fi
run test env ${require_data:+BENILLA_REQUIRE_DATA=1} cargo test --workspace
run test env ${require_data:+BENILLA_REQUIRE_DATA=1} BENILLA_SKIP_LOG="$skips" cargo test --workspace
report_skips test
# **doc-links** (decision 1925) — the docs are the knowledge base, so a doc link pointing at a
# DELETED item is rot, and nothing else here runs rustdoc. Deliberately narrow: it fails only on a
@@ -185,8 +200,9 @@ run player-build cargo build -p benilla --no-default-features
# and let the client-data tests say so themselves (`BENILLA_REQUIRE_DATA` is deliberately NOT
# handed down: rung 2 is compiled out here, and what this rung falsifies is the player ladder,
# not the resolver — a skip here would be the honest one).
run player-tests env ${wow_data:+WOW_DATA="$wow_data"} \
run player-tests env ${wow_data:+WOW_DATA="$wow_data"} BENILLA_SKIP_LOG="$skips" \
cargo test -p benilla-formats -p benilla-app --no-default-features --lib
report_skips player-tests
# **The enforcer** (decision 1160's second binary, made into a gate by 1164). `benilla-worldview`
# boots the engine plugin set with no server, no login, no UI and no player, and any system whose
+22 -11
View File
@@ -1,25 +1,32 @@
#!/usr/bin/env python3
r"""Generate `crates/benilla-ui/src/messages/catalog.rs` from wow-5875-re's message-catalog.tsv.
r"""Generate `crates/benilla-ui/src/messages/catalog.rs` from `reference/1.12-message-catalog.tsv`.
The TSV is the sibling repo's byte-exact sweep of build 5875's `0xb4b498` registry -- the table
`CGGameUI::DisplayError 0x496720` indexes by message id. It is *their* artifact (regenerated there
with `re/ui/extract-message-catalog.py`); this script only re-shapes it into a Rust const, so the
table ships with benilla instead of being re-read out of a sibling checkout at build time.
The TSV is wow-5875-re's byte-exact sweep of build 5875's `0xb4b498` registry -- the table
`CGGameUI::DisplayError 0x496720` indexes by message id -- vendored under `reference/` like the
other tables (it is regenerated there with `re/ui/extract-message-catalog.py`). This script only
re-shapes it into a Rust const, so the table ships with benilla and is never read out of a
sibling checkout.
python3 scripts/gen-message-catalog.py ../wow-5875-re/re/ui/message-catalog.tsv
python3 scripts/gen-message-catalog.py # the vendored table -> the .rs, in place
python3 scripts/gen-message-catalog.py <tsv> # another table
Refuses rather than emits on anything it does not understand: a non-dense id range, a duplicate key,
an unknown kind. A generated table that quietly dropped a row would be worse than no table.
"""
import os
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TABLE = os.path.join(REPO, "reference", "1.12-message-catalog.tsv")
OUT = os.path.join(REPO, "crates", "benilla-ui", "src", "messages", "catalog.rs")
KINDS = {0: "Chat", 1: "Info", 2: "Error"}
HEADER = """\
//! The 5875 message catalog, row for row.
//!
//! @generated by `scripts/gen-message-catalog.py` from wow-5875-re `re/ui/message-catalog.tsv`
//! (itself generated by that repo's `re/ui/extract-message-catalog.py` out of `WoW.exe`).
//! @generated by `scripts/gen-message-catalog.py` from `reference/1.12-message-catalog.tsv`
//! (wow-5875-re's `re/ui/message-catalog.tsv`, extracted out of `WoW.exe` there).
//! Do not edit by hand — regenerate.
use super::{MessageRecord, MsgKind};
@@ -36,6 +43,8 @@ pub static CATALOG: [MessageRecord; {count}] = [
def main(path: str) -> int:
if not os.path.exists(path):
raise SystemExit(f"no table at {path}")
rows = []
for line in open(path):
if line.startswith("#") or line.startswith("id_hex"):
@@ -75,11 +84,13 @@ def main(path: str) -> int:
f'type_tag: {r["type_tag"]:#04x}, chat_type: {r["chat_type"]} }},\n'
)
out.append("];\n")
sys.stdout.write("".join(out))
with open(OUT, "w") as f:
f.write("".join(out))
print(f"wrote {os.path.relpath(OUT, REPO)} ({len(rows)} rows)")
return 0
if __name__ == "__main__":
if len(sys.argv) != 2:
if len(sys.argv) > 2:
raise SystemExit(__doc__)
sys.exit(main(sys.argv[1]))
sys.exit(main(sys.argv[1] if len(sys.argv) == 2 else TABLE))
+19 -12
View File
@@ -2,7 +2,7 @@
"""Regenerate `reference/1.12-events.tsv` — every FrameScript event the 1.12.1 client can
dispatch, with the ARGUMENTS its producers push.
scripts/gen-reference-events.py [--wow-re DIR] [--out FILE]
scripts/gen-reference-events.py [--catalog FILE] [--firesites FILE] [--out FILE]
**Why this exists** (decision 2140). benilla already gates two halves of the event seam: a stock
chain file listening for an event nothing fires
@@ -15,12 +15,13 @@ instances: `PLAYERBANKSLOTS_CHANGED`, `UNIT_PET_EXPERIENCE` and `UNIT_PET_TRAINI
last of which routes through `PetPaperDollFrame_OnEvent`'s `elseif ( arg1 == "pet" )` catch-all and
so reached nobody at all.
**The oracle is wow-5875-re's own two censuses**, vendored the way `1.12-shapes.tsv` is:
**The oracle is wow-5875-re's own two censuses**, vendored under `reference/` the way
`1.12-shapes.tsv` is:
- `re/events/event-catalog.tsv` eventId -> name, off the name-pointer array at `.data 0xbe1198`
- `re/events/event-firesites.tsv` every `call` AND tail-`jmp` to `FrameScript_SignalEvent`
(`0x703e50`, no varargs) and `SignalEvent2` (`0x703f50`,
printf-style), with the FORMAT STRING each site pushes
- `1.12-event-catalog.tsv` eventId -> name, off the name-pointer array at `.data 0xbe1198`
- `1.12-event-firesites.tsv` every `call` AND tail-`jmp` to `FrameScript_SignalEvent`
(`0x703e50`, no varargs) and `SignalEvent2` (`0x703f50`,
printf-style), with the FORMAT STRING each site pushes
**Three producer families, because a literal fire site is not the whole picture.** The firesites
census is over the two signal helpers' own call sites, and its own header says in capitals that an
@@ -55,8 +56,8 @@ same integer names a different event on the glue screen than in the world. wow-r
rows' names; this drops them by fire-site address as well, from the four TU ranges its header
names, so a future re-derivation that fills a name in cannot leak one through.
**The input lives outside the repo** (the sibling RE repo), so this is a manual regeneration, like
`gen-reference-globals.py` — not something CI can run.
Both inputs are in the repo, so this runs from any clone and reproduces the committed table byte
for byte; only the two inputs themselves are regenerated in the RE repo.
"""
import argparse
import os
@@ -75,7 +76,12 @@ UNIT_WINDOW_FIELDS = 0xB6
NONE = "()"
REFERENCE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "reference"))
def rows(path):
if not os.path.exists(path):
sys.exit(f"no table at {path}")
for line in open(path, encoding="utf-8"):
if line.startswith("#"):
continue
@@ -87,12 +93,13 @@ def rows(path):
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--wow-re", default=os.path.expanduser("~/dev/wow-5875-re"))
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "..", "reference", "1.12-events.tsv"))
ap.add_argument("--catalog", default=os.path.join(REFERENCE, "1.12-event-catalog.tsv"))
ap.add_argument("--firesites", default=os.path.join(REFERENCE, "1.12-event-firesites.tsv"))
ap.add_argument("--out", default=os.path.join(REFERENCE, "1.12-events.tsv"))
a = ap.parse_args()
catalog = {}
for f in rows(os.path.join(a.wow_re, "re/events/event-catalog.tsv")):
for f in rows(a.catalog):
if len(f) >= 2 and f[1]:
catalog.setdefault(int(f[0]), f[1])
if len(catalog) < 300:
@@ -110,7 +117,7 @@ def main():
e["notes"].append(note)
dropped_glue = 0
for f in rows(os.path.join(a.wow_re, "re/events/event-firesites.tsv")):
for f in rows(a.firesites):
if len(f) < 4:
continue
va = int(f[0], 16)
+11 -5
View File
@@ -2,7 +2,7 @@
"""Regenerate `reference/1.12-globals.tsv` — the 1.12.1 client's global namespace, with each name
attributed to whoever provides it.
scripts/gen-reference-globals.py [--wow-re DIR] [--out FILE]
WOW_RE=<the RE repo> scripts/gen-reference-globals.py [--wow-re DIR] [--out FILE]
**Why this exists** (decision 1189): every wrong number in the addon arc came from remembering a
list of WoW functions instead of asking the client. wow-5875-re already ships an addon
@@ -30,8 +30,9 @@ Attribution is by *definition site*, computed from a complete 1.12 shipped-UI co
- LUA_5_0 below wins over all of the above: FrameXML *clobbers* `string` in three files
(`string = getglobal(...)`), which overwrites the stdlib table rather than defining it.
**Inputs live outside the repo** — the director's 1.12 install and the sibling RE repo — so this
is a manual regeneration, like `genmap.sh`, not something CI can run. It needs:
**Inputs live outside the repo** — a 1.12 install and the RE repo's runtime capture (`--wow-re`,
or `$WOW_RE`) — so this is a manual regeneration, like `genmap.sh`; the committed table is the
surface benilla tracks. It needs:
- `<wow-re>/WoW/_w5875_fixtures/item13/W5875Capture.lua` the captured in-world `_G`
- `<wow-re>/WoW/_extracted_framexml/` FrameXML, already extracted there
@@ -50,7 +51,6 @@ import re
import subprocess
import sys
WOWRE_DEFAULT = "/Users/sam/dev/wow-5875-re"
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# The twelve addons that ship inside the MPQs. Their folders exist in a real install carrying only
@@ -249,9 +249,15 @@ def composed(name, roots, suffixes, depth=0):
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--wow-re", default=WOWRE_DEFAULT)
ap.add_argument("--wow-re", default=os.environ.get("WOW_RE"))
ap.add_argument("--out", default=os.path.join(REPO, "reference", "1.12-globals.tsv"))
args = ap.parse_args()
if not args.wow_re:
sys.exit(
"this table is derived from the RE repo's runtime capture of the reference client's "
"_G: pass --wow-re or set WOW_RE. The committed reference/1.12-globals.tsv is the "
"surface benilla tracks."
)
fixture = os.path.join(args.wow_re, "WoW", "_w5875_fixtures", "item13", "W5875Capture.lua")
if not os.path.exists(fixture):
+24 -11
View File
@@ -2,7 +2,7 @@
"""Regenerate `reference/1.12-verb-events.tsv` — every FrameScript event the 1.12.1 client fires
FROM INSIDE A LUA VERB, keyed by the verb.
scripts/gen-reference-verb-events.py [--wow-re DIR] [--out FILE]
WOW_RE=<the RE repo> scripts/gen-reference-verb-events.py [--wow-re DIR] [--out FILE]
**Why this exists.** `reference/1.12-events.tsv` (2140) says what arguments an event carries and
`reference_ui`'s two name gates (1883/1889) say whether *something* fires what a stock file listens
@@ -45,8 +45,10 @@ and the unit-field bridge / token fan-out ids below 0xb6, which are state events
fire-site census is over the two signal helpers' literal call sites (its header says so in
capitals), and `shape=helper` stops one call deep on purpose. Gate on what IS here.
The input lives outside the repo (the sibling RE repo), so this is a manual regeneration, like
`gen-reference-events.py` — not something CI can run.
The fire sites and the binding shapes are the vendored `reference/` tables; the function names,
the ledgers' extents and the disassembly are the RE repo's (`--wow-re`, or `$WOW_RE`), and the
disassembly is the client's own binary, so this is a manual regeneration there. The committed
table is the surface benilla tracks.
"""
import argparse
import bisect
@@ -69,7 +71,12 @@ BRANCH = re.compile(r"^(call|jmp)\s+0x([0-9a-f]+)\s*$")
SIZE = re.compile(r"\bsize=(\d+)")
REFERENCE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "reference"))
def rows(path):
if not os.path.exists(path):
sys.exit(f"no table at {path}")
for line in open(path, encoding="utf-8"):
if line.startswith("#"):
continue
@@ -81,12 +88,15 @@ def rows(path):
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--wow-re", default=os.path.expanduser("~/dev/wow-5875-re"))
ap.add_argument(
"--out",
default=os.path.join(os.path.dirname(__file__), "..", "reference", "1.12-verb-events.tsv"),
)
ap.add_argument("--wow-re", default=os.environ.get("WOW_RE"))
ap.add_argument("--out", default=os.path.join(REFERENCE, "1.12-verb-events.tsv"))
a = ap.parse_args()
if not a.wow_re:
sys.exit(
"this table is derived from the RE repo's function names, ledgers and disassembly: "
"pass --wow-re or set WOW_RE. The committed reference/1.12-verb-events.tsv is the "
"surface benilla tracks."
)
starts = set()
for f in rows(os.path.join(a.wow_re, "re/names/out/names-5875.tsv")):
@@ -111,7 +121,7 @@ def main():
starts |= set(extent)
verbs = {} # fn address -> name
for f in rows(os.path.join(a.wow_re, "re/audit/binding-shapes.tsv")):
for f in rows(os.path.join(REFERENCE, "1.12-shapes.tsv")):
if len(f) < 2:
continue
fn = int(f[1], 16)
@@ -127,7 +137,7 @@ def main():
starts |= set(verbs)
sites = [] # (site va, event name)
for f in rows(os.path.join(a.wow_re, "re/events/event-firesites.tsv")):
for f in rows(os.path.join(REFERENCE, "1.12-event-firesites.tsv")):
if len(f) < 4 or not f[0].startswith("0x") or not f[3] or f[2] == "dyn":
continue
va, eid = int(f[0], 16), int(f[2])
@@ -145,7 +155,10 @@ def main():
after_pad = False
ended = False # the previous real instruction was a `ret`
n_lines = 0
for line in open(os.path.join(a.wow_re, "system/ui/scratch/disasm-full.txt"), encoding="utf-8", errors="replace"):
disasm = os.path.join(a.wow_re, "system/ui/scratch/disasm-full.txt")
if not os.path.exists(disasm):
sys.exit(f"no disassembly at {disasm} — is --wow-re right?")
for line in open(disasm, encoding="utf-8", errors="replace"):
m = LINE.match(line)
if not m:
continue
+3 -3
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Generate docs/MAP.md — the "what's built" map — derived entirely from what's on disk so it cannot
# drift (docs/METHOD.md / docs/METHOD.md: "the generated map … regenerated from the code, never
# hand-written"). `wt.sh land` runs it on the rebased tree and commits the file (decision 2049);
# run it by hand only to LOOK, never commit its output yourself, and never edit it by hand.
# hand-written"). It is regenerated and committed whenever a change lands (decision 2049); run
# it by hand only to LOOK, never commit its output yourself, and never edit it by hand.
# Deterministic: identical tree → identical output (no timestamps), so a diff means the structure
# actually changed.
# No `pipefail`/`-e`: greps that legitimately find nothing (a single-file lib, a crate with no bins)
@@ -125,7 +125,7 @@ field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]
# is the source, so it cannot drift. (`winlab/` is the Windows lab laptop's, 2205/2211.)
echo "## Scripts (\`scripts/\`)"
echo
for f in scripts/*.py scripts/*.sh scripts/*.swift scripts/winlab/*; do
for f in scripts/*.py scripts/*.sh; do
[ -f "$f" ] || continue
# The summary: the header's first non-empty content after the shebang, comment or docstring
# markers stripped, joined until its first blank line, cut at a sentence end or ~150 chars.
-100
View File
@@ -1,100 +0,0 @@
#!/bin/bash
# leg.sh — the interleaved A/B leg runner (1353 law 1, mechanized).
#
# Runs N rounds of (binary A, binary B) at a pin, one slot, one body, enforcing the leg laws the
# campaign kept re-typing by hand:
# · load guard: waits for 1-min load average < 3 before every leg (1157)
# · lock guard: refuses to start — and stops between legs — while the screen is locked
# (1355: a locked screen throttles drawables at occluded_frames=0; the leg is invalid)
# · occlusion: a leg with occluded_frames != 0 is discarded and re-run, up to 2 retries (0731)
# · the whole FPS_PROBE line is kept per leg (never a grepped subset) under $LEG_DIR
#
# Usage:
# scripts/leg.sh <binaryA> <binaryB> [rounds] [rig]
# Env:
# WOW_USER/WOW_PASS/WOW_CHAR probe credentials (default probe0/pprobe0/Probezero — pool-0)
# LEG_RIG WOW_RIG spec (default: the LBRS pin, 0731)
# LEG_FRAMES / LEG_AT probe window (default 300 / 45)
# LEG_DIR where leg logs land (default: $CLAUDE_JOB_DIR/tmp or /tmp)
#
# Motion legs: export WOW_PROBE_KEY / WOW_PROBE_CAM alongside — they inherit into every leg,
# A and B alike — to measure a MOVING regime, e.g. WOW_PROBE_KEY="W@40:20"
# WOW_PROBE_CAM="30,10@40:25" with the default LEG_AT=45 puts the whole FPS window inside a
# straight walk with the camera panning (1427: parked pins cannot see the play regime the
# director reports). Streaming churn inside the window is the point, not noise — but quote
# motion legs only against motion legs.
#
# Output: one line per kept leg (cpu_ms fps sys_busy), then per-round deltas and the means.
set -euo pipefail
A="${1:?usage: leg.sh <binaryA> <binaryB> [rounds] [rig]}"
B="${2:?usage: leg.sh <binaryA> <binaryB> [rounds] [rig]}"
ROUNDS="${3:-3}"
RIG="${4:-${LEG_RIG:-at:229,24.90,-396.50,48.80}}"
USER_="${WOW_USER:-probe0}"; PASS_="${WOW_PASS:-pprobe0}"; CHAR_="${WOW_CHAR:-Probezero}"
FRAMES="${LEG_FRAMES:-300}"; AT="${LEG_AT:-45}"
DIR="${LEG_DIR:-${CLAUDE_JOB_DIR:-/tmp}/tmp}"; mkdir -p "$DIR" 2>/dev/null || DIR="/tmp"
locked() { ioreg -n Root -d1 -a | grep -A1 CGSSessionScreenIsLocked | grep -q true; }
loadwait() { until [ "$(uptime | sed -E 's/.*averages?: ([0-9]+)[.,].*/\1/')" -lt 3 ]; do sleep 10; done; }
run_leg() { # $1 binary $2 tag → prints "cpu_ms fps sys_busy" or fails
local bin="$1" tag="$2" try line occ fatal
for try in 1 2 3; do
if locked; then echo "leg.sh: screen is LOCKED — legs are invalid (1355); aborting" >&2; exit 2; fi
loadwait
WOW_UNATTENDED=1 WOW_USER="$USER_" WOW_PASS="$PASS_" WOW_CHAR="$CHAR_" WOW_RIG="$RIG" \
WOW_LIVE_FPS="$FRAMES" WOW_LIVE_FPS_AT="$AT" \
timeout 300 "$bin" >"$DIR/leg-$tag-$try.log" 2>&1 || true
line=$(grep -a "FPS_PROBE" "$DIR/leg-$tag-$try.log" | sed 's/\x1b\[[0-9;]*m//g' | tail -1)
# A FATAL is the client's own verdict that the run can never measure (refused login, missing
# character, no world by the boot deadline) — deterministic, so a retry buys the same failure.
# Abort the sitting loudly instead of burning tries × timeout (the 1371 legs sat 3 × 300 s at
# a login screen the account guard had already refused).
fatal=$(grep -a "FATAL" "$DIR/leg-$tag-$try.log" | sed 's/\x1b\[[0-9;]*m//g' | tail -1)
if [ -n "$fatal" ]; then
echo "leg.sh: $tag ABORTING the sitting — $fatal" >&2
echo "leg.sh: full log: $DIR/leg-$tag-$try.log" >&2
exit 4
fi
[ -n "$line" ] || { echo "leg.sh: $tag try $try produced no FPS_PROBE — see $DIR/leg-$tag-$try.log" >&2; continue; }
occ=$(sed -E 's/.*occluded_frames=([0-9]+).*/\1/' <<<"$line")
if [ "$occ" != "0" ]; then echo "leg.sh: $tag try $try occluded ($occ) — discarded unread" >&2; continue; fi
# Regime guard (1388 round 3): one round's legs once read 126.5 vs 60.0 fps under the same
# uncapped present mode — Metal displaySync is off either way, but the WindowServer rails a
# composited window's drawables at its display's rate, and only sometimes. Every campaign
# anchor is a railed leg (1380 "fps locked 60"), so a leg that escaped the rail measures a
# different regime and is discarded here, by machine, instead of by a human reading the
# round's deltas sideways. The probe line's display=...@hz stamp is the rail.
fps=$(grep -oE " fps=[0-9.]+" <<<"$line" | cut -d= -f2)
hz=$(grep -oE " display=[^ ]*@[0-9]+" <<<"$line" | sed 's/.*@//')
if [ -n "$fps" ] && [ -n "$hz" ] && awk -v f="$fps" -v h="$hz" 'BEGIN{exit !(f > h + 3)}'; then
echo "leg.sh: $tag try $try FREE-RUNNING (fps=$fps above the $hz Hz rail) — discarded" >&2; continue
fi
echo "$line" >>"$DIR/legs-kept.log"
grep -oE "cpu_ms=[0-9.]+" <<<"$line" | cut -d= -f2
return 0
done
echo "leg.sh: $tag failed 3 tries" >&2; exit 3
}
echo "leg.sh: $ROUNDS round(s), A=$(basename "$A") B=$(basename "$B"), rig=\"$RIG\", body=$CHAR_"
# Law 6 (1351/1353): the first leg after a build — or after the machine sat idle — is a warm-up,
# not a measurement. One discarded A-leg primes every cache the rounds then share.
# The assignment (not an inline $(...) in the echo) is load-bearing: run_leg's exit runs in the
# substitution subshell, and only an assignment's status is seen by set -e — inline, a warm-up
# abort would be discarded and the rounds would start anyway.
w=$(run_leg "$A" "warmup"); echo " warm-up (discarded): cpu_ms=$w"
declare -a AV BV
for r in $(seq 1 "$ROUNDS"); do
a=$(run_leg "$A" "r${r}A"); AV+=("$a"); echo " round $r A: cpu_ms=$a"
b=$(run_leg "$B" "r${r}B"); BV+=("$b"); echo " round $r B: cpu_ms=$b"
done
awk_list() { local IFS=,; echo "$*"; }
python3 - "$(awk_list "${AV[@]}")" "$(awk_list "${BV[@]}")" <<'EOF'
import sys
a = [float(x) for x in sys.argv[1].split(',')]
b = [float(x) for x in sys.argv[2].split(',')]
ma, mb = sum(a)/len(a), sum(b)/len(b)
print(f"mean A={ma:.2f} mean B={mb:.2f} Δ={mb-ma:+.2f} (rounds: {['%+.2f' % (y-x) for x,y in zip(a,b)]})")
EOF

Some files were not shown because too many files have changed in this diff Show More