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. `