OnUpdate ran in frame-creation order, from a list built before the walk, so a frame hidden by an earlier handler still updated, and strata and level played no part.
The reference's update walk `0x765650` goes through the nine strata from the bottom, each stratum's levels from 0, and each level's list oldest link first. It saves the next frame before each call, so a hide skips a frame before its turn. A frame linked into the level being walked waits a tick; one linked into a level still ahead runs there.
The walk now follows that order, using the link stamps shows and level changes already keep. It checks each frame at its turn and places frames linked or given an `OnUpdate` mid-walk. An EditBox still flushes right after its own `OnUpdate`.
Tests cover the order, mid-walk hides and shows, and a box's flush.
Fixes#742
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Script errors reached the Lua error handler as mlua's display text: a `runtime error: ` word and a `stack traceback:` block, which overflow the stock `ScriptErrors` dialog. And a raise in a `CreateFrame` template's OnLoad was logged as a warning and never reached the handler at all.
In the reference, handlers run under `lua_pcall` with the error function `0x703b40`, which calls the `seterrorhandler` function with the bare message. `CreateFrame` builds its template through the XML instantiator `0x6ee280`, whose OnLoad runs under that same function (`0x704d50`).
Now every caught error records two texts: the decorated one for benilla's log and the bare message for the handler. A load report pairs the two per row, and `CreateFrame` records its template's errors as script errors.
Tests: the handler hears bare messages from handlers, and a template OnLoad raise reaches it.
Fixes#697Fixes#859
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Show and hide propagated a whole subtree first, then fired from a snapshot, parent first: a parent's `OnShow` ran before its children's, and a child hidden by an earlier handler still got its `OnShow`. A frame visible at the end of its load got no `OnShow` at all.
In 1.12.1 show `0x76ae10` and hide `0x76ad50` mark the frame, walk the live child list re-checking each child's gate, and fire the frame's own handler last (`0x76aef5`, `0x76adfd`). The post-load hook fires `OnShow` after `OnLoad` for a visible frame (`0x76a3b3`), from XML and `CreateFrame` alike.
Now handlers run inside a live post-order walk, and the post-load `OnShow` fires. Tests cover the order, a sibling hidden mid-walk, and one or two `OnShow`s after a load; they fail without the fix. Four existing tests' expected orders flip to the reference's.
Fixes#350Fixes#59🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI scale was folded into the pixel seam, with the root and `UIParent` at scale 1. Frames outside `UIParent` (dropdown menus, need/greed rolls, the world map) shrank with it, `UIParent:GetScale()` answered 1, and `GetCursorPosition()` was off by the scale.
In 1.12.1 the scale is `UIParent`'s own frame scale (`0x494550` → `0x76ac10`, floored at 0.64) over a 768-unit root. `GetScreenWidth`/`GetScreenHeight` divide by it (`0x494590`), and `GetCursorPosition` (`0x48b820`) does not.
Now the seam is `height/768`, and the scale is applied to `UIParent` at the load's end and on a resize. The nameplates' compensation is gone. Backdrops, hit insets and minimap pings now follow a frame's own scale (`0x76b580`, `0x77e8d0`); tooltips at the cursor and `OnCursorChanged` divide by it (`0x530b20`, `0x77db23`). Tests cover each, and a stock-UI comparison shows nothing inside `UIParent` moves.
Fixes#172🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`PLAYER_LOGIN` fired at the end of the UI load, before the player existed: its handlers saw `UnitExists("player")` nil and level, health and XP 0, at every login and `/reload`. Ace2's `OnEnable` runs there.
In 1.12.1 the load only arms a one-shot (`0x49011d`). The world-enter cascade fires `PLAYER_LOGIN` if armed (`0x49094b`), then `PLAYER_ENTERING_WORLD` (`0x49096a`), inside a sound-suppression bracket (`0x4908d5`–`0x490a56`). It runs from the player's own create (`0x5deb60`), or on `/reload` from the load (`0x490168`) with the player still in the world. A worldport re-arms nothing.
Now the load arms it, and the unit feed fires both, silenced, once the player is seated. A `/reload` loads under the live player and no longer plays the target-select sound. Tests cover login, worldport and reload; they fail without the fix.
Fixes#189Fixes#191🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reopening the world map after zooming out showed the old map for a frame before the current zone.
In 1.12.1, every Lua verb that moves the map selection (`SetMapZoom`, `SetMapToCurrentZone`, a `ProcessMapClick` hit) goes through the setter `0x4a67a0`. The setter stores the selection, rebuilds the landmark list, then fires `WORLD_MAP_UPDATE` in place before returning (`0x4a6ce4`). So `WorldMapFrame`'s OnShow repaints the tiles before the first draw.
benilla queued the event to the next tick, so the first frame painted the previous map. The three verbs now run the setter whole. The landmark list is projected from per-row sources inside the setter, so the new tiles never show the old map's markers. The engine's own pushes still queue, as a narrowed deviation.
Tests replay the report's steps on the stock UI and check the event fires inside the verb with landmarks rebuilt first; they fail before the fix.
Fixes#811
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PvE realm, zone names were still coloured friendly, hostile or contested.
In 1.12.1, `GetZonePVPInfo` (`0x48d540`) answers a type only when its display gate is open (`0x48d5c2`/`0x48d5cc`). The gate opens on the realm's PvP flag or in a zone with Flags `0x10`, which is the six capitals. The realm flag is read from `Cfg_Configs.dbc` for the selected realm's type and stored once per world entry (`0x4015de`). A type with no row skips the store. Otherwise the type is nil, so the stock zone splash shows cream and the minimap zone text gold.
benilla now reads `Cfg_Configs.dbc` instead of a transcription that stopped at type 8, so realm type 10 lists as PvP. It latches the realm flag at world entry and applies the gate.
Tests cover the gate, the six capitals and every shipped Cfg_Configs row; each failed before the fix.
Fixes#816
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The quest log never showed the `[n]` count of party members on a quest, because `IsUnitOnQuest` was a stub that always answered nil.
1.12.1 (`0x4dfe10`) checks both arguments first. It reads the row's quest id (`0x4df150`; a header or out-of-range row answers nil before the unit is resolved), resolves the token, and admits only you or one of the four party slots, by guid (`0x4e7f70`). It then needs the unit held as a player and scans the 20 `PLAYER_QUEST_LOG` ids for that quest. The answer is 1 or nil. The ids reach party members on the wire. `UNIT_QUEST_LOG_CHANGED` (`0x51bc63` → `0x51bd90`) fires once whenever any slot of a unit's window changes, so an open log recounts.
Unit snapshots now carry the window, the binding follows those steps, and the event fires. Tests cover each path and run the stock `QuestLogFrame.xml` to a `[2]` row; they fail against the stub.
Fixes#818
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A unit tooltip's title read the bare name where 1.12.1 decorates it.
The tooltip calls the name builder `0x609370` itself (`0x52a1ab`, or `0x52a1db` when there is no second part) with the decoration flag a literal `1`, the same call `UnitPVPName` makes, so no CVar gates it. With the flag on, a ranked player reads `UNIT_PVP_NAME` of the gendered `PVP_RANK_<rank>_<team>` and the name, a city protector's `PVP_MEDAL<n>` follows on a second line (`0x6093ef`), and a civilian kill (`0x612550`) reads the `PVP_RANK_CIVILIAN` prefix (`0x609461`).
The title now runs the builder `UnitPVPName` already uses. The test covers rank, gender, medal, civilian, an unranked player and a creature; an on-demand `ui-tooltip-rank` capture shows a ranked player's tooltip.
Fixes#794
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- capture: a name-rank scenario shoots a ranked player's overhead name line
- Overhead names carry the player's PvP title under UnitNamePlayerPVPTitle, as 1.12.1 does (#550)
- review: bundle the rank-title inputs; doc the snapshot and rank-19 limits
- nameplates: read rank strings per VM; test the composed name line
- pvp: correct the rank-title docs; convert the credit team without a cast
- Fix the titles row comment; name-rank reuses WOLF_POS, cites :900
- rank strings: the honor feed reads them off the VM; nameplates only reads the value
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
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
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>
- 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>
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#735Fixes#736Fixes#737Fixes#738Fixes#745Fixes#747🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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#724Fixes#727🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A multi-line EditBox kept its authored height, so a scroll frame around a long mail body or ticket had no range. A box made in Lua with no template never placed its text, so it drew nothing.
In 1.12.1, the flush's relayout sets a multi-line box's height to its insets plus its wrapped text height, or one line when empty (`0x77d4d0` at `0x77d8ad`). Every resize, including the first layout, re-seats the text at the top-left inset (`0x77a8d0` → `0x77b8c0`). `SetTextInsets`, `GetTextInsets`, `SetMultiLine` and a font change re-seat it too.
benilla now does the same. A focused multi-line box's caret and text are clipped only by a scroll frame, as the reference clips neither.
Tests cover growth and shrink, the stock mail and ticket scroll ranges, each re-seat trigger, a Lua box's first layout, and the caret in an empty box and after a trailing newline.
Fixes#722Fixes#726🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The `Unit*` getters and `SpellCanTargetUnit` answered nil for chained tokens like `raid3target` or `party1target`, so a raid-assist addon's main-tank frame stayed empty.
Every getter and `SpellCanTargetUnit` resolves its token through `0x515970` and then reads the guid's object (`0x468460`); see `UnitHealth 0x517511`/`0x51752d` and `SpellCanTargetUnit 0x6e6d3a`.
A token with target hops now resolves to a guid in the VM and reads a per-guid snapshot. A new feed pushes one for each held unit a chain ends on. `SpellCanTargetUnit` keeps its verdicts by guid. The three target, target-of-target and NPC snapshot builders are folded into one.
Tests: `UnitName`/`UnitHealth("raid3target")` and `UnitExists("pettarget")` answer the chained unit, `SpellCanTargetUnit("party1target")` answers its verdict, and plain tokens are unchanged.
Fixes#693Fixes#694
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`PickupPetAction` lifted a pet action onto the cursor while the pet was out of the client's view, because the pickup gate read an unstreamed pet's missing flags as "not possessed".
The reference's `PickupPetAction` (`0x4be180`) looks the pet up first (`0x4be1f7`). With no object it returns at once (`0x4be1fe`), and only then does it test `UNIT_FLAG_POSSESSED` (`0x4be20a`).
The gate now needs the pet's object before the possessed test, and three comments that cited a mid-instruction address now cite these.
Tests: with the pet out of view, a pickup lifts nothing and a held action stays on the cursor. An in-view pet still lifts, and a possessed one does not.
Fixes#688
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`TargetUnit`, `AssistUnit`, `SpellTargetUnit`, `CheckInteractDistance` and `CanInspect` ignored target chains. `party1target`, `pettarget` and `raid3target` named nobody.
The reference's resolver `0x515970` takes a base token, then any number of `target` hops over held units' `UNIT_FIELD_TARGET` (`0x5159d3`-`0x515a2c`). Every one of these verbs resolves through it.
Changes:
- The app and the VM now share one parser, `parse_unit_token`.
- The reach map behind the two range verbs is keyed by guid, filled after the aura feed, and read through the VM's resolver.
- `ObjectStore::is_unit` replaces two private copies of the same test.
- Side effect: the reach feed's new order puts a few feeds after `feed_auras` where their order was undeclared before.
Tests: every chain form, a broken chain, junk after a hop and case folding. `TargetUnit("party1target")` works from Lua.
Fixes#669
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No pet-bar verb asked whether the pet's actions were usable. So a stunned, feared or confused pet, a pet out of view, a pet that is not yours, or a charmed player still did all of this:
- pressed, latched Stay, Follow and the reactions, and swept the GCD;
- sent `CMSG_PET_ACTION` and flipped autocast;
- showed `GetPetActionsUsable()` as 1, which kept the stock bar in colour.
`0x4bcf70` is the reference's one predicate. It returns 0 in any of these cases:
- the active player does not resolve (`0x4bcf92`);
- the player is charmed (`0x4bcf98`-`0x4bcfa4`);
- the player's far-sight guid (`PLAYER_FARSIGHT`) is set and is not the pet (`0x4bcfbf`-`0x4bd012`);
- the pet guid does not resolve to a unit (`0x4bd034`-`0x4bd03d`);
- the pet's charmed-by guid, else its summoned-by guid, is not the player (`0x4bd03f`-`0x4bd067`);
- the pet is stunned, confused or fleeing (`0x4bd06f`-`0x4bd08b`);
- bar-state bit 27 is set (`0x4bd08d`).
These callers ask it:
- `GetPetActionsUsable` (`0x4be0b3`);
- the dispatcher behind `CastPetAction`, the orders and Dismiss (`0x4bd1f2`, before its type switch);
- `TogglePetAutocast` (`0x4bcbcf`);
- the pet book's toggle (`0x4bccce`);
- the bar write used by the drag's drop and the pickup's blank (`0x4bc9d0`).
Changes:
- `ui_pet/usable.rs` builds `0x4bcf70` once. The bar's feed pushes it as `GetPetActionsUsable`. `PetPress::press` asks it first, before the aura cancel.
- Both autocast drains, Dismiss and the drag's two writes ask it too. A pickup still lifts the action onto the cursor, as the reference does.
- The spell arm's later player and pet lookups, and the Attack arm's null branch, repeat the predicate's, so their dead handling goes.
- `arm_pet_gcd`'s doc now names the three places the pet's GCD starts: the press (`0x4bd36e`), a pet-addressed `SMSG_SPELL_COOLDOWN` (`0x6e9598`) and the generic cast send (`0x6e58fb`).
Tests:
- `ui_pet/usable_tests.rs` tries each of 11 exits alone, and 5 usable lookalikes, against `GetPetActionsUsable`, a spell press, the orders, both autocast toggles, Dismiss and the drag.
- A stock-UI test loads the default UI. A stunned pet's icon desaturates, and a click sends nothing until the stun ends.
- The three press tests that pinned ungated sends are flipped.
Fixes#672Fixes#673Fixes#674🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`UnitCreatureType` read only a creature's template type, and fell back to "Humanoid" for a player. The form stage was not built. So a druid in cat, bear, travel or aquatic form and a shaman in Ghost Wolf all answered "Humanoid", where 1.12.1 answers "Beast".
`UnitCreatureType 0x51a280` names the type the resolver `0x605570` returns, and 0 names nil (`0x51a2c3`). The resolver tries three stages in order:
1. **Form:** the form byte (`UNIT_FIELD_BYTES_1` byte 2) indexes `SpellShapeshiftForm.dbc` column 12, which applies only above 0 (`0x60559a`).
2. **Template:** else the cached creature record (`0x6055a8`).
3. **Race:** else the race's `ChrRaces.dbc` type (`0x6055ca`).
A token naming no unit answers nil (`0x51a2b8`). The unit tooltip's level line takes the type only for a non-player (`0x52a4a6`).
Changes:
- `ui_unit::snapshot` sets `creature_type_name` for every unit through the shared `CreatureTypeSources::of`: players, creatures, pets and group tokens alike. `enrich_unit` no longer sets it.
- `UnitCreatureType` reads that field, with no fallback.
- The feeds at Bevy's parameter limit carry the sources in a `SnapshotTables` param.
- A player's tooltip level line is unchanged, since it reads race and class.
Behaviour that now matches the reference beyond the issue:
- a held pet, `partypetN` and `raidpetN` answer their template's type rather than nil;
- a party member in a form reads the form's type;
- a roster member with no object, and the pre-arrival player seat, answer nil.
Tests:
- `the_shipped_forms_name_a_beast_or_fall_through_to_the_race` reads the real `SpellShapeshiftForm.dbc`.
- `unit_creature_type_names_the_form_before_the_race_and_a_creature_its_template` runs `feed_units` and asks the Lua binding.
- Mouseover, party, raid and pet feeders each have a test.
- `a_players_level_line_ignores_the_snapshots_creature_type`.
Fixes#641🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`ui_unit::snapshot` builds a held unit's view and left `is_connected` at its default, false. Each feed that calls it had to set the field afterwards. The pet feed did not, so `UnitIsConnected("pet")` answered nil for a live pet. The stock `UnitFrameManaBar_Update` (`UnitFrame.lua`) then took its disconnected branch, and the pet frame's mana, focus or energy bar read full and grey. The mouseover feed had the same gap.
`UnitIsConnected` (`0x517d50`) looks the unit up in the object manager (`0x517da6`, typemask 8), and any held unit answers 1 (`0x517daf`). The roster record's online bit is read only when no object is held.
- `snapshot` now sets `is_connected` itself.
- The per-feed overrides for player, target, targettarget, npc, our own raid row and the held party pet go.
- The party feed's record-derived value for its members stays.
- The pet frame tests build their pet through the real `snapshot` rather than pushing the flag by hand.
Test: `a_held_pet_reads_connected_and_its_stock_power_bar_shows_its_power` loads the default UI and runs the real `feed_pet_unit` with a held focus pet. It checks that `UnitIsConnected("pet")` is 1 and that `PetFrameManaBar` reads 45 of 80 in the focus colour.
Fixes#642🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`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>
`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>
`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#600Fixes#601
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
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#570Fixes#563🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 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>
**#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#549Fixes#329Fixes#583🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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#336Fixes#541🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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#539Fixes#544Fixes#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>
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#555Fixes#307🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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#548Fixes#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>
`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#167Fixes#139Fixes#547🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 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>
`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>
`"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>
`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>
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>
`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>
`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>
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>
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>
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>
`EnableAllAddOns` and `DisableAllAddOns` now switch only the addons in the Lua index array, as 1.12.1's `0x48e720` and `0x48e7f0` do: bound `0x51def0`, each name from `0x51df00`, into the enable setter `0x51ea20`. That array leaves out the records `SMSG_ADDON_INFO` hid (`0x51dc4f`), which on vmangos is every `Blizzard_*` addon (`AddonHandler.cpp:129`), so an addon manager's disable-all no longer leaves the auction house, talents, trainers, trade skills, inspect, `/macro` or Key Bindings unable to load.
- The per-addon verbs and the all verbs share one setter, standing for `0x51ea20`.
- Before the server's addon reply the index is empty and both verbs change nothing, as the reference's count is 0 until then.
Fixes#414
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Item tooltips in repair mode show the repair cost: SetInventoryItem and SetBagItem return each item's cost after the open vendor's discount
- Bank items price in their tooltips too, the repair-cost gate is tested input by input, and logout keeps the last push
- The repair-cost tests share one fixture for the feed's world and the VM's sword
- The repair-cost comments say one fact each, and two that restated the code are gone
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>