Sync upstream UI fixes through c0630f3d #7

Merged
sindoring merged 29 commits from sync/upstream-20261011-latest into main 2026-10-11 17:52:10 +03:00
Owner

Integrate six upstream commits through c0630f3d (2026-10-11), preserving MoonWell branding, launcher integration and ruRU localization.

The update fixes OnUpdate ordering by strata/level/link order, bare Lua errors passed to the error handler, case-insensitive addon file lookup and UNIT_FACTION on faction-template changes. It also adds an optional Bevy dynamic-linking feature and lets test gates report all failing crates.

Merge conflicts retain localized spell costs, race/class display names, Cyrillic character-name input and the twelve-letter creation limit. UI error handling and update traversal follow upstream. docs/MAP.md was regenerated and has no delta.

Validation: full scripts/gates.sh passed on the exact source tree (1884 s) on Windows with the real 1.12.1 MPQ/DBC data and ruRU pack, including workspace tests with and without install data, player tests without dev, and engine boot checks. The 31 optional addon corpus tests are unavailable on this checkout. No live account login was attempted because no probe identity is configured. The optional dynamic-linking feature is retained but not enabled by the default gates.

Integrate six upstream commits through c0630f3d (2026-10-11), preserving MoonWell branding, launcher integration and ruRU localization. The update fixes OnUpdate ordering by strata/level/link order, bare Lua errors passed to the error handler, case-insensitive addon file lookup and UNIT_FACTION on faction-template changes. It also adds an optional Bevy dynamic-linking feature and lets test gates report all failing crates. Merge conflicts retain localized spell costs, race/class display names, Cyrillic character-name input and the twelve-letter creation limit. UI error handling and update traversal follow upstream. docs/MAP.md was regenerated and has no delta. Validation: full scripts/gates.sh passed on the exact source tree (1884 s) on Windows with the real 1.12.1 MPQ/DBC data and ruRU pack, including workspace tests with and without install data, player tests without dev, and engine boot checks. The 31 optional addon corpus tests are unavailable on this checkout. No live account login was attempted because no probe identity is configured. The optional dynamic-linking feature is retained but not enabled by the default gates.
sindoring added 29 commits 2026-10-11 17:49:33 +03:00
- A loot window that opens while you move closes, as in 1.12.1 (#782)
- Set auto_loot in the LootConfig initializer (clippy field_reassign_with_default)
- Close a window opened on the move in the feed's pass, so it never paints

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- 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>
- A loot right-click while moving does nothing, as in 1.12.1 (#760)
- loot gate: name the emitter's drain and write the landing deviation

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- Change `LevelUpInfo` to signed to match vanilla behavior
- Add parse test for negative values in SMSG_LEVELUP_INFO, update comments
- grammar
- level-up gains: cite the %d fire and vmangos's wrapped deltas; pin the reported line

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- capture: a name-rank scenario shoots a ranked player's overhead name line
- Overhead names carry the player's PvP title under UnitNamePlayerPVPTitle, as 1.12.1 does (#550)
- review: bundle the rank-title inputs; doc the snapshot and rank-19 limits
- nameplates: read rank strings per VM; test the composed name line
- pvp: correct the rank-title docs; convert the credit team without a cast
- Fix the titles row comment; name-rank reuses WOLF_POS, cites :900
- rank strings: the honor feed reads them off the VM; nameplates only reads the value

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
`TargetUnit("npc")`, `AssistUnit("npc")` and `SpellTargetUnit("npc")` did nothing with a merchant, gossip or quest window open: the app's unit-token resolver answered nobody for `npc`.

In 1.12.1 the resolver (`0x515970`) matches `npc` as a whole token (`0x515bec`, an unbounded compare, so `npctarget` stays unknown) and takes the interaction NPC's guid `[0xb4e2d0]`, which `0x4930d0` writes when the window opens.

`npc` now resolves to the held unit `InteractNpc` names. Tests cover the three verbs, a closed window and a streamed-out NPC, and `/script TargetUnit("npc")` typed into the stock chat box.

Fixes #707

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A unit tooltip's title read the bare name where 1.12.1 decorates it.

The tooltip calls the name builder `0x609370` itself (`0x52a1ab`, or `0x52a1db` when there is no second part) with the decoration flag a literal `1`, the same call `UnitPVPName` makes, so no CVar gates it. With the flag on, a ranked player reads `UNIT_PVP_NAME` of the gendered `PVP_RANK_<rank>_<team>` and the name, a city protector's `PVP_MEDAL<n>` follows on a second line (`0x6093ef`), and a civilian kill (`0x612550`) reads the `PVP_RANK_CIVILIAN` prefix (`0x609461`).

The title now runs the builder `UnitPVPName` already uses. The test covers rank, gender, medal, civilian, an unranked player and a creature; an on-demand `ui-tooltip-rank` capture shows a ranked player's tooltip.

Fixes #794

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
At login benilla sent `CMSG_SET_ACTIVE_MOVER`, `CMSG_QUERY_TIME`, `MSG_QUERY_NEXT_MAIL_TIME` and `CMSG_BATTLEFIELD_STATUS` right after `CMSG_PLAYER_LOGIN`, before the server had seated the player. cmangos drops them; after a worldport or `/reload` the time and mail queries never went again.

In 1.12.1 all four follow the local player's own create (`0x5dea50`): `SetActiveMover` (`0x6006e0`), then the world-enter cascade (`0x5deb60 call 0x4908c0`), whose inits send `0x1CE`, `0x284` and `0x2D3` in that order. A cross-map worldport re-creates the player and takes the same route; `/reload` re-enters the cascade alone (`0x490168`).

The four now ride the self create, in that order, and `/reload` re-sends the three. Tests cover login, the create, a worldport, the schedule order and the reload; a live trace shows the order at login, after `.tele` and on `ReloadUI()`.

Fixes #789
Fixes #100

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Elias Miettinen <eliaspmiettinen@gmail.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
The `out` trace tag logged only what the long-lived writer sent, drained after each command. Everything the session sends before the reader and writer split never reached it: `CMSG_AUTH_SESSION`, `CMSG_CHAR_ENUM`, `CMSG_PLAYER_LOGIN`, character create and delete, the cinematic ack and logout. So a login trace could not show where `CMSG_PLAYER_LOGIN` sits against `SMSG_LOGIN_VERIFY_WORLD`, which wire-timing work needs.

The world socket has one write path, `send_packet`. It now reports each packet to an observer right after the write, and the app installs the trace there only when the `out` tag is on. The writer's sent-log and its drains are gone; the line format is unchanged.

A protocol test over the fake server checks that the handshake, a pre-split send and a writer send are all observed, in order. A live login trace now opens with `CMSG_AUTH_SESSION`, `CMSG_CHAR_ENUM` and `CMSG_PLAYER_LOGIN`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A crate built on benilla through `run_with` has its own `benilla-config/`, but could not ask where it is: `local_state::home` is crate-private. A crate that ships an addon has to write it into that folder's `AddOns/`, where benilla discovers addons and resolves their art and fonts, so without the path it needed a fork.

- `benilla_app::config_dir()` answers `local_state::home()`, asked from `run_with` on, since a dev build resolves it from the launcher's folder set there.
- CONTRIBUTING's "Building on top" says a crate's files and addons go there, and names `run_with` and this folder as the two seams opened for crates alone.
- The example launcher logs its `AddOns/` folder through it, so the example, which clippy and the tests build, is the outside caller that keeps it public.

No 1.12.1 behaviour changes.

Fixes #807

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The meeting-stone status query (`CMSG 0x296`) and its text reset ran once per UI VM, and the text drop only on leaving the world. So after a cross-map worldport benilla kept the old text and never asked again.

In 1.12.1 the world-enter cascade's meeting-stone init (`0x490a14` → `0x4c9f40`) resets the text to `UNKNOWN` and sends `0x296`, right after the battlefield init, on every pass: login, every cross-map worldport and every `/reload`. The leave-world sweep `0x490a80` drops the text (`0x490b2f` → `0x4c9f80`) on a worldport, a `/reload` and leaving the world.

The enter leg now runs on the world-enter cascade after the battlefield status. The sweep is one edge, written whenever its latch is spent, and the drop reads it. Tests cover the cascade, a worldport, a reload and the schedule order; a live trace shows `0x296` after `0x2D3` and its reply at login, on `ReloadUI()` and after `.tele`.

Fixes #99

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capture: trainer scenarios shoot the service description field (#793)
- fix: trainer services show their spell's description, as 1.12.1 does (#793)
- review: reuse the caster closure, cut repeated comments, pin the profession rows
- trainer: the pet's skill value is the tooltip's one helper, not a copy
- trainer: the recipe row's real-data test reads a product that has text

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
The quest log never showed the `[n]` count of party members on a quest, because `IsUnitOnQuest` was a stub that always answered nil.

1.12.1 (`0x4dfe10`) checks both arguments first. It reads the row's quest id (`0x4df150`; a header or out-of-range row answers nil before the unit is resolved), resolves the token, and admits only you or one of the four party slots, by guid (`0x4e7f70`). It then needs the unit held as a player and scans the 20 `PLAYER_QUEST_LOG` ids for that quest. The answer is 1 or nil. The ids reach party members on the wire. `UNIT_QUEST_LOG_CHANGED` (`0x51bc63` → `0x51bd90`) fires once whenever any slot of a unit's window changes, so an open log recounts.

Unit snapshots now carry the window, the binding follows those steps, and the event fires. Tests cover each path and run the stock `QuestLogFrame.xml` to a `[2]` row; they fail against the stub.

Fixes #818

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Escape on the realm list closed the list and then also quit the game from the login screen, or went back to login from character select.

1.12.1 walks the keyboard frames once per press, strata high to low, and stops at the first handler that takes it (`0x765f10`, leaving at `0x765f64`/`0x765f86`). The realm list is DIALOG strata (`RealmList.xml:246`), so its Escape never reaches the screen beneath, even though it hides itself.

benilla's list and screens read the keys in no fixed order. A screen only checked whether the list was still shown, and the list's own Escape had just hidden it. Who owns a frame's keys is now decided once, before any of them runs (`Realms::owns_input`, set in `PreUpdate`).

Tests run the real list and screen systems in both orders, and fail before the fix with the reported symptoms.

Fixes #817

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a free-for-all area such as the Gurubashi battle ring, your own overhead name and selection ring turned red.

1.12.1 colours a player's name and ring through `0x605960` by mutual attackability: `CanAttack` (`0x606980`) in both directions. Both true reads red, only theirs soft blue, only yours yellow. Neither splits on PvP: yourself green or blue, a party member pale, others green or blue. `UnitReaction` (`0x6061e0`) answers friendly for a unit toward itself before the FFA rung (`0x606200`), so you can never attack yourself.

benilla stood in a reaction-rank threshold for that matrix, and its reaction lacked the identity rung. The selector is now the reference's four-arm matrix, `CanAttack` runs in both directions, and a unit is friendly to itself. `UnitReaction`, `UnitCanAttack` and the cursor follow for yourself.

Tests pin each arm and yourself in FFA, on the shipped faction templates too; they fail before the fix.

Fixes #814

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reopening the world map after zooming out showed the old map for a frame before the current zone.

In 1.12.1, every Lua verb that moves the map selection (`SetMapZoom`, `SetMapToCurrentZone`, a `ProcessMapClick` hit) goes through the setter `0x4a67a0`. The setter stores the selection, rebuilds the landmark list, then fires `WORLD_MAP_UPDATE` in place before returning (`0x4a6ce4`). So `WorldMapFrame`'s OnShow repaints the tiles before the first draw.

benilla queued the event to the next tick, so the first frame painted the previous map. The three verbs now run the setter whole. The landmark list is projected from per-row sources inside the setter, so the new tiles never show the old map's markers. The engine's own pushes still queue, as a narrowed deviation.

Tests replay the report's steps on the stock UI and check the event fires inside the verb with landmarks rebuilt first; they fail before the fix.

Fixes #811

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An instance with no `name=` took its template's name: `framexml::merge` laid the instance's attributes over the template's, so `name` and `id` carried through. Stock `UIOptionsFrame.xml`'s unnamed `<Button inherits="UIPanelCloseButton">` became the global `UIPanelCloseButton`, with `GetName()` answering it, and any unnamed addon frame did the same to its template's name.

In 1.12.1, `PreLoadXML` (`0x769770`) reads `name` and `id` from the instance node before `LoadXML` walks the templates (`0x76985c`). The texture and font string factories also read `name` once, outside that walk (`0x6f2751`, `0x6f27e1`). So neither is ever inherited.

The merge now drops the template's `name` and `id`. Tests cover an unnamed frame, a named frame (no template id), an unnamed texture and `CreateFrame` with a nil name. All four fail without the fix.

Fixes #394

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`PLAYER_LOGIN` fired at the end of the UI load, before the player existed: its handlers saw `UnitExists("player")` nil and level, health and XP 0, at every login and `/reload`. Ace2's `OnEnable` runs there.

In 1.12.1 the load only arms a one-shot (`0x49011d`). The world-enter cascade fires `PLAYER_LOGIN` if armed (`0x49094b`), then `PLAYER_ENTERING_WORLD` (`0x49096a`), inside a sound-suppression bracket (`0x4908d5`–`0x490a56`). It runs from the player's own create (`0x5deb60`), or on `/reload` from the load (`0x490168`) with the player still in the world. A worldport re-arms nothing.

Now the load arms it, and the unit feed fires both, silenced, once the player is seated. A `/reload` loads under the live player and no longer plays the target-select sound. Tests cover login, worldport and reload; they fail without the fix.

Fixes #189
Fixes #191

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI scale was folded into the pixel seam, with the root and `UIParent` at scale 1. Frames outside `UIParent` (dropdown menus, need/greed rolls, the world map) shrank with it, `UIParent:GetScale()` answered 1, and `GetCursorPosition()` was off by the scale.

In 1.12.1 the scale is `UIParent`'s own frame scale (`0x494550` → `0x76ac10`, floored at 0.64) over a 768-unit root. `GetScreenWidth`/`GetScreenHeight` divide by it (`0x494590`), and `GetCursorPosition` (`0x48b820`) does not.

Now the seam is `height/768`, and the scale is applied to `UIParent` at the load's end and on a resize. The nameplates' compensation is gone. Backdrops, hit insets and minimap pings now follow a frame's own scale (`0x76b580`, `0x77e8d0`); tooltips at the cursor and `OnCursorChanged` divide by it (`0x530b20`, `0x77db23`). Tests cover each, and a stock-UI comparison shows nothing inside `UIParent` moves.

Fixes #172

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Show and hide propagated a whole subtree first, then fired from a snapshot, parent first: a parent's `OnShow` ran before its children's, and a child hidden by an earlier handler still got its `OnShow`. A frame visible at the end of its load got no `OnShow` at all.

In 1.12.1 show `0x76ae10` and hide `0x76ad50` mark the frame, walk the live child list re-checking each child's gate, and fire the frame's own handler last (`0x76aef5`, `0x76adfd`). The post-load hook fires `OnShow` after `OnLoad` for a visible frame (`0x76a3b3`), from XML and `CreateFrame` alike.

Now handlers run inside a live post-order walk, and the post-load `OnShow` fires. Tests cover the order, a sibling hidden mid-walk, and one or two `OnShow`s after a load; they fail without the fix. Four existing tests' expected orders flip to the reference's.

Fixes #350
Fixes #59

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An incremental rebuild of `benilla-app` spent most of its time in ThinLTO: above opt-level 0 rustc runs it over a crate's own codegen units by default, and incremental compilation redoes it whole. `cargo play` already turns it off, which left its dependencies without cross-unit inlining.

`[profile.dev]` now sets `lto = "off"`, and dev and play build dependencies as one codegen unit each.

Measured on an M4 Max, one-constant edit in `benilla-app`, cpu_ms at the leg pin:

| | before | after |
|---|---|---|
| `cargo build -p benilla` | 24.4 s, 5.2 GB | 5.6 s, 2.7 GB |
| `cargo test -p benilla-app --lib --no-run` | 25.8 s, 7.0 GB | 8.3 s, 4.0 GB |
| cold dev build | 5:19 | 4:06 |
| dev cpu_ms | 11.06 | 11.71 |
| play cpu_ms (release: 10.1) | 11.74 | 10.63 |

Release and ship are unchanged.

Fixes #852.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add bevy/dynamic_linking feature forward for benilla-app for faster dev builds
- bevy_dynamic_linking says the binary starts only through cargo run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
A target whose faction template the server swaps live (35 to 14, neither naming a faction group) kept its friendly plate colour: `UNIT_FACTION` fired only when the pvp, FFA, faction-group or tapped state moved.

The reference's per-field watch fires `UNIT_FACTION` on any change of `UNIT_FIELD_FACTIONTEMPLATE`, as it fires `UNIT_FLAGS` for that field. `fire_transitions` now also fires it on the faction template's edge.

Test: `a_live_faction_template_change_recolours_the_target_plate`.

From #855 by @kalistoking, landed here because the fork refuses maintainer pushes.

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>
An addon's Lua and XML files were read with the exact case its `.toc` or `<Script file>` spelt. On a case-sensitive filesystem, Linux's, a file named in another case never loaded: Questie 3.7.1 lists `Astrolabe\Astrolabe.lua` from a folder shipped as `astrolabe`, so `Astrolabe` stayed nil and its OnUpdate raised every frame.

The reference is a Windows client, and every path it opens matches without regard to case. Addon textures and sounds already resolved that way through `loose_addon_file`; addon scripts, XML and `Bindings.xml` now go through the same resolver, and the second, case-sensitive one is gone. The shared resolver also refuses a component the host reads as more than a plain name (a Windows `C:`), so it still cannot leave the AddOns root.

Test: a file asked for in three spellings resolves to the one on disk; it fails without the change on a case-sensitive volume.

Fixes #851

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Script errors reached the Lua error handler as mlua's display text: a `runtime error: ` word and a `stack traceback:` block, which overflow the stock `ScriptErrors` dialog. And a raise in a `CreateFrame` template's OnLoad was logged as a warning and never reached the handler at all.

In the reference, handlers run under `lua_pcall` with the error function `0x703b40`, which calls the `seterrorhandler` function with the bare message. `CreateFrame` builds its template through the XML instantiator `0x6ee280`, whose OnLoad runs under that same function (`0x704d50`).

Now every caught error records two texts: the decorated one for benilla's log and the bare message for the handler. A load report pairs the two per row, and `CreateFrame` records its template's errors as script errors.

Tests: the handler hears bare messages from handlers, and a template OnLoad raise reaches it.

Fixes #697
Fixes #859

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
OnUpdate ran in frame-creation order, from a list built before the walk, so a frame hidden by an earlier handler still updated, and strata and level played no part.

The reference's update walk `0x765650` goes through the nine strata from the bottom, each stratum's levels from 0, and each level's list oldest link first. It saves the next frame before each call, so a hide skips a frame before its turn. A frame linked into the level being walked waits a tick; one linked into a level still ahead runs there.

The walk now follows that order, using the link stamps shows and level changes already keep. It checks each frame at its turn and places frames linked or given an `OnUpdate` mid-walk. An EditBox still flushes right after its own `OnUpdate`.

Tests cover the order, mid-walk hides and shows, and a box's flush.

Fixes #742

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cargo test` stops at the first test binary that fails, so a red `scripts/check.sh` or gate run showed one crate's failures and hid the rest: the next run found more. Both scripts now pass `--no-fail-fast`, so one run reports every failing test across the workspace. The exit status is unchanged: any failure still fails the step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integrate six upstream fixes while retaining MoonWell and ruRU changes.

Verified on Windows: fmt, clippy, workspace tests with real 1.12.1 data and
without an install, doc links and render-pass lint. The 31 optional addon
corpus cases are absent. Player tests and engine boot checks are running
before this branch lands.
sindoring marked the pull request as ready for review 2026-10-11 17:51:59 +03:00
sindoring merged commit 241e37d760 into main 2026-10-11 17:52:10 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sindoring/moonwell-client-core#7