Commit Graph

854 Commits

Author SHA1 Message Date
Sam Sung b3fbaa86d4 World clicks, attack starts and /target follow 1.12.1's gates (#2117)
Clicks and attack starts passed gates 1.12.1 refuses at:

- A right press with a cursor payload went on to the world.
- StartAttack ran without control or while pacified.
- A charmed player's right-click selected and interacted.
- Talk ignored shapeshift forms.
- A plate right-click while targeting skipped the interact.
- A `/target` miss said nothing.
- `/follow` read the cast slot.

Reference: `0x7662c0` runs the press pre-hook `0x492b50` before any hit-test, which drops a payload and consumes the press. StartAttack returns on `0x20000` or lost control (`0x5ecbaa`, `0x5ecbc0`) for every caller. The router tests `CHARMEDBY` first (`0x481f98`), which plates skip (`0x4949f0`). Talk tests form flag `0x8` (`0x5f0150`). `0x489dbd` picks the miss message, and `/follow` reads `UNIT_CHANNEL_SPELL` (`0x60ffb9`).

Changed: each gate follows these, and tests pin each.

Fixes #321
Fixes #381
Fixes #785
Fixes #1305
Fixes #1397
Fixes #52
Fixes #1326

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:31:37 +07:00
Sam Sung 70432f0566 TGA, spell text, MCNR and DBC readers read what 1.12.1 reads (#2106)
The readers differed from 1.12.1:

- A TGA's descriptor bit 4 mirrored it.
- A 32-bit TGA with no alpha bits drew with byte 3 as alpha.
- Spell-text slot digits 0 and 4-9 read a wrong slot and `$g`/`$l` parsed their own way.
- MCNR normals were named `x, z, y`.
- A DBC record of the wrong size was read.

Reference:

- No TGA read path tests bit 4.
- `0x5a37e0` admits truecolour only at 24 bits besides alpha.
- A refused texture draws opaque white (`0x44a260`) at sizes `0x4484ae` admits.
- Only '1'-'9' is a slot digit (`0x507827`), and `0x508180`/`0x5082a0` parse the selectors.
- MCNR is X, Y, Z (`0x6aff10`).
- The loader refuses any other record size (`0x545e36`).

Changed: each reader follows these; three TGA edge cases carry `Deviation:` lines. Three BigWigs ready-check icons now draw white, as in 1.12.1.

Fixes #1679
Fixes #1693
Fixes #1684
Fixes #710
Fixes #799
Fixes #15
Fixes #17
Fixes #2102

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:18:28 +07:00
Sam Sung 33067c38ac Console commands and CVar defaults follow 1.12.1's registrations (#2104)
Seven console commands were not registered: `port`, `worldport`, `setrawpos`, `whois`, `togglehelm`, `togglecloak` and `cameraClip`. Three CVar rows also left the reference: `SoundBufferSize` shipped 100, `WorldDetail` was marked as a 1.12 CVar, and `gamma` shipped `1.000000`.

Reference:
- `0x4049e0` registers the movement and toggle commands, `port` and `setrawpos` splitting with the comma (`0x82ef90`) and `worldport` without it (`0x82ecc8`).
- `whois` sends its name (`0x5adee0`).
- `cameraClip` flips or sets its word (`0x50ddc0`).
- `0x457520` registers `SoundBufferSize` 50 above 1500 MHz, which current x86 parts read, and `0x402d70` registers `gamma` as `1.0`.

Changed: each command is registered with its category and its sends, which have byte-exact goldens. `SoundBufferSize` and the mix-ahead ring run at 50, `WorldDetail` is benilla's own row, and `gamma` registers `1.0`. Tests pin each.

Fixes #1639
Fixes #1663
Fixes #1749
Fixes #1270
Fixes #1279
Fixes #1281

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:51:38 +07:00
Sam Sung 374556aef3 The game clock and its server messages follow 1.12.1 (#2097)
The game-speed, game-time update and game-time set messages were dropped. The login time packet also applied a malformed body and kept unset fields as 31 and 63.

Reference: each handler reads its body, refuses one not exactly its length with the "Malformed message" line, and decodes an all-ones field as -1 (`0x642b30`):

- **Set** steps back a minute and advances (`0x6434d0`), so an unset field lands at 00:01.
- **Sync** steps forward or banks a skip count (`0x6436c0`).
- **Frame tick** pays the skip (`0x6435a0`). It runs from world entry to logout (`0x401684`, `0x401f04`).
- **Rate** is clamped to [1/60, 60], NaN kept (`0x643820`).

Changed: the four bodies parse with exact-length goldens, and the clock is a port of the reference's, ticking only in the world. The day number uses UTC civil days, a written deviation from `mktime`. Tests pin each step.

Fixes #2056
Fixes #2082

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:46:30 +07:00
Sam Sung 44e4032a1f Combat-log lines take 1.12.1's types, names, XP forms and arguments (#2096)
Combat-log lines read wrong in nine ways:

- Your split damage and extra attacks were damage-typed.
- The XP line ignored the group float.
- An item-name wait dropped its line after 120 frames.
- Game objects and items printed "Unknown".
- An XP kill also printed a death line.
- Templates with unused or positional arguments were refused.
- Open-lock effects wrote nothing.

Reference: split form 2 (`0x62df92`) and the extra-attack line (`0x62b190`) type through the buff table unless the spell is hostile. The XP line compares `ftol(xp / float)` with `xp` (`0x637cd0`). The wait list never expires (`0xc4e208`). `0x6264e0` names by type mask. The XP packet marks the victim (`0x60a0e6`). `vsnoprintf` binds `%N$` and skips unused arguments (`0x669888`). `0x62dc10` writes the open-lock line.

Changed: each follows its formatter; one `fill_exact` serves both fills. Tests pin each.

Fixes #2067
Fixes #2073
Fixes #2068
Fixes #2071
Fixes #2039
Fixes #2041
Fixes #2044
Fixes #2043
Fixes #2046

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:26:40 +07:00
Sam Sung 914b44b42d Group leader tests, loot lines, kicks and subgroups follow 1.12.1 (#2091)
Four group behaviours were wrong:

- `IsRaidLeader` and `IsPartyLeader` misread a raid leader outside your subgroup.
- A loot method, master or threshold change printed no line.
- The uninvite menus always kicked by name.
- `SetRaidSubgroup` outside 1-8 did nothing.

Reference:

- Both leader tests compare the leader cell with your guid; only `IsPartyLeader` guards a zero cell (`0x4e9141`).
- The group list stores the loot cells and prints each change (`0x4e86f0`, jump table `0x4e880c`). The master line needs a cached name; the threshold reads `ITEM_QUALITY<n>_DESC`.
- `0x5e9510` kicks a held player by name and anything else with `CMSG_GROUP_UNINVITE_GUID`.
- `SetRaidSubgroup` raises its usage line first (`0x4bb9fe`).

Changed: the leader tests read the guids, the list stores and prints the loot cells, the kicks follow `0x5e9510`, and the subgroup is checked first. Tests pin each.

Fixes #141
Fixes #70
Fixes #267
Fixes #266

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:33:39 +07:00
Sam Sung b39d928f6f Action bar answers and events follow 1.12.1's producers (#2090)
The action bar answered and signalled where 1.12.1 does not:

- `IsActionInRange` measured distance before the target gates.
- `GetActionCooldown` answered enable 1 for slots that reach no cooldown query.
- `IsCurrentAction` held item slots against nothing.
- The cooldown and usable events fired off a button's own state.

Reference: `0x4e56f0` runs `0x6e4440`'s relation and implicit-target gates first. Enable starts 0 (`0x4e6f0d`) and only the query sets it (`0x6e1406`). The cast keeps its item's guid (`0xcecab0`). `BAG_UPDATE_COOLDOWN` comes from `0x4f93d0`'s callers, `ACTIONBAR_UPDATE_USABLE` from `0x4e5c00`'s four, and `SPELL_UPDATE_USABLE` from `0x4b31c0`'s fourteen. Placement signals only 213.

Changed: a usable cache refreshes at those producers. The push half runs before the unit snapshots and the events after them, so handlers read this frame's state, after the control edge. Tests pin each answer and the order.

Fixes #1528
Fixes #1532
Fixes #1537
Fixes #1596
Fixes #1597

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:16:13 +07:00
Sam Sung 58f3b05b98 Glue cleans realm names, seeds agreements once and flags new characters (#2085)
Four glue-screen behaviours were wrong.

- **Realm names.** The list showed realm names raw.
- **Agreement flags.** The terms-agreement flags were reseeded from the CVars on every glue VM.
- **New characters.** A new character's record kept level 0 and no new-character flag.
- **PreviewRider.** Its comment misstated the sheathe.

Reference: the realm list fill turns each of `"*/:<>?\|` into a space and cuts trailing spaces and dots (`0x5b2450`, `0x5b24b0`). The agreement flags are seeded once per process. A level-0 record stores level 1 and sets `+0x10a` (tested at `0x46c203`), and that character enters at its race's intro eye with no loading tip. The select build holds each weapon in hand (`0x472c8a`, `0x47a15d`).

Changed: names are cleaned at the fill, the flags live in one shared set, and the roster stores the flag and looks up the intro eye at the pick. Tests pin each.

Fixes #1215
Fixes #1317
Fixes #1318
Fixes #1217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:59:32 +07:00
Sam Sung a66dc2d9b8 Combat text raises 1.12.1's words, SPELL_ACTIVE and swing actions (#2081)
Combat text missed most of the words 1.12.1 raises. A damage shield, an interrupt, extra attacks, a resisted proc, an immunity, a dispel, an aura, environmental damage, a drain and a reputation change about you wrote their log line but no `COMBAT_TEXT_UPDATE`. `SPELL_ACTIVE` never fired, and a swing's `UNIT_COMBAT` skipped the victim-state table.

Reference: each formatter raises its word at its own unit test after the line (`0x62cb5d`, `0x626b53`, `0x62db7d`, `0x628b8a`, `0x62d3ab`, `0x628daa`, `0x62d5ce`, `0x62b74f`, `0x62ac74`, `0x627b94`, `0x62c661`). The usable refresh walks the reactive list `[0xb71150]` (`0x4b31c0`). A swing's action comes from `0x83de28`, environmental damage sends a WOUND (`0x624f83`), and the float resolves its global (`0x703bf0`).

Each word rides the existing line path. The reactive list lives from arrival to arrival. Tests pin each word and gate.

Fixes #2027
Fixes #2028
Fixes #2029
Fixes #2030
Fixes #81
Fixes #82
Fixes #370
Fixes #2032
Fixes #2040

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:45:40 +07:00
Sam Sung 77cc954eaa check.sh runs the no-install test pass, as the land gates do (#2080)
`scripts/check.sh` ran the scoped tests with the install only, so a test that reads the install without `wow_data_or_skip!` passed every round and failed first in the gates' `test-no-install` leg, at the land.

`check.sh` now runs that leg on its own scope, right after its tests: `WOW_DATA=` set and empty, `BENILLA_REQUIRE_DATA` unset, the same `scripts/unit-tests.sh` and package flags. It builds nothing new, so it reuses the binaries the test step just built; on the three app and UI crates it ran 8434 tests in under a minute.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:04:59 +07:00
Sam Sung c678c196dd Unit, spell and aura tooltips draw 1.12.1's lines (#2072)
Unit, spell and aura tooltips draw what 1.12.1's builders draw:
- Unit: ?? for a hostile player ten up, the drunk level, the owner, Beast Lore, a group member's Offline or zone line, Resurrectable, contested PvP, Skinnable by skill, GlobalStrings wording.
- Spell: rank on pet and stance bars, cast speed, per-school cost terms, skill-based level terms, and the totem, use-all and reputation lines.
- Aura: stacks multiply, the caster's level scales, raidFilter applies, and the player's slots are read.

Tests pin each line.

Fixes #220
Fixes #221
Fixes #588
Fixes #590
Fixes #591
Fixes #594
Fixes #599
Fixes #605
Fixes #662
Fixes #663
Fixes #664
Fixes #1184
Fixes #1185
Fixes #1541
Fixes #1542
Fixes #1553
Fixes #1554
Fixes #1558
Fixes #1560
Fixes #1561
Fixes #1566
Fixes #1567
Fixes #1570
Fixes #1573
Fixes #1574
Fixes #1579
Fixes #273
Fixes #682

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:31:34 +07:00
Sam Sung 76bff74b02 The world frame takes 3-byte sizes; dials and clocks follow 1.12.1 (#2059)
The logon and world connections and the session clocks follow the reference:
- A world packet above 0x7fff takes a 3-byte size header, read and sent (`0x5b674d`, `0x5b56da`).
- A realm address with no port dials 9090, cut at its first colon, its port read by `_atol`.
- The logon dials one of the realmlist's addresses at random.
- GetNetStats reports the world link's KB/s.
- A transfer abort shows its TRANSFER_ABORT line.
- The game-time speed is held to 1/60 to 60.
- GetGameTime answers -1, -1 until the server sets the clock.
- The ping sequence carries across reconnects.

The handshake timeout, the 23-bit refusal and a port on a named logon server carry `Deviation:` lines. Byte-exact frame tests cover both header forms.

Fixes #1107
Fixes #1657
Fixes #1662
Fixes #1669
Fixes #1193
Fixes #1656
Fixes #1672
Fixes #1677

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:04:32 +07:00
Sam Sung 0ab3a3b5b5 Update packets write every block, then notify, and release objects (#2057)
The update-object handler now runs 1.12.1's two passes:
- It writes every block of a packet first, then runs the field watches at the packet's end against each object's last write. A packet with two values blocks for one object fires once.
- A re-create notifies every field.
- An object that leaves range is released with its fields and reactivated without its create hook, not destroyed.
- A first create runs no watch, so the flag tutorials fire only on a change, from their watchers.

The guild tag rides the same core:
- `TABARD_CANSAVE_CHANGED` comes only from RefreshGuildTag.
- A `GetGuildInfo` miss waits and signals `PLAYER_GUILD_UPDATE`.
- A new `PLAYER_GUILD_TIMESTAMP` re-asks the guild.

Tests pin each pass, the released table and the guild waiters.

Fixes #1391
Fixes #1392
Fixes #1499
Fixes #1418
Fixes #1420
Fixes #1416
Fixes #1417

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:58:01 +07:00
Sam Sung 9950c469a8 Item tooltips draw 1.12.1's lines for every source (#2050)
The item tooltip setters (bag, inventory, link, merchant, trade, mail, bank, petition) draw the lines 1.12.1's builder draws:
- spell lines for every trigger, and "Already known" by the taught spell;
- PvP rank and medal, zone and map, the gendered reputation label;
- a recipe's product and reagents, and a charter's signatures;
- a link's enchant, and a trade item's proposed enchant;
- the creator, and a second click on the same link hiding the tooltip;
- the merchant price and repair cost.

Charges and durability come from the object, the block or the template, as `0x52d9bd` and `0x52cca8` choose, so a link, a loot row or a merchant item shows no durability line. Tests pin each line and its gate.

Fixes #153
Fixes #154
Fixes #283
Fixes #1183
Fixes #1497
Fixes #1498
Fixes #1501
Fixes #1551
Fixes #1552
Fixes #1564
Fixes #1565
Fixes #1569
Fixes #1571
Fixes #1572
Fixes #1838

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:58:25 +07:00
Sam Sung c958f22c7e The combat log, combat text and UNIT_COMBAT follow the reference (#2047)
The combat log, combat text and UNIT_COMBAT follow the reference's handlers:
- An unresolved unit is named at once as the unit getter's miss, not held and dropped.
- Each spell formatter's `Attributes & 0x180`, unknown-spell and empty-name gates apply.
- SPELL_GO's miss list and a missile's arrival write their miss lines, words and UNIT_COMBAT.
- Melee words follow the victim-state table.
- Hostile, party and pet are classed by the reference's tests.
- Honor and XP lines take their side, gender and gates from the bytes.
- UNIT_COMBAT queues at its place against its line in each handler: a swing's flash goes ahead of its line, a spell hit after it.

Tests pin each order and gate.

Fixes #45
Fixes #83
Fixes #84
Fixes #208
Fixes #210
Fixes #211
Fixes #353
Fixes #357
Fixes #367
Fixes #382
Fixes #383
Fixes #384
Fixes #398
Fixes #399
Fixes #1172
Fixes #1240

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:24:39 +07:00
Sam Sung e9796754bf The ui_quad shader test finds its file on Windows too (#2037)
On Windows, `the_ui_quad_shader_composes_and_translates` panicked with `ui_quad.wgsl is embedded`. The test finds the shader by comparing each embedded path's string with `benilla_app/shaders/ui_quad.wgsl`, and Bevy's in-memory reader builds a child path with `PathBuf::join` (bevy_asset 0.18.1, `io/memory.rs:239`), so on Windows the string carries `\` and never matches. The same string became each shader's `embedded://` label.

The test now rebuilds each path from its components with `/`, as an asset path is written on every platform.

Test: the test itself, on the Windows CI job.

Fixes #2020

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:54:55 +07:00
Sam Sung 927f0117fb Test gates run under cargo-nextest, doctests beside them (#2036)
The test gates ran `cargo test`, which runs the workspace's test binaries one after another. On a warm tree the workspace's tests take 103 s that way, and 59 s under cargo-nextest, which runs every binary's tests in one pool; all 10,411 pass both ways.

`check.sh` and `gates.sh` now run tests through `scripts/unit-tests.sh`: cargo-nextest where it is installed, else `cargo test` as before. nextest runs no doctests, so `scripts/doc-tests.sh` runs those of the library crates in scope. Each gate keeps its environment (`BENILLA_REQUIRE_DATA`, `WOW_DATA=`, the skip log), and a failing run's summary now catches nextest's `FAIL` lines. CI is unchanged.

Test: this land's own gates run under it.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:27:18 +07:00
Sam Sung 85feb2c865 Cast reports and events happen inside the call, as 1.12.1's do (#2023)
A cast's refusal line, `SPELLCAST_FAILED`, `CURRENT_SPELL_CAST_CHANGED` and `IsCurrentAction` answered after the verb returned, and in-flight state was cleared from many places, each by its own rule.

1.12.1 keeps one in-flight slot and one saved strike (`0xceca88`, `0xcecaa8`). The attempt pushes it, the cancel (`0x6e4940`) and the cast result (`0x6e741a`) pop it, and the report runs inside the call (`0x6e1a00`).

benilla now has that slot, and every cancel goes through it:
- auto-repeat sends its cast before ATTACKSTOP;
- a right-click skin casts through the attempt;
- a click the cursor cannot bind deselects;
- a second Esc re-sends a strike's cancel, the old deviation removed;
- a channel's button unchecks as it begins.

Tests: each behaviour, failing without it; a live trace shows the cast, its cancel and a refusal's cancel in the reference's order.

Fixes #1985, #1990, #1989, #295, #1325, #1986, #1585, #2012, #2015, #1695, #2014, #2021

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:01:50 +07:00
Sam Sung 18ab7d8481 math.sin, math.cos and math.tan answer past the x87's range as 1.12.1 (#2019)
On Windows, `vm_parity`'s cos sweep failed on one item, `cos(1e300)`, whose last digit was the host libm's. 1.12.1 never computes it. `math_sin`, `math_cos` and `math_tan` are a bare `fsin`, `fcos` and `fptan` (`0x7fae1d`, `0x7fae4d`, `0x7fae7d`), and a finite operand of 2^63 or more is outside the x87's range and is left unchanged:
- `math.sin(1e300)` and `math.cos(1e300)` answer `1e300`;
- `math.tan` answers `-1.#IND`: `fptan` pushes nothing, so the pop at `0x7fae84` takes the operand and `0x7fae86` stores from an empty stack.

benilla-lua now answers the same. The sweep's recorded answers for that argument are the reference's, the same on every platform.

Test: the parity sweep's `1e300` items for `math.sin`, `math.cos`, `math.tan` and their degree forms.

Fixes #1999

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:09:26 +07:00
Sam Sung a6dbffd8e7 Windows audio keeps COM's shared apartment for cpal's one enumerator (#2018)
On Windows, `benilla-app`'s tests died with an access violation (`0xc0000005`) at the second test that opens the audio device. cpal 0.17.3 keeps one device enumerator for the process, made in the first caller's single-threaded COM apartment; when that thread exits, its `CoUninitialize` unloads the DLLs behind it, and the next call goes through a dead vtable (RustAudio/cpal#1302, open, the same `windows-latest` fault).

Every way into cpal now goes through one `host()`, which on Windows first anchors COM's shared multithreaded apartment for the process (`CoIncrementMTAUsage`, the pattern SDL uses) and joins it to build the enumerator, so no thread's exit can tear it down.

Test: two joined threads each read the device and agree.

Fixes #1984

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 05:51:46 +07:00
Sam Sung 7dfd17e520 Unit relations answer from one model of the reference's functions (#2017)
Unit relations answered from scattered stand-ins: `UnitCanAttack` ignored which unit attacks, `UnitReaction` its second unit, `UnitCanAssist` any pair without the player, and duels, FFA, contested guards and charm read wrong.

One model now follows the reference's functions rung by rung, two units in, argument order kept: `0x606170`, `0x6061e0`, `0x606530`, `0x606980`, `0x6066f0`, `0x605ff0`, `0x606ba0` and `0x6067f0`. Every caller asks it (ring, cursor, TAB, plates, tooltip, cast binder, usable, inspect, follow), and the six Lua verbs answer from it.

Tests: duels both ways and the opponent's pet, FFA, contested guards, a charmed player, cooperation, shipped-DBC reputation rows, and the follow and inspect gates.

Fixes #50
Fixes #132
Fixes #308
Fixes #309
Fixes #317
Fixes #826
Fixes #1323
Fixes #1545
Fixes #1588
Fixes #1602
Fixes #2008

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 05:34:38 +07:00
Sam Sung adbfedc366 Every text file checks out with LF on every platform (#2011)
On a Windows checkout every text file but Rust took CRLF, since `.gitattributes` pinned LF for `*.rs` alone. A test that matches a shader's source on `\n` (`a_fade_twin_blends_by_the_stage_zero_alpha_op`) failed there, and any `include_str!` of a shader, Lua, XML or TOML file read different bytes than on macOS and Linux.

`* text=auto eol=lf` now pins LF for every text file, keeping the two byte-for-byte exceptions (the vendored benilla-re tables and kira). Every file in the index is LF already, so a renormalize changes nothing stored; only what a Windows checkout writes changes.

Fixes #1998

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 05:04:32 +07:00
Sam Sung eaf93054d7 The party and raid roster reads and signals as 1.12.1's does (#2010)
The party and raid roster as Lua and its events see it did not follow 1.12.1: our raid row came first, a kick or disband kept the old members, status bits and chain ends read the wrong source, and events fired on edges, merged per frame.

Per `SMSG_GROUP_LIST 0x5e6a40`, `RebuildRaidRoster 0x4ba5f0` and the name callbacks `0x4ba1b0`/`0x4bad90`: our row is last and held only with members; endings clear at once (`0x4e84a0`); views read the descriptor or the record; and every signal is logged where the reference raises it, then fired once, in packet order.

Tests: wire-order signal sequences, solo, party, a raid with us in a later group, kick, disband, worldport, and names that arrive late.

Fixes #263
Fixes #265
Fixes #395
Fixes #670
Fixes #684
Fixes #701
Fixes #704
Fixes #705
Fixes #712
Fixes #715
Fixes #717
Fixes #2002

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:58:33 +07:00
andi79h-claude abcdced192 A frame in a saved variable writes as its name, as 1.12.1 writes it (#1996)
- A frame in a saved variable writes as its name, as 1.12.1 writes it
- A saved frame's name is read off the model, not a Lua GetName

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-20 02:21:45 +07:00
andi79h-claude b14793c294 The combat log fills deDE's numbered holes, so a German client prints its fights (#1994)
- The combat log fills deDE's numbered holes, so a German client prints its fights
- The fill's comment cites the client's ordered printf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-20 02:09:31 +07:00
Sam Sung 851a349d79 CI's Windows tests run every crate and name a test that crashes (#1995)
The first Windows run (#1984) died inside `benilla-app`'s lib tests with an access violation, which names no test, and `cargo test` stopped there, so no crate after it ran on Windows.

The Windows job now runs `--no-fail-fast`, so every crate reports, and on a failure reruns single-threaded, where each test's name prints before it runs, so the one a crash takes down is the last name in the log.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:58:49 +07:00
Sam Sung c0e301eaec A Lua cast arms inside its call, through a host lent to the VM (#1992)
A Lua cast was queued for a later frame, so on the next line of the same script `SpellIsTargeting()` read the old state and `SpellTargetUnit` found nothing to bind, as FuBar_CorkFu and SmartRes do.

In 1.12.1 `CastSpellByName` reaches `ArmCastTargets 0x6e5250` through direct calls (`0x4b4ab0` → `0x4b3300` → `0x6e5a90` → `0x6e4b60`), and `SpellIsTargeting` (`0x6e6cd0`) reads the word the arm stores.

The input pass and key bindings now lend the engine's call host to the VM around each script. The 20 verbs that queued apply inside the call, each under its own input events, and `CURRENT_SPELL_CAST_CHANGED` fires there. Calls an unhosted entry leaves apply at the next lend's start. The one unsafe lend is in `script/host.rs`.

Tests: CorkFu's and SmartRes's lines arm and bind in the call; a refused cast does not arm; every routed verb runs with a host lent.

Fixes #447

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:52:31 +07:00
Sam Sung 17d8b0e5dd CI runs the workspace tests on Windows (#1981)
v0.3's definition asks for the full test suite green on Windows (#798), and nothing runs it there: the gates run on macOS, the Windows check before a release only compiles the app, and CI runs on Linux.

A `windows` job now runs `cargo test --workspace` on `windows-latest`, with no install, for a fork's pull request and on a dispatch, beside the Linux gates. Its checkout uses the runner's own line-ending setting, as a Windows contributor's does, which is where #798's failures came from.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:11:54 +07:00
Sam Sung b305e6afc5 Main is 0.2.0 again until 0.3.0 ships (#1980)
The 0.3.0 tag was withdrawn within the hour: the milestone missed open Lua API defects its own definition covers, so 0.3 is not done.

With the tag gone and the version left at 0.3.0, every build from main would stamp a bare `0.3.0`, the same line the real release will print. Back at 0.2.0, the stamp reads `0.2.0+N` again, N commits past v0.2.0, until the 0.3.0 bump lands with its tag. The toolchain pin stays.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:06:03 +07:00
Sam Sung 9a5f2a56fd Release 0.3.0 (#1979)
Sets the workspace version to 0.3.0 and refreshes the lock, so the client's startup line reads `benilla build 0.3.0`. The toolchain pin stays at 1.98.1, the compiler the release checks ran on.

Checked on this tree: the Linux and Windows compile, the Vulkan run, and a live login and logout against a local server, whose log reads `benilla build 0.3.0`.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:39:02 +07:00
andi79h-claude 9969b8459b A moved bar item takes its green border along and leaves none behind (#1977)
The reference answers IsEquippedAction for the slot's action at call
time (0x4e74e0, the worn-slot walker 0x622270), so the in-place
ACTIONBAR_SLOT_CHANGED of a pickup reads nil for the emptied slot and a
place reads the moved item's own answer. The VM answers from the app's
per-slot states instead, which the cursor verbs left where they were:
the pickup's handler read the lifted item's equipped flag off the empty
slot and drew the green border there, and the app's re-feed fires no
event ActionButton_Update answers, so it stayed. A spell placed over a
worn item read the item's border the same way.

The pickup now drops the slot's state with the action; a lifted bar
item carries its state on the cursor (CursorAction.state, beside count
and consumable) and the place writes it with the slot; any other placed
action waits for the re-feed instead of reading the displaced occupant's.

Verified: cargo fmt --check, clippy -D warnings for benilla-ui and
benilla-app, cargo test -p benilla-ui --tests (2669 lib tests, the new
an_equipped_items_green_border_moves_with_it among them),
cargo test -p benilla-app --lib (5088). Built on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:56:50 +07:00
andi79h-claude cda0affaeb UnitDebuff's dispelType is SpellDispelType's column 11 on every locale, as 1.12.1 returns it (#1975)
UnitDebuff (0x5198f0) reads the class's record at [0xc0d83c], gates on
[+0x28] and returns [+0x2c], column 11: Magic, Curse, Disease, Poison
on every install (0x519a89). FrameXML keys DebuffTypeColor on those
words in every locale. benilla returned the Name block at the locale
slot instead, Magie on a German install, so DebuffTypeColor[debuffType]
was nil and TargetFrame.lua:303 raised "attempt to index local `color'"
on every frame a debuff with a dispel class sat on the target. The aura
tooltip (0x52f880) keeps the locale's word: SpellDispelTypes now holds
both, name (column 11) and label (the Name block), and the tooltip reads
dispel_label. locale_routing probes the label, the localized string.

Verified: on a German install real_dispel_types_name_only_what_the_gate_allows
failed before (Magie for Magic) and passes now, and the new
the_key_is_english_on_every_locale_and_the_label_is_the_locales passes
on German and English data; benilla-formats lib tests 487/487 on
English data; on German data the 55 failures left are the existing
English-name expectations, none new. cargo test -p benilla-app --lib,
fmt, clippy -D warnings green. Built on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:47:55 +07:00
Sam Sung 05e7cf4b93 A level-60 character shows the max-level bar, not the XP bar (#1976)
A level-60 character entered the world with the XP bar shown instead of the max-level bar.

The stock watch bar sets the XP bar from `UnitLevel("player")` on `UPDATE_FACTION` (`ReputationFrame.lua:161-244`). The server sends the faction list before the player's create, and benilla's only login signals came from that first push, before `"player"` was seated, so the bar read level 0 and stayed shown.

1.12.1 rebuilds the list in the world-enter cascade (`0x4909d3` calls `0x4d5430`), after `PLAYER_ENTERING_WORLD`, and that rebuild's sort signals `UPDATE_FACTION` (`0x4d55de` → `0x4d5db0`); the rebuild does nothing without a player (`0x4d5453`). benilla now runs that rebuild in the cascade, and the first push into a VM is silent. A test drives the server's order: level 60 hides the XP bar and shows the max-level art, level 59 the reverse.

Fixes #1973

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:09:38 +07:00
Sam Sung f52df3a111 A key moved off an action button takes its label along (#1974)
After a key binding moved between action buttons, the buttons it left kept showing its label, so several buttons read the same key.

1.12.1 does the same: when a button has no key, `ActionButton_UpdateHotkeys` (`ActionButton.lua:129-140`) clears the label only for an empty slot or a ranged action, and writes nothing for an action with no range (a stance, a self buff), so the old label stays.

This is a deviation, in the layer's `FrameXMLFixes.xml`: the stock function runs unchanged, then a button with no key and a no-range action has its label cleared. A test moves Shift-2 across three stance buttons and checks each one it leaves goes blank; without the fix the first still reads `s-2`.

Fixes #1842

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:43:05 +07:00
Sam Sung 881152bda6 Key-binding confirm dialogs no longer raise a Lua error each frame (#1972)
Unticking Character Specific Key Bindings, or pressing Defaults, raised `StaticPopup.lua:1713: attempt to compare number with nil` every frame the confirm stayed up.

1.12.1: `StaticPopup_Show` copies an entry's `timeout` into `dialog.timeleft`, and `StaticPopup_OnUpdate` compares it with 0 each frame; every stock entry declares one, the stock key-binding confirm included (`Blizzard_BindingUI.lua:10-24`, `timeout = 0`, `whileDead = 1`, `showAlert = 1`).

Our key-bindings page's two confirms declared none. Both now do, the character-set one with the stock entry's three fields. The checkbox test now runs a frame with the confirm up, and a new test fails on any dialog the loaded interface defines without a numeric timeout; both fail without the fix.

Fixes #1971

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:31:11 +07:00
Sam Sung 866e2cac31 A nameplate's health fill shrinks with the unit's health again (#1970)
Since #1959 a bar texture cuts its drawn rect only when its install set the texcoord-modifies-rect flag. The nameplate attached its fill by hand, never through that install, so the fill stayed full whatever the health.

1.12.1 builds the fill with the bar's own path verb: the plate setup calls `0x783140` with the fill's path on layer 2 (`0x7cb31a`-`0x7cb329`), which pins it to the bar and installs it through `0x7831f0`, the step that sets the flag (`0x783220`).

The plate now builds its fill through that same path leg, lifted into a model-level function, so it is installed, flagged and pinned like any bar texture. A test drives a plate at 40, 20 and 10 of 40 health and checks the drawn fill at full, half and a quarter width; it fails without the fix.

Fixes #1969

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:43:21 +07:00
Sam Sung 88eaf16799 GetContainerNumSlots and GetBagName answer on 1.12.1's bag ranges (#1968)
Both verbs answered whatever the container snapshot held, so the keyring, the bank and the backpack answered as ordinary bags.

1.12.1: `GetContainerNumSlots` (`0x4f9560`) answers 16 for the backpack, 24 for the bank, the bag's size for 1-10 and 0 for anything else, the keyring included (`0x4f95fb`). `GetBagName` (`0x4fa670`) answers `BACKPACK_TOOLTIP` for the backpack (`0x4fa6f0`), the bag item's name for 1-10, and nil for the bank, the keyring and the rest (`0x4fa712`).

Now both apply those ranges, the backpack is named by its global, and the snapshot no longer carries English names for the backpack, bank and keyring. A test pins every range. The keyring stays readable through `GetKeyRingSize` and the item verbs.

Found in Thorsten42/manila@a9fffcb5df.

Fixes #1967
Fixes #372

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Thorsten42 <3400178+Thorsten42@users.noreply.github.com>
2026-10-19 10:10:28 +07:00
Sam Sung 4dc5f1c191 Every by-name lookup runs 1.12.1's one resolver (#1966)
1.12.1 resolves every name through one function, `0x493aa0`: party and raid passes for members with no object held, then every held object its mode's filter passes, feeding one matcher (`0x493c60`). benilla had partial copies: the app's scan ran the object pass only, SetLootMethod the roster passes only, and Promote, assistant, duel and emote lookups matched exact names.

`benilla_ui::script::by_name::resolve` now runs the passes with each caller's typemask, mode and exact flag: TargetByName, /target, /assist, /follow, DoEmote, SetLootMethod, PromoteByName, PromoteToAssistant, DemoteAssistant and StartDuel. It answers nothing with no active player (`0x493ae0`). Mode 2's filter is `CanAssist` and alive (`0x493eca`).

The roster passes stay off for /target, /assist and /follow until the selection can hold a guid (#1965).

Tests per caller: prefix, a held stranger, a dead groupmate, a miss, no player. A byte audit re-derived the call sites and the matcher.

Fixes #1964

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 05:37:34 +07:00
Sam Sung 8e5114d8b1 Setters, bindings and loot methods gate their arguments as 1.12.1 does (#1963)
Method setters, bindings and the nameplate solver now gate their arguments as 1.12.1 does.

- Setters given a non-number raise their own usage line; SetTextHeight, SetFrameLevel, SetMaxLines and SetHistoryLines raise on the values 1.12.1 refuses (`0x87c750`, `0x879628`). XML `historyLines="0"` still loads, through the ring setter (`0x77cd90`).
- Bindings gated by `lua_isnumber` take 1.12.1's miss path: nil, a default, or no values.
- SetLootMethod follows `0x4e92a0`: group and leader gates, threshold argument 2 except for master, 2 on a miss, master names resolved by prefix (`0x493aa0`), errors for an unknown method or looter. SetLootThreshold takes the same gates.
- Quest watches fall back to the last selected quest; MessageFrame times store unclamped and expire to 0 (`0x786323`).
- The placement bound is `0x509dd0`'s 32-bit product, so a zero-width rect no longer panics.

Credit to kalistoking/benilla-twow@5b5374bc for #1956. A byte audit re-derived every site.

Fixes #1953
Fixes #1955
Fixes #1956
Fixes #408
Fixes #415
Fixes #416

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 04:22:14 +07:00
Sam Sung b2387b277b Resize, inset, backdrop, font and XML lengths follow the screen-unit law (#1962)
Beside the frame size and point methods (#1951), 1.12.1 puts every other script length through the screen-unit law: a setter stores `S(v)` and a getter answers `G(l)` through `K1` (`0x41ad70`, `0x41ae40`, `0x41ae60`). benilla stored and answered these as given.

- Resize bounds, hit-rect and text insets, backdrop lengths, pushed text offset, text extents, font heights (Button:GetFont through `0x79f3b0` too), shadows and spacing follow the law.
- A `<Rel…>` XML length is stored as parsed, in layout units (`0x6f2037`-`0x6f2388`).
- The tooltip sizer keeps its pad, line gap and wrap cap at 53 bits, as `0x41ae60` leaves them in `st0`.
- User-placed frames re-seat from the layout cache when the cells change, over what the scripts last built.

`SetMinResize(100, 100)` now reads back `G(S(100))`, not exactly 100, as in 1.12.1. Tests pin each kind against the law at 4:3 and 16:9; a byte audit re-derived each site.

Fixes #1942

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 03:49:55 +07:00
Sam Sung e550df4b0d Model frames load their own file again, not the extension-less key (#1961)
Since #1932 every Model frame drew the error cube: the VM queued a pane's file by its match key, `model_key`, which drops the extension, and the host loaded that key as the path, which the cache's extension rule (`0x706a50`) refuses (`0x70737f`).

The queue now carries the pane's own path beside the key, and the host opens that path through the rule: `.mdx`, `.mdl` and `.m2` load the `.m2`, and a refused name stands the cube. `model_key` is now the file the cache opens, so a refused name keys as the cube and never shares facts with its valid twin.

Tests: the three spellings of the minimap arrow load `interface/minimap/minimaparrow.m2`; `Creature\Wolf\Wolf` and `Foo.blp` load and key as the cube; the minimap's children queue their stock models.

Found in Thorsten42/manila@103bbe4.

Fixes #1958

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 03:22:22 +07:00
Sam Sung d9926c6be1 StatusBar bits, index usage lines, stack names and cursors follow 1.12.1 (#1959)
Ten differences from 1.12.1 in the UI core.

- StatusBar range and value follow `0x783270`/`0x7832d0` (a reversed pair collapses, a value before a range is dropped); the health watchers set both each update; the fill draws once both are set, its coordinates built from scratch (`0x7833c0`).
- A full collection runs before PLAYER_LOGIN (`0x490941`); the addon harness runs the client's world-entry cascade.
- Every index argument raises its own usage line (`0x6f34d0`, then the binding's literal).
- Strata, layer and frame-type names match whole (`0x6ee32c`); stack lines name unnamed functions by global (`0x6fbf90`).
- SetCursor takes a custom file (`0x523c40`); EditBox accepts OnInputLanguageChanged (`0x77a40d`).
- debugprofilestop answers milliseconds (a Deviation: multi-core 1.12.1 returns cycles).

A byte audit re-derived every address.

Fixes #1178
Fixes #1915
Fixes #1917
Fixes #1908
Fixes #1839
Fixes #1869
Fixes #1170
Fixes #1105
Fixes #1186
Fixes #1952

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:48:36 +07:00
Sam Sung 1c0b8dadc7 SimpleHTML font nodes, direct FontStrings and pane cameras follow 1.12.1 (#1957)
SimpleHTML's element fonts, a plain frame's direct FontString, and a model pane's camera differed from 1.12.1.

- SimpleHTML's four element fonts are font nodes (`0x789e7c`) its blocks link to, P's when an element holds no face (`0x78ae29`), so a later font change reaches pages already shown and MONOCHROME reaches the text; the 16 font methods are the Font object's own (`0x79ef10`-`0x79fd70`). The loader links `font=` and reads `file=` before `hyperlinkFormat` (`0x78a2b2`).
- A FontString directly under a plain frame builds nothing; the base loader has no arm for it (`0x769b27`).
- A model pane keeps the camera it resolved at SetCamera (`0x76cec0`); after SetModel the old camera holds as last painted, in the glue scene and in-world panes.

No stock screen changes. A byte audit re-derived every address.

Fixes #1890
Fixes #1923
Fixes #1924
Fixes #1948
Fixes #1930

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:13:57 +07:00
Sam Sung 817ce79993 XML anchors, numbers, lengths, shadows and frame elements follow 1.12.1 (#1951)
- Anchors take a point from every child; a bad point or relativeTo is logged and skipped before its Offset is read (`0x767992`).
- Numbers parse with SStrToFloat (`0x64aaa0`) at every site; length readers take the node, then its first child.
- A bare `<Shadow/>` is 0.001 layout units, drawn rounded to whole pixels: the zone and world-map text shadows draw 2 px at 1080p.
- EditBox HighlightColor; the text frames' `font=`, a `<FontString>` child loading into the frame's own font, and message-frame TextInsets.
- Insets, resize bounds, title region and fog colour apply every child, the instance's last; a Backdrop dispatches its children in order and logs unknown ones.

A byte audit re-derived every address.

Fixes #1889
Fixes #1926
Fixes #1273
Fixes #1920
Fixes #1903
Fixes #1906
Fixes #1925
Fixes #1936
Fixes #1937
Fixes #1943
Fixes #1944
Fixes #1945

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:13:10 +07:00
Sam Sung 7ab0596a24 GetAuctionItemInfo answers the miss shape for a row not yet cached (#1950)
An auction row whose item template had not landed answered a half-filled row: a nil quality beside the real count, prices and owner. In 1.12.1 `GetAuctionItemInfo` (`0x4cee40`) looks the row's item up in the item cache (`0x55ba30`); a miss jumps at `0x4cefa0` to the one exit `0x4cf1ec`, which answers the twelve-value miss shape with `count = 1` and `quality = -1`. Such a row now takes that exit. Tests cover an uncached row and a cached one.

Supersedes #1939.

Co-authored-by: kalistoking <148718830+kalistoking@users.noreply.github.com>

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 23:54:39 +07:00
Sam Sung 57384551de Widget methods, chat-frame state and line fades follow 1.12.1 (#1949)
Widget methods 1.12.1 has were missing or differed.

- EditBox IsAutoFocus, IsMultiLine, IsNumeric and IsPassword; ColorSelect GetColorHSV and SetColorHSV (`0x7bbc80`, `0x7bbd60`).
- AddMessage with no colour takes the frame font's colour.
- ScrollingMessageFrame: the write slot and the four line-position methods; the cached lines-shown, at-top and at-bottom state its readers use (`0x788440`); a relayout on resize (`0x788310`); OnMessageScrollChanged (`0x788a9a`); SetTimeVisible and SetFadeDuration usage, unclamped; each line fading on its own countdowns (`0x7884c1`).
- GetFramerate averages the last 30 frames (`0x672700`).
- SetCursor takes the eight named cursors in any case, with its usage line; custom cursor files are still to do.
- SimpleHTML links get buttons, so OnHyperlinkClick fires.

A byte audit re-derived every address.

Fixes #1518
Fixes #1520
Fixes #1506
Fixes #1519
Fixes #1883
Fixes #1682
Fixes #1538
Refs #1186

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:43:30 +07:00
Sam Sung fdbb4bb070 GetTrainerServiceInfo answers "" for a rankless service's subText (#1947)
`GetTrainerServiceInfo` answered nil for a rankless service's subText. In 1.12.1 the subText is the Spell record's NameSubtext (`0x4d8b50`, read at `0x4d8b84`), the empty string for a spell with no rank; only a service with no Spell record answers nil (`0x4d8b58`). The resolver now keeps an empty rank as `""` and the binding pushes None as nil. Tests cover both rows and the install-backed Apprentice Blacksmith trainer.

Supersedes #1935.

Co-authored-by: kalistoking <148718830+kalistoking@users.noreply.github.com>

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:28:55 +07:00
Sam Sung 837a6e1336 Frame sizes and positions follow 1.12.1's screen-unit law (#1946)
Frame sizes and positions skipped 1.12.1's screen-unit law. The client keeps layout units: a setter stores `S(v) = RN24(K1·RN24(RN24(v)/1024A))` and a getter answers `RN24(1024A·l)/K1`, from the cells `0x41ad10` sets from the aspect (benilla-re `systems/screencoord.md`, two readings).

- The layout resolves in layout units with the reference's four float roundings (`0x767414`, `0x76741f`, `0x767428`, `0x767a30`); the pixel seam converts.
- Size, edge, centre and point getters and setters, the screen-size and cursor globals (glue and game), scroll ranges and scroll events follow the law; OnSizeChanged passes the layout size (`0x76b660`).
- Scroll bars hide on `floor(range) == 0`, as the stock template does.
- The minimap and world-map arrows re-seat when the cells change.

At 4:3, `GetScreenHeight()` is 767.9999427795418. Nothing moves on screen beyond 2e-4 px. A byte audit re-derived the law and the resolver.

Fixes #1918
Fixes #1321

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:02:31 +07:00
Mathih13 e0031ab9ef A tooltip line wrapped past 20 rows sizes to its pieces, as in 1.12.1 (#1933)
A tooltip line wrapped past 20 rows sized narrower than in 1.12.1. The sizer splits a long line into at most 20 pieces, the last running to the end of the text (`0x53082f`, `0x53088e`), and sizes the plate to the widest. Each piece runs from one row's start to the next (`0x530884`-`0x5308a8`), so it carries the whitespace the wrap skipped (`0x5c24fe`, past `0x5c7710`), and a space measures as a glyph. Pieces are now measured that way, so most wrapped tooltips are about a space wider. Tests pin the 20-piece cap, the kept space and a piece crossing a line break.

Fixes #1880

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-18 21:11:17 +07:00
Sam Sung d463c0f03f Minimap state, zoom, ping, mask and model arrows follow 1.12.1 (#1941)
The minimap's methods, state and arrows differed from 1.12.1.

- Zoom, indoor flag, mask and ping are process-wide (#1876); SetZoom caps unsigned and writes only a change (`0x6da8e0`); every world entry reseeds the zoom from its CVars (`0x4ed9d0`).
- PingLocation pings in the call and signals MINIMAP_PING at once (`0x4eeca0`); raid and party pingers get their tokens.
- SetMaskTexture loads in the call, a miss stores green and raises; the tiles take the mask's colour (`0x4eae74`).
- The nine Model children are built (`0x4edbc0`); the rim and player arrows draw through them, above the dots, with SetArrowModel, SetPlayerModel, SetIconTexture and SetBlipTexture. The corpse is rim slot 2.

On screen: the player arrow is a 3D model; the corpse's in-range icon is about 4% larger. A byte audit re-derived every address.

Fixes #1522
Fixes #1645
Fixes #1652
Fixes #1653
Fixes #1871
Fixes #1872
Fixes #1876
Fixes #1877
Fixes #364

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:57:49 +07:00