The host's state leaves `_G`: the eleven `__benilla_*` globals, which any addon could read and overwrite, move into the host's model (and, for the error handler, the Lua registry, as the reference keeps it), and the getters over them are Rust bindings answering the reference's shapes. - `GetTime` returns one number (`0x515ea0`), and `GetGameTime` returns hour then minute (`0x515ee0`). The tick advances the model clock, and every cooldown and buff reader reads it. - `GetZoneText`, `GetRealZoneText`, `GetSubZoneText` and `GetMinimapZoneText` always answer a string, `""` when unset (`0x48a0a0`-`0x48a100`). `GetZonePVPInfo` answers three values (`0x48d540`). - `seterrorhandler`/`geterrorhandler` work over a registry slot that starts empty, as the reference's does (`0x7039e0`, `0x702950`, `0x702900`). A non-function raises `Usage: seterrorhandler(errfunc)`. The engine dispatches caught errors through the slot, not through the global (`0x703b40`). The stock `BasicControls.xml` installs `_ERRORMESSAGE` before any addon, so play is unchanged. - The engine's surface allowlist now holds only `_G`. `scripts/api-coverage.sh` no longer excuses `__` names. Verified: - New tests show each of the eleven names absent from `_G` and that writing it changes nothing. On main, `__benilla_now = 0` rewound `GetTime` and every buff and cooldown. - The getter shapes and the error-handler slot are pinned. The surface gate fails when a name is re-added. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run read the clock, zone texts, PvP info and game time, then showed that writing the old names changes nothing. Built on macOS; no platform seam touched. Fixes #576 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
118 lines
5.2 KiB
Bash
Executable File
118 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Addon-API coverage: how much of the 1.12.1 client's global surface benilla's UI VM presents.
|
|
#
|
|
# scripts/api-coverage.sh # the report
|
|
# scripts/api-coverage.sh --missing # + every engine global we do not have
|
|
# scripts/api-coverage.sh --beyond # + every global we have that 1.12 does not
|
|
#
|
|
# The 1.12 side is `reference/1.12-globals.tsv`, the reference client's in-world `_G`
|
|
# (scripts/gen-reference-globals.py); ours is a real `UiScript::new()` dumped through `pairs(_G)`.
|
|
# Never quote the percentage alone: a missing global is an unbuilt feature or a hole in a shipped
|
|
# one, and a global beyond 1.12 sends a feature-detecting addon down a path we cannot honour.
|
|
set -u
|
|
cd "$(dirname "$0")/.."
|
|
|
|
REF="reference/1.12-globals.tsv"
|
|
[ -f "$REF" ] || { echo "no $REF — run scripts/gen-reference-globals.py" >&2; exit 1; }
|
|
|
|
show_missing=0
|
|
show_beyond=0
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--missing) show_missing=1 ;;
|
|
--beyond) show_beyond=1 ;;
|
|
*) echo "usage: $0 [--missing] [--beyond]" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
ours=$(mktemp) || exit 1
|
|
trap 'rm -f "$ours"' EXIT
|
|
echo "asking benilla's VM what it exposes..." >&2
|
|
if ! cargo run -q -p benilla-ui --example dump_globals >"$ours" 2>/dev/null; then
|
|
echo "dump_globals failed — build the workspace first" >&2
|
|
exit 1
|
|
fi
|
|
|
|
awk -F'\t' -v ref="$REF" -v show_missing="$show_missing" -v show_beyond="$show_beyond" '
|
|
# ── the reference side ──────────────────────────────────────────────────────────────────────
|
|
FNR == NR {
|
|
if ($0 ~ /^#/) next
|
|
origin[$1] = $3
|
|
n_ref[$3]++
|
|
if ($2 == "function") n_ref_fn[$3]++
|
|
next
|
|
}
|
|
# ── our side ───────────────────────────────────────────────────────────────────────────────
|
|
{
|
|
ours[$1] = $2
|
|
n_ours++
|
|
o = ($1 in origin) ? origin[$1] : "beyond"
|
|
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.
|
|
if ($1 ~ /^Benilla/) bridge[$1] = 1
|
|
else if ($1 ~ /^(_G|_VERSION|coroutine|print|select)$/) lua51[$1] = 1
|
|
else api[$1] = 1
|
|
}
|
|
}
|
|
END {
|
|
surface = n_ref["engine"] + n_ref["lua"]
|
|
missing = surface - have
|
|
pct = surface ? sprintf("%.0f%%", 100 * have / surface) : "-"
|
|
printf "\nthe 1.12.1 surface — %s (%d names: the running client'\''s _G, plus what the\n shipped UI defines behind a LoadOnDemand window)\n", ref, \
|
|
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", \
|
|
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"]
|
|
|
|
printf "\nbenilla'\''s VM — %d globals (UiScript::new(), asked at runtime)\n\n", n_ours
|
|
printf "1.12 globals: %d we have: %d (%s) missing: %d beyond-1.12: %d (listed)\n", \
|
|
surface, have, pct, missing, length(api)
|
|
print " ^ never quote this without the split: unbuilt-feature vs missing-verb vs superset."
|
|
|
|
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 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)
|
|
dump(transcribable, " ")
|
|
|
|
if (show_beyond || length(api) <= 30) {
|
|
printf "\nWoW API beyond 1.12 (%d):\n", length(api)
|
|
dump(api, " ")
|
|
}
|
|
if (show_missing) {
|
|
printf "\nmissing engine globals (%d):\n", missing
|
|
for (k in origin)
|
|
if ((origin[k] == "engine" || origin[k] == "lua") && !(k in ours)) miss[k] = 1
|
|
dump(miss, " ")
|
|
}
|
|
print ""
|
|
}
|
|
# Print a set as wrapped, sorted, space-separated names.
|
|
function dump(set, indent, k, sorted, i, n, line) {
|
|
n = 0
|
|
for (k in set) sorted[++n] = k
|
|
asort_names(sorted, n)
|
|
line = indent
|
|
for (i = 1; i <= n; i++) {
|
|
if (length(line) + length(sorted[i]) + 1 > 96) { print line; line = indent }
|
|
line = line sorted[i] " "
|
|
}
|
|
if (line != indent) print line
|
|
}
|
|
# Insertion sort — `asort` is a gawk extension and macOS ships BSD awk.
|
|
function asort_names(a, n, i, j, t) {
|
|
for (i = 2; i <= n; i++) {
|
|
t = a[i]
|
|
for (j = i - 1; j >= 1 && a[j] > t; j--) a[j + 1] = a[j]
|
|
a[j + 1] = t
|
|
}
|
|
}
|
|
' "$REF" "$ours"
|