Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -50,9 +50,10 @@ awk -F'\t' -v ref="$REF" -v show_missing="$show_missing" -v show_beyond="$show_b
|
||||
if (o == "engine" || o == "lua") have++
|
||||
else if (o == "framexml") transcribable[$1] = 1
|
||||
else {
|
||||
# Categorize the superset. Only the last bucket is an API-target question; the first is
|
||||
# benilla being benilla, and the second is our Lua runtime being 5.1 where 1.12 is 5.0.
|
||||
# Categorize the superset. Only the last bucket is an API-target question; the first two are
|
||||
# benilla being benilla, and the third is our Lua runtime being 5.1 where 1.12 is 5.0.
|
||||
if ($1 ~ /^Benilla/) bridge[$1] = 1
|
||||
else if ($1 ~ /^__/) internal[$1] = 1
|
||||
else if ($1 ~ /^(_G|_VERSION|coroutine|print|select)$/) lua51[$1] = 1
|
||||
else api[$1] = 1
|
||||
}
|
||||
@@ -65,7 +66,7 @@ awk -F'\t' -v ref="$REF" -v show_missing="$show_missing" -v show_beyond="$show_b
|
||||
n_ref["engine"] + n_ref["framexml"] + n_ref["lua"]
|
||||
printf " engine %5d functions, %5d other <- benilla implements these in Rust\n", \
|
||||
n_ref_fn["engine"], n_ref["engine"] - n_ref_fn["engine"]
|
||||
printf " framexml %5d functions, %5d other <- the stock FrameXML defines these\n", \
|
||||
printf " framexml %5d functions, %5d other <- benilla transcribes these into assets/ui\n", \
|
||||
n_ref_fn["framexml"], n_ref["framexml"] - n_ref_fn["framexml"]
|
||||
printf " lua %5d functions, %5d other <- mlua provides these\n", \
|
||||
n_ref_fn["lua"], n_ref["lua"] - n_ref_fn["lua"]
|
||||
@@ -77,6 +78,7 @@ awk -F'\t' -v ref="$REF" -v show_missing="$show_missing" -v show_beyond="$show_b
|
||||
|
||||
printf "\nbeyond 1.12, by kind — every one of these is a deliberate exception or a bug:\n"
|
||||
printf " %3d benilla host bridge Benilla*, called only by our own FrameXML\n", length(bridge)
|
||||
printf " %3d VM internals __benilla_*, pushed by the tick\n", length(internal)
|
||||
printf " %3d Lua 5.1 past 1.12'\''s 5.0 ", length(lua51); dump(lua51, "")
|
||||
printf " %3d WoW API past 1.12 the phase-5 list\n", length(api)
|
||||
printf " %3d ours in Rust that 1.12 defines in FrameXML\n", length(transcribable)
|
||||
|
||||
+3
-4
@@ -95,7 +95,7 @@ esac
|
||||
pflags=""
|
||||
for p in $pkgs; do pflags="$pflags -p $p"; done
|
||||
echo "check: scope = $pkgs"
|
||||
echo "check: (changed crates + everything that depends on them; the full chain runs once, at land)"
|
||||
echo "check: (changed crates + everything that depends on them; full chain still runs at sync→land)"
|
||||
|
||||
log="$(mktemp "${TMPDIR:-/tmp}/benilla-check.XXXXXX")"
|
||||
skips="$(mktemp "${TMPDIR:-/tmp}/benilla-check-skips.XXXXXX")"
|
||||
@@ -113,8 +113,7 @@ run() {
|
||||
|
||||
run fmt cargo fmt --all -- --check
|
||||
run clippy cargo clippy $pflags --all-targets -- -D warnings
|
||||
# --no-fail-fast: one red test binary does not hide the failures in the ones after it.
|
||||
run test env BENILLA_SKIP_LOG="$skips" cargo test --no-fail-fast $pflags
|
||||
run test env BENILLA_SKIP_LOG="$skips" cargo test $pflags
|
||||
# Data-gated tests skip without the install or the addon corpus and libtest swallows their line,
|
||||
# so each skip is also logged to $BENILLA_SKIP_LOG and counted here.
|
||||
if [ -s "$skips" ]; then
|
||||
@@ -124,4 +123,4 @@ if [ -s "$skips" ]; then
|
||||
fi
|
||||
|
||||
echo "CHECK GREEN (scoped: $pkgs)"
|
||||
echo " (the full chain, scripts/gates.sh, runs once on the tree that lands; a run before it repeats it)"
|
||||
echo " (land still pays the full chain once: scripts/gates.sh — memoized, so an already-gated tree is free)"
|
||||
|
||||
+14
-18
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# The land gate chain: every gate in order, stopping at the first failure.
|
||||
#
|
||||
# fmt, clippy, test, test-no-install, doc-links, pass-span-lint, player-tests, enforcer,
|
||||
# enforcer-no-install. A failure prints the gate's log tail and exits nonzero; a green
|
||||
# fmt, clippy, test, test-no-install, doc-links, pass-span-lint, player-build, player-tests,
|
||||
# enforcer, enforcer-no-install. A failure prints the gate's log tail and exits nonzero; a green
|
||||
# run stamps the tree, which then re-greens without running. GATES_FORCE=1 runs the chain anyway,
|
||||
# WOW_DATA picks the install (set and empty: none) and TMPDIR holds the scratch files; the script
|
||||
# sets BENILLA_REQUIRE_DATA, BENILLA_ADDON_CORPUS, BENILLA_SKIP_LOG and WOW_WORLDVIEW_CHECK for
|
||||
# its gates.
|
||||
# sets BENILLA_REQUIRE_DATA, BENILLA_SKIP_LOG and WOW_WORLDVIEW_CHECK for its gates.
|
||||
# `.github/workflows/ci.yml` runs the gates that need neither the install nor a display on a fork's
|
||||
# pull request: a gate added here goes there too, if it can run on a bare Linux runner.
|
||||
#
|
||||
@@ -123,12 +122,12 @@ 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} BENILLA_SKIP_LOG="$skips" cargo test --no-fail-fast --workspace
|
||||
run test env ${require_data:+BENILLA_REQUIRE_DATA=1} BENILLA_SKIP_LOG="$skips" cargo test --workspace
|
||||
report_skips test
|
||||
|
||||
# test-no-install: refuses a test that reads the install without `wow_data_or_skip!`. `WOW_DATA=`
|
||||
# (set, empty) is the resolver's "no install"; no build reads it, so the test binaries are reused.
|
||||
run test-no-install env -u BENILLA_REQUIRE_DATA WOW_DATA= cargo test --no-fail-fast --workspace
|
||||
run test-no-install env -u BENILLA_REQUIRE_DATA WOW_DATA= cargo test --workspace
|
||||
|
||||
# doc-links: refuses a doc link to a `crate::`, `super::`, `self::` or `Self::` path whose leaf is
|
||||
# declared nowhere in the workspace. Narrow on purpose: every other unresolved link is a convention.
|
||||
@@ -138,18 +137,15 @@ run doc-links scripts/doc-links.py
|
||||
# wgpu validation error on Vulkan and a silent no-op on Metal and DX12, which no macOS run sees.
|
||||
run pass-span-lint scripts/pass-span-lint.py
|
||||
|
||||
# player-tests: the player build, `benilla-app` without `dev`, which compiles out the debug panel,
|
||||
# perf HUD, inspector, capture harness and probes, so it fails when any other code names one of
|
||||
# them. Its test build compiles every line of the player graph but the `benilla` launcher, a shim
|
||||
# with no `dev` seam, so a `cargo build -p benilla --no-default-features` would add only the shim
|
||||
# and a link. It runs the unit tests of the `cfg(not(feature = "dev"))` code (the resolver skips
|
||||
# the source tree; the state folder sits beside the binary), which no other gate runs. WOW_DATA, which
|
||||
# a player build reads too, goes in for the shipped-UI tests that read FrameXML off the install.
|
||||
# Without `dev` only $BENILLA_ADDON_CORPUS finds the addon corpus, so the link goes in by that name,
|
||||
# and BENILLA_REQUIRE_DATA refuses a skip here exactly where it does in `test`.
|
||||
run player-tests env ${wow_data:+WOW_DATA="$wow_data"} \
|
||||
${corpus:+BENILLA_ADDON_CORPUS="$root/wow-addons-vanilla"} \
|
||||
${require_data:+BENILLA_REQUIRE_DATA=1} BENILLA_SKIP_LOG="$skips" \
|
||||
# player-build: `benilla` without `dev`, which compiles out the debug panel, perf HUD, inspector,
|
||||
# capture harness and probes, so it fails when any other code names one of them.
|
||||
run player-build cargo build -p benilla --no-default-features
|
||||
|
||||
# player-tests: the unit tests of the `cfg(not(feature = "dev"))` code (the resolver skips the
|
||||
# source tree; the state folder sits beside the binary), which no other gate runs. WOW_DATA, which
|
||||
# a player build reads too, goes in for the shipped-UI tests that read FrameXML off the install; no
|
||||
# BENILLA_REQUIRE_DATA, since without `dev` only $BENILLA_ADDON_CORPUS finds the addon corpus.
|
||||
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
|
||||
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate `reference/1.12-glue-surface.tsv`: every Lua function the glue screens' VM has in `_G`.
|
||||
|
||||
scripts/gen-reference-glue-surface.py [--shapes FILE] [--globals FILE] [--out FILE]
|
||||
|
||||
The inputs are vendored under `reference/`, so this runs from any clone and reproduces the
|
||||
committed table byte for byte.
|
||||
|
||||
The glue builder (`0x46a7b0`) starts from the bare base set `0x7039e0`: a rebuild resets the Lua
|
||||
state (`0x703b80` -> `0x7039e0`, at `0x46a87b`), and the first build (`0x46a505`, passing 0) skips
|
||||
that reset (`je` at `0x46a879`) and takes the state the startup init left (`0x402aee`). It then
|
||||
calls the glue registrar `0x46abb0`, which installs seven registrar tables and nothing else:
|
||||
|
||||
0x51bb20 table 0x8503f4 6 shared: in-game, `0x51bb60` calls it before its own table
|
||||
0x706350 table 0x872e74 4 shared: in-game at `0x490251`; then the 17 widget method tables
|
||||
0x46ddf0 table 0x8373b8 60 glue core
|
||||
0x4736c0 table 0x838578 11 character select
|
||||
0x471a20 table 0x8380f8 24 character create
|
||||
0x46f480 table 0x837ea0 10 realm list
|
||||
0x4587f0 table 0x835a50 4 shared: in-game at `0x4902a6`
|
||||
|
||||
Each registrar is a `0x704120` loop over its table with the bound read from its own `cmp`, the
|
||||
counts above. `0x7039e0`, FrameScript's Lua init, is the base set both states share: the 36-entry
|
||||
base array `0x811e28` (`1.12-shapes.tsv`'s `baselib` table), the `string`, `table`, `math` and
|
||||
`bit` libraries (fields, not globals, so not rows here), `math`'s global `__pow` ([`POW`]), and
|
||||
the compat chunk `0x8722e8` run at `0x703b26`. The chunk's 39 function assignments are
|
||||
[`COMPAT`]: 32 bind a library function under a bare name (`tinsert = table.insert`), each row
|
||||
taking that binding's `fn` and table, and 7 wrap one in a degree-converting Lua closure (`sin`,
|
||||
`acos`, ...), `fn` `lua`. Its one other assignment, `PI = math.pi`, is a number. Every engine
|
||||
function `1.12-globals.tsv` has that no `global` or `baselib` row of the shapes table names must be
|
||||
one of these, so a chunk the list missed fails the generator.
|
||||
|
||||
Columns:
|
||||
scope base (0x7039e0) | shared (a registrar the in-game state calls too) | glue.
|
||||
registrar the function that installs the row.
|
||||
fn, table_va the binding and its table; a compat alias carries the library binding it names,
|
||||
or fn `lua` and table `-` for a closure; `__pow` has no table.
|
||||
arity the shapes table's return arity, `?` for a closure.
|
||||
world_fn a glue row's in-game namesake from any other `global` table, else `-`. The in-game
|
||||
state never sees a glue row, but the name is shared: a glue verb benilla builds is a
|
||||
different binding from the in-game one of the same name.
|
||||
note `shape-differs` where both shapes are exact and the argument count or the return
|
||||
arity differs between the glue binding and its in-game namesake.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
# (registrar, table_va, rows, scope), in the order `0x46abb0` calls them.
|
||||
REGISTRARS = [
|
||||
(0x51BB20, 0x8503F4, 6, "shared"),
|
||||
(0x706350, 0x872E74, 4, "shared"),
|
||||
(0x46DDF0, 0x8373B8, 60, "glue"),
|
||||
(0x4736C0, 0x838578, 11, "glue"),
|
||||
(0x471A20, 0x8380F8, 24, "glue"),
|
||||
(0x46F480, 0x837EA0, 10, "glue"),
|
||||
(0x4587F0, 0x835A50, 4, "shared"),
|
||||
]
|
||||
BASE_INIT = 0x7039E0
|
||||
BASE_TABLE = 0x811E28
|
||||
BASE_ROWS = 36
|
||||
# The libraries `0x7039e0` opens, by the name the compat chunk reaches them under.
|
||||
LIBRARIES = {"table": 0x822D40, "math": 0x822C60, "string": 0x822D88, "bit": 0x822C18}
|
||||
# The compat chunk's function assignments, in its order: (global, "library.field"), or None for a
|
||||
# Lua closure that converts degrees around the `math` function of the same name.
|
||||
COMPAT = [
|
||||
("foreach", "table.foreach"), ("foreachi", "table.foreachi"), ("getn", "table.getn"),
|
||||
("tinsert", "table.insert"), ("tremove", "table.remove"), ("sort", "table.sort"),
|
||||
("abs", "math.abs"), ("acos", None), ("asin", None), ("atan", None), ("atan2", None),
|
||||
("ceil", "math.ceil"), ("cos", None), ("deg", "math.deg"), ("exp", "math.exp"),
|
||||
("floor", "math.floor"), ("frexp", "math.frexp"), ("ldexp", "math.ldexp"), ("log", "math.log"),
|
||||
("log10", "math.log10"), ("max", "math.max"), ("min", "math.min"), ("mod", "math.mod"),
|
||||
("rad", "math.rad"), ("random", "math.random"), ("randomseed", "math.randomseed"),
|
||||
("sin", None), ("sqrt", "math.sqrt"), ("tan", None),
|
||||
("strbyte", "string.byte"), ("strchar", "string.char"), ("strfind", "string.find"),
|
||||
("format", "string.format"), ("gsub", "string.gsub"), ("strlen", "string.len"),
|
||||
("strlower", "string.lower"), ("strrep", "string.rep"), ("strsub", "string.sub"),
|
||||
("strupper", "string.upper"),
|
||||
]
|
||||
# `luaopen_math` 0x7fb470 also sets the global `__pow` to 0x7fb050 (`0x7fb4af`-`0x7fb4d2`), which
|
||||
# Lua 5.0 calls for `^`.
|
||||
POW = ("__pow", 0x7FB050)
|
||||
|
||||
REFERENCE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "reference"))
|
||||
|
||||
|
||||
def rows(path, header):
|
||||
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
|
||||
f = line.rstrip("\n").split("\t")
|
||||
if f and f[0] == header:
|
||||
continue
|
||||
yield f
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--shapes", default=os.path.join(REFERENCE, "1.12-shapes.tsv"))
|
||||
ap.add_argument("--globals", default=os.path.join(REFERENCE, "1.12-globals.tsv"))
|
||||
ap.add_argument("--out", default=os.path.join(REFERENCE, "1.12-glue-surface.tsv"))
|
||||
a = ap.parse_args()
|
||||
|
||||
# name, fn, pair_va, table_va, table_kind, argc, argc_conf, arity, arity_conf, ...
|
||||
shapes = [f for f in rows(a.shapes, "name") if len(f) >= 9]
|
||||
by_table = {}
|
||||
for f in shapes:
|
||||
by_table.setdefault(int(f[3], 16), []).append(f)
|
||||
|
||||
out = []
|
||||
|
||||
def take(table_va, n, registrar, scope):
|
||||
got = by_table.get(table_va, [])
|
||||
if len(got) != n:
|
||||
sys.exit(f"table 0x{table_va:x}: the registrar loops {n} rows, the shapes table has {len(got)}")
|
||||
for f in got:
|
||||
out.append({"f": f, "scope": scope, "registrar": registrar})
|
||||
|
||||
take(BASE_TABLE, BASE_ROWS, BASE_INIT, "base")
|
||||
for registrar, table_va, n, scope in REGISTRARS:
|
||||
take(table_va, n, registrar, scope)
|
||||
|
||||
# The base set's Lua side: the compat chunk's aliases and `__pow`, each a binding of a library
|
||||
# the shapes table carries, or a Lua closure.
|
||||
fields = {}
|
||||
for lib, table_va in LIBRARIES.items():
|
||||
for f in by_table.get(table_va, []):
|
||||
fields[f"{lib}.{f[0]}"] = f
|
||||
aliases = []
|
||||
for name, target in COMPAT:
|
||||
if target is None:
|
||||
aliases.append((name, "lua", "-", "?"))
|
||||
continue
|
||||
f = fields.get(target)
|
||||
if f is None:
|
||||
sys.exit(f"compat alias {name} = {target}: no such library binding in {a.shapes}")
|
||||
aliases.append((name, f[1], f[3], f[7]))
|
||||
aliases.append((POW[0], f"0x{POW[1]:x}", "-", "1"))
|
||||
|
||||
# A 1.12 engine function no registrar row names is one of the aliases, or the list is short.
|
||||
registered = {f[0] for f in shapes if f[4] in ("global", "baselib")}
|
||||
alias_names = {x[0] for x in aliases}
|
||||
for f in rows(a.globals, "name"):
|
||||
if len(f) >= 3 and f[1] == "function" and f[2] in ("engine", "lua"):
|
||||
if f[0] not in registered and f[0] not in alias_names:
|
||||
sys.exit(f"{f[0]} is a 1.12 engine function no registrar and no compat alias names")
|
||||
|
||||
# In-game namesakes: rows of every other `global` table, which only the in-game state installs.
|
||||
ours = {t for _, t, _, _ in REGISTRARS}
|
||||
world = {}
|
||||
for f in shapes:
|
||||
if f[4] == "global" and int(f[3], 16) not in ours:
|
||||
world.setdefault(f[0], []).append(f)
|
||||
|
||||
lines = []
|
||||
differs = 0
|
||||
for r in out:
|
||||
f = r["f"]
|
||||
name, fn, table_va, argc, argc_conf, arity, arity_conf = f[0], f[1], f[3], f[5], f[6], f[7], f[8]
|
||||
world_fn, note = "-", ""
|
||||
if r["scope"] == "glue" and name in world:
|
||||
w = world[name]
|
||||
world_fn = "|".join(x[1] for x in w)
|
||||
notes = []
|
||||
for x in w:
|
||||
if argc_conf == "exact" and x[6] == "exact" and argc != x[5]:
|
||||
notes.append(f"argc {argc} here, {x[5]} in-game ({x[1]})")
|
||||
if arity_conf == "exact" and x[8] == "exact" and arity != x[7]:
|
||||
notes.append(f"arity {arity} here, {x[7]} in-game ({x[1]})")
|
||||
if notes:
|
||||
note = "shape-differs: " + "; ".join(notes)
|
||||
differs += 1
|
||||
lines.append((r["scope"], f"0x{r['registrar']:x}", name, fn, table_va, arity, world_fn, note))
|
||||
for name, fn, table_va, arity in aliases:
|
||||
lines.append(("base", f"0x{BASE_INIT:x}", name, fn, table_va, arity, "-", ""))
|
||||
|
||||
names = [l[2] for l in lines]
|
||||
if len(names) != len(set(names)):
|
||||
sys.exit("a name appears twice in the glue state; the table is keyed by name")
|
||||
|
||||
count = {s: sum(1 for l in lines if l[0] == s) for s in ("base", "shared", "glue")}
|
||||
path = os.path.normpath(a.out)
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(
|
||||
"# The glue screens' Lua surface in the 1.12.1 client (build 5875): every function in `_G`\n"
|
||||
"# once the glue builder 0x46a7b0 has installed its registrars, before GlueXML loads.\n"
|
||||
"# Generated by scripts/gen-reference-glue-surface.py from reference/1.12-shapes.tsv and\n"
|
||||
"# reference/1.12-globals.tsv; the generator's header carries the registrars and the method.\n"
|
||||
"#\n"
|
||||
"# scope base = FrameScript's Lua init 0x7039e0, which both states run; shared = a\n"
|
||||
"# registrar the in-game state calls too (0x51bb20, 0x706350, 0x4587f0); glue =\n"
|
||||
"# a glue-only registrar table (0x46ddf0, 0x4736c0, 0x471a20, 0x46f480).\n"
|
||||
"# fn, table_va, arity as 1.12-shapes.tsv has them. A base alias the compat chunk\n"
|
||||
"# 0x8722e8 defines carries the library binding it names (`tinsert` is table\n"
|
||||
"# 0x822d40's `insert`), or fn `lua` for a degree-converting closure; math's\n"
|
||||
"# global `__pow` (0x7fb050) has no table.\n"
|
||||
"# world_fn a glue row's in-game namesake, a different binding; `-` if none.\n"
|
||||
"# note shape-differs = both shapes exact, and the glue binding takes or returns a\n"
|
||||
"# different count than its in-game namesake.\n"
|
||||
f"# {len(lines)} functions: {count['base']} base, {count['shared']} shared, {count['glue']} glue;"
|
||||
f" {differs} glue rows differ in shape from their in-game namesake.\n"
|
||||
"scope\tregistrar\tname\tfn\ttable_va\tarity\tworld_fn\tnote\n"
|
||||
)
|
||||
for l in lines:
|
||||
fh.write("\t".join(l) + "\n")
|
||||
print(f"wrote {path}: {len(lines)} functions, {differs} shape differences")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env bash
|
||||
# Import addon settings (SavedVariables) from a real WoW install's WTF folder into benilla.
|
||||
#
|
||||
# scripts/import-wtf.sh <WTF-dir> <ACCOUNT> [<Realm> <Character>] # dry run, prints a plan
|
||||
# scripts/import-wtf.sh <WTF-dir> <ACCOUNT> <Realm> <Character> --apply
|
||||
#
|
||||
# scripts/import-wtf.sh ~/Games/Octo-wow/WTF GROMGOSH "C'Thun" Gromgosh --apply
|
||||
#
|
||||
# Why a script and not a copy: benilla does not use the WTF layout. It keeps saved variables in
|
||||
# `benilla-config/saved/`, and the two differ in both shape and naming:
|
||||
#
|
||||
# WTF/Account/<ACC>/SavedVariables/<Addon>.lua → saved/<Addon>.lua
|
||||
# WTF/Account/<ACC>/<Realm>/<Char>/SavedVariables/<A>.lua → saved/<Realm>-<Char>/<A>.lua
|
||||
#
|
||||
# and the per-character directory name is sanitised the way the client does it
|
||||
# (`local_state.rs`'s `file_token`: every non-alphanumeric character becomes `_`), so the realm
|
||||
# `C'Thun` is the directory `C_Thun`. Getting that wrong leaves the files in place but unread,
|
||||
# which looks exactly like "the import did nothing".
|
||||
#
|
||||
# Key bindings come across too, and they are NOT a copy — the two formats are transposed:
|
||||
#
|
||||
# bindings-cache.wtf bind <KEY> <COMMAND> one line per key
|
||||
# bindings/account.txt bind <COMMAND> <key...> one line per command
|
||||
#
|
||||
# So `bind W MOVEFORWARD` copied verbatim is read by benilla as "the command W, bound to the key
|
||||
# MOVEFORWARD" — it parses without complaint and binds nothing usable. This transposes properly:
|
||||
# it groups every key by the command it runs and emits one line per command, key order preserved.
|
||||
# benilla stores a diff against its own defaults and an absent command keeps those defaults, so a
|
||||
# full snapshot is safe to write; the one asymmetry is that a command you *deliberately unbound*
|
||||
# in WoW is simply absent from the cache, and will come back at benilla's default rather than
|
||||
# staying unbound.
|
||||
#
|
||||
# Flags:
|
||||
# --apply actually write (default is a dry run that changes nothing)
|
||||
# --all import settings for every addon in the WTF folder, not just the ones installed
|
||||
# under benilla-config/AddOns. Default is installed-only: benilla ignores saved
|
||||
# variables for an addon it does not load, so the rest are clutter.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
dest="$root/benilla-config/saved"
|
||||
|
||||
wtf="${1:-}"; account="${2:-}"; realm="${3:-}"; character="${4:-}"
|
||||
apply=0; all=0
|
||||
for a in "$@"; do
|
||||
[ "$a" = "--apply" ] && apply=1
|
||||
[ "$a" = "--all" ] && all=1
|
||||
done
|
||||
|
||||
if [ -z "$wtf" ] || [ -z "$account" ]; then
|
||||
sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||
exit 2
|
||||
fi
|
||||
acct_dir="$wtf/Account/$account"
|
||||
[ -d "$acct_dir" ] || { echo "import: no account dir at $acct_dir" >&2; exit 1; }
|
||||
|
||||
# `file_token`: every non-alphanumeric character becomes `_`, matching local_state.rs exactly.
|
||||
token() { printf '%s' "$1" | sed 's/[^[:alnum:]]/_/g'; }
|
||||
|
||||
installed() {
|
||||
# An addon counts as installed if benilla-config/AddOns has a directory of that name. The
|
||||
# saved-variable file is named after the addon, which is the same key.
|
||||
[ "$all" = "1" ] && return 0
|
||||
[ -d "$root/benilla-config/AddOns/$1" ]
|
||||
}
|
||||
|
||||
copied=0; skipped=0; backed=0
|
||||
tmp_known="$(mktemp)"
|
||||
trap 'rm -f "$tmp_known"' EXIT
|
||||
do_one() {
|
||||
local src="$1" out="$2" addon
|
||||
addon="$(basename "$src" .lua)"
|
||||
case "$src" in *.bak) return 0;; esac
|
||||
if ! installed "$addon"; then
|
||||
skipped=$((skipped+1)); return 0
|
||||
fi
|
||||
if [ "$apply" = "1" ]; then
|
||||
mkdir -p "$(dirname "$out")"
|
||||
# Back up rather than clobber: benilla may already hold settings from play sessions, and
|
||||
# this import is the kind of thing a person runs twice by accident.
|
||||
if [ -f "$out" ]; then
|
||||
cp -p "$out" "$out.pre-import.bak"; backed=$((backed+1))
|
||||
fi
|
||||
cp -p "$src" "$out"
|
||||
fi
|
||||
copied=$((copied+1))
|
||||
printf ' %-34s → %s\n' "$addon" "${out#$root/}"
|
||||
}
|
||||
|
||||
echo "import: account-wide (## SavedVariables)"
|
||||
if [ -d "$acct_dir/SavedVariables" ]; then
|
||||
for f in "$acct_dir/SavedVariables"/*.lua; do
|
||||
[ -e "$f" ] || continue
|
||||
do_one "$f" "$dest/$(basename "$f")"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$realm" ] && [ -n "$character" ]; then
|
||||
key="$(token "$realm")-$(token "$character")"
|
||||
echo "import: per-character (## SavedVariablesPerCharacter) → $key"
|
||||
cdir="$acct_dir/$realm/$character/SavedVariables"
|
||||
if [ -d "$cdir" ]; then
|
||||
for f in "$cdir"/*.lua; do
|
||||
[ -e "$f" ] || continue
|
||||
do_one "$f" "$dest/$key/$(basename "$f")"
|
||||
done
|
||||
else
|
||||
echo " (no such character dir: $cdir)" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── key bindings ──────────────────────────────────────────────────────────────────────────────
|
||||
# Transpose `bind <KEY> <COMMAND>` into `bind <COMMAND> <key...>`, grouping by command and keeping
|
||||
# the order the keys appeared in, which is the order WoW itself lists a command's primary and
|
||||
# secondary binding.
|
||||
# benilla's command registry, read from the source so this cannot drift from it.
|
||||
known_commands() {
|
||||
local f="$root/crates/benilla-app/src/bindings/commands.rs"
|
||||
# `spec!("NAME", ...)` on one line, plus the rustfmt-wrapped form with the name on the next.
|
||||
{ grep -ohE 'spec!\("[A-Z0-9_]+"' "$f" | grep -oE '"[A-Z0-9_]+"' | tr -d '"'
|
||||
awk '/spec!\($/{getline; gsub(/[ \t",]/,""); print}' "$f"; } | sort -u
|
||||
}
|
||||
|
||||
# Transpose `bind <KEY> <COMMAND>` into `bind <COMMAND> <key...>`, grouping by command and keeping
|
||||
# the order the keys appeared in — the order WoW itself lists a command's primary then secondary.
|
||||
#
|
||||
# Two things here are load-bearing:
|
||||
#
|
||||
# * `gsub(/\r/, "")` — `bindings-cache.wtf` is a Windows file with CRLF endings, so every command
|
||||
# name carries a trailing CR. Without this, *nothing* matches the registry and the import looks
|
||||
# like it worked while binding nothing (verified: 0 of 161 matched before stripping, 147 after).
|
||||
# * the registry filter — benilla does not implement all 161 commands, and `store.rs`'s `resolve`
|
||||
# *keeps* an unknown command's row (for addon commands registered later at world entry) after
|
||||
# having already stolen its keys from whatever held them. So importing a command benilla lacks
|
||||
# takes a key away from a working binding and gives it to something that never exists. The
|
||||
# stock snapshot contains exactly that hazard: `CAMERAORSELECTORMOVE <- BUTTON1` and
|
||||
# `TURNORACTION <- BUTTON2` would steal left- and right-click mouselook.
|
||||
transpose_bindings() {
|
||||
known_commands > "$tmp_known"
|
||||
awk -v knownfile="$tmp_known" '
|
||||
BEGIN { while ((getline k < knownfile) > 0) known[k] = 1 }
|
||||
{ gsub(/\r/, "") }
|
||||
$1 == "bind" && NF >= 3 {
|
||||
cmd = $3
|
||||
if (!(cmd in known)) { skipped[cmd] = 1; next }
|
||||
if (!(cmd in keys)) { order[++n] = cmd; keys[cmd] = $2 }
|
||||
else { keys[cmd] = keys[cmd] " " $2 }
|
||||
}
|
||||
END {
|
||||
print "# benilla key bindings — imported from bindings-cache.wtf (transposed)"
|
||||
for (i = 1; i <= n; i++) print "bind " order[i] " " keys[order[i]]
|
||||
ns = 0; for (c in skipped) ns++
|
||||
if (ns > 0) {
|
||||
printf "# %d command(s) in the source file are not in benilla'"'"'s registry and were\n", ns
|
||||
print "# left out on purpose: importing one steals its key from a working binding."
|
||||
for (c in skipped) print "# " c
|
||||
}
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
bind_one() {
|
||||
local src="$1" out="$2" label="$3" lines
|
||||
[ -f "$src" ] || return 0
|
||||
lines=$(awk '$1 == "bind" && NF >= 3' "$src" | wc -l)
|
||||
cmds=$(transpose_bindings "$src" | grep -c '^bind ' || true)
|
||||
unknown=$(transpose_bindings "$src" | grep -c '^# ' || true)
|
||||
printf ' %-22s %s key line(s) → %s command(s)%s → %s\n' \
|
||||
"$label" "$lines" "$cmds" \
|
||||
"$([ "$unknown" -gt 0 ] && printf ', %s not in registry (listed in the file)' "$unknown")" \
|
||||
"${out#$root/}"
|
||||
if [ "$apply" = "1" ]; then
|
||||
mkdir -p "$(dirname "$out")"
|
||||
[ -f "$out" ] && { cp -p "$out" "$out.pre-import.bak"; backed=$((backed+1)); }
|
||||
transpose_bindings "$src" > "$out"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "import: key bindings (transposed, not copied)"
|
||||
bind_one "$acct_dir/bindings-cache.wtf" "$root/benilla-config/bindings/account.txt" "account-wide"
|
||||
if [ -n "$realm" ] && [ -n "$character" ]; then
|
||||
bind_one "$acct_dir/$realm/$character/bindings-cache.wtf" \
|
||||
"$root/benilla-config/bindings/$(token "$realm")-$(token "$character").txt" \
|
||||
"$character"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "import: $copied file(s)$([ "$all" = "1" ] || echo ", $skipped skipped as not-installed")$([ "$backed" -gt 0 ] && echo ", $backed existing backed up to *.pre-import.bak")"
|
||||
if [ "$apply" != "1" ]; then
|
||||
echo "import: DRY RUN — nothing written. Re-run with --apply to do it."
|
||||
fi
|
||||
+6
-16
@@ -104,15 +104,6 @@ printf '%s' "$plain" | grep -q "logout-smoke: empty roster" &&
|
||||
fail "$user has no characters — the re-entry leg cannot run (make one, or rig it: WOW_RIG=…)"
|
||||
printf '%s' "$plain" | grep -q "logout-smoke: done" ||
|
||||
fail "the round trip never completed (no 'logout-smoke: done')"
|
||||
# The logout returns to character select on the link the world was entered by and re-lists there
|
||||
# (`0x5aaeb0` → `0x46b880`): a roster on that world session, and one realmd logon for the walk.
|
||||
printf '%s' "$plain" | grep -q "logout-smoke: FAILED" &&
|
||||
fail "$(printf '%s\n' "$plain" | sed -n 's/.*logout-smoke: FAILED — //p' | tail -1)"
|
||||
printf '%s' "$plain" | grep -q "logout-smoke: roster after the logout on the same link" ||
|
||||
fail "no roster came on the same link after the logout"
|
||||
logons="$(printf '%s\n' "$plain" | grep -cF "net: parked at the realm list")"
|
||||
[ "$logons" -eq 1 ] ||
|
||||
fail "$logons realmd logons in the round trip, expected 1 — the logout logged on again"
|
||||
|
||||
# The re-entered character must be drivable: the leg lists what would block movement (a /logout
|
||||
# root must end with its session), and anything but `none` fails.
|
||||
@@ -138,25 +129,24 @@ panics="$(printf '%s\n' "$plain" | grep -cE 'panicked at')"
|
||||
[ "$errors" -ne 0 ] && fail "$errors ERROR line(s)"
|
||||
|
||||
# A marker counts once per edge it fires on. Each world entry builds a world VM beside the
|
||||
# character screen's glue VM (the reference's `0x490bd0`/`0x48fbf0` pair), so the VM marker counts
|
||||
# character screen's glue VM (the reference's `0x490bd0`/`0x48fbf0` pair), so per-VM markers count
|
||||
# twice per login; half that means an entry adopted the glue VM and its spent `VmMemo`s.
|
||||
sessions=2 # world entries in this walk
|
||||
vms=$((sessions * 2)) # …and the Lua states they cost: a glue VM and a world VM each
|
||||
for marker in "new VM, session"; do
|
||||
for marker in "Fonts.xml loaded"; do
|
||||
n="$(printf '%s\n' "$plain" | grep -cF "$marker")"
|
||||
printf ' %-24s %s\n' "$marker" "$n"
|
||||
[ "$n" -eq "$vms" ] ||
|
||||
fail "'$marker' happened $n time(s), expected $vms — one per VM built, two per login; \
|
||||
$sessions would mean the entry adopted the character screen's VM and inherited its spent VmMemos"
|
||||
done
|
||||
# The in-game UI, `FrameXML.toc`'s `Fonts.xml` included, and the key-binding commands (the core's
|
||||
# `Bindings.xml`, loaded after the toc) build once per login; one count for two logins means the
|
||||
# second reused the first's frame tree.
|
||||
for marker in "Fonts.xml loaded" "UIParent.xml loaded" "commands registered"; do
|
||||
# The in-game UI and the keybinding table (`seed_bindings_for_vm`, run on the world-entry edge)
|
||||
# build once per login; one count for two logins means the second reused the first's frame tree.
|
||||
for marker in "UIParent.xml loaded" "commands registered"; do
|
||||
n="$(printf '%s\n' "$plain" | grep -cF "$marker")"
|
||||
printf ' %-24s %s\n' "$marker" "$n"
|
||||
[ "$n" -eq "$sessions" ] ||
|
||||
fail "'$marker' happened $n time(s), expected $sessions — once per login: the UI and its binding commands rebuilt per login"
|
||||
fail "'$marker' happened $n time(s), expected $sessions — once per login: the UI rebuilt per login and the keybinding table seeded on the entry edge"
|
||||
done
|
||||
|
||||
# The shutdown tail runs once per session: at the /logout, and at the exit, which closes the window
|
||||
|
||||
Reference in New Issue
Block a user