An edit box always drew its text left-aligned: `SetJustifyH("RIGHT")` or `("CENTER")` moved only the `OnCursorChanged` x.
In 1.12.1 the box's `SetJustifyH` writes the box's own font (`0x797a17`, `+0x324`) through the shared setter `0x79fc20`, which pushes the change to the label linked to that font (`0x779c27`). `SetMultiLine` cuts off only the label's vertical bits (`0x77a509`, `0x77a599`), so the horizontal justify draws.
The box's font now exists with its label, justified `LEFT` as the constructor leaves it (`0x779bd2`). The four justify methods act on that font, the label no longer pins H, and the caret reads the label's justify. The separate justify word on the box is gone.
Test: `the_text_draws_by_the_boxs_justify_h_and_its_own_justify_v` checks the drawn H follows `SetJustifyH` while V stays the box's own. Without the fix, RIGHT draws LEFT.
Fixes#2977🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ipairs(t)` answered nil once a script had cleared the global `ipairs`, a collection had run, and the global was set again.
The reference reads the global `ipairs` raw as the loop's iterator (`0x702d15`), by name. We read it by a string id interned once at start, but the collector's root list of those names was kept by hand and left `ipairs` out. A collection could free the string, and setting the global again interned it under a new id.
`Names::all` now lists every field through an exhaustive pattern, and the collector roots that list, so a name added later does not compile until it is rooted.
Test: `ipairs_finds_its_global_after_it_is_cleared_and_collected` runs the issue's sequence. It answers nil without the fix and the function with it.
Fixes#2974🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A localized client tagged its own race's speech with a language header, so Say showed `[Common]` on a zhCN client.
The stock chat frame draws the header when the line's language name differs from `GetDefaultLanguage()` (`ChatFrame.lua:1442`). Both names come from `Languages.dbc`, in the locale's column (benilla-re `systems/ui/chatlanguage.md`). We answered `GetDefaultLanguage()` from the DBC but passed each line's language from a hard-coded English table, so the two never matched outside enUS.
The line's language is now named from the same `Languages.dbc` rows, and the English table is gone.
Test: `a_line_names_its_language_in_the_locale` gives a monster line language 7 with a zhCN row and checks `arg3` is that name; without the fix it reads `Common`.
Fixes#2996🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping a bag item on the ground left the camera in mouse-look, with the pointer hidden, while the destroy confirmation stood.
The world drop marked its release as the interface's, so the world frame's binding never ran its up-half and `CameraOrSelectOrMoveStop` never ended the session. In 1.12.1 the drop happens inside that release: `0x514b70` ends mouse-look (`0x514c48`), then dispatches the click (`0x481f60(1)`), whose ground or nothing leg raises the confirmation. The release is the world frame's (`0x483cb0`).
The drop no longer claims the release. A press the drop takes still arms no select, so nothing else clicks.
Test: holding an item, a left click on the world runs the binding's up-half; it fails without the fix. The world-drop tests now expect the release to be the world's.
Diagnosed in #2993.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: hyrw <47274436+hyrw@users.noreply.github.com>
M2 translucent batches sorted by their own centre with a file-order bias, so a glow could draw under the part it lights.
1.12.1's transparent order `0x70ae10` compares the instance's squared distance, the fill copy, the signed priority plane (texture unit `+0x02`), the vertex shader under the default mode, the batch depth, then the unit's layer (`+0x0c`). A model frame's scene skips depth fill (`0x76ccf5`). benilla now reads both unit words and re-sorts the M2 batches in that order, merged with the other transparents by depth. Texture handles are stood in for by file order, a recorded deviation.
Mount children are no longer grounded or swum as free units (`0x607a00`), and worn items take their idle sequence, or the first one, as `0x710153` arms it.
Tests use the install's HangingSkullLight and a staff whose sheen orders by shader.
Fixes#2922Fixes#2931Fixes#1208Fixes#327🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A body placed flush on a slope at a tile seam fell through the far tile: the backface band tested a facet's raw vertices, so it dropped the far floor once the body had passed part of it.
1.12.1 clips each facet into a leading face's swept prism of its own volume (`0x632700`, `0x6318c0`). It keeps the facet when a clipped vertex is ahead of −2^-20 (`0x6328a1`), or within 1/36 behind after a second clip (`0x632910`). The band now builds that volume at the capsule's feet (`0x631440`, `0x631be0`) and runs those steps; it matches benilla-re's tested port on 399,550 random cases. A facet ahead is held at any distance, recorded as a deviation, since the capsule bulges past the volume.
A real-terrain test walks a body flush on the Valley of Trials ramp over the seam: main sinks it 6.48 yd, the fix holds it.
Fixes#1121Fixes#2958🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla found a tile's chunks by walking record sizes, so a Noggit export whose MCNK sizes count their header lost every chunk after the first. 1.12.1 reads the tile through MHDR's offsets (`0x6c2010`) and each chunk from its MCIN entry's file offset (`0x6c299d`), never an MCNK's size. benilla now does too.
Two more reads follow the reference. With MCNK flag 0x8000 an alpha map decodes whole at 64×64 (`0x6b0a6b`, `0x6b0ac0`), and without it at 63×63 with the edge copied. Doodads and WMOs are made only from the chunks' MCRF refs (`0x6b00f0`), so an entry no chunk references never appears. The static item and terrain shaders now sample in uniform control flow, which WGSL requires, and a lint holds them to it.
Every chunk of 2,380 install tiles parses identically before and after. Each fix has a test that fails without it.
Fixes#2981Fixes#16Fixes#21Fixes#613🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A release bump changes only `[workspace.package] version` in `Cargo.toml` and the lock's entries for the workspace's own crates. The gates' memo counted only docs as unread, so the 0.3.0 bump would have rerun the whole chain on a tree whose parent had just passed it.
No gate reads the version: it reaches only the launchers' `env!("CARGO_PKG_VERSION")`, and the build-id tests pass their own strings.
`docs_only_delta` now also keeps the verdict when `Cargo.toml` and `Cargo.lock` differ in nothing but those version lines, compared with each file's workspace versions blanked. Checked on history: the 0.3.0 bump keeps the verdict; a code commit, a dependency version in the lock, a changed edition, and the bump together with its parent's code each rerun.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes#2980
A glue scene's seat read only a rotation and scale parked on global-sequence keys, once, when the scene was built. That lost a seat keyed in the scene's sequence: a custom `UI_CharacterSelect` turns its seat bone 180° and scales it 0.673 there, so the body stood backwards at about 1.5×. No stock scene keys its seat that way.
The reference seats a child at the bone's whole animated matrix times the attachment position, every frame (`0x7140d8`-`0x714149`), after it animates the scene (`0x714000`). The body and the pet now take that matrix from the scene rig's pose each frame. This runs in `PostUpdate` after the rig's model pass, now the named set `RigModelPass`, and before propagation, so the seat and the scene share a frame.
Test: `a_seat_takes_the_bones_whole_matrix_at_the_attachment`.
🤖 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>
Fixes#2978
A Wayland trackpad sends pixel travel. benilla asked 100 physical pixels for one wheel step and gave at most one step per event, so at scale 1.33 a list needed about 7.5 times GNOME's notch of finger travel.
10 axis units are one step on Wayland (SDL's `WAYLAND_WHEEL_AXIS_UNIT`, GLFW, Xwayland, mutter's `DISCRETE_SCROLL_STEP`). On Linux a notch is now 10 logical pixels at the window's base scale, and an event steps once per whole notch. macOS keeps Bevy's factor and its one step per event. Line events stay at one step each, like the reference's `WM_MOUSEWHEEL` (`0x76c1a9`), and winit reports only lines on Windows and X11. The `WheelNotches` deviation says so.
Tests: `pixel_travel_steps_by_the_platforms_notch` runs both platforms' arms on any host, and `a_pixel_notch_reads_the_base_scale` pins the scale.
🤖 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>
Under Proton-based Wine, `tabtip.exe` is a UI Automation client, and `bevy_winit` gives every window an AccessKit provider. That traffic faults in `rpcrt4` on blocks the game has reused, which damages the heap and later crashes the client at varying addresses. Blocking `tabtip.exe` stops the faults and the crash, and other programs in the same bottle show no such faults.
The reference's window has no provider: `WoW.exe` imports no accessibility library and names none of its calls. benilla builds no AccessKit tree either.
`VideoPlugin` now drops the adapter when Windows reports a new window, which restores winit's window procedure. Other platforms keep theirs.
No test: the adapter needs a real window and the fault a UI Automation client. Built for macOS and Windows.
Refs #2932🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README named kira as the only vendored code under `third_party/`, but wgpu-hal is vendored there too, under the same two licenses with its notices beside it. `third_party/alsa-0.9` stays unnamed: it is benilla's own stand-in, with no upstream code (its `BENILLA.md`).
Fixes#2972
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workspace version and its lock move to 0.3.0, and nothing else. The toolchain pin stays at 1.98.1, the compiler these checks ran on.
The Linux and Windows compile, the Vulkan run and the smoke run passed on this tree. The full gates passed on its parent (#2968) and were not rerun for the version change, at the maintainer's word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`docs/DEVIATIONS.md` never said why the login screen has a realmlist control: the reason sat in a test comment, and the list leaves tests out. It is now a `Deviation:` on the control, in `AccountLoginRealmlist.xml`. The host-name port's reason, "everyday use", said nothing, so it now names the case and why it's safe: an address 1.12.1 can dial means the same here.
Comments only.
Refs #2860
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server whose changes are all in MPQ archives plays: benilla reads the install's patch chain (`crates/benilla-formats/src/chain.rs:41`), and a client change outside the archives is what it can't follow. The status line now says that.
Refs #2863
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SetView` snapped pitch and yaw, `FlipCameraYaw` was undone by the auto-follow, and a spyglass zoom left the camera at distance 0.
`SetView` blends all three by `cameraViewBlendStyle` (`0x512eb4`), and a new camera counts as moved (`0x50c400`). The flip sits outside the follow's yaw offset. The spyglass's start and end select their views, a held scope refuses, and arrival ends it (`0x5deb44`). Freelook's entry and leave stop a view's yaw and bias (`0x50f97e`, `0x50f984`).
A `/reload` builds a new camera (`0x481b4e`, `0x50c0e0`): offset, flip, moves and shakes start over, and the views reread their settings (`0x50c3db`).
Each has a test.
Fixes#1697Fixes#1699Fixes#1706Fixes#2948
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Any 1.12.1 server" overclaimed: a server that enforces Warden disconnects the session, as "Running it" says, and one that needs its own client changes won't work. The status line now says so.
Refs #2863
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The select screen looped one Stand clip and never showed a guild's emblem. Select models now replay Stand with a fresh pick at each window's end, and a guild tabard asks for its guild and wears the emblem.
An interface teardown kept every store's pending callbacks, so an answer after a `/reload` raised events in the new VM. The reference frees them unrun (`0x490c35`, `0x5556c0`, `0x5615b0`), and the score board's count stands until a packet zeroes it (`0x4aabb5`). What would strand the player stays or ends cleanly, recorded as a deviation.
Tests cover the item, name and guild stores through a real `/reload`, the board's hold, the tabard and the replay.
Fixes#1314Fixes#1316Fixes#2946
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Casting, item use and targeting differed from 1.12.1 in eight places.
A ready cast leaves the wand's repeat running (`0x609570`), and a local refusal of the repeat spell stops it (`0x6e1cd9`). A broken use item is refused and one not worn must be equipped (`0x5d8fd6`). An item with no use spell leaves silently, ahead of the form and disarm refusals (`0x5d8fc1`).
TAB reads `targetNearestDistance` at each press (`0x494350`). TargetLastEnemy's memory is stamped as the select makes it (`0x49372f`). The minimap and world map arrows take the facing of the object the camera follows (`0x4eb07e`, `0x4a7c7c`).
Each has a test.
Fixes#1197Fixes#1198Fixes#2482Fixes#2487Fixes#2591Fixes#2614Fixes#2945Fixes#2947
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven small differences from 1.12.1, in the log, tooltips and screenshots.
Combat log: a line needs its target held, through the gate at `0x626630`; environmental damage and four spell lines need a held unit (`0x5e8145`, `0x5e8178`, `0x5e82a8`); the enchantment log reads its owner first (`0x628f23`).
Item tooltips and trade skills walk ChrRaces and ChrClasses at the install's locale (`0x52ce14`, `0x4fea2e`) in place of English literals. The lockout warning divides signed and keeps an empty map name. A screenshot is the frame of its ask (`0x442a10`, `0x442350`).
Each has a test against those addresses.
Fixes#2078Fixes#2384Fixes#2480Fixes#2485Fixes#2920Fixes#2943Fixes#2944
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Complete" overclaimed, and "Addons, options" did not say what is left. The status now says the client is fully playable, what the long tail is, and that addon compatibility, the remaining options and performance are ongoing.
Fixes#2863
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A damaged file or a malformed list took the client down, or was shown as it came.
Files: a DXT width of 0, a zero-sided BLP, an M2 under 0x11c bytes, a first sequence that ends before it starts and a large geoset group each panicked. A DXT level past the file failed the whole texture, where `0x5a8558` tests its size for zero alone. Geoset ids are 32-bit (`0x477592`, `0x47869a`).
Lists: the realm list's size and string caps are checked (`0x5ba9e7`, `0x5b22d9`), a character list over 10 or unread to its end fails (`0x5b42a0`), and a failed re-dial empties the list (`0x5b2560`).
Each has a test.
Found in Thorsten42/benilla@af86636a54Fixes#7Fixes#10Fixes#14Fixes#67Fixes#1660Fixes#1661Fixes#2941Fixes#2942
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #2429 the UI's input records are read from the window's `WindowEvent` stream. The layout remap (#602) rewrote only the typed `KeyboardInput` messages, so a key a layout remaps, such as Caps Lock under X11's `caps:escape`, reached the bindings and text boxes as its physical key again.
1.12.1 reads a key as the virtual-key code the OS layout produced (`0x42d21e` → `0x42d800`), so Caps under `caps:escape` is Escape.
`remap` now rewrites the `KeyboardInput` a `WindowEvent` carries by the same rule, in `PreUpdate`, before the records are collected in `Update`.
Test: `both_streams_carry_the_layout_key`, both streams come out as Escape.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The Windows main-thread reserve was set only in `crates/benilla/build.rs`. So `benilla-worldview`, and every launcher built on benilla by the recipe in `extended_launcher.rs`, linked with the linker's default: 1 MB on MSVC, 2 MB on mingw. That is less than the login screen needs.
`benilla_buildstamp::emit()`, which every launcher's build script already calls, now asks for 8 MB through `rustc-link-arg`: `/STACK` on MSVC, `--stack` on mingw. Unlike `rustc-link-arg-bins`, cargo accepts it from a package with no binary, so a library that calls `emit()` still builds. The recipe and CONTRIBUTING's "Building on top" say so.
Test, mingw cross-build: `SizeOfStackReserve` is 0x800000 for `benilla.exe` (unchanged) and `benilla-worldview.exe` (was 0x200000). A library-only crate calling `emit()` checks clean on MSVC and builds on mingw.
Fixes#2934🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Off macOS, the cursor chain #2923 added ran the pointer tracking and the cursor driver unordered against the VM pass. The macOS chain orders them after it, since a Lua call in the pass can move the camera rig whose look the cursor shows. Linux's Update schedule gained 40 undeclared orders (1507 against the ceiling's 1467), so the Linux gates failed on every pull request.
The non-macOS chain now runs `track_pointer_device` and `other::drive` after `VmPass::End`, as macOS does.
Test: the schedule ceiling test passes on Linux (aarch64, in a container) and on macOS.
Fixes#2938🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A building's pool drew forever once its room had been visited, and a portal-less building with an area id never drew its pool, Northshire Abbey's wall fountain among them. In 1.12.1 the flood's visit `0x685d70` links a visited group into the liquid list (`0x685f6b`, the only link), and the liquid pass `0x684cd0` unlinks each record as it draws (`0x684e1d`). So a pool draws exactly in the frames its group is visited. The latch is gone, and the fountain draws. Across 184,624 camera poses around the Great Forge, benilla's flood keeps the lava wherever the reference's portal graph does.
A pool's liquid type read every tile row and the group's override. `0x6ba970` scans only the first row and takes the first wet nibble, and its caller never reads the override. benilla now does the same.
`WOW_LIQUID_TRACE=1` prints each pool's per-frame verdict.
Fixes#119Fixes#65Fixes#2492🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Units, players, creatures and game objects in sunlight were lit at ×1.0, where 1.12.1 lights them at ×2.5. The cap `min(I, 1)` was recorded as a deviation on the premise that lifting it takes sun-facing surfaces past 1.0. That premise is false: the per-vertex sum is clamped in both clients.
In 1.12.1 a node is created at intensity 1.0, and its first sync sets the target: 2.5 lit, 0.5 in the chunk's shadow, 1.0 as an interior (`0x670db0`, `0x69e280`). Each tick steps toward the target at 3.3333 × the frame time (`0x69e770`), so a unit appears at 1.0 and ramps to 2.5 over about 0.45 s. The intensity reaches the SH unclamped (`0x71c4e0`).
benilla now does the same, in the entity shader and the static one, and the two deviation notes are gone.
Tests: the ramp from 1.0, the three targets, the uncapped shader line.
Fixes#506🤖 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>
Twelve workspace tests failed on Windows, from three causes:
- The census cut a site's path at `src/`, so a Windows path kept its whole prefix. It now cuts at either separator and prints `/`.
- The raise-origin panic named its caller from the backtrace, which a build without debug info resolves wrongly. It names the raise by its line, and lists callers only from frames that carry one.
- Ten zone tests set `TZ` through `std::env`, which never reaches the Windows C runtime's own copy of the environment. In 1.12.1, Lua's `date` and the screenshot names read the zone through the CRT's `_tzset`, which honours `TZ` (`0x4145d6`). The logon bias reads `GetTimeZoneInformation` alone (`0x4297c0`). The test helper now writes `TZ` where the CRT reads it, and the logon bias calls `GetTimeZoneInformation` directly, as the reference does.
Cross-checked for `x86_64-pc-windows-gnu`.
Fixes#2927Fixes#2928Fixes#2929🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three gaps:
- The world camera's vertical field of view was a fixed 45°. 1.12.1's camera holds π/2 (`0x50a707`), and its projection takes the vertical half-angle as fov / (2·√(aspect² + 1)) (`0x5c3cc0`). That gives 54.0° at 4:3, 47.7° at 16:10, 44.1° at 16:9 and 35.0° at 21:9. benilla now derives it from the viewport's aspect and re-derives it on a resize. The spyglass keeps its 6× narrowing.
- A unit, item, effect or doodad whose model file is missing drew nothing. `CM2Scene::CreateModel` (`0x707350`), which every lane creates through, opens `Spells\ErrorCube` instead (`0x70737f`). benilla's asset readers now do the same for a missing `.m2`.
- `view::msaa_from_env` was dead code that defaulted to 4x, and is deleted.
Tests: the field of view at each aspect, and `CreatureDisplayInfo` 4 (`CrystalSpider.mdx`, absent) loading the cube.
Fixes#124Fixes#330Fixes#129🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Three gaps:
- CMSG_ITEM_TEXT_QUERY sent the text id, the mail id and a u32 0. `0x569980` writes the text id and a u64 key (`0x569a99`): the mail id under high dword 0x70000000, an item's guid, or 0 for the invoice. benilla now sends that key. vmangos reads only the first dword after the text id, which is unchanged.
- A group list and a deactivate asked for stats once per member. `0x4e85d0` and `0x4badc0` each send their own request, party first, so a subgroup mate is asked twice. benilla now runs both legs.
- `/console repeat N <command>` (`0x639250`) was missing. It now runs the command once a frame through the console executor and counts down (`0x63bd50`).
Tests: goldens for the three keys, the request count and order, the repeat count.
Fixes#2601Fixes#2604Fixes#2587🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Three wire gaps:
- An area trigger sent CMSG_AREATRIGGER alone and at any health. `0x5e2110` runs only while health is above 0 (`0x5e215e`). On entry it sends a heartbeat from the live pose (`0x5e221e`), then the trigger (`0x5e225d`), so the server checks the trigger against a fresh position. benilla now does both.
- A landed swing on a far victim kept auto-attack on. `0x5ea800` sends CMSG_ATTACKSTOP when the 3D distance is at or beyond 4 × max(both combat reaches + 4/3, 5) (`0x5ea899`, `0x5ea8a8`), as after a knockback or a blink. benilla now does the same.
- An ignored GM was silenced, and every ignored whisper drew CMSG_CHAT_IGNORED. `0x49d736` drops an ignored sender only under a non-GM tag, and replies only to such a whisper. benilla now does the same.
Tests: the heartbeat-then-trigger order, the reach boundary, and the tag and type matrix.
Fixes#2595Fixes#2598Fixes#2603🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two gaps against 1.12.1:
- A shapeshifted player's item use went to the server and could ask to bind the item first. In 1.12.1 `0x5d9105` refuses it when the form is no stance (`0x60e9f0`) and its `SpellShapeshiftForm` flags lack 0x8, or carry 0x40 and the item is not equippable. It shows ERR_NO_ITEMS_WHILE_SHAPESHIFTED and sends nothing, before the bind ask. So cat, bear, travel and aquatic form, and ghost wolf, allow only equippable items, Tree of Life none, and stances all. benilla now refuses the same way.
- A helm's model took its race prefix from an eight-entry table clamped to Troll. `0x4799a0` reads the `ChrRaces` row's client prefix and attaches no helm for a race with no row. benilla now does the same, so a goblin's helm loads `_Gb`.
Tests: the route order, the stock forms and the stock race prefixes.
Fixes#2477Fixes#611🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two UI gaps:
- The pointer stayed hidden while any of mouselook, a playing cinematic or the glue's `HideCursor` held. In 1.12.1 the pointer is one device flag (`[dev+0x2880]`, `0x595c20`), and the last call of `0x58b590` wins. Its eight sites are the glue's `ShowCursor`/`HideCursor`, a cinematic's start and end, leaving the world, mouselook's start and end, and the glue build's `SetCursor`. benilla now keeps that one flag, set by each site in order. So releasing mouselook during a cinematic shows the pointer.
- Merging one load status into another copied lines verbatim. `0x41a100` re-adds each line through the target's Add with its text as the format, so a `%%` collapses, a line over 255 bytes is cut and an empty one is dropped. The merge now does the same.
Tests: `the_last_call_on_the_device_decides`, `a_merge_formats_each_line_again`. Built for Windows too.
Fixes#2575Fixes#2621🤖 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>
`the_loose_source_falls_through_per_candidate` made a file unreadable through `std::os::unix`'s `PermissionsExt` with no `cfg(unix)`, so the Windows test build failed and no test ran there.
The permission calls are now unix-only. Elsewhere the file stays readable, and the test already skips its second assertion in that case. A cross-check of every workspace test for `x86_64-pc-windows-gnu` finds no other break.
Fixes#2916🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two console commands 1.12.1 registers were missing, so each ended in `Unknown command`.
- `gxRestart` (`0x63b360`, handler `0x639f60`, category 1, no help) applies the latched gx CVars. A console gx write printed `set pending gxRestart` and then never applied. It now raises the same restart `RestartGx()` does.
- `bind` (`0x4b6050`, handler `0x4b60b0`, category 5, no help) takes the key as the first space-split token, cut at 31 bytes, and binds the rest of the line on the live set with no `UPDATE_BINDINGS`. An empty rest unbinds.
Both are listed by `help`, which answers them with "No help yet".
Tests: `help_knows_gx_restart_and_bind`, plus `ConsoleExec` tests for each command.
Fixes#2585Fixes#2586🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two archive-chain gaps:
- The MPQ reader decoded a shrunken sector under block flags `0x300`. The one sector reader in 1.12.1 (`0x651450`) masks the flags with `0xff00` and reads such a sector only when the result is exactly `0x200` (`0x651993`-`0x65199f`). Any other codec flags fail the read. benilla now fails it the same way. A sector that did not shrink is still copied under any flags (`0x651982`).
- A base archive missing from `Data` was mounted around. In 1.12.1 the first base archive that will not open, `model` first, ends the client with `Failed to open archive %s: %s` (`0x4038ef`). benilla now stops at mount and names it in the existing fatal dialog. `speech2.MPQ` and the patches stay optional. The no-install note's premise, that the reference goes on past a missing archive, is corrected.
Tests: `a_shrunken_sector_reads_only_under_exactly_the_compress_flag`, `a_missing_base_archive_is_an_error_naming_it`.
Fixes#2716Fixes#2616🤖 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>
Three wire and unit-state gaps:
- An AUTH_OK or queue reply with a 5 to 8 byte billing tail failed the world connect. `0x5b41b0` reads each field through `GetUInt32` (`0x418eb0`), which writes nothing on a short read, and admits the player anyway. A short field now keeps its old value. Test: every tail length from 0 to 9.
- `SMSG_GAMEOBJECT_RESET_STATE` (`0x6e9790`) was unhandled. An object now re-applies its descriptor state, undoing the open-lock and loot predictions. Test: a byte-exact golden test of the body and a handler test.
- `IsSelfControlled` (`0x5fa550`) now reads flags 0x2 and 0x1, the charmer, and possession, which answers with the charmer's or creator's own reading. A moving charmed player's emote is no longer refused.
Fixes#2681Fixes#2582Fixes#2682🤖 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>
1.12.1 runs a signalled event's handlers inline, before the verb that raised it goes on. The census that refuses a send, lock or cursor write made while such a handler is still held now panics by default in a debug build (`BENILLA_SEND_CENSUS_PANIC=0` turns it off), after a survey of the whole suite came back clean.
The survey found one real ordering defect: `OpenItem` sent `CMSG_OPEN_ITEM` before its lock's `ITEM_LOCK_CHANGED` listeners ran. In the reference the lock signals at `0x495415`, before the send at `0x5edd26`. The send is now owed past them, as the loot release is.
Two cells, the used mailbox and the item-text reader's queue, were marked written by a use that touched neither, and now are not. The survey's listener frame is hidden, so it no longer moves frame levels.
Tests: `an_open_items_lock_listeners_run_before_its_send`, `an_item_use_that_opens_no_window_writes_none_after_its_held_report`.
Fixes#2271🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A spell effect whose texture transform has per-sequence tracks resolved its UV loop by the playing slot, then fell back to the slot-0 loop when that slot had none. So in a sequence with no track it kept scrolling.
In 1.12.1 a transform animates by the sequence the model plays, and a sequence with no keys holds still. The per-placement lane and stage 1 already do this.
The effect lane now takes the playing slot's loop when per-slot loops exist, and the single loop only when they do not. With no player yet it reads slot 0, as before. The probe's resolver does the same.
Test: `a_dead_slot_holds_an_effect_still`, a two-slot effect with a dead slot 1.
Fixes#2909🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The debug-build check on where an event is raised passed an allow-listed site by finding its function's name in a captured backtrace. CI builds tests without debug info, and on x86_64 an allowed function inlined away leaves no frame. So #2900's Linux run failed 368 tests: 367 on `minimap::feed_minimap_inside`, 1 on `drive_glue_mgr`. Apple silicon keeps those frames, which is why the Mac gates stayed green.
Each of the 13 allowed sites now opens `Origin::Allowed("<site>")` around the part that raises, and the check reads only the open scope. The backtrace is kept only to name callers in a failure.
Tested:
- `an_allowed_site_passes_by_its_scope_never_by_the_stack` fails under the old verdict;
- a guard ties every allow row to a scope in the function it names, and every scope to a row.
Fixes#2901🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla's Mac key-handling comments cited the 1.12.1 Mac client as `0x5bf320` and `0x8a4aa`. The first resolved to an unrelated `WoW.exe` function, and nothing documented the second. benilla-re now publishes the Mac client's key input with `mac:` addresses (`systems/core/mackeys.md`), and every citation points there; `cited` finds all 15 backed.
Read against it:
- Caps Lock queues nothing on macOS, as the Mac client's modifier handler tests only Shift, Control and Option (`mac:08a06c`).
- F17-F20 are named by the OS layout there, so benilla's names are noted as its own.
- Deviation: Z keeps its capital name, where the Mac client's fill uppercases only a-y (`mac:08a3df`), because the fault leaves the stock sheathe binding unreachable.
Tested: Caps Lock is a key only off the Mac.
Fixes#2895🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A two-texture M2 batch drew its second stage only when it was sphere-mapped, and a lookup past its table was taken as generated or untransformed.
- **#1831.** The stage's coordinate and transform lookups are unchecked (`0x70b8bd`, `0x70b8aa`). Past its table each reads the file's next u16, and benilla now does the same. A transform index at or above the count gives identity (`0x70b90b`).
- **#1832.** Every lit `Model2.bls` program passes `texcoord[k]` to stage k, and vertex format 12 carries set 1 at `+0x28` (`0x71de2d`). So stage 1 reads UV set 1, through its own transform, rotation and scale included (`0x70b91f`). This applies in every M2 lane and the skybox.
No stock model changes, since set 1 is zero in every stock vertex. Custom and HD models draw their masks.
Tests cover the past-table reads, the set-1 parse and the slot rules.
Found in Adrinalin4ik/manila.
Fixes#1831Fixes#1832🤖 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>
Four registered CVars had no effect, a default was off, and eight glue verbs were missing.
- `shadowLOD` 0 draws no unit blob shadow (`0x6d7904`). Its handler refuses values outside 0..1 (`0x6d8450`).
- `shadowBias` scales the ring's and blob shadow's depth bias by value/0.1, with none at 0 (`0x6d7506`).
- `violenceLevel` drives the blood spurt (`0x60a39d`), clamped to the locale's cap (`0x6c5af0`).
- `MapWaterSounds` 0 hard-stops the liquid loops and arms none (`0x462af0`, `0x462b50`).
- `mouseSpeed` registers "1.0", the OS speed × 0.1 as `%1.1f` (`0x402c4d`).
- The glue registers the billing, patch download, subtitle and survey verbs, with the reference's answers when no such state exists. `SurveyNotificationDone` reports the login authenticated, as EndSurvey does (`0x473ef5`).
Each fix has a unit test, and the glue surface test lists no verb as unbuilt.
Fixes#346Fixes#361Fixes#409Fixes#344Fixes#2592Fixes#2607🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model textures loaded by their literal name, and BLP mip chains stopped one level short.
- **#610:** a model texture resolves as `TextureCreate` (`0x449d90`) resolves it. A three-character extension is stripped (`0x449590`), then `.blp` and `.tga` are tried in the reference's order, and a `.tga` the size gate refuses (`0x448450`) falls to the next. This covers M2, WMO, clutter, skybox and baked NPC textures. A `.tga` gets its full chain from `_mipN.tga` files, then box-filtered (`0x4491f0`); one that fails to decode is opaque white (`0x44a2eb`). Found in Adrinalin4ik/manila.
- **#1680:** a BLP keeps its 1x1 level (`0x5a4ac0`), and ground layers take 9 levels.
- Bake, hair and extra-skin textures clamp on both axes (`0x4795ed`, `0x4782a5`, `0x4781dd`).
Tests run the loader and the readers over in-memory sources: the candidate order, the gate's fall-through, the mip chains and the white fallback.
Fixes#610Fixes#1680🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1.12.1 lights models and buildings per vertex and interpolates the result. benilla lit them per pixel, and clamped the light sum before the material colour multiplied it.
- **#509:** M2 lighting (the `Model2.bls` vertex programs under the stock `M2UseShaders` 1, `M2UsePixelShaders` 0) and WMO exterior T&L now run in the vertex stage.
- **#530:** the vertex and model colour is the lit material inside the one clamp. For WMO this is MOCV, through GL colour material (`0x59cfec`, `0x59cff3`). For M2 it is `c28`, the model colour times the batch's M2Color (`0x70c4d3`-`0x70c712`). The programs clamp E first only when a point light is bound.
- **#115:** the clutter grey (`0xffc0c0c0`, `0x6b2615`) sits inside the clamp.
Tests:
- an interpreter runs `Model2.bls` programs 135 and 136 against the shader's mirror;
- both program levels pin the clamp law per light count;
- per-vertex and clamp-order checks run on both shaders.
Fixes#509Fixes#530Fixes#115🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five group, guild and spell-target defects in the Lua API.
- The looking-for-group reply (`0x4e8dc0`) filled nothing and `UPDATE_LFG_LIST` never fired. The list now fills with the reference's caps (50 records, name 0x30, comment 0x80) and signals once its quest titles are known. Deviation: a login starts with an empty list, where 1.12.1's getters read leftover memory.
- `SMSG_GROUP_CANCEL` reads the name it drops and fires `PARTY_INVITE_CANCEL` (`0x5e6770`, `0x48f440`).
- `GuildSetMOTD("")` sends no string (`0x48b2c1`-`0x48b2da`).
- `SpellCanTargetUnit` passes a unit on any arm (`0x6e6460`). A unit or corpse click that leaves bits in the word holds the spell, and the send carries the mask its binds gathered (`0xceac5c`, `0x7e4e10`).
Each new wire body has a byte-exact golden, and each fix has a unit test.
Fixes#2583Fixes#2569Fixes#2609Fixes#2133Closes#2666 (fixed by #2691)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.12.1 stamps its frame time once per tick on the main thread (`ContextUpdateFrameTime`, `0x420f70`), so a stall in one frame shows in the next frame's `GetTime()`. Under pipelined rendering, Bevy's `time_system` stamped `Time<Real>` with the instant the render world sent after presenting the frame before. A main-thread stall in frame F reached `Time<Real>`, the frame step, `GetTime()`, aura stamps and the session clock only at F+2, so a timer reading `GetTime()` in F+1 lost the stall.
`frame_pace::plugin` now drops the render world's `TimeReceiver` at `PreStartup`, so `time_system` reads `Instant::now()` as each frame starts. A manual `TimeUpdateStrategy` is unaffected.
Tested by emulating the render thread's lagging instant: a 600 ms stall moves `GetTime()` by 600 ms in the next frame.
Supersedes #2890, by kalistoking.
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: kalistoking <148718830+kalistoking@users.noreply.github.com>
Part of #2271. `EndLootWindow` (`0x48f200`) unlocks an item source (`0x48f2bd`, `ITEM_LOCK_CHANGED`) before it sends `CMSG_LOOT_RELEASE` (`0x48f311`). The teardown's second unlock and `LOOT_CLOSED` follow (`0x4c1d36`, `0x4c1dac`). Taking the last row signals `LOOT_SLOT_CLEARED` (`0x4c1f77`) before that close.
benilla sent every release ahead of those listeners. Now each close's unlocks are turns of their own, and every source's release waits behind what was signalled ahead of it. A nested close sends its own release, and a dropped loot lane still sends its queued ones. A Shift-open's lock and send wait for the sweep's `LOOT_OPENED` listeners.
With every lock and cursor write at its point, their rewinds are deleted. The census refuses such a write ahead of a held or lane-queued signal, in tests or when asked.
Each order has a test that fails on main.
Fixes#2877Fixes#2888🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The create screen's paint calls `UpdateCustomizationScene` (`0x471300`), which forces the character's composite (`0x477860(1)`, `0x471308`) and then waits on `IsReady(1, 1)` over the scene (`0x471320`). So a race, sex, class or dial change draws whole in the first frame that shows it. benilla built the create look at the asset server's pace, so a change could keep the old look, or show the new one without its textures, for several frames.
The select's stage wait (#2886) now runs for every glue look. A dial loads no model, only its new textures (fur `0x478160`, hair `0x478220`), so its wait holds for those alone.
Tested by a create look and then a dial change, each waiting for its stage; the test fails on main.
Fixes#2884🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A first click on a character at the select screen lagged; a repeat did not. In 1.12.1 the select's model build ends on `IsReady(1, 1)` over the scene (`0x473083` → `0x710450`), which loads the scene, the character, its items and the pet before returning. A failed read counts as done (`0x4434a8`). So the stage draws whole in the frame after the click.
benilla built the stage one asset step per frame, so a first select took 4 to 10 frames. A select now holds the next frame until the whole stage has loaded or failed, uncapped. A mesh built in that wait is built whole; the old vertex budget had cut the human booth model's skinned forms on first spawn.
Measured with the new `gsel` trace: a first select now draws whole in 3 frames. Tests cover the wait, a failed read and a return.
Fixes#2365🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>