Commit Graph
1685 Commits
Author SHA1 Message Date
chairmanpow 3ba52ee608 build: line tables only and unpacked debuginfo, so the dev tree stops filling the disk
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.
2026-10-03 17:41:41 +01:00
chairmanpow 445624217a ui: load TurtleWoW's Transmog and Shop windows
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.
2026-10-03 17:41:41 +01:00
chairmanpow d9c820ea27 anim lod: a far on-screen rig parks, so a city crowd costs no per-bone work
`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.
2026-10-03 17:41:40 +01:00
chairmanpow 55040a63ea Revert "shadow: skin the character caster on the GPU, so a crowd costs no CPU skin"
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.
2026-10-03 17:14:49 +01:00
chairmanpow 4c15ab4bdd shadow: skin the character caster on the GPU, so a crowd costs no CPU skin
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.
2026-10-03 16:37:38 +01:00
chairmanpow 6eaba32006 wipe: reset 5.0's remembered table size, so a refilled list counts from 1
`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.
2026-10-03 16:23:39 +01:00
chairmanpow bd0e24e9a9 ui: load TurtleWoW's LFT group finder, so its minimap eye and /lft exist
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.
2026-10-03 16:23:39 +01:00
chairmanpow 8c9b1d9984 TurtleWoW compat: the create screen loads races 9/10, so the character and Blood Elf stage show
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.
2026-10-03 15:51:27 +01:00
chairmanpow 4ed0d0c5ca gossip: log each option's label, so a row's meaning is readable
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.
2026-10-03 14:08:51 +01:00
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
pkuzicandGPT-6 Astra (Codex) f5547a63bd MONKEY (gfx): keep named rock doodads and ground clutter rigid in wind
Reject rock, stone, boulder, pebble, gravel and rubble model filenames from retained foliage. Give matching ground-detail models zero bend height so neither grass wind nor player parting moves them. Limit the name veto to the filename to preserve vegetation in stone-named zones.

Verified on Windows: two foliage classifier tests and one rigid-model test pass; benilla-app lib tests have 4313 passed, 0 failed, 10 ignored (no-install run); play build passes. Elwynn wind captures at 0 and 3 seconds preserve coloured foliage motion. The tester's specific small-rock assets still need an in-game retest.

Co-Authored-By: GPT-6 Astra (Codex)
2026-10-03 11:48:53 +01:00
cf891dc375 fix: close a loot window that opens while you move, as 1.12.1 does (#786)
- A loot window that opens while you move closes, as in 1.12.1 (#782)
- Set auto_loot in the LootConfig initializer (clippy field_reassign_with_default)
- Close a window opened on the move in the feed's pass, so it never paints

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-03 17:36:21 +07:00
pkuzicandGPT-6 Astra (Codex) fbeb0f02f6 MONKEY (gfx): separate fading foliage from downstream material markers
Move the foliage wind marker from bit 14 to bit 15 in its Rust producer and WGSL consumer so distance-fade twins cannot trigger a downstream fragment effect. Keep bit 14 reserved for those extensions.

Verified on Windows: cargo build -p benilla --profile play; cargo test -p benilla-app --lib (4313 passed, 0 failed, 10 ignored; no-install run). Public-base and fixed captures at the reported Elwynn position are pixel-identical; downstream integration still needs its in-game retest.

Co-Authored-By: GPT-6 Astra (Codex)
2026-10-03 11:22:53 +01:00
pkuzicandClaude Opus 5.5 494d232c11 Merge upstream benilla main (b396bbf6) into everwood-graphics
105 upstream commits (v0.2.0 ba7fe6e2 -> b396bbf6) onto the graphics programme.

Conflicts resolved, keeping both sides:
- OptionsFrame.xml: upstream renamed every layer global (Options* -> BenillaOptions*,
  OPTIONS_* -> BENILLA_OPTIONS_*, KeyBindings* -> BenillaKeyBindings*, ...) and moved handlers
  to `this`; the Advanced Graphics page (rows, strings, presets) is carried over onto the new
  names, and upstream's Click-to-Move / Spell Detail rows and nameplate store are kept.
- cvars.rs -> cvars/{mod,table}.rs split: our REGISTERED rows go into table.rs, the preset
  ladder + tests into mod.rs beside upstream's ffx/gxMaximize tests.
- video.rs: lighting arms + upstream gxMaximize/ffx arms; VideoConfig keeps both field sets.
- ffx_glow.rs: SkyDither and FfxSwitches both registered/extracted; dither lane on
  upstream's switched player state.
- dest_fx.rs: our AreaLightPlan radius + upstream's spellEffectLevel shard scale.
- fixtures.rs: seed_perf_crowd + upstream's set_enemy_plates.
- options_tests.rs: tipped/described counts 100/99 (upstream 80 + ours 20/19); our Advanced
  Graphics tests use the renamed globals and `this`.
- game_plugins.rs: Update actionable ceiling 4,999 = upstream 4,980 + 19 ours (measured).
- docs/MAP.md regenerated with scripts/genmap.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:35:23 +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
b396bbf69a Click to Move walks to the ground, along the sky and into melee (#783)
With Click to Move on, benilla walked only to things you interact with. In 1.12.1 a right-click on the ground walks to the point (`0x5e0378` → `0x6102b0`, 0.5 yd short, speed-scaled, no leash, held in a deck's frame). A right-click on the sky with an empty cursor walks along the ray (`0x492dca` → `0x610100`), pitching while swimming. An attack on an enemy out of melee walks to where it stood (`0x60c22b` → `0x60fcc0`, mode 10): the stop is about 1.9 yd because the arm names the player's own guid, the radii are unscaled, and the 80 yd leash says ERR_AUTOFOLLOW_TOO_FAR. Inside melee the player turns to face it.

Deviations, each written where it lives: the melee point keeps the enemy's height, does not snap to the reference's fixed arrival heading, and rides the deck it was armed on.

Tests: the router, the steering, the gates, the leash and the deck frames.

Fixes #758

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mathih13 <15229325+Mathih13@users.noreply.github.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-02 21:03:36 +07: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
Mathih13andClaude Opus 5.5 9821765451 A loot window opening on a unit selects it, as in 1.12.1 (#779)
In 1.12.1 the `SMSG_LOOT_RESPONSE` handler selects a looted unit before it opens the window (`0x5ebc35` → `0x48f3a0`, SetSelection at `0x48f3be`). benilla opened the window without selecting it. So a Tab pressed during a Click-to-Move walk to a corpse left the Tab target selected when the loot opened.

The handler now admits the response and arms the latch, then selects a unit through the one SetSelection path, then opens the window. A refusal, a chest or an item selects nothing. SetSelection's re-swing now uses StartAttack's target gate (alive, then CanAttack, `0x5ecc16`), so switching to a body mid-swing never swings at it.

Tests: the handler's select, dedup, refusals, teardown order and swing stop, a hostile body looted mid-swing, and the stock TargetFrame naming the looted body.

Fixes #763

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 17:33:06 +07:00
HEOJUNFOandClaude Opus 5.5 6c25640a37 Light bands with out-of-order keys interpolate as a cycle, as in 1.12.1 (#768)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:14:41 +07:00
Andrew Barnes c627483a85 Return numeric flags from spell-targeting predicates (#764)
Use binding_abi::flag for SpellIsTargeting and SpellCanTargetUnit so addons
can compare their results with 1, as in 1.12.1. Keep nil for false.

Verified on Windows with Rust 1.98.1: three regression failures before
the fix, all 35 focused spell tests passing after it, formatting and
render-pass lint clean. The full UI run passed 1677 tests, with one
ignored and one traceback-name failure reproduced on untouched main.

Fixes #711
2026-10-02 13:45:16 +07:00
Sam SungandClaude Opus 5.5 4fd0c643a6 One-shot emotes: LiftOff/Land in combat, no restarts, local play (#773)
Three ways the one-shot emote player differed from 1.12.1.

- **LiftOff and Land were refused in combat.** `0x5fcd20` exempts AnimIDs 192 and 200 from its combat test only (`0x5fcd5f`-`0x5fcd92`), so a dragon's take-off and landing now play mid-fight.
- **An emote could restart a clip already armed.** The player skips an id already armed on the key bone or bone 0 (`0x5fcd56`, `0x5fdb50`). It compares the requested id, which op4 stores at `+0xf8` (`0x71252f`), not the fallback that plays.
- **Your own emote waited for the server.** `DoEmote` plays it locally while a direction key is held (`0x5fd680`, called at `0x5ef660`), so a moving `/wave` shows at once and its echo is skipped.

Tests cover the exemption, the armed test (including on a rider), each local-play gate, and the skipped echo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 10:58:50 +07:00
Sam SungandClaude Opus 5.5 56d8011ab7 NPCs pause their work loop while you talk to them, as in 1.12.1 (#772)
Two ways the emote-state base pick differed from 1.12.1.

- A blacksmith kept hammering while you used his vendor window. The resolver `0x5fd770` passes over a state whose `Emotes.dbc` flags have `0x2000` (work, mining, ready, dance) while the unit is the interact NPC (`0x5fd7e2`-`0x5fd7fd`). `SetInteractNPC` re-picks on open (`0x493198`) and on close (`0x493219`, before the name clears at `0x49334b`), never for a player. So the NPC stands, and the loop resumes at its next re-pick.
- A set state outranks the idle fallbacks: `0x5fd770` runs before `0x5fd830`, and a row with AnimID 0 claims Stand. After `/stand` (state 26), a swimmer at rest shows Stand until it moves.

Tests cover the open, the close hold, players, a pose under a one-shot, and the precedence order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 10:32:20 +07:00
Sam SungandClaude Opus 5.5 2dc7d8a9e6 /wave <name> targets the named player; two DoEmote gates follow 1.12.1 (#769)
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>
2026-10-02 09:23:08 +07:00
Sam SungandClaude Opus 5.5 fbea2d9f3a Emote routing follows 1.12.1's upper-body and mount tables (#767)
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>
2026-10-02 09:02:52 +07:00
Sam SungandClaude Opus 5.5 4f36c843f1 Emote voices play only for you and your party, as in 1.12.1 (#766)
Every text emote's voice played for every performer, so every `/cheer` in a city was heard. 1.12.1's composer tail (`0x623c80`) plays it only for you or a member of the four party slots (`0x623cc2`-`0x623cce`, `0x5efea0`), and drops an ignored sender before that (`0x49dc36`).

What changed:
- The voice plays at attachment `0x11` and stops the unit's previous emote voice once a new one opens (`0x623c10`).
- Race and sex come from the display override before the descriptor (`0x60c6a0`/`0x60c6c0`).
- `SMSG_EMOTE` no longer plays `EventSoundID`. The one-shot path never reads it (`0x5e66b0` → `0x5fcd20`); only the state emote's `$ESD` does.

Tests cover each relation class, an ignored performer and the per-unit voice slot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 08:32:29 +07:00
e620a0d095 feat: let a crate on top interact with an object it picks (#753)
- A crate on top of benilla interacts with an object it picks through `Interact`, the object's own dispatcher with no select, and reads the streamed objects through `Objects`
- A right-click on a GameObject, a corpse or empty world no longer marks the selection changed; only the unit leg's select touches it, as before the click took the pick
- Interact's attack leg selects its unit through StartAttack, as the click does
- An outside crate test reads Objects and writes Interact
- Wrap the Interact finish's doc lines at the column limit
- The outside-crate test's comment says what it picks

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-02 06:38:12 +07:00
2ce6a3e881 feat: click to move walks to an out-of-reach target, then interacts (#752)
- Click to Move: with AutoInteract on, a right-click on an NPC, a body to loot or skin, or a GameObject out of reach walks there and runs the verb on arrival
- Click to Move: the arrival scales by the mover's current speed, and a Use still out of range walks again
- Click to Move: a Loot walk stops at the square root of the melee reach
- Put the meeting-stone test's doc comment back on its test
- A GameObject click out of range raises ERR_USE_TOO_FAR unless the walk starts
- Wrap the finish's doc lines at the column limit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-02 05:57:47 +07:00
Mathih13andClaude Opus 5.5 cfd5fd2c8a fix: stop shift-click opening the stack splitter on vendor bundles (#751)
- 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>
2026-10-02 04:54:20 +07: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
Sam SungandClaude Opus 5.5 f22ec0016c EditBox caret keys, selection and focus blink follow 1.12.1 (#750)
Several EditBox keys differed from 1.12.1:
- Up/Down did nothing in a multi-line box;
- Home/End went to the text's ends;
- `HighlightText` and Ctrl+A moved the caret;
- focus did not restart the blink;
- a non-digit kept a numeric box's selection;
- Option+Up could not recall chat history on a Mac.

In 1.12.1, Up/Down move a wrapped row, keeping the letter column (`0x77cb20`), or recall history in a single-line box with any modifier but Shift. Home/End stop at a newline (`0x77c980`, `0x77c9f0`). `HighlightText` writes only the selection (`0x77cca0`), and Shift extends by `0x77cd10`. Focus restarts the blink (`0x77dd68`), and Insert deletes the selection before its digit test (`0x77bf41`).

benilla now does each. Down onto the last row lands at the end, and history stays shell-style, both deviations.

Tests cover each key, in the engine and the stock mail and ticket boxes.

Fixes #735
Fixes #736
Fixes #737
Fixes #738
Fixes #745
Fixes #747

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 10:56:03 +07:00
Sam SungandClaude Opus 5.5 be9e3d8aac Keys are named by the active layout, as 1.12.1 names them (#748)
Letter and punctuation keys were named by their US position, so on AZERTY the key labelled Z bound as W, and Ctrl+A sat on the key labelled Q.

1.12.1 on Windows names a key from the VK its layout produced. `VK_A`–`VK_Z` and digits are named as themselves, and other keys by `MapVirtualKeyA` (`0x42da39`); a key with no character is dropped (`0x42da49`). The EditBox's Ctrl chords switch on that same name (`0x77b1fb`). The Mac client names keys by `KeyTranslate`.

Now:
- Windows takes the same path through `MapVirtualKeyExW`.
- The Mac names keys through the current layout, or the ASCII-capable layout it pairs with.
- Linux follows one documented rule.
- Bindings, Key Bindings capture and Ctrl/Cmd+A/C/X/V all follow the layout.
- A character above U+00FF names its key, as a deviation.

Tests cover AZERTY, Hebrew, Greek, Lithuanian, Turkish, Bulgarian, Hungarian and Dvorak names, layout switches and chords.

Fixes #621
Fixes #746

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 09:55:26 +07:00
Sam SungandClaude Opus 5.5 e9f0e89a0b EditBox events fire from each box's flush, as 1.12.1 orders them (#734)
EditBox events fired from hoisted drains before any OnUpdate, OnTextChanged before OnCursorChanged, and only on an edit.

In 1.12.1:
- Each box's update `0x77a790` fires its own `OnUpdate` and then its flush `0x77d3e0`, and a key-down or click flushes first (`0x77b1e2`, `0x77b819`).
- The flush runs off dirty bits in `[E+0x31c]`: relayout on bit 0, then `OnCursorChanged` on bit 2, then `OnTextChanged`.
- The ctor sets bit 0, so a box fires at its first show. Every text-region re-seat raises bits 0–2 (`0x77ba7f`), so a resize, new insets, `SetMultiLine` or a font change fires it again.

benilla now keeps the same bits and flushes each box in the walk and at its keys and clicks. So typing DELETE and pressing Enter in one frame enables the popup's button, as in 1.12.1.

Tests cover the order, first show, re-seats, a key's flush and no-op moves.

Fixes #724
Fixes #727

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 07:38:38 +07:00
Mathih13andClaude Opus 5.5 87f65faffd fix: stamp the chat log and FrameXML.log in local time (#723)
The reference stamps each log line from GetLocalTime (0x65a871); benilla stamped UTC. Unix reads
localtime_r, Windows calls GetLocalTime. Built and tested on macOS; the unix path type-checked for
x86_64 Linux and the Windows path for x86_64-pc-windows-msvc in a scratch crate.

Fixes #586

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 07:21:57 +07:00
Sam SungandClaude Opus 5.5 4772489aad Arriving bodies composite off the main thread and show once dressed (#732)
A crowd of arriving players was composited in one frame, on the main thread, under the archive lock: 40 geared looks took 76–115 ms.

In 1.12.1 an unforced full composite never waits on its section loads (`0x44b430`), and a unit whose first composite is pending is not drawn: ShouldRender `0x607da0` feeds the draw flag `0x710c50`, and the flag goes to the mount model when mounted. It appears dressed once the composite is done.

Now an arriving body composites on the async pool, one task per look, reading outside the lock. The body, and its mount, are built once the atlas lands. Arriving now costs the main thread 0.26–0.45 ms for 40 looks. The appear fade runs from arrival, as `FadeTo` is armed at create.

A re-dress, a rig heal and the glue and dressing-room previews stay in the frame. Baked NPC skins are unchanged.

Fixes #609

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 20:14:10 +07:00
Sam SungandClaude Sonnet 5.5 15e83f2483 Ghost and underwater ambience beds resolve by their reference names (#731)
A ghost heard no ambience. The ghost bed was looked up as a SoundEntries row named "Ghost", which does not exist, so it fell back to kit 0. The underwater bed was a hard-coded row.

At init, 1.12.1's `0x4609b0` walks SoundEntries from the last row down. It keeps the rows whose names match `"Ghost (DONOTRENAME)"` (`0x836444`) and `"Underwater (DONOTRENAME)"` (`0x836428`) exactly and case-sensitively (`0x64a480`, a `strncmp` through the NUL). In the install those are rows 4160 (`GhostState.wav`) and 4209.

The catalog now resolves both beds once, the same way, and the ambience selector takes them from it, with order and fades unchanged.

Tests cover the install's two rows, the exact-name match, the walk order, a ghost selecting its bed over water, and an unresolved bed staying silent.

Fixes #721

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 19:43:09 +07:00
Sam SungandClaude Sonnet 5.5 7e117809b2 /errors: the selected error is copyable, in a read-only EditBox (#730)
`/errors` showed the selected error in a FontString, so a player could not select or copy the message and traceback they came to report.

The detail pane now hosts a multi-line EditBox the way stock `MailFrame.xml` hosts the mail body:
- `autoFocus="false"`, so the keyboard stays with the game until a click;
- Escape clears focus;
- a mouse-enabled scroll child that focuses the box on a click below its text;
- `ScrollingEdit_OnTextChanged` for the range, since the box is as tall as its text.

The box is read-only: an edit is undone on `OnTextChanged`. Focus selects all, so Ctrl+C copies the whole error, and a new error arriving keeps the selection. The text keeps its font, its 6/6 inset and its 270 wrap.

Tests cover the whole text in the box, the undone edits, select-all and copy, a click below the text, Escape, and the range over a long traceback.

Fixes #703

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 19:18:52 +07:00
Sam SungandClaude Opus 5.5 f2e71ccebd A multi-line EditBox grows with its text; a Lua-made box draws its text (#729)
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 #722
Fixes #726

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 18:37:30 +07:00
Sam SungandClaude Opus 5.5 4c18c23c94 Ambience beds stream from the archive, opened off the main thread (#725)
An ambience change read and decoded the whole bed on the main thread, about 40 ms a swap. 1.12.1 opens the bed as a looping FMOD stream that reads the archive through its file callbacks (`0x460b54` → `0x7a54d0`, `FSOUND_Stream_Open` at `0x7a5525`).

Now:
- MPQ files open for in-place reads, one sector at a time.
- The bed streams from the archive, opened on the IO pool. A newer swap drops an open still in flight.
- kira moves to 0.12.4 on symphonia 0.6. 0.5 sized PCM frames from `nBlockAlign`, which 98 stereo WAVs store as 2, so a streamed bed fell silent halfway.
- A fork patch stops a stream at its last packet instead of spinning.
- A sector that inflates short is refused.

Tests cover a bed's true length, a seamless loop, an off-thread open, the stop at the last packet and the refusal.

Fixes #700
Fixes #8

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 17:22:42 +07:00
Sam SungandClaude Sonnet 5.5 cd34ab77d2 /errors: put Clear and Close in the panel art's sockets (#720)
The `/errors` window wears the ClassTrainer panel art, but its Clear and Close buttons sat at y −455, below the art, and its detail pane ran to −442, into the button sockets.

On that art the class trainer lays out (`Blizzard_TrainerUI.xml`, `ClassTrainer_SetToClassTrainer`):
- a 296×184 list at (−67, −96) from the top right, with 11 rows;
- a 119-tall detail pane 8 below the list, ending at −407;
- Train and Exit, 80×22, centred at (224, −420) and (305, −420).

The log now uses those frames: Clear and Close sit in the sockets, the detail ends above them, and the list shows 11 rows, not 13.

The new test lays out the stock trainer and the log in one UI and checks that each log frame equals the trainer's, with the buttons inside the window. It fails on the old layout.

Fixes #698

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 15:31:35 +07:00