From ecbdf2f716e2dbc1c57aecf182e0f58d885b2926 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 8 Sep 2026 16:11:11 +0700 Subject: [PATCH] the leader crown updates, the trail darkens, and four dead options work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the party-leader crown on your target's portrait only ever appeared or vanished when you re-targeted them: `PLAYER_FLAGS_CHANGED` was one of eighteen events the migrated stock windows register that nothing here produced, and the census row naming it said "TargetFrame.lua — the AFK/DND badge", which sent the first look at the wrong window — 1.12 draws no AFK/DND badge on any unit frame and build 5875 has no `UnitIsAFK`/`UnitIsDND` at all (ours are a deliberate invention, 2043; the ``/`` the era shows is the chat line's `arg6`), and the event's sole FrameXML consumer is `TargetFrame.lua:88-95` re-running `UnitIsPartyLeader("target")`, whose descriptor leg is bit `0x1` of this very dword; the handler `0x5ee990` XOR-diffs old against new at `0x5ee9b8` and fires 407 at `0x5eea3d` with no bit test in between and ABOVE the local-player GUID gate at `0x5eea93`, once per unit token naming that GUID through `0x515e50`'s fan-out (`arg1` = the token, no `arg2`, and a zero token count skips the fan-out so a stranger nobody targets signals nothing — free here, since `fire_transitions` only ever runs per token); so the trigger is the RAW dword, `UnitState::player_flags`, the `flags`/`UNIT_FLAGS` pattern one field over, because firing off the two bits the struct decodes would under-announce AFK, DND, GM, PvP-desired, helm, cloak and both play-time bits, and `prev.is_some()` keeps a unit's first snapshot silent as its CREATE runs no notify pass; reading the whole handler found an over-fire that was inherited, not invented: benilla fired `PLAYER_UPDATE_RESTING` on ANY flags delta because wow-re's rested-XP note said in as many words that `0x5eeaf2` fires "on every flags change, not only the resting bit", and the bytes say otherwise — `0x5eead0` `f6 45 fc 20 / 74 26` is `test byte [ebp-4],0x20 ; je 0x5eeafc` and the fire sits inside that arm — so every helm toggle, leadership pass and AFK flip had been announcing a resting change to the whole UI; narrowed to the resting bit's own edge, and the same handler's `0x5eeb65 test ah,0x30` arm becomes `PLAYTIME_CHANGED`, whose consumer `PlayerFrame_UpdatePlaytime` already had all three verbs it reads; the claim is corrected in wow-re by a four-derivation round that confirmed all three arms plus the CREATE question; three tests pin it — the producer with a control on `0x400`, a bit no field decodes; the over-fire regression on the real feed, where a helm bit moving fires neither self event; and the crown itself on the stock `TargetFrame.lua`, leadership passing to a held target lighting it with no re-target and the same event for `arg1 = "player"` not — a fixture that first failed on its opening assertion because `UnitIsPartyLeader`'s second leg is a raw GUID compare with no zero guard, so a default `guid: 0` equalled the zeroed leader of an empty group (verified 1.12: it is why `UnitIsPartyLeader(nil)` answers 1 while solo); the unproduced-events census goes 18 → 16 per window, 31 → 29 whole (2078) - the weapon swing trail darkens with the scene instead of burning at its authored colour: 2076 shipped it unlit and said why — the draw callback turns lighting ON (EGxRs `0x0e = 1` at `0x6c6847`) over vertex format 7, position + packed colour with no normal, and the state bit alone does not say what fixed-function produces from that; the bytes now do: `0x5a1e30` sets DIFFUSE- and AMBIENTMATERIALSOURCE both to `D3DMCS_COLOR1` off the format's diffuse bit, so the vertex colour IS the material; the normal is explicitly disabled rather than stale (`0x592a60` replaces the format mask instead of OR-ing it, `0x59c100` issues `glDisableClientState(GL_NORMAL_ARRAY)` when the bit is clear, and D3D's format-7 descriptor carries −1 in the normal column), so there is no N·L and the diffuse half contributes nothing; and the global `D3DRS_AMBIENT` (EGxRs id 6) has zero writers image-wide and defaults to 0, an earned zero — leaving `out = (Σ enabled lights' ambient) × authoredColour + inherited emissive`, which is why this does not take the lit emitters' `clamp(ambient + diffuse·max(N·L, 0))` term, a sun the reference does not have; `weapon_trail.rs` multiplies the trail's RGB by `WowLighting.ambient`, folded on the CPU into the vertex colours because a per-draw constant rides the vertex stream on this lane, drawing as `EffectLighting::None` and at the authored colour until the first lighting resolve; two named approximations of the class the fishing line already carries — the scene ambient rather than a per-model light collector, so an interior trail takes the exterior ambient, and no emissive, since `EMISSIVEMATERIALSOURCE` is set nowhere and reproducing it would mean inventing a constant; the other half of 2076's open list was a deviation benilla does not have: it claimed the combat-over-combat fast path (`0x5fe43c` returning at `0x5fe48b`, before the trail latch's only read at `0x5fe48e`) was unmodelled and cost trails starting one play early — both wrong, because `0x60d835` arms a one-slot field with a plain `mov` so a second proc OVERWRITES the first and firing on every request would draw superseded arms with superseded colours on 23 of the 34 type-8 kits, and because the driver's request loop already fast-paths it (0406, `select::is_combat_anim` = `0x5fcc10`'s byte-decoded set) and `continue`s without playing, so the edge never rises; a negative asserted about our own code from having written it rather than looked, shipped to wow-re where it drove a build-it verdict for something already built — now pinned by a test that names both mechanisms; and `benilla-extract charprocs` prints `CharParamOne` beside every trail row: wow-re's note justified "not read by this arm" with "every shipped type-8 row carries 20.0 there", and over the 34 kits the column takes six values (22 at 20, six at 15, two at 25, two at 0, one at 17, one at 22 — Sinister Strike's kit 399 is a 15); nothing here reads it either way, but a census that hides the one column a note asserts about cannot catch the note being wrong, and wow-re's re-measure agreed value for value and found the column live at four sites in `0x60d7c0` for other proc types (2079) - four options that ticked and did nothing — Show Tutorials, Tip of the Day, Profanity Filter, Disable Spam Filter — get their backing, and two of the reasons they were dead were wrong: none of the four was a row in our window at all; they sat in the transcribed `UIOptionsFrameCheckButtons` declaration table beside thirteen more whose features benilla lacks, and these four are exactly the ones where the feature existed and only the setting was missing; `SHOW_TUTORIALS` is a bare `{ index = 28 }`, which an earlier census read as "an index-only entry with no store at all" — wrong, the store is the three special-case arms `UIOptionsFrame_Load` (l.239), `_Save` (l.318) and `_SetDefaults` (l.645) keep for the key, `TutorialsEnabled()` reading the acknowledged bank and `ResetTutorials()`/`ClearTutorials()` writing it, verbs benilla has had since 1976, so the row was one `func` pair away, and the reference's `~=` guard comes with it because without it a Defaults click on a page where tutorials are already on would `ResetTutorials()` and re-arm every popup the player has dismissed; that census moves out of prose into a test checked both directions, the posture the CVar defaults already hold — a name the reference's table carries that nothing registers and no listed blocker fails, and a listed name that has since been registered fails too, twelve rows listed today with the feature each waits on; the two chat filters are PCRE over shipped DBCs, not word lists: `ChatProfanity.dbc` (2289 rows off `patch.MPQ` — `dbc.MPQ`'s copy is 1512) feeds the masker `0x4a1a60` and `SpamMessages.dbc` (28 gold-seller URL patterns; `dbc.MPQ` has none) feeds the predicate `0x4a1ca0`; Blizzard PATCHED PCRE's escape table (`.rdata 0x812330`) so `\<`/`\>` alias onto `\b`, and an unpatched engine makes them literals and silently kills the whole ASCII half of the profanity list while looking correct; CASELESS folds ASCII only, so pattern and subject are both ASCII-lowercased and matched case-sensitively rather than through `(?i)`, which would match `É` where the reference matches nothing; the masker is in place and length-preserving, one token per byte cycling `!@#$%^&*` from a process-global index never reset for the session, patterns in list order not text order — wow-re's oracle transcript reproduced against the real list, "you are a twat and a faggot" → "you are a &*!@ and a !@#$%^", the later word taking the earlier characters because its row (507) precedes the other's (530); two corrections to what this tree believed: `spamFilter` is armed for every chat type EXCEPT `CHAT_MSG_FILTERED` (`0x49aad2`'s `jne` skips the disarm, which the older note read backwards, so our comments claimed the filter bit only on the one type it is exempt from), and `profanityFilter` gates INSIDE the masker at `0x4a1a66`, covering all thirteen call sites rather than the guild MOTD alone; `CHAT_MSG_FILTERED` (`0x5B`) comes with them — it was on the "never `SMSG_MESSAGECHAT` wire" list and is the server's "your message was filtered" notice `ChatFrame.lua` renders at l.1405, dropped with a warning before and delivered now; the chokepoint runs spam, then mask, then fire, in the reference's order, since a dropped line must not consume mask indices; named rather than faked: the other twelve masker sites (mail, guild, item text, the send path) and their `dl = 1` memo, and the whisper leg's `CMSG_CHAT_FILTERED` report whose body is a wow-re DEFERRED with no vmangos handler; each row sits in 1.12's own seat — Profanity Filter closes the Basic Options Display column, Show Tutorials and Show Loading Screen Tips open the Help box, Disable Spam Filter is the last link of the Advanced chat chain and value-inverted, checked ⇔ `spamFilter == "0"` (2077) - the loading screen shows the tip of the day: `GameTips.dbc` is 74 records in the 5875 enGB chain, off `patch.MPQ`, ids unsorted (the file opens on 396 and closes on 412), so the index the client persists is a record index in load order, never an id; `gameTip` holds the NEXT index, not the one on screen — the client reads it, shows that row and stores `read + 1`, so on-disk values run 1..=74 and never 0, and the reference's own `Config.wtf` reading `SET gameTip "34"` while showing row 33 is exactly what an off-by-one anchors on; the wrap is a clamp, not a modulo (`if (i < 0 || i >= count) i = 0` at `0x46b682`), the walk is sequential, and both guards — `showGameTips` off and the roster's level-zero flag — jump past the `CVar::Set` as well as the draw, so turning tips off freezes the cursor rather than burning through the table invisibly; it draws only on the glue→world transition, because the setter `0x406630` has exactly one caller inside `EnterWorld` and neither `SMSG_TRANSFER_PENDING` arm reaches it, so the pick hangs off the loading screen's world-entry raise and a portal or worldport screen carries no tip; placement is the reference's: `s = 515/(a·1024)` with `a` = aspect × 0.75 (not W/H, which inflates the wrap width by a third at 16:9, and not the FrameXML-length idiom, which is off by 1.25 at 4:3), the block at `(0.5 − s·0.5, 0.1)`, left-justified, body `0xd7c8c8c8` with an opaque black shadow at `(+0.001, −0.001)`, laid out once per raise between the backdrop and the progress bar; the one term deliberately not applied is `yoff = (1 − a)·0.5`, which wow-re left unreconciled (`0x406a60` already applies the letterbox as a viewport and `yoff` is the same term added again inside it, a possible double count on a display narrower than 4:3) — benilla's loading screen is a 4:3 content box by construction and at 4:3 the reference's own `yoff` is zero, so the tip sits at the reference's layout on the one aspect where the ambiguity cannot bite; the `|cffffd100Tip:|r ` prefix and the trailing CR-LF are in the data and the escapes are interpreted, so the gold "Tip:" is markup and the line is built through the markup tokenizer; the cursor persists through the VM's own CVar table (`write_host_cvar`), since the config file is composed from that table and a host write stopping at the knob would reset it every launch — a live run reads `74 game tips` → `tip 0 of 74` → `tip 1 of 74` with `gameTip = "2"` left in the config, and an earlier live run is what caught a `GameTipSetting` the test harness initialised and the real app did not, a panic no unit test could see (2077) Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 1 + crates/benilla-app/Cargo.toml | 5 + crates/benilla-app/assets/ui/OptionsFrame.xml | 142 +++- crates/benilla-app/src/char_select/mod.rs | 9 + .../src/creature_anim/driver/tests.rs | 64 ++ crates/benilla-app/src/cvars.rs | 70 +- crates/benilla-app/src/game_tip.rs | 487 ++++++++++++++ crates/benilla-app/src/lib.rs | 4 + crates/benilla-app/src/loading_screen.rs | 58 ++ crates/benilla-app/src/text_filter.rs | 611 ++++++++++++++++++ crates/benilla-app/src/ui_chat/event.rs | 11 + crates/benilla-app/src/ui_chat/feed.rs | 37 +- crates/benilla-app/src/ui_chat/language.rs | 8 + crates/benilla-app/src/ui_chat/tests.rs | 4 +- .../src/ui_script/options_tests.rs | 293 ++++++++- .../benilla-app/src/ui_script/reference_ui.rs | 5 - .../src/ui_script/unit_frame_tests.rs | 81 +++ crates/benilla-app/src/ui_unit.rs | 321 ++++++++- crates/benilla-app/src/weapon_trail.rs | 84 ++- .../src/bin/benilla-extract/charprocs.rs | 12 +- crates/benilla-formats/src/game_tips.rs | 109 ++++ crates/benilla-formats/src/lib.rs | 4 + .../benilla-formats/src/text_filter_lists.rs | 120 ++++ crates/benilla-protocol/src/messages/chat.rs | 16 +- crates/benilla-protocol/src/messages/mod.rs | 12 +- crates/benilla-ui/src/script/unit/mod.rs | 15 + 26 files changed, 2514 insertions(+), 69 deletions(-) create mode 100644 crates/benilla-app/src/game_tip.rs create mode 100644 crates/benilla-app/src/text_filter.rs create mode 100644 crates/benilla-formats/src/game_tips.rs create mode 100644 crates/benilla-formats/src/text_filter_lists.rs diff --git a/Cargo.lock b/Cargo.lock index 1b03558c..fd91f862 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,6 +536,7 @@ dependencies = [ "objc2-foundation 0.2.2", "rand 0.8.6", "raw-window-handle", + "regex", "rtrb", "serde", "smithay-clipboard", diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml index dba96a5c..b8f530f9 100644 --- a/crates/benilla-app/Cargo.toml +++ b/crates/benilla-app/Cargo.toml @@ -58,6 +58,11 @@ half = "2" # macro plus a no-dependency runtime shim — it adds nothing to the graph at runtime, and it is the # crate everyone uses for this. `assets/ui` is OUR content (MIT/Apache), not Blizzard's. include_dir = "0.7" +# The 1.12 chat filters are PCRE over shipped DBCs (decision 2077; `text_filter`): 2289 profanity +# expressions and 28 spam ones, compiled once at startup and run over every chat line. `RegexSet` is +# exactly the shape that needs — one automaton over the whole list for the "can anything match at +# all" pass — and the crate is already in the tree transitively. +regex = "1" # `getrusage` — whole-process CPU time (all threads), the perf probes' `cpu_ms`/`cpu_pct` metric # (`perf::process_cpu_secs`). Wall-clock fps is unusable as a regression instrument on a machine # running parallel sessions' builds; CPU-per-frame is, and it is also the number the Mac report diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index e1544e23..f5dd916f 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -295,6 +295,31 @@ end end + -- ── Show Tutorials' SETTER: the reference's own Save arm (decision 2077) ──────────── + -- UIOptionsFrame.lua l.318-327, verbatim in behaviour. Its CheckButtons row is a bare + -- `{ index = 28 }` with no cvar and no uvar, which is why decision 1140's census filed it + -- as an "index-only entry with no store at all" and left it out; that reading is wrong. + -- The store is not in the TABLE, it is in the three special-case arms `UIOptionsFrame_Load` + -- (l.239), `_Save` (l.318) and `_SetDefaults` (l.645) keep for this key — the getter is + -- `TutorialsEnabled()` and the setter is the Reset/Clear pair, because "disable tutorials" + -- in 1.12 literally means "mark every bit of the acknowledged bank", not a CVar (1976; + -- wow-re censused all 210 `CVar::Register` sites and there is no tutorial CVar). + -- + -- The `~=` guard is the reference's and it matters: `TutorialsEnabled()` returns the NUMBER + -- 1 or nil, so re-ticking an already-on row does not re-`ResetTutorials()` — which would + -- re-arm every tutorial the player has already dismissed. + function OptionsFrame_SetTutorialsEnabled(value) + if ( tonumber(value) ~= TutorialsEnabled() ) then + if ( value == "1" ) then + ResetTutorials() + TutorialFrameCheckButton:SetChecked(1) + else + ClearTutorials() + end + TutorialFrame_HideAllAlerts() + end + end + UIOptionsFrameCheckButtons = { }; ------------------- Basic Options ------------------- -- Controls @@ -927,7 +952,12 @@ -- reference's own on-screen order. Its Basic Options Display box is three COLUMNS read -- left to right: A gives Status Text (CheckButton2), B gives Show Cloak → Show Helm -- (31 → 20, sitting under the name toggles that live on our Nameplates page), C gives - -- the 42 → 39 → 66 chain. Help (29) closes the page, as before. + -- the 42 → 39 → 66 chain. The **Help** box closes the page, and since 2077 it is + -- the box's own order rather than its one shippable row: 28 (Show Tutorials), 44 + -- (Show Loading Screen Tips, anchored under 28 there) then 29 (Show Detailed + -- Tooltips). 28 was left out by 1140's census as an "index-only entry + -- with no store at all"; that reading missed the three special-case arms + -- `UIOptionsFrame_Load`/`_Save`/`_SetDefaults` keep for the key, which are its store. -- The target-of-target pair (decision 1576) breaks that left-to-right reading, and -- it has to: 1.12 keeps it in a box this window has no page for — Advanced Options' -- "Raid & Party" — so there is no column order to preserve, only a neighbour to pick. @@ -936,7 +966,8 @@ "RowStatusText", "RowTargetOfTarget", "RowTargetOfTargetMode", "RowShowCloak", "RowShowHelm", "RowInstantQuestText", "RowBuffDurations", "RowAutoQuestWatch", - "RowNewbieTips", + "RowProfanityFilter", + "RowShowTutorials", "RowTipOfTheDay", "RowNewbieTips", }, -- 1.12's Advanced Options **Chat** box (decision 1589, B246 — "no chat section in -- options"). The page was called `Social` and held exactly the two chat-bubble @@ -954,17 +985,17 @@ -- · 51 Auto-join Guild Recruitment Channel — carries **no cvar, uvar or func in the -- reference either** (UIOptionsFrame.lua l.51 is a bare `{ index = 51 }`), so -- it is inert there; a working row here would be an invention, not a fix - -- · 68 Disable Spam Filter — benilla filters no chat text. Unlike 51 this is a REAL - -- omission and not an inert row: `spamFilter` is registered "1" at `0x402e8e` - -- with a change callback, and its one reader is the chat chokepoint `0x49a870`, - -- which drops a `CHAT_MSG_FILTERED` line on a word-list hit unless the type is - -- whisper (§5, wow-re `system/ui/scratch/spam-filter-cvar.md`). The row lands - -- with the filter. + -- Row 68, Disable Spam Filter, **shipped by 2077** — and the note it replaces was + -- wrong twice over, both corrected at the bytes by wow-re's `text-filter-law.md`. + -- The filter is not a word list, it is 28 compiled PCRE patterns from + -- `SpamMessages.dbc`; and it does not drop `CHAT_MSG_FILTERED` lines, it is armed for + -- every type EXCEPT that one (`0x49aad2`'s `jne` skips the disarm, which the older + -- note read backwards). It drops a matching line silently, with nothing in its place. -- 1139's note about the 37 → 46 → 38 gap is closed by this record: 46 is Loot Spam, -- and it has a knob now. Chat = { "RowGuildMemberAlert", "RowRemoveChatDelay", "RowChatBubbles", "RowLootSpam", - "RowPartyChatBubbles", + "RowPartyChatBubbles", "RowSpamFilter", }, -- 1.12's Advanced Options "Action Bars" group, whole (decision 1500 completing 1136). -- Its six rows, the four bar toggles then the grid switch then the lock. That is the @@ -3367,11 +3398,74 @@ "Automatic Quest Tracking", "OPTION_TOOLTIP_AUTO_QUEST_WATCH") - + + + OptionsRow_OnLoad(self, "profanityFilter", "Profanity Filter", + "OPTION_TOOLTIP_PROFANITY_FILTER") + + + + + + + + + + OptionsFuncRow_OnLoad(self, TutorialsEnabled, + OptionsFrame_SetTutorialsEnabled, "1", + "Show Tutorials", "OPTION_TOOLTIP_SHOW_TUTORIALS") + + + + + + + + + OptionsRow_OnLoad(self, "showGameTips", + "Show Loading Screen Tips", + "OPTION_TOOLTIP_SHOW_TIPOFTHEDAY") + + + + + + OptionsGlobalRow_OnLoad(self, "SHOW_NEWBIE_TIPS", "Show Detailed Tooltips", "OPTION_TOOLTIP_SHOW_NEWBIE_TIPS") @@ -3481,6 +3575,34 @@ "Show Party Chat Bubbles", "OPTION_TOOLTIP_PARTY_CHAT_BUBBLES") + + + + + + + + OptionsRow_OnLoad(self, "spamFilter", "Disable Spam Filter", + "OPTION_TOOLTIP_DISABLE_SPAM_FILTER") + self.invert = 1 + +