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>
213 lines
11 KiB
Python
Executable File
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()
|