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>
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>
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>
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>
A friendly or resurrection spell that binds nothing now enters the unit targeting cursor, as `TryCast` does for any residual word without the enemy bit (`0x6e507b`–`0x6e50ab`). `autoSelfCast` boots at its reference default of `"0"`, and its `Deviates` row is gone.
- **Binding.** The cursor binds a unit through a world click, and through `SpellTargetUnit` from the unit frames (`BindTarget 0x6e5b40`). The selection never moves.
- **Relation checks.** Heals and buffs ask `CanAssist 0x6066f0` instead of reaction alone, so a friendly NPC without the PvP flag is no longer assistable. That is part of #50.
- **Range.** `SpellCanTargetUnit` and the cursor answer per unit through `0x6e6460`'s unit leg, including the spell's min/max range (`6e677c`–`6e6802`). A click on a unit out of range raises "Out of range." or "Target too close" and keeps the cursor armed. The reference's abort there also strands the cursor, and that is not copied.
- **Your own model** joins the world pick only for a word `0x6e61a0` admits (`0x850e`, no `AttributesEx 0x80000`), and never in first person.
Fixes#136
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
A friendly or resurrection spell that binds nothing now enters the unit targeting cursor, as `TryCast` does for any residual word without the enemy bit (`0x6e507b`–`0x6e50ab`). `autoSelfCast` boots at its reference default of `"0"`, and its `Deviates` row is gone.
- **Binding.** The cursor binds a unit through a world click, and through `SpellTargetUnit` from the unit frames (`BindTarget 0x6e5b40`). The selection never moves.
- **Relation checks.** Heals and buffs ask `CanAssist 0x6066f0` instead of reaction alone, so a friendly NPC without the PvP flag is no longer assistable. That is part of #50.
- **Range.** `SpellCanTargetUnit` and the cursor answer per unit through `0x6e6460`'s unit leg, including the spell's min/max range (`6e677c`–`6e6802`). A click on a unit out of range raises "Out of range." or "Target too close" and keeps the cursor armed. The reference's abort there also strands the cursor, and that is not copied.
- **Your own model** joins the world pick only for a word `0x6e61a0` admits (`0x850e`, no `AttributesEx 0x80000`), and never in first person.
Fixes#136
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
The owner's "wind does not work" was the motion being too small to see (WXL's
0.06 yd grass tips on a 6 s wave, 0.1 yd tree crowns). Retuned at the 9 yd/s
base speed:
- grass: tips about +-0.2-0.3 yd, 2.5 s main wave (182 cycles/wrap), 1.1 s
cross flutter (410); 55 yd gust bands roll downwind (68), swelling the sway
to 1.2x and lulling it to 0.55x; a bent tip dips (no stretched blades).
Parting around units unchanged.
- trees/bushes: 4 s crown sway (114), 5.5 s side wave (82), 0.8 s leaf
flutter (569); crown weight grows with height (0.43x at 1.5 yd, 1x at 6 yd,
1.3x cap), so canopies sway about +-0.3 yd while bushes nod and bases stay
planted.
- new cvar foliageWindStrength 0.25..3 (default 1, not on the preset ladder):
Advanced Graphics slider under Foliage Wind (EN+RU label/tooltip), greyed
while Foliage Wind is Off; carried in sway_strength, so the parting ignores
it; a storm adds up to +60 % sway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's "wind does not work" was the motion being too small to see (WXL's
0.06 yd grass tips on a 6 s wave, 0.1 yd tree crowns). Retuned at the 9 yd/s
base speed:
- grass: tips about +-0.2-0.3 yd, 2.5 s main wave (182 cycles/wrap), 1.1 s
cross flutter (410); 55 yd gust bands roll downwind (68), swelling the sway
to 1.2x and lulling it to 0.55x; a bent tip dips (no stretched blades).
Parting around units unchanged.
- trees/bushes: 4 s crown sway (114), 5.5 s side wave (82), 0.8 s leaf
flutter (569); crown weight grows with height (0.43x at 1.5 yd, 1x at 6 yd,
1.3x cap), so canopies sway about +-0.3 yd while bushes nod and bases stay
planted.
- new cvar foliageWindStrength 0.25..3 (default 1, not on the preset ladder):
Advanced Graphics slider under Foliage Wind (EN+RU label/tooltip), greyed
while Foliage Wind is Off; carried in sway_strength, so the parting ignores
it; a storm adds up to +60 % sway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lamp_terrain_occlusion_scan takes an optional light height and ray clearance, counts blocked
samples within 15 yd, prints the ground height, and ranks by the near count. Used to pick the
leftovers B1 scenes (Elwynn campfires, lampposts, bridge lamp): the gather and the terrain
receiver work, the terrain-blocked ground is grazing or at the rim, so the lit image is unchanged.
LIGHTING.md says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lamp_terrain_occlusion_scan takes an optional light height and ray clearance, counts blocked
samples within 15 yd, prints the ground height, and ranks by the near count. Used to pick the
leftovers B1 scenes (Elwynn campfires, lampposts, bridge lamp): the gather and the terrain
receiver work, the terrain-blocked ground is grazing or at the rim, so the lit image is unchanged.
LIGHTING.md says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ShadowCasterMaterial and CutoutShadowCasterMaterial get one warm rig each (production attribute
sets, layers 0+31: the world camera mints the forward pipeline, a live sun rig's shadow views the
depth one). The cutout pair (MAY_DISCARD opaque + prepass) otherwise first compiles when the first
leaf card enters sun-rig reach, after the cover lifts. Fixes the failing
pipe_warm::menagerie::every_material_lane_has_a_warm_contributor census for real.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ShadowCasterMaterial and CutoutShadowCasterMaterial get one warm rig each (production attribute
sets, layers 0+31: the world camera mints the forward pipeline, a live sun rig's shadow views the
depth one). The cutout pair (MAY_DISCARD opaque + prepass) otherwise first compiles when the first
leaf card enters sun-rig reach, after the cover lifts. Fixes the failing
pipe_warm::menagerie::every_material_lane_has_a_warm_contributor census for real.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review report (grass and tree sway not moving in the play build) did not
reproduce: the release/play exe publishes grass and tree strength 1 with
foliageWind absent from the config, and capture pairs show sway. The tier is
now logged at boot and on every change so a player run shows what the
receivers were given, and a test holds the no-pin default path: moving
travel, grass and tree rows set, tier 0 clearing them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review report (grass and tree sway not moving in the play build) did not
reproduce: the release/play exe publishes grass and tree strength 1 with
foliageWind absent from the config, and capture pairs show sway. The tier is
now logged at boot and on every change so a player run shows what the
receivers were given, and a test holds the no-pin default path: moving
travel, grass and tree rows set, tier 0 clearing them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0x8: the sky's weight lifts the daylight fade so a low/just-set sun keeps full shafts.
Mask: water writes no depth, so the sea under the horizon read as sky and got a shaft lift
(Westfall sunset: up to 9.7 levels on the water, 0 above the horizon after). The mask pass now
binds the view uniform and keeps depth-0 only where the view ray is above -0.01 elevation.
Adds the leftovers-sea-sunset capture scenario.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0x8: the sky's weight lifts the daylight fade so a low/just-set sun keeps full shafts.
Mask: water writes no depth, so the sea under the horizon read as sky and got a shaft lift
(Westfall sunset: up to 9.7 levels on the water, 0 above the horizon after). The mask pass now
binds the view uniform and keeps depth-0 only where the view ray is above -0.01 elevation.
Adds the leftovers-sea-sunset capture scenario.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SKYBOX_FORCE_SUN_SHAFTS (0x8) and SKYBOX_NO_SUN_FOG (0x10) constants, CameraSkybox::flag_weight
(heaviest main layer carrying a flag). update_fog_model scales sun_fog_strength by 1 - weight of a
0x10 sky. LIGHTING.md documents both flags (0x8 consumer lands in the next commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SKYBOX_FORCE_SUN_SHAFTS (0x8) and SKYBOX_NO_SUN_FOG (0x10) constants, CameraSkybox::flag_weight
(heaviest main layer carrying a flag). update_fog_model scales sun_fog_strength by 1 - weight of a
0x10 sky. LIGHTING.md documents both flags (0x8 consumer lands in the next commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build Blizzard-derived sky assets only from the user's own CASC install, merge lighting DBCs from mounted winners, and pack a preserved patch-Z candidate. Include bilingual install and recovery guidance plus project-owned grading data.
Verified all 46 default stage payloads byte-identical to the reviewed stage; moderated WoW Forever LightIntBand byte-identical; band and mounted-chain checks pass on a hard-linked client copy. No Cargo command run.
Co-Authored-By: GPT-5.6 Sol (Codex)
Build Blizzard-derived sky assets only from the user's own CASC install, merge lighting DBCs from mounted winners, and pack a preserved patch-Z candidate. Include bilingual install and recovery guidance plus project-owned grading data.
Verified all 46 default stage payloads byte-identical to the reviewed stage; moderated WoW Forever LightIntBand byte-identical; band and mounted-chain checks pass on a hard-linked client copy. No Cargo command run.
Co-Authored-By: GPT-5.6 Sol (Codex)
- A repair the purse cannot cover is refused as in 1.12.1: the not-enough-money error and its voice line, no repair sound, nothing sent; Repair All tests the worn gear alone
- Tidy three comments: the gift-wrap getter's reader, the repair-reset test's claim, the merchant module doc's width
Fixes#412
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A repair the purse cannot cover is refused as in 1.12.1: the not-enough-money error and its voice line, no repair sound, nothing sent; Repair All tests the worn gear alone
- Tidy three comments: the gift-wrap getter's reader, the repair-reset test's claim, the merchant module doc's width
Fixes#412
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Repair costs take the vendor's price discount, 10% off at Honored and up to 10% more by honor rank at a PvP-flagged vendor, rounded as 1.12.1 rounds them
- Flight map fares take the flight master's price discount, as 1.12.1 shows them, while an express flight still sends the full fare
Fixes#411Fixes#434
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Repair costs take the vendor's price discount, 10% off at Honored and up to 10% more by honor rank at a PvP-flagged vendor, rounded as 1.12.1 rounds them
- Flight map fares take the flight master's price discount, as 1.12.1 shows them, while an express flight still sends the full fare
Fixes#411Fixes#434
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Casting a targeted spell ends repair mode, as 1.12.1's targeting arm writes its cursor over Repair
- A right mouse-down on the world ends repair mode, as 1.12.1's WorldFrame hook resets the cursor to Point
Fixes#420Fixes#422
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Casting a targeted spell ends repair mode, as 1.12.1's targeting arm writes its cursor over Repair
- A right mouse-down on the world ends repair mode, as 1.12.1's WorldFrame hook resets the cursor to Point
Fixes#420Fixes#422
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Right-clicking a bag or worn item puts a held item back first, as 1.12.1 clears the cursor before any use
- The bag frame's end-to-end test: a right-click with a held item puts it back, then uses the slot
Fixes#413Fixes#421
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>