Commit Graph

831 Commits

Author SHA1 Message Date
andi79h-claude abcdced192 A frame in a saved variable writes as its name, as 1.12.1 writes it (#1996)
- A frame in a saved variable writes as its name, as 1.12.1 writes it
- A saved frame's name is read off the model, not a Lua GetName

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-20 02:21:45 +07:00
andi79h-claude b14793c294 The combat log fills deDE's numbered holes, so a German client prints its fights (#1994)
- The combat log fills deDE's numbered holes, so a German client prints its fights
- The fill's comment cites the client's ordered printf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-20 02:09:31 +07:00
Sam Sung 851a349d79 CI's Windows tests run every crate and name a test that crashes (#1995)
The first Windows run (#1984) died inside `benilla-app`'s lib tests with an access violation, which names no test, and `cargo test` stopped there, so no crate after it ran on Windows.

The Windows job now runs `--no-fail-fast`, so every crate reports, and on a failure reruns single-threaded, where each test's name prints before it runs, so the one a crash takes down is the last name in the log.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:58:49 +07:00
Sam Sung c0e301eaec A Lua cast arms inside its call, through a host lent to the VM (#1992)
A Lua cast was queued for a later frame, so on the next line of the same script `SpellIsTargeting()` read the old state and `SpellTargetUnit` found nothing to bind, as FuBar_CorkFu and SmartRes do.

In 1.12.1 `CastSpellByName` reaches `ArmCastTargets 0x6e5250` through direct calls (`0x4b4ab0` → `0x4b3300` → `0x6e5a90` → `0x6e4b60`), and `SpellIsTargeting` (`0x6e6cd0`) reads the word the arm stores.

The input pass and key bindings now lend the engine's call host to the VM around each script. The 20 verbs that queued apply inside the call, each under its own input events, and `CURRENT_SPELL_CAST_CHANGED` fires there. Calls an unhosted entry leaves apply at the next lend's start. The one unsafe lend is in `script/host.rs`.

Tests: CorkFu's and SmartRes's lines arm and bind in the call; a refused cast does not arm; every routed verb runs with a host lent.

Fixes #447

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:52:31 +07:00
Sam Sung 17d8b0e5dd CI runs the workspace tests on Windows (#1981)
v0.3's definition asks for the full test suite green on Windows (#798), and nothing runs it there: the gates run on macOS, the Windows check before a release only compiles the app, and CI runs on Linux.

A `windows` job now runs `cargo test --workspace` on `windows-latest`, with no install, for a fork's pull request and on a dispatch, beside the Linux gates. Its checkout uses the runner's own line-ending setting, as a Windows contributor's does, which is where #798's failures came from.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:11:54 +07:00
Sam Sung b305e6afc5 Main is 0.2.0 again until 0.3.0 ships (#1980)
The 0.3.0 tag was withdrawn within the hour: the milestone missed open Lua API defects its own definition covers, so 0.3 is not done.

With the tag gone and the version left at 0.3.0, every build from main would stamp a bare `0.3.0`, the same line the real release will print. Back at 0.2.0, the stamp reads `0.2.0+N` again, N commits past v0.2.0, until the 0.3.0 bump lands with its tag. The toolchain pin stays.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:06:03 +07:00
Sam Sung 9a5f2a56fd Release 0.3.0 (#1979)
Sets the workspace version to 0.3.0 and refreshes the lock, so the client's startup line reads `benilla build 0.3.0`. The toolchain pin stays at 1.98.1, the compiler the release checks ran on.

Checked on this tree: the Linux and Windows compile, the Vulkan run, and a live login and logout against a local server, whose log reads `benilla build 0.3.0`.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:39:02 +07:00
andi79h-claude 9969b8459b A moved bar item takes its green border along and leaves none behind (#1977)
The reference answers IsEquippedAction for the slot's action at call
time (0x4e74e0, the worn-slot walker 0x622270), so the in-place
ACTIONBAR_SLOT_CHANGED of a pickup reads nil for the emptied slot and a
place reads the moved item's own answer. The VM answers from the app's
per-slot states instead, which the cursor verbs left where they were:
the pickup's handler read the lifted item's equipped flag off the empty
slot and drew the green border there, and the app's re-feed fires no
event ActionButton_Update answers, so it stayed. A spell placed over a
worn item read the item's border the same way.

The pickup now drops the slot's state with the action; a lifted bar
item carries its state on the cursor (CursorAction.state, beside count
and consumable) and the place writes it with the slot; any other placed
action waits for the re-feed instead of reading the displaced occupant's.

Verified: cargo fmt --check, clippy -D warnings for benilla-ui and
benilla-app, cargo test -p benilla-ui --tests (2669 lib tests, the new
an_equipped_items_green_border_moves_with_it among them),
cargo test -p benilla-app --lib (5088). Built on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:56:50 +07:00
andi79h-claude cda0affaeb UnitDebuff's dispelType is SpellDispelType's column 11 on every locale, as 1.12.1 returns it (#1975)
UnitDebuff (0x5198f0) reads the class's record at [0xc0d83c], gates on
[+0x28] and returns [+0x2c], column 11: Magic, Curse, Disease, Poison
on every install (0x519a89). FrameXML keys DebuffTypeColor on those
words in every locale. benilla returned the Name block at the locale
slot instead, Magie on a German install, so DebuffTypeColor[debuffType]
was nil and TargetFrame.lua:303 raised "attempt to index local `color'"
on every frame a debuff with a dispel class sat on the target. The aura
tooltip (0x52f880) keeps the locale's word: SpellDispelTypes now holds
both, name (column 11) and label (the Name block), and the tooltip reads
dispel_label. locale_routing probes the label, the localized string.

Verified: on a German install real_dispel_types_name_only_what_the_gate_allows
failed before (Magie for Magic) and passes now, and the new
the_key_is_english_on_every_locale_and_the_label_is_the_locales passes
on German and English data; benilla-formats lib tests 487/487 on
English data; on German data the 55 failures left are the existing
English-name expectations, none new. cargo test -p benilla-app --lib,
fmt, clippy -D warnings green. Built on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:47:55 +07:00
Sam Sung 05e7cf4b93 A level-60 character shows the max-level bar, not the XP bar (#1976)
A level-60 character entered the world with the XP bar shown instead of the max-level bar.

The stock watch bar sets the XP bar from `UnitLevel("player")` on `UPDATE_FACTION` (`ReputationFrame.lua:161-244`). The server sends the faction list before the player's create, and benilla's only login signals came from that first push, before `"player"` was seated, so the bar read level 0 and stayed shown.

1.12.1 rebuilds the list in the world-enter cascade (`0x4909d3` calls `0x4d5430`), after `PLAYER_ENTERING_WORLD`, and that rebuild's sort signals `UPDATE_FACTION` (`0x4d55de` → `0x4d5db0`); the rebuild does nothing without a player (`0x4d5453`). benilla now runs that rebuild in the cascade, and the first push into a VM is silent. A test drives the server's order: level 60 hides the XP bar and shows the max-level art, level 59 the reverse.

Fixes #1973

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:09:38 +07:00
Sam Sung f52df3a111 A key moved off an action button takes its label along (#1974)
After a key binding moved between action buttons, the buttons it left kept showing its label, so several buttons read the same key.

1.12.1 does the same: when a button has no key, `ActionButton_UpdateHotkeys` (`ActionButton.lua:129-140`) clears the label only for an empty slot or a ranged action, and writes nothing for an action with no range (a stance, a self buff), so the old label stays.

This is a deviation, in the layer's `FrameXMLFixes.xml`: the stock function runs unchanged, then a button with no key and a no-range action has its label cleared. A test moves Shift-2 across three stance buttons and checks each one it leaves goes blank; without the fix the first still reads `s-2`.

Fixes #1842

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:43:05 +07:00
Sam Sung 881152bda6 Key-binding confirm dialogs no longer raise a Lua error each frame (#1972)
Unticking Character Specific Key Bindings, or pressing Defaults, raised `StaticPopup.lua:1713: attempt to compare number with nil` every frame the confirm stayed up.

1.12.1: `StaticPopup_Show` copies an entry's `timeout` into `dialog.timeleft`, and `StaticPopup_OnUpdate` compares it with 0 each frame; every stock entry declares one, the stock key-binding confirm included (`Blizzard_BindingUI.lua:10-24`, `timeout = 0`, `whileDead = 1`, `showAlert = 1`).

Our key-bindings page's two confirms declared none. Both now do, the character-set one with the stock entry's three fields. The checkbox test now runs a frame with the confirm up, and a new test fails on any dialog the loaded interface defines without a numeric timeout; both fail without the fix.

Fixes #1971

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:31:11 +07:00
Sam Sung 866e2cac31 A nameplate's health fill shrinks with the unit's health again (#1970)
Since #1959 a bar texture cuts its drawn rect only when its install set the texcoord-modifies-rect flag. The nameplate attached its fill by hand, never through that install, so the fill stayed full whatever the health.

1.12.1 builds the fill with the bar's own path verb: the plate setup calls `0x783140` with the fill's path on layer 2 (`0x7cb31a`-`0x7cb329`), which pins it to the bar and installs it through `0x7831f0`, the step that sets the flag (`0x783220`).

The plate now builds its fill through that same path leg, lifted into a model-level function, so it is installed, flagged and pinned like any bar texture. A test drives a plate at 40, 20 and 10 of 40 health and checks the drawn fill at full, half and a quarter width; it fails without the fix.

Fixes #1969

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:43:21 +07:00
Sam Sung 88eaf16799 GetContainerNumSlots and GetBagName answer on 1.12.1's bag ranges (#1968)
Both verbs answered whatever the container snapshot held, so the keyring, the bank and the backpack answered as ordinary bags.

1.12.1: `GetContainerNumSlots` (`0x4f9560`) answers 16 for the backpack, 24 for the bank, the bag's size for 1-10 and 0 for anything else, the keyring included (`0x4f95fb`). `GetBagName` (`0x4fa670`) answers `BACKPACK_TOOLTIP` for the backpack (`0x4fa6f0`), the bag item's name for 1-10, and nil for the bank, the keyring and the rest (`0x4fa712`).

Now both apply those ranges, the backpack is named by its global, and the snapshot no longer carries English names for the backpack, bank and keyring. A test pins every range. The keyring stays readable through `GetKeyRingSize` and the item verbs.

Found in Thorsten42/manila@a9fffcb5df.

Fixes #1967
Fixes #372

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Thorsten42 <3400178+Thorsten42@users.noreply.github.com>
2026-10-19 10:10:28 +07:00
Sam Sung 4dc5f1c191 Every by-name lookup runs 1.12.1's one resolver (#1966)
1.12.1 resolves every name through one function, `0x493aa0`: party and raid passes for members with no object held, then every held object its mode's filter passes, feeding one matcher (`0x493c60`). benilla had partial copies: the app's scan ran the object pass only, SetLootMethod the roster passes only, and Promote, assistant, duel and emote lookups matched exact names.

`benilla_ui::script::by_name::resolve` now runs the passes with each caller's typemask, mode and exact flag: TargetByName, /target, /assist, /follow, DoEmote, SetLootMethod, PromoteByName, PromoteToAssistant, DemoteAssistant and StartDuel. It answers nothing with no active player (`0x493ae0`). Mode 2's filter is `CanAssist` and alive (`0x493eca`).

The roster passes stay off for /target, /assist and /follow until the selection can hold a guid (#1965).

Tests per caller: prefix, a held stranger, a dead groupmate, a miss, no player. A byte audit re-derived the call sites and the matcher.

Fixes #1964

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 05:37:34 +07:00
Sam Sung 8e5114d8b1 Setters, bindings and loot methods gate their arguments as 1.12.1 does (#1963)
Method setters, bindings and the nameplate solver now gate their arguments as 1.12.1 does.

- Setters given a non-number raise their own usage line; SetTextHeight, SetFrameLevel, SetMaxLines and SetHistoryLines raise on the values 1.12.1 refuses (`0x87c750`, `0x879628`). XML `historyLines="0"` still loads, through the ring setter (`0x77cd90`).
- Bindings gated by `lua_isnumber` take 1.12.1's miss path: nil, a default, or no values.
- SetLootMethod follows `0x4e92a0`: group and leader gates, threshold argument 2 except for master, 2 on a miss, master names resolved by prefix (`0x493aa0`), errors for an unknown method or looter. SetLootThreshold takes the same gates.
- Quest watches fall back to the last selected quest; MessageFrame times store unclamped and expire to 0 (`0x786323`).
- The placement bound is `0x509dd0`'s 32-bit product, so a zero-width rect no longer panics.

Credit to kalistoking/benilla-twow@5b5374bc for #1956. A byte audit re-derived every site.

Fixes #1953
Fixes #1955
Fixes #1956
Fixes #408
Fixes #415
Fixes #416

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 04:22:14 +07:00
Sam Sung b2387b277b Resize, inset, backdrop, font and XML lengths follow the screen-unit law (#1962)
Beside the frame size and point methods (#1951), 1.12.1 puts every other script length through the screen-unit law: a setter stores `S(v)` and a getter answers `G(l)` through `K1` (`0x41ad70`, `0x41ae40`, `0x41ae60`). benilla stored and answered these as given.

- Resize bounds, hit-rect and text insets, backdrop lengths, pushed text offset, text extents, font heights (Button:GetFont through `0x79f3b0` too), shadows and spacing follow the law.
- A `<Rel…>` XML length is stored as parsed, in layout units (`0x6f2037`-`0x6f2388`).
- The tooltip sizer keeps its pad, line gap and wrap cap at 53 bits, as `0x41ae60` leaves them in `st0`.
- User-placed frames re-seat from the layout cache when the cells change, over what the scripts last built.

`SetMinResize(100, 100)` now reads back `G(S(100))`, not exactly 100, as in 1.12.1. Tests pin each kind against the law at 4:3 and 16:9; a byte audit re-derived each site.

Fixes #1942

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 03:49:55 +07:00
Sam Sung e550df4b0d Model frames load their own file again, not the extension-less key (#1961)
Since #1932 every Model frame drew the error cube: the VM queued a pane's file by its match key, `model_key`, which drops the extension, and the host loaded that key as the path, which the cache's extension rule (`0x706a50`) refuses (`0x70737f`).

The queue now carries the pane's own path beside the key, and the host opens that path through the rule: `.mdx`, `.mdl` and `.m2` load the `.m2`, and a refused name stands the cube. `model_key` is now the file the cache opens, so a refused name keys as the cube and never shares facts with its valid twin.

Tests: the three spellings of the minimap arrow load `interface/minimap/minimaparrow.m2`; `Creature\Wolf\Wolf` and `Foo.blp` load and key as the cube; the minimap's children queue their stock models.

Found in Thorsten42/manila@103bbe4.

Fixes #1958

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 03:22:22 +07:00
Sam Sung d9926c6be1 StatusBar bits, index usage lines, stack names and cursors follow 1.12.1 (#1959)
Ten differences from 1.12.1 in the UI core.

- StatusBar range and value follow `0x783270`/`0x7832d0` (a reversed pair collapses, a value before a range is dropped); the health watchers set both each update; the fill draws once both are set, its coordinates built from scratch (`0x7833c0`).
- A full collection runs before PLAYER_LOGIN (`0x490941`); the addon harness runs the client's world-entry cascade.
- Every index argument raises its own usage line (`0x6f34d0`, then the binding's literal).
- Strata, layer and frame-type names match whole (`0x6ee32c`); stack lines name unnamed functions by global (`0x6fbf90`).
- SetCursor takes a custom file (`0x523c40`); EditBox accepts OnInputLanguageChanged (`0x77a40d`).
- debugprofilestop answers milliseconds (a Deviation: multi-core 1.12.1 returns cycles).

A byte audit re-derived every address.

Fixes #1178
Fixes #1915
Fixes #1917
Fixes #1908
Fixes #1839
Fixes #1869
Fixes #1170
Fixes #1105
Fixes #1186
Fixes #1952

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:48:36 +07:00
Sam Sung 1c0b8dadc7 SimpleHTML font nodes, direct FontStrings and pane cameras follow 1.12.1 (#1957)
SimpleHTML's element fonts, a plain frame's direct FontString, and a model pane's camera differed from 1.12.1.

- SimpleHTML's four element fonts are font nodes (`0x789e7c`) its blocks link to, P's when an element holds no face (`0x78ae29`), so a later font change reaches pages already shown and MONOCHROME reaches the text; the 16 font methods are the Font object's own (`0x79ef10`-`0x79fd70`). The loader links `font=` and reads `file=` before `hyperlinkFormat` (`0x78a2b2`).
- A FontString directly under a plain frame builds nothing; the base loader has no arm for it (`0x769b27`).
- A model pane keeps the camera it resolved at SetCamera (`0x76cec0`); after SetModel the old camera holds as last painted, in the glue scene and in-world panes.

No stock screen changes. A byte audit re-derived every address.

Fixes #1890
Fixes #1923
Fixes #1924
Fixes #1948
Fixes #1930

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:13:57 +07:00
Sam Sung 817ce79993 XML anchors, numbers, lengths, shadows and frame elements follow 1.12.1 (#1951)
- Anchors take a point from every child; a bad point or relativeTo is logged and skipped before its Offset is read (`0x767992`).
- Numbers parse with SStrToFloat (`0x64aaa0`) at every site; length readers take the node, then its first child.
- A bare `<Shadow/>` is 0.001 layout units, drawn rounded to whole pixels: the zone and world-map text shadows draw 2 px at 1080p.
- EditBox HighlightColor; the text frames' `font=`, a `<FontString>` child loading into the frame's own font, and message-frame TextInsets.
- Insets, resize bounds, title region and fog colour apply every child, the instance's last; a Backdrop dispatches its children in order and logs unknown ones.

A byte audit re-derived every address.

Fixes #1889
Fixes #1926
Fixes #1273
Fixes #1920
Fixes #1903
Fixes #1906
Fixes #1925
Fixes #1936
Fixes #1937
Fixes #1943
Fixes #1944
Fixes #1945

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:13:10 +07:00
Sam Sung 7ab0596a24 GetAuctionItemInfo answers the miss shape for a row not yet cached (#1950)
An auction row whose item template had not landed answered a half-filled row: a nil quality beside the real count, prices and owner. In 1.12.1 `GetAuctionItemInfo` (`0x4cee40`) looks the row's item up in the item cache (`0x55ba30`); a miss jumps at `0x4cefa0` to the one exit `0x4cf1ec`, which answers the twelve-value miss shape with `count = 1` and `quality = -1`. Such a row now takes that exit. Tests cover an uncached row and a cached one.

Supersedes #1939.

Co-authored-by: kalistoking <148718830+kalistoking@users.noreply.github.com>

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 23:54:39 +07:00
Sam Sung 57384551de Widget methods, chat-frame state and line fades follow 1.12.1 (#1949)
Widget methods 1.12.1 has were missing or differed.

- EditBox IsAutoFocus, IsMultiLine, IsNumeric and IsPassword; ColorSelect GetColorHSV and SetColorHSV (`0x7bbc80`, `0x7bbd60`).
- AddMessage with no colour takes the frame font's colour.
- ScrollingMessageFrame: the write slot and the four line-position methods; the cached lines-shown, at-top and at-bottom state its readers use (`0x788440`); a relayout on resize (`0x788310`); OnMessageScrollChanged (`0x788a9a`); SetTimeVisible and SetFadeDuration usage, unclamped; each line fading on its own countdowns (`0x7884c1`).
- GetFramerate averages the last 30 frames (`0x672700`).
- SetCursor takes the eight named cursors in any case, with its usage line; custom cursor files are still to do.
- SimpleHTML links get buttons, so OnHyperlinkClick fires.

A byte audit re-derived every address.

Fixes #1518
Fixes #1520
Fixes #1506
Fixes #1519
Fixes #1883
Fixes #1682
Fixes #1538
Refs #1186

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:43:30 +07:00
Sam Sung fdbb4bb070 GetTrainerServiceInfo answers "" for a rankless service's subText (#1947)
`GetTrainerServiceInfo` answered nil for a rankless service's subText. In 1.12.1 the subText is the Spell record's NameSubtext (`0x4d8b50`, read at `0x4d8b84`), the empty string for a spell with no rank; only a service with no Spell record answers nil (`0x4d8b58`). The resolver now keeps an empty rank as `""` and the binding pushes None as nil. Tests cover both rows and the install-backed Apprentice Blacksmith trainer.

Supersedes #1935.

Co-authored-by: kalistoking <148718830+kalistoking@users.noreply.github.com>

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:28:55 +07:00
Sam Sung 837a6e1336 Frame sizes and positions follow 1.12.1's screen-unit law (#1946)
Frame sizes and positions skipped 1.12.1's screen-unit law. The client keeps layout units: a setter stores `S(v) = RN24(K1·RN24(RN24(v)/1024A))` and a getter answers `RN24(1024A·l)/K1`, from the cells `0x41ad10` sets from the aspect (benilla-re `systems/screencoord.md`, two readings).

- The layout resolves in layout units with the reference's four float roundings (`0x767414`, `0x76741f`, `0x767428`, `0x767a30`); the pixel seam converts.
- Size, edge, centre and point getters and setters, the screen-size and cursor globals (glue and game), scroll ranges and scroll events follow the law; OnSizeChanged passes the layout size (`0x76b660`).
- Scroll bars hide on `floor(range) == 0`, as the stock template does.
- The minimap and world-map arrows re-seat when the cells change.

At 4:3, `GetScreenHeight()` is 767.9999427795418. Nothing moves on screen beyond 2e-4 px. A byte audit re-derived the law and the resolver.

Fixes #1918
Fixes #1321

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:02:31 +07:00
Mathih13 e0031ab9ef A tooltip line wrapped past 20 rows sizes to its pieces, as in 1.12.1 (#1933)
A tooltip line wrapped past 20 rows sized narrower than in 1.12.1. The sizer splits a long line into at most 20 pieces, the last running to the end of the text (`0x53082f`, `0x53088e`), and sizes the plate to the widest. Each piece runs from one row's start to the next (`0x530884`-`0x5308a8`), so it carries the whitespace the wrap skipped (`0x5c24fe`, past `0x5c7710`), and a space measures as a glyph. Pieces are now measured that way, so most wrapped tooltips are about a space wider. Tests pin the 20-piece cap, the kept space and a piece crossing a line break.

Fixes #1880

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-18 21:11:17 +07:00
Sam Sung d463c0f03f Minimap state, zoom, ping, mask and model arrows follow 1.12.1 (#1941)
The minimap's methods, state and arrows differed from 1.12.1.

- Zoom, indoor flag, mask and ping are process-wide (#1876); SetZoom caps unsigned and writes only a change (`0x6da8e0`); every world entry reseeds the zoom from its CVars (`0x4ed9d0`).
- PingLocation pings in the call and signals MINIMAP_PING at once (`0x4eeca0`); raid and party pingers get their tokens.
- SetMaskTexture loads in the call, a miss stores green and raises; the tiles take the mask's colour (`0x4eae74`).
- The nine Model children are built (`0x4edbc0`); the rim and player arrows draw through them, above the dots, with SetArrowModel, SetPlayerModel, SetIconTexture and SetBlipTexture. The corpse is rim slot 2.

On screen: the player arrow is a 3D model; the corpse's in-range icon is about 4% larger. A byte audit re-derived every address.

Fixes #1522
Fixes #1645
Fixes #1652
Fixes #1653
Fixes #1871
Fixes #1872
Fixes #1876
Fixes #1877
Fixes #364

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:57:49 +07:00
Sam Sung 2bfbf19e93 XML font legs and font-object getters follow 1.12.1 (#1940)
The XML font legs and the font-object getters differed from 1.12.1.

- A font file that fails to load writes nothing and logs `Unable to load font file` (`0x878dc8`, `0x87a4c8`); a font already held skips the load (`0x771502`).
- A FontString's `inherits`/`font=` look in the one font table `CreateFont` fills (`0x783870`) before templates; a miss logs `Couldn't find inherited node`.
- Regions read no `alpha=`; the options window's icon sets it in its OnLoad.
- Justify tokens follow `0x6f1990`: a miss is skipped, the other axis's token zeroes the axis.
- A Font object's default shadow is transparent.
- `GetFont` lists every flag, comma-joined (`0x6f1ad0`); only `THICKOUTLINE` is thick.

No stock screen changes. A byte audit re-derived every address and string.

Fixes #1888
Fixes #1891
Fixes #1921
Fixes #1922
Fixes #1887
Fixes #1919
Fixes #1928
Fixes #1899
Fixes #1905
Fixes #1816
Fixes #1929

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:14:21 +07:00
Sam Sung 3acad6b1b9 UI usage lines, argument gates and number text follow 1.12.1 (#1938)
Argument checks and usage lines across the UI API now raise 1.12.1's text in its order.

- One name helper reads the object's own name or `<unnamed>` (`0x84c7f0`), never Lua `GetName`.
- CreateFrame checks type, parent, template, then frame type, with the reference's five messages (`0x7060d5`-`0x7062a0`).
- Justify, scroll, max-letters, orientation, value, insert-mode, hyperlink-format, CreateFont and IsFrameType usage lines; not-movable errors say `<unnamed>`.
- Numbers become text by Lua's `%.14g` at the tooltip and eight other bindings.
- AppendText and the Slider XML loader follow theirs.

Every string was read from the client; a byte audit re-derived each.

Fixes #1882
Fixes #1529
Fixes #1535
Fixes #1549
Fixes #1885
Fixes #1904
Fixes #1886
Fixes #1168
Fixes #1909
Fixes #1910
Fixes #1911
Fixes #1879
Fixes #1912
Fixes #1913
Fixes #1914
Fixes #1916
Fixes #1927

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:48:49 +07:00
Sam Sung 011571498e The macro icon list is 1.12.1's, built on first use (#1934)
The macro icon list and its verbs differed from 1.12.1.

- The list is built on first use per in-game interface (`GetNumMacroIcons`, and `CreateMacro`/`EditMacro` past their gates), never at VM creation; `GetMacroIconInfo` never builds and answers `""` off the list or for an empty entry (`0x4f1ac5`, `0x4f1a88`).
- Its sources are 1.12.1's three (`0x4f0090`): the listfiles of the patches, speech2 and interface.MPQ only, then the two loose `Interface\Icons\` scans (`0x4f0350`, `0x4f04f0`), sorted case-blind and deduplicated.
- `EditMacro`/`CreateMacro` read the icon as a number (a numeric string counts) and keep the old icon on a miss (`0x4f1945`, `0x4f12f0`).
- `GetMacroIconInfo` with a non-number raises its usage line.

The listing takes about 7 ms on the stock chain, down from 150. Tests pin each fact; a byte audit re-derived every address.

Fixes #1512
Fixes #1833
Fixes #1874
Fixes #1875

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:26:37 +07:00
Sam Sung cc16abfae9 Glue scenes stand any file; one model-name rule for every lane (#1932)
Three differences from 1.12.1 on the model and glue lanes.

- `SetCharSelectBackground`/`SetCharCustomizeBackground` stood only the stock seven scenes. Any pane file now stands as itself, opened loose-aware as the scene model is (`0x76ccf5` flags 5, `0x707365`), and a scene with no camera draws on the orthographic leg (`0x76ce80`, `0x7ad7f0`) instead of keeping the last aim.
- Every model lane takes one name rule (`0x706a50`): the last dot, `.mdl`/`.mdx` to `.m2`, `.m2` as is, anything else the ErrorCube (`0x70737f`); the name is cut at 0x103 bytes first.
- `LaunchURL` refuses a digit after `http://` (`0x73fb91`, isalpha).

A missing file that passes the rule still stands nothing; that is #330's fallback. Tests pin each fact; a byte audit re-derived every address and lane.

Fixes #1836
Fixes #1873
Fixes #1108
Refs #330

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:43:15 +07:00
Sam Sung a5cb73a895 CVar reset, cvarlist, saving and console text follow 1.12.1 (#1931)
The console and CVar store differed from 1.12.1 in seven places.

- `cvar_reset` restores the session-start value (`0x63dff0`), `cvar_default` the default; a latched row moves its value, never the device (RestartGx reads only the stage, `0x639ec0`).
- `cvarlist` prints `  "%s" is "%s"` with the default and reset suffixes, cut at 0x100 bytes.
- A staged latched value is saved (SaveConfig takes stage, value, default, `0x63da27`).
- The setting callbacks refuse what 1.12.1's refuse (MaxLights `0x688bd0`, horizonfarclip `0x689250`, ErrorFilter `0x403200` and nine more), printing their lines.
- Console replies go to the log in 1.12.1's words, never to chat.
- The saved account name is the `accountName` CVar (`0x46cdd0`); the old `benilla-config/account` file moves into config.toml once.

Tests pin each against the bytes; a byte audit re-derived every address.

Fixes #1267
Fixes #1268
Fixes #1269
Fixes #1340
Fixes #1643
Fixes #1650
Fixes #1852

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:20:24 +07:00
Mathih13 d80200ed69 The addon harness seats its target at world entry, so TargetFrame loads (#1900)
- The addon harness seats its target at world entry, so TargetFrame loads
- The harness's world-entry target cites the selection and load order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-18 16:10:05 +07:00
Sam Sung 7940c2bdfe Colours and alphas are bytes, and frame verbs gate as 1.12.1's do (#1907)
Colours and alphas were kept as floats where 1.12.1 packs them into bytes, and several frame verbs answered bad input unlike the client.

1.12.1's colour setters clamp, round half up into a byte, and read back times `0x81c6b4`; a frame's alpha is a truncated byte (`0x774f32`) whose walk stops at an unchanged byte (`0x76a690`); region and Font `SetAlpha` wrap. `SetScale`, the draw-layer verbs, the scroll range (`0x786e30`) and the binding verbs gate their arguments as the bytes do.

Now one packed `Rgba8` serves every colour, frame alpha is a byte, and each verb raises the reference's usage line. Read-backs shift by at most 1/255; nothing visible changes.

Fixes #1523
Fixes #1536
Fixes #1868
Fixes #1867
Fixes #1247
Fixes #1248
Fixes #1546
Fixes #1557
Fixes #1562
Fixes #1189
Fixes #1509

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:59:24 +07:00
Mathih13 ed65145540 The addon probe fires events, clicks and runs on the bare interface (#1898)
- The addon probe fires events, clicks and runs on the bare interface
- A probe click and hover answer what the hover raised
- The probe outcome's doc names the bare interface and every answer form

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-18 15:46:31 +07:00
andi79h-claude 2f9dd94a33 RAID_TARGET_UPDATE fires after the unit snapshots, so a new mark reaches the target frame at once (#1897)
feed_party and feed_units share FieldEventPushes with no order between
them. When the party feed ran first, TargetFrame's RAID_TARGET_UPDATE
handler read GetRaidTargetIndex("target") off the previous snapshot, so
a new mark showed over the mob but not on the target frame until the
target changed. A UnitSnapshots set holds feed_units, and the party feed
runs after it. the_party_feed_runs_after_the_unit_snapshots reads the
order off the declared graph; it fails on the tree before this change.

Verified: cargo fmt --check, clippy -D warnings, cargo test -p
benilla-app --lib (5093 passed). Live on a vmangos server, a build with
this change shows raid marks set in a raid on the target frame. Built
on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:36:11 +07:00
andi79h-claude ecb49a5f02 A unit the player cannot select shows no name, as 1.12.1 does (#1895)
- A unit the player cannot select shows no name
- Nameplates: the own name's first-person fade is a Deviation, not ShouldShowName's leg

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-18 15:20:02 +07:00
Sam Sung dd61ea731e Textures load, answer and blend as 1.12.1's do (#1902)
Texture paint answered and drew unlike 1.12.1: `SetTexture("")` and non-string names answered nil, a missing file cleared desaturation, `SetTexCoord()` and an unknown `SetBlendMode` passed quietly, a failing bar path still installed a bar, and DISABLE, ALPHAKEY and MOD drew as BLEND.

1.12.1's load (`0x770200`) writes the shader slot only for a file that loads (`0x7702c0`); the binding answers 1 for a null name (`0x79bdae`). The submit draws a coloured DISABLE or ALPHAKEY texture as BLEND (`0x76fb82`); ALPHAKEY alpha-tests at 224/255 and MOD multiplies the destination (DESTCOLOR, ZERO, `0x80a1fc`).

Now one texture load serves every setter, the usage lines raise, the shader draws each mode with a MOD pipeline of its own, and a new test validates the UI shader with naga. The stock UI draws as before.

Fixes #1425
Fixes #1864
Fixes #1863
Fixes #1521
Fixes #1556
Fixes #1550
Fixes #1559
Fixes #1555

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:07:34 +07:00
andi79h-claude ed630ffb78 IsRaidOfficer answers 1 for an assistant or the leader, as 1.12.1 does (#1894)
It was a nil stub, so an assistant got no raid target icons in the unit
popup (UnitPopup.lua offers RAID_TARGET_ICON to the leader or to
IsRaidOfficer) and the raid frame's add-member button stayed off for
them. 0x4bb910 walks the raid roster (0xb712a8, count [0xb713e0]) for the
row whose guid is the local player's and pushes 1 when that row's rank
([row+0xc], GetRaidRosterInfo's return 2) is above 0, so for an
assistant and for the leader; outside a raid, without a player or
without our row it answers nil.

Verified: cargo test -p benilla-ui --lib
(is_raid_officer_reads_our_own_roster_rank), clippy -D warnings.
Built on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:54:36 +07:00
Sam Sung 39311b2fc6 Fonts link live to their parent, as 1.12.1's do (#1901)
Fonts were copied at load: a later change to a font object never reached the fonts and strings built from it, and a Button's state fonts were names overlaid at draw, so a label measured in one font and drew in another.

In 1.12.1 a font follows one parent (`0x770c60`) under a five-bit inherit mask; each setter clears its own bit and pushes only what changed to its followers (`0x784180`, `0x770910`). A Button holds three embedded fonts and relinks its label on each real state change, after the script (`0x779810`).

Now fonts link live, case-blind by name (`0x64b3f0`), `Font:SetFontObject` links and refuses loops, a Button's fonts are real linked fonts with their getters, and `<NormalColor>`-style elements load (a `Deviation:` for two). Tests cover the push, the masks and each button state; the stock UI draws as before.

Fixes #1204
Fixes #1517
Fixes #1403
Fixes #1855
Fixes #1856

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:33:56 +07:00
Sam Sung ba96e0ccd4 The CVar set path is 1.12.1's InternalSet (#1896)
The CVar set path differed from 1.12.1's: values compared case-sensitively, text on a numeric CVar was refused, a read-only CVar still took console and host writes, `ConsoleExec` ran commands a frame late, and console `set` kept the whole line.

1.12.1's InternalSet (`0x63e0b0`) returns for a read-only record (`0x63e0ba`), compares with `SStrCmpI` (`0x63e0ec`) and stores any text, with the record's int and float by `SStrToInt`/`SStrToFloat` (`0x63e127`). `ConsoleExec` runs the line inside the call (`0x515f43`), and `set` tokenizes with `SStrTokenize` (`0x64ae50`).

Now there is one set path: the registered default strings, one `SStrToInt`, read-only refusing every writer, CVar commands run inside `ConsoleExec`, and the host copies are gone. Saved values match 1.12.1: a slider at its default writes "1" over "1.0", and hand-edited text reads 0.

Fixes #1478
Fixes #1853
Fixes #1278
Fixes #1319
Fixes #1274
Fixes #1640
Fixes #1851
Fixes #1475

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:24:35 +07:00
Sam Sung 39ad58c703 Buttons click, check and press their label as 1.12.1's do (#1893)
Four Button behaviours differed from 1.12.1: `Click()` on a disabled check box did not toggle it, a checked texture made from a path drew with BLEND, `Click(name)` passed its argument raw, and a pressed button's label never shifted.

1.12.1's CheckButton click (`0x785550`) toggles before the base click's enabled gate (`0x779552`); the create leg of the checked setters stores ADD (`0x7852f4`); `Click` maps its argument case-blind to a button name or `UNKNOWN` (`0x7826c0`); and the label relink (`0x779810`) offsets the text by `PushedTextOffset` while PUSHED.

Now each follows the reference: a disabled box also ignores a press, `Set/GetPushedTextOffset` and `<PushedTextOffset>` are built, and held stock buttons shift their label about 1.3 units down-right. `Set*FontObject` raises on an unknown font name (`0x87c5fc`). Each has a test.

Fixes #1175
Fixes #1176
Fixes #1179
Fixes #1516
Fixes #1854

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:04:05 +07:00
Sam Sung 31ff8ec00c The XML loader reads each template level, as 1.12.1 does (#1892)
The XML loader merged a template chain into one element, losing each attribute's level: `setAllPoints` lost to a template's `<Anchors>`, an empty `text=` wiped a template's text, the template's `<TexCoords>` won.

1.12.1 never merges: each loader re-enters on `inherits` (`0x76fe36`, `0x770f9e`, `0x76985c`), then reads its own node's attributes and first children (`0x6f2c50`).

Now regions load per level in `0x770f40`'s order; frames read geometry, `frameLevel`, StatusBar attributes and button text per level. A failed texture fills green (`0x878d24`). Level tests cover each rule; the stock load is unchanged but for the AddOns window's crops.

Fixes #1534
Fixes #1548
Fixes #1547
Fixes #1866
Fixes #1423
Fixes #1858
Fixes #1424
Fixes #1472
Fixes #1861
Fixes #1859
Fixes #1171
Fixes #1860
Fixes #1817

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:35:06 +07:00
Sam Sung 20abc74643 GameTooltip sizes in the show, as 1.12.1 does (#1884)
GameTooltip re-sized itself on every resolve from its lines, at 10/40/260 units, counting a taller right text. So `GetWidth` after `Show` read stale, a script's `SetHeight` after `AddLine` was overwritten, and descriptions wrapped wide.

1.12.1 sizes only in the show (`0x530aad`), `AppendText` (`0x530615`) and the cache answers, never in `AddLine` (`0x531630`). `GameTooltipSize` (`0x530640`) takes the widest unwrapped line plus a 38.4 gap, wraps the rest at the running widest (capped at 230.4), sums shown left heights with 2.0 gaps, pads 10.24 a side, then re-anchors without clearing.

Now the sizer is that function, called from those sites, and the engine no longer anchors line cells; the stock templates declare them. A measure memo keeps a steady hover from measuring again. Tests pin each loop, constant and caller.

Found in Thorsten42/manila@50452d3fa5.

Fixes #1206
Fixes #1182
Fixes #1540

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:53:25 +07:00
Sam Sung 9e55f481f4 A pane's blend slot keeps the outgoing sequence on its own clock (#1881)
A held paperdoll turn re-arms a shuffle once per cycle. When it fell inside Stand's half-blend, the pane reused the one track it held for that clip, so the fading shuffle jumped back about 3 frames every 0.53 s, and the fade's end restarted the primary.

1.12.1's arm (`0x7121a0`) copies the outgoing primary into the blend slot with its own clock (`0x7125d6`). A refused arm (`0x7125c9`/`0x7125d4`) keeps that slot, the latched shortcut (`0x712549`) leaves it alone, and the slot ends on its own (`0x7147b1`), weighted by the smoothstep (`0x714880`).

Now each sequence has a second graph node, and the pane's blend slot runs on whichever node the primary is not on. A refused arm leaves it untouched, and the window's end stops only the secondary. Headless player tests cover the held turn, the fade end and the shortcut.

Fixes #1845

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:39:40 +07:00
Sam Sung 94fb118517 The UI ticks on the paced frame step the world gets (#1870)
The UI ticked on raw `Time<Real>`, which under pipelined rendering mis-splits shown frames (26.4 and 8.0 ms for two 16.7 ms frames), so a held paperdoll arrow turned in uneven steps while the bones, on paced `Time<Virtual>`, did not.

1.12.1 hands `OnUpdate` the loop's own frame time, unclamped (`0x420f70` → `0x885608` → `0x76b2c0`).

Now the pacer publishes one step per frame (`FrameStep`) that `Time<Virtual>` and the UI both read: `OnUpdate`'s elapsed, `GetTime` (a `Deviation:`, since 1.12.1 reads its clock live at `0x515ea0`), the cooldown anchor, aura expiries and `GetFramerate`. A stall reaches the UI whole. The pacer keeps every millisecond, drains its carry, and no longer jumps after a pause or splits a long hitch. Tests replay a stream measured on this machine.

Fixes #1846

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:58:30 +07:00
Sam Sung 615198d3bb The Lua library answers as 1.12.1's does (#1862)
Six places where benilla's Lua answered unlike 1.12.1's:

- The lexer took `[=[` long-string levels; 1.12.1's `luaX_lex` tests one byte after `[` (`0x6ff793`) and `--[=[` is a line comment (`0x6ff6c9`).
- `math.random` ran a glibc-style `rand` seeded with 1; now MSVC's (`0x7400e5`, `% 0x7fff` times 1/32767 at `0x7fb310`), clock-seeded as the client is (`0x5d1c8b`), fixed in captures and the addon harness.
- `getfenv`/`setfenv` honour a raw `__fenv` field (`0x702ba0`), and `setfenv` returns nothing.
- An object's text is `type: %08X` in the embedding API too (`0x74301d`).
- `time` converts a date table through MSVC's `_mktime` (`0x74066b`).
- `date` formats with MSVC's C-locale patterns (`0x8323a0`): `%c` is `09/09/01 01:46:40`, `#` drops zeros.

Each has a test that fails without it; the addon corpus parses as before.

Fixes #1257
Fixes #1258
Fixes #1510
Fixes #1511
Fixes #1503
Fixes #1504

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:35:43 +07:00
Sam Sung 81fd090ef2 Saved variables are read as 1.12.1 reads them (#1857)
The flat saved-variables file ran as Lua, and an add-on's saved file was skipped when its toc declared no scope.

1.12.1 reads `SavedVariables.lua` as lines (`0x4913b0`): a name, `=`, then a quoted string (raw, empty is nil) or a number read by `SStrToFloat` (`0x64aaa0`) and rounded to f32; anything else is skipped and no Lua runs. An add-on's account and character files run whenever they exist (`0x51f4a2`), and at save a scope that declares nothing removes its file (`0x51f859`). `RegisterForSave` is refused outside Blizzard code (`0x4884e0`).

Now the reader parses lines, unescaping the writer's escapes (a `Deviation:`). An old-format file with lines the reader drops is copied once to `.bak`. An undeclared scope's file read cleanly this session is renamed to `.bak` instead of deleted (a `Deviation:`), and `RegisterForSave` raises 1.12.1's two errors.

Fixes #1166
Fixes #1167

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:24:02 +07:00
Sam Sung 03f192f246 RegisterAllEvents joins every event's list in registration order (#1850)
A `RegisterAllEvents` frame sat on a list of its own: it heard every event after all named listeners, even untabled names, and `UnregisterEvent` could not take one event off it.

In 1.12.1 there is no all-events flag. `RegisterAllEvents` (`0x774c20` → `0x7023e0`) appends the frame to each tabled event's list where it is absent, as one `RegisterEvent` per event would, so dispatch (`0x703e50`) runs in registration order and `UnregisterEvent` (`0x702280`) removes that one event. The walk sets `event` once and restores it after (`0x703ea4`, `0x703f04`); `this` and the args stay per listener (`0x704f10`).

Now registration goes through one per-event list and the second pass is gone. Tests cover the interleaving, the per-event unregister, a re-register going last, a mid-dispatch registration, a frame registered both ways, and the `event` global.

Fixes #1165

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:26:37 +07:00
Sam Sung 30f2955e97 SetCamera on a character pane draws it through the camera it names (#1849)
`Model:SetCamera(n)` on a PlayerModel did nothing: the character pane always drew through its fixed pane rig.

1.12.1's PlayerModel registers no SetCamera of its own (`0x506230`), so the Model verb (`0x76e0e0` → `0x76cec0`) installs camera n of the copy; one past the model's count drops to the orthographic leg (`0x76ce80`). Every new copy gets camera 1 back at the ready hook (`0x505890` → `0x76cec0(1)` at `0x50589c`), and is armed with Stand (`0x505a2f`).

Now the pane draws through the installed camera, live or frozen as the reference does, with the ortho leg's particles at its unit (`0x7b2a50`). A camera or arm held for a pending copy dies when a new build is asked. The API-wall scanner now follows `#[path]` test modules. Tests cover each leg, the reset and both held-state sequences.

Fixes #1835
Fixes #1847

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

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