#!/usr/bin/env bash # Import addon settings (SavedVariables) from a real WoW install's WTF folder into benilla. # # scripts/import-wtf.sh [ ] # dry run, prints a plan # scripts/import-wtf.sh --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//SavedVariables/.lua → saved/.lua # WTF/Account////SavedVariables/.lua → saved/-/.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 one line per key # bindings/account.txt bind 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 ` into `bind `, 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 ` into `bind `, 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