Files
benilla-octo/scripts/import-wtf.sh
T
gromandClaude Opus 5 81d96af35d 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
2026-10-15 15:01:14 -06:00

192 lines
8.8 KiB
Bash
Executable File

#!/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