Files
benilla-octo/scripts/gen-reference-glue-surface.py
T
Sam SungandClaude Opus 5.5 a9dd325bb7 A glue Lua state with the reference glue surface, events and ModelFFX (#963)
The first piece of running the stock GlueXML: benilla had no glue Lua state, and no reference data for its surface.

In 1.12.1 the glue state holds the base set (`0x7039e0`) and seven registrars (`0x46abb0`): 119 functions, 105 of them glue-only, with 26 glue events (`0xb41e70`, `0x46a88f`). `RegisterEvent` matches names case-insensitively and drops an unknown one (`0x702140`). The glue builds the 13 base frame types plus `ModelFFX` (`0x46a4fe`), a Model with its own show and paint (`0x46fa00`); the in-game types come only at world entry (`0x495940`).

`reference/` gains the glue event and surface tables, generated and checked against the shapes table. `UiScript::new_glue()` builds that state; a gate holds its globals to the table, with the 108 unbuilt names listed. Nothing in the app changes yet.

Part of #856

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:38:02 +07:00

213 lines
11 KiB
Python
Executable File

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