Commit Graph
645 Commits
Author SHA1 Message Date
chairmanpow 67b7ef8994 spell: a shot with a real cast time opens the cast bar
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.
2026-10-03 14:08:43 +01:00
chairmanpow 503cde608a build: green fmt and clippy on the pinned 1.98.1
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.
2026-10-03 14:08:26 +01:00
chairmanpow 3b068b15c2 worldport: re-claim our own body after a transfer
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.
2026-10-02 18:38:56 +01:00
chairmanpow bf52a0abb1 player: say what holds WASD when a settle releases
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.
2026-10-02 18:23:09 +01:00
chairmanpow c2d83dc22e probe bg: a leave leg, so WOW_PROBE_BG can measure the worldport out
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.
2026-10-02 17:56:08 +01:00
chairmanpow 7f99a3f35a scoreboard: the board dies with the battleground, and the clear reaches the VM
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.
2026-10-02 17:56:00 +01:00
chairmanpow 8b81d08153 loader: an unnamed instance keeps its own name, so $parent reaches the real parent
`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.
2026-10-02 15:45:38 +01:00
chairmanpow ec3349ad96 fmt: repro_mail.rs, and the hand-taken zip snapshot ignored
`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.
2026-10-02 13:37:01 +01:00
chairmanpow 7d7ae749ab run.sh: launch the built client against a local install
`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.
2026-10-02 13:37:01 +01:00
chairmanpow 2f6f35b054 build: scripts/sccache.sh wraps rustc when sccache is installed, and the gates source it
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.
2026-10-02 13:37:01 +01:00
chairmanpow 0b71937624 Options: the era AddOns page, one checkbox per installed addon, then Load out of date AddOns
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.
2026-10-02 13:36:51 +01:00
chairmanpow 28cac71578 world map: the AreaPOI walk extracted, with the Alterac Valley objectives gating by world state
`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.
2026-10-02 13:36:51 +01:00
chairmanpow 3247bc7631 TurtleWoW compat: attach on the display model, sides for races 9/10, table-returning wipe, no-op arena request
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.
2026-10-02 13:36:36 +01:00
chairmanpow f573d1ad67 TurtleWoW compat: restore OptionsFrameSliders, resolve races 9/10, append .blp to bare M2 textures, nil-safe wipe, explode + addon delimiters, CONTINENTS_LENGTH 2026-10-01 22:52:42 +01:00
chairmanpow 031acdd8bb Fix TurtleWoW baked NPC atlases (append .blp) plus in-progress GFX work: moonlight, torch depth, water/scene-depth, skybox 2026-10-01 21:40:02 +01:00
pkuzicandClaude Opus 5.5 72647d4ab0 GFX: volumetric light (sun/moon shafts) and moonlight
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>
2026-09-29 17:31:51 +01:00
pkuzicandClaude Opus 5.5 51a3ef5076 Merge graphics-programme (upstream v0.2.0 sync) into main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:53:47 +01:00
pkuzicandClaude Opus 5.5 884dff7747 MONKEY (sync-020): Update ambiguity ceiling re-measured on the v0.2.0 merge, 5,488 -> 5,509
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:53:25 +01:00
pkuzicandClaude Opus 5.5 c80f85f660 Merge upstream/main (samwhosung/benilla v0.2.0 ba7fe6e2) into the graphics programme
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>
2026-09-27 12:09:38 +01:00
Sam SungandClaude Opus 5.5 16d060a7a8 release 0.2.0 (#468)
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>
2026-09-27 15:10:12 +07:00
Sam SungandClaude Opus 5.5 980378b696 With realms in more than one category, the realm list shows the stock category tabs, so every realm can be reached (#467)
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>
2026-09-27 14:27:13 +07:00
Sam SungandClaude Opus 5.5 365ff7b758 README: the Windows forms of the run command and the WoW link, since the POSIX VAR=value prefix does not run in cmd or PowerShell (#466)
Fixes #465

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 14:12:32 +07:00
Sam SungandClaude Opus 5.5 15ca25b6f5 A released player's corpse takes a resurrection: the targeting cursor, the pick and the click bind it as 1.12.1 does (#462)
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>
2026-09-27 14:01:33 +07:00
Sam SungandClaude Opus 5.5 ce9d3f23cb The ground tilt bounds its duration with the reference's two compares, so a cameraTerrainTiltTimeMin above cameraTerrainTiltTimeMax, or a NaN, no longer panics the client (#459)
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>
2026-09-27 13:39:41 +07:00
Sam SungandClaude Opus 5.5 243fd09154 Logging in to an account with no characters lands on the empty character list, as in 1.12.1, instead of creating a Human Warrior (#457)
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>
2026-09-27 10:43:33 +07:00
Sam SungandClaude Opus 5.5 0106b66b25 A unit's reaction caps the reputation rank at Revered, as in 1.12.1, so an Exalted faction's NPC no longer breaks the target frame (#455)
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>
2026-09-27 10:13:15 +07:00
Sam SungandClaude Opus 5.5 519602308b contributing: an issue labelled in progress is taken, and a draft pull request marks one you start (#454)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 09:38:57 +07:00
Sam SungandClaude Opus 5.5 a5cae4eb40 The build line names its release: benilla build 0.2.0 · <sha>, and 0.2.0+N for a commit N past its tag (#452)
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>
2026-09-27 08:48:12 +07:00
e4cc695f84 A friendly or resurrection spell with no usable target brings up the targeting cursor, and autoSelfCast boots at 1.12.1's 0 (#445)
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>
2026-09-27 07:56:21 +07:00
pkuzic 985c9ec0ea Merge graphics-programme (upstream samwhosung/benilla b17be271) into main 2026-09-26 22:28:50 +01:00
pkuzicandClaude Opus 5.5 5e6a9ec4a0 MONKEY (upstream-merge): Update ambiguity ceiling re-measured on the merged tree, 5,582 -> 5,488
Upstream lowered its own ceiling to 5,463; this branch's lanes add 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:27:38 +01:00
pkuzicandClaude Opus 5.5 853c09f44d Merge upstream/main (samwhosung/benilla b17be271) into the graphics programme
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:28:31 +01:00
pkuzic ff8a02242b Merge graphics-programme (visible wind + strength slider, skybox flags, shaft water mask, Optional sky & grading builder) into main 2026-09-26 18:18:10 +01:00
pkuzic ef5eb5e5d1 Merge gfx/optional (asset-free sky & grading builder) into integration 2026-09-26 18:00:44 +01:00
pkuzic 557cb71319 Merge gfx/leftovers (skybox flags 0x8/0x10, shaft water mask, shadow-caster warm, torch terrain census) into integration 2026-09-26 18:00:43 +01:00
pkuzicandClaude Opus 5.5 0299979121 MONKEY (fix-wind): visible foliage wind + Foliage Wind Strength slider
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>
2026-09-26 17:53:32 +01:00
pkuzicandClaude Opus 5.5 bd756f2a72 MONKEY (leftovers): torchTerrainShadows evidence - census light height, clearance, near count
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>
2026-09-26 17:09:13 +01:00
pkuzicandClaude Opus 5.5 af38c6eec8 MONKEY (leftovers): warm the two sun-shadow proxy lanes in the menagerie
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>
2026-09-26 16:51:30 +01:00
pkuzicandClaude Opus 5.5 37dcd39e51 MONKEY (fix-wind): log the live foliageWind tier; test the player-path rows
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>
2026-09-26 16:21:45 +01:00
pkuzicandClaude Opus 5.5 ffaa7863d3 MONKEY (leftovers): sun shafts - skybox 0x8 forces them; the sky mask ignores depth-0 below the horizon
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>
2026-09-26 15:55:38 +01:00
pkuzicandClaude Opus 5.5 218ff0cfc0 MONKEY (leftovers): LightSkybox flags 0x8/0x10 plumbing; 0x10 suppresses the Modern sun-fog lobe
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>
2026-09-26 15:55:15 +01:00
pkuzicandGPT-5.6 Sol (Codex) 67388625ec MONKEY (optional): add local sky and grading patch builder
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)
2026-09-26 15:19:25 +01:00
pkuzicandClaude Opus 5.5 5102550b2e README: Everwood graphics fork header — features, upstream link, open contributions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:27:33 +01:00
Sam SungandClaude Opus 5.5 6982109d86 A repair the purse cannot cover is refused with the not-enough-money error, as in 1.12.1 (#443)
- 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>
2026-09-26 18:01:39 +07:00
Sam SungandClaude Opus 5.5 211f9bfabf Repair costs and flight fares take 1.12.1's price discount: 10% at Honored, up to 10% more by honor rank at a PvP-flagged vendor (#439)
- 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 #411
Fixes #434

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 17:20:10 +07:00
Sam SungandClaude Opus 5.5 d13648a5e0 Repair mode ends on a right mouse-down in the world and when a targeted spell arms, as in 1.12.1 (#435)
- 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 #420
Fixes #422

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 16:23:36 +07:00
Sam SungandClaude Opus 5.5 5c7ef3a402 contributing: where to start, a pull request finished here, and the counts and paths as they are (#433)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:56:47 +07:00
Sam SungandClaude Opus 5.5 05b1441c96 ClearCursor signals CURSOR_UPDATE on every call, an empty cursor included, as 1.12.1's shared tail does (#432)
Fixes #425

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:38:21 +07:00
Sam SungandClaude Opus 5.5 863998aa54 Right-clicking a bag or worn item puts a held item back first, as 1.12.1 clears the cursor before any use (#431)
- 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 #413
Fixes #421

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:25:41 +07:00
Sam SungandClaude Opus 5.5 cf969431a3 Repair All greys out after a repair: its total is swept before the inventory event that refreshes the button (#426)
Fixes #417

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:09:32 +07:00