Commit Graph
407 Commits
Author SHA1 Message Date
gromandClaude Opus 5 a7a54c6d02 Dismount and CancelShapeshiftForm, because our own pfUI port calls both
`pfUI/modules/autoshift.lua:45` answers `ERR_CANT_INTERACT_SHAPESHIFTED` with

    if IsMounted() then Dismount() elseif GetShapeshiftFormID() ~= 0 then CancelShapeshiftForm() end

and grom's 2026-11-05 log carries `attempt to call global 'Dismount' (a nil value)` from
exactly there. That is a port written against verbs this client does not have, with a
comment claiming both "do their own engine-side aura scan and send the cancel packet
directly" — which was the design and not the state.

Both were on `compat/classic_api/state.rs`'s not-built list for a reason that turned out to
be answerable. Each needs the SPELL ID behind a state the engine reports as a flag
(`UNIT_FIELD_MOUNTDISPLAYID`) or an index (`SelfMovement::shapeshift_form`), because
`CMSG_CANCEL_AURA` carries an id and nothing else; and the player's aura list carries spell
ids and nothing about what each aura IS. The missing half was one derivation —
`EffectApplyAuraName` 78 (`SPELL_AURA_MOUNTED`) and 36 (`SPELL_AURA_MOD_SHAPESHIFT`), now
`SpellDisplay::mount_aura`/`shapeshift_aura` and two fields on `AuraState` beside
`channeled`, which the feed derives the same way from the same row it already reads.

Each verb is then the aura it names cancelled by id, which is what the reference's own
right-click on the buff does. Neither is gated on `aura::cancel_authorized`, unlike
`CancelPlayerBuff`: that gate is the reference's check inside THAT verb, this is not that
verb, the server makes its own decision about a cancel it is sent, and a mount whose aura
is not flagged `AFLAG_CANCELABLE` would otherwise be undismountable with no way to tell
why.

Allowlisted in `reference_surface` with the reason, as the four taint globals were: 1.12
has no verb for this because 1.12's player has the buff icon under the cursor, and an addon
reacting to an error message has neither an icon nor a cursor.

Verified: `dismount_and_cancel_form_each_cancel_their_own_aura_by_id` seats a plain buff, a
form and a mount in one list and asserts each verb finds its own id past the others, and
that neither fires with nothing to cancel. benilla-ui 2128/0, benilla-app at its recorded
baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-05 10:38:02 -07:00
gromandClaude Opus 5 65520a4a58 The map's darkening was a grid of grey boxes, and a 12 px string was emptying the glyph sheet
grom, after a night's play: "make the map look a lot nicer, it would be really nice if the
darkened areas were not just massive squares on the map, and should fit into the actual
edges of the terrian if that makes sense, also I shouldn't see this dark squares on the
fully zoomed out world map, also make it so the world map can't move out of the frame."

**The darkening.** OpenMapClassic washed a never-visited zone through its land mask, and
those masks are Voronoi-dilated by construction — "so that the forty of them tile each
continent with no gap" — so for any zone that is not on a coast the mask *is* the
rectangle. Measured at zoom 2.5 over the Eastern Kingdoms: a grid of flat grey boxes.
Nothing could have been softened into a fix; it was the wrong mask. It was also drawn at
full strength from every zoom, and at the whole-world view the zones' own art is not drawn
at all, so the only thing on the world map was the fog's rectangles.

A land mask for every map now comes from the client's own data. `bake_zone_land` unions a
map's `WorldMapOverlay` alpha — the pieces the game reveals as you explore, cut to the
shape of the ground — into one 256×256 mask; `overlay-land-index.py` writes the list the
addon reads, because Lua cannot test for a file and a *missing* mask is dropped by the
engine so the whole rectangle draws, which is the defect itself. 53 maps have overlays
(the 40 stock zones, Hyjal, Alterac Valley and 11 of the realm's own); 76 have none and
keep a wide feather. `clip-zone-masks-to-coast.py` then takes the open sea off the dilated
sheet masks — the same dilation was painting parchment out on the ocean — by closing the
continent's own land by 500 yards (above the 466-yard inradius of the widest enclosed gap,
so seams fill and the sea does not) and keeping a 300-yard coastal margin. Twelve
landlocked zones lose 0.0%; Darkshore loses 53%, all of it water.

`tga.rs` gains the writer that needed, `script::uimap` and `ui_world_map::build_catalog`
become public so an example can reach the uiMapID graph, and `WorldMapUiData` grows the
accessor so the VM push and an outside caller cannot build different lists.

**The glyph sheet.** His four-minute session reset it 27 times, each dropping every cached
glyph. The producer is Atlas-CFM's cursor coordinate readout: a 12 px FontString on
`WorldMapDetailFrame`, rewritten every frame, and the canvas zooms by scaling that frame —
140 device px a digit at a canvas scale of 6.9. Nothing about the addon is wrong, so the
fix is ours: `raster_ppem` rasterizes exactly up to 64 device px and snaps to a
third-octave ladder above it, with `cell()` scaling the cached bitmap and metrics left
keyed on the true ppem, so advances and `GetStringWidth` are bit-for-bit unchanged. Same
1,200-frame reproduction: 3 resets and 6,623 rasters before, **0 and 1,424** after, with
occupancy climbing once to 30% and stopping. A reset no longer clears the metric cache
either, and `note_loud_cell` names the string that asked for an outsized cell.

**Four more from the same log.** `SetOwner`'s unknown-anchor line named a forwarding
wrapper rather than the addon that chose the anchor (two `debugstack` frames now); the nine
reference anchor names are written down with the binary evidence, because `ANCHOR_TOP` is
not one of them. The `CreateFrame(kind, …, inherits=<other kind>)` warning fired in both
directions and only one drops content — `kind_subsumes` makes it directional, and
`type_chain` moves out of `install` so `IsObjectType` and the loader read one table. A
string that is not UTF-8 now names the Lua line that cut it, and both glyph warnings quote
the string the character came out of. The three world-map CVars stay unregistered, with the
argument written down beside `REGISTERED` so it is not re-litigated from the warning.

**`World.zmp`.** The realm's `WorldMapArea` carries a third `areaID == 0` row, `694 World`,
with an all-zero rect; the reference ships no bitmap for it and neither the graph nor the
realm's own `Overrides.lua` lets it reach a player. The miss is a `debug!` line now, and
the `warn!` is kept for a continent that *has* a rectangle and is missing its bitmap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-04 21:16:20 -07:00
gromandClaude Opus 5 74fd1a52a2 Four counts the 2026-11-04 client update moved
The realm rewrote `patch-4.mpq` and `patch-5.mpq` again (19:28 local). Everything
structural held — 152 toc rows with the same six unnamed, `Bindings.xml` still 235/229
live, DBC field counts unchanged everywhere, `WorldMapArea`/`Map`/`AreaTable`/
`WorldMapOverlay` byte-identical where our tests pin them, and the addon census identical
on every column. The content is nine mount spells (`65417-65425`, the Dragonhawks and the
Blood Ring pair) with their ten `CreatureDisplayInfo` rows, and every model and texture
variation they name resolves in the chain.

Four tests pinned counts that moved with them, and all four are examples rather than
rules, so each is re-recorded and the rule beside it left alone:

  * `Spell` 28046 → 28055 rows, and the `SpellFamilyName` histogram's family 0 22387 →
    22396 — all nine new spells carry family 0, so the eleven-family set, the three junk
    keys, the multi-bit count and `max_bit` 53 are all untouched.
  * `CreatureDisplayInfo` 13452 → 13462, and the blood-tier array 12516 → 12526. The
    companion assertion still describes the data: exactly one display (185, `modelId` 0)
    has no `CreatureModelData` row, so `tiers.sum() == len - 1` holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-04 21:15:49 -07:00
gromandClaude Opus 5 62c019e744 blp: a short colour map is a sliver tile, not a corrupt file
Seven of the client's own Interface\WorldMap textures never decoded. They
are the last column or row of an exploration overlay -- a few pixels
wide, a handful of colours -- so they store a SHORT BGRA palette and end:
RuinsOfThaurissan4.blp is 404 bytes in all, LakeEverstill6.blp is 276.
The parser sliced the nominal 256 entries and refused anything shorter.

A refused decode is a quad that draws nothing, and until this week a
texture that drew nothing also said nothing, so the symptom was a hole in
a zone's revealed art -- indistinguishable from not having explored it.
It surfaced only once the stitched world's geometry was right and the
render reached those tiles.

The palette is now as much of it as the file has, and an index past the
end is transparent black, which is what an unused index means.
every_world_map_texture_in_the_chain_decodes walks the folder: 3553
textures, 7 failures before, 0 after, and it fails on the old decoder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-03 16:54:06 -07:00
gromandClaude Opus 5 14d5dc36e9 formats: benilla-formats is green — the world-data goldens, and two tests that
were measuring nothing

The last 11. The realm reships `World\Maps\`, `HumanMale.m2` and
`TaxiPathNode.dbc`, so these tests had lost the *examples* they picked without
losing the properties they were for. Two had quietly stopped testing anything.

**A guard that was a proxy, not a fact.** The lava seam test skipped a vertex
whose UV was `[0.0, 0.0]`, standing in for "this vertex is dry". The realm's
lava leaves non-zero texcoords on undrawn verts, so 121 meaningless pairs were
entering the compare and one of them failed. Gating on the mesh's own `wet` map
instead leaves 277 pairs — the verts both neighbouring blocks actually draw —
and every one of them agrees. The invariant was never in doubt; the guard was.

**A keyless bone read as an empty band.** `empty_band_poses_the_bone` asserted
HumanMale's waist (bone 27) carries exactly one clamp key in Run. In the realm's
model bone 27 has no rotation or translation keys in *any* of its 156 sequences,
so there was nothing to clamp and the assertion was reading a keyless bone. The
subject is the five finger bones 100..=104 now: each appears in every one of the
156 sequences with exactly one clamp key at t = 0, which is the rule — an empty
band poses the bone and never drops it.

**The skeleton was renumbered, and the poses came with it.** 1.12's finger bone
102 is unposed at the HandsClosed frame; bone 47 carries the curl, within 0.004
of 1.12's `[0.2536, -0.016, 0.0577, 0.9654]`. Same for the eyelid: bone 75 ->
bone 24, still scale-only, still a 6633 ms loop that opens at t=0 and shuts
mid-loop. That the *pose* survived is what says renumbering rather than
re-animation, so the window arithmetic is still what is under test. (The model
does have HandsClosed throughout — `parse_m2_animations` not surfacing it is
exactly why the raw reader exists.) Ten new global-sequence bones came with it,
all parks: six 33 ms two-key constant scales and four rotation channels with
empty tables, recorded so a real loop appearing among them reads as a change.

**Tiles chosen instead of inherited.** `examples/liquid_scan.rs` walks a map and
reports which tiles still have each liquid property, so a re-recorded test names
a tile that was picked:
- the river mouth moved to `Azeroth_33_38`, three Still-over-Ocean chunks, all
  disjoint — 1.12's 33_33 has no two-block chunk left. Four tiles *do* overlap
  and are kept out deliberately: Kalimdor 26_54, 41_29, 41_30 and the realm's
  own Azeroth 36_51, where 1.12 had only 26_54.
- the ocean depth divisor moved to `Azeroth_29_54`: 13210 depth bytes inside the
  ramp and not one pinned at 255, so a wrong divisor cannot hide behind
  saturation. 1.12's 41_41 carries no ocean at all now.

**The impassable band was thinned.** `Azeroth_33_44` authors 17 impassable
chunks where 1.12's copy had 48, and the chunk 1.46 yd east of the reported
`.go xyz -6601.98 -531.87 335.60` is among the ones it dropped — so the data no
longer walls the reported spot. The `Some(true)` leg would have stopped being
tested, so it moved to a column inside one of the 17 the realm does flag.

**The transport goldens no longer describe this client.** `TaxiPathNode.dbc` is
re-authored: 11219 rows against 9582, and 7975 rows kept their id while changing
path, index *and* coordinates. Path 436 is gone; the eight survivors miss
vmangos's 1.12 periods by −290815 to +271184 ms, and no transport path crosses
continents any longer. Closing that needs the realm's own transport periods,
which only live observation gives — so the calibration is a report now, with the
deltas printed on the same variant grid a future observation can be compared on.
What is still assertable is asserted: `client_period_ms` and the timetable's own
`period_ms` are one number on all eight. The two map-invariant tests never
needed a server period at all, so they run on taxi path 72 — 37 nodes over maps
{0, 1}, the only shipped path that still spans both — and additionally assert
that every transport golden is single-map, so none of them can silently stand in
for it again.

**benilla-formats: 445 passed, 0 failed.** `cargo fmt --all` and
`clippy -p benilla-formats --all-targets` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:58:18 -06:00
gromandClaude Opus 5 c0685f0273 formats: the realm's tenth race, and what it switched on
Four goldens, and three of them turn out to be the same fact: `ChrRaces.dbc`
has a tenth row — id 10, **High Elf**, on the Blood Elf art (`ClientFileString`
"BloodElf"), Alliance-side.

- **Every talent tab opened to it.** All 27 `TalentTab` rows went raceMask
  511 -> 1023, so race 10 takes every class's tree rather than being gated out
  of any; the test asserted the opposite, that "race bit 10 is outside 511".
  `Talent.dbc` itself was rebuilt too — 476 rows, 317 ids added, 273 removed,
  159 changed — so the trees are the realm's, not 1.12's.

- **Dwarf Mages are real here, and the gate was already written for it.**
  `chain_is_stock` calls a chain stock when no playable race exceeds 8, so this
  install read as stock until race 10 arrived and the `UNUSED_COMBOS`
  Dwarf-Mage skip kept applying; the test's "patched chain or not" passed by
  accident. The skip only ever modelled a compare in the *stock* executable at
  `0x4706b0`, and this install's own `WoW.exe` has none — the whole builder
  holds no immediate compare against 3 or 8 and gates on a `0xFE` sentinel
  (`cmpb $0xfe,0x1(%ecx)` at `0x4706e8`). Re-verified against the binary rather
  than trusted from the comment. `CharBaseInfo` grew 103 -> 139 bytes, which at
  2 bytes a row is 18 new race/class pairs.

- **`CharHairGeosets` was re-authored, and took a test's only coverage with
  it.** 299 rows against 148, 136 of the survivors rewritten, and 1.12's one
  duplicated key — the goblin male's variation 0, listed four times as geosets
  1, 2, 1, 2 — is gone with none replacing it. That key was the *only* thing
  exercising the loader's first-row-wins rule, so re-recording the count alone
  would have quietly dropped it: `or_insert` could have become `insert` and
  nothing would have noticed. The hair decode is now a `hair_geosets_from_bytes`
  seam and a synthetic DBC replays 1.12's goblin block to pin the rule, plus
  the reverse order to prove the later row is discarded and not merged. The
  real-data test asserts the absence instead. Race 9 is where the re-authoring
  shows: the goblin has a full hair set for both sexes now, 14 male variations
  and 17 female, and the male's variation 0 is `GeosetID` 0 where 1.12's
  winning row said 1 — both land on scalp geoset 1, since `visible_geosets`
  takes `max(1, GeosetID)`.

Also:

- **WorldStateUI** 20 -> 34 rows, and the realm rewrote the text and tooltip on
  every one of 1.12's 20. Only 33 ids are distinct — **168 ships twice**, both
  map 0 / area 44, one reading "Banners Controlled: %3809w" and one "Tower
  Score : %3811w / %3813w". Safe, and checked rather than assumed: the catalog
  exposes no id lookup at all, only `rows()`, and the app filters by (map,
  area), so both readouts reach the frame — which is the only thing two rows
  under one id can have been meant to do. Four zones narrow by area where 1.12
  had two, the new three being outdoor PvP: Redridge Mountains, Hillsbrad
  Foothills and Silithus. Live `StateVariable`s run to 3810 where 1.12's three
  all sat under 3000.

- **WMOAreaTable** 21115 -> 26653 rows and 21105 -> 26570 distinct
  (wmo, nameset, group) keys, so the repeats went 10 -> 83; 5633 ids added, 95
  removed, 4080 changed.

benilla-formats: 16 -> 11 failures (and one test more than before). `cargo fmt`
and `clippy -p benilla-formats --all-targets` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:44:37 -06:00
gromandClaude Opus 5 d2414d9f50 stable: the realm sells four pet slots, and our window drew two
Found by the `StableSlotPrices.dbc` golden: the table has four rows where 1.12
had two, and `NUM_STABLE_SLOTS` was 3 (the current pet plus two). Three
independent sources in the install agree on four, so this is not a guess:

- `StableSlotPrices.dbc` ships 4 rows — 500c, 5g, 250g, and a fourth at one
  copper — against 1.12's two.
- the realm's own `PetStable.lua` (patch-4) declares `NUM_PET_STABLE_SLOTS = 4`.
- the realm's `WoW.exe` caps the purchase at four, and `0x4cb0bf` reads straight
  across: `cmp $0x4,%eax; je refuse` on the bought count, then `inc %eax` and
  `cmp 0xc0d728,%eax; jg refuse` against the price table's max id — which is
  exactly `num_stable_slots != NUM_STABLE_SLOTS - 1 && next_slot_cost != 0`.

So the window drew two slots the server would happily sell, and
`GetStablePetInfo(3)`/`(4)` answered nothing: the last two buttons were dead.
`NUM_STABLE_SLOTS` is 5 now, the gate's comment cites the realm's binary rather
than 1.12's 2, and the test fixture helper takes however many leading slots a
case cares about so the existing three-slot fixtures still read.

Also five goldens re-recorded, two of them findings:

- **The spam filter is one line now.** patch-4 ships a 93-byte
  `SpamMessages.dbc` of one row where `patch.MPQ`'s 2161 bytes held 28, and the
  chain takes the whole winning file rather than merging ids — so Blizzard's 28
  gold-seller patterns are gone and what is left matches a rival server's
  address, whitespace-tolerant between every letter.

- **`md5translate.trs` stopped hashing.** 9794 rows against 8401, and an Azeroth
  tile now resolves to the readable path `Azeroth\map32_48.blp` rather than
  `ea283abc0bf9637c3fad5e840a65b38b.blp`. The realm ships the tile at that path
  in patch-4; 1.12's hashed copy is still in `texture.MPQ` at the same 33940
  bytes, so only the table moved and the lookup still has one job.

- ItemSet 172 -> 363 rows, and Defias Leather (161) gained a sixth item, 81007,
  one of the realm's own. Every one of 1.12's 172 sets also gained a name in
  five more locales — the same five-locale fill the realm gave `EmotesText`.
- NPCSounds 156 -> 247, purely additive; the one pre-existing row patch-4
  touched is 90, which gained two kits.

benilla-formats: 21 -> 16 failures. `cargo fmt` and
`clippy -p benilla-formats -p benilla-ui -p benilla-app --all-targets` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:36:30 -06:00
gromandClaude Opus 5 e61976bc0e formats: the world-map goldens, and a nondeterministic continent lookup
The four map tables against the realm's own files. One real bug, and three
findings that are the realm's data rather than ours.

**`WorldMapArea::continent` was a coin flip.** It `find`s the `area_id == 0`
row for a map id, and 5875 had exactly one per map so order never mattered. The
realm ships two for map 0 — 14 "Azeroth" and its own 694 "World" — and the
lookup iterated the `by_id` `HashMap`, whose order is seeded per process. So
`continent(0)` answered Azeroth or World depending on the run. Now it walks
file order, which is the reference's own walk and picks 1.12's row; the test
asserts 14 and passes eight runs in a row where it used to flip.

- **WorldMapArea** 51 -> 132 rows loaded, of 175 in the file. The 43 dropped are
  dropped on purpose: `AreaName` empty, `AreaID` 0, the whole rect zero, one per
  instance map (33 Shadowfang through 533 Naxxramas, then the realm's 800s). An
  instance has no continent art — but `AreaID == 0` is exactly what
  `ui_world_map.rs:146` reads as "this row is a continent", so keeping them
  would put 43 unnamed continents in `GetMapContinents()`. `str_at` folds an
  empty string to `None` and the row falls out; the test now pins that as
  deliberate. The realm's third *named* continent is 694 "World", a second sheet
  over map 0 whose art ships (patch-M) but whose four rect columns are all zero
  — the only named row like that. `map_proj::zone_uv` already refuses a zero
  span and answers 0.0 instead of dividing, so that sheet puts a blip in the
  corner rather than NaN.

- **WorldMapContinent** 2 -> 3 rows. The new row's `MapID` is 42, which the same
  patch *deleted* from `Map.dbc` along with the rest of 1.12's dev test maps
  (13 "Testing", 25 "Scott Test", 29 "CashTest", 37 "Azshara Crater", 42
  "Collin's Test", 44 "<unused> Monastery"). It is inert rather than broken: the
  continent list comes from `WorldMapArea`, and this table is only ever reached
  by `wmc.get(map_id)` for maps 0 and 1, so nothing asks for continent 42.

- **The zone bitmaps are repainted.** `Azeroth.zmp` and `Kalimdor.zmp` have the
  realm's own copies in patch-3 and patch-4, and every id they write is a
  **zone** where 1.12 wrote sub-areas and left the client to roll them up.
  Azeroth: 1874 labelled cells over 34 distinct ids, all zone-level, against
  2630 over 334 of which 310 were sub-areas (Northshire Valley, Crystal Lake,
  Fargodeep Mine). After the one-hop rollup `ui_world_map.rs:193` applies, 1280
  cells agree, 534 name a *different* zone, 816 lost a zone and 60 gained one —
  a redrawn map, not a mechanical flattening. Nothing of ours needed changing:
  the rollup is a no-op on an already-flat grid. The test asserts the structural
  claim behind the number (every labelled cell is a top-level zone), which 1.12's
  own file would fail, so a decode slip cannot hide inside a re-recorded count.

- **WorldMapOverlay** 526 -> 707, and not an extension: 360 ids added, 179
  removed, 347 survivors given a different texture and rect. None of this
  table's numbering is 1.12's, so only the joins are worth asserting.

`dbc_diff` gained `--row=N`, which dumps a row from the winning copy *and* the
baseline — the only way to read a row the realm deleted, which is how the six
test maps above got their names.

benilla-formats: 25 -> 21 failures, no regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:20:54 -06:00
gromandClaude Opus 5 e58ddc10ab formats: re-record the sound and visual goldens, and a tool to say what moved
Six modules' data goldens against the realm's own files, plus the thing that
made the last three honest rather than guessed: `examples/dbc_diff.rs`.

The tool reads the copy of a DBC the chain *wins* and the copy it shadows and
diffs them row by row. `Chain::copies` is ascending priority and several
`patch-*` archives are mounted, so 1.12's own file is usually still underneath
the realm's; the baseline is the highest copy in a stock archive (`dbc.MPQ`,
`patch.MPQ`, `patch-2`) so the diff reads "what OctoWoW changed about 1.12".
Schema-free — field 0 is the id, a row is its raw 4-byte slots — so an
undecoded column still shows up as "id N differs at slot K". `--row=N` dumps a
row with its text slots as text, which is how three ids below got names.

Two findings, both the shape of a reference into a table that has no such row:

- **The glow band.** The realm rewrote `ItemDisplayInfo.dbc` (patch-5) and left
  `ItemVisuals.dbc` as 1.12's out of base `patch.MPQ` — 34 rows, ids `1..=134`.
  22 ids its new displays name run past that end, where a later expansion's
  table continues, so 61 items wear no glow. The reference drops them exactly
  as we do (`0x479700` skips a visual id past the max id), so this is the
  realm's data, not our decode. Pinned by id with the displays behind each.

- **The silent kits.** `SoundEntries.dbc` grew 4623 -> 8805 rows and 4764 of
  its 16537 variation cells name a file the chain has not got: 236 directories
  hold no file at all (LesserManaFiend, ChineseDragon, ZoneMusic\BloodElf), and
  the ones that exist miss the TBC boss voices — Illidan, Akama, Lady Vashj,
  Kael'thas — plus 552 of `Sound\Doodad`. What makes that an observation about
  the data and not about our path join is *where* the misses fall: of the 2834
  kits with a dead variation, 2832 have an id past 4623, the last row 5875
  shipped. A faulty separator would miss evenly across old rows and new. Only
  395 OgreFidget and 3792 WaterFallLarge predate the realm, and only 3792 is
  wholly silent, so nothing that used to make a sound has gone quiet.

The rest are censuses re-recorded with their 5875 figures beside them. Every
one is safe to re-record for a mechanical reason: `benilla-dbc` fails the load
outright when a schema's expanded field count differs from the header's
(`lib.rs:215`), so a test that *reaches* a count assertion has already proved
its layout unchanged.

- SoundEntries 4623 -> 8805 rows, EAXDef census (2072,2,2549) -> (6241,4,2560),
  type-17 NPC voices 275 -> 529 (still every one authored dry), variation cells
  8961 -> 16537. Kit 8940's ten ASH_SPEAK misses stay, and now assert *why*:
  the asset ships and it is the authored leading `\` that cannot reach it.
- VocalUISounds 533 -> 688 rows, live normal kits 785 -> 1085.
- ItemSounds nonzero field-11 20513 -> 32728.
- CreatureSoundData 406 -> 525 rows. A fifth row carries pet barks, and
  `dbc_diff` settles what it is: row 348 is 1.12's own and columns 27-29 are
  the only thing patch-4 changed about it, filled with the Imp's own kits — so
  an imp barks whichever of its three models is in play, `Creature\Imp\Imp.mdx`
  and TBC's `Impoutland` and `Moargminion`. Row 348 says nothing else at all.
- SpellVisual 2165 -> 2313, SpellVisualKit 1772 -> 1969, SpellChainEffects
  18 -> 21, chain CharProc census (68,48,20) -> (110,51,59). The three new
  beams are HarvestBeam, SpiritLinkBeam and Beam_ChainIron_NoAlpha, and unlike
  the sound rows the art ships with them; 23 zeroes every parameter the others
  animate. All three keep 1.12's segment length.
- ItemVisuals: enchants carrying a glow 102 -> 109, and 108 of 109 resolve —
  still exactly the one -1, so every new glowing enchant names a row that
  exists. Only the display side dangles.

benilla-formats: 38 -> 25 failures, no regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:10:27 -06:00
gromandClaude Opus 5 ca60826bec The creature tables, and three things in them worth more than a count
`CreatureDisplayInfo` is 13452 rows where 5875 had 10534 and `CreatureModelData` ships different
art under the same paths, so every census over them moved. Field counts are unchanged (12 and 16),
so the layouts are intact.

Three are findings, not bookkeeping:

- **One display row dangles.** Display 185's `modelId` is 0 — it points at no `CreatureModelData`
  row at all, which 1.12 had none of. The blood-tier walk skips it because there is no model to
  read a column from, and our client falls back the same way: no model, nothing to draw. Pinned by
  id so a *second* dangling row gets looked at rather than absorbed.
- **Five races' collision boxes no longer match their columns.** The realm ships its own character
  art under unchanged paths, and the boxes moved while the DBC columns stayed: OrcMale and
  TrollMale carry a 2.031 box (HumanMale's — a reused rig) under columns of 2.361 and 2.083,
  DwarfMale 1.607 under 1.667, TrollFemale 2.427 under 1.839, GnomeFemale 1.056 under exactly 1.0.
  The column stays authoritative, which is the part that matters: the reference reads it and not
  the model (`0x60b270` → `0x6174b0`), so the depth lines follow the column and an orc's swim line
  sits a little above where its art suggests. The exception list asserts they still *dis*agree, so
  a race coming back into line gets the entry deleted instead of a stale carve-out.
- **The prism floor is exercised now, not inert.** Five `CreatureModelData` rows scale below 1
  (2602, 2719-2722 at 0.4 down to 0.2) where 1.12 had none, which is exactly where
  `max(SCALE_X, display scale)` bites. The arithmetic it exercises already has its own test in
  `entities::collision_height`, so this one names the five rows and points at it; the test is
  renamed for what it now says.

Also: HumanMale's footprint is 10 inches long and 14 wide, where 1.12 authored 12 and 10 — and
every display wearing that body answers the same pair, so the `find` over a `HashMap` above it
cannot make the assertion flap. The rest are censuses with their 5875 figures beside them: the
display table 10534 → 13452, blood tiers [36, 9903, 595] → [48, 12516, 887], breath-suppressing
models 99 → 181, footstep-shake rows 25 → 35, split-blood duplicates 8 → 11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:52:40 -06:00
gromandClaude Opus 5 e9fe05dab3 The spell table's goldens, and four more realm changes in them
`Spell.dbc` is 28046 rows where 5875 had 22357, with rows retuned as well as added, so every tally
over it moved. Field count is still 173, so the layout is intact and these are data.

Four are gameplay, not bookkeeping:

- **Bloodrage is free.** 1.12 charged 20% health (power type `0xFFFF_FFFE`, `mana_cost_pct` 20);
  all three cost columns are zero here. Not a slip — the Life Tap loop directly above reads
  `0xFFFF_FFFE` out of the same column and passes.
- **Health Funnel costs 3 up front and 6 per second**, where 1.12 took 11 and 5.
- **Auto Shot works with thrown weapons.** Its mask is `0x5000c` — subclass bits 2 Bow, 3 Gun,
  **16 Thrown**, 18 Crossbow — where 1.12's `0x4000c` had the three ranged ones only.
- **Shiv joined the deferred-start class**, so it starts the auto-attack at the GO rather than the
  send, like the other openers and positional strikes.

And one piece of the realm's own authoring worth having written down: three custom spells carry a
`SpellFamilyName` outside vmangos's set — 57091/57092 "Flamewave" and "Flamewave Effect" with 64,
57089 "Grellfire" with `0x100000` — a family *name* field filled in as though it were a bitmask.
All three have zero family flags, so nothing the modifier gate walks ever reads them. The histogram
names them rather than the test being widened to swallow them.

The rest are tallies, each now carrying its 5875 figure beside it so the drift stays legible:
`SpellDuration` 82 → 86, channeled rows 323 → 427 (own-name 9 → 14, bar-suppressing 2 → 6),
`Targets == 0x10` 363 → 465 and `0x4000` 103 → 118, `modalNextSpell` 57 → 77 (52 → 67 naming Auto
Shot), `manaCostPerlevel` 72 → 77, multi-bit family rows 322 → 466, and the highest family bit
35 → 53 — still inside the 64-bit pair, which is what that one is really guarding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:43:17 -06:00
gromandClaude Opus 5 2d745760ac Nine more goldens, and four of them are realm features worth knowing
Second golden batch. Field counts were re-checked first and are still intact everywhere, so again
these are the realm's data and not a misread column.

Four are things grom would want to know about his own realm, not bookkeeping:

- **The professions need no tools.** Mining (2575) carried the Mining Pick and Skinning (8613) the
  Skinning Knife in 1.12; both are `[0, 0]` here, as are Herbalism, Blacksmithing, Cooking and
  First Aid. You gather bare-handed, and the pre-send possession check (`0x6e4000`) has nothing to
  refuse. Read rather than assumed: 362 rows still carry a tool — Pick Lock still wants its
  thieves' tools (5060) — so it is six rows blanked in the data, and the reagent columns beside
  them are untouched, which Slow Fall's Light Feather shows.
- **There is a shipped World channel**, row 27, flags `0x9` — global and auto-joined, with no `%s`
  in its pattern because there is one of it. 1.12 had no row at all; there "World" was a
  player-made channel like any other, which is exactly what the old test asserted.
- **Thalassian is implemented.** Spell 813 was "Thalassian (NYI)" declaring Common in 1.12 and
  declares language 10 here, so nine player languages have both a declaring spell and a skill line
  where 1.12 had eight. It goes with a **tenth `ChrRaces` row**, which speaks Common and is
  therefore Alliance-side.
- **Emote text ships five locales**, not four: ruRU arrived in column 5 — the column this very test
  used as its "unshipped" case. The dry-ladder case moved to 1/2/8 and the five that compose are
  asserted, so a column sliding under the ladder cannot pass unnoticed.

The rest are counts and rows: `LockType` has a fourth cursor row (id 20, "Survival", reusing
`PickLock`), `ItemBagFamily` has twelve rows with the realm's four profession bags (Meat, Fish,
Leather, Mining Supplies), `EmotesText` 169 → 171, and a fourth spell carries
`SPELL_ATTR_EX3_NO_CASTING_BAR_TEXT`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:37:57 -06:00
gromandClaude Opus 5 fbf01c486e Eleven data goldens, and the gesture scan's order now matters
First batch of the benilla-formats goldens, all of them the realm's tables being bigger or retuned
rather than anything misread. The safety check came first and is worth recording: **every
`*_FIELDS` constant in the crate still matches its live DBC header** — Spell 173, AreaTable 25,
CreatureFamily 18, Talent 21, Lock 33 and the rest — so no decoder is reading a moved layout, which
is the only way one of these counts could have been a defect rather than data. Added
`examples/dbc_shapes` to read that off the chain in one go.

Counts: `AnimationData` 208 → 227, `AreaPOI` 339 → 483, `AreaTrigger` 432 → 530 (172/144 across the
two continents, from 133/121), `AreaTable` 1081 → 1481, `GameTips` 74 → 95, cinematics 10 → 13
sequences and cameras both.

Three that are more than a count:

- **Rested experience is ×1.5 here, not ×2.** `Exhaustion.dbc` row 1 is where the client reads the
  multiplier it shows, and the realm retuned it. Every vanilla guide quotes 2.0.
- **The help window offers two ticket categories, not ten** — "report someone's behavior" and "an
  issue with my character" replace 1.12's Stuck/Guild/Item/… list.
- **Warlock minion pet icons are blank.** `CreatureFamily` rows 15-19, 23, 28, 29 carry no icon
  where 1.12 gave each one a path, so `GetPetIcon()` answers nil for a minion and an addon that
  concatenates it unguarded gets nothing. Not a column slip: every hunter family in the same column
  answers a real path. The realm also backported the TBC families (Dragonhawk, Ravager, Warp
  Stalker, Sporebat, Nether Ray, Serpent), added four of its own (Fox, Plagued, Horse, Moth) and
  filled two minion ids, Incubus 18 and Felguard 29 — 35 families where 5875 had 23.

And `Emotes.dbc`'s gesture scan, which was pinned on a premise that has stopped holding: 1.12 had
one row per gesture bit, so the walk order could not matter. The realm's file has three rows
carrying `0x8` and a row (424) carrying `0x8|0x10|0x20`, so it matters now — and **our scan is
already right**, which the disassembly settles rather than luck. `0x603a60` walks the table
backwards (`imul $0x1c` to the end, `sub $0x1c` per step) and overwrites each slot, so the earliest
record survives; and its bit test is an if/else-if chain, so one row fills at most the slot of its
first set bit. Ours walks forward keeping the first, with `position` for the chain — the same table.
The test now asserts that rule, re-derived from the file, instead of counting carriers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:30:54 -06:00
gromandClaude Opus 5 266af6eb26 The realm's flight network is bigger than 1.12's, and a second capture point
All of today's update, measured rather than assumed. `TaxiNodes.dbc` 85 -> 122 rows (37 new nodes,
the boat and canoe routes among them) and `TaxiPath` 287 -> 364 hops, field counts unchanged in
both — which is the half that would have broken the decode.

What moved with them:

- **Stormwind's node is "Stormwind, Elwynn Forest"**, where stock clipped it to "Elwynn".
- **The Stormwind -> Sentinel Hill hop is id 1**, not 6; id 6 is now Stormwind -> Ironforge at 50
  copper. The 110-copper fare did not move.
- **Path 302 is 23 nodes and stays on Eastern Kingdoms**, where 1.12's was 36 and crossed. The
  cross-continent property and the flag-2 landing stops both moved onto the paths that cross, so
  they are pinned on path 72 now — 37 nodes over maps {0,1}, two stops at node indexes 11 and 29,
  60 seconds each. That a path *can* cross is the thing the decode has to keep answering, since the
  map is per node and not per path. 302 and 285 are asserted as the straight runs they now are.
- **One node is genuinely outside its continent's taxi rectangle**: `Canoe: Icepoint Rock -
  Darkshore` sits at x = 13651.5 where Kalimdor's `WorldMapContinent.dbc` box ends at 12470, so it
  projects 4.85% below the map. `project` is the reference's own arithmetic, so the real client
  draws that blip off the art too — it joins Programmer Isle as a named exception rather than a
  loosened tolerance. The other 121 rows are inside.

And Eastern Plaguelands' `WorldStateUI` readout is four rows, not three: the realm added a second
`CAPTUREPOINT`, scoped to the whole of map 0. Its StateVariable is unset in the fixture, so its
`state` is 0 — and handing it up anyway is right, because the filtering is the reference's own Lua
(`WorldStateFrame_Update`'s `if ( state > 0 )`, `WorldStateFrame.lua:41`). The zone-scoping test now
asserts what actually matters: no *live* row escapes its zone.

All seven gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 17:31:44 -06:00
gromandClaude Opus 5 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
2026-10-29 09:06:59 -06:00
gromandClaude Opus 5 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
2026-10-28 21:15:41 -06:00
gromandClaude Opus 5 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
2026-10-27 09:35:02 -06:00
gromandClaude Opus 5 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
2026-10-26 07:28:31 -06:00
gromandClaude Opus 5 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
2026-10-26 07:21:36 -06:00
gromandClaude Opus 5 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
2026-10-25 18:33:44 -06:00
gromandClaude Opus 5 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
2026-10-25 18:00:13 -06:00
gromandClaude Opus 5 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
2026-10-24 10:21:31 -06:00
gromandClaude Opus 5 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
2026-10-24 09:28:05 -06:00
gromandClaude Opus 5 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
2026-10-23 17:44:21 -06:00
gromandClaude Opus 5 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
2026-10-23 16:52:40 -06:00
gromandClaude Opus 5 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
2026-10-23 16:27:03 -06:00
gromandClaude Opus 5 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
2026-10-18 12:35:33 -06:00
gromandClaude Opus 5 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
2026-10-18 09:02:19 -06:00
gromandClaude Opus 5 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` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.

Today's work, which is the reason a baseline exists at all:

- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
  characters were entering the sun's shadow cascades, so every skinned-rig
  shadow specialization compiled live mid-frame; the hitch detector logged
  "main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
  the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
  tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
  an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
  and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
  measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
  frame-cost reading no longer pays 2.7 ms a frame for it.

Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 15:01:14 -06:00
chairmanpow 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.
2026-10-12 17:24:31 +01:00
chairmanpow 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 (884dff77 == 4e0e89cb
tree), so the merge base was replaced to keep a real three-way diff. The
large conflicts are upstream moving first: the UI manifest (benilla.toc ->
the binary LAYER_MANIFEST/layer.toc split), the CVar table (cvars.rs ->
cvars/{mod,table}.rs, +24 video rows), the editbox flush model, the
host-side GetTime, the Options* -> BenillaOptions* rename, expanded
ui_unit.rs, and a rebuilt volumetric_light.rs.

Local-unique pieces re-instated on the upstream structure:
- OptionsFrame.xml: the 1.12 OptionsFrameSliders table restored under the
  Benilla rename, with the TurtleWoW deviation note (patch-4 replaces
  OptionsFrame.lua).
- layer.toc: CharacterFrameAdapters.xml (the sixth TurtleWoW character tab).
- ui script: wipe/sizeof, BenillaGetMerchantItemStats, and the attach/mount
  teardown re-walk all survive.
- game_plugins.rs keeps UPDATE_ACTIONABLE_CEILING=5_509 (re-measure pending).

The LFT/Turtle_TransmogUI rows benilla.toc named off the player's install
now come from the chain's own FrameXML.toc via the new core loader.
2026-10-12 17:20:45 +01:00
Sam SungandClaude Sonnet 5.5 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>
2026-10-12 15:59:54 +07:00
pkuzic 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.
2026-10-09 12:50:37 +01:00
Sam SungandClaude Opus 5.5 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>
2026-10-08 08:53:16 +07:00
Can H. Tartanoglu 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.
2026-10-05 00:31:21 +02:00
HEOJUNFOandClaude Opus 5.5 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>
2026-10-04 15:48:21 +01:00
Sam SungandClaude Opus 5.5 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>
2026-10-04 15:48:21 +01:00
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>
2026-10-04 08:29:04 +07:00
pkuzicandGPT-6 Astra (Codex) 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)
2026-10-04 01:16:09 +01:00
chairmanpow 8c9b1d9984 TurtleWoW compat: the create screen loads races 9/10, so the character and Blood Elf stage show
The char-create catalog compared `CharBaseInfo` against the stock 1.12 rows exactly and bailed
on any difference. TurtleWoW adds class combos (Human Hunter, Orc Mage, Dwarf Warlock, ...), so
the whole catalog failed to load: `body_display` was unavailable and the glue booth assembled no
body on both the create and select screens. The creatable set is now read from `CharBaseInfo`
(the stock eight, or a patched chain's more), the stock rows are a subset guard against a
misparse, and race 9 (Goblin) and 10 (BloodElf) load their display, dial ranges and outfit like
the shipped eight.

The chain's `ChrRaces` row 10 is `BloodElf`, not the "High Elf" the code named it, and
`scene_token` mapped 9/10 to Human, so a Blood Elf stood on the Human stage. Both now resolve
(`UI_Goblin`, `UI_BloodElf`) and the display name is "Blood Elf"; the fileString is what the
race stage keys on.

The create screen hard-coded the eight vanilla races. It now splits the catalog's creatable set
into the Alliance and Horde columns, so a TurtleWoW chain shows five rows per column (Blood Elf
appended to the Alliance, Goblin to the Horde), with the tower and lower clusters shifted to
match its XML. Race-icon UVs are computed from the loaded sheet's size, so the stock four-column
sheet and TurtleWoW's widened eight-column sheet both read correctly.
2026-10-03 15:51:27 +01:00
chairmanpow 67b7ef8994 spell: a shot with a real cast time opens the cast bar
The bar was suppressed for every `Attributes & 0x2` spell, the reference's
`0x6e7700` gate read literally. That gate is why Aimed Shot and Multi-Shot show
no bar in 1.12.1 — a later client added `Force Display Castbar` to fix exactly
this, so the reference behaviour is the bug.

Gate on the DBC row instead: a spell opens a bar on the wire cast time whenever
`SpellCastTimes.dbc` has a nonzero one for it. Aimed Shot's real 3 s cast does,
so it bars. The instant shots do not — they point at row 1, the instant
sentinel — so Throw, Multi-Shot, Auto Shot and wand Shoot stay bar-free, and
vmangos's 500 ms pad on a non-auto-repeat ranged cast (`SpellEntry::GetCastTime`)
cannot conjure one either. An unknown spell, or a build with no cast-time table,
keeps the reference's bar rather than losing every bar.

`SpellCastTimeCatalog::from_rows` is the constructor the DBC loader and the test
share.

Verified: `cargo test -p benilla-app -p benilla-formats` — 3833 pass, the new
`a_timed_ranged_cast_opens_the_shot_bar_but_an_instant_shot_does_not` among
them. The one failure, `assets_ui_does_not_grow`, is pre-existing on main.
2026-10-03 14:08:43 +01:00
chairmanpow 503cde608a build: green fmt and clippy on the pinned 1.98.1
The gate was already red on main, in files no round since had touched.

`cargo fmt` reflowed one wrapped assert in the benilla-ui loader tests. Clippy's
1.98 set added four lints the tree does not satisfy: `doc_lazy_continuation`
on room_claim.rs's `2b.` item (indented, as it already renders), and in
benilla-extract `chunks_exact_to_as_chunks` on the MOGI walk,
`manual_checked_ops` on the census mean, and `needless_borrow` on the class
table call. Each takes the fix clippy names.

33 errors remain in benilla-world's lighting, static_gx, water_fx and liquid
trees, from the same lint bump; they are left for their own round rather than
folded in here.

Verified: cargo fmt --all --check clean; `cargo clippy --workspace
--all-targets -- -D warnings` now fails only in benilla-world.
2026-10-03 14:08:26 +01:00
pkuzicandClaude Opus 5.5 494d232c11 Merge upstream benilla main (b396bbf6) into everwood-graphics
105 upstream commits (v0.2.0 ba7fe6e2 -> b396bbf6) onto the graphics programme.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:35:23 +01:00
HEOJUNFOandClaude Opus 5.5 6c25640a37 Light bands with out-of-order keys interpolate as a cycle, as in 1.12.1 (#768)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:14:41 +07:00
Sam SungandClaude Opus 5.5 56d8011ab7 NPCs pause their work loop while you talk to them, as in 1.12.1 (#772)
Two ways the emote-state base pick differed from 1.12.1.

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

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 10:32:20 +07:00
Sam SungandClaude Opus 5.5 2dc7d8a9e6 /wave <name> targets the named player; two DoEmote gates follow 1.12.1 (#769)
Three ways `DoEmote` and the chat talk gesture differed from 1.12.1:

- `/wave Bob` ignored the name and targeted the selection. 1.12.1 resolves a name that does not start with `%` as a player, at any range (`0x49fdb1`-`0x49fdf0`, `0x493aa0`), and sends the emote untargeted on a miss. `DoEmote(token, name)` from Lua does the same.
- A polymorphed, stunned or sleeping speaker still played the talk gesture. `UNIT_FIELD_FLAGS 0x20000000` suppresses it when the sender's name is cached (`0x49d7f7`).
- `DoEmote` sent while our unit was possessed (`0x5ef57e`). A chat-only emote now reads `Emotes.dbc` row 0's flags, so it is refused while asleep or dead (`0x5ef5b1`, `0x47db8e`).

Tests cover the name, miss, `%t` and self cases, the gesture gate, and both `DoEmote` gates against the install's row 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 09:23:08 +07:00
Sam SungandClaude Opus 5.5 4f36c843f1 Emote voices play only for you and your party, as in 1.12.1 (#766)
Every text emote's voice played for every performer, so every `/cheer` in a city was heard. 1.12.1's composer tail (`0x623c80`) plays it only for you or a member of the four party slots (`0x623cc2`-`0x623cce`, `0x5efea0`), and drops an ignored sender before that (`0x49dc36`).

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

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 08:32:29 +07:00
chairmanpow 031acdd8bb Fix TurtleWoW baked NPC atlases (append .blp) plus in-progress GFX work: moonlight, torch depth, water/scene-depth, skybox 2026-10-01 21:40:02 +01:00
Sam SungandClaude Opus 5.5 4772489aad Arriving bodies composite off the main thread and show once dressed (#732)
A crowd of arriving players was composited in one frame, on the main thread, under the archive lock: 40 geared looks took 76–115 ms.

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

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

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

Fixes #609

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

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

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

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

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

Fixes #721

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

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