Commit Graph
1487 Commits
Author SHA1 Message Date
grom 7bd3635463 Update README.md 2026-10-16 14:26:06 +00:00
grom 48b43d0662 Update README.md 2026-10-16 14:25:24 +00:00
gromandClaude Opus 5 fc63dd022a ui_pass: a resized image re-prepares the materials that sample it
A booth pane's render target is resized between 256 and 512 as the pane it
is lent to changes size, and `GpuImage::prepare_asset` builds a brand-new
texture and view every time it runs. Nothing re-prepares a material because
an image it names changed — `PreparedMaterial2d` prepares from its own asset
events alone — so the UI's cached quad material went on holding the dead
view and sampled a texture nothing rendered into any more.

That is what "the preview is frozen" was: the first window to claim a slot
bound a live texture and every later one bound a corpse, while the camera
kept rendering and the bakes kept restaging into the texture that was no
longer on screen. The cards never froze because a 256 card never resizes
its slot; the big previews, which are the only panes that ask for 512, froze
every time.

`rebuild_ui_mesh` already hears the image stream for removals. It now also
hears `Modified` and touches every cached material naming that image, which
re-extracts it and rebuilds the bind group against the image's current view.
Only the two resize sites emit this event, so a steady frame does no work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 07:23:18 -06:00
gromandClaude Opus 5 77ffc36fbd portrait: the mirror clears the stage latch, so a reopened window restages
`Booth::staged` records which pane's bake a booth holds, and only
`stage_pane_look` ever wrote it. Nothing cleared it, so a booth that fell
back to mirroring the live unit still claimed to hold the pane's bake.

That is a stale latch with a delay fuse. A pane's bake lives as long as its
room, and every pane numbers its own bakes from 1, so a window that closes
and reopens brings the same pane back at revision 1 — meeting a latch that
still said `(that pane, 1)` from its previous life. `needs_restage` read
that as "already current" and the window came back showing whatever the
mirror had left in the booth. It was the same shape as the pane-to-pane
collision the latch already guards, one lifetime further out.

Clearing on the way through the mirror can only cost one extra restage.

Also adds `[booth] pane geom`, a `WOW_BOOTH_LOG` line per pooled slot
whenever the aspect its camera is framed through, the square bake it
renders into, or the square the UI samples changes — the three numbers
that decide whether a pane is drawn un-stretched, and the ones a pane
whose content and framing both check out must be disagreeing on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 21:19:13 -06:00
gromandClaude Opus 5 1a26bb0b2a portrait: one stuck pane stops starving the rest of its page
`build_pane_previews` assembles one pane a frame and chose the first stale
one. A pane that cannot be assembled — an item model that never resolves —
is stale by definition, so it was chosen again every frame and gave the
frame up every time: every pane behind it in slot order waited on it
forever and stayed on the mirrored live player, which looks exactly like
the per-pane dress-up never having run. The shop's Smoky Wooden Torch was
doing this to its page.

The choice now offers every stale pane in slot order and the caller takes
the first it can actually make, so a pane that cannot be assembled costs
its own turn and nothing else. Walking past one is free: every `assemble`
bail is a lookup, all of them before the skin composite.

Also adds `[booth] pane source`, a `WOW_BOOTH_LOG` line per pooled slot
whenever where its character comes from changes — its own bake, or the
mirror because the room is closed, the bake has no parts, or its rig is
still loading. Those three look identical on screen and were the reason
this took a second run to see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 20:46:40 -06:00
gromandClaude Opus 5 da0f647be6 portrait: size a pooled pane's bake to the pane, without touching what it shows
Tmog's full-body preview is ~500 physical pixels tall and was drawn from a
256² bake, so it read soft beside its own grid of cards — which 256²
oversamples. One size cannot serve both. A pane larger than its bake now steps
up to 512², so a page of fifteen cards plus one large preview pays for a single
upgrade rather than sixteen.

This is the second attempt. The first widened `BoothPanes`' value type, which
`sync_body_booth` and the dressing room read to frame their cameras: a change
meant for resolution reached into the path deciding what a pane *shows*, and a
page of previews stopped showing their own looks. The size now travels in
`PaneBakeSizes`, written by the extract and read by `resize_pane_targets`
alone. Nothing on the content path sees it.

The first attempt also called `Assets::get_mut` to *read* a target's size. That
marks the asset modified, and a modified `Image` is re-uploaded from its CPU
pixels — the zero-fill it was created with — silently erasing whatever the
booth had just rendered. The size is now probed read-only and `get_mut` is
reached for only when it must change, pinned by
`a_target_already_the_right_size_is_not_touched`.

An absent entry means "not drawn this frame", never "wants the default", so a
paced pane cannot shrink its own target between renders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 15:37:13 -06:00
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.

Today's work, which is the reason a baseline exists at all:

- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
  characters were entering the sun's shadow cascades, so every skinned-rig
  shadow specialization compiled live mid-frame; the hitch detector logged
  "main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
  the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
  tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
  an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
  and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
  measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
  frame-cost reading no longer pays 2.7 ms a frame for it.

Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 15:01:14 -06:00
chairmanpow 706cd717a3 merge: absorb samwhosung/main on top of the upstream merge
Same fork policy: upstream structure wins, local-unique re-instated.

Cargo.toml unions both measured dev-profile philosophies (codegen-units=1 +
lto=off from samwhosung, debug=line-tables-only + split-debuginfo from the
fork's disk measurements).

glue_booth.rs takes samwhosung's typed GlueScene (token/of_race/from_model_path)
and re-adds the fork's TurtleWoW races (Goblin=9, BloodElf=10) as variants;
their glue fog stays on Human's row, exactly as the fork's scene_fog fallback did.

ui script takes samwhosung's mouseover/hyperlink rework (world_mouseover,
minimap_blip, hyperlink.rs) over the fork's inline hover_link/span_at/pointer
path — the rework covers the same OnHyperlinkEnter/Leave behaviour.

game_plugins.rs keeps the fork's re-measured ceilings (466 / 5,509); mail.rs
keeps the explicit 13-tuple with a float daysLeft; MAP.md takes the superset.
2026-10-12 17:24:31 +01:00
chairmanpow 26d714fd1d merge: absorb upstream/main into the Turtle fork
Fork policy (docs/METHOD.md): upstream structure wins; local-unique work
re-applied on top. This commit is the upstream half of the reconciliation.

Upstream is the same program at a rewritten history (884dff77 == 4e0e89cb
tree), so the merge base was replaced to keep a real three-way diff. The
large conflicts are upstream moving first: the UI manifest (benilla.toc ->
the binary LAYER_MANIFEST/layer.toc split), the CVar table (cvars.rs ->
cvars/{mod,table}.rs, +24 video rows), the editbox flush model, the
host-side GetTime, the Options* -> BenillaOptions* rename, expanded
ui_unit.rs, and a rebuilt volumetric_light.rs.

Local-unique pieces re-instated on the upstream structure:
- OptionsFrame.xml: the 1.12 OptionsFrameSliders table restored under the
  Benilla rename, with the TurtleWoW deviation note (patch-4 replaces
  OptionsFrame.lua).
- layer.toc: CharacterFrameAdapters.xml (the sixth TurtleWoW character tab).
- ui script: wipe/sizeof, BenillaGetMerchantItemStats, and the attach/mount
  teardown re-walk all survive.
- game_plugins.rs keeps UPDATE_ACTIONABLE_CEILING=5_509 (re-measure pending).

The LFT/Turtle_TransmogUI rows benilla.toc named off the player's install
now come from the chain's own FrameXML.toc via the new core loader.
2026-10-12 17:20:45 +01:00
chairmanpow d2c99b2764 wip: GPU skin palette compute pass (WOW_GPU_SKIN=1), not yet wired into the app
Standalone module and its palette shader; no caller references it yet, so the
default CPU skin path is unchanged.
2026-10-12 17:02:49 +01:00
chairmanpow 0487bea08f gfx: the torch shadow cubes drop to 256, 36 MiB for the whole bank
The soft torch kernel is wider than one texel at 256 over the 48 yd cube
(~5 texels/yd), the same trade the sun map took at 4096 -> 2048.
2026-10-12 17:02:42 +01:00
chairmanpow 799196e1d1 gfx: bound the torch and point-light tables by the camera frustum
A light whose whole candidacy sphere (the shader's own reach, the interior fill
dome included) sits off-screen leaves the packed table, so a city's torches
behind the player stop padding the per-vertex walk. The packer also reuses one
scratch buffer and tests the cheap camera distance before the portal walk.
2026-10-12 17:02:42 +01:00
chairmanpow a0e7087546 fix: a TRAP GameObject never becomes the mouseover, so an AB node banner wins the pick
A trap is proximity-triggered and never clicked, so its interaction predicate
\+0xc is constant false like the transports'. Arathi Basin's visual-only aura
GameObjects (180100-180102, type 6, faction 114 hostile-to-all) otherwise stay
eligible through the inverted TRAP term and take the pick from the banner.
2026-10-12 17:02:37 +01:00
chairmanpow fa02975473 fix: the attach and mount teardown path uses try_ commands and child-side ChildOf
A parent-side add_child panics against an entity a same-frame teardown already
despawned; spawn with ChildOf on the child and use try_insert/try_remove/
try_despawn so the same-frame churn warns instead of aborting.
2026-10-12 17:02:33 +01:00
Sam SungandClaude Sonnet 5.5 2af917044d Stock login fetches the server alert; LaunchURL behind the gate (#1040)
The stock login's Breaking News box (`WOW_GLUE_STOCK=1`):

- At glue init, as `0x46a400` does, the client reads the Lua global `SERVER_ALERT_URL`, which the player's own GlueStrings or a server's patch sets. It fetches the URL once on a background thread: plain HTTP, 5 s timeouts, redirects followed.
- The body builds the reference's buffer (`0x46a580`: 0x7ff cap, appended chunks). A case-insensitive `SERVERALERT:` prefix fires `SHOW_SERVER_ALERT` once from the update (`0x46b939`). Every failure is silent.
- `LaunchURL` opens the browser only for URLs that pass the reference's gate (`0x5abc10`): `http://`, a strict charset and the 10-host whitelist. An over-long host is refused rather than cut.
- No fetch or browser ever runs in a test, capture or unattended run.

Tests run against a local fake server: prefix, 404, redirects, chunked, timeouts, caps, and the frame shown end to end.

Part of #1026

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 21:42:29 +07:00
Sam SungandClaude Opus 5.5 5c7eb5fc75 Stock glue screens draw their 3-D scenes through the glue booth (#1039)
Part 4b of the stock GlueXML (`WOW_GLUE_STOCK=1`): the stock screens' ModelFFX scenes now go through the glue booth instead of the model-tile path, which drew them black.

- The scene, look and facing come from the glue VM. `SetCharSelectBackground` runs the frame's own SetModel and swaps its fill callback (`0x472128`, `0x472144`), so the select scene fogs until a character sets its stage.
- An ModelFFX pane draws the booth's scene at its own bucket's ARTWORK layer (`0x46fa90`), framed at the pane's aspect with the diagonal law (`0x7ada40`, `0x5c3cc0`). The native box Deviation now holds for the native screens only.
- Each SetModel restarts the stage. The select glow holds through an empty roster until the screen shows again.
- An empty roster shows the Orc scene fogged, and after the last delete the last stage stays, as `CharacterSelect.lua` leaves them.

Part of #856

Fixes #978

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:13:47 +07:00
Sam SungandClaude Opus 5.5 eb63c8861b Cursor runs on ClearCursor and the setters' own event tails (#1038)
The cursor now runs on the client's two primitives. `ClearCursor(a,b)` (`0x495190`) cancels an armed gift wrap, then runs the payload's arm (an item's unlock fires `ITEM_LOCK_CHANGED`; coins fire `PLAYER_MONEY`), then the grid hide and `CURSOR_UPDATE`. Each setter runs that clear first, then its own tail (`0x494b60`, `0x4950f0`, `0x494d20`, `0x494f80`, `0x494e20`, `0x495010`). Every pickup, place, split, sale, trade, mail and auction path goes through them. So a `CURSOR_UPDATE` handler reads the emptied cursor, and `PickupSpell`/`PickupMacro`/`PickupPetAction`/`SplitContainerItem` clear what is held instead of refusing.

An item attached to mail or an auction stays locked until it leaves the slot (`0x4adef0`). Attaching to an occupied slot puts the old item back on the cursor.

Event-order tests cover each setter, the coin clear, and the attach lock and swap.

Fixes #1011
Fixes #1012
Fixes #1006
Fixes #1007
Part of #156

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:47:20 +07:00
Sam SungandClaude Opus 5.5 bb82dd1ac8 Lua getters answer the reference's miss shapes; tooltip setters' flag (#1030)
Lua bindings answer the reference's shapes on misses, bad indices and tooltips:

- Indexed getters push their full miss tuple (`GetCraftInfo`, `GetPetActionInfo`, the trainer requirements and others). Indices go through `ftol` and an unsigned compare, so a negative misses instead of raising. Verified slot readers keep their own raises (`IsInventoryItemLocked`, `0x4c8520`).
- The eight GameTooltip setters return the builder's refresh flag, gated as `0x53267c` gates it, so a hovered cooldown tooltip refreshes. `LOCKED` follows the item object (`0x52bdb3`).
- Quality goes through the InventoryType gate (-1), and a pending template shows the question-mark icon.
- `GetLootSlotInfo` returns four values. `SetSpell`, `CreateMacro`, `JoinChannelByName`, `GetQuestReward` and the shapeshift and trade-skill filters raise or answer as the reference does.
- The shape gate probes each getter's miss legs through an argument ladder.

Fixes #145
Fixes #993
Fixes #994
Fixes #995
Fixes #996
Fixes #997
Fixes #998
Fixes #1016

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:28:35 +07:00
Sam SungandClaude Opus 5.5 781bf36ab3 Key input follows the client's walks; stock login takes typing (#1027)
Key input follows the client's walks: key-down (`0x765f10`), char (`0x765df0`), key-up to the recorded consumer (`0x765fd0`) and repeat (`0x766070`).

- EditBoxes are in the walk from creation (`0x779ced`), so the stock login's boxes take typing; only a focused box consumes. A box declines PRINTSCREEN and unaltered arrows on an `ignoreArrows` box, so stack-split steps under a focused chat box. A binding's press records the world frame.
- Repeats follow the pressed-key list, so a held key resumes after alt-tab. Chords reach a box only through the walk. Keys the client can't name walk nowhere.
- The stock login reads the agreements before its first screen. Cinematics returns to login (no movie player; Deviation). The Realmlist box has its middle border.
- Alt+F4 closes the window on Windows and Linux (Deviation: the reference relies on the OS).

Fixes #179
Fixes #1003
Fixes #1004
Fixes #1005
Fixes #1019

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:07:34 +07:00
Mathih13andClaude Opus 5.5 7547081a58 fix: a same-map teleport shows no loading screen, as 1.12.1 does (#1018)
1.12.1's MSG_MOVE_TELEPORT_ACK handler (0x603f90) reaches none of the
loading screen's raises: 0x406800 is entered only from SMSG_TRANSFER_PENDING
(0x401900), the glue EnterWorld (0x46b6f1) and 0x407de0. benilla raised the
cover on a same-map snap twice over: the "focus not resident" backstop on the
snap frame, and a >=100 yd relocation raise. The relocation raise goes, and
the backstop stays off while a snap's settle hold is up; the body is held over
the destination as before.

Verified live against the local realm (map 0, Gmagent): .tele ironforge from
Northshire (~3960 yd) and back covered 50 and 93 frames (0-916 ms, 0-1680 ms)
before, 0 and 0 after; cross-map .tele valley and back still raise the cover
("worldport"). New tests in loading_screen.rs fail on the old code at the snap
frame. benilla-app and benilla-world lib tests green, install-reading tests
green with BENILLA_REQUIRE_DATA=1 (the addon corpus is absent on this
machine). Built on macOS (arm64).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:46:50 +07:00
Sam SungandClaude Opus 5.5 d759008172 Logout returns to character select on the same world connection (#1022)
In 1.12.1, logging out or a refused character login returns to character select on the same world connection and re-lists there (`0x5aaeb0` → `0x401ee0`, then `0x46b880` → `CMSG_CHAR_ENUM`). There's no realmd logon and no new `CMSG_AUTH_SESSION`. Enter World closes the realmd link (`0x46b70c` → `0x5b3320`), and only the glue's own re-dials reopen it.

- benilla used to drop the socket and log on again from cached credentials. Now the world session splits and rejoins on one connection, and the header cipher carries its state across.
- The realmd link closes at the pick.
- A leftover packet at character select is skipped rather than fatal.
- The write thread applies a pending writer swap before any command.
- A kept-link teardown ends the character without a "server down".

Fake-server tests pin one logon, one auth and one more enumerate. The logout smoke fails without a same-link relist.

Fixes #984
Fixes #983

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:38:05 +07:00
Sam SungandClaude Opus 5.5 aba120cdcb Lua verbs fire their events before returning, in the reference's order (#1015)
In 1.12.1 an event a Lua verb raises runs its handlers before the verb returns: `SignalEvent` (`0x703e50`) walks the listeners in place. About 35 benilla binding sites queued theirs for a later drain instead.

- Verbs now raise inside `tick::verb` steps, and the events fire as each step's model borrow drops. A step follows the reference's split wherever it signals before changing state a getter reads: the money pickup, bag pickup and place, merchant grab, repair cursor, item use, action and pet-bar pickup and drop.
- The grid events fire before `CURSOR_UPDATE`, and a clear's unlock before both, as at `0x4952b3`/`0x495455`. `ITEM_LOCK_CHANGED` carries no arguments.
- `pending_events` is host-only now.

Tests pin handler order per family, nesting from a handler, and the order changes.

Fixes #999
Fixes #582

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:14:10 +07:00
Sam SungandClaude Sonnet 5.5 24c7cab983 Stock character select roster; the screen asks for the list (#1014)
Part 4a of the stock GlueXML (`WOW_GLUE_STOCK=1`): character select's roster.

- The select verbs run on the reference's roster (`0x4736c0`): `GetCharacterInfo` (race and class from `Name_Lang`), `SelectCharacter` with its clamp and event, the facing gate, and `GetCharacterListUpdate`. They are backed by the roster load and teardown (`0x4724d0`, `0x472580`) and screen state 3.
- Under the stock screens the character list is fetched only when the screen asks, as `0x46b880` does. The world connect completes on the auth response (`0x5aab80`), not on a roster.
- Every character-park request goes through one ordered channel. A roster reply completes only the current link's query. A late auth after a cancel is dropped, Warden stays terminal, and a failed list shows `CHAR_LIST_FAILED`.

The native screens are unchanged. Fake-server tests cover the ordering, the cancel, the realm change and the native policy.

Part of #856

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 15:59:54 +07:00
Sam SungandClaude Opus 5.5 0f505a2ebf RegisterEvent ignores case; a frame's first resize precedes OnLoad (#1001)
- `RegisterEvent` and `UnregisterEvent` match a name against the client's event table case-insensitively and drop a name that isn't in it (`0x702140`). A new gate checks that every event name the code fires is in the table.
- A visible XML frame resolves its layout at the post-load hook (`0x76a310` → `0x7680e0(1)`). So its first `OnSizeChanged` fires before its `<Frames>`, `OnLoad` and `OnShow`, as in 1.12.1. A hidden frame still waits for its show.
- `pending_events`' doc now says what the queue is: `SignalEvent` (`0x703e50`) runs handlers in place, and the Lua verbs that still queue are a gap.
- #890 was already fixed by #907; two stale comments around it are corrected.

Fixes #959
Fixes #972
Fixes #560
Closes #890

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:08:07 +07:00
Sam SungandClaude Opus 5.5 720dcf8188 Lua bindings return 1/nil and the reference's value counts (#1000)
Lua bindings now return what 1.12.1 pushes, in type and count:

- Flags are 1 or nil, never a Lua boolean. This covers `SetInventoryItem`/`SetBagItem` hasCooldown, `GetQuestLogLeaderBoard` finished, `GetContainerItemInfo` locked/readable, `AtTop`/`AtBottom`, `IsOwned` and `SetAlphaGradient`.
- Eleven verbs whose handlers end `xor eax,eax` return nothing. That covers `PickupAction`, `PlaceAction`, `PickupSpell`, `PickupInventoryItem`, `EquipCursorItem`, `SplitContainerItem`, `PickupMacro`, `DeleteMacro`, `ResetChatColors` and others.
- The miss tuples: `GetInboxHeaderInfo` gives 13 values (`0x4aeff0`), `GetInboxText` four nils, and `GetQuestLogLeaderBoard` three.
- `GetTrainerServiceDescription` is nil with no text (`0x4d9d44`). `GetZonePVPInfo`'s faction is `""`, not nil (`0x48d633`).

The boolean gates now probe every global and widget method.

Fixes #442
Fixes #574
Fixes #577
Fixes #578
Fixes #822
Fixes #823
Fixes #872

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:01:32 +07:00
Sam SungandClaude Opus 5.5 1b335717c3 Addon loading and .toc parsing follow AddOn_Load and Toc_Parse (#992)
Addon loading follows the client's `AddOn_Load` (`0x51f240`) and `.toc` reader (`0x51c9b0`):

- One driver serves the startup walk, `LoadAddOn` and the survey harness. It marks the addon loaded before its dependencies (`0x51f313`), so a requirement cycle loads both and `IsAddOnLoaded` answers during an addon's own load. It loads `## LoadWith` dependents after `ADDON_LOADED` (`0x51f5b2`).
- Every directive is matched by its own literal. `LoadOnDemand`, `Secure` and XML bool attributes parse with the client's string-to-bool (`0x6f1b30`), so `hidden="1"` works. Values are not trimmed, and only an unindented `##` line is a directive.
- `GetAddOnMetadata` reads the metadata map (`0x51d580`): Title defaults to the folder, the last value wins, and locale suffixes apply. The url comes from the server, never the `.toc`.
- `EnableAddOn` keeps a toggle for an addon that isn't installed.

Tests are mutation-checked against the reference order.

Fixes #192
Fixes #193
Fixes #405
Fixes #986
Fixes #987
Fixes #990

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:28:48 +07:00
Sam SungandClaude Opus 5.5 1efd9a7163 Lua bindings coerce numbers, flags and colours as 1.12.1 does (#991)
Lua bindings now read their arguments the way the 1.12.1 client does:

- Numeric strings parse as Storm's `SStrToInt` (`0x64ac60`) and `SStrToFloat` (`0x64aaa0`). That covers channel keys, `GetNumber` and `item:` fields.
- Flag arguments go through `GetBoolOrDefault` (`0x6f1c10`) with each site's default. That is about 40 bindings, `SetChecked()` and `ShowHelm()` among them.
- Colour channels take numeric strings (`lua_tonumber`), and alphas are `isnumber`-gated: no raise, 1.0 by default. A three-argument `SetVertexColor` keeps the current alpha (`0x79ae0a`). So action-bar rings dimmed by a drag stay dimmed, as in 1.12.1.
- `SetTexture("1")` takes the colour form. MessageFrame channels follow `0x795590`.
- `LoggingChat`/`LoggingCombat` return the flag.

Binding-level tests use the reference's values.

Fixes #135
Fixes #144
Fixes #160
Fixes #161

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:41:47 +07:00
Sam SungandClaude Opus 5.5 b2926edf53 Stock realm list and wizard; realm park cancels cleanly (#985)
Part 3b of the stock GlueXML (`WOW_GLUE_STOCK=1`): the stock `RealmList` and `RealmWizard` run on the reference's ten realm verbs (`0x46f480`).

- The realm park is now a cancellable operation. Each dial carries the cancel generation it was asked under, so no Cancel is lost, and a status Cancel closes the logon (`0x5b3320`).
- Change Realm with the world up drops the session first (`0x46b22b`).
- The roster arm (`0x4724d0`) sets the chosen realm, or asks for the list when the roster is empty.
- An empty realm list is shown, never dialled at the world port.
- The native type sort puts PvP before RP.

Deviation lines cover the re-dial's full logon, comparator ties, a missing Region row and the native close.

Part of #856 and #834

Fixes #833
Fixes #974
Fixes #982

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:46:31 +07:00
Sam SungandClaude Opus 5.5 0c58d85cf9 Layout file saves the rect; bubbles re-sort on change; resize Deviation (#981)
- **The layout file** now saves what the reference's writer does (`0x490e60`): `X`, `Y`, `W`, `H` and `FrameLevel` from the frame's held rect, rows in creation order, and only frames that are user-placed and movable or resizable. The restore (`0x4905e0`) pins TOPLEFT to the screen, sets the size and level, and resolves each frame as it goes (`0x490710`). Old anchor-form files still load and are rewritten once.
- **Chat bubbles** re-rank only on a camera move or a new line (`0x4b11ff`), and a repeat speaker moves to the tail, as `0x4b1870` links it.
- **The live re-layout on a window resize** gets its `Deviation:` line. 1.12.1 reloads the UI on an aspect change of 0.001 or more (`0x492f26`).

Tests cover parent/child level restore, the held rect, the hidden-frame resolve, the junk lines, the migration and the bubble tie order.

Fixes #954
Fixes #955
Fixes #847

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:26:11 +07:00
Sam SungandClaude Opus 5.5 8ccaa7b411 An unattended run keeps its state in a fresh folder of its own (#980)
An unattended run (`WOW_UNATTENDED`, `WOW_RIG`) resolved the same `benilla-config/` as a person. In a dev build, every linked worktree maps that to the primary checkout's folder. So a probe read the player's CVars, bindings and layout, and wrote its realm, settings and window positions back over them.

`local_state::home` now gives an unattended run a fresh folder of its own (`$TMPDIR/benilla-run-<pid>/benilla-config`) and prints it at boot. `BENILLA_HOME` still pins one, and a capture still resolves none. CONTRIBUTING's "Running it unattended" says so.

Test: `an_unattended_run_keeps_its_own_state_folder` checks the rig path, the override and the capture, and that the folder is never the resident one.

Fixes #975

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:15:31 +07:00
Sam SungandClaude Opus 5.5 a329f19e06 Reputation lines, NPC greeting gates, and packet bytes in the trace (#976)
Player reports and the trace, in one batch.

- SetFactionInactive/SetFactionActive raise `UPDATE_FACTION`, so the list moves at once.
- NPC greetings: the latch, a dead unit, then the reaction pair with the active mover (`0x60c270`); a dead or ghost player hears none. Only a passing click re-tracks the NPC, and a window hello resets it (`0x60c3d1`).
- "You are now <rank> with <faction>." prints on first contact and rank change (`0x62c670`), in packet order, with the real gender.
- A standing packet prints its "increased by" line for the first entry only, never a hidden one (`0x4d57dd`).
- `WOW_MOVE_TRACE` carries each packet's bytes as hex; chat bodies are opt-in, and dot/bang commands and channel passwords are always redacted.

Tests cover each gate, the line order and the redaction.

Fixes #927
Fixes #882
Fixes #881
Fixes #867
Fixes #965
Fixes #966
Fixes #967

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:54:21 +07:00
Sam SungandClaude Opus 5.5 7158c58575 Hidden frames defer their layout; GetWidth answers the set size (#973)
A hidden frame resolved and fired `OnSizeChanged` at once, a getter's resize fired at the drain, and `GetWidth`/`GetHeight` answered the rect over scale.

In 1.12.1 hiding sets the deferral flag `0x2` (`0x76ad50`); while set, an invalidate marks the frame pending and passes it to its dependents without a resolve (`0x768178`). Show resolves it first (`0x76ae58`), a getter resolves a pending one and fires `OnSizeChanged` inside the read (`0x7a1980`, `0x76b580`), and an anchor read pulls it for good (`0x7a2fc0`). `GetWidth` answers the set size when non-zero, else the rect span, unscaled (`0x7a1e93`, `0x7a1ee6`).

The deferral is modelled for frames and regions, getters fire inside the read, layout ids mint in handle order (deterministic), and the options window measures its scroll by its edges.

Tests bite on the parent; the verifier is green; stock quads match.

Fixes #948
Fixes #971

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:06:04 +07:00
Sam SungandClaude Opus 5.5 45b7914c9e The stock login screen logs in; realmd 0x0E is a success (#970)
Under `WOW_GLUE_STOCK=1` the stock login screen now logs in. Separately, realmd's 0x0E answer was refused by the native login too.

In 1.12.1 the glue manager runs the screen states (`0xb41da0`): `DefaultServerLogin` (`0x46afb0`) needs the agreements accepted and opens the status dialog; each tick re-texts it from the logon's state and result (`0x46b140`, tables `0x836b40`, `0x836b78`); the world connect re-texts it every frame (`0x46be8b`). The agreements seed from `readTOS`/`readEULA` (`0x46a400`). Realmd 0x0E is a success (`0x5b2cd4`); a proof's 0x0A changes nothing (`0x5bada2`).

The glue core verbs, the glue manager over the native login, the agreement CVars, `SimpleHTML file=`, case-insensitive loose files and a benilla Realmlist layer file are built; the protocol accepts 0x0E. A recorded `Deviation:` keeps the interface signature checks off.

Tests: fake-realmd refusals and 0x0E; stock-screen login reaching the native submit.

Part of #856

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:28:08 +07:00
Sam SungandClaude Opus 5.5 133414f5c1 WOW_GLUE_STOCK=1 loads and draws the stock GlueXML (#969)
With `WOW_GLUE_STOCK=1` (dev builds), benilla now loads the stock GlueXML off the player's own patch chain into the glue state and draws it; with the switch off nothing changes.

In 1.12.1 the glue builder (`0x46a7b0`) opens `Logs\GlueXML.log` (`0x46a8ce`), runs `GlueXML.toc` through the shared toc runner (`0x6edb90` at `0x46a9b3`), fires `FRAMES_LOADED` (`0x46aa34`) and opens on "login" or "movie" (`0x402b3b`), or "charselect" back from the world (`0x402084`). Glue `GetScreenWidth`/`GetScreenHeight` answer the root unscaled (`0x46dd30`, `0x46dd80`).

Under the switch the glue state replaces the boot state, takes input, and fires `SET_GLUE_SCREEN` on those edges; the native screens are hidden but still drive the network. A data-gated test loads the stock files with only seven unbuilt verbs raising, a list that may only shrink.

Part of #856

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:40:34 +07:00
Sam SungandClaude Opus 5.5 51a2ce5586 Layout writes invalidate as 1.12.1 does; only a reached node re-resolves (#964)
The solver took any node's recomputed rect whenever it resolved, because about 55 layout writes named a node without invalidating it. So a freed region stayed in its dependents' anchors, `SetTexture` re-resolved a failed texture, and a scroll child kept its authored anchors behind a per-solve override.

In 1.12.1 a node re-resolves only when invalidated or a target stores a new rect (`0x768d20`, slot 0 `0x768cf0`). `SetTexture` only dirties the draw (`0x770200`). A destructor frees its dependents' anchors to it (`0x768e20` → `0x767e70`, no invalidate). A scroll child is re-anchored to one TOPLEFT (`0x787100`).

Every write now invalidates exactly where the reference's does, and the solver's guess, the hold and the art latch are gone. The tooltip invalidates only on its relayout (`0x530640`).

Tests bite on the parent; the verifier is green; the stock UI's quads are byte-identical.

Fixes #947
Fixes #949
Fixes #958

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:46:24 +07:00
Sam SungandClaude Opus 5.5 a9dd325bb7 A glue Lua state with the reference glue surface, events and ModelFFX (#963)
The first piece of running the stock GlueXML: benilla had no glue Lua state, and no reference data for its surface.

In 1.12.1 the glue state holds the base set (`0x7039e0`) and seven registrars (`0x46abb0`): 119 functions, 105 of them glue-only, with 26 glue events (`0xb41e70`, `0x46a88f`). `RegisterEvent` matches names case-insensitively and drops an unknown one (`0x702140`). The glue builds the 13 base frame types plus `ModelFFX` (`0x46a4fe`), a Model with its own show and paint (`0x46fa00`); the in-game types come only at world entry (`0x495940`).

`reference/` gains the glue event and surface tables, generated and checked against the shapes table. `UiScript::new_glue()` builds that state; a gate holds its globals to the table, with the 108 unbuilt names listed. Nothing in the app changes yet.

Part of #856

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:38:02 +07:00
Sam SungandClaude Opus 5.5 985dabff44 Layout-mode drag, title press takes the slot, links are their own button (#961)
A title-region press refused while another drag held the slot, `SetLayoutMode` stored nothing, and a press on a chat link made the chat frame itself the target, so every link click started a title move of the window.

In 1.12.1 the drag entry takes the slot unread (`0x7652b0`); a mid-drag press goes to the frozen hover (`0x76637f`). With `SetLayoutMode` (`0x488540`) on, a Ctrl or Alt press drags the nearest movable toplevel (`0x765490`) from a 3×3 grip grid (`0x765320`) and is consumed, world included; its release only ends the drag (`0x7664e0`). Each link span is its own Button (`0x7a3240`) routing `OnHyperlinkEnter`/`Leave`/`Click` to the parent (`0x7a3780`–`0x7a37e0`).

These now hold; a release goes to the capture whatever its button (`0x76642b`), and number arguments keep `_ftol`'s low dword.

Tests bite on the parent for each.

Fixes #951
Fixes #952
Fixes #957

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:22:54 +07:00
Sam SungandClaude Opus 5.5 108c354fc6 Text measures by the reference kernel: kerned steps, ink tail, no pad (#956)
Text measured `ceil(sum of steps) + 1`: no kerning, float advances, the last glyph priced by its step, and no outline or shadow lead.

In 1.12.1 the kernel (`0x5c6940`) seeds a line with the outline lead plus the shadow (`0x5c69c0`, `0x5c69a4`), steps each glyph by `ceil(floor(adv) + 1 + min(kern, 0))` on FreeType's 26.6 advances (`0x5ca2d0`, `0x5d1298`), prices the line's last glyph by its ink (`0x5c6b70`, `0x5cb080`) and carries the previous glyph across a break (`0x5c6aa0`). The draw rows use the same law, shadow-seeded, wrapping at `ceil(width)` (`0x5c7470`, `0x5cddb1`).

The measure, the wrap, the justify and the EditBox now follow it, reading the face's own metrics, kerning and composite build (`0x7d40e6`).

Tests pin widths computed outside the code base from FRIZQT and ARIALN at 12–34 px, with and without outline and shadow.

Fixes #427

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:27:55 +07:00
Sam SungandClaude Opus 5.5 5af22799d0 A drag pins one TOPLEFT anchor at the frame rect, as in 1.12.1 (#953)
A drag kept the frame's authored anchors: `GetPoint()` answered them afterwards, the frame kept following its old target, and a resize grip on a CENTER- or TOPLEFT-anchored frame moved the wrong edges.

In 1.12.1 every drag enters at `0x7652b0`, whose first act pins the frame (`0x768430`): `ClearAllPoints`, one TOPLEFT on the screen root's TOPLEFT at the current rect, and an explicit size. The pump `0x768710` moves that anchor (case 4) or, for each of the eight grips, computes the clamped size and re-pins shifted by the left and top travel only (`0x76898a`). A frame with no rect starts no drag (`0x7652cd`).

Drags now pin and pump that way, with one drag slot; `StartSizing` defaults to BOTTOMRIGHT (`0x776913`).

Tests: the pin and `GetPoint` after a drag, the eight grips on a CENTER-anchored frame, the clamps, the layout cache round trip, and stock chat-window resize and off-screen drop.

Fixes #173

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:57:59 +07:00
Sam SungandClaude Opus 5.5 aff59fd55b ClearAllPoints keeps the rect; a failed resolve reaches nothing (#950)
`ClearAllPoints` invalidated the node, so an anchorless frame lost its rect at once, with everything anchored to it, and any region whose resolve failed stopped drawing.

In 1.12.1 `ClearAllPoints` (`0x7a2b70` → `0x767620`) only frees the anchors, queuing no resolve, so the rect stands. Only the node's own invalidate re-resolves it; a failure (`0x768d20` at `0x768d55`) clears the valid bit and skips slot 0, so its dependents keep their rects. A Texture keeps drawing its last vertices (`0x7706e0` checks only its texture); a FontString stops (`0x773080`, `0x7730b7`). A new string, font or spacing forces a store (`0x771ea4`).

Each node now carries an entry state, and takes a recomputed result only when invalidated or a target stored a new rect; a failed Texture's last quad is kept, and the full derive and the incremental pass read the same state.

Tests bite on the parent; the verifier is green.

Fixes #942
Fixes #943

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:36:21 +07:00
Sam SungandClaude Opus 5.5 75edfd7ca5 SetFont gates as 1.12.1 does; SetSpacing on every text table (#946)
`SetFont` raised on a `false` flags argument and wrote the height even when the load failed, and only SimpleHTML had `SetSpacing`/`GetSpacing`, so SpellAlert's message frame raised in its OnLoad.

In 1.12.1 `SetFont` (`0x79f210`, the FontString's `0x79d4f0`) reads flags only when `lua_isstring` (`0x79f287`), writes them on every successful load (`0x79f334`), and writes nothing on a failed load or a height ≤ 0 (`0x79f361`, `0x79f2f4`, `0x771571`). Every text table has the spacing pair (`0x79fb40`/`0x79fbe0`; the FontString's clamps at `0x772240`); the line step adds the gap rounded up to a device pixel (`0x5c2070`, `0x5cdc20`), as do the message frames (`0x78899e`, `0x785f06`) and EditBox rows (`0x77d133`).

All seven SetFont bindings share these gates and ask the host's font probe; spacing is stored, inherited, measured and drawn on all six tables.

Tests bite on the parent for each.

Fixes #873
Fixes #692
Fixes #945

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:13:09 +07:00
Sam SungandClaude Opus 5.5 87bf5904ff A region keeps its own name; only the name lookup is first-come (#944)
Region names lived in one first-wins name → id map that also served `GetName`, so a second region given a taken name answered `GetName()` with nil and was `<unnamed>` in its error strings.

In 1.12.1 the name lives on the region: `SetName` (`0x76c650`) keeps the region's own copy at `[+0x98]`, freed by its destructor (`0x76c560`) and read back by `GetName`. Only `_G` is first-come (`0x701bd0`, `0x701c8a`).

Each region now carries its name; `GetName` and the error strings read it directly, and the name map is a first-come lookup only, whose entry a free drops only when it holds that region.

Tests: same name twice, `$parentText` under colliding parents, the second region's error string, a free leaving a same-named sibling's name.

Fixes #939

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:17:03 +07:00
Sam SungandClaude Opus 5.5 73623e31fa Hover walk: no arg1, held during a move, no fling, published mouseover (#941)
Four pointer defects: OnEnter and OnLeave set `arg1 = true`; a frame being moved or sized still ran the hover walk; the pointer leaving the window fed `f32::MIN` to a move in flight, flinging the frame off-screen; and the mouseover token read the world pick, not the published mouseover.

In 1.12.1 both scripts run through `0x702690`, which sets no arg global. While `[root+0xcfc]` holds a move or size target, the walk pumps it (`0x7655b0`) and returns before any probe (`0x766164`–`0x7661a0`); a hide clears that slot (`0x76492e`). The pump only sees real cursor events. The token resolver reads the published pair `[0xb4e2c8]` (`0x515970`).

OnEnter and OnLeave fire with no arguments, `mouse_move` pumps a move or size and returns, a hide ends it, the pointer leave has its own call, and the aura feed reads the published mouseover.

Tests bite on the parent for each.

Fixes #932
Fixes #933
Fixes #936
Fixes #938

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:08:29 +07:00
Sam SungandClaude Opus 5.5 0fc40d3f87 An XML state texture installs a fresh region, as in 1.12.1 (#940)
An XML state texture on an instance over a template went through the Lua path leg, which reuses the populated region, so the template's TexCoords, size, tint, blend and anchors stayed on the new art.

In 1.12.1 `Button::LoadXML` (`0x7788c0`) builds a fresh region for each state tag through the `<Layers>` adder (`0x6f26f0`) and hands it to the slot store (`0x778fd0`, `0x779110`; the checked pair `0x785330`, `0x785440`), which destroys the region it held. A fresh region has TexCoords 0..1 and BLEND (`0x76fc40`, `0x76fc64`), and an XML highlight keeps its own blend (`0x7789a4`).

The XML leg now builds the region as a `<Layers>` texture and installs it, freeing the old one; the Lua leg is unchanged. An XML highlight with no `alphaMode` now draws BLEND.

Tests: all six tags on a CheckButton template, the name kept, the Lua leg's region kept.

Fixes #875

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:59:14 +07:00
Sam SungandClaude Opus 5.5 3bfec89085 A template name registers once, case-folded, as in 1.12.1 (#937)
A FrameXML template registered under a name already taken replaced the first, nothing was logged, and names differing only in case were stored twice.

In 1.12.1 the registrar `0x6ee500` folds case (`0x64b3f0`, `0x64a4c0`) and keeps the first entry, logging "Virtual object named %s already exists" (`0x871188`), unless the old entry carries a non-zero script context and the new one zero (`0x6ee594`). The stock files load at context 0; `AddOn_Load` (`0x51f240`) loads an addon under its name unless its signature checked SECURE (`0x51dbfb`).

Templates are now keyed case-folded with their context, an addon's load sets the context, an addon counts as secure only from the player's own data with `## Secure: 1`, and the refusal and the unnamed-node line (`0x871020`) reach FrameXML.log.

Tests: duplicates and case variants refused, the context rule, the log lines.

Fixes #556

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:36:57 +07:00
Sam SungandClaude Opus 5.5 6fff0fa7a1 Hover leaves publish the world mouseover loss before the next OnEnter (#935)
Moving from the world onto a window, the world mouseover's loss reached the tooltip a frame after the window's OnEnter, so a gate kept the fade off any tooltip the world had not put up.

In 1.12.1 the hover walk runs the old frame's leave before the new frame's enter (`0x766229`, `0x766239`). The WorldFrame's leave (`0x483b10`) runs OnLeave, then publishes the empty mouseover, whose publisher fades GameTooltip whoever holds it (`0x492890` → `0x530ae0`). A nameplate's leave publishes first (`0x7cb8b0`); the Minimap's hides the tooltip (`0x4eb880`), and its probe drops a held blip on a miss (`0x4eb0c0`).

The VM now holds the published mouseover and runs these leaves in the walk, the hide and every mouse disable, freelook's plates included (`0x60f830`); the ownership gate is gone.

Tests: OnEnter no longer sees "mouseover", an addon tooltip fades on a change, blip and Minimap leaves hide it.

Fixes #901

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:28:42 +07:00
Sam SungandClaude Opus 5.5 377542daeb One anchor slot per point; SetPoint skips an unchanged anchor (#934)
Anchors were a list: SetPoint re-applied an unchanged anchor, GetPoint counted in the order points were set and answered five nils past the last, and the cursor tooltip replaced every anchor each frame.

In 1.12.1 a frame holds one anchor per point. SetPoint (`0x767c70`) returns at `0x767d5e` with no store and no layout queue when that point's anchor has the same target and relative point and both offsets differ by under 2⁻²² layout units (`[0x8029d4]`). GetPoint (`0x7a2340`) walks the nine slots in point order and pushes nothing past the last (`0x7a240d`). The `ANCHOR_CURSOR` update sets BOTTOM alone (`0x530b20`).

Anchors are now nine slots, and SetPoint, GetPoint and the cursor update follow those rules.

Tests: an unchanged re-set and an in-threshold offset touch no layout, GetPoint order and indexes, a cursor tooltip keeps an addon's second point.

Fixes #926
Fixes #131
Fixes #879

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:19:11 +07:00
Sam SungandClaude Sonnet 5.5 af281a05f7 A drag starts at 1% of the screen diagonal, as in 1.12.1 (#931)
A drag started past a fixed 4 pixels from the press. In 1.12.1 the threshold is 0.01 layout units, where the screen diagonal is 1: `0x76b300` stores `0.01f × 0.01f` (`0x81c468`) as an f32, and `0x76b320` starts the drag when the squared distance from the press point is not below it, comparing at x87 double precision. The converters `0x425610`/`0x425710` scale the cursor into those units (`0x41ad80`).

The drag test now divides both points by the layout unit and compares the squared distance in f64 against the f32 threshold. At 1024×768 a drag needs 12.8 px, at 1920×1080 about 22 px; the UI scale does not change it.

Tests: just under and over 1% of the diagonal, 12 and 13 px at 1024×768, the exact f32 boundary, two screen sizes and three UI scales.

Fixes #157

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 02:51:42 +07:00
Sam SungandClaude Sonnet 5.5 a2825a185f A second button pressed mid-drag re-arms or keeps it, as in 1.12.1 (#930)
Any press during a drag abandoned it with `OnDragStop`, so touching the right button while dragging an item or a window with the left stopped the drag.

In the reference, each press on the capture runs the drag arm: a button in the frame's drag mask re-arms it, started back to 0, with no `OnDragStop`, and any other button leaves the drag running. The release of a button in the mask ends a started drag with `OnDragStop`, or disarms an unstarted one. `OnDragStart` names the lowest held button.

Presses now re-arm or leave the drag as the reference does, the release tests the released button against the mask, and `OnDragStart` names the lowest held button. A release the client lost still stops the drag.

Tests cover a right press on a left-only drag, a re-arm, the first release after it, an item drag and the start's button.

Fixes #924

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 02:33:54 +07:00
Sam SungandClaude Opus 5.5 56f7fa003a A resolve keeps a rect that moved under 1.12.1's 1e-5 threshold (#929)
Every resolve stored the rect it computed, however small the move, so float noise in anchor chains moved rects, hit edges and size-change notifications where the reference keeps the old ones.

In the reference, the layout drain compares each of a node's four new edges with the rect it last stored. If every edge moved less than 1e-5 in its units (where the screen diagonal is 1), it keeps the stored rect and skips the store, its dependents and `ApplyRect`. A scale change, a new font or new text clears the resolved bit, so the next resolve stores without the test.

A resolve now keeps a frame's or region's rect under that threshold, scaled by the layout unit, and a scale change or a new measure stores the exact rect.

Tests cover jitter, a move just over, slow drift, a region and the forced stores.

Fixes #919

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