0e04e54fbff6b49e4d90815d32ff2a2ea8eff4cf
511
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0e04e54fbf |
pet bar: a spell press starts the pet's global cooldown on the pet's list, as 1.12.1's spell arm does before it sends (#636)
A pet-bar spell press armed no global cooldown on the pet's list. Claw or Firebolt sent `CMSG_PET_ACTION`, and no slot on the pet bar swept, so the bar looked ready while the pet could not cast. Only a spell with its own recovery timer, such as Bite, showed a sweep. In the reference, the pet bar's spell arm (`0x4bd240` in `0x4bd1d0`) first handles two cases that skip the cooldown: - A spell that is a live aura on the pet sends the cancel and returns. - A possessed unit, or a spell with `AttributesEx4 & 0x20` (`0x4bd355`, `0x4bd35e`), takes the generic cast entry `0x6e4b60`. Every other press calls `StartGlobalCooldown 0x6e2de0(spellId, 1)` (`0x4bd367`-`0x4bd36e`) before the `0x175` send. That call applies the spell's own `StartRecoveryCategory`/`StartRecoveryTime` pair, under spell-mod op 21, into the pet's list (`0xcecaec + 0x18`). This press is the only place the pet's GCD starts, because the `SMSG_SPELL_GO` pet leg inserts none (`0x6e85f7`). Changes: - `SpellDisplay` reads `AttributesEx4` (column 10). - `PetPress::press` arms the pet list's GCD through the existing `Cooldowns::start_gcd`, with the player's modifier tables. It skips the arm for the cases above and for an unresolved record or pet object (`0x4bd2fe`, `0x4bd34f`). - The feed's cooldown diff then fires `PET_BAR_UPDATE_COOLDOWN`. In the shipped `Spell.dbc` the pair is 133/1500 for most pet spells (later Claw ranks, Bite, Lash of Pain, Torment, Suffering) and 133/1000 for Firebolt and Claw rank 1. A real-data test pins both values. Tests: - `ui_pet/press_tests.rs` presses through `PetPress::press_slot` and covers: - the GCD across the slots sharing the category, read back through `GetPetActionCooldown`; - no pair; - possessed; - `AttributesEx4 & 0x20`; - the aura cancel; - no pet object; - op 21. - `real_pet_spells_carry_the_gcd_pair_and_ex4_reads_column_10` reads the real `Spell.dbc`. Fixes #589 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
66340800a9 |
unit tokens: partypetN and raidpetN resolve to the member's held pet, tested before party and raid, and the reach feeds cover them (#635)
`UnitTokens::resolve` had no arm for `partypetN` or `raidpetN`. `"partypet1"` fell into the `party` arm and failed to parse `pet1`, and `"raidpet1"` did the same in the `raid` arm. As a result, the stock TARGETPARTYPET1-4 bindings, the pet half of the TARGETPARTYMEMBER toggle, `TargetUnit`, `AssistUnit` and `SpellTargetUnit` all did nothing on a party or raid pet. The reference's resolver `0x515970` tests `partypet` before `party` (`0x515a9a`) and `raidpet` before `raid` (`0x515b1c`): - `partypetN` (`0x491930` → `0x4e81d0`) takes the Nth party slot's member. It names that member's `CHARM`, else its `SUMMON`, while the member is held. Otherwise it names the pet guid on the party record, behind the member's online bit (`0x4e8221`-`0x4e8230`). - `raidpetN` (`0x491960`) does the same for the Nth raid row, bounded by the raid count, with no online test. Changes: - The two arms resolve through the existing `GroupState::party_pet_guid` and `raid_pet_guid`, then the object manager. - The reference's `partypetN` has no bound. Here an index outside 1-4 names nobody, as it does for `partyN`. - The four group arms now share one digit parse, `slot_number`, which is digits-only as the reference's is. So `"party+1"` no longer resolves. - `reach_tokens` now covers the pet tokens, so `CheckInteractDistance`, `CanInspect` and `SpellCanTargetUnit` measure them. - The token tables `ui_aura` kept privately now live in `ui_party` beside `PARTY_TOKENS` and `RAID_TOKENS`. Tests: - `ui_unit.rs`'s `group_pet_tokens` covers the order against `party`/`raid`, case folding, the bounds, charm-else-summon, the record leg and its online bit, raid rows and the reach feed. - `target/script.rs` adds `target_unit_selects_a_party_pet`. This is the resolver half of #448. The per-token unit snapshot that `UnitExists`, `UnitName` and `UnitHealth` read does not push the pet tokens yet, so the party pet frames stay hidden until that half lands. Part of #448 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
f768cf3e60 |
CastSpellByName's onSelf and CastSpell's third argument cast the spell at the player, the pet book's at the player too, read through GetBoolOrDefault as the reference does (#634)
`CastSpellByName(name, onSelf)` dropped its second argument, and `CastSpell(slot, book, onSelf)` read only two. So a self-cast macro line such as `CastSpellByName("Power Word: Fortitude", 1)` buffed the selected friend instead of you. With no friendly target and `autoSelfCast` off, it did not cast at all.
In the reference, `CastSpellByName` (`0x4b4ab0`) reads its second argument through `GetBoolOrDefault(L, 2, 0)` (`0x4b4aea`-`0x4b4af1`). `CastSpell` (`0x4b42f0`) reads its third the same way (`0x4b4333`-`0x4b433c`). When the value is nonzero, the active player's guid replaces the selection as the guid handed to the dispatcher `0x4b3300` (`0x4b4afa`, `0x4b4345`). The player's book then binds it through `ArmCast`'s explicit-guid leg (`0x6e5393`). The pet book sends it as `CMSG_PET_ACTION`'s target (`0x4b34ce`).
- `ScriptCall::CastSpell` and `CastPetSpell` now carry `on_self`, read with `binding_abi::bool_or_default`. That is not Lua truthiness: `"0"`, `"false"` and `0.5` are false; `"yes"`, `"on"` and `1` are true.
- The action bar's self-bind swap is now `CastContext::self_bound`, which `UseAction`'s `onSelf` and the two script casts share. `ui_spellbook::cast_spell` applies it and hands `on_self` to the attack pick.
- The pet book's `CMSG_PET_ACTION` aims at the player under `onSelf`.
Tests:
- `cast_spell_by_name_reads_on_self_as_the_reference_coerces_it` and `cast_spell_reads_on_self_from_its_third_argument` cover the coercion.
- `on_self_casts_at_the_player_and_leaves_the_selection`, `on_self_binds_the_player_where_the_selection_is_not_assistable` and `a_pet_book_cast_on_self_aims_at_the_player` check the wire target through `apply_script_calls`.
Fixes #446
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
666300c6a5 |
doodad anim gate: a meshed host is drawn only while its fade sphere intersects the world camera's frustum, like a meshless one (#633)
A meshed doodad counted as drawn whenever one of its parts had a `Visibility` other than `Hidden`. Bevy's frustum culling writes only `ViewVisibility`, so a doodad behind the camera kept an `Inherited` part and went on animating and ticking its particles. The reference animates (`0x714260`) and ticks (`0x718960`) only the M2 scene's worklist, which `0x707680` builds. A doodad is admitted to that worklist only after the 6-plane frustum test `0x683700`. `gate_doodad_anim` now also tests a meshed host's fade sphere against the world camera's frustum. That sphere is the doodad's world bounding sphere, fixed at spawn. A meshless host already had the same test, and both branches now share one closure. The `Visibility` term stays, because it carries the fade, exterior and room verdicts. Test: `a_meshed_host_outside_the_frustum_is_parked`. Two meshed hosts with `Inherited` parts sit one behind the camera and one ahead of it. Only the host behind is parked. Fixes #619 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
cd441385c7 |
zone music: a start that plays nothing leaves the pump armed, so the next frame retries with a fresh pick, and a bad path warns once (#632)
The zone music pump cleared its due time before asking for a track, and a zone-music change started its track inline with nothing armed behind it. So a start that played nothing left the zone silent until the next change. Any of these did it: no kit for the phase, or a weighted pick landing on a file the install does not ship. The Crossroads (and every area on its kit) ships 1 of its 4 tracks, Alterac Valley's kit 6 of 8, and Everlook's has no night kit. In the reference, the pump `0x460040` calls the start `0x460240` whenever no stream is live and the due time `[0x836400]` is `-1` or past. The start returns without writing `[0x836400]` on every failure path (`0x46024b`, `0x46025f`, `0x46027b`-`0x460282`, `0x4602a4`), so the next tick retries with a fresh pick (`0x45bb70`). Now: - `pump_zone_music` spends the deadline only when a stream opened; - `begin_zone_music` arms a retry (`Some(now)`, the change's `-1`) when neither the intro nor the zone track started; - a path that cannot play warns once, not once per retry. `a_pick_that_lands_on_a_missing_file_is_retried_until_one_plays`, `a_zone_change_whose_start_fails_keeps_the_pump_armed` and `a_phase_with_no_kit_stays_armed_until_the_other_phase_has_one` fail without the change. Four more pin the edges: warn once, a started change arms nothing, no row drops the deadline, a failed intro falls through unstamped. Built and gated on macOS. Fixes #64 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
06f9cc93df |
char create: the CharBaseInfo guard requires every shipped race/class pair and lets a patch add more, as the class-list builder lists every row (#631)
`CharCreateCatalog::load`'s misparse guard demanded each race's `CharBaseInfo` classes equal the shipped set exactly. A patch that adds a race/class row therefore failed the load, and without the catalog the create screen is disabled and the glue preview stands no body. 1.12.1's class-list builder (`0x4706b0`) lists every `CharBaseInfo` row for the selected race, skipping only Dwarf+Mage (`0x4706e3`-`0x4706ec`), so the patched class appears there. The guard, now `shipped_combos_present`, requires every shipped pair to be present and lets extra pairs through, so a misparse that loses a row still refuses. `a_patched_combo_passes_the_guard_and_a_lost_one_does_not` fails under the old exact-match rule. Built and gated on macOS. Fixes #617 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
6e9e5e995d |
ui_dialog_verbs: a battleground's run time is the time since its status plus the wire's elapsed ms, read forwards so no Instant precedes the clock's origin (#630)
`GetBattlefieldInstanceRunTime`'s stamp was set backwards, as `now - Duration::from_millis(elapsed_ms)`, with `elapsed_ms` the u32 from `SMSG_BATTLEFIELD_STATUS`'s in-progress tuple. On Windows, std's `Instant` is a `Duration` from the performance counter's origin, so a run longer than the machine's uptime panics there. On macOS and Linux the subtraction cannot panic. The stamp is now the status's arrival and the wire span, and `run_time_ms` reads `now.saturating_duration_since(at) + elapsed`. That is the old value everywhere the old one did not panic, taken forwards the way the queue's time waited is. `a_run_longer_than_any_uptime_reads_forwards` pins a 49.7-day run, and the status-handler test now also checks that the stamp runs on after the status. Built and gated on macOS; the Windows target was not built. Fixes #627 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
57f33aa620 |
guild roster: zone and class keys order a miss after every name and NaN days get a place, so the comparator is a total order (#629)
The guild roster's zone and class keys went through `abstaining`, which answered `Equal` whenever either value was empty. So `"" = "Mage"` and `"" = "Warrior"` while `"Mage" < "Warrior"`: not a total order, and since Rust 1.81 `sort_unstable_by` may panic on one. The feed writes `""` for any class or zone id with no name. In the same comparator, the both-offline arm ordered `last_online_days`, a wire float, with `partial_cmp().unwrap_or(Equal)`, so a NaN equalled everything. The reference's class arm (`0x4d0e25`-`0x4d0e2f`, `ChrClasses`) and zone arm (`0x4d0e9f`-`0x4d0ea5`, `AreaTable`) jump to `0x4d0efd` when either row is missing: a tie that passes to the next key. Now: - `misses_last` compares two resolved names as before; - two misses tie, so the next key decides, as in the reference; - a miss orders after every name, marked as a deviation, since no total order can tie there; - the offline arm uses `f32::total_cmp`. `every_key_is_a_total_order_over_mixed_rows` checks antisymmetry and transitivity for Zone, Class and Online over 24 mixed rows, a NaN included. `sorting_a_mixed_roster_puts_misses_after_hits` checks the sorted order. Both fail with the old arms, as does `the_online_tiebreak_and_the_dbc_miss_order`. Built and gated on macOS. Fixes #614 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
45825b0609 |
ui_battlefield: a queued battleground's time waited is the time since the status plus the wire's waited ms, computed forwards so no Instant precedes the clock's origin (#628)
A queued battleground's time waited was computed backwards: `*at - Duration::from_millis(waited)`, with `waited` the server's u32 milliseconds. On Windows, std's `Instant` is a `Duration` from the performance counter's origin, near boot, so a wait longer than the machine's uptime panics there. On macOS and Linux, `Instant` is a signed timespec and the subtraction cannot panic; the issue's platform note overstated that. The time waited is now taken forwards, as `now.saturating_duration_since(*at) + waited`. That is the same value the reference's `[slot+0x1c] = now − Δ`, read back as `now − stamp`, gives, and the old formula's value everywhere it did not panic. `a_waited_span_longer_than_any_uptime_reads_forwards` pins a 49.7-day wait reading forwards and saturating at the getter's u32. `the_slot_view_reduces_the_clocks` still gives 7 000 ms for 5 s waited plus 2 s since. Built and gated on macOS; the Windows target was not built. Fixes #615 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
61f17d46c3 |
who_sort: the class, race and zone arms order a miss after every resolved name and tie two misses, a total order sort_by can take (#626)
`dbc_name_cmp`, behind the who list's class, race and zone keys, answered `Equal` whenever either name was empty. So `"" = "Mage"` and `"" = "Warrior"` while `"Mage" < "Warrior"`: not a total order, and since Rust 1.81 `slice::sort_by` may panic on one. Empty names are real input, since `who_row` writes `""` for any id with no name. The reference's arms (`0x5ada7b`, `0x5adadf`, `0x5adb40`) look both ids up and, when either row is missing, jump to the chain loop's continue label (`0x5adbb2`, from `0x5adab6`): a tie. Its `qsort` tolerates that; ours cannot. Now: - two resolved names compare as before; - two misses tie, so the next key decides, as in the reference; - a miss orders after every resolved name. That case is marked as a deviation, since no total order can tie there. `the_dbc_arms_are_a_total_order` checks antisymmetry and transitivity over a mixed 21-row set for each key. It fails with the old arm, as do `an_unresolved_dbc_name_sorts_after_the_resolved_ones` and `sorting_a_mixed_list_puts_misses_last_without_panicking`. Built and gated on macOS. Fixes #620 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
b065554f4b |
config.toml saves one quiet second after a change, not only on the exit frame, and a unit test resolves no state folder unless it pins BENILLA_HOME (#625)
`save_config` was registered only on the exit frame, so its "one quiet second" leg never ran, and a crash or kill lost every setting changed in the session. 1.12.1's `SaveConfig` (`0x63d980`) also writes on entering the world (`0x46b6f6`) and on leaving it (`0x490c35`-`0x490c64`), not only at shutdown (`0x63daf0`). - `save_config_when_quiet` runs in `Last`, before `OnAppExit`, after every `Update` writer of the registry. It saves once the registry has been dirty for one quiet second (`SAVE_QUIET`). - `save_config_on_exit` keeps the exit-frame flush, unchanged. - `write_config` is the atomic write both share. - Under `cfg(test)`, `local_state::home()` resolves nothing unless `BENILLA_HOME` is pinned. The quiet save gives tests a write path they never had, and it could otherwise reach the primary checkout's real `benilla-config/`. The resolution tests read `resident_home()`. `a_change_saves_after_one_quiet_second_with_no_exit` fails without the quiet registration. `a_test_without_an_override_has_no_state_folder` fails without the test guard. Built and gated on macOS. Fixes #618 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
b26f7c1f20 |
terrain_stream: a placement or WMO prop whose model load failed is retired as resolved, so it stops counting as pending and the loading screen clears (#624)
A placement whose model load failed (a file missing from the chain, or a model the parser rejects) stayed unspawned for ever. The spawner treats `m2s.get(h) == None` as "still loading", the progress count holds it as pending, and `scene_ready` needs zero pending, so the loading screen never cleared. `spawn_loaded_placements` now first retires any unspawned placement (M2 or WMO root) or WMO prop whose handle's `LoadState` is `Failed`. A retired entry spawns nothing, is flagged spawned, and leaves `pending_spawns`, with one warning naming the path. That is the same fallback the WDT load already takes. Handles that are still loading keep waiting. WMO groups read inline in the WMO loader, so the root's state covers them. `a_failed_model_load_retires_the_placement` and `a_failed_prop_load_retires_the_prop` load unresolvable paths through a real `AssetServer`. Both fail with the failure check removed. Built and gated on macOS. What draws in place of a missing model stays #330. Fixes #616 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
5d25ac9286 |
entities: a torn-down net unit gets no new visual work (attach, rebuild, re-dress, re-seat, held items), so no command lands on a despawned unit (#623)
A unit torn down (out of range or destroyed) before its visual was built still got one built, and the build's plain entity commands could apply after `apply_despawn_fade` had despawned it. That chain is unordered against the attach chain, and Bevy's default error handler panics. The reference frees the object at once on destroy (`0x4674a0`, `0x465ec0` → `0x464920`), and only an already-built model fades (`0x672df0`). So a torn-down unit now gets no new visual work: `Without<DespawnFade>` on the queries of `attach_entity_visuals`, `refresh_live_display`, `heal_rig_starved`, `redress_player_looks`, `reseat_mounts` and `attach_held_items`. `a_torn_down_unit_is_not_built` drives the real `attach_entity_visuals`, despawns the torn-down unit between the system's run and its command flush, and panics with "Entity despawned" on `insert<AppliedMount>` without the filter. `a_torn_down_starved_unit_is_not_rebuilt` covers the heal path. Built and gated on macOS. Fixes #612 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
d4c280d2b1 |
Caps Lock remapped to Escape (X11 caps:escape) acts as Escape on Linux, as in the real client (#602)
- On Linux a key the keyboard layout remaps (X11's caps:escape) acts as the key it is remapped to, as in the real client - A layout-moved named key also acts as that key on Windows, where the reference reads the layout's virtual key; macOS keeps its physical table Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
f000aa0128 |
The pet bar's, pet book's and pet-learn trainer's spell tooltips are built against the pet: its level scales the per-level terms, cost and cast time, and its base mana is the percentage cost's basis (#606)
`SetPetAction` (`0x532888`), `SetSpell` on the pet book (`0x532e23`) and a pet-learn `SetTrainerService` (`0x533a7f`) pass the spell builder `0x52e610` its unit selector. With it set, the level lookup `0x6e3130` asks the player's charm, else its summon (`6e3159`-`6e317f`), for `UNIT_FIELD_LEVEL × 5` capped at `maxLevel × 5` (`0x60cd80`), and `GetPowerCost` `0x6e31b0` costs against that unit: its level term and `0x612c50` on it, base mana for a mana spell (`6e327d`). `GetCastTime` takes the same level (`6e3397`). No such unit reads level 0 and no cost. The VM keeps a second view store for the pet, which the selector's setters read; the app builds those views against the pet's fields and rebuilds only them when the pet, its level or its bases change. Verified: real-catalog tests (Sacrifice rank 1 at pet level 22 reads 318, Blood Pact rank 1 at 14 reads 3, Seduction costs 449 from the pet's base mana 1874), each failing with the change undone; the player's views of the same spells are unchanged. Live, a level-60 warlock's succubus shows Seduction at "449 Mana" (main computes 329 from the warlock's base mana 1373) and its voidwalker's Sacrifice rank 1 absorbs 414, the capped level-22 value with the warlock's 30% talent mod, where main reads 396. Built on macOS only; no platform seam. Fixes #600 Fixes #601 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
e4b06c5c2f |
The battleground queue slots answer "none" from the first frame, so a party login no longer breaks BattlefieldFrame_Update (#603)
The reference's three queue slots are a static array (`0xb6e9d0`): `GetBattlefieldStatus(1..3)` always reads one (`0x4ab4df`-`0x4ab4f3`), and a zeroed slot answers "none". The VM held an empty list until the app's first push, so an in-range index took the off-slot leg and answered a nil status. A party login fires PARTY_LEADER_CHANGED before that push, `BattlefieldFrame_Update` read the nil status as queued and concatenated the nil map name (`BattlefieldFrame.lua:242`). The VM now holds exactly three slots, zeroed until pushed, and the push takes three. Verified: the new app test fires PARTY_LEADER_CHANGED on the stock BattlefieldFrame before any push and failed with the reported error before the change; live, Probefive relogged into a stored party raised the same error on the old build and none on this one (slot 1 reads "none", "Eastern Kingdoms"), and a solo login after leaving the party is clean. Built on macOS only; no platform seam. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
ca7111ed1a |
What 1.12.1 lacks lives on top of benilla, which opens a piece of itself to a crate only when its own 1.12.1 work creates it, and loads whatever the stock client loads from a patch (#597)
- `docs/CONTRIBUTING.md`, "What does not": what 1.12 client mods add (a new key binding, a spell queue, new Lua functions, graphics past 1.12's) is named as a feature 1.12.1 does not have, so that what benilla does is what 1.12.1 does. - A new "Building on top" section: such a feature lives in a crate started through `benilla_app::run_with`, or in a fork. A client started that way says `extended` in its build line. The crate keeps its own settings. benilla makes a piece of itself public to a crate only when its own 1.12.1 work creates it, never for the crate alone, with no stable API: a crate pins the revision it builds on. - "What gets in": whatever the stock 1.12.1 client loads from a patch, benilla loads. - `docs/METHOD.md`'s rule for every change points there, where it said "Anything else is a fork." Docs only; no code or platform seam touched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
91cd43c88d |
fix: apply per-level scaling to spell descriptions (#579)
- fix: apply per-level scaling to spell descriptions - The description's per-level terms scale by the player's skill in the spell's line, not the character level - The skill value reads its bonus word as the reference does: the high half unsigned over a positive value, the low half signed - The $d and $o durations take the row's per-level term at the spell's own skill level - $d reads a duration at or below 0, or no duration row, as until cancelled Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
084354c231 |
Spell text keeps "every 1 second": the $l plural keying on every token is recorded as a deviation (#595)
The reference's $a, $d, $t, $e, $c, $p, $f, $F and $z arms never write the plural count [0xbe0b84], so 1.12.1's Blizzard and Consecration read "every 1 seconds". benilla keys on the nearest token and prints the grammar the text means; a test pins it. Built and tested on macOS. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
4f86560b4a |
fix: spell modifiers (#554)
- fix: spell modifiers - fix: round scaled spell tooltip values to client tenths - fix: refresh only modifier-dependent item tooltips - The pet leg of the SPELL_GO cooldown insert takes op 11, and the item leg takes none - The tooltip's cast cell is GetCastTime's unclamped result - The Improved Fire Blast tooltip test reads Fire Blast's real recovery columns - The effect-points aura ops follow the reference's jump table - The template filler rounds %f and %g ties half up, as the client's CRT does - The $s, $S, $m, $M and $o tokens compute and print their values as the reference does - The $t, $r, $e and $a tokens follow the expander's arms - The unclamped cast-cell test skips without an install, as its GlobalStrings come from one - start_gcd's doc names the reference's gate: category or time nonzero, then op 21, and nothing armed for a zero result Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
5757ab2334 |
fix: send no repair-all at a vendor that does not repair (#561)
1.12.1's RepairAllItems (0x4fbfe0) calls 0x4fadb0, which resolves the open merchant's guid (0xbddfa0) to its unit and tests UNIT_NPC_FLAGS bit 14, and returns at 0x4fbff0 before the purse test when the bit is clear. The binding now queues the repair-all only when the merchant state's can_repair is set, the value CanMerchantRepair reads, so a macro or addon at an ordinary vendor sends no CMSG_REPAIR_ITEM and shows no "You don't have enough money.". Verified live on the local LyraCore server (macOS build): at Brother Danil (npc_flags 4) main sends 0x02a8 CMSG_REPAIR_ITEM and the server refuses it, or shows the purse refusal with an empty purse; with this change neither happens (out trace live to 30 s). At Dermot Johns (0x4004) the repair-all still goes out and repairs, and an empty purse still refuses. scripts/check.sh green (benilla, benilla-app, benilla-ui); the new tests fail with the check removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
34b7c30af2 |
The core loads the chain's Bindings.xml and DefaultBindings.wtf, and every command runs its stock body (#587)
The core now loads the chain's `Interface\FrameXML\Bindings.xml` after the `FrameXML.toc` rows, as the reference's UI load does. The load checks the file at `0x48fff9` and loads it through the addons' loader `0x4b6f70` at `0x490018`. The default keys come from the chain's `WTF\DefaultBindings.wtf` (`0x4b62b0`). Every command now runs the stock file's own body, and the registry compiled into benilla, with its 91 retyped bodies, is deleted. - **The loader's rules (`0x4b6f70`).** It reads the root's children only and skips empty, `debug="true"` and non-Windows rows. A second definition of a name is skipped, and duplicate headers are refused. - **Keys and commands are separate tables, as in 1.12.** `SetBinding` never reads the command (`0x4b7490`). `RunBinding` runs the body in the VM. `RunCommand` sets `keystate` around the body and clears it after (`0x4b7c42`). - **Engine behaviour sits behind the Lua globals the bodies call.** These are the movement, turn and mouse-steer bits. Focus loss clears the direction bits and keeps the mouse pair (`0x514490`). One key's stop clears its bit, as `0x514b70` does, so releasing W while UP is held stops the character. - **Behaviour changes that follow the stock bodies.** - The nameplate keys (V, Shift-V, Ctrl-V) now run the stock bodies. - ALT-Z runs `CloseAllWindows(); UIParent:Hide()`. - The minimap, sound, stance and bonus-bar keys call their stock functions. - The wheel zooms one `CameraZoomIn(1.0)` per notch. - **Platform.** benilla answers as the PC build, so a `platform="mac"` row does not register on any OS. Verified: - New tests: a fixture `Bindings.xml` (reordered, edited, and with debug, empty, nested and Mac rows) drives the command list, defaults and bodies. The V keys follow the stock bodies. No stock body's text remains in the tree. Every stock body's calls resolve after a production load. Each test fails with its change undone. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run with `WOW_GM=off`: - W and Space moved and jumped. - B toggled the backpack. - 1 reached `UseAction`. - V, Shift-V and Ctrl-V behaved as the stock bodies do. Built on macOS; no platform seam touched. Fixes #570 Fixes #563 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
c574e20031 |
fix: drop the spell body from passive talent tooltips (#538)
- A passive talent's tooltip is the talent builder's: name, rank, requirements, description and the next rank, with no weapon-class "Requires" row or other spell line (#476) - Talent tooltip tests pin the talent builder's gold spacer and every line of an exceptional talent's spell body - SetTalent's doc states why an exceptional talent's next-rank pass never shows: no 1.12 exceptional talent has a second rank Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
459e0e90c1 |
The stock video window's boxes are registered and ffx, ffxGlow, ffxDeath and gxMaximize work; benilla's options gain Spell Detail; load failures go to FrameXML.log (#585)
**#549: the stock video window's CVars.** Its check boxes, `ffx` and `gxRefresh` are registered at the reference's defaults. `pixelShaders` and `specular` ship as `overridden` "1" (`hwDetect`, `0x639a60`), and `gxFixLag` ships "1".
- `ffx`, `ffxGlow` and `ffxDeath` gate the full-screen effect passes every frame, as the reference's gate does (`0x6cd890`; `CFFXGlow` reads `ffxGlow` at `0x6cc5a8`, `CFFXDeath` reads `ffxDeath` at `0x6cdf10`).
- `gxMaximize` is latched. At the `RestartGx` commit it makes a windowed client borderless over the monitor, as the reference's window rebuild does (`0x58cf10`).
- The rest are registered with no effect, and each row says why: `useWeatherShaders` (snow's fallback leg is a separate CPU draw), `lod`, `M2UseShaders`, `M2UsePixelShaders`, `movieSubtitle`, `gxTripleBuffer`, `gxCursor`, `gxRefresh`, `DesktopGamma`.
- `cvars.rs` is split into the runtime and the table.
**#329: Spell Detail.** benilla's options window gains 1.12's Spell Detail Level slider on the Graphics page, over `spellEffectLevel` on the stock 0-2 grid.
**#583: `FrameXML.log`.** Load failures now go to `benilla-config/Logs/FrameXML.log`, as the reference's go to `Logs\FrameXML.log`, and no longer to chat.
- The log uses the reference's lines ("Couldn't open", "Error loading", "Couldn't parse XML in") and its banners.
- A UI load that reports something rewrites the file, and a runtime `LoadAddOn` appends to it (`0x490187`, `0x46aac0`).
Verified:
- New tests for each switch, the maximize latch, every box's stored value with no unknown-CVar warning, the Spell Detail row and the log file's rewrite and append, each failing on main.
- A region dump of the options window shows the new row, and the rows below it moved 35 px, and nothing else.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run unticked Full Screen Glow in the stock window, which turned the glow pass off and saved `ffxGlow = "0"`, and a missing addon file produced its four log lines.
Built on macOS; no platform seam touched.
Fixes #549
Fixes #329
Fixes #583
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
b8cb886210 |
fix: select the nearest enemy when a combat ability is pressed with no target (#503)
- Sinister Strike, Heroic Strike, Judgement and the other combat-starting spells pressed with no target select the nearest enemy, as the Attack button does - A finisher pressed with no combo points refuses before the attack pick, and a self-cast press always Tabs past the caster - The plain cast send skips the attack pick because no 1.12 item, form, recipe or chained spell carries predicate 0x6e5200, and a real-catalog test pins it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
33c2c5aa70 |
The fourteen missing 1.12.1 functions are defined, the keyring hover runs the stock path, and the stock CloseAllBags keeps a backpack that was open (#581)
The fourteen 1.12.1 functions #336 lists are defined, each derived from its reference address, and benilla's layer drops the two places it stood in for the stock bag code. - **Keyring:** `KeyRingButtonIDToInvSlotID` (`0x4c8150`) exists, and the keyring slots answer as inventory slots 82-113. The stock `ContainerFrame.lua:616-619` hover path now runs unchanged, and the layer's keyring `OnEnter` wrapper is deleted. - **Bags:** the layer no longer replaces `CloseAllBags`. The stock one (`ContainerFrame.lua:702-707`) closes the backpack only when it was not open before the vendor or mailbox opened it (#541). The deviation that opens every bag stays, and its comment now names its only callers. - **Inventory:** `ShowInventorySellCursor`, `SetInventoryPortaitTexture` and `UpdateInventoryAlertStatus` exist. - **Character and pet:** `GetDamageBonusStat` (ChrClasses field 2), `GetPetTimeRemaining` (armed from `SMSG_PET_SPELLS`) and `GetActionAutocast` exist. - **Mail:** `ClearSendMail`, `GetNumPackages`, `GetPackageInfo` and `SelectPackage` exist, over a new `Package.dbc` loader. A selected package rides in the mail send. The compose reset now also clears the stationery, the package and the attachment, as `0x4acdc0` does. - **Tabard:** `GetTabardInfo` opens the tabard designer through the registrar (`0x5e00e0`). - **World map:** `GetWorldLocMapPosition` projects a map position onto the displayed map (`0x4a7360`). - **Loot:** `SetLootPortrait` binds a unit loot source's portrait (`0x4c2b40`). Verified: - A test per function for its shapes, raises and effects, failing on main. - A keyring hover test through the stock path, failing on main. - A backpack-over-vendor test, failing on main. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live A/B run: hovering a key asked for `player:82` and showed the key's tooltip. After a vendor, opening the Help panel left the backpack open and closed bag 1; on main the backpack closed. Built on macOS; no platform seam touched. Fixes #336 Fixes #541 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
48afcbce0a |
The host's clock, zone texts, game clock and error-handler slot leave _G, and the getters over them answer the reference's shapes (#580)
The host's state leaves `_G`: the eleven `__benilla_*` globals, which any addon could read and overwrite, move into the host's model (and, for the error handler, the Lua registry, as the reference keeps it), and the getters over them are Rust bindings answering the reference's shapes. - `GetTime` returns one number (`0x515ea0`), and `GetGameTime` returns hour then minute (`0x515ee0`). The tick advances the model clock, and every cooldown and buff reader reads it. - `GetZoneText`, `GetRealZoneText`, `GetSubZoneText` and `GetMinimapZoneText` always answer a string, `""` when unset (`0x48a0a0`-`0x48a100`). `GetZonePVPInfo` answers three values (`0x48d540`). - `seterrorhandler`/`geterrorhandler` work over a registry slot that starts empty, as the reference's does (`0x7039e0`, `0x702950`, `0x702900`). A non-function raises `Usage: seterrorhandler(errfunc)`. The engine dispatches caught errors through the slot, not through the global (`0x703b40`). The stock `BasicControls.xml` installs `_ERRORMESSAGE` before any addon, so play is unchanged. - The engine's surface allowlist now holds only `_G`. `scripts/api-coverage.sh` no longer excuses `__` names. Verified: - New tests show each of the eleven names absent from `_G` and that writing it changes nothing. On main, `__benilla_now = 0` rewound `GetTime` and every buff and cooldown. - The getter shapes and the error-handler slot are pinned. The surface gate fails when a name is re-added. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run read the clock, zone texts, PvP info and game time, then showed that writing the old names changes nothing. Built on macOS; no platform seam touched. Fixes #576 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
78a983b00e |
The VM speaks 1.12 alone: the Era globals and the uncalled hooks are gone, the engine's tooltip seams are Rust calls, strata/draw-layer/state-texture/sound/OnClick/quest-item/pickup take their 1.12 shapes, and the surface gate covers the production load (#573)
The Lua VM keeps no surface the 1.12.1 client lacks. - **Removed:** the nine Era-only globals: `UnitAura`, `CancelUnitBuff`, `GetCursorInfo`, `GetInventoryItemID`, `GetPlayerFacing`, `GetTradePartnerName`, `IsGossipOptionCoded`, `UnitIsAFK`, `UnitIsDND`. - **Hooks off Lua:** `GameTooltip:BenillaSetItemById` and `:BenillaGetTooltipOwner` are replaced by Rust-side calls. The uncalled `Benilla*` hooks are deleted. - **Frame strata:** `SetFrameStrata` knows the reference's eight strata and raises its message for any other (`0x774450`). - **`SetDrawLayer`** reads the layer name alone (`0x79a780`). - **`Set<State>Texture`** reads a number as a path (`0x781b23`); there is no colour form. - **`PlaySound`** names a kit and returns nothing (`0x4586d0`). `PlaySoundFile` returns one value (`0x458780`); benilla answers 1, a recorded deviation, since it plays after the call. - **`OnClick`** sets only `arg1` (`0x779540`). - **Quest getters:** `GetQuestItemInfo` and the quest-log reward and choice getters answer the reference's five values and its miss behaviour (`0x501f80`, `0x4e0b00`, `0x4e0cf0`). - **`PickupContainerItem`** returns nothing (`0x4f9b30`). - **Surface gates:** the bare VM's globals and every widget class's methods are held to `reference/1.12-globals.tsv` with one commented allowlist. A new gate on the full production load (core plus layer) allows beyond that only the layer's `Benilla`-prefixed globals. Verified: - A test per removal and per corrected form; a scripted undo of each fails its test. - Each gate fails on a re-added name. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run showed the same warnings and 0 script errors before and after. - In the 219-addon corpus, AckisRecipeList's bundled AceGUI-3.0 and Roid-Macros' Grid extension now behave as they do on 1.12.1. Built on macOS; no platform seam touched. Fixes #171 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
0a1f69466c |
The nameplate verbs write two runtime bits, cleared at world entry, and the setting lives in FrameXML's saved variables, not two CVars (#571)
The nameplate verbs now answer as the reference's (`0x489450`-`0x489480`): they flip bits `0x1` (enemy) and `0x8` (friend) of runtime state, write no CVar, and world entry clears both (`0x401639` → `0x6033c0`, after `VARIABLES_LOADED`). The setting persists only through FrameXML's `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON`, as on 1.12.1. - The `nameplateShowEnemies`/`nameplateShowFriends` CVar rows and the host's plate push are gone. A player's saved plate setting in `config.toml` moves once into the saved variables at the first world entry. - benilla's layer keeps what players see today. Its `UpdateNameplates` fix drops the stock show-then-hide (`UIOptionsFrame.lua:775-776`), and it registers `FRIENDNAMEPLATES_ON` for saving, which the stock file never does (`:180-183`). So plates turned on come back after login. - With the layer off, benilla behaves as 1.12.1: friendly plates are off after entry. - The V keys flip the verb and the saved global together. V and Shift-V keep their recorded deviation; #570 tracks moving the binding table onto the stock `Bindings.xml`. Verified: - New tests for the verbs, the entry clear, the stock-only and layered persistence across entry and reload, the V keys and the one-time migration, each failing with its change undone. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run with `WOW_GM=off` turned both kinds on, relogged and read both back, from the file too. Built on macOS; no platform seam touched. Fixes #552 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
05e204de10 |
benilla's layer speaks the 1.12 API alone: handlers take this and no arguments, GetBinding answers the 1.12 list, the Key Bindings page captures through its window, /errors chains the error handler, and the host hooks leave the VM (#569)
benilla's layer now uses only the 1.12 API and the 1.12 Lua forms, and the host hooks it used are gone from the VM. - **The VM calls handlers as 1.12.1 does.** A handler body is compiled as the chunk itself, so `self` in it is an ordinary global (`0x704c70`). A handler is called with no arguments (`0x705199`). The loader reads no `function=` attribute. - **`GetBinding`/`GetNumBindings` answer the 1.12 list** (`0x4b7f60`): `HEADER_*` section rows, plus every key bound to a command, with no category column. The stock `KeyBindingFrame` now shows its section headers and both key columns. - **The Key Bindings page captures keys through its own window**, as `Blizzard_BindingUI` does: the frame's `OnKeyDown` for keys, clicks for mouse buttons, and the wheel. It takes the `Benilla` prefix, so loading the stock binding addon no longer breaks it. The UI pointer feed now hands frames mouse buttons 4 and 5. - **`/errors` keeps its log by chaining the Lua error handler**, and the stock ScriptErrors dialog is unchanged. Load failures that never raise now print one chat line each at world entry. - **Slash commands:** `/reload` and `/errors` are `SlashCmdList` rows in the layer. `/convertraid` is gone; 1.12.1 has no such command. The dev commands are host rows in dev builds only, so a player build answers them with the unknown-command line once. - **Naming:** every global the layer defines is `Benilla`-prefixed. `BenillaBindCapture`, `KeyBindings_OnHostKey`, the three script-error globals and `SubmitChatInput` are removed. Verified: - New tests for each rule and each removed surface; 16 mutations each fail a test. - Region dumps of the Options window (general page, Key Bindings, armed) are identical before and after. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run bound a key, a Shift chord and the middle button through the page, and opened `/errors`. Built on macOS; no platform seam touched. Fixes #539 Fixes #544 Fixes #407 Part of #171 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
0f36374445 |
The core loads the player's own FrameXML.toc in its order, and the boot VM loads no Fonts.xml (#568)
The core now loads the player's own `Interface\FrameXML\FrameXML.toc` off their chain, row by row in its order (`0x6edb90`, from `UI_Init` at `0x48ffed`), instead of benilla's copy of the list, and `benilla.toc` is gone. Order is observable in 1.12.1: event listeners run in registration order, children are kept in creation order, and hit-test ties go to the earlier frame. - The world VM starts bare: `GlobalStrings.lua` and `Fonts.xml` load as the toc's first rows. The boot VM keeps `GlobalStrings.lua` and the emote tokens, and no longer loads `Fonts.xml`, which nothing reads there (#307). - A chain without `FrameXML.toc` builds no stock UI and still loads the layer and addons, as the reference does (`"Couldn't open %s"`, severity 2, return unread); benilla reports it as an error. - Every test kit now loads in the production order, `FrameXML.toc`'s rows then `layer.toc`'s, and a guard fails any kit that loads out of it. Two tests whose premises the stock order disproves are gone. - `scripts/smoke.sh` counts VMs by the VM marker, since `Fonts.xml` now loads once per world VM. Verified: new tests for the load list against the chain toc and a reordered fixture toc, the missing-toc case, and two stock frames on one event running in the toc's order (ExhaustionTick before PlayerFrame on `PLAYER_UPDATE_RESTING`), each failing on main's order. A before/after dump of globals, frames, draw order, event listeners and saved variables differs only in stock-order effects: the reordered listeners and children, draw-order ties between non-overlapping frames, and `UnitPopup`'s quality colours now sharing the live tables. `scripts/gates.sh` and `scripts/smoke.sh` are green, and live before/after runs show 0 errors and identical warnings. Built on macOS; no platform seam touched. Fixes #555 Fixes #307 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
a21fb2750a |
The stock video options window opens, saves, restores defaults and cancels clean: its CVars are registered, spellEffectLevel drives the particle and shard rates, and the video pairs, frame enumeration and client queries are defined (#565)
The stock video options window (`OptionsFrame`, off the player's chain) now opens, saves, restores defaults and cancels with no Lua error, as on 1.12.1. - The CVars it reads are registered at the reference's defaults: `SmallCull` "0.04" (`0x68854a`), `shadowLevel` "1" (`0x6885bc`), `baseMip` "0" (`0x6887aa`), `DesktopGamma` "0" (`0x402d4d`), `spellEffectLevel` "2" (`0x688900`), plus `doodadAnim` and `texLodBias`, which the new getters read. `SetWorldDetail` keeps `SmallCull` in step (`0x488e56`), and the wrong comment that called the getter its only reader is gone. - `spellEffectLevel`'s handler (`0x689510`): the value is clamped to 0-2, "Spell effect level set to %d." is printed, the particle emission scalar is set to 0.33/0.66/1.0, and the shard emitter's rate follows the level (`0x6eb95e`). - The 21 functions #337 lists: the six video getter/setter pairs (`0x488ec0`-`0x489300`), `GetNumFrames`/`EnumerateFrames` in creation order (`0x705f00`, `0x705f60`), and the client queries, including `GetDebugStats` and `OpeningCinematic`'s empty `CMSG_OPENING_CINEMATIC` (0xf9, `0x48c8c9`). Verified: a headless test walks the stock window's 18 boxes and 9 sliders, then Okay, Defaults and Cancel, with no error (it fails on main at `OptionsFrame.lua:146`), plus unit tests for each binding and handler; 12 mutations each fail a test. A live run opened the window, saved, restored defaults and cancelled with no Lua error. `scripts/gates.sh` green. Built on macOS; no platform seam touched. Fixes #548 Fixes #337 Refs #329 (benilla's own options window has no Spell Detail row yet) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
6e869eea4e |
The core loads the stock GameMenuFrame.xml, and the layer reshapes it in place into benilla's menu, keeping every stock rung in the chain anchored TOP (#564)
The core now loads the stock `GameMenuFrame.xml` at its `FrameXML.toc` row, right after `UIPanelTemplates.xml`, and benilla's ESC menu look moves into the layer as `GameMenuAdapters.xml`, which reshapes the stock frame in place: a later file cannot take an occupied frame name (`0x701bd0`). - Every stock rung stays in the chain with its name and a `TOP` anchor. The three the menu does not show are hidden and laid over the rung above, since a zero height leaves a rung's bottom edge unresolved (`combineEdge 0x767440`). So an addon that inserts under Interface Options and re-anchors Key Bindings (ShaguTweaks), or under Macros and re-anchors Logout (MCP, MoveAnything), lays out as on 1.12.1, with no error. - The era rungs keep their look and behaviour: Options opens benilla's window; the `BenillaOptionsFrame`-aware wrappers of `ToggleGameMenu`, `IsOptionFrameOpen` and `UpdateMicroButtons` move into the layer; the non-stock names take the `Benilla` prefix. The core toc now names no file of ours. - With the layer off (`WOW_STOCK_UI=1`), the menu is the stock menu and every rung opens its stock window. Verified: a region dump of the open menu on main and here differs only in our own frames' names, their insertion order within one level (the rects do not overlap) and sub-1e-4 px float noise; new tests for ShaguTweaks' insert, MCP from the corpus, a pfUI-style skin and the stock-only menu, each failing on main; `scripts/gates.sh` and `scripts/smoke.sh` green; a live run opened the menu, opened Options and returned to the game, and MoveAnything's own insert landed under Macros with Logout below it. Built on macOS; no platform seam touched. Fixes #553 Fixes #540 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
22634279bd |
benilla's own interface loads as one layer after the stock FrameXML and before the addons; the core loads StatsFrame.xml and runs LocaleProperties.lua once, as FrameXML.toc does (#562)
benilla's own interface now loads as one layer on top of the stock UI, as `docs/METHOD.md` now states: the core loads the stock FrameXML rows, the layer (`assets/ui/layer.toc`) loads after the last of them and before the third-party addons, and like FrameXML it has no AddOn API row and fires no `ADDON_LOADED` (the reference loads `FrameXML.toc` outside `AddOn_Load`, `UI_Init` `0x48fbf0`). - Moved into the layer unchanged: `ScrollTemplates.xml`, `KeyBindingsPage.xml`, `OptionsFrame.xml`, `ContainerFrameAdapters.xml`, `SpellBookAdapters.xml`, `ScriptLogFrame.xml`. `GameMenuFrame.xml` stays the core's last row until #553. - The host's Lua fixes to stock code (`FCF_ChatTabFadeFinished`, the `DurabilityFrame` re-seat) move into the layer's `FrameXMLFixes.xml`; the resize re-seat stays host-side as an engine adaptation. - The core now loads `StatsFrame.xml` and runs `LocaleProperties.lua` once, from `UIParent.xml`, so its chain rows are exactly the stock `FrameXML.toc` rows but the menu; the order is #555. - A dev build boots without the layer under `WOW_STOCK_UI=1`; the host's calls into the layer are guarded. Verified: new tests for the layer's file list, the core list against the stock toc, the load position (a `_G` definition log), absence from the AddOn API and the stock-only boot, each failing with the change undone; a before/after dump of every global, frame, draw order and saved variable differs only by `StatsFrame` and the moved fixes' source; `scripts/gates.sh` and `scripts/smoke.sh` green, and a live logout/re-login under `WOW_STOCK_UI=1` clean. Built on macOS; no platform seam touched. Part of #552 Fixes #542 Fixes #543 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
44fa1813ac |
SetCVar and RegisterCVar read their arguments as 1.12.1 does: a number as %.14g text, "0" for a value with no string form, the reference usage text, realmList/realmName/scriptMemory read-only in the world, and CVAR_UPDATE fired inside the call for every token (#557)
`SetCVar` and `RegisterCVar` now read their arguments as the reference's bindings do (`0x488c10`, `0x488b00`): - a number is stored as Lua's `%.14g` text (`luaV_tostring` `0x6f7c80`), so the stock Sound slider's 0.4 stores `"0.40000000596046"`; - a value with no string form (nil, absent, boolean, table) stores `"0"` (`0x488c98`, `0x488b6f`), so unticking a stock Sound options box no longer raises and music can be switched off there; - the reference's usage texts (`0x84235c`, `0x8422e8`); - `realmList`, `realmName` and `scriptMemory` are read-only to Lua `SetCVar` in the world (`0x48f566`-`0x48f584`), and host writes still land; - `CVAR_UPDATE` fires inside the call for every string token, changed or not (`0x488cad`-`0x488cd5`, `SignalEvent2` `0x703f50`). Verified: 7 new VM tests, a stock Sound window test off the chain and a world-entry read-only test, each failing with the change undone; `scripts/gates.sh` green. Built on macOS; no platform seam touched. Fixes #167 Fixes #139 Fixes #547 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
db94ea038e |
fix: show spell tooltips whole on the first hover (#472)
- Spell tooltips for pet spells, quest reward spells, Beast Training, enchant links and pet or target-of-target debuffs show whole on the first hover - The spell-tooltip tests' doc comments say the one thing the test name does not, in a line - The spell feed builds each view once per frame, the pet-bar Firebolt test slot is one helper, and the wait's comment reads in one pass - A spell tooltip completed by its view fires no second OnTooltipCleared: one setter call, one event, as in 1.12.1 - The spell feed reads every aura and the tracking spell from the VM alone, dropping the three app-side aura reads that duplicated it - The spell feed reads the auras from the VM's player list and every guid's list, the aura model main moved to - A spell tooltip waiting on its view re-renders only while it still shows what the miss drew, so a line Lua rewrote in place, by AppendText or a cell's SetTextColor, is kept - The spell tooltip's one-frame re-render is stated as a known difference from the reference, which builds every spell tooltip at the call Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
ae6d9f3500 |
MouselookStart and MouselookStop start and end mouse look with no button held, and a look session outlives losing focus with the OS cursor handed back (#534)
`MouselookStart()` and `MouselookStop()` now start and end mouse look with no button held, as in 1.12.1, so an addon such as CustomNameplates can steer with the mouse; `IsMouselooking()` reads the same state. Before, both did nothing. - `MouselookStart` (`0x514210`, ungated) disarms a pending world click (`0x514810(0)`) and sets the turn channel the held right button drives (`0x515090(1, 1, …)`), entering the right button's look session: the same cursor lock and hide, the camera and the character turning with the mouse. `MouselookStop` (`0x514240`) clears it; `IsMouselooking` (`0x514270`) tests it. A right-button release ends a script-started look, as its `TurnOrActionStop` clears the same bit; the loading cover clears it (`0x5144c0`); a window deactivate does not (`0x5144a3 and eax,0xfffff00f` keeps the bit). - A look session, right-drag or script-started, outlives losing window focus, as 1.12.1's does, and the OS cursor is handed back while the window is unfocused and taken again on focus, with no turn from the pointer's travel away. On macOS winit's cursor lock is the global `CGAssociateMouseAndMouseCursorPosition(false)`, which a deactivate leaves in place, so a held lock would freeze the pointer in other apps; 1.12.1's Windows client gives the cursor back while inactive. - While focused, the plain right- and left-button paths are unchanged. Fixes #487 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
0e39273f13 |
"mouseover" names nobody once no unit wins the hover: empty ground, a corpse or a nearer GameObject clear it, and the fading tooltip's health bar follows the unit's guid, not the token (#518)
`"mouseover"` now names nobody once the mouse leaves a unit, as in 1.12.1, where the publisher `0x492890` zeroes the mouseover guid pair on teardown (`0x4928e8`, `0x4928f2`) and a corpse or GameObject hover leaves it naming no unit. Before, `UnitExists("mouseover")`, `UnitName("mouseover")` and `UnitIsUnit("mouseover", …)` kept answering for the last unit hovered.
- The world tooltip's health bar follows its subject's guid, as the builder `0x529fe0` registers its health watcher on the guid (`0x52a0a8`), so clearing the token leaves the fading plate and its bar as they were; the clear fires no `UPDATE_MOUSEOVER_UNIT`, whose one fire site is the unit arm (`0x4929b3`).
- One definition of the hovered unit (none when a nearer GameObject wins the pick) serves the tooltip, the unit tokens and the aura feed, so they clear together.
Fixes #500
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
829194ac1a |
SendChatMessage sends nothing for an empty line unless its type is AFK or DND, checked after the chat type as 1.12.1 does (#499) (#505)
`SendChatMessage` with an empty line now sends nothing unless the type is AFK or DND, as in 1.12.1, where `0x49f28d`-`0x49f2a1` end the call on an empty first byte for every other type, after the chat-type check (`0x49f276`, "Unknown chat type" at `0x49f285`) and before the language, the target checks, the AFK clear and the tutorial acknowledge. Before, an empty line went out as an empty packet, and an empty SAY cleared a standing AFK. Fixes #499 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
6633a11d60 |
A dev build of a crate on top of benilla finds its install, benilla-config/ and .probe-identity in its own folder, which its build stamp hands in, not in cargo's checkout of benilla (#504)
A dev build finds its project folder, where its `WoW` link, `benilla-config/` and `.probe-identity` live, from its launcher, not from a benilla crate's own `CARGO_MANIFEST_DIR`. For a crate built on top of benilla, where benilla is a git dependency, that manifest dir is cargo's checkout of benilla. - `benilla_buildstamp::emit()` stamps `BENILLA_PROJECT_DIR`, the launcher's workspace root (the nearest folder up holding `Cargo.lock`), only when the launcher's `dev` feature is on. A player build stamps it empty and carries no source path. The shims pass it in as `BuildId::project_dir`. - `benilla_formats::project_folder` is the one home for the folder. Each entry point records the stamped folder once, before anything resolves from it. The install's dev rung, the state folder (which keeps its step from a linked worktree to the primary checkout) and the probe identity all read it. With nothing recorded (a test, an example, a tool) it is benilla's checkout, as before, and in a player build it is `None`. - Live `assets/ui` and the addon corpus stay on benilla's own source. - The example launcher's header now says a dev build of a crate on top keeps these in its own folder. - The comment above the stamp's resource names the build banner, the stamp's reader beside the panel footer. Fixes #494 Fixes #493 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
1a37909c9c |
The mouseover unit carries the hovered guid, so UnitIsUnit("mouseover", "target") answers 1 over the target: unit snapshots take their guid (#502)
Hovering a unit now makes `UnitIsUnit("mouseover", "target")` (or any other token naming the same unit) answer 1, as in 1.12.1, where `UnitIsUnit` (`0x516070`) resolves both tokens through `0x515970` and compares their guids, and `mouseover` reads the hovered unit's guid (`0xb4e2c8`/`0xb4e2cc`, written by `0x492890`). The `mouseover` unit state was pushed with guid 0, so every such comparison answered nil.
`ui_unit::snapshot` now takes the unit's guid and sets it, so every unit state built from a snapshot carries it; the callers that set it by hand afterwards no longer do.
Fixes #496
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
bacbb791db |
The player's Attack runs the attack validator's target pick: a friend selected retargets the nearest enemy in call order, a corpse or a fruitless scan says "You cannot attack that target.", nothing selected and no enemy says "There is nothing to attack." (#249) (#501)
The player's Attack (the Attack Target key, `AttackTarget()` and the Attack action button) now picks its target as 1.12.1's validator `0x612df0` does, where before it swung at whatever was selected, a friendly unit or a corpse included. - With nothing selected, or a unit the player is friendly toward (reaction ≥ 4, `0x6130a3`), it runs the Tab cycler for the nearest enemy (`0x6130b5` → `0x493f60`), which commits the new selection at once, so `AttackTarget()` then `/cast` in one script casts at the new target. - It then reads the selection again (`0x6130c1`): none raises "There is nothing to attack." (`0x6130d9`); a unit neither alive nor feigning, or one `CanAttack` (`0x606980`) rejects, raises "You cannot attack that target." (`0x613171`). A friendly unit with no enemy near stays selected and meets that second gate. - The swing toggle runs only once a target is accepted (`0x6131a0`), so Attack at a corpse mid-swing gives the error and keeps the swing. - The pet's attack order shares the same pick. vmangos stops a swing at a friendly or dead target server-side (`CombatHandler.cpp:46-59`), which the client no longer sends. Fixes #249 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
c6767d6a79 |
SendChatMessage resolves its language name against Languages.dbc, raising "Unknown language" on a miss, and the chat send carries that id, so a racial language picked in the chat menu is spoken (#164) (#498)
`SendChatMessage`'s language argument is spoken, as in 1.12.1: a racial language picked from the chat menu (`ChatFrame.lua:2404`, passed on by `ChatEdit_SendText` at `:1950-1954`) or named by an addon goes out in `CMSG_MESSAGECHAT`, where before every line went in the faction tongue. - `0x49f1e0` starts from the speaker's default language (`0x5ec890`); when argument 3 passes `lua_isstring` (a string or a number) it is matched by name against every `Languages.dbc` row, case-insensitively (`0x49f8a0`, `SStrCmpI`), and a miss raises "Unknown language" (`0x49f2e1`). nil, an absent argument, a table or a boolean keep the default. A language the character has not learned is sent and left to the server, which drops it (vmangos `ChatHandler.cpp:175`). - The packet builder writes the type, the language and a target for a whisper or a channel only, then the text (`0x49f6c3`-`0x49f72a`); fourteen per-type writers become one, with byte-exact goldens. - Each new UI state is seeded with the language table. Checked live against the local vmangos: a say, a yell, a whisper to self and a GM command round-trip through the new builder; a channel line reaches the server's channel lookup; an unknown language raises in Lua. Fixes #164 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
65a5e44d48 |
Unit aura bindings resolve their token as 1.12.1 does, so mouseover, party1target and any casing read the unit they name (#497)
`UnitBuff`, `UnitDebuff`, `UnitAura`, `CancelUnitBuff` and the tooltip's `SetUnitBuff`/`SetUnitDebuff` now resolve their unit token as 1.12.1's `0x515970` does before reading any aura, so `mouseover`, `party1target`, `raid3target`, `pettarget`, `targettargettarget` and any casing (`"PLAYER"`) answer for the unit they name, where before they answered nil. - The resolver matches the nine base names as prefixes in the reference's order, folding ASCII case (`SStrCmpI 0x64a4c0`), with `npc` whole; the number after `party`/`raid` wraps at 32 bits and counts from 1; `mouseover` counts only while the unit is loaded (`0x515bd2`); each `target` suffix hops to that unit's `UNIT_FIELD_TARGET` (`0x5159d3`-`0x515a2c`); a name none of the nine match raises `Unknown unit name: %s` (`0x515c14`), before any index check, as `UnitBuff` calls it at `0x519542`. - The VM holds one aura list per guid, plus the player's own in cache order (`0xbc6040`); any token naming the player reads that list, so its order and durations are unchanged. - The feed builds lists only for guids some token can name, through the existing per-guid cache. `UNIT_AURA` fires for the same tokens as before. Fixes #488 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
8d31e9e91e |
A crate on top of benilla adds its own Bevy plugins through benilla_app::run_with, and a client started through it reads extended in its build line (#495)
A crate built on top of benilla now adds its own Bevy plugins through `benilla_app::run_with(build, extend)`: `extend` gets the client fully built, after the game plugins, the asset loaders and the `ExtractSchedule` executor flip, and before the probe fleet, which sees what it adds. `run` is `run_with` with nothing added, so the stock client is unchanged. - A client started through `run_with` carries `extended` in its build id (`BuildId::summary`), so the startup banner, the crash report and the debug panel's footer all say so, and a report from an extended client reads apart from stock. The entry point sets the flag; a launcher leaves it to `..Default::default()`. - `crates/benilla-app/examples/extended_launcher.rs` is a launcher with a plugin of its own, compiled by the gates. Its header is the shape a crate of its own copies: the manifest (benilla-app without default features and `dev` forwarded, the workspace's bevy, and the root's `[patch]` and `[profile.dev]`, which cargo reads from the root manifest only), the build script's stamp, and a `Cargo.lock` started from benilla's. - `tests/launcher_shape.rs` holds that documented manifest to the root `Cargo.toml` and the `benilla` launcher's, so a patch or profile added later cannot leave the documented shape building a client that is not benilla's. - `docs/CONTRIBUTING.md`: a feature 1.12.1 does not have lives in its own crate on top of benilla, started through `run_with`. Fixes #483 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
476da9b2f0 |
GetAuctionItemInfo names the bidder on the player's own owner-list auctions, nil until the name lands, and 1 or nil on every other row (#259) (#492)
The auction house's Auctions tab now names the high bidder on each of your auctions, and shows "No Bids" only for an auction with none, as in 1.12.1; before, every auction read "No Bids". `GetAuctionItemInfo`'s eleventh value follows `0x4cee40`: on the owner list, for a row the player owns (`0x4cf12a`-`0x4cf142`), it is the bidder's name from the name cache (`0x4cf144`-`0x4cf174`), nil with no bid (`0x4cf150`) or while the name has not arrived; on every other row it is 1 when the player holds the bid, else nil (`0x4cf17b`-`0x4cf1a0`). A bidder name that arrives later refreshes the list with `AUCTION_OWNED_LIST_UPDATE`, the event the reference's callback `0x4cf2b0` fires. vmangos sends the bidder guid with each row (`AuctionHouseMgr.cpp:840`). Fixes #259 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
a6cbb932fa |
Guid-tail channel notices are delivered through the channel stamp, so a member's join or leave, a kick, a ban and an owner or moderation change print numbered in the stock chat frame (#491)
A channel notice about another player (a member joining or leaving, a kick, a ban or unban, an owner, password, announcement or moderation change) now prints in the chat window with its channel number, as in 1.12.1, where before it never printed. The reference hands the chat composer `0x49a870` the notice's channel name for every notice, directly when the names are cached (`0x49c59f`-`0x49c5b0`), else from the queued line (`0x49c567`-`0x49c580`, node `+0x2c`) in the name-query callback (`0x49cfb4`-`0x49cfc1`); the composer numbers it against the joined channels (`0x49aa1e`-`0x49aa48`) and fills arg7-arg9 (`0x49b10c`-`0x49b12a`). These notices were held for their names and then routed unstamped, so the stock filter (`ChatFrame.lua:1373-1392`) found no channel and dropped them; they now go through the same stamp as the other two paths, when the line is delivered. vmangos sends them for every custom channel (`Channel.cpp:126-130`, `:179-184`, `:249-257`, `:769-773` and others). Fixes #275 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
36b359f007 |
Logout and the AddOns screen's Okay write only a changed character's own enable rows, so a character with no row keeps following the realm's others (#418) (#490)
A character's AddOns file now holds only the rows the character chose, as in 1.12.1, so a character that never chose an addon keeps following what the realm's other characters agree on (`0x51e470`) instead of freezing its inherited values at the first logout. - Each character has an enable hash and a dirty flag: the loader `0x51ebe0` fills the hash from the file, clean; the setter `0x51ea20` inserts on a miss and dirties the hash only on a change (`0x51ebab`-`0x51ebbc`); the writer `0x51ef20` writes a dirty character's hash, a line per entry, and nothing else (`0x51ef59`). - Logout and exit (`0x490c88`) and the character-select AddOns screen's Okay (`SaveAddOns` `0x46d990`) both write through it, so a session that toggles nothing writes nothing, and no `Blizzard_*` rows appear. - `ResetDisabledAddOns` restores the hash as loaded, clean (`0x48e830`). - On the AddOns screen a character with no row now follows another character's toggle at once, as `AddonList_Update` re-reads each column after every `EnableAddOn`. Fixes #418 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
78862ff291 |
UnitBuff and UnitDebuff answer for raid1-40 and raidpet1-40, each unit's list built once and shared by every token naming it (#489)
`UnitBuff` and `UnitDebuff` now answer for `raid1`-`raid40` and `raidpet1`-`raidpet40`, so the raid pullouts draw each member's and each pet's buffs or debuffs (`Blizzard_RaidUI.lua:451`, `:477`), and `UNIT_AURA` fires for every group token naming a unit whose list changed (`0x515e50`). - As for the party tokens, a unit with a live object reads its own aura slots through `UnitBuff`'s unit gate; one with none reads its roster record's aura block (`0x519735`, `0x519b25`: the member record, else the pet record), with a stack count of 1 and no gate. - `raidN` names the rows `GetRaidRosterInfo` does; `raidpetN` is the member's `CHARM`, else `SUMMON`, while it is held, else the record's pet guid with no online test (`0x491960`, `0x4919ae`). - One feed serves all 88 group tokens: each unit's list is built once per frame and shared by every token naming it, rebuilt only when its inputs move, and pushed to the VM only when it changed. In a 40-man raid with 16 auras on each member this costs 87 µs a frame, where rebuilding every token every frame cost 414. Fixes #471 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
06bc451bac |
The 48 movement, camera and targeting functions Bindings.xml calls are Lua globals on their keys' paths: Start/Stop hold the key's command inside a binding body, targeting and AttackTarget go through the call queue (#485)
The 48 functions the stock `Bindings.xml` bodies call are now Lua globals, registered from the reference's three tables (`0x8500b8`, `0x84f7a0`, `0x83de68`), so an addon, an addon's `Bindings.xml` body or a `/script` line that calls `TargetNearestEnemy`, `TargetLastTarget`, `AttackTarget` or the rest no longer raises. Each runs the code its native binding runs. - **Targeting** goes through the ordered script-call queue, so `/script TargetNearestEnemy()` then `/cast` casts at the unit the cycle picked. `TargetNearestEnemy`, `TargetNearestFriend`, `TargetNearestPartyMember` and `TargetNearestRaidMember` share the Tab cycler (`0x493f60`, modes 1-4; party and raid modes are new: `0x493eed`, `0x493f15`), reading the reverse flag as `0x6f1c10` does. `TargetLastTarget` (`0x489b00`) swaps to the last-target pair `SetSelection` stamps, and deselects when it is empty. `AttackTarget` (`0x489b50`) shares the ATTACKTARGET key's body. - **Movement:** 23 of the 26 movement-table functions open with the gate `0x494a50`, which lets a call through only while a binding runs; here that is a binding body running, stock or an addon's. Inside one, `MoveForwardStart`/`Stop` and the rest set and clear the same held state the key does (`0x515090`); a Start with no Stop keeps moving, and the window deactivate (`0x514490`) and the loading cover (`0x5144c0`) clear it. `Jump`, `ToggleRun`, `ToggleAutoRun`, `SitOrStand` (`0x48b920`) and `ToggleSheath` (`0x48a070`) fire their key's command. `IsMouselooking` (`0x514270`) answers 1 or nil. - **Camera:** `CameraZoomIn`/`Out([yards])` (`0x50b400`, `0x50b450`) take a number or numeric string, 1.0 when absent, into the wheel's zoom. - Not built, so they answer and do nothing: the pitch pair, `TurnOrAction*`, `CameraOrSelectOrMove*`, `MouselookStart`/`Stop`, the `MoveView*` pairs and `ToggleMouseMove`; each is named with its address where it is registered. Fixes #334 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |