From 7c17921601ab2d6044bb783ddf95905ba96d1c08 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 23 Sep 2026 19:32:13 +0700 Subject: [PATCH] comments: the generators, the tools and the reference table headers Docstrings keep their usage and exit codes; table headers keep the column contract and the client addresses and lose the dates, the correction history and every path into another repository. A generated header changes in its generator's template, so each of the four generators still reproduces its output byte for byte, and no data row changed in any of the seven tables. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/benilla-ui/src/messages/catalog.rs | 15 +-- reference/1.12-event-catalog.tsv | 15 ++- reference/1.12-event-firesites.tsv | 77 ++++++------ reference/1.12-events.tsv | 14 +-- reference/1.12-globals.tsv | 12 +- reference/1.12-message-catalog.tsv | 2 +- reference/1.12-shapes.tsv | 87 ++++++-------- reference/1.12-verb-events.tsv | 18 ++- scripts/doc-links.py | 54 ++------- scripts/gen-message-catalog.py | 25 ++-- scripts/gen-reference-events.py | 90 +++++--------- scripts/gen-reference-globals.py | 137 +++++++--------------- scripts/gen-reference-verb-events.py | 101 ++++++---------- scripts/mixsum.py | 34 ++---- scripts/pass-span-lint.py | 17 ++- scripts/soundprobe.py | 60 ++++------ scripts/vplsum.py | 63 ++++------ 17 files changed, 304 insertions(+), 517 deletions(-) diff --git a/crates/benilla-ui/src/messages/catalog.rs b/crates/benilla-ui/src/messages/catalog.rs index d8b8f23c..2f61414e 100644 --- a/crates/benilla-ui/src/messages/catalog.rs +++ b/crates/benilla-ui/src/messages/catalog.rs @@ -1,17 +1,14 @@ -//! The 5875 message catalog, row for row. +//! The 1.12 client's message catalog, row for row. //! -//! @generated by `scripts/gen-message-catalog.py` from `reference/1.12-message-catalog.tsv` -//! (wow-5875-re's `re/ui/message-catalog.tsv`, extracted out of `WoW.exe` there). -//! Do not edit by hand — regenerate. +//! @generated by `scripts/gen-message-catalog.py` from `reference/1.12-message-catalog.tsv`. +//! Do not edit by hand; regenerate. use super::{MessageRecord, MsgKind}; -/// Every row of `0xb4b498`, indexed by message id (the ids are dense `0..465`, so -/// `CATALOG[id]` *is* the reference's `0xb4b498 + 20*id`). +/// Every row of `0xb4b498`, indexed by message id: the ids are dense `0..465`, so `CATALOG[id]` +/// is the reference's `0xb4b498 + 20*id`. /// -/// `rustfmt` is held off so that a regeneration is a no-op diff: one row per line is the shape -/// this script emits, and a formatter that rewrapped it would make the file drift from its own -/// generator. +/// `rustfmt` is off: the generator emits one row per line, and a rewrap would drift from it. #[rustfmt::skip] pub static CATALOG: [MessageRecord; 465] = [ MessageRecord { id: 0, key: "ERR_INV_FULL", kind: MsgKind::Error, sound: None, type_tag: 0x00, chat_type: 10 }, diff --git a/reference/1.12-event-catalog.tsv b/reference/1.12-event-catalog.tsv index 0adeae49..f5030486 100644 --- a/reference/1.12-event-catalog.tsv +++ b/reference/1.12-event-catalog.tsv @@ -1,12 +1,11 @@ -# FrameScript C->Lua event catalog — eventId -> name, extracted from WoW.exe 1.12.1.5875 -# source: the event-name pointer array at .data 0xbe1198 (stride 4, 0x225=549 slots), RE 2026-06-21, -# BASE CORRECTED 2026-07-21 (was 0xbe11d8 -> every id was 16 too low; see -# system/object-layer/scratch/lootroll-chat-and-lifecycle.md §3). The base is VERIFIED at -# 0x48fea1-0x48feab: `mov edx,0x225; mov ecx,0xbe1198; call 0x703d90`, and 0x703d90 builds the +# FrameScript C->Lua event catalog of the 1.12.1 client (WoW.exe build 5875): eventId -> name. +# Source: the event-name pointer array at .data 0xbe1198 (stride 4, 0x225 = 549 slots), passed +# at 0x48fea1-0x48feab (`mov edx,0x225; mov ecx,0xbe1198; call 0x703d90`). 0x703d90 builds the # runtime 16-byte-stride dispatch table [0xceef68] as table[i].name = strdup(nameArray[i]) -# (0x703e06-0x703e2e), so the id passed to FrameScript_SignalEvent 0x703e50 / 0x703f50 IS this index. -# NB slots 0..15 carry no `mov imm32` name write in this scan (they are set from a register at -# 0x51acbf and neighbours); coverage is unchanged from the pre-correction extraction (383 rows). +# (0x703e06-0x703e2e), so the id passed to FrameScript_SignalEvent 0x703e50 / 0x703f50 is +# this index. +# Slots 0..15 carry no `mov imm32` name write in this scan (they are set from a register at +# 0x51acbf and neighbours). 0 UNIT_PET 2 UNIT_PET 16 UNIT_HEALTH diff --git a/reference/1.12-event-firesites.tsv b/reference/1.12-event-firesites.tsv index e161c7ea..9542b0b8 100644 --- a/reference/1.12-event-firesites.tsv +++ b/reference/1.12-event-firesites.tsv @@ -1,50 +1,39 @@ -# C->Lua event fire sites — every call AND TAIL-JMP to FrameScript_SignalEvent (0x703e50, ecx=eventId) -# (the 85 `jmp 0x703e50` sites were absent until 2026-08-21 — the census had been written over `call` -# alone, so a tail-jmp fire read as "this event has no fire site here"; see re/audit/re-sweep-status.md) -# and SignalEvent2 (0x703f50, [esp]=eventId + printf-style arg_format). eventId resolved via event-catalog.tsv. -# eventId "dyn" = computed (e.g. a UnitField update loop); blank name = eventId has no entry in the name table. +# C->Lua event fire sites of the 1.12.1 client (build 5875): every call and tail-jmp to +# FrameScript_SignalEvent (0x703e50, ecx=eventId) and SignalEvent2 (0x703f50, [esp]=eventId + +# printf-style arg_format). eventId is resolved via 1.12-event-catalog.tsv; "dyn" = computed +# (e.g. a UnitField update loop); a blank name = the id has no entry in the name table. # -# CAVEAT (2026-09-04, staticpopup round): the `arg_format` column is the FORMAT STRING the site pushes, -# which is a declaration, NOT a census of what the caller actually pushed. The marshaller 0x704f10 -# consumes one vararg per directive (%f eight bytes, everything else four), so a site whose format -# declares more than its caller pushed hands Lua undefined values. `re/audit/signalevent2-fmt-args.py` -# audits all 149 sites (0 unadjudicated) and finds exactly THREE such sites -- read that column with -# these in mind: 0x496230 TRADE_REPLACE_ENCHANT "%s%s%d" declares 3 and pushes 2; 0x5e4527 and -# 0x5e7960 UPDATE_TICKET "%d%s%f%f%f%d%d" declare 10 dwords and push 7 (their sibling 0x5e45e2 pushes -# the full 12). Every other site's format matches its caller. See method.md §6. +# arg_format is the format string the site pushes: a declaration, not a census of what the +# caller pushed. The marshaller 0x704f10 consumes one vararg per directive (%f eight bytes, +# everything else four), so a format that declares more than its caller pushed hands Lua +# undefined values. Of the 149 SignalEvent2 sites exactly three do: 0x496230 +# TRADE_REPLACE_ENCHANT "%s%s%d" declares 3 and pushes 2; 0x5e4527 and 0x5e7960 UPDATE_TICKET +# "%d%s%f%f%f%d%d" declare 10 dwords and push 7 (their sibling 0x5e45e2 pushes the full 12). +# Every other site's format matches its caller. # -# CAVEAT (2026-09-03, found while carving `UpdateSpells` for benilla; the answer was already on `main` -# in system/glue/): the `name` column resolves every id through event-catalog.tsv, which is the -# IN-GAME table (0xbe1198, 549 entries). 0x703d90 is called TWICE with different populations -- -# 0x46a88f with (0xb41e70, 26) for the GLUE phase and 0x48feab with (0xbe1198, 549) for the world -- -# and it rebuilds [0xceef68] wholesale from whichever it is handed, so THE SAME INTEGER NAMES A -# DIFFERENT EVENT IN THE TWO PHASES and 0x703e50 bounds-checks nothing. 22 rows here sit in a -# `Source\Glue\` TU (0x46aa34; 0x46bc9d-0x46c52e inside 0x46b930; 0x46e73d; 0x47461e) and their ids -# belong to the GLUE space: name them from system/glue/scratch/glue-event-name-map.md, never from -# event-catalog.tsv. 0x46aa34 was the one row where that produced a WRONG name (id 22 read as -# UNIT_MAXHEALTH; it is FRAMES_LOADED -- glue-event-name-map.md's own control, with GlueParent.lua:94 -# as the independent oracle); it is blanked below. The other 21 were already blank, because those ids -# are NULL slots in the in-game table. Owed: the glue node names them in its own artifact. +# The name column resolves every id through the in-game table (0xbe1198, 549 entries). 0x703d90 +# is called twice with different populations, 0x46a88f with (0xb41e70, 26) for the glue phase +# and 0x48feab with (0xbe1198, 549) for the world, and rebuilds [0xceef68] wholesale from +# whichever it is handed, so the same integer names a different event in the two phases, and +# 0x703e50 bounds-checks nothing. 22 rows here sit in a `Source\Glue\` TU (0x46aa34; +# 0x46bc9d-0x46c52e inside 0x46b930; 0x46e73d; 0x47461e): their ids belong to the glue space and +# are never named from 1.12-event-catalog.tsv; their names are blank here. 0x46aa34 fires id 22, +# which is FRAMES_LOADED in the glue table (GlueParent.lua:94), not the in-game UNIT_MAXHEALTH. # -# CAVEAT 2 (2026-09-03, carving UNIT_FLAGS for benilla): AN EVENT WITH NO ROW HERE IS NOT AN EVENT -# WITH NO PRODUCER. This census is over the two signal helpers' own call/jmp sites, and the whole -# per-field UNIT_* family reaches them through a WRAPPER: 0x515e50(&guid, id) fans out over the unit -# tokens and fires 0x703f50(id, "%s", token) at 0x515e7a -- the "?" row below. Its 38 callers carry -# the id in edx, and one of them, 0x51bd5b in the UpdateField watch callback 0x51bd50, computes it as -# (registered field byte offset >> 2). So ids 0x00-0xb5 -- every named unit descriptor field, -# UNIT_FLAGS 40 included -- are produced there and appear nowhere in this file. -# SCOPE WIDENED 2026-09-07 (PLAYER_FLAGS round, benilla dispatch): "0x00-0xb5" is the COMPUTED-id -# subset only, and reading it as the whole absent set is a trap -- most 0x515e50 sites carry a -# LITERAL id in edx, and those are absent from this file too, at any magnitude. The literal set is -# {0x10 0x16 0x1c 0x1d 0x1e 0x29 0xb7 0xb8 0xb9 0xba 0xbb 0x159 0x197 0x20a}. Notably 0x197 = 407 -# PLAYER_FLAGS_CHANGED (0x5eea35 mov edx,0x197 / 0x5eea3d call 0x515e50, inside the PLAYER_FLAGS -# delta handler 0x5ee990) sits OUTSIDE 0x00-0xb5, so a reader checking 407 against the range above -# would wrongly conclude it has no producer. It has one; it is simply not a SignalEvent call site. -# See system/object-layer/scratch/player-flags-delta-arms.md. Mechanism, the -# bounded four-way census that makes 0x51bd50 the SOLE producer of 40, and the fire condition -# (a repe cmpsb of the watched span against the object's shadow, at 0x4655bb) are in -# system/ui/scratch/unit-field-event-bridge.md. Same shape for 0x5de4b0 (id = its userdata, -# arg1 hardcoded "player") and 0x49b199 (id from the table at 0x8062e0). +# An event with no row here is not an event with no producer. This covers the two signal +# helpers' own call/jmp sites, and the per-field UNIT_* family reaches them through a wrapper: +# 0x515e50(&guid, id) fans out over the unit tokens and fires 0x703f50(id, "%s", token) at +# 0x515e7a, the "?" row below. Its 38 callers carry the id in edx; one of them, 0x51bd5b in the +# UpdateField watch callback 0x51bd50, computes it as (registered field byte offset >> 2), so +# ids 0x00-0xb5 (every named unit descriptor field, UNIT_FLAGS 40 included) are produced there +# and appear nowhere in this file. 0x51bd50 is the sole producer of 40; its fire condition is a +# repe cmpsb of the watched span against the object's shadow, at 0x4655bb. Most other +# 0x515e50 sites carry a literal id in edx, absent here too at any magnitude: {0x10 0x16 0x1c +# 0x1d 0x1e 0x29 0xb7 0xb8 0xb9 0xba 0xbb 0x159 0x197 0x20a}. So 0x197 = 407 +# PLAYER_FLAGS_CHANGED (0x5eea35 mov edx,0x197 / 0x5eea3d call 0x515e50, inside the +# PLAYER_FLAGS delta handler 0x5ee990) lies outside 0x00-0xb5 and still has a producer. Same +# shape for 0x5de4b0 (id = its userdata, arg1 hardcoded "player") and 0x49b199 (id from the +# table at 0x8062e0). fire_site_va dispatcher eventId name arg_format 0x4018df SignalEvent2 259 TIME_PLAYED_MSG %d%d 0x46aa34 SignalEvent 22 diff --git a/reference/1.12-events.tsv b/reference/1.12-events.tsv index b1a865a8..e31f163d 100644 --- a/reference/1.12-events.tsv +++ b/reference/1.12-events.tsv @@ -1,14 +1,14 @@ -# The 1.12.1 client's FrameScript events and the ARGUMENTS their producers push. -# GENERATED — scripts/gen-reference-events.py. Source: wow-5875-re's -# re/events/event-catalog.tsv + event-firesites.tsv. Decision 2140. +# The 1.12.1 client's FrameScript events and the arguments their producers push. +# Generated by scripts/gen-reference-events.py from reference/1.12-event-catalog.tsv +# and reference/1.12-event-firesites.tsv. # # arg_formats `|`-separated, one per distinct producer shape. `()` = zero Lua values. # Directives: %s string, %d/%u number, %f number. An event with two -# producers of different shapes carries both, and which one a given -# transition takes is a question about the transition, not the name. +# producers of different shapes carries both; which one fires depends +# on the transition, not the name. # conf exact = gate on it. advisory = a contributing site declares more -# varargs than it pushes. none = no producer family reaches it HERE, -# which is not a claim that it has none; never gate on it. +# varargs than it pushes. none = no producer family reaches it here, +# which does not mean it has none; never gate on it. # producers signal (0x703e50, no varargs) | signal2 (0x703f50, format-driven) | # unit-field-bridge (0x51bbb0/0x51bd50 -> 0x515e50, always %s) | # token-fanout (0x515e50 with a literal id, also %s). diff --git a/reference/1.12-globals.tsv b/reference/1.12-globals.tsv index 5b7cd6f6..1671d002 100644 --- a/reference/1.12-globals.tsv +++ b/reference/1.12-globals.tsv @@ -1,9 +1,9 @@ -# The 1.12.1 client's global namespace. GENERATED — scripts/gen-reference-globals.py. -# Source: wow-5875-re's W5875Capture fixture (the running client's in-world _G), -# attributed against the complete 1.12 shipped-UI corpus. Decision 1189. -# UNION every name that corpus defines but the capture did not contain (type -# `lod`): the twelve Blizzard_* addons are LoadOnDemand, so a live dump misses -# them unless the player had opened those windows. Decision 1200. +# The 1.12.1 client's global namespace. Generated by scripts/gen-reference-globals.py. +# Source: a capture of the running client's in-world _G (the maintainer's analysis, +# `WOW_RE`), attributed against the complete 1.12 shipped-UI corpus. +# Also every name that corpus defines but the capture lacks, as type `lod`: the +# twelve Blizzard_* addons are LoadOnDemand, so a live dump misses them unless +# their windows were opened. # name type origin(lua|engine|framexml) ABANDON_PET string framexml ABANDON_QUEST string framexml diff --git a/reference/1.12-message-catalog.tsv b/reference/1.12-message-catalog.tsv index 05df0939..1e73bb34 100644 --- a/reference/1.12-message-catalog.tsv +++ b/reference/1.12-message-catalog.tsv @@ -1,7 +1,7 @@ # build 5875 CGGameUI message catalog (the 0xb4b498 table indexed by 0x496720) # 465 rows, ids 0x0..0x1d0; consumer bound `cmp id,0x1d1; jge` at 0x49672c # kind 0 -> 0x49a870(text, chan); 1 -> UI_INFO_MESSAGE 0xe1; 2 -> UI_ERROR_MESSAGE 0xe0; 3 -> console 0x63cd00 -# regenerate (in wow-5875-re): python3 re/ui/extract-message-catalog.py > re/ui/message-catalog.tsv +# Vendored from the maintainer's analysis (`WOW_RE`); messages/catalog.rs is generated from it. id_hex id_dec row_va key kind sound type_tag chan 0x0 0 0xb4b498 ERR_INV_FULL 2 NONE 0x0 10 0x1 1 0xb4b4ac ERR_BANK_FULL 2 NONE 0x44 10 diff --git a/reference/1.12-shapes.tsv b/reference/1.12-shapes.tsv index 33370c70..4bae74f5 100644 --- a/reference/1.12-shapes.tsv +++ b/reference/1.12-shapes.tsv @@ -1,56 +1,43 @@ -# Lua binding SHAPE table for WoW.exe 1.12.1 build 5875 — one row per registered -# binding: name -> (argc, return arity, return kinds), with per-column confidence. -# WHY: a client that gates its Lua globals by NAME accepts a binding with the right -# name and the wrong return arity, which silently breaks a stock FrameXML file. -# Evidence + five worked examples: system/ui/scratch/binding-shape-arity-law.md. +# Lua binding shapes of the 1.12.1 client (WoW.exe build 5875): one row per registered +# binding, name -> (argc, return arity, return kinds), with per-column confidence. # -# 82 registrar tables, 1722 bindings, 1722 analysed, 0 unresolved registrar sites. -# Four registration routes, extents read from the REGISTRAR's own literal, never a -# run scan (see re/audit/registrar-dump.py): 0x704120 RegisterFunction loops; -# 0x701d80 widget method tables; the INLINED base-library loop at 0x811e28; and the -# four luaL_openlib 0x6f4dc0 tables (bit/math/table/string). +# 82 registrar tables, 1722 bindings, 1722 analysed, 0 unresolved registrar sites. Four +# registration routes, extents read from the registrar's own literal: 0x704120 +# RegisterFunction loops; 0x701d80 widget method tables; the inlined base-library loop at +# 0x811e28; and the four luaL_openlib 0x6f4dc0 tables (bit/math/table/string). # -# COLUMN CONTRACT — a consumer may rely on exactly this: -# arity / arity_conf SOUND. exact = one constant; set = a `|`-separated set of -# possible counts; unknown = `?`. The lattice only widens on merge, so an -# imprecise result is a set or `?` and NEVER a wrong single number. Gate on this. -# kind `string?` STRING-OR-NIL, and this distinction has bitten a consumer. -# lua_pushstring 0x6f3890 alone begins `test edx,edx ; jne` and TAIL-JUMPS to -# lua_pushnil when the char* is NULL, so a push call does not prove a string -# reached Lua. `string` is emitted only where the pointer is a constant that -# resolves to a readable literal; every other lua_pushstring is `string?`. This -# is why an empty/miss path can show a fixed arity with no kind alternation and -# still answer nil in that slot (GetMapInfo -> nil,0,0; GetChannelName -> 0,nil,0). -# kinds / kinds_conf ADVISORY unless kinds_conf == agree, which means the -# independent lua_push* trace length matches `eax` at every ret AND every ret -# was checked. A loop (GetTexCoord: 2 textual pushes, 8 real) -> unverified. -# `agree` is a claim about THIS BINDING'S OWN ops. Two things it does not say. -# (1) It used to say nothing about an op with a net delta of ZERO that rewrites -# a slot in place: lua_gettable 0x6f3a40 and lua_rawget 0x6f3b00 pop the key -# off the top and write the looked-up value into that same slot, so the kind -# recorded was the KEY's. That is now modelled (REPLACE_TOP -> `any`), and it -# was wrong on exactly three rows: getglobal, pairs, ipairs. -# (2) It still says nothing about a CALLEE that pushes on its own behalf -- -# the trace cannot see those, so the returned window may hold slots it never -# described. Not corrected; DECLARED, as the note token `delegated-push`. -# Evidence for both: system/ui/scratch/binding-kinds-stack-overwrite.md. -# argc / argc_conf NEVER GATE ON THIS. argc is a max over observed constant Lua -# stack indices; it has no unknown state and degrades to a silent LOWER BOUND when -# a binding delegates argument parsing to a helper. Such rows say lower-bound, but -# the detector is one level deep, so treat EVERY argc as a floor. -# name NOT A KEY. 60 names are registered from more than one table and 21 of -# those DIFFER IN ARITY between registrations — GetBuildInfo returns 5 on the glue -# screen (table 0x8373b8) and 3 in-game (0x83de68); GetAddOnInfo 8 and 7. Key on -# `fn`, or on (table_va, name). Every such row carries the note name-not-unique. -# notes stack-leak = the reference pushes a value and then returns 0 (or fewer). -# A faithful client must NOT copy this; these are the disabled lottery/vendor -# stubs. variadic = the count comes from lua_gettop, no constant exists. -# delegated-push = the binding calls a foreign callee that itself pushes, so -# `agree` covers this row's own ops only (see the kinds contract above). -# cfg-truncated / indirect-branch / iteration-cap = an edge the walk could not -# resolve; such a row's arity is not covered by the soundness claim above. +# Column contract: +# arity / arity_conf sound. exact = one constant; set = a `|`-separated set of possible +# counts; unknown = `?`. An imprecise result is a set or `?`, never a wrong single +# number. Gate on this. +# kind `string?` string or nil. lua_pushstring 0x6f3890 begins `test edx,edx ; jne` +# and tail-jumps to lua_pushnil when the char* is NULL, so `string` is emitted only where +# the pointer is a constant that resolves to a readable literal; every other +# lua_pushstring is `string?`. A miss path can show a fixed arity and still answer nil in +# that slot (GetMapInfo -> nil,0,0; GetChannelName -> 0,nil,0). +# kinds / kinds_conf advisory unless kinds_conf == agree: the independent lua_push* trace +# length matches `eax` at every ret, and every ret was checked. A loop (GetTexCoord: 2 +# textual pushes, 8 real) -> unverified. `agree` is about the binding's own ops: +# lua_gettable 0x6f3a40 and lua_rawget 0x6f3b00 replace the key on top with the looked-up +# value (recorded as `any`), and a callee that pushes on the binding's behalf is invisible +# to the trace, so the returned window may hold slots it never described (note +# `delegated-push`). +# argc / argc_conf never gate on this. argc is a max over observed constant Lua stack +# indices, with no unknown state, and a silent lower bound when a binding delegates +# argument parsing to a helper. Such rows say lower-bound, but the detector is one level +# deep, so treat every argc as a floor. +# name not a key. 60 names are registered from more than one table and 21 of those +# differ in arity between registrations: GetBuildInfo returns 5 on the glue screen (table +# 0x8373b8) and 3 in-game (0x83de68); GetAddOnInfo 8 and 7. Key on `fn`, or on +# (table_va, name). Every such row carries the note name-not-unique. +# notes stack-leak = the reference pushes a value and then returns 0 (or fewer): the +# disabled lottery/vendor stubs, which a faithful client does not copy. variadic = the +# count comes from lua_gettop, no constant exists. delegated-push = a foreign callee +# pushes on the binding's behalf, so `agree` covers this row's own ops only. +# cfg-truncated / indirect-branch / iteration-cap = an edge the walk could not resolve; +# such a row's arity is not covered by the soundness claim above. # -# regenerate (in wow-5875-re): python3 re/audit/binding-shapes.py > re/audit/binding-shapes.tsv +# Vendored from the maintainer's analysis (`WOW_RE`); no generator for it lives in this repo. name fn pair_va table_va table_kind argc argc_conf arity arity_conf kinds kinds_conf notes AbandonQuest 0x4dfe00 0x84b128 0x84b0f0 global 0 exact 0 exact () agree AbandonSkill 0x4d3ab0 0x849d08 0x849cf8 global 1 exact 0 exact () agree diff --git a/reference/1.12-verb-events.tsv b/reference/1.12-verb-events.tsv index 49198d09..d5a4ab3e 100644 --- a/reference/1.12-verb-events.tsv +++ b/reference/1.12-verb-events.tsv @@ -1,16 +1,14 @@ -# The 1.12.1 client's FrameScript events fired FROM INSIDE A LUA VERB, keyed by the verb. -# GENERATED — scripts/gen-reference-verb-events.py. Source: wow-5875-re's -# re/audit/binding-shapes.tsv + re/events/event-firesites.tsv + re/names/out/names-5875.tsv -# + system/ui/scratch/disasm-full.txt. +# The 1.12.1 client's FrameScript events fired from inside a Lua verb, keyed by the verb. +# Generated by scripts/gen-reference-verb-events.py from reference/1.12-shapes.tsv, +# reference/1.12-event-firesites.tsv and the maintainer's analysis (`WOW_RE`). # # shape body = a fire site inside the verb's own extent. -# helper = a fire site inside a function that ONLY registered verbs call +# helper = a fire site inside a function that only registered verbs call # (at most 8 of them); `via` is that helper. A helper with any -# non-verb caller is a state event's producer and is NOT here. -# sites the fire sites themselves, for the grep into wow-re. -# A floor, not a census: no row is not a claim of no fire (see the generator's header). -# Sites are attributed against the ledgers' verified function extents; a site past its -# candidate's extent is left out rather than mis-filed (decision 2257). +# non-verb caller produces a state event and is not here. +# sites the fire sites themselves. +# A floor, not a census: a verb with no row may still fire an event. +# A site past its candidate function's extent is left out rather than mis-filed. # 43 pairs over 41 verbs and 27 events; 2 site(s) past a verified extent. verb event shape via sites AddSkillUp SKILL_LINES_CHANGED helper 0x4d32d0 0x4d346f diff --git a/scripts/doc-links.py b/scripts/doc-links.py index 8bf509f8..705a3609 100755 --- a/scripts/doc-links.py +++ b/scripts/doc-links.py @@ -1,36 +1,16 @@ #!/usr/bin/env python3 -"""doc-links — fail on a doc link whose target does not exist ANYWHERE in the workspace. +"""Fail on a doc link whose target is declared nowhere in the workspace. -WHY THIS IS NARROW, AND WHY THE OBVIOUS VERSION IS WRONG (decision 1925). The docs are this -project's knowledge base, so a doc link that goes nowhere is real rot — decision 1903 found a -`UNIT_FLAG_STUNNED` comment still pointing at a `MovementState::stunned` that a later decision had -deleted, and nothing caught it because nothing runs rustdoc. - -The obvious fix is `-D rustdoc::broken_intra_doc_links`. Measured, it does not survive contact with -this codebase: **941 warnings, 264 of them unambiguous `crate::`/`super::`/`self::` paths** — and -almost none of that is rot. - - * **195** name a target that IS declared in its crate. They do not resolve because the target is - a *private* module named through its parent (`crate::net::apply`, where `mod apply;` is private - to `net`) — not a rustdoc quirk but Rust's own name resolution, so the link never worked. - Making them resolve means widening visibility for a documentation reason, trading encapsulation - for navigation. - * **~50** are one crate naming another's module with `crate::` — `benilla-world` writing - `crate::entities`, which is benilla-app's and which benilla-world cannot depend on. Those can - never resolve; the only "fix" is de-linking prose that reads fine as prose. - -Both are conventions this codebase writes on purpose. A gate on them would be 249 edits of churn -that improve nothing, and would then have to stay green forever. - -What IS unambiguous rot is the residue: a link whose **leaf is declared nowhere in the workspace** -— the item is gone, so no reading of the link can be right. That was 15 sites when this was -written (1925 fixed them); this script keeps it at zero. +It reads rustdoc's unresolved-link warnings and fails on a `crate::`, `super::`, `self::` or +`Self::` path whose leaf name is declared nowhere: the item is gone. Every other warning is a +convention here, not rot (a private module named through its parent, another crate's module +named with `crate::`, a bare name for an unimported bevy type). scripts/doc-links.py # the gate: exit 1 on any dead-target link - scripts/doc-links.py --report # + the benign counts, for anyone re-opening the question + scripts/doc-links.py --report # + the benign counts -The leaf test is a declaration grep, deliberately loose: a name declared anywhere counts, because -the failure this catches is "the thing was deleted", not "the path is spelled right". +The leaf test is a loose declaration grep: a name declared anywhere counts, since what it catches +is a deleted item, not a misspelled path. """ from __future__ import annotations @@ -54,15 +34,10 @@ def run_rustdoc() -> str: def declared_names() -> set[str]: - """Every name the workspace declares, in ONE pass. - - A grep per link is what the first version did, and at ~900 links over a 19-crate tree it does - not finish inside a gate's patience. One scan, then set lookups. - """ + """Every name the workspace declares, in one scan: a grep per link is too slow for a gate.""" names: set[str] = set() - # `fn` gets its own pattern: a single alternation swallows the `const` of `const fn foo` and - # captures "fn" as the name, so `foo` is never indexed and the gate reports a live item as - # deleted. That bug shipped for one run of this script and is the reason the two passes exist. + # `fn` gets its own pattern: in one alternation, `const fn foo` captures "fn" and never + # indexes `foo`. pats = [ re.compile(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)"), re.compile(r"\b(?:struct|enum|trait|type|const|static|union|mod)\s+([A-Za-z_][A-Za-z0-9_]*)"), @@ -87,11 +62,8 @@ def main() -> int: names = declared_names() dead, benign = [], 0 for target, path, line in found: - # ONLY paths the author asserted are in THIS crate. A bare `[`SystemParam`]` or - # `[`KeyboardInput`]` names a bevy type that is simply not imported for linking — real, - # not rot — and there is no honest way to tell those from a deleted local name. A - # `crate::`/`super::`/`self::`/`Self::` path carries the assertion, so its absence is - # unambiguous. + # Only a `crate::`/`super::`/`self::`/`Self::` path asserts a local item; a bare name may + # be a bevy type not imported for linking (`SystemParam`), which is not rot. if not re.match(r"^(crate|super|self|Self)::", target): benign += 1 continue diff --git a/scripts/gen-message-catalog.py b/scripts/gen-message-catalog.py index d9384ce7..211f83e7 100755 --- a/scripts/gen-message-catalog.py +++ b/scripts/gen-message-catalog.py @@ -1,17 +1,13 @@ #!/usr/bin/env python3 r"""Generate `crates/benilla-ui/src/messages/catalog.rs` from `reference/1.12-message-catalog.tsv`. -The TSV is wow-5875-re's byte-exact sweep of build 5875's `0xb4b498` registry -- the table -`CGGameUI::DisplayError 0x496720` indexes by message id -- vendored under `reference/` like the -other tables (it is regenerated there with `re/ui/extract-message-catalog.py`). This script only -re-shapes it into a Rust const, so the table ships with benilla and is never read out of a -sibling checkout. +The table is the reference's message registry at `0xb4b498`, which `CGGameUI::DisplayError` +`0x496720` indexes by message id; this re-shapes it into a Rust static that ships with benilla. python3 scripts/gen-message-catalog.py # the vendored table -> the .rs, in place python3 scripts/gen-message-catalog.py # another table -Refuses rather than emits on anything it does not understand: a non-dense id range, a duplicate key, -an unknown kind. A generated table that quietly dropped a row would be worse than no table. +It refuses rather than emits on a non-dense id range, a duplicate key or an unknown kind. """ import os import sys @@ -23,20 +19,17 @@ OUT = os.path.join(REPO, "crates", "benilla-ui", "src", "messages", "catalog.rs" KINDS = {0: "Chat", 1: "Info", 2: "Error"} HEADER = """\ -//! The 5875 message catalog, row for row. +//! The 1.12 client's message catalog, row for row. //! -//! @generated by `scripts/gen-message-catalog.py` from `reference/1.12-message-catalog.tsv` -//! (wow-5875-re's `re/ui/message-catalog.tsv`, extracted out of `WoW.exe` there). -//! Do not edit by hand — regenerate. +//! @generated by `scripts/gen-message-catalog.py` from `reference/1.12-message-catalog.tsv`. +//! Do not edit by hand; regenerate. use super::{MessageRecord, MsgKind}; -/// Every row of `0xb4b498`, indexed by message id (the ids are dense `0..465`, so -/// `CATALOG[id]` *is* the reference's `0xb4b498 + 20*id`). +/// Every row of `0xb4b498`, indexed by message id: the ids are dense `0..465`, so `CATALOG[id]` +/// is the reference's `0xb4b498 + 20*id`. /// -/// `rustfmt` is held off so that a regeneration is a no-op diff: one row per line is the shape -/// this script emits, and a formatter that rewrapped it would make the file drift from its own -/// generator. +/// `rustfmt` is off: the generator emits one row per line, and a rewrap would drift from it. #[rustfmt::skip] pub static CATALOG: [MessageRecord; {count}] = [ """ diff --git a/scripts/gen-reference-events.py b/scripts/gen-reference-events.py index 91e9309e..e7fa8ae9 100755 --- a/scripts/gen-reference-events.py +++ b/scripts/gen-reference-events.py @@ -1,76 +1,51 @@ #!/usr/bin/env python3 -"""Regenerate `reference/1.12-events.tsv` — every FrameScript event the 1.12.1 client can -dispatch, with the ARGUMENTS its producers push. +"""Regenerate `reference/1.12-events.tsv`: each 1.12 FrameScript event and the arguments it carries. scripts/gen-reference-events.py [--catalog FILE] [--firesites FILE] [--out FILE] -**Why this exists** (decision 2140). benilla already gates two halves of the event seam: a stock -chain file listening for an event nothing fires -(`reference_ui::every_event_a_chain_file_registers_has_a_producer`, 1889) and an event we fire that -1.12 does not have (`every_event_we_fire_is_an_event_the_reference_has`, 1883). Both compare -NAMES. Nothing compared the *arguments*, and an event name is a plain string at both ends — so a -fire with the wrong argument count is silent on both sides, exactly as a binding with the wrong -return arity was before 1842. It is the same class of bug one API over, and it had three live -instances: `PLAYERBANKSLOTS_CHANGED`, `UNIT_PET_EXPERIENCE` and `UNIT_PET_TRAINING_POINTS`, the -last of which routes through `PetPaperDollFrame_OnEvent`'s `elseif ( arg1 == "pet" )` catch-all and -so reached nobody at all. - -**The oracle is wow-5875-re's own two censuses**, vendored under `reference/` the way -`1.12-shapes.tsv` is: +The inputs are vendored under `reference/`, so this runs from any clone and reproduces the +committed table byte for byte: - `1.12-event-catalog.tsv` eventId -> name, off the name-pointer array at `.data 0xbe1198` - - `1.12-event-firesites.tsv` every `call` AND tail-`jmp` to `FrameScript_SignalEvent` + - `1.12-event-firesites.tsv` every `call` and tail-`jmp` to `FrameScript_SignalEvent` (`0x703e50`, no varargs) and `SignalEvent2` (`0x703f50`, - printf-style), with the FORMAT STRING each site pushes + printf-style), with the format string each site pushes -**Three producer families, because a literal fire site is not the whole picture.** The firesites -census is over the two signal helpers' own call sites, and its own header says in capitals that an -event with no row there is not an event with no producer: +A literal fire site is not every producer; the families are: - signal a `0x703e50` site — `__fastcall(ecx = id)`, a plain `ret`, ZERO Lua values. - signal2 a `0x703f50` site — the pushed format string is the argument shape. + signal a `0x703e50` site: `__fastcall(ecx = id)`, a plain `ret`, zero Lua values. + signal2 a `0x703f50` site: the pushed format string is the argument shape. unit-field-bridge the generic UpdateField -> event bridge (`0x51bbb0` registers one watch per named unit-window field; `0x51bd50` -> `0x515e50` fans out over the unit - tokens and calls `0x703f50(id, "%s", token)`). Every event id below `0xb6` - that HAS a name is produced there and appears in no firesites row. - Cross-check: the 54 catalog ids under `0xb6` are exactly the id set - wow-re read out of the registration loop by hand - (`system/ui/scratch/unit-field-event-bridge.md` §2.1) — two independent - derivations, no difference. - token-fanout the same `0x515e50` fan-out reached with a LITERAL id in `edx` rather than a - computed one, so also `%s` and also absent from the firesites census. The - fourteen ids are wow-re's own list, in that file's header. + tokens and calls `0x703f50(id, "%s", token)`). Every named event id below + `0xb6` is produced there and has no fire-site row. + token-fanout the same `0x515e50` fan-out reached with a literal id in `edx`: also `%s`, + also absent from the fire sites. -**Confidence, and what a consumer may gate on:** +Confidence, and what a consumer may gate on: exact every contributing producer's shape is known. Gate on this. - advisory a contributing site is one of the three whose pushed format DECLARES more varargs than - its caller pushes (`re/audit/signalevent2-fmt-args.py` audits all 149 and finds - exactly these): `0x496230 TRADE_REPLACE_ENCHANT`, `0x5e4527`/`0x5e7960 UPDATE_TICKET`. - Faithful clients must not copy a bug that hands Lua undefined values. - none the name is in the catalog and NO producer family reaches it here. **This is not a - claim that it has none** — see the census caveat above. Never gate on it. + advisory a contributing site's format declares more varargs than its caller pushes + (`0x496230` TRADE_REPLACE_ENCHANT, `0x5e4527`/`0x5e7960` UPDATE_TICKET), which hands + Lua undefined values; do not copy it. + none no producer family reaches the name here, which does not mean it has none. Never + gate on it. -Glue-space rows are dropped: `0x703d90` is called twice with different name populations, so the -same integer names a different event on the glue screen than in the world. wow-re blanks those -rows' names; this drops them by fire-site address as well, from the four TU ranges its header -names, so a future re-derivation that fills a name in cannot leak one through. - -Both inputs are in the repo, so this runs from any clone and reproduces the committed table byte -for byte; only the two inputs themselves are regenerated in the RE repo. +Glue-space sites are dropped by address: `0x703d90` loads a separate name table for the glue +screen, so the same id names a different event there. Their names are blank in the input, and +dropping by address keeps a name filled in later from leaking through. """ import argparse import os import sys -# The `Source\Glue\` TU ranges whose event ids belong to the GLUE name space, from -# `re/events/event-firesites.tsv`'s own header. +# The `Source\Glue\` fire-site ranges (`1.12-event-firesites.tsv`'s header): glue-space ids. GLUE_SITES = [(0x46AA34, 0x46AA34), (0x46BC9D, 0x46C52E), (0x46E73D, 0x46E73D), (0x47461E, 0x47461E)] # The three sites whose format string declares more varargs than the caller pushes. DECLARES_MORE_THAN_IT_PUSHES = {0x496230: "TRADE_REPLACE_ENCHANT", 0x5E4527: "UPDATE_TICKET", 0x5E7960: "UPDATE_TICKET"} -# `0x515e50` callers carrying a LITERAL id — the token fan-out reached outside the generic bridge. +# `0x515e50` callers with a literal id: the token fan-out reached outside the generic bridge. TOKEN_FANOUT_IDS = [0x10, 0x16, 0x1C, 0x1D, 0x1E, 0x29, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0x159, 0x197, 0x20A] -# `0x51bbb0`'s walk bound: `cmp esi,0xb6`. Every NAMED unit-window field below it is watched. +# `0x51bbb0`'s walk bound (`cmp esi,0xb6`): every named unit-window field below it is watched. UNIT_WINDOW_FIELDS = 0xB6 # The zero-argument format, spelled so the column is never empty. NONE = "()" @@ -140,8 +115,7 @@ def main(): if eid in catalog: add(catalog[eid], eid, "token-fanout", "%s") - # Every remaining catalog name, so the table is the whole surface and a consumer can tell - # "no producer found" from "not an event". + # Every remaining catalog name too, so "no producer found" differs from "not an event". for eid, name in catalog.items(): if name not in ev: ev.setdefault(name, {"ids": set(), "producers": set(), "formats": set(), "notes": []})["ids"].add(eid) @@ -149,17 +123,17 @@ def main(): out = os.path.normpath(a.out) with open(out, "w", encoding="utf-8") as fh: fh.write( - "# The 1.12.1 client's FrameScript events and the ARGUMENTS their producers push.\n" - "# GENERATED — scripts/gen-reference-events.py. Source: wow-5875-re's\n" - "# re/events/event-catalog.tsv + event-firesites.tsv. Decision 2140.\n" + "# The 1.12.1 client's FrameScript events and the arguments their producers push.\n" + "# Generated by scripts/gen-reference-events.py from reference/1.12-event-catalog.tsv\n" + "# and reference/1.12-event-firesites.tsv.\n" "#\n" "# arg_formats `|`-separated, one per distinct producer shape. `()` = zero Lua values.\n" "# Directives: %s string, %d/%u number, %f number. An event with two\n" - "# producers of different shapes carries both, and which one a given\n" - "# transition takes is a question about the transition, not the name.\n" + "# producers of different shapes carries both; which one fires depends\n" + "# on the transition, not the name.\n" "# conf exact = gate on it. advisory = a contributing site declares more\n" - "# varargs than it pushes. none = no producer family reaches it HERE,\n" - "# which is not a claim that it has none; never gate on it.\n" + "# varargs than it pushes. none = no producer family reaches it here,\n" + "# which does not mean it has none; never gate on it.\n" "# producers signal (0x703e50, no varargs) | signal2 (0x703f50, format-driven) |\n" "# unit-field-bridge (0x51bbb0/0x51bd50 -> 0x515e50, always %s) |\n" "# token-fanout (0x515e50 with a literal id, also %s).\n" diff --git a/scripts/gen-reference-globals.py b/scripts/gen-reference-globals.py index fa23bda9..b390d617 100755 --- a/scripts/gen-reference-globals.py +++ b/scripts/gen-reference-globals.py @@ -1,49 +1,31 @@ #!/usr/bin/env python3 -"""Regenerate `reference/1.12-globals.tsv` — the 1.12.1 client's global namespace, with each name -attributed to whoever provides it. +"""Regenerate `reference/1.12-globals.tsv`: the 1.12.1 client's global namespace, by origin. - WOW_RE= scripts/gen-reference-globals.py [--wow-re DIR] [--out FILE] + WOW_RE=DIR scripts/gen-reference-globals.py [--wow-re DIR] [--out FILE] -**Why this exists** (decision 1189): every wrong number in the addon arc came from remembering a -list of WoW functions instead of asking the client. wow-5875-re already ships an addon -(`W5875Capture`) that dumps the in-world `_G` of the *running* reference client, and its output is -committed there as a fixture. This vendors that capture, attributes every name, and writes the -result as the one artifact benilla measures its API surface against. +Each name in a capture of the running reference client's in-world `_G` gets one origin: -**Three origins**, because "is this in 1.12?" and "whose job is it to provide?" are different -questions: + lua Lua 5.0's own runtime, which mlua provides. + engine the C client provides it; benilla implements these in Rust. + framexml the shipped UI defines it (its Lua, its XML, or a `$parent`-composed child of one); + it runs off the player's own patch chain, never hardcoded in Rust. - lua Lua 5.0's own runtime. We get these from mlua; neither we nor FrameXML write them. - engine The C client provides it. **This is the list benilla implements in Rust.** - framexml Defined by the shipped UI itself — its Lua, its XML, or a `$parent`-composed child of - one. Runs off the player's own patch chain at runtime (1751), never hardcoded in Rust. +Attribution is by definition site in the complete 1.12 shipped-UI corpus: -Attribution is by *definition site*, computed from a complete 1.12 shipped-UI corpus: + - a name assigned or `function`-declared in shipped Lua, including Lua inside a FrameXML + document, is FrameXML's; indented assignments count, because FrameXML leaks globals out of + function bodies (`button = getglobal(...)`); + - a `name="..."` on any shipped XML element is FrameXML's, `virtual="true"` included: a virtual + `` is a real font object, and 1.12 registers named virtual frames too; + - `` is a composed child (`ContainerFrame1Item16IconTexture`); + - LUA_5_0 wins over all of these: FrameXML's `string = getglobal(...)` would otherwise claim + the stdlib table. - - a name assigned or `function`-declared in shipped Lua (including a `` -# body (Fonts.xml defines CHAT_FONT_HEIGHTS that way), and every handler element under -# `` — ``, ``, `` and kin, which is where MainMenuBar.xml -# assigns SHOW_KEYRING and PERFORMANCEBAR_LOW_LATENCY. Matched by shape rather than by a list of -# handler names so a rarely-used one cannot be forgotten. `