fff48cd75caba0c40ef78225a55bd013bd7e61ba
393
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0da710810e |
An EditBox's engine quads answered nil, and ate the trade window's money row
His trade window has no money amounts: one empty box where the gold/silver/ copper row belongs. The cause is four regions that answer the wrong thing. pfUI skins that row by walking the gold EditBox's regions for the first Texture whose texture names "MoneyIcons", showing it, and re-anchoring it to the right of the box — then the silver box hangs off that icon and the copper box off the silver one (api/ui-widgets.lua:1225, api/api.lua:1693). Its scan reads a nil GetTexture() as "no name to match against, take this one". An EditBox's first four regions are the engine's own: the three selection highlights and the caret, which GetRegions lists ahead of the authored <Layers> ones. They paint host-side here, so they carried no texture and no fill and answered nil — and pfUI took the caret for the gold coin. Silver and copper then anchored off a zero-size region and the whole row collapsed. The reference builds those four as colour-set textures, not empty ones, so GetTexture answers them "Solid Texture" — the string is in WoW.exe, and pfUI's scan does filter that out. They answer it here now. Nothing renders differently: the host still draws the highlight and caret from sel_start / sel_end / caret_shown, and the fill stays unset. The new test runs pfUI's scan itself rather than asserting on our side of it, because what matters is which region that loop picks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f4960efc53 |
The realm update lands clean, and three of the "269 known red" were real
His 2026-10-28 20:18 update rewrote `Data/patch-4.mpq` (1.37 GB) and
`patch-5.mpq`
(230 MB). Verified rather than assumed, and nothing of ours broke: the realm's
`FrameXML.toc` went 146 → 151 rows and patch-5 holds the richest copy, so
`chain::demote_stale_snapshots` fires and it wins the manifest; the five new
rows
are realm modules (`OctoChallengesSession`, `OctoHardcoreBGMenu`, `OctoReplay`,
`OctoReplayBar`, `OctoReport`) that `unnamed_chain_rows` picks up by design and
that load with **0 load errors and 0 session errors**, every global they name
resolving; DBC field counts are unchanged everywhere, so no parser is at risk,
while record counts moved (Spell 27992 → 28026, CreatureDisplayInfo 13423 →
13451), which is the proof the new data is live; a `WOW_CAPTURE=vista` at
Ironforge prints **0 ERROR lines**; and both golden baselines held exactly, 269
with an identical failing name set and 80, `goldshire_cell` still at 1874.
Two examples answer "did it land", because "the file is in the archive" never
does. The reference mounts `patch-?.MPQ` by case-folded name, so every
`patch-A..Z` outranks `patch-4`/`patch-5`, and the toc-subset demotion cannot
see
an archive that carries no toc at all. `chain_report` prints the `FrameXML.toc`
copies in priority order and, with `--toc`, the winner of every row of the
realm's manifest with a tally — 68 to patch-4, 43 to patch-5, 32 to `patch.MPQ`,
7 to `interface.MPQ`, 1 to patch-3, and **none to a stale letter patch**.
`chain_shadow` answers from the archive's side: patch-4 carries 20326 paths of
which 10220 are shadowed, patch-5's 1494 are not shadowed at all.
Then, grouping the red bucket by panic *site* rather than trusting its label:
252 of the 269 are shipped-UI tests and **136 share one site**. Three of the
rest
were real, and are fixed here.
Six `OptionsFrame_*` enable/disable helpers were missing outright. `benilla.toc`
is the only thing that loads two of the reference's settings windows — the
realm's own toc lists neither `SoundOptionsFrame.xml` nor `UIOptionsFrame.xml` —
stock 1.12 keeps those six beside them in its own `OptionsFrame.lua`, and the
realm replaced that file with Turtle's rewrite, which drops them. Checked
against
all 151 interface rows, Lua and XML: nothing defines one. So those windows held
controls that could not grey out, with the first raise waiting on the click that
reached one. `assets/ui/OptionsFrameAdapters.lua` carries the reference's own
bodies, read out of `patch.MPQ` rather than from memory.
`CHARACTERFRAME_SUBFRAMES` was asserted to hold exactly five. The realm's
`CharacterFrame.lua` carries a sixth, `ArenaFrame` — which this client already
stubs (`CharacterFrameAdapters.xml`), so the count was the only thing wrong. It
now checks the reference's five by membership and order, which is what the test
is for; a realm may append.
And the kit's stand-ins were anonymous. `CreateFrame("Frame")` with no name
answers `GetName()` nil and `GetID()` 0, the publish is `setglobal` either way,
so naming them costs nothing and the absence cost two of the 136's three errors:
`FCF_SetWindowName` concatenating a nil name and `SetChatWindowDocked` refusing
window 0. `ChatFrame1`/`ChatFrame2` are no longer seated at all — every row of
`UIPARENT_MANAGED_FRAME_POSITIONS` is read through `frame = _G[index]; if
( frame )`, so a missing one is skipped, and seating them only shadowed the real
windows for good. The pass runs at load, from `ShapeshiftBar_OnLoad`, twelve toc
rows before the chat files, which is why the stand-ins were always first.
That leaves 136 on one cause, and the two tests added to `loader/tests.rs` are
what ruled out the first hypothesis: a template's `parent="UIParent"` does reach
the frame that inherits it, one level and two, which is how `ChatFrame1` is
built. The next suspects are written down in the gates memory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
280c2e886f |
Planar water: the mirror works, and ladders in at waterQuality 3 (Ultra)
The starburst was three defects, and none of them was the oblique projection I blamed it on in the last commit. What found them was looking at the mirror's own render target instead of reasoning about the composited frame — the water reads it through a fresnel weight, a rippled UV, a plane-match band and an SSR march that paints over it, so from the outside a broken mirror and a broken water shader look identical. WOW_MIRROR_DUMP=1 now writes that target beside the capture. (Bevy's own save_to_disk cannot: try_into_dynamic knows four 8-bit formats and answers UnsupportedFormat for Rgba16Float, logging and writing nothing.) - The oblique row's scale. Replacing the near plane with the water plane costs the FAR plane: `z >= 0` stops meaning "nearer than CAM_FAR" and becomes `depth >= a*d`, a plane through the eye tilted by a. Lengyel's own scale puts that plane exactly through the frustum's far corner, and the sky dome is pinned just inside the far plane, so it straddled the boundary and was sliced along its own tessellation. Scale by the supremum of d/depth over the frustum instead, with 2% of margin. - The water surface lay ON its own clip plane. This was the starburst and it was fatal rather than untidy: depth now means "distance above the plane", so a fragment at d=0 reads z/w=1, the nearest value reverse-Z has, and beats every other surface in the frame. The mesh is wave-displaced, so its triangles straddled the plane instead of lying in it, and each was clipped to a sliver that kept the winning depth. SURFACE_CLEARANCE lifts the plane a yard: the swell sums to 0.34 and the probed height answers for the surface under the eye, not this fragment. - The mirror was stored upside down. A true reflection is what a camera on the far side sees with its up vector reflected too — an improper basis no Transform can hold. The honest right-handed camera sees the same surfaces through the same frustum and stores the image flipped, so the shader samples 1 - v; that costs no winding flip, so backface culling stays correct. It read as a lake reflecting the ridge the RIGHT way up. Measured: Lake Everstill 2.87 -> 3.36 ms mean over 600 frames (+17%), the Stormwind canal inside the noise, CPU +0.2 to +1.1 ms. It is the first lane in docs/GRAPHICS.md that spends main thread, so it is a rung of its own above High and the default stays 2. Seated as a setting, per the standing rule that no graphics lane hides behind an env var: waterQuality gains tier 3, the Ultra preset takes it, and the Advanced Graphics dropdown gains a fourth entry in both locales. With the lane off, both scenarios are byte-identical to the build before it — which took two findings of its own: - The system could not live in Update at all. `&mut Transform` on a camera is shared with everything that moves anything; declaring it put 59 undeclared orders into Update and tripped the schedule ratchet, which is what kept this behind an env var. It belongs in PostUpdate with the skybox and the billboards, after propagation and before visibility, where it also reads the eye's settled pose instead of a frame-old one. (The comment blaming ResMut<Assets<Image>> for those 59 was wrong — dropping the asset handle changed the count by zero.) - A system that runs and returns on its first line is still not free. Holding &mut Camera and &mut GlobalTransform for a frame is enough to change which undeclared orders the executor picks: the canal moved 0.75% of its pixels by up to 96/255 with the lane switched off. So the whole system sits behind a run condition rather than an early return — and the same effect killed a .before(UpdateDirectionalLightCascades) order that was correct in itself. PostUpdate's ambiguity ceiling goes 466 -> 495 with the reason (the pairs are structurally impossible: nothing else in the schedule names the mirror camera); Update's ratchets down 4973 -> 4952. Also restores scripts/gates.sh's `where` example, which had gone missing. Its absence was silent — cargo's error went to /dev/null and `|| true` made it read as "no install" — so the memo key and the player-tests gate saw none even when WOW_DATA was set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
b8e8fd4382 |
A creature-skin record fills its slot only where it holds a path
`Monster1/2/3` exist so `CreatureDisplayInfo`'s skin variation can be dropped in
at spawn, and the record's own filename is the fallback where the display has no
variation for that index. The fallback was unconditional, and the slot does not
always hold a name: the stock models leave whatever the exporter left there.
That matters because a filled `texture` wins over the slot downstream
(`benilla_app::entities::display::skin_for`), so junk in it would mask the
display's real variation for good.
Measured over this chain, separating the two populations by whether the name
reads as a path at all:
* **26 records in 17 models bake a real `.blp` path** and are the winning copy
— the Crab, Scorpion, Crocodile, Thunder Lizard and RavenLord mounts, the
WC2 Death Knight (body, mount and a cape out of `ITEM\ObjectComponents`),
the Wizard Frog's hat, the Duck wearing a turkey skin, the Goblin rocket
car, the Worgen Demon Mage reaching for the Felhound's skin, Incubus,
Crystal Satyr, the alternate and Northrend bear mounts. These have nothing
else to draw with.
* **55 more carry unprintable leftovers** — Banshee, every DragonSpawn,
DruidCat, FrostSabre, Lobstrok, ObsidianDestroyer and the rest — all of them
in `model.MPQ` and `patch.MPQ` copies that this chain's letter patches
shadow, so none of them is a copy a read reaches today.
So `monster_skin_name` keeps only what reads as a texture path: printable ASCII,
no control bytes, longer than the extension, ending `.blp`. Every one of the 26
passes it and not one of the leftovers does.
benilla-bourbon (tripleslash/benilla-bourbon, "junk creature-skin records")
drops the fallback outright, on Turtle WoW's `Stag.m2` carrying one junk byte
there. Applied here that removes the skin from all 17 models above — the new
test fails exactly that way, "17 of 17 models with a baked skin path did not get
it" — so this takes the diagnosis and not the patch. The leftovers it names are
real; dropping the whole fallback is what this chain cannot afford.
Tests: a synthetic pair beside the guard for both kinds of name, and a corpus
test gated on `$WOW_DATA` that measures both populations over the winning copies
and asserts a path reaches `texture` while a leftover never does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
d95f26e57f |
A group WMO's MOGP runs to EOF, whatever size it declares
MOGP is a group file's last top-level chunk, so EOF is its real end. The walk
trusted the declared size instead, and `parse_group` answers an empty group for
a payload shorter than the 0x44 header — so a group that declares less than it
holds left MOVT/MOVI/MOPY sitting as top-level siblings *after* the payload and
parsed to no vertices, no indices and no batches. A building that draws nothing,
with no error anywhere to say why.
Seven files in this realm's chain do it, and all seven are the winning copy
(`patch-3.mpq`):
* the six `World\wmo\Outland\Shadowmoon\Slagpit\Shadowmoon_Slagpit01_00{0..5}
.wmo` declare 0x40, four bytes short of the header, over 33 KiB to 546 KiB
of geometry;
* `HumanHouse\WestfallHouse_MediumStraw_000.wmo` declares 0, over 41 KiB.
Measured before and after over the whole corpus: 7 of 7,795 group files parsed
to an empty group, now 0.
`wmo_chunk_overrun.rs` already covered the other direction — Undercity_144
declares one byte *past* EOF — but it only asks for the MOGP header, which
`find_wmo_chunk` clamps and which survives a short declaration too (0x40 still
carries flags, the portal refs and groupLiquid). The geometry is what was lost,
so the new corpus test asks for that: no group file of 1 KiB or more may parse
to zero vertices.
Ported from benilla-bourbon (tripleslash/benilla-bourbon, "a WMO group's MOGP
runs to EOF", credited there to kalistoking), whose `mogp_payload` and unit test
are taken unchanged; the Westfall house and the corpus test are this chain's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
44aac335fd |
classicapi: the last two with data behind them — item sets and area triggers
Both over DBCs 1.12 reads C-side and publishes to Lua nowhere, so both
are facts no addon on this realm could reach.
`C_Item.GetItemSetInfo`: the tooltip prints a set block and `GetItemInfo`
reports no set at all, so nothing can tell which pieces complete one. The
table's shape is its caller's own documentation (`SuperCleveRoidMacros/
ClassicAPI.lua:171-174`) — including the bonus pair being
`{ spellID, threshold }`, which is the REVERSE of the DBC's column order,
so the swap happens once in the host and not in every reader.
`C_Map.GetAreaTriggerInfo`: pfQuest's own comment says it reads trigger
geometry from this "instead of a shipped coordinate table", and the three
fields it takes — areaID, mapX, mapY — go straight into `pfMap:AddNode`,
whose coordinates are 0-100. So the percentages are the consumer's
measured shape rather than a guess. The zone resolution is the host's,
through the same `zone_uv` law the map window's own hover uses
(`WorldMapUiData::zone_at`), so a trigger lands in the zone the map would
name for that point — quirk included: a point exactly on a rect's east or
south edge reads as outside, which is that law's own `(0, 0)`. The three
map fields are absent TOGETHER when the centre resolves to no zone (an
instance interior), which is exactly the case pfQuest guards for.
Both ride host probes rather than pushed tables: the resolution needs the
`WorldMapArea` rects, which do not cross the seam, and neither catalog is
something to copy per VM for a verb nothing calls at boot. Two catalogs
gained an `iter()` for it, each said to be for this.
That leaves four of the original 56. Two have no caller in the corpus;
`C_FriendList.SetFriendNotes` has no note field on the 1.12 wire at all,
so a note typed and lost at logout would be worse than a button that does
nothing; and `C_BattleNet.SendGameData` stays deliberately absent, because
every caller guards it and registering it would claim a BattleNet this
client does not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
546e60765d |
classicapi: fourteen members over four namespaces — the measured tail down to six
Of the 153 `C_<Namespace>.<Member>` names grom's addons reference, the fresh VM was missing 56; this takes it to 6. Fourteen members here, each over data or a stock verb that already existed: `C_Spell` gains the four melee-timing predicates and `IsSpellInRange`. 1.12 answers all of those only about an ACTION BAR SLOT — `IsAttackAction` and `IsAutoRepeatAction` — so a spell the player knows but has not placed on a bar cannot be asked at all, which is the gap a condition engine keeps running into. The three attribute answers ride named booleans on the spell lookup rather than a raw attribute word, so the bits and their reference citations stay in `benilla-formats` where they can be checked; `do_not_reset_combat_timers` is named there for the first time, beside the `ranged_speed_cooldown` that already tested the same bit. The melee auto-attack is an ID (6603) rather than an attribute, because no bit marks it — the ranged half has the auto-repeat flag and the melee half is that spell. `C_SpellBook.GetSkillLineName` is the most-read missing member of the whole family (nine call sites) and is one delegation to `GetSpellTabInfo`. `IsAutoAttackSpellBookItem` resolves by id against the same 6603, so the two cannot disagree about what auto-attack is. `C_Item.GetCurrentItemLevel` answers a number 1.12 prints in a tooltip and returns from nothing, so no addon can compare two pieces of gear. `RequestLoadItemDataByID` is the modern async load over the ask-once queue `ItemMixin:ContinueOnItemLoad` already drives. `GetWeaponEnchantInfo` is a pass-through. `C_QuestLog`'s abandon pair delegates to the stock two-step, so the mark, the still-in-the-log search and the send keep one owner — a namespaced abandon cannot drop a quest the stock path would have refused. The six left are each written down with what they need. Two are a host probe over a DBC `benilla-formats` already parses (`AreaTrigger` for pfQuest's trigger geometry, `ItemSet`), two have no caller in the corpus, `SetFriendNotes` has no note field on the 1.12 wire at all — a note typed and lost at logout is worse than a button that does nothing — and `C_BattleNet.SendGameData` stays deliberately absent, because every caller guards it and registering it would claim a BattleNet we do not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f87ef148b8 |
app+ui: the trinket cooldown — and a stock verb that always said "cold"
Built: GetTrinketCooldown, UseTrinket, GetSpellRangeData, SetLocalRaidTargetIndex, GetQuestLogQuestIds, GetQuestDialogQuestId, CombatLogFlush. The find on the way through is not a nampower verb. **`GetInventoryItemCooldown` is a stock 1.12 verb and it answered `(0, 0, 1)` unconditionally** — right shape, always "cold", so nothing errored and no worn trinket ever drew its cooldown swirl on the character pane. Stock PaperDollFrame.lua:691 calls it on every slot update. Feeding an equipped slot's cooldown for GetTrinketCooldown fixes that one off the same column, and the test runs `CooldownFrame_SetTimer`'s own arithmetic against it. The feed keys its query on the (use spell, item entry) pair exactly as the bag feed does, so one trinket reads the same cooldown from the doll and from a bag. **The risk was the gate, and my first test for it was vacuous.** `feed_char` sits behind a change gate and no descriptor moves when a cooldown is armed, so `Cooldowns::feed_epoch` had to become a gate input. The first test passed with that input deleted: `run_system_once` builds a fresh system each call, so the gate's `Local` memo resets and `vm_reset` opens the gate every time. A test for a gate input has to run the system the way the schedule does. Rewritten with `add_systems` + `update`, it fails with the input removed — verified by probe, and the reason is written at the test. `feed_char` also hit Bevy's sixteen-parameter ceiling, which was a fair signal its parameters wanted grouping by what they are for: `ItemCooldownInputs` and `RepairInputs` replace six. Two decisions recorded at the code: - `GetSpellRangeData(rangeIndex)` answers three values, not a table. Every table-returning verb in this DLL has its key names in the string pool and this row's are not there. `flags` bit 0 is the melee family, where the engine takes the combat-reach sum instead of the authored pair — a caller ignoring the bit reads 0..5 for every melee ability. - `CombatLogFlush()` is a deliberate no-op. Upstream buffers its combat-log writes and this empties the buffer, so what an addon asks for is "my lines are on disk now" — and `ui_chat/logging.rs` writes each line to an unbuffered File as the window shows it, so that holds at all times. Defining it is the honest answer: the capability is present, and a missing global makes `HasFeature` report it absent. Also fed, both one wire column each: `QuestState::quest_id` (the wire names the quest on all three giver panels and 1.12's Lua names it nowhere — a title is not a key) and `SpellDisplay::stack_amount`'s neighbour work from the previous commit. Five names stay out, each now with the field list that makes it a decision rather than a shrug — `GetUnitData` (80 raw descriptor columns, read off the pool, zero call sites, and a partial answer is worse than none), `GetRaidTargets` (the reference keeps a board; this client feeds the mark per unit, and a scan over named tokens would silently omit a marked unit), `DisenchantAll` (a server-paced sequencer, which is the queue), `GetSpellPower`, `StartItemExport` (writes a file — it belongs with the withheld seven). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f06caa9968 |
ui+app+formats: the Spell.dbc row, and seven columns that answered nil
`GetSpellRecField` knew three names. Counting what the corpus actually asks it for: `name` (4 sites), `school` (3), `stackAmount` (2), `spellIconID` (2), then one each of `rank`, `rangeIndex`, `mechanic`, `manaCost`, `effectMechanic`, `effectApplyAuraName`. Seven of those ten answered nil, and each was a feature quietly off rather than an error — DoiteAuras caps its stack display on `stackAmount` (DoitePlayerAuras.lua:539) and SuperCleveRoidMacros classifies a crowd-control effect off the two arrays (Utility.lua:7776-7794). The eighty field names are the DLL's own, read off its pool in Spell.dbc column order. The mapping to `SpellDisplay` lives in `ui_script/spell_rec.rs`, beside the struct it reads — a copy on the VM side would be free to drift — and `GetSpellRec(spellId)` is that same field list walked through that same per-field reader, so the two verbs cannot answer differently for one name. A test asserts exactly that, key by key, over one row. The array columns are a 1-based table of three, which is measured, not guessed: SuperCleveRoidMacros walks `effectMechanic` as `for i = 1, 3 do local em = effectMechanics[i]`. `totem`, `reagent` and `reagentCount` take the same shape, two and eight long. One new DBC column — `StackAmount`, 39, bracketed by two constants this tree already had (38 ModalNextSpell, 40 Totem[0]). It is the only one added: fifteen further names upstream answers are left nil rather than putting fifteen unused u32s on a struct the spell catalog holds one of per row. They are listed at `FIELDS` with that reason, so the gap is a decision rather than an omission. `GetSpellRecField`'s three string columns keep the spellbook fallback the verb had before the probe, so a VM with a book and no catalog — a capture, or the boot VM before the catalog loads — still answers a learnt spell's name and rank. Every other column is a DBC row and has nowhere else to come from, which is why the fallback is exactly three names wide and says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
02e6fc4208 |
formats+app: a stale snapshot archive mounts below the realm's patches
grom is keeping `patch-A.mpq` — it is almost certainly textures — so the shadowing it causes is handled in the chain instead of on his disk. `mount_order` stays the reference's law, letter above digit and all, and `demote_stale_snapshots` runs after it: an archive whose `FrameXML.toc` is a **strict subset** of another's is an earlier edition of the realm's manifest, so it mounts just below the patch set — above `patch.MPQ` and the base archives, so anything only it carries still resolves. A toc that differs by *removal* is a different manifest rather than an older one and is left exactly where its name puts it. `WOW_CHAIN_DEMOTE=0` keeps the reference's order for comparison. On his install `patch-A.mpq` (Aug 7) now loses to `patch-5.mpq`, so the realm's current data is what loads: its `FrameXML.toc`, its `GlobalStrings.lua` and a `Spell.dbc` with **28019** rows against the 27992 his last session reported — 27 spells the update added that were invisible. Reading the realm's real files then broke ten tests, all of which had pinned August's behaviour, and each is now pinned the realm's way: - `AIOHandlers.lua` claimed the `OctoRadio` block as a no-op on the evidence that the radio file carried no AIO call. The update built the radio out into a file twice the size whose `RegisterNowPlayingFeed` registers that name at `VARIABLES_LOADED`, and `AIO.RegisterEvent` asserts on a duplicate, so our shim raised through seven tests. The row is gone, and the shim now claims at `PLAYER_LOGIN` through `pcall` so a realm that implements one of these takes the name and we step aside. `no_aio_block_we_shim_is_one_the_realm_now_handles` fails a row the realm has implemented — verified by putting `OctoRadio` back. - `BATTLEFIELDS_CLOSED` no longer hides the battlefield list, by the realm's decision: the server stamps `SMSG_BATTLEFIELD_LIST`'s battlemaster guid with the player's own, so the client anchors the window to the spot it opened and closes it when you step away, on foot or on a boat (bug tracker #330 and its follow-up). Pinned that way, with the close button proving the list still closes. - `MAX_QUESTLOG_QUESTS` is 25, not 1.12's 20. The test reads the realm's global now, so the next bump is data and not a failure. - The realm's `OctoQuestLogPoller` redraws the log every 0.2 s, which legitimately costs a layout gate walk. `POLLS_ON_A_TIMER` stands it down by name before the quiet-frame bench measures, rather than raising the threshold and blunting the one instrument that catches an every-frame writer. A timed realm handler that is not listed fails. 269 data-gated failures, identical to the baseline at HEAD name for name, and all nine gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
73bc9a9e8e |
app+formats: name the realm files a shadowing archive holds stale
The archive found holding an old copy of the realm's own `FrameXML.toc` is holding an old snapshot of the realm's interface, not one old file. `Chain::copies` answers which archives hold a name and at what size off the hash and block tables alone, decompressing nothing, so the boot log can now name every row of the realm's toc whose winning copy comes from that archive while the realm's own has a different one. On grom's install that is ten, and it is the better half of the finding: `GlobalStrings.lua`, five `OctoLocale` files, `AIO.lua` — the client and server's whole transport — the calendar, the challenge tips and the radio all load as an August `patch-A.mpq` has them, because a letter patch outranks the realm's `patch-5.mpq`. `Spell.dbc` is shadowed the same way, 20 KB behind. Reported and not repaired, deliberately: shadowing a file with different content is what a patch archive is *for*, so outside the toc — where "holds every row the winner does, plus more" is a decidable relation — this client cannot tell a stale copy from an intended override. The real 1.12 client reads the same copies, so this is a fix on disk, and the line is what makes it findable instead of invisible. `richest_toc` splits the superset rule out of `realm_toc_rows` so it is testable without client data, which is where it ships from: the winner stands unless a shadowed copy drops nothing, a longer copy that drops a row is passed over, and the caller is told which archive it read so it can say so. The staleness report is checked against the real chain and every row it names must really differ in size between the two archives, so it cannot be noise. The 269 data-gated failures remain identical to the baseline at HEAD, name for name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
eaadc9f73b |
app+formats: take the realm's own FrameXML list, not a curated copy of it
`benilla.toc` enumerated every interface file, so a row the realm adds in a client update was *silently* absent: no "not found", no warning, just a global nobody defined. That is what `/xpboost` was, and the only way to find it was to diff two MPQ archives by hand. The manifest now decides order and substitution, and the realm's own `FrameXML.toc` decides membership: every row it carries that the manifest does not name loads after it, in the chain's order, and the boot log names them. `DECLINED` is the short list of rows this client must not run (`StatsFrame.xml`, whose OnUpdate is the only reader of a `GetDebugStats` we do not have), each with its reason; a row we merely have no verb for still loads and raises where it is used, which beats silence. `WOW_FRAMEXML_TAIL=0` opts out and says what it is leaving out. Finding that out turned up the reason the row was missing in the first place, which is worse and is now fixed too. `Chain::open` mounts `patch-?.MPQ` in the reference's order — case-folded, last wins — so `patch-A.mpq` mounts **above** `patch-5.mpq`. The realm ships its interface in `patch-5`; this update added six rows to that toc; and the 140-row copy inside an older `patch-A.mpq` kept winning. The interface was loading August's list of files. So `Chain::read_all` returns every archive's copy of a name in ascending priority, and `realm_toc_rows` reads a shadowed copy when it holds every row the winner does *plus* more. That case is unambiguous — nothing was removed, so nothing can be lost — and it is a packaging accident with no intent behind it either way. A copy that disagrees by removal is a real change of mind we cannot arbitrate: the winner stands and the log says what was passed over. Both paths say which archive, so the clean fix on disk is one line of log away. Verified against grom's own install and his addon corpus: with the `OctoXpBoost` row deleted from the manifest — tomorrow's update, simulated — the tail finds it, resolves it to a readable chain path, and `/xpboost` is a live slash command with a function behind it. The tail is empty with the manifest as it stands, which is the point: the mechanism is armed and changes nothing today. Tests: the merge takes both lists as arguments so it runs where there is no client data, since that is the machine where a regression in it would otherwise skip unseen; the letter-above-digit mount order is pinned on its own, because that fact is what cost the six rows; `read_all`'s ordering contract is checked against a real chain; and a `DECLINED` row that the realm stops shipping now fails as stale. The 269 data-gated failures are the same 269, name for name, as at HEAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
089990a929 |
chain: the icon catalog lists its own directory, not all 102,144 names
Profiled off the player's own 2026-10-18 session log, where one blocked frame ran 7,725 ms: the file loading is 4,505 ms of it (pfQuest's `init\data.xml` alone 695 ms, pfQuest-octo's 311 ms, all of FrameXML 693 ms), then `load_icon_catalog` 396 ms, the random-property table 259 ms, and the first VM tick the remaining ~2.3 s. This takes the icon catalog. It walked `Chain::list()`, and the walk is what the catalog costs — the module says so itself: classifying the three buckets out of the names is 1.4 ms. Per name a full list allocates a normalized dedup key, hashes it into a 102k-entry set, resolves the winning archive and asks it for a size; the prefix this caller wants was tested *after* all of that, on every name in the chain. So `Chain::list_under(prefix)` tests it first, with the same case-insensitive, separator-blind compare the caller was doing for itself. Measured on this install, and the test prints the pair it quotes: 102,144 entries in 471 ms listed whole against 51 ms for the 3,205 under `Interface\Icons\`, the same entries and the same sizes either way. 420 ms off every login, and off every `/reload`, which rebuilds the catalog for the new VM. Nothing else in the client lists the chain; `benilla-extract` does and wants the lot, so `list()` keeps its name and its body, now shared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
df921cdb47 |
creature: a display that names a model is drawn, whatever its scale column says
A screenshot of the Gnomeregan Reclamation Facility (OctoWoW's own area 5211) with its NPCs replaced by flat grey boxes, and the target frame filled — `M. C. Volterwhite`, 57, 4.13k — behind an empty portrait. The boxes are `entities::attach`'s model-less debug cube, and the empty portrait is the same cause: the display resolved to nothing to draw. `new_creature_display` took a row only `if m.scale > 0.0`, where `scale` is `CreatureModelData.ModelScale × CreatureDisplayInfo.CreatureModelScale`. That column does not scale a spawned unit: the server folds it into `OBJECT_FIELD_SCALE_X`, which is why `Creatures::model_scale` says in its own doc that the glue screens' pet is its only reader. So the gate was testing a number nothing downstream consults, and a `0.0` in it meant "no model". His chain's `CreatureDisplayInfo.dbc` (the effective copy, `patch-A.mpq` — patches sort ascending by lowercase name, so A wins over 5) carries a contiguous custom block, **20256-20356, 100 rows, every one a character model**: 27 HumanMale, 16 HumanFemale, 16 TrollMale, 12 GnomeMale, 10 NightElfFemale and the rest, all authored with that column left at `0.0`. One batch of custom NPCs, one zone's worth of grey boxes. Stock 1.12 ships three of its own that way too (displays 16, 17 and 19: HumanThief, HUMLBlackSmith, HUMLMerchant), which is the proof the column was never a visibility switch. The gate is gone. The one shape left that earns a cube — a display id with no row in either DBC — now says so once per id, as the GameObject leg already does, so the next zone full of cubes names the ids instead of needing a screenshot. A test over the real chain pins the data: every zero-scale row still names a real model, and the only row that answers `None` does so for want of a `CreatureModelData` row (display 185, `ModelID = 0`), not for its scale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
81d96af35d |
Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (
|
||
|
|
706cd717a3 |
merge: absorb samwhosung/main on top of the upstream merge
Same fork policy: upstream structure wins, local-unique re-instated. Cargo.toml unions both measured dev-profile philosophies (codegen-units=1 + lto=off from samwhosung, debug=line-tables-only + split-debuginfo from the fork's disk measurements). glue_booth.rs takes samwhosung's typed GlueScene (token/of_race/from_model_path) and re-adds the fork's TurtleWoW races (Goblin=9, BloodElf=10) as variants; their glue fog stays on Human's row, exactly as the fork's scene_fog fallback did. ui script takes samwhosung's mouseover/hyperlink rework (world_mouseover, minimap_blip, hyperlink.rs) over the fork's inline hover_link/span_at/pointer path — the rework covers the same OnHyperlinkEnter/Leave behaviour. game_plugins.rs keeps the fork's re-measured ceilings (466 / 5,509); mail.rs keeps the explicit 13-tuple with a float daysLeft; MAP.md takes the superset. |
||
|
|
26d714fd1d |
merge: absorb upstream/main into the Turtle fork
Fork policy (docs/METHOD.md): upstream structure wins; local-unique work re-applied on top. This commit is the upstream half of the reconciliation. Upstream is the same program at a rewritten history ( |
||
|
|
24c7cab983 |
Stock character select roster; the screen asks for the list (#1014)
Part 4a of the stock GlueXML (`WOW_GLUE_STOCK=1`): character select's roster. - The select verbs run on the reference's roster (`0x4736c0`): `GetCharacterInfo` (race and class from `Name_Lang`), `SelectCharacter` with its clamp and event, the facing gate, and `GetCharacterListUpdate`. They are backed by the roster load and teardown (`0x4724d0`, `0x472580`) and screen state 3. - Under the stock screens the character list is fetched only when the screen asks, as `0x46b880` does. The world connect completes on the auth response (`0x5aab80`), not on a roster. - Every character-park request goes through one ordered channel. A roster reply completes only the current link's query. A late auth after a cancel is dropped, Warden stays terminal, and a failed list shows `CHAR_LIST_FAILED`. The native screens are unchanged. Fake-server tests cover the ordering, the cancel, the realm change and the native policy. Part of #856 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
ee6c76b988 |
Merge upstream main (samwhosung/benilla c2fc3cf3) into the graphics line
Conflicts: capture fixtures/scenarios (both sides appended: kept both), options_tests row counts (upstream 80->81 rows: ours 100->101 and 99->100), docs/MAP.md regenerated with scripts/genmap.sh. |
||
|
|
ea7e0d087a |
Zone names stay uncoloured on PvE realms outside capitals, as in 1.12.1 (#825)
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> |
||
|
|
eecbf04d6e |
Normalize existing sources and documentation with treefmt
Apply the formatter baseline needed by the generated CI gate. Verified the full treefmt check with vendored sources and generated MAP.md excluded. Parsed formatted Cargo manifests and compared their values with HEAD; all are identical. |
||
|
|
efbf14a22a |
Light bands with out-of-order keys interpolate as a cycle, as in 1.12.1 (#768)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
e12ca6769c |
/wave <name> targets the named player; two DoEmote gates follow 1.12.1 (#769)
Three ways `DoEmote` and the chat talk gesture differed from 1.12.1: - `/wave Bob` ignored the name and targeted the selection. 1.12.1 resolves a name that does not start with `%` as a player, at any range (`0x49fdb1`-`0x49fdf0`, `0x493aa0`), and sends the emote untargeted on a miss. `DoEmote(token, name)` from Lua does the same. - A polymorphed, stunned or sleeping speaker still played the talk gesture. `UNIT_FIELD_FLAGS 0x20000000` suppresses it when the sender's name is cached (`0x49d7f7`). - `DoEmote` sent while our unit was possessed (`0x5ef57e`). A chat-only emote now reads `Emotes.dbc` row 0's flags, so it is refused while asleep or dead (`0x5ef5b1`, `0x47db8e`). Tests cover the name, miss, `%t` and self cases, the gesture gate, and both `DoEmote` gates against the install's row 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
9bc7276dd8 |
fix: spell text $g takes the player's gender, as 1.12.1 does (#787)
- spell text: $g/$G take the active player's gender and the chosen form is space-trimmed, so a female character reads "her" (0x508180, 0x508214) - spell text: trim the $g docs, drop the colon-less $g pin - spell tooltips: $g reads the view's player store; nets cover gender - item tooltips: re-substitute $-texts when the gender byte moves - tests: share one real-spells loader beside Spells Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
bd48bb5478 |
MONKEY (gfx): repair alpha-blended eye glow materials
Use additive RGB, black fog and no depth writes for named unlit mode-2 eye-glow batches. This preserves the background behind opaque black glow-texture backing. Verified two focused tests including installed Undead models, Windows play build and the app test gate. Co-Authored-By: GPT-6 Astra (Codex) |
||
|
|
8c9b1d9984 |
TurtleWoW compat: the create screen loads races 9/10, so the character and Blood Elf stage show
The char-create catalog compared `CharBaseInfo` against the stock 1.12 rows exactly and bailed on any difference. TurtleWoW adds class combos (Human Hunter, Orc Mage, Dwarf Warlock, ...), so the whole catalog failed to load: `body_display` was unavailable and the glue booth assembled no body on both the create and select screens. The creatable set is now read from `CharBaseInfo` (the stock eight, or a patched chain's more), the stock rows are a subset guard against a misparse, and race 9 (Goblin) and 10 (BloodElf) load their display, dial ranges and outfit like the shipped eight. The chain's `ChrRaces` row 10 is `BloodElf`, not the "High Elf" the code named it, and `scene_token` mapped 9/10 to Human, so a Blood Elf stood on the Human stage. Both now resolve (`UI_Goblin`, `UI_BloodElf`) and the display name is "Blood Elf"; the fileString is what the race stage keys on. The create screen hard-coded the eight vanilla races. It now splits the catalog's creatable set into the Alliance and Horde columns, so a TurtleWoW chain shows five rows per column (Blood Elf appended to the Alliance, Goblin to the Horde), with the tower and lower clusters shifted to match its XML. Race-icon UVs are computed from the loaded sheet's size, so the stock four-column sheet and TurtleWoW's widened eight-column sheet both read correctly. |
||
|
|
67b7ef8994 |
spell: a shot with a real cast time opens the cast bar
The bar was suppressed for every `Attributes & 0x2` spell, the reference's `0x6e7700` gate read literally. That gate is why Aimed Shot and Multi-Shot show no bar in 1.12.1 — a later client added `Force Display Castbar` to fix exactly this, so the reference behaviour is the bug. Gate on the DBC row instead: a spell opens a bar on the wire cast time whenever `SpellCastTimes.dbc` has a nonzero one for it. Aimed Shot's real 3 s cast does, so it bars. The instant shots do not — they point at row 1, the instant sentinel — so Throw, Multi-Shot, Auto Shot and wand Shoot stay bar-free, and vmangos's 500 ms pad on a non-auto-repeat ranged cast (`SpellEntry::GetCastTime`) cannot conjure one either. An unknown spell, or a build with no cast-time table, keeps the reference's bar rather than losing every bar. `SpellCastTimeCatalog::from_rows` is the constructor the DBC loader and the test share. Verified: `cargo test -p benilla-app -p benilla-formats` — 3833 pass, the new `a_timed_ranged_cast_opens_the_shot_bar_but_an_instant_shot_does_not` among them. The one failure, `assets_ui_does_not_grow`, is pre-existing on main. |
||
|
|
503cde608a |
build: green fmt and clippy on the pinned 1.98.1
The gate was already red on main, in files no round since had touched. `cargo fmt` reflowed one wrapped assert in the benilla-ui loader tests. Clippy's 1.98 set added four lints the tree does not satisfy: `doc_lazy_continuation` on room_claim.rs's `2b.` item (indented, as it already renders), and in benilla-extract `chunks_exact_to_as_chunks` on the MOGI walk, `manual_checked_ops` on the census mean, and `needless_borrow` on the class table call. Each takes the fix clippy names. 33 errors remain in benilla-world's lighting, static_gx, water_fx and liquid trees, from the same lint bump; they are left for their own round rather than folded in here. Verified: cargo fmt --all --check clean; `cargo clippy --workspace --all-targets -- -D warnings` now fails only in benilla-world. |
||
|
|
494d232c11 |
Merge upstream benilla main (b396bbf6) into everwood-graphics
105 upstream commits (v0.2.0 |
||
|
|
6c25640a37 |
Light bands with out-of-order keys interpolate as a cycle, as in 1.12.1 (#768)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
56d8011ab7 |
NPCs pause their work loop while you talk to them, as in 1.12.1 (#772)
Two ways the emote-state base pick differed from 1.12.1. - A blacksmith kept hammering while you used his vendor window. The resolver `0x5fd770` passes over a state whose `Emotes.dbc` flags have `0x2000` (work, mining, ready, dance) while the unit is the interact NPC (`0x5fd7e2`-`0x5fd7fd`). `SetInteractNPC` re-picks on open (`0x493198`) and on close (`0x493219`, before the name clears at `0x49334b`), never for a player. So the NPC stands, and the loop resumes at its next re-pick. - A set state outranks the idle fallbacks: `0x5fd770` runs before `0x5fd830`, and a row with AnimID 0 claims Stand. After `/stand` (state 26), a swimmer at rest shows Stand until it moves. Tests cover the open, the close hold, players, a pose under a one-shot, and the precedence order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
2dc7d8a9e6 |
/wave <name> targets the named player; two DoEmote gates follow 1.12.1 (#769)
Three ways `DoEmote` and the chat talk gesture differed from 1.12.1: - `/wave Bob` ignored the name and targeted the selection. 1.12.1 resolves a name that does not start with `%` as a player, at any range (`0x49fdb1`-`0x49fdf0`, `0x493aa0`), and sends the emote untargeted on a miss. `DoEmote(token, name)` from Lua does the same. - A polymorphed, stunned or sleeping speaker still played the talk gesture. `UNIT_FIELD_FLAGS 0x20000000` suppresses it when the sender's name is cached (`0x49d7f7`). - `DoEmote` sent while our unit was possessed (`0x5ef57e`). A chat-only emote now reads `Emotes.dbc` row 0's flags, so it is refused while asleep or dead (`0x5ef5b1`, `0x47db8e`). Tests cover the name, miss, `%t` and self cases, the gesture gate, and both `DoEmote` gates against the install's row 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
4f36c843f1 |
Emote voices play only for you and your party, as in 1.12.1 (#766)
Every text emote's voice played for every performer, so every `/cheer` in a city was heard. 1.12.1's composer tail (`0x623c80`) plays it only for you or a member of the four party slots (`0x623cc2`-`0x623cce`, `0x5efea0`), and drops an ignored sender before that (`0x49dc36`). What changed: - The voice plays at attachment `0x11` and stops the unit's previous emote voice once a new one opens (`0x623c10`). - Race and sex come from the display override before the descriptor (`0x60c6a0`/`0x60c6c0`). - `SMSG_EMOTE` no longer plays `EventSoundID`. The one-shot path never reads it (`0x5e66b0` → `0x5fcd20`); only the state emote's `$ESD` does. Tests cover each relation class, an ignored performer and the per-unit voice slot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
031acdd8bb | Fix TurtleWoW baked NPC atlases (append .blp) plus in-progress GFX work: moonlight, torch depth, water/scene-depth, skybox | ||
|
|
4772489aad |
Arriving bodies composite off the main thread and show once dressed (#732)
A crowd of arriving players was composited in one frame, on the main thread, under the archive lock: 40 geared looks took 76–115 ms. In 1.12.1 an unforced full composite never waits on its section loads (`0x44b430`), and a unit whose first composite is pending is not drawn: ShouldRender `0x607da0` feeds the draw flag `0x710c50`, and the flag goes to the mount model when mounted. It appears dressed once the composite is done. Now an arriving body composites on the async pool, one task per look, reading outside the lock. The body, and its mount, are built once the atlas lands. Arriving now costs the main thread 0.26–0.45 ms for 40 looks. The appear fade runs from arrival, as `FadeTo` is armed at create. A re-dress, a rig heal and the glue and dressing-room previews stay in the frame. Baked NPC skins are unchanged. Fixes #609 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
15e83f2483 |
Ghost and underwater ambience beds resolve by their reference names (#731)
A ghost heard no ambience. The ghost bed was looked up as a SoundEntries row named "Ghost", which does not exist, so it fell back to kit 0. The underwater bed was a hard-coded row. At init, 1.12.1's `0x4609b0` walks SoundEntries from the last row down. It keeps the rows whose names match `"Ghost (DONOTRENAME)"` (`0x836444`) and `"Underwater (DONOTRENAME)"` (`0x836428`) exactly and case-sensitively (`0x64a480`, a `strncmp` through the NUL). In the install those are rows 4160 (`GhostState.wav`) and 4209. The catalog now resolves both beds once, the same way, and the ambience selector takes them from it, with order and fades unchanged. Tests cover the install's two rows, the exact-name match, the walk order, a ghost selecting its bed over water, and an unresolved bed staying silent. Fixes #721 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
4c18c23c94 |
Ambience beds stream from the archive, opened off the main thread (#725)
An ambience change read and decoded the whole bed on the main thread, about 40 ms a swap. 1.12.1 opens the bed as a looping FMOD stream that reads the archive through its file callbacks (`0x460b54` → `0x7a54d0`, `FSOUND_Stream_Open` at `0x7a5525`). Now: - MPQ files open for in-place reads, one sector at a time. - The bed streams from the archive, opened on the IO pool. A newer swap drops an open still in flight. - kira moves to 0.12.4 on symphonia 0.6. 0.5 sized PCM frames from `nBlockAlign`, which 98 stereo WAVs store as 2, so a streamed bed fell silent halfway. - A fork patch stops a stream at its last packet instead of spinning. - A sector that inflates short is refused. Tests cover a bed's true length, a seamless loop, an off-thread open, the stop at the last packet and the refusal. Fixes #700 Fixes #8 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
2b16219357 |
Spell descriptions resolve $b, $f, $F, $i, $q, $u and $v (#622)
Spell descriptions printed `$b`, `$f`, `$F`, `$i`, `$q`, `$u` and `$v` raw. The reference's expander resolves each from Spell.dbc: | token | column | |---|---| | `$b` | EffectPointsPerComboPoint | | `$f`/`$F` | DmgMultiplier | | `$i` | MaxAffectedTargets | | `$q` | EffectMiscValue | | `$u` | StackAmount | | `$v` | MaxTargetLevel | `$b` chops toward zero. `$f` prints one decimal, and `$F` rounds half away from zero, both after the scale and in double precision. The integer arms print a signed `%d`. This change reads the new columns and adds the arms. `$f` and `$F` keep keying the `$l` plural, under the recorded deviation. Tests pin each arm, the schema's float columns and every shipped use of the six letters. Chain Heal now reads "Each jump is 50% as effective". Fixes #593 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
602c30389a |
spell range: two running units get GetMinMaxRange's moving bonus (#702)
A warrior chasing a running mob was refused Hamstring at 6-7 yd with "Out of range.", where 1.12.1 lets it go out. After both arms, `GetMinMaxRange 0x6e3480` adds 2.6667 yd (`[0x811914]`) to the max when all of these hold (`0x6e3648`-`0x6e36a2`): - the caster and the target unit both move, with a bit of `0x200d` set; - both run faster than twice their walk speed (`0x5fc350`); - the row is melee, or the target is a player. On the melee row, the auto-attack target stands in when no unit is passed. `min_max_range` now takes each unit's motion and adds the bonus. Every caller passes what its reference call site passes. The ambiguity ceiling rises by the 11 pairs this adds, with the reason beside it. Test: Hamstring goes out at 7.5 yd with both units running and is refused standing still. Fixes #678 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
3a613971bb |
UnitCreatureType answers the one resolver's type for every unit, so a cat-form druid or a Ghost Wolf shaman is a Beast: the snapshot takes the form, then the template, then the race (0x605570, 0x51a2bc) (#681)
`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> |
||
|
|
3b3ae850fa |
spell range: min_max_range takes the unit target and the auto-attack target apart, so a tooltip no longer pads a ranged spell by the swing target's reach (#654)
`min_max_range` took one `target_reach` that served two roles: the melee arm's fallback to the auto-attack target and the ranged arm's explicit target. The ranged arm padded whenever it was `Some`. The tooltip passed the auto-attack target's reach, so while you swung, every ranged spell's tooltip read its row plus both reaches: Fireball read "38 yd range" where 1.12.1 reads 35.
`GetMinMaxRange 0x6e3480` counts its target (`[ebx+0x10]`) only when it is a unit (`0x6e34e1`-`0x6e34f9`).
- **Melee arm** (`0x6e3552`-`0x6e3594`): with no unit target, it falls back to the caster's auto-attack target (`0x6e356a`), then to the caster's own reach.
- **Ranged arm:** pads only for a non-null target (`0x6e35d8`, `0x6e35ec`). The max always takes the pad, and the min only when nonzero.
What each caller passes:
| Caller | Call site | Target |
|---|---|---|
| Tooltip | `0x52e9c2` | none |
| `ActionHasRange` | `0x4e5852` | none |
| The cast check and `IsActionInRange` | through `CanTargetUnit 0x6e4440` → `0x6e47b0` | the bound or selected target |
Changes:
- `min_max_range` takes `RangeTargets { target, attack_target }`.
- The melee arm reads the target, else the attack target, else the caster. The ranged arm pads for the target alone.
- Each caller passes what its reference call site passes. The action button's two verbs are now two calls: `has_range` with no target and `in_range` with the selected one.
- `min_max_range_reads_reach` narrows to melee rows. The tooltip safety net then counts 813 reach-dependent views instead of 10,298, so starting to swing no longer rebuilds every ranged tooltip.
Tests:
- `ranges.rs`: `the_ranged_arm_pads_only_for_the_explicit_target`, `the_melee_arm_falls_back_from_the_target_to_the_attack_target_to_the_caster`, and the narrowed exactness test.
- `range_cell_on_real_data` reads Fireball as "35 yd range" and Charge as "8-25 yd range" with an auto-attack target present.
- `cast_range_refusal_follows_the_two_compares` covers a bound target's pad.
- `a_ranged_button_pads_its_range_by_the_selected_target_alone`.
Fixes #643
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
44fb6e5e43 |
spell tooltip feed: each pushed view records what it read, and a change of the player's state rebuilds only the views that read it (#650)
Every spell-tooltip invalidation rebuilt every view the feed had ever pushed. The remembered set only grows: the book, every talent rank and trainer service, and every aura ever seen. So in a long session each of these frame-hitched for 1.5 to 4 ms: - a crit or dodge change; - a worn-slot change; - a target swap while auto-attacking; - a spell-modifier packet; - a reagent cast. The reference builds a tooltip at the hover (`0x52e610`) and keeps nothing. benilla pushes views ahead of the hover, so it has to know what each view was built from. Each pushed view now records, as it is built, the inputs it read: the `$z` home area, the form line, the worn slots the equipped-item search reads, and so on. Each record sits at the line that reads the input. The inputs: - the `$z` home area; - the form line (a spell with `Stances`); - the worn slots the equipped-item search reads, each slot's item, template and broken state, and the disarm flag; - the chance line's percentage (`Effect[0]` 20, 22, 23, 78); - the combat reach the range cell reads; - the skill lines the `$`-tokens scale by, the spell's own and those of each spell it cross-references; - the modifier family-flag bits of the spell and of those it cross-references; - the reagents it lists; - the player's level, base mana, max health and max power where the cost or cast cell reads them; - the ranged attack time the cooldown cell pads by. The feed diffs those inputs frame to frame and rebuilds only the views whose recorded reads intersect what moved. A class-family change still rebuilds everything, and the pet's own inputs still rebuild the pet views. The feed moves out of `ui_tooltip/mod.rs` into `spell_feed.rs` (the builder and the system) and `spell_deps.rs` (the records and the diff). `TokenContext::home_area` becomes a closure, so a `$z` read is recorded exactly. Measured in release on the real data with 2,000 remembered ids, per forced change: - crit or dodge: 3.3 ms → 26-38 µs; - one worn slot: 3.5 ms → ~100 µs; - one modifier cell: 3.5 ms → ~60 µs; - all 64 modifier bits: 3.5 ms → ~0.6 ms; - reach: 3.3 ms → 1.3 ms, still high because the range cell also pads ranged rows by reach (#643); - a quiet frame: unchanged. The bench is `ui_tooltip/bench.rs`, `#[ignore]`d. Tests: - `deps_tests.rs`, the safety net, builds all 22,357 real views. For each input it moves the input alone and fails on any view that changed without having recorded it, naming the spell and the input. - It also moves everything each view did not record at once, from both sides. - It pokes every player-store field (and every pet-store field for the pet views) to discover reads mechanically. - It enumerates the equipment slot states the item search distinguishes. - `feed_tests.rs` drives `feed_spell_tooltips` itself. A dodge change re-pushes only the dodge chance-line view, and a modifier cell only its family's views. Fixes #535 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
8ef62da6ac |
spell bind: BindTarget's unit gates run ahead of the relation arms at the press, the click and the hover, through one creature-type resolver (#640)
The spell target binder went straight to the relation checks and tested none of `BindTarget`'s unit gates. So: - a heal with a dead friendly player selected was sent at the corpse and refused by the server; - Hibernate, Banish or Turn Undead at the wrong creature type was sent and refused, instead of refusing locally with "Invalid target"; - a self-excluding spell with yourself selected bound you. `BindTarget 0x6e5b40` refuses a unit silently, before any relation arm, when any of these holds: - the unit is the caster and the spell has `AttributesEx & 0x80000` (`6e5bd8`-`6e5bf7`); - the unit has `UNIT_FIELD_FLAGS & 0x10000` (`6e5c1d`); - the spell's `TargetCreatureType` mask excludes the unit's type (`6e5c26`-`6e5c53`). A zero mask admits every unit. Otherwise a unit of type 0 is refused and a typed one needs bit `type - 1`; - the unit is dead (signed `UNIT_FIELD_HEALTH` not above 0), unless `UNIT_DYNAMIC_FLAGS & 0x20`, a word in `0x8600` or `AttributesEx2 & 1` admits it (`6e5c5f`-`6e5c8b`); - the unit is alive and the word has `0x400` (`6e5ca2`). A refused selection falls to the player behind `autoSelfCast`, then to the enemy-word refusal or the cursor. `SpellCanTargetUnit 0x6e6460` runs the same five gates (`6e6507`-`6e65ab`). - `spell/bind_gates.rs` holds the five gates. `cast_target::unit_binds` is the one predicate for the selection, the `autoSelfCast` fallback, the cursor's click, `SpellTargetUnit` and the hover verdict. The cursor's separate `excludes_caster` checks fold into it. - The dead test now matches the reference, which catches a corpse whose create block sent no health. The `0x400` bit is now cleared by a dead unit's bind (`6e5d89`, `6e5ddb`), where before any non-self candidate cleared it. - `creature_type.rs` is the one `0x605570`, reading these in order: - the form's `SpellShapeshiftForm` creature type; - the creature template's type, by `OBJECT_FIELD_ENTRY`; - `ChrRaces`'s type. The minimap's tracking dots used their own copy, keyed by the guid's entry bits and missing the fall-through past the form. They now call the shared resolver. - `SpellDisplay` reads `TargetCreatureType` (column 14). Tests: - `bind_gates.rs` has a truth table per gate. Its `ladder` module drives `resolve_cast_target` through: - a dead heal target, with `autoSelfCast` on and off; - Hibernate at the wrong type; - a self-excluding spell; - the `0x10000` flag; - the `0x402` word. - `the_press_the_click_and_the_hover_bind_the_same_units` runs one table through all three paths. - `real_spell_catalog_reads_the_bind_gate_columns` pins the real rows for Hibernate, Banish, Turn Undead, Mind Control, Bloodlust and Revive. Fixes #449 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |