Apply the formatter baseline needed by the generated CI gate.
Verified the full treefmt check with vendored sources and generated MAP.md excluded. Parsed formatted Cargo manifests and compared their values with HEAD; all are identical.
- Change `LevelUpInfo` to signed to match vanilla behavior
- Add parse test for negative values in SMSG_LEVELUP_INFO, update comments
- grammar
- level-up gains: cite the %d fire and vmangos's wrapped deltas; pin the reported line
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
Three ways `DoEmote` and the chat talk gesture differed from 1.12.1:
- `/wave Bob` ignored the name and targeted the selection. 1.12.1 resolves a name that does not start with `%` as a player, at any range (`0x49fdb1`-`0x49fdf0`, `0x493aa0`), and sends the emote untargeted on a miss. `DoEmote(token, name)` from Lua does the same.
- A polymorphed, stunned or sleeping speaker still played the talk gesture. `UNIT_FIELD_FLAGS 0x20000000` suppresses it when the sender's name is cached (`0x49d7f7`).
- `DoEmote` sent while our unit was possessed (`0x5ef57e`). A chat-only emote now reads `Emotes.dbc` row 0's flags, so it is refused while asleep or dead (`0x5ef5b1`, `0x47db8e`).
Tests cover the name, miss, `%t` and self cases, the gesture gate, and both `DoEmote` gates against the install's row 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One-shot emotes were routed by a hand-made class table that wrongly let `/pray` (75), the `/beg` family (79), and 126, 127, 131, 132 and 135 play upper-body. A mounted rider played every id on its upper body.
1.12.1 reads two byte tables:
- CLASS_A (`0x5fed90`): the ids that may play on the key bone.
- The mount set (`0x5fec80`): what goes to the mount model while mounted (`0x5fe7b5`..`0x5fe803`). Anything else plays nowhere.
What changed:
- Both tables now match the bytes, and a mount-set id is forwarded to the mount child.
- Unmounted `/eat` (61) always plays upper-body (`0x5fe834`).
- A model with no split bone arms nothing for an upper-body play (`0x5fdcc4`).
Tests pin both tables for every id 0..300, plus the mounted, eat and no-bone driver cases.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Shift-clicking water or arrows at a vendor takes one bundle, as 1.12.1 does
- Drop the merchant comments that restate the code
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A multi-line EditBox kept its authored height, so a scroll frame around a long mail body or ticket had no range. A box made in Lua with no template never placed its text, so it drew nothing.
In 1.12.1, the flush's relayout sets a multi-line box's height to its insets plus its wrapped text height, or one line when empty (`0x77d4d0` at `0x77d8ad`). Every resize, including the first layout, re-seats the text at the top-left inset (`0x77a8d0` → `0x77b8c0`). `SetTextInsets`, `GetTextInsets`, `SetMultiLine` and a font change re-seat it too.
benilla now does the same. A focused multi-line box's caret and text are clipped only by a scroll frame, as the reference clips neither.
Tests cover growth and shrink, the stock mail and ticket scroll ranges, each re-seat trigger, a Lua box's first layout, and the caret in an empty box and after a trailing newline.
Fixes#722Fixes#726🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DWARF is ~97% of what rustc writes here: the largest `benilla-app` codegen unit measured 145 MB,
of which 61 MB was `.debug_str` and 13 MB `.debug_info` against 1.5 MB of `.text`. That carried a
1.5 GB `libbenilla_app` rlib, a 2.9 GB test binary and an 82 GB `target/debug`. `.debug_line`
survives, so a backtrace and `gdb bt` still name a file and a line; `split-debuginfo` leaves the
rest in the object files rather than copying it into the linked binary.
Both are the client half of Turtle's custom systems, off the chain's own `FrameXML.toc`:
`Turtle_TransmogUI` hooks `GOSSIP_SHOW`, hides the stock gossip frame for its NPCs and drives
the transmog window over `TW_TRANSMOG`; `Turtle_ShopUI` (prefix `TW_SHOP`) is the custom
merchant/shop. Without these rows the server's trigger gossip text is what the stock gossip
window shows and neither window exists. Inspect-Talents and Guild-Bank stay out until
`ToggleInspect`/`SetTalentButtonLocation` and the GuildBank globals are built.
`animLodDistance` (0..=300 yd, default 60, `0` off) adds a distance leg to the animation-LOD
gate: a rig past it parks its pose evaluation even when it is in frame and in a PVS-visible
room, joining the frustum and room legs. The gate reads the world camera's `GlobalTransform`
for the position. `0` restores the shipped frustum-and-room-only behaviour.
The GPU-skinned character caster made realtime shadows render wrong, and the
Stormwind lag it was meant to fix does not come from the shadow lanes anyway
(character shadows are off there, so the rig is torn down). Restore the
CPU-skinned merged proxy exactly as it was.
The character shadow lane CPU-skinned every admitted unit into one merged
Mesh asset and re-uploaded it each rebuild — the documented expensive half
of the lane's ~3 ms, and the part that scales with players. Replace it with
one GPU-skinned caster per admitted creature part: the part's own skinned
mesh and MeshTag, drawn through a new SkinnedShadowCasterMaterial whose
prepass vertex skins from the shared rig palette (the same rows wow_model
reads). Pose changes are free; only membership is reconciled per frame.
Palette region offsets are pushed as shader defs, so the caster shader reads
the shared light buffer's tail as a flat array and needs no copy of the
WowLight layout.
`wipe` cleared elements with a bare `pairs` loop, which leaves Lua 5.0's remembered table size
stale (the `getsizes` side store `luaL_setn` writes). TurtleWoW's LFT fills its group list, wipes
it on the next `S2C_GROUPS_LIST_UPDATE` start, and refills; the refill's `getn` answered the old
length, so `LFT_UpdateGroupsList` read nil entries and `CustomSort` raised on `value.category`.
Take `Globals.lua:126`'s body instead: a `tremove` walk down from `getn` resets the size, then
clear the non-array keys. The test now pins the return and the size reset.
benilla.toc sources the stock FrameXML off the player's chain, and it omitted the LFT
(Looking-For-Turtle) files, so the group finder never loaded. Add LFT_Data.lua, LFT.lua,
LFTTemplates.xml and LFT.xml in the reference toc's order, with `json.lua` (the codec its group
payloads decode through) and OctoWoW's blink-pause overlay last. `CharacterFrameTemplates.xml`
comes in ahead of its users: LFT's Browse/Queue tabs, and CharacterFrame.xml's own tabs, inherit
`CharacterFrameTabButtonTemplate`, which only that file defines and the reference toc lists apart.
The char-create catalog compared `CharBaseInfo` against the stock 1.12 rows exactly and bailed
on any difference. TurtleWoW adds class combos (Human Hunter, Orc Mage, Dwarf Warlock, ...), so
the whole catalog failed to load: `body_display` was unavailable and the glue booth assembled no
body on both the create and select screens. The creatable set is now read from `CharBaseInfo`
(the stock eight, or a patched chain's more), the stock rows are a subset guard against a
misparse, and race 9 (Goblin) and 10 (BloodElf) load their display, dial ranges and outfit like
the shipped eight.
The chain's `ChrRaces` row 10 is `BloodElf`, not the "High Elf" the code named it, and
`scene_token` mapped 9/10 to Human, so a Blood Elf stood on the Human stage. Both now resolve
(`UI_Goblin`, `UI_BloodElf`) and the display name is "Blood Elf"; the fileString is what the
race stage keys on.
The create screen hard-coded the eight vanilla races. It now splits the catalog's creatable set
into the Alliance and Horde columns, so a TurtleWoW chain shows five rows per column (Blood Elf
appended to the Alliance, Goblin to the Horde), with the tower and lower clusters shifted to
match its XML. Race-icon UVs are computed from the loaded sheet's size, so the stock four-column
sheet and TurtleWoW's widened eight-column sheet both read correctly.
A count cannot say which row opens a shop, which is greyed, or which is
TurtleWoW's `CUSTOM_MERCHANT_TRIGGER` marker. The labels are the server's own
sentence; print them with the index, icon and coded flag beside each.
Verified: cargo clippy --workspace --all-targets -- -D warnings reports nothing
in this file. No test covers a debug! line.
The bar was suppressed for every `Attributes & 0x2` spell, the reference's
`0x6e7700` gate read literally. That gate is why Aimed Shot and Multi-Shot show
no bar in 1.12.1 — a later client added `Force Display Castbar` to fix exactly
this, so the reference behaviour is the bug.
Gate on the DBC row instead: a spell opens a bar on the wire cast time whenever
`SpellCastTimes.dbc` has a nonzero one for it. Aimed Shot's real 3 s cast does,
so it bars. The instant shots do not — they point at row 1, the instant
sentinel — so Throw, Multi-Shot, Auto Shot and wand Shoot stay bar-free, and
vmangos's 500 ms pad on a non-auto-repeat ranged cast (`SpellEntry::GetCastTime`)
cannot conjure one either. An unknown spell, or a build with no cast-time table,
keeps the reference's bar rather than losing every bar.
`SpellCastTimeCatalog::from_rows` is the constructor the DBC loader and the test
share.
Verified: `cargo test -p benilla-app -p benilla-formats` — 3833 pass, the new
`a_timed_ranged_cast_opens_the_shot_bar_but_an_instant_shot_does_not` among
them. The one failure, `assets_ui_does_not_grow`, is pre-existing on main.
The gate was already red on main, in files no round since had touched.
`cargo fmt` reflowed one wrapped assert in the benilla-ui loader tests. Clippy's
1.98 set added four lints the tree does not satisfy: `doc_lazy_continuation`
on room_claim.rs's `2b.` item (indented, as it already renders), and in
benilla-extract `chunks_exact_to_as_chunks` on the MOGI walk,
`manual_checked_ops` on the census mean, and `needless_borrow` on the class
table call. Each takes the fix clippy names.
33 errors remain in benilla-world's lighting, static_gx, water_fx and liquid
trees, from the same lint bump; they are left for their own round rather than
folded in here.
Verified: cargo fmt --all --check clean; `cargo clippy --workspace
--all-targets -- -D warnings` now fails only in benilla-world.
Leaving a battleground left the character unable to walk for the rest of the session. The
`settle released` line named it: `WASD held by: control-lost`.
vmangos takes control for the teleport (`SetClientControl(player, 0)`), so `Revoked` sets
`control_lost` and parks the mover, and only a later `SMSG_CLIENT_CONTROL_UPDATE` with
`allow_move = true` clears it. Nothing guarantees that packet, and nothing else clears the flag:
the worldport arm replaces the pose and the settle releases, neither asks, and `seizing` at
`wire_in.rs` wants `reseat`, which a worldport never sets. So the lock on the body we just left
parked the body we came back as.
A transfer is a mover change, so re-claim at the transfer's completion, with the ack, the way
login does — the same teardown as the reference's `SetActiveMover` reset. `reseat` rides along so
the existing seize path resets the outgoing mover's state. A possession is left alone: there the
lock is the point.
Compile-verified only — the live run is the test.
Coming out of a worldport unable to walk was a story, because nothing in the log said whether
the hold was the settle, a lost mover or a pending reseat. `movement_suppressors` named six
states and was only ever printed by the logout smoke test; `settling` was missing from it
although the settle hold is a mover hold (`mover.rs`), and a cross-map worldport is exactly
where it fires.
One line per settle, on the frame it ends, naming the hold or `none` — so the next transfer
reports its own cause. Instrument only; no gate reads either list.
The probe's last act was a GM teleport, which never exercises the worldport a leave causes — so
it could say nothing about the scoreboard or the movement latches after one. `WOW_PROBE_BG_LEAVE=1`
swaps the flag and graveyard legs for the real thing: wait inside for the match to end, leave with
the stock `LeaveBattlefield()`, and census for 90 s across the worldport.
A solo match never reaches `min_players_per_team`, so vmangos ends it with `WINNER_NONE` about
300 s after the doors, and a non-nil winner is what the modal needs to show itself
(`BattleGround.cpp:318` is the AV exception). A census gained `SCOREFRAME IsShown=` with the three
latches that can freeze a body — `settling`, `world_stale`, `control_lost` — beside the worldport
ack, and a `moved=` column on the position so "stuck" is one number rather than an eyeball.
`PLAYER_ENTERING_WORLD` joins the tap, to see whether the world-entry event fires at all.
The tap's window is drained once per frame before the phase match, for the three in-battleground
phases, so the one-shot consume stays in `bg_probe` where `game_plugins.rs` argues for it.
Leaving a finished battleground brought the end-of-battleground panel back in the open world,
with a leave button that had nothing left to leave. Two defects, both in how the board outlived
the instance.
`SMSG_BATTLEFIELD_STATUS` is the packet that says the player is out: the active slot's map going
to zero, or that same slot losing `in_progress`. The handler already zeroed the run-time clocks on
exactly those two branches and left the scoreboard standing, so `GetBattlefieldWinner()` kept
answering the finished battleground's winner and stock Lua showed the panel again on the next
score event (`WorldStateFrame.lua:351-355`). TurtleWoW drops its own board on the same signal —
`instanceType == "none"` clears `customData` (`WorldStateFrame.lua:243-247`), which is the only
thing its `GetBattlefieldWinner()` override can fall back from. `BattlefieldQueue` now marks the
exit and the feed zeroes the board with the clocks.
Zeroing the resource alone changed nothing, because the feed could not push an empty one:
`changed = fresh.is_some() && …` withheld a cleared board, and the VM keeps whatever it was last
handed. The clear is pushed as a default board, which answers nil for the winner and fires the
`UPDATE_BATTLEFIELD_SCORE` that lets the frame drop its winner text, timer and button.
`PLAYER_ENTERING_WORLD` is not the lever: the reference fires it once per session beside
`PLAYER_LOGIN`, `PLAYER_LEAVING_WORLD` and `PLAYER_LOGOUT` (`0x490959`, `0x49096a`, `0x490b4d`,
`0x490c2a`), not per teleport.
The world-state test becomes the Lua contract it always was — the panel returns whenever the board
answers a winner — and two app-level tests pin the fix, one per exit packet. Both fail without it.
`framexml::merge` merged the inherited template's `name` onto the instance, so an unnamed child of a named virtual template took the template's name. ItemRack's `<Frame inherits="ItemRackQueueTemplate"/>` then named its texture after the template instead of the parent frame, and `Rack.OnEvent` indexed the missing `ItemRackInv*Queue` global.
The reference reads `name` and `id` from the instance alone (`PreLoadXML 0x769770`); drop both from the merge base.
`cargo fmt --all` wanted a `vec!` wrap and a trailing newline in repro_mail.rs, which
`main` has been carrying unformatted. The gate is `cargo fmt --all -- --check`, so it was
red before this and for nothing to do with the changes around it.
`benilla-octo.zip` is a zipped snapshot of this checkout, taken by hand to move the tree
somewhere. It is binary, redundant with the history it would land in, and 10 MB of it.
`WOW_DATA` names the install, defaulting to the TurtleWoW one, and `WOW_HOST` overrides the
realm for a single run without touching `benilla-config/config.toml`. Builds the release
binary first if it is not there, then execs it, so the run line is one command.
A build cache serves a dependency rebuild after a branch switch or a `git clean` instead of
recompiling it, which is most of what the in-loop verify spends. It stays a convenience and
never a requirement: with nothing on `PATH` nothing is exported and a clone needs only stable
Rust and a C compiler, exactly as before.
`check.sh`, `gates.sh` and `smoke.sh` source it, so the loops pick it up without being asked
and a machine that would rather not have it sets `BENILLA_NO_SCCACHE=1`. An existing
`RUSTC_WRAPPER` wins. Only rustc is wrapped — the vendored Lua build wants its own
`CC="sccache cc"`, so it is not cached by this.
The era's tenth category was a button in the tree that went nowhere. It now opens onto the
era's own per-addon enable list, rebuilt on show from the AddOn API: one checkbox per
installed addon carrying its `GetAddOnInfo` enabled state, then the Load out of date AddOns
row under it (`checkAddonVersion`, inverted, so checked means "0"). A toggle calls
`EnableAddOn`/`DisableAddOn`, which the host persists to `AddOns.txt` at logout — the
reference's own contract for this screen, where the change lands on the next world entry.
`the_defaults_button_is_armed_by_rows_not_by_a_category` is the invariant that every
category leads somewhere and that Defaults is live on every page with a CVar behind it, and
AddOns is the second category to run its own page after Keybindings. It is exempted from the
`OPTIONS_PAGE_ROWS` half for that reason, and from the Defaults half because an addon's
enabled state is not a CVar: there is nothing there for Defaults to write back.
`build_landmarks` is the `0x4a6819` walk of `[0xc0e054]` lifted out of `feed_world_map`, so
the landmark list can be built against a world state without a live `WorldMapUiData` feed.
The guard-directions marker stays with the caller, appended after it, as `0x4a69c7` has it.
The new test drives the whole path — resolve the instance map, project through its rect,
gate by flag, exploration and world state — against the real catalogues. Alterac Valley's
towers and graveyards are `AreaPOI` rows on map 30, each objective owned by a separate
`WorldStateID` row at one position, so the row whose state the server set is the one that
lands: before any state arrives only the six ungated rows show, and afterwards the held
objective shows once with its own POIIcon cell, not every owner variant.
Four fixes a TurtleWoW client needs and stock 1.12.1 never asks for, each for a raise it
provokes:
A player attaches as soon as its display model is ready. Gating on `Equipment::settled`
withheld a body for good on a chain that never answers one `PLAYER_VISIBLE_ITEM` template —
in a battleground crowd, every other player. 1.12.1 withholds a unit only until its first
composite (`0x477860`); the compositor paints the armour it knows and re-dresses when a
template lands.
Races 9 (Goblin) and 10 (High Elf) get their sides, from TurtleWoW's `FactionTemplate.dbc`
group masks (Goblin's template 1609 is mask 5, Horde; High Elf's 1610 is mask 3, Alliance).
Without a side, the roster seat's `UnitFactionGroup("player")` is nil and an addon that
concatenates it at file scope raises and loses its whole load phase.
`wipe` hands back a table for nil. `Blizzard_RaidUI.lua:77,116` clears its
`RAID_SUBGROUP_LISTS` with `t[i] = wipe(t[i])` and then `tinsert`s into the result, so a
never-populated entry must come back insertable.
`RequestArenaScoreInfo` is defined as a no-op. `WorldStateFrame.lua:217` calls it on the
battleground score events; the function is native to TurtleWoW's client and the reply is
parsed by its own native code, never by that Lua. 1.12.1 has no arenas, so there is no
scoreboard here for it to fill.
Volumetric light: the shadow rig's directional light (sun by day, moon at
night) is ray-marched through its cascades at half resolution (16 Medium /
32 High jittered samples from 0.35 yd, height-thinned air, HG g 0.6), then a
depth-aware blur and upsample composite it over the fog pass. Density and
colour follow the zone fog; no shafts in WMO interiors. The volumetric fog
pass retires its own midpoint shafts while this is on. Cvars volumetricLight
0/1/2 and volumetricLightStrength, env WOW_VOLLIGHT; Graphics Preset Medium 1,
High/Ultra 2.
Moonlight: a cool, dim directional term from the visible moon added to the
stock night on terrain, models, WMO exteriors and doodads, shadowed by the
moon's map (shadow_hook::realtime_shadow_moonlit .z), ramping with the moon's
elevation and handing back to the sun at dawn. MonkeyFrame grows one row
(per-frame blob 8800 B). Cvar moonLight 0..2 (0 = the reference night).
Advanced Graphics rows (EN + RU), capture scenarios vol-*, docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two conflicts: docs/MAP.md (our map plus upstream's cfg_categories/wow_ini modules
and WOW_CHAR row) and the Update ambiguity ceiling in game_plugins.rs (kept ours,
re-measured in a follow-up commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 0.2.0 release: the workspace version is 0.2.0, `rust-toolchain.toml` pins the stable the release was checked with, `1.98.1`, so the tag builds with that compiler after stable moves on, and the README says each release is a tag on the Releases page.
Checked on this tree: the land gates, the live smoke run (its build line reads `benilla build 0.2.0`), the Linux and Windows compile of `benilla-app`, and the client on Vulkan (lavapipe) through its four capture scenes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With realms in more than one category, the realm list shows the stock category tabs, so every realm can be reached. Before, it listed only the first category's realms and had no way to the rest.
- **The Region** is `Wow.ini`'s `[WoW Config] Region`, read through the archive chain as `0x40249d` loads it (the stock install ships it in `interface.MPQ`), with the section and key compared case-insensitively (`0x445356`, `0x4453cb`) and the value parsed as `0x445470` does. It is 0 when the file or the key is missing.
- **The categories** are the `Cfg_Categories.dbc` rows whose Region column equals it, in file order (`0x46e300`): id from column 0, name from the row.
- **Placement** (`0x46e510`): a realm's category byte 0 takes the first category's id (`0x46e5b8`), then joins every category with that id (`0x46e60b`). A byte matching no category puts the realm on no tab, as in 1.12.1. Tab ordinals count the non-empty categories only (`0x46edf0`), and the load-band statistics still run over every realm.
- **The tabs** are built from `RealmList.xml`: hidden for one category, `RealmListTab1`..`N` with the category names for two to eight, the front one raised and disabled. A click fronts the tab and re-lists (`RealmListTab_OnClick`), the highlight falls back to the current realm or none, and the scroll clamps to the new tab. Change Realm and `OnShow` keep the stored category (`0x46f188`, `0x46f400`).
- **No Region rows**: every realm lists on one untabbed page. The reference reads a NULL category there (`0x46f26a`, `0x46e5b8`), and benilla does not copy the fault.
- **Logging in** by name, the remembered or current realm, still searches every realm, as the reference resolves by name.
- A dev capture, `WOW_CAPTURE=glue-realmlist`, shows two tabs over five invented realms.
Checked: tests for the Region read, the category rows for a US and an EU Region from the install's DBC, placement (byte 0 folded, an unmatched byte on no tab), the strip hidden for one category and named for two, a tab click through the real click path, the highlight and scroll on a switch, the stored category across Change Realm, the statistics over every realm, and the no-rows fallback. Each fails with its piece undone. Gates and the smoke run green on macOS; no platform seam touched.
Fixes#362🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Resurrection, Redemption or Rebirth armed and nothing selected, the targeting cursor takes a released party member's corpse: it lights over the corpse in range, and a click casts on it. Before, the cursor stayed grey over a corpse and a click bound nothing.
- **The pick** (`0x480816`, `0x6e6260`). While targeting, a corpse is a candidate only under a word in `0x8600`: under `0x8000` a corpse that is not bones and passes `0x6067d0`, and under `0x200`, tried only without `0x8000`, one that is not bones and fails it. Outside targeting every corpse stays a candidate, as before.
- **The reaction gate `0x6067d0`**: the caster's faction template toward the corpse owner's race template (`ChrRaces`, as `0x5d7120` reads it), friendly at 4 and up. The faction catalog now carries each race's template, and the PvP team loader reads it from there.
- **The cursor**: `0x6e6460`'s corpse leg (`0x6e6719`-`0x6e676d`) with its range tail. `GetMinMaxRange 0x6e3480` pads a corpse like a unit, reading the caster's own reach twice (`0x6e3605`-`0x6e361e`).
- **The click**: `BindTarget`'s corpse arm (`0x6e5f89`-`0x6e6020`), the `0x8000` arm then the `0x200` arm, through the same range merge as a unit (`0x6e602c`). Out of range raises the error and keeps the cursor armed, as the unit arm already does.
- **The wire**: `CMSG_CAST_SPELL` with the corpse bit and the corpse's packed guid, which the client writes for any mask bit in `0x8a02` (`0x7e4e10`) and vmangos reads for either corpse bit (`SpellCastTargetsInfo.cpp:156-157`); `CMSG_USE_ITEM` the same, for Goblin Jumper Cables.
Checked: byte-exact goldens for both packets; unit tests for the pick filter, the cursor verdict (bones, a hostile corpse, out of range, the reach pad), the bind arms from a world click to the sent command, and the reaction gate on the shipped DBCs. Each fails with its piece undone. There is no live run: the probes cannot aim or click in the 3D world. Gates green on macOS; no platform seam touched.
Fixes#450🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Follow Terrain on, a `cameraTerrainTiltTimeMin` above `cameraTerrainTiltTimeMax`, or a NaN in either, no longer panics the client on the first slope. The ground channel bounded its tween duration with `f32::clamp(lo, hi)`, which panics when `lo > hi` or a bound is NaN.
- **The bound** is now the reference's two ordered compares (`0x50dd29`-`0x50dd83`, `lo = TimeMin × factor`, `hi = TimeMax × factor`): below `lo` takes `lo`, else at or above `hi`, or NaN, takes `hi`, else the duration stands. Nothing orders the pair, so an inverted one is an odd duration and never a fault. `lo` is compared unrounded, as it stays on the x87 stack (`0x50dd4c`), and `hi` as the f32 it is stored as (`0x50dd40`).
- **In flight too.** The armer `0x512490` returns 1 when the request matches the one in flight (`0x512555`), and the caller bounds the duration on any nonzero return (`0x50dd21`), so the bound runs again every frame a tilt is in flight. For an ordered pair that changes nothing; an inverted pair leans for `TimeMax × factor`, and a TimeMax changed mid-tilt bounds the tilt in flight.
- **The floor** that keeps a zero or NaN duration from stalling the tween moved from the stored duration to the divide, so the in-flight bound reads the raw value the reference reads.
Checked: nine tests, each derived from the reference's rule rather than the code, covering an inverted pair below, between and above, a NaN in each input, the unrounded `lo` compare, the in-flight bound, and the issue's case end to end (Min 20, Max 1 on a 20° slope). Each fails with its piece undone, the first ones with the old `clamp` panic (`min > max, or either was NaN`). The bound matches the reference's bytes over 20M random inputs with no bit difference. Gates green on macOS; no platform seam touched.
Fixes#304🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Logging in to an account or realm with no characters lands on the empty character list. Until now the login cycle created a male Human Warrior on the server, named `WOW_CHAR` or "One", on every login that found an empty roster, a typed one included and again after Change Realm.
- **The reference.** The client builds `CMSG_CHAR_CREATE` in one place, `0x5aac50`, reached only from the glue binding `CreateCharacter` (`0x4719e0` → `0x4711d0` → `0x46b290` → `0x5aac50`), which stock `CharacterCreate.lua` calls only from `CharacterCreate_Okay`. With an empty roster, stock `CharacterSelect.lua`'s `UpdateCharacterList` disables Enter World and Delete and shows Create New Character, which benilla's select screen already does.
- **The change.** The create-if-empty step is gone from the per-realm cycle, and `NetConfig`, which existed only for it, goes with it. The create screen's own path is untouched.
- **Scripted runs.** `WOW_CHAR` must now be on the account: a driverless run whose `WOW_CHAR` is missing fails at select, as it already did on a non-empty roster, and a player's run shows the list. `docs/CONTRIBUTING.md` says so, and names `WOW_PROBE_CHARCREATE` as the explicit way to make one.
Checked: `an_empty_account_parks_at_select_without_creating_a_character` drives the real login cycle against a fake realmd (the server half of SRP6) and a fake world server answering an empty roster. It fails with the old step restored (the client sends `CMSG_CHAR_CREATE` on its own) and passes without it. Live against a local vmangos on a fresh account with no characters: before, the server gained a level-1 "One"; after, the client parks on the empty list and the account still holds no character. With a `WOW_CHAR` not on the account, a driverless run exits at select, and nothing is created. Gates and the smoke run green on macOS; no platform seam touched.
Fixes#343🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Targeting an NPC whose faction you are Exalted with no longer opens a script error. `UnitReaction` answered 8 there, and stock `TargetFrame_CheckFaction` indexes `UnitReactionColor[reaction]`, which has seven entries (`TargetFrame.lua:6-14`, `:183`).
- **The cap.** `UnitReaction 0x6061e0` takes `0x606530`'s answer through `min(rank, 6)` at `0x606439` (`cmp esi,6` / `jl` / `mov esi,6`), so Revered and Exalted both read 6, and the Lua binding adds one (`0x51683e`), so Lua sees at most 7. `ring_reaction` now caps its answer there.
- **The ladder stays whole.** `reputation_rank` is untouched: the reputation pane and the item requirement lines still read Exalted.
- **Every caller** of `ring_reaction` models `0x6061e0` and tests at 1, 3, 4 or 5, so none changes but the four Lua unit feeds (target, targettarget, npc, mouseover), which now share one `unit_reaction` helper. The vendor discount's Exalted row reads as Revered, as in the reference, and still takes 10%.
- **Comments** that gave the scale as 0..7 or 1..8 now say 0..=6 and 1..=7.
Checked: `the_reputation_rank_is_capped_at_revered` walks both sides of the Honored, Revered and Exalted thresholds; `an_exalted_npc_target_reads_revered_and_its_plate_is_green` runs stock `TargetFrame` on an Exalted Stormwind unit. With the cap loosened to 7 both fail, the second with the issue's `TargetFrame.lua:183: attempt to index field '?'`; restored, both pass. Gates green on macOS; no platform seam touched.
Fixes#440🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The startup line, crash reports and the debug panel now name the release a build is, so a bug report carries it: `benilla build 0.2.0 · b17be27 · 2026-09-27 · release` on a release, `0.2.0+12 · …` twelve commits past its tag.
- `benilla-buildstamp` also stamps `git describe --tags --long --match 'v[0-9]*'` and watches `packed-refs` and `refs/tags`, so a fetched tag restamps without a new commit.
- `BuildId` carries the workspace version and the describe. `release()` reads the version, with `+N` when the describe's tag is that version's; with no tag in reach (a source archive, a shallow clone) or the previous version's tag (the release commit before its tag exists) it reads the version alone.
- The bug template's example line shows the new form.
Checked: the four new `build_id` tests fail with the distance parser broken and pass restored; the describe command in a scratch clone reads empty with no tag, `v0.1.0-443-g…` over a launch tag, `v0.2.0-0-g…` on the release commit, and skips tags that are not `v`-numbered; the build script's output carries the new watches and variable. Gates green on macOS; no platform seam touched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>