From fd386e75aed791ea9502d0acdb03edd8922fa5e3 Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:58:49 +0700 Subject: [PATCH] Comments in benilla-ui: one line of fact each, no history, no record numbers (#143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one. - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`. - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale. - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent. - A StatusBar fills from its internal normalised fraction, not from `GetValue()`. - The addresses of `StartDuel` and `StartDuelUnit` were swapped. - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default. - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference. Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/benilla-ui/examples/dump_globals.rs | 52 +- .../examples/dump_widget_methods.rs | 7 +- crates/benilla-ui/src/bindings_xml.rs | 177 +- crates/benilla-ui/src/civil.rs | 33 +- crates/benilla-ui/src/framexml.rs | 387 +--- crates/benilla-ui/src/justify.rs | 157 +- crates/benilla-ui/src/layout.rs | 395 +--- crates/benilla-ui/src/lib.rs | 24 +- crates/benilla-ui/src/loader/backdrop.rs | 14 +- crates/benilla-ui/src/loader/geometry.rs | 131 +- crates/benilla-ui/src/loader/mod.rs | 688 ++---- crates/benilla-ui/src/loader/regions.rs | 273 +-- crates/benilla-ui/src/loader/scripts.rs | 144 +- crates/benilla-ui/src/loader/tests.rs | 577 ++--- crates/benilla-ui/src/loader/widgets.rs | 365 +--- crates/benilla-ui/src/markup.rs | 478 ++--- crates/benilla-ui/src/messages.rs | 108 +- crates/benilla-ui/src/order.rs | 441 ++-- crates/benilla-ui/src/script/action.rs | 253 +-- .../src/script/action_bar_toggles.rs | 120 +- crates/benilla-ui/src/script/addon.rs | 513 ++--- crates/benilla-ui/src/script/addon_gate.rs | 108 +- crates/benilla-ui/src/script/addon_message.rs | 371 +--- crates/benilla-ui/src/script/auction.rs | 354 +-- crates/benilla-ui/src/script/aura/mod.rs | 295 +-- .../benilla-ui/src/script/aura/player_buff.rs | 259 +-- crates/benilla-ui/src/script/backdrop.rs | 192 +- crates/benilla-ui/src/script/bank.rs | 171 +- .../src/script/battlefield_positions.rs | 40 +- .../src/script/battlefield_queue.rs | 152 +- .../src/script/battlefield_score.rs | 104 +- crates/benilla-ui/src/script/bind_confirm.rs | 83 +- crates/benilla-ui/src/script/binder.rs | 32 +- crates/benilla-ui/src/script/binding_abi.rs | 342 +-- crates/benilla-ui/src/script/button.rs | 651 ++---- crates/benilla-ui/src/script/camera_view.rs | 117 +- crates/benilla-ui/src/script/channel.rs | 311 +-- crates/benilla-ui/src/script/char_stats.rs | 867 ++------ crates/benilla-ui/src/script/chat_misc.rs | 44 +- crates/benilla-ui/src/script/chat_send.rs | 134 +- crates/benilla-ui/src/script/chat_types.rs | 62 +- crates/benilla-ui/src/script/chat_window.rs | 487 +---- crates/benilla-ui/src/script/client.rs | 275 +-- crates/benilla-ui/src/script/clip.rs | 26 +- crates/benilla-ui/src/script/colorselect.rs | 282 +-- crates/benilla-ui/src/script/container.rs | 640 ++---- crates/benilla-ui/src/script/craft.rs | 342 +-- crates/benilla-ui/src/script/cursor.rs | 460 ++-- .../benilla-ui/src/script/cursor/bag_verbs.rs | 193 +- crates/benilla-ui/src/script/cursor/bar.rs | 132 +- crates/benilla-ui/src/script/cursor/doll.rs | 161 +- crates/benilla-ui/src/script/cursor/drag.rs | 135 +- crates/benilla-ui/src/script/cursor/money.rs | 59 +- crates/benilla-ui/src/script/cursor/pet.rs | 249 +-- crates/benilla-ui/src/script/cvars.rs | 836 ++------ crates/benilla-ui/src/script/death.rs | 91 +- crates/benilla-ui/src/script/diagnostics.rs | 219 +- crates/benilla-ui/src/script/dialog_verbs.rs | 72 +- crates/benilla-ui/src/script/dressup.rs | 80 +- crates/benilla-ui/src/script/duel.rs | 50 +- crates/benilla-ui/src/script/editbox.rs | 371 +--- .../benilla-ui/src/script/editbox/interact.rs | 62 +- .../benilla-ui/src/script/editbox/methods.rs | 229 +- crates/benilla-ui/src/script/editbox/seam.rs | 65 +- crates/benilla-ui/src/script/editbox/tests.rs | 294 +-- crates/benilla-ui/src/script/event.rs | 170 +- crates/benilla-ui/src/script/extract.rs | 376 +--- crates/benilla-ui/src/script/follow.rs | 56 +- crates/benilla-ui/src/script/font.rs | 304 +-- crates/benilla-ui/src/script/font_block.rs | 261 +-- crates/benilla-ui/src/script/gm_ticket.rs | 92 +- crates/benilla-ui/src/script/gossip.rs | 143 +- crates/benilla-ui/src/script/guild.rs | 577 ++--- crates/benilla-ui/src/script/handler_prof.rs | 151 +- crates/benilla-ui/src/script/inspect.rs | 202 +- crates/benilla-ui/src/script/instance.rs | 48 +- crates/benilla-ui/src/script/item_stats.rs | 503 ++--- crates/benilla-ui/src/script/item_text.rs | 37 +- crates/benilla-ui/src/script/keybind.rs | 396 +--- crates/benilla-ui/src/script/keyboard.rs | 171 +- crates/benilla-ui/src/script/layout.rs | 922 ++------ crates/benilla-ui/src/script/layout_cache.rs | 142 +- crates/benilla-ui/src/script/loot.rs | 383 +--- crates/benilla-ui/src/script/loot_roll.rs | 174 +- crates/benilla-ui/src/script/lua50.rs | 672 ++---- .../benilla-ui/src/script/lua50/table_size.rs | 167 +- crates/benilla-ui/src/script/macros.rs | 202 +- crates/benilla-ui/src/script/mail.rs | 401 +--- crates/benilla-ui/src/script/measure.rs | 107 +- crates/benilla-ui/src/script/merchant.rs | 390 +--- .../benilla-ui/src/script/messageframe/mod.rs | 221 +- .../src/script/messageframe/plain.rs | 164 +- .../src/script/messageframe/scrolling.rs | 216 +- crates/benilla-ui/src/script/minimap.rs | 103 +- crates/benilla-ui/src/script/mod.rs | 876 ++------ crates/benilla-ui/src/script/model.rs | 1890 ++++------------- crates/benilla-ui/src/script/modelframe.rs | 418 +--- crates/benilla-ui/src/script/nameplate.rs | 475 +---- crates/benilla-ui/src/script/net_stats.rs | 25 +- crates/benilla-ui/src/script/object.rs | 451 ++-- .../src/script/object/anchor_args.rs | 128 +- .../src/script/object/events_regions.rs | 267 +-- .../src/script/object/frame_state.rs | 389 +--- .../src/script/object/layout_methods.rs | 176 +- .../benilla-ui/src/script/object/movable.rs | 376 +--- .../benilla-ui/src/script/object/toplevel.rs | 180 +- crates/benilla-ui/src/script/party.rs | 650 ++---- crates/benilla-ui/src/script/pet.rs | 470 +--- crates/benilla-ui/src/script/petition.rs | 285 +-- crates/benilla-ui/src/script/pointer.rs | 593 ++---- crates/benilla-ui/src/script/pvp/mod.rs | 573 ++--- crates/benilla-ui/src/script/pvp/tests.rs | 248 +-- crates/benilla-ui/src/script/quest.rs | 213 +- crates/benilla-ui/src/script/quest_log.rs | 556 ++--- crates/benilla-ui/src/script/region.rs | 464 +--- crates/benilla-ui/src/script/region/layout.rs | 80 +- crates/benilla-ui/src/script/region/paint.rs | 382 +--- crates/benilla-ui/src/script/region/text.rs | 166 +- crates/benilla-ui/src/script/region_map.rs | 134 +- crates/benilla-ui/src/script/reputation.rs | 302 +-- crates/benilla-ui/src/script/saved.rs | 162 +- crates/benilla-ui/src/script/screenshot.rs | 23 +- crates/benilla-ui/src/script/scrollframe.rs | 189 +- crates/benilla-ui/src/script/session.rs | 76 +- crates/benilla-ui/src/script/shapeshift.rs | 63 +- .../benilla-ui/src/script/simplehtml/mod.rs | 342 +-- .../benilla-ui/src/script/simplehtml/parse.rs | 255 +-- crates/benilla-ui/src/script/skills.rs | 378 +--- crates/benilla-ui/src/script/slash.rs | 83 +- crates/benilla-ui/src/script/slider.rs | 171 +- crates/benilla-ui/src/script/social.rs | 239 +-- crates/benilla-ui/src/script/sound.rs | 118 +- crates/benilla-ui/src/script/spellbook.rs | 644 ++---- crates/benilla-ui/src/script/stable.rs | 336 +-- crates/benilla-ui/src/script/statusbar.rs | 74 +- crates/benilla-ui/src/script/stdlib.rs | 263 +-- crates/benilla-ui/src/script/summon.rs | 66 +- crates/benilla-ui/src/script/surface.rs | 53 +- crates/benilla-ui/src/script/tabard.rs | 103 +- crates/benilla-ui/src/script/talent.rs | 148 +- crates/benilla-ui/src/script/taxi.rs | 105 +- .../src/script/tests/addon_argument_abi.rs | 47 +- .../src/script/tests/addon_index_space.rs | 59 +- crates/benilla-ui/src/script/tests/anchors.rs | 114 +- .../benilla-ui/src/script/tests/backdrop.rs | 42 +- crates/benilla-ui/src/script/tests/button.rs | 295 +-- crates/benilla-ui/src/script/tests/channel.rs | 98 +- .../benilla-ui/src/script/tests/cooldown.rs | 38 +- .../src/script/tests/create_frame_template.rs | 126 +- .../src/script/tests/dispatch_bench.rs | 59 +- .../benilla-ui/src/script/tests/end_to_end.rs | 11 +- crates/benilla-ui/src/script/tests/events.rs | 87 +- .../src/script/tests/font_object.rs | 268 +-- .../benilla-ui/src/script/tests/frame_api.rs | 15 +- .../src/script/tests/generic_for.rs | 82 +- .../benilla-ui/src/script/tests/gm_ticket.rs | 46 +- .../src/script/tests/handler_prof.rs | 28 +- crates/benilla-ui/src/script/tests/input.rs | 200 +- .../benilla-ui/src/script/tests/keyboard.rs | 51 +- .../src/script/tests/layout_gate.rs | 129 +- crates/benilla-ui/src/script/tests/measure.rs | 135 +- crates/benilla-ui/src/script/tests/minimap.rs | 81 +- crates/benilla-ui/src/script/tests/mod.rs | 61 +- .../src/script/tests/model_clock.rs | 82 +- .../benilla-ui/src/script/tests/modelframe.rs | 216 +- crates/benilla-ui/src/script/tests/movable.rs | 141 +- .../src/script/tests/name_targets.rs | 61 +- .../benilla-ui/src/script/tests/nameplate.rs | 246 +-- .../src/script/tests/numeric_shape_gate.rs | 42 +- .../src/script/tests/reference_surface.rs | 569 +---- .../benilla-ui/src/script/tests/region_map.rs | 46 +- crates/benilla-ui/src/script/tests/regions.rs | 395 +--- .../src/script/tests/resize_bounds.rs | 56 +- .../src/script/tests/scrollframe.rs | 115 +- .../benilla-ui/src/script/tests/setparent.rs | 56 +- .../benilla-ui/src/script/tests/simplehtml.rs | 215 +- .../src/script/tests/size_changed.rs | 51 +- crates/benilla-ui/src/script/tests/slider.rs | 137 +- .../benilla-ui/src/script/tests/statusbar.rs | 9 +- crates/benilla-ui/src/script/tests/stdlib.rs | 161 +- .../src/script/tests/structure_queries.rs | 57 +- crates/benilla-ui/src/script/tests/talent.rs | 25 +- crates/benilla-ui/src/script/tests/taxi.rs | 9 +- .../src/script/tests/texcoord_font.rs | 36 +- crates/benilla-ui/src/script/tests/tooltip.rs | 220 +- .../src/script/tests/tooltip_item/bindings.rs | 110 +- .../src/script/tests/tooltip_item/law.rs | 195 +- .../script/tests/tooltip_item/mechanics.rs | 110 +- .../src/script/tests/tooltip_item/mod.rs | 30 +- .../src/script/tests/tooltip_item/set.rs | 38 +- .../src/script/tests/tooltip_spell.rs | 147 +- .../src/script/tests/tooltip_unit.rs | 98 +- .../benilla-ui/src/script/tests/toplevel.rs | 210 +- .../benilla-ui/src/script/tests/visibility.rs | 2 +- .../src/script/tests/widget_surface.rs | 105 +- .../benilla-ui/src/script/tests/worldframe.rs | 12 +- .../benilla-ui/src/script/tests/worldmap.rs | 177 +- crates/benilla-ui/src/script/tick.rs | 198 +- crates/benilla-ui/src/script/tooltip/mod.rs | 380 +--- crates/benilla-ui/src/script/tooltip/verbs.rs | 247 +-- .../benilla-ui/src/script/tooltip_item/mod.rs | 491 ++--- .../src/script/tooltip_item/names.rs | 98 +- .../src/script/tooltip_item/render.rs | 578 ++--- crates/benilla-ui/src/script/tooltip_spell.rs | 379 +--- crates/benilla-ui/src/script/tooltip_unit.rs | 254 +-- crates/benilla-ui/src/script/trade.rs | 208 +- .../benilla-ui/src/script/tradeskill/api.rs | 112 +- .../benilla-ui/src/script/tradeskill/mod.rs | 237 +-- .../benilla-ui/src/script/tradeskill/tests.rs | 127 +- .../benilla-ui/src/script/tradeskill/view.rs | 138 +- crates/benilla-ui/src/script/trainer/mod.rs | 507 +---- crates/benilla-ui/src/script/trainer/tests.rs | 147 +- crates/benilla-ui/src/script/tutorial.rs | 34 +- crates/benilla-ui/src/script/types.rs | 762 ++----- crates/benilla-ui/src/script/ui_errors.rs | 22 +- crates/benilla-ui/src/script/unit/bindings.rs | 794 ++----- crates/benilla-ui/src/script/unit/mod.rs | 775 ++----- crates/benilla-ui/src/script/unit/tests.rs | 492 ++--- .../benilla-ui/src/script/weapon_enchant.rs | 130 +- crates/benilla-ui/src/script/who_sort.rs | 158 +- crates/benilla-ui/src/script/worldmap.rs | 496 ++--- .../benilla-ui/src/script/worldmap_arrow.rs | 96 +- crates/benilla-ui/src/script/worldstate.rs | 76 +- crates/benilla-ui/src/script/worn_display.rs | 79 +- crates/benilla-ui/src/source.rs | 136 +- crates/benilla-ui/src/strings.rs | 207 +- crates/benilla-ui/src/toc.rs | 168 +- crates/benilla-ui/src/widget/kinds/editbox.rs | 521 ++--- .../src/widget/kinds/messageframe.rs | 364 +--- crates/benilla-ui/src/widget/kinds/mod.rs | 1412 +++--------- crates/benilla-ui/src/widget/mod.rs | 542 +---- crates/benilla-ui/src/widget/propagation.rs | 291 +-- crates/benilla-ui/src/widget/tests.rs | 50 +- 233 files changed, 14108 insertions(+), 42094 deletions(-) diff --git a/crates/benilla-ui/examples/dump_globals.rs b/crates/benilla-ui/examples/dump_globals.rs index b85f510f..1ff2df50 100644 --- a/crates/benilla-ui/examples/dump_globals.rs +++ b/crates/benilla-ui/examples/dump_globals.rs @@ -1,40 +1,17 @@ -//! `dump_globals` — print benilla's Lua global namespace, asked of a real VM. +//! `dump_globals`: print benilla's Lua global namespace, asked of a real VM. //! //! ```text //! cargo run -q -p benilla-ui --example dump_globals # nametype, one per line -//! cargo run -q -p benilla-ui --example dump_globals --members # ...the stdlib TABLES' members too +//! cargo run -q -p benilla-ui --example dump_globals --members # plus the stdlib tables' members //! ``` //! -//! **The point is that it is a run, not a grep** (decisions 1188, 1189). Every wrong number in the -//! addon arc came from measuring our API surface by pattern-matching Rust source: a regex over -//! `.set("Name", …)` misses the Lua prelude, miscounts a `format!`-registered family, and cannot -//! see what the sandbox removed. When the question is what a running system exposes, ask the -//! running system — this is the four-minute answer 1188 §4 describes, made permanent. -//! -//! This is [`UiScript::new`]'s namespace: the Rust bindings plus the Lua stdlib prelude, i.e. what -//! an addon sees *before* any FrameXML loads. That is deliberately the engine half — it is -//! deterministic, needs no install, and is exactly what `scripts/api-coverage.sh` compares against -//! `reference/1.12-globals.tsv`'s `engine` and `lua` rows. -//! -//! ## `--members`, and the blind spot it closes (decision 1194) -//! -//! `_G` is not the whole surface an addon can tell apart. `table.setn` is not a global — it is a -//! member of the `table` table — and it stopped **61 of 218** real addons dead, because mlua's Lua -//! 5.1 raises `'setn' is obsolete` where the 1.12 client's Lua 5.0 does the thing. A `_G`-only -//! instrument cannot see that, and did not: the arc measured coverage for two sessions with this -//! gap wide open. `--members` prints `table.setn`-style rows for the stdlib tables, so the dialect -//! is measurable the same way the API surface is. -//! -//! It also prints the **per-type metatables** (decision 2171), which is the same blind spot one -//! layer further down and stayed open longer because a metatable is not a *name*: 5.1 gives the -//! string type one and 5.0 cannot have one, so `("x"):upper()` worked here and raised on the -//! reference for as long as nothing thought to look. +//! The namespace is [`UiScript::new`]'s, the engine half before any FrameXML loads, which +//! `scripts/api-coverage.sh` compares with `reference/1.12-globals.tsv`. `--members` adds what `_G` +//! cannot show: the stdlib tables' members (`table.setn` is one) and the per-type metatables. use benilla_ui::script::UiScript; -/// The tables whose membership an addon can observe and depend on. -/// -/// Deliberately a fixed list, not "every table-valued global": `_G` would recurse, and a *frame* -/// table's members are the widget API, which is a different question with a different reference. +/// The stdlib tables whose members an addon can observe: a fixed list, since `_G` would recurse +/// and a frame's members are the widget API. const STDLIB: &[&str] = &["string", "table", "math", "coroutine", "os", "io", "debug"]; fn main() -> mlua::Result<()> { @@ -66,18 +43,9 @@ fn main() -> mlua::Result<()> { rows.extend(members); } - // **The per-type metatables — the surface `_G` and the member lists both miss** (decision - // 2171). `table.setn` taught 1194 that a `_G`-only instrument cannot see a *member*; the - // string metatable is the same lesson one layer further down, and it stayed invisible for - // longer because it is not a name at all. 1.12 installs none — `lua_setmetatable 0x6f4020` - // accepts only LUA_TTABLE and LUA_TUSERDATA and returns 0 for every other tag, so a - // primitive type there *cannot* carry one — while stock 5.1 gives `string` a metatable - // whose `__index` is the `string` table itself, which is what makes `("x"):upper()` work - // on 5.1 and raise on 5.0. Every row here should read `nil`. - // - // Written as a fixed list rather than a table because a `nil` value has no entry to - // iterate — the one type whose metatable slot is easiest to forget is the one a - // constructor silently drops. + // The per-type metatables. Every row should read `nil`: 1.12's `lua_setmetatable` + // (`0x6f4020`) takes only tables and userdata, where 5.1 gives strings one whose `__index` + // makes `("x"):upper()` work. Probed one by one, since a `nil` has no table entry. let metatables: Vec = script.eval( "local out = {} \ local function probe(name, v) \ diff --git a/crates/benilla-ui/examples/dump_widget_methods.rs b/crates/benilla-ui/examples/dump_widget_methods.rs index ad0d3dc7..27550a20 100644 --- a/crates/benilla-ui/examples/dump_widget_methods.rs +++ b/crates/benilla-ui/examples/dump_widget_methods.rs @@ -1,13 +1,10 @@ -//! `dump_widget_methods` — print benilla's widget METHOD surface, asked of a real VM. +//! `dump_widget_methods`: print benilla's widget method surface, asked of a real VM. //! //! ```text //! cargo run -q -p benilla-ui --example dump_widget_methods # classmethod, one per line //! ``` //! -//! The sibling of [`dump_globals`](../dump_globals.rs). The measurement itself lives in -//! `benilla_ui::script::widget_method_census` — its module doc is the *why*, and the widget-surface -//! gate (decision 2142) reads the same function, so the instrument you look at by hand and the gate -//! that fails the build can never disagree about what our surface is. +//! The census is `widget_method_census`, the same function the widget-surface gate reads. use benilla_ui::script::{widget_method_census, UiScript}; fn main() -> mlua::Result<()> { diff --git a/crates/benilla-ui/src/bindings_xml.rs b/crates/benilla-ui/src/bindings_xml.rs index 0a714285..895c9d3e 100644 --- a/crates/benilla-ui/src/bindings_xml.rs +++ b/crates/benilla-ui/src/bindings_xml.rs @@ -1,107 +1,37 @@ -//! **`Bindings.xml`** — an addon's key-binding declaration (decision 1188 phase 4). +//! An addon's `Bindings.xml`, its key-binding declarations. The reference loads one per addon in +//! `AddOn_Load 0x51f240`, after the addon's `.toc` files (`0x51f3fa`) and before its saved +//! variables (`0x51f400` to `0x51f4b5`). The stock file has 228 live bindings (94 `runOnUp`, 13 +//! `header`, 12 `hidden`, 9 `debug`, 5 `platform`); the six `MOVEVIEW*` ones sit in an XML comment. //! -//! The reference loads one per addon, at a verified position inside `AddOn_Load 0x51f240`: -//! **after** that addon's `.toc`-listed files (`0x51f3fa`) and **before** its saved-variables -//! files (`0x51f400` → `0x51f4b5`). Both of benilla's load paths -//! attach there — the startup walk (`benilla_app::ui_script::addons`) and the demand load -//! ([`crate::script::addon`]) — and what they parse here feeds the same table the Key Bindings -//! window edits ([`crate::script::keybind`]). -//! -//! ## The format, read off the client's own file, not remembered -//! -//! `Interface\FrameXML\Bindings.xml` (extracted 1.12.1) is **228 live `` elements**, -//! every one named, and the whole schema it uses is six attributes: `name` (228), `runOnUp` (94), -//! `header` (13), `hidden` (12), `debug` (9), `platform` (5). -//! -//! *Live*, because a text search says 234: six more sit inside an XML comment — Blizzard commented -//! out the whole `MOVEVIEW*` family in place. A parser that finds bindings by scanning text (and -//! any count taken by `grep`) registers those six as real, which is why the counts here are the -//! ones this module's own parse returns and why `a_commented_out_binding_is_not_a_binding` exists. -//! The counts are pinned against the install itself by -//! `benilla_app::bindings::commands::tests::the_installs_bindings_xml_reads_as_the_parsers_header_says` -//! — there, because there is where the patch chain is (this crate reads no install). -//! -//! ```xml -//! -//! -//! if ( keystate == "down" ) then MoveForwardStart(); else MoveForwardStop(); end -//! -//! -//! ``` -//! -//! - **The body is ONE Lua chunk, run once or twice.** A `runOnUp="true"` binding runs on the -//! press *and again* on the release, with the global `keystate` set to `"down"` / `"up"` — which -//! is why every shipped `runOnUp` body is an `if ( keystate == "down" )` fork. A binding without -//! it runs once, on the press. (Our own registry says the same thing a second way: -//! `benilla_app::bindings::commands::Kind::EdgeUpDown` holds the two halves as two strings, -//! because a host command's halves are ours to write. An addon's is one string and a global.) -//! - **`header` is a GLOBAL-STRING KEY, and it opens a SECTION.** `header="MOVEMENT"` means the -//! category `BINDING_HEADER_MOVEMENT`, and every following binding *without* a `header` belongs -//! to it: 13 headers over 228 bindings is 13 sections, and the client's own list is flat with -//! `HEADER_*` pseudo-entries in it (`Blizzard_BindingUI.lua:87` tests -//! `strsub(commandName, 1, 6) == "HEADER"`). The carry-forward is applied at *registration*, not -//! here, so this type stays a faithful record of what the file says — see -//! [`crate::script::UiScript::register_addon_bindings`]. -//! - **`hidden="true"` keeps a binding out of the Key Bindings window** while leaving it bindable -//! and dispatchable — 1.12 uses it for the debug toggles and for the three mouselook bindings -//! (`TURNORACTION`, `CAMERAORSELECTORMOVE`, …) that the mouse owns rather than the player. -//! - **`platform="mac"` is the file's own OS gate**, and its five uses are the whole of it: the -//! `ITUNES_REMOTE` block, which remotes the *system* music player and therefore exists only in -//! the Mac build. It is carried on [`AddonBinding::platform`] and acted on at registration -//! ([`crate::script::UiScript::register_addon_bindings`]) — a row for another platform is not -//! registered at all, so it never reaches the Key Bindings window, a save file, or a chord. -//! Registering it instead would list a command whose body calls functions this build does not -//! have, which is a row that can only ever error on its first press. -//! - `debug="true"` is **read and not acted on**: it appears only on the client's own nine -//! hidden dev toggles, always together with `hidden="true"` (which IS acted on and is what -//! keeps them out of the window), and no addon in the wild uses it. Recording that it exists is -//! the point; inventing a second meaning for it would be a guess. -//! -//! ## Why this lives beside `toc` and `framexml` rather than under `script/` -//! -//! It is the third of the client's **file formats**, and the crate's top level is where a format -//! becomes owned data with no VM in sight: [`crate::toc`] turns manifest text into a load list, -//! [`crate::framexml`] turns document text into a tree, and this turns binding text into -//! [`AddonBinding`]s. `script/` is the other side of that line — the live Lua-facing runtime, -//! which *consumes* what these three produce. A parser under `script/keybind` would also be -//! unreachable from the host's own load path without going through the VM, which is exactly the -//! shape 1186 spent a decision untangling. -//! -//! XML *parsing* is delegated plumbing here for the same reason it is in [`crate::framexml`] — -//! `roxmltree` reads the bytes, and only the schema→meaning map below is ours. +//! A body is one Lua chunk; with `runOnUp="true"` it runs on press and again on release, the +//! global `keystate` set to `"down"` or `"up"`. `header="X"` opens the section +//! `BINDING_HEADER_X` that following header-less bindings join, in a flat list with `HEADER` +//! pseudo-entries (`Blizzard_BindingUI.lua:87`). `hidden="true"` keeps a binding bindable but out +//! of the Key Bindings window. `platform="mac"` marks the `ITUNES_REMOTE` rows; the reference +//! skips another platform's row at load (`0x4b70c3`–`0x4b70e5`), benilla at registration. use std::fmt; -/// One `` element, exactly as the file states it. -/// -/// Deliberately a *record of the file*, not of the registered binding: `header` is the raw -/// attribute (`None` when the element carries none, which is 221 of the reference's 234), and the -/// section carry-forward and the `BINDING_HEADER_` prefixing happen where the addon's identity is -/// known — [`crate::script::UiScript::register_addon_bindings`]. (`None` for 215 of the -/// reference's 228 rows, which is what makes the carry-forward the whole story rather than a -/// detail.) +/// One `` element as the file states it; the section carry-forward and the +/// `BINDING_HEADER_` prefix are applied by [`crate::script::UiScript::register_addon_bindings`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AddonBinding { - /// The `name` attribute — the command name `SetBinding`/`GetBindingKey` speak in. + /// The command name `SetBinding` and `GetBindingKey` speak in. pub name: String, - /// The `header` attribute's global-string *suffix* (`MOVEMENT` → `BINDING_HEADER_MOVEMENT`), - /// when this element opens a section. + /// The raw `header` suffix (`MOVEMENT` of `BINDING_HEADER_MOVEMENT`), opening a section. pub header: Option, - /// `runOnUp="true"` — the body runs on the press **and** on the release (see the module doc). + /// `runOnUp="true"`: the body runs on press and on release. pub run_on_up: bool, - /// `hidden="true"` — bindable and dispatchable, but not listed in the Key Bindings window. + /// `hidden="true"`: bindable, but not listed in the Key Bindings window. pub hidden: bool, - /// `platform="mac"` — the row belongs to one build only, lower-cased. `None` on every row - /// that carries no such attribute, which is every row but five. + /// `platform=`, lower-cased: the one build the row belongs to. pub platform: Option, - /// The element's own text: one Lua chunk, verbatim (entities already decoded, so a body that - /// wrote `<` arrives as `<`). + /// The element's own text, one Lua chunk, entities decoded (`<` arrives as `<`). pub body: String, } -/// A malformed document. Everything softer — an element with no `name`, an unknown attribute, an -/// unexpected root — is tolerated the way the real loader tolerates it; only bytes that are not -/// XML stop the file. +/// Bytes that are not XML. A nameless element, an unknown attribute or an unexpected root is +/// tolerated, as the reference's loader tolerates them. #[derive(Debug)] pub enum Error { Xml(roxmltree::Error), @@ -123,23 +53,13 @@ impl std::error::Error for Error { } } -/// Parse `Bindings.xml` text into its bindings, in document order. -/// -/// **Every `` under the root**, at any depth and matched case-insensitively like every -/// other element compare in this crate (`0x64a4c0`, see [`crate::framexml`]'s header). The shipped -/// root is `` and the elements are its direct children, but a descendant walk costs -/// nothing and cannot over-collect — a `` never nests — while it does keep an addon that -/// wraps its bindings in a `` root (or ships them inside a `` inside one) working. -/// -/// **A `` with no `name` is skipped**: there is nothing for `SetBinding` to name and -/// nothing for the reference's record to key on. Defensive rather than observed — all 228 of the -/// client's own are named. +/// Parse `Bindings.xml` text into its bindings, in document order: every `` at any +/// depth, the tag matched case-insensitively (`0x64a4c0`), skipping one with no `name`. The +/// reference reads only the root's children (`0x4b701f`) and also skips a row with an empty body +/// (`0x4b7093`–`0x4b709e`) or `debug="true"` (`0x4b70a4`–`0x4b70bd`), which this keeps. pub fn parse(text: &str) -> Result, Error> { - // Namespace-oblivious, like the FrameXML document layer beside it and for the same reason - // (decision 2155; `framexml::parse`'s comment is the mechanism): a `Bindings.xml` is read by - // the same `XMLTree.cpp` tree the client builds with expat's `XML_ParserCreate`, which has no - // namespace processing at all — so an undeclared prefix cannot be an error there, and must not - // cost the whole file here. + // The reference's expat has no namespace processing, so an undeclared prefix is bound and the + // text re-read rather than costing the file, as in `framexml::parse`. let repaired; let doc = match roxmltree::Document::parse(text) { Ok(doc) => doc, @@ -171,25 +91,21 @@ pub fn parse(text: &str) -> Result, Error> { Ok(out) } -/// Case-insensitive attribute lookup — Blizzard's own XML is inconsistent about attribute casing -/// and the real loader's `GetAttribute 0x6f2cf0` folds case ([`crate::framexml`]). +/// Case-insensitive attribute lookup, as the reference's `GetAttribute 0x6f2cf0` folds case. fn attr_ci(node: roxmltree::Node, name: &str) -> Option { node.attributes() .find(|a| a.name().eq_ignore_ascii_case(name)) .map(|a| a.value().to_string()) } -/// A boolean attribute: true **iff** present and equal to the literal `"true"`, case-insensitively -/// — the client's own rule (`0x6f1b30`'s true-cmp; there is no `"false"` branch, so `runOnUp="1"` -/// is false exactly as it is in the real loader). +/// True only for a case-insensitive `"true"` (`0x6f1b30` has no `"false"` branch), so +/// `runOnUp="1"` is false. fn attr_bool(node: roxmltree::Node, name: &str) -> bool { attr_ci(node, name).is_some_and(|v| v.eq_ignore_ascii_case("true")) } -/// The node's own direct text/CDATA children, concatenated — the binding's Lua chunk. Does not -/// descend into child elements, and so drops the `` comments the shipped file is full -/// of. Concatenation (rather than the first text node) is what keeps a body containing an entity -/// whole: `a < b` arrives from `roxmltree` as three text nodes. +/// The node's direct text and CDATA, concatenated: `roxmltree` splits `a < b` into three text +/// nodes. fn direct_text(node: roxmltree::Node) -> String { node.children() .filter(|n| n.is_text()) @@ -201,15 +117,6 @@ fn direct_text(node: roxmltree::Node) -> String { mod tests { use super::*; - /// **The reference's own shape**, element for element: the `runOnUp` + `header` opener with a - /// multi-line `keystate` fork, a plain one-shot binding, a `hidden` debug toggle, and — the - /// one case a hand-written addon file actually hits — an entity inside a body. - /// - /// What this catches: a parser that reads only the first text node (the `<` body comes back - /// truncated at the entity), one that treats `runOnUp="1"`/absent as true, one that swallows - /// the newlines a Lua chunk needs to stay a Lua chunk, and one that hands back the `header` - /// already prefixed (the prefixing is registration's job — a doubled - /// `BINDING_HEADER_BINDING_HEADER_MOVEMENT` is the failure that would follow). #[test] fn the_reference_shape_parses_attribute_for_attribute() { let binds = parse( @@ -270,8 +177,7 @@ mod tests { "the body is a Lua chunk — its newlines are load-bearing" ); - // A binding with no header of its own carries none: the section it belongs to is a - // property of the LIST, resolved at registration. + // Section membership is resolved at registration, not here. assert_eq!(binds[1].name, "JUMP"); assert_eq!(binds[1].header, None); assert!(!binds[1].run_on_up); @@ -279,23 +185,18 @@ mod tests { assert!(binds[2].hidden, "hidden=\"true\" is recorded, not dropped"); assert!(!binds[2].run_on_up); - // Attribute name AND value fold case (the loader's own compares do); the entity is - // decoded and the body around it survives whole. + // Attribute names and values fold case; the entity decodes and the body stays whole. assert!(binds[3].run_on_up, "RUNONUP=\"TRUE\" is runOnUp"); assert_eq!(binds[3].body.trim(), "if ( a < b ) then Probe(); end"); - // ...but only the literal `true` is true — there is no `"false"` branch in the client's - // parse, so `"1"` is simply not it. assert!( !binds[4].run_on_up, "runOnUp=\"1\" is false: the client's bool compares against \"true\" alone" ); } - /// A nameless `` is skipped rather than registered under `""` — where it would eat - /// `GetBindingAction`'s "no command is bound to this key" answer, which IS the empty string. - /// A `` nested under a wrapper element still counts, so an addon that wraps its - /// bindings does not silently lose them. + /// Registered under `""`, a nameless binding would shadow `GetBindingAction`'s empty answer + /// for an unbound key. #[test] fn nameless_bindings_are_skipped_and_nesting_is_tolerated() { let binds = parse( @@ -312,11 +213,6 @@ mod tests { assert_eq!(binds[0].name, "PROBEWRAPPED"); } - /// **A commented-out `` is not a binding** — and this is not a hypothetical: the - /// client's own file carries the entire `MOVEVIEW*` family (six bindings) inside one XML - /// comment, which is the whole gap between the 234 elements a text search finds and the 228 - /// the loader sees. Registering them would put six commands in the Key Bindings window that - /// the client does not have, each one a key a player could bind to nothing at all. #[test] fn a_commented_out_binding_is_not_a_binding() { let binds = parse( @@ -339,9 +235,6 @@ mod tests { ); } - /// Bytes that are not XML stop the file — the one hard error. The addon's other files have - /// already run by then (`Bindings.xml` loads after them, `0x51f400`), so the load path reports - /// this and carries on; what it must not do is silently register nothing. #[test] fn malformed_xml_is_an_error() { let e = parse("").expect_err("malformed"); diff --git a/crates/benilla-ui/src/civil.rs b/crates/benilla-ui/src/civil.rs index c050a795..a919a1a7 100644 --- a/crates/benilla-ui/src/civil.rs +++ b/crates/benilla-ui/src/civil.rs @@ -1,14 +1,6 @@ -//! **The tree's one civil-time conversion** — broken-down UTC from an epoch second. -//! -//! It lives here because this crate already had to have one: 1.12's `date()` global is Lua 5.0's -//! `os.date` hoisted, and the sandbox strips `os` ([`crate::script`]'s stdlib), so the calendar -//! had to be written by hand. The screenshot writer is its second caller (decision 1487), and one -//! correct closed form with two callers beats two hand-rolled loops. -//! -//! **UTC, deliberately, and it is a stated divergence wherever a caller wanted local time.** The -//! workspace has no date dependency at all — every format crate in it is in-repo — and resolving a -//! local offset needs a timezone source, not a different algorithm. `date()` records the same -//! divergence for the same reason. +//! Broken-down UTC time from an epoch second, the calendar behind `date()` and screenshot names. +//! Deviation: UTC where the reference's callers use local time, because the workspace has no +//! timezone source. /// Broken-down UTC time. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -27,12 +19,8 @@ pub struct Civil { pub yearday: u32, } -/// Break an epoch second down into UTC fields. -/// -/// The civil-from-days conversion is Howard Hinnant's, shifted to a March-based year so the leap -/// day lands at the end and no month-length table is needed. Chosen over a hand-rolled loop -/// because it is a known-correct closed form with no accumulation error, which matters for the -/// "persisted last session, compared this session" use every corpus `date()` caller has. +/// Break an epoch second down into UTC fields, by Howard Hinnant's civil-from-days: years count +/// from month 3, so the leap day falls last and no month-length table is needed. pub fn from_unix(secs: i64) -> Civil { let days = secs.div_euclid(86_400); let rem = secs.rem_euclid(86_400); @@ -55,7 +43,6 @@ pub fn from_unix(secs: i64) -> Civil { let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32; let year = if month <= 2 { y + 1 } else { y }; - // Day of the year, from the same conversion applied to Jan 1. let yearday = (days - days_from_civil(year, 1, 1) + 1) as u32; Civil { year, @@ -69,7 +56,6 @@ pub fn from_unix(secs: i64) -> Civil { } } -/// Days since the epoch for a civil date — the inverse of the above, used only for `yearday`. fn days_from_civil(y: i64, m: u32, d: u32) -> i64 { let y = if m <= 2 { y - 1 } else { y }; let era = y.div_euclid(400); @@ -80,8 +66,7 @@ fn days_from_civil(y: i64, m: u32, d: u32) -> i64 { era * 146_097 + doe - 719_468 } -/// Wall-clock seconds since the Unix epoch. Before the epoch is not a state a game client is in; a -/// clock that somehow reports it clamps to 0 rather than raising inside an addon's file scope. +/// Wall-clock seconds since the Unix epoch; a clock set before 1970 reads 0 rather than raising. pub fn unix_seconds() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -93,8 +78,7 @@ pub fn unix_seconds() -> i64 { mod tests { use super::*; - /// Four instants that between them cover the epoch itself, a leap day, a century boundary and - /// an ordinary afternoon — the cases a closed-form conversion gets wrong when it is wrong. + /// The epoch, an ordinary afternoon, a leap day and the eve of 2000. #[test] fn known_instants_break_down() { let cases = [ @@ -121,8 +105,7 @@ mod tests { } } - /// Before 1970 is not a state a client is in, but the arithmetic must not wrap into nonsense - /// if a clock reports it — `div_euclid`/`rem_euclid` are load-bearing here, not stylistic. + /// `div_euclid`/`rem_euclid` keep a second before 1970 from wrapping into nonsense. #[test] fn negative_seconds_stay_civil() { assert_eq!( diff --git a/crates/benilla-ui/src/framexml.rs b/crates/benilla-ui/src/framexml.rs index 7720333f..957e8e3a 100644 --- a/crates/benilla-ui/src/framexml.rs +++ b/crates/benilla-ui/src/framexml.rs @@ -1,44 +1,25 @@ -//! The FrameXML **document** layer (decision 0068): turns a `.xml` file's text into an owned, -//! order-preserving tree, resolves `virtual`/`inherits` template expansion, and rewrites `$parent` -//! name tokens. This is XML-tree semantics only — no widget instantiation (frame factories, anchor -//! resolution, script compilation) and no Lua; those live in the layer above, which walks the tree -//! this module produces. -//! -//! Ground truth is the real client's loader (`0x6edc00–0x6f3000`): -//! - the file loader `0x6ede10` — the schema → frame-op interpretation, the full top-level + -//! per-element op map -//! - `LoadChildFrames 0x76a060` — `` / load-order (widget-layer concern; cited -//! here only for the "spliced before the instance's own nodes" ordering that also governs template -//! `` children, which this module's generic child-concat already gets right) -//! - `0x76c5b0` — the `$parent` substitution rule -//! -//! XML *parsing* is delegated plumbing in the reference too (an embedded expat); the schema→op map -//! is its own — we use `roxmltree` rather than reproduce a tokenizer; only the schema→op -//! interpretation below is transcribed from the reference. +//! The FrameXML document layer: a `.xml` file's text as an owned, order-preserving tree, with +//! `inherits` template expansion and `$parent` name substitution; no widgets and no Lua. It +//! follows the reference's loader (`0x6edc00`–`0x6f3000`, the file loader `0x6ede10`), whose XML +//! tokenizer is an embedded expat and ours `roxmltree`. use std::collections::{HashMap, HashSet}; use std::fmt; -/// An owned XML element: tag, attributes (order preserved — Blizzard XML attribute order is -/// sometimes meaningful for authoring tools, and always useful for round-tripping/debugging), and -/// element children. Case-insensitive attribute lookup: "attrs via `GetAttribute 0x6f2cf0`" and -/// element-name compares are case-insensitive in the real loader (`0x64a4c0`), and Blizzard's own -/// shipped XML is inconsistent about attribute casing (e.g. both `relativeTo` and stray -/// all-lowercase variants appear in the wild), so lookups here fold case rather than assume a -/// canonical spelling. +/// An owned XML element, attributes in document order. Lookups fold case, as the reference's +/// `GetAttribute 0x6f2cf0` and element-name compares (`0x64a4c0`) do. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Element { pub tag: String, attrs: Vec<(String, String)>, pub children: Vec, - /// The node's own direct text content (the XML node struct's `body-text @ +0xc`). Populated for - /// text-bearing elements such as `` handler bodies (`lua…`) and - /// inline ``). Both run in document order relative to everything else at the top -/// level (`0x704bc0` / `0x704cd0`, from `0x6ede10`'s walk — this is how XML-referenced FrameXML Lua -/// loads). +/// A `` with no text at all. +/// The 1-based line of the first text byte inside a ` assert!(doc.warnings.is_empty()); } - /// `inherits="A, B"` is **one name**, not a list — and it misses. - /// - /// This test used to assert the opposite (that both templates merged, children concatenated in - /// order). That was a superset of 1.12 borrowed from later clients: there is no splitter - /// anywhere in the loader — all 8 call sites hand `0x6ee6f0` the pointer `GetAttribute` - /// returned, one or two instructions later, and no comma is examined in `0x6ed000–0x6f6000` at - /// all. So the lookup runs **once**, for the literal name `"A, B"`, misses, warns at - /// severity 1, and the element loads with **neither** template applied. - /// - /// Whitespace is never trimmed either, which is why the second half asserts `" A "` misses a - /// template that genuinely exists. + /// All eight callers of `0x6ee6f0` pass `GetAttribute`'s pointer as is, and nothing in + /// `0x6ed000`–`0x6f6000` examines a comma; the value is not trimmed either. #[test] fn a_comma_list_is_one_literal_name_that_misses_and_applies_neither_template() { let doc = parse( @@ -867,14 +658,13 @@ print(x) let mut warnings = Vec::new(); let expanded = expand(inst, &templates, &mut warnings); - // Warned, naming the whole unsplit string — not split, not silently ignored. assert_eq!(warnings.len(), 1, "{warnings:?}"); assert!( warnings[0].contains("A, B"), "the warning must name the literal it looked up: {warnings:?}" ); - // The element still loads, with all of its OWN content and none of the templates'. + // The element loads with its own content only. assert_eq!(expanded.attr("alpha"), Some("1.0")); assert_eq!(expanded.children.len(), 1); fn texture_name(el: &Element) -> &str { @@ -882,7 +672,7 @@ print(x) } assert_eq!(texture_name(&expanded.children[0]), "FromInst"); - // And a padded name misses a template that exists — the value is used verbatim. + // A padded name misses: the value is used verbatim. let TopLevel::Instance(padded) = &doc.items[3] else { panic!("expected instance") }; @@ -916,8 +706,7 @@ print(x) let mut warnings = Vec::new(); let expanded = expand(leaf, &templates, &mut warnings); assert!(warnings.is_empty()); - // Mid's own frameStrata (it overrode Root's) wins, since chain-expanding Mid resolves Root - // first and Mid's own attrs on top, before Leaf (which sets nothing) merges on top of that. + // Mid's frameStrata overrides Root's, and Leaf sets none. assert_eq!(expanded.attr("frameStrata"), Some("MEDIUM")); assert_eq!( expanded.children.len(), @@ -941,7 +730,6 @@ print(x) panic!("expected instance") }; let mut warnings = Vec::new(); - // Must terminate (not infinitely recurse) and report the cycle. let _expanded = expand(inst, &templates, &mut warnings); assert!( warnings.iter().any(|w| w.contains("cycle")), @@ -964,14 +752,13 @@ print(x) resolve_name("NotAParentName", "PlayerFrame"), "NotAParentName" ); - // No named ancestor: caller passes the literal "Top" fallback. + // No named ancestor: the caller passes the literal "Top". assert_eq!(resolve_name("$parentFoo", DEFAULT_PARENT_NAME), "TopFoo"); } #[test] fn parent_name_substitution_composes_through_nested_children() { - // A named parent, a child using $parent, and a grandchild using $parent against the - // child's *resolved* name — not the child's raw literal "$parent..." text. + // The grandchild resolves against the child's resolved name, not its raw "$parent…" text. let parent_name = "PlayerFrame"; let child_name = resolve_name("$parentHealthBar", parent_name); assert_eq!(child_name, "PlayerFrameHealthBar"); diff --git a/crates/benilla-ui/src/justify.rs b/crates/benilla-ui/src/justify.rs index bce2e153..a2a2255c 100644 --- a/crates/benilla-ui/src/justify.rs +++ b/crates/benilla-ui/src/justify.rs @@ -1,56 +1,11 @@ -//! The justify enum — **one** transcription of `.rdata 0x811ad0`, shared by every table that -//! speaks it. -//! -//! ## Why this is a module and not three sets of `match` arms -//! -//! Justification is set on **three** widget tables — a `FontString` (`0x87c1d8`), a `` -//! object (`0x87c7c8`) and an `EditBox` — and each had been transcribed *independently* from the -//! same byte law. Each drifted its own way, and each looked right alone: -//! -//! | | FontString ([`crate::script::region`]) | Font ([`crate::script::font`]) | EditBox ([`crate::widget::kinds::editbox`]) | the bytes | -//! |---|---|---|---|---| -//! | `GetJustifyH`/`V` | **absent** | present | present | present (`0x79e5f0`/`0x79e7f0`) | -//! | space-padded token | rejected | `.trim()`ed, **accepted** | rejected | rejected (whole-string) | -//! | unmatched string | silently `CENTER` | silently `CENTER` | **raises** | **raises** | -//! | cross-axis token | silently `CENTER` | silently `CENTER` | **clears the axis** | clears the axis | -//! | axis storage | resolved enum → **the dword** (1239) | resolved enum | **the raw dword** | one dword | -//! -//! That is the seam class the addon-lane round trip named: halves transcribed separately from one -//! law drift where they meet, while every test on every side still passes. **The EditBox column is -//! the one that was right** — written last, straight from the bytes, and never consulted by the two -//! older tables. So this module is not new law: it is that column, lifted to where all three reach -//! it. It sits at the crate root rather than under `script` because `widget::kinds` needs it too. -//! -//! ## The law -//! -//! Storage is a single dword — `CSimpleFont+0x54`, `CSimpleFontString+0x120` — with **bits 0–2 -//! horizontal and bits 3–5 vertical**. `SetJustifyH 0x79fc20` computes `(cur ^ parsed) & 0x07 ^ -//! cur`, i.e. `(cur & !0x07) | (parsed & 0x07)`, replacing only its own axis (VERIFIED at -//! `0x79fc5d`–`0x79fc64`); `SetJustifyV 0x79fce0` uses mask `0x38`. The ctor default is `0x212` = -//! `CENTER | MIDDLE | 0x200`, bit 9 being outside both masks and read by neither accessor. -//! -//! Two consequences fall out of that arithmetic, and both are transcribed below: -//! -//! - **A cross-axis token is accepted and clears.** `SetJustifyH("TOP")` parses fine (`0x08`) but -//! `0x08 & 0x07 == 0`, so it erases the horizontal justification with **no error raised**, and a -//! following `GetJustifyH()` answers `"UNKNOWN"` — while the glyphs keep drawing **centred**. -//! The two readers genuinely disagree, which is why [`Justify`] stores the dword rather than a -//! resolved enum: [`Justify::name_h`] is the getter's answer and [`Justify::paint_h`] is the -//! draw path's, and they differ only in this state. -//! - **A non-token raises.** `0x6f1990` scans all six entries with `SStrCmpI` — whole-string, -//! case-insensitive, **no trimming** — and returns 0 on a miss, which is the caller's cue to -//! raise `Usage: %s:SetJustifyH("justify")` (`.rdata 0x87c77c`) / `…SetJustifyV…` -//! (`0x87c7a0`). Per `script::binding_abi`, that arm does not return: it abandons the caller's -//! statement. -//! -//! The setter also clears the inherit bit unconditionally, whether or not the axis value changed -//! (`0x79fc7c`). +//! The justify dword and its token table (`.rdata 0x811ad0`), shared by the `FontString` +//! (`0x87c1d8`), `Font` (`0x87c7c8`) and `EditBox` method tables. Each setter replaces only its +//! own axis, and also clears the inherit bit whether or not the value changed (`0x79fc7c`). use crate::script::{JustifyH, JustifyV}; -/// `.rdata 0x811ad0` — six `{u32 bits, const char* name}` entries, in image order. The order is -/// load-bearing twice over: `0x6f1990` scans it linearly, and `0x6f1a00` answers with the **first** -/// entry whose bit is set. +/// `.rdata 0x811ad0`, six `{u32 bits, const char* name}` entries in image order. The order +/// matters: `0x6f1990` scans linearly and `0x6f1a00` answers the first entry with its bit set. const TOKENS: [(u32, &str); 6] = [ (0x01, "LEFT"), (0x02, "CENTER"), @@ -63,43 +18,30 @@ const TOKENS: [(u32, &str); 6] = [ /// The literal `0x6f1a00` answers when no bit in the requested axis is set (`.data 0x838044`). pub const UNKNOWN: &str = "UNKNOWN"; -/// The horizontal axis — bits 0–2, `SetJustifyH`'s mask. +/// The horizontal axis, bits 0–2: the mask of `SetJustifyH 0x79fc20`. pub const H_MASK: u32 = 0x07; -/// The vertical axis — bits 3–5, `SetJustifyV`'s mask. +/// The vertical axis, bits 3–5: the mask of `SetJustifyV 0x79fce0`. pub const V_MASK: u32 = 0x38; -/// What a justify argument turned out to be — the three outcomes of `0x6f1990` followed by the -/// setter's mask. +/// A justify argument after `0x6f1990` and the setter's mask. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Set { - /// A token carrying a bit on *this* axis: the new justification. + /// A token with a bit on this axis. To(J), - /// A token that matched the table but carries no bit on this axis — `SetJustifyH("TOP")`. - /// The reference **clears** the axis and raises nothing; `GetJustifyH()` then answers - /// `"UNKNOWN"` while the glyphs keep drawing centred ([`Justify::paint_h`]). - /// - /// Not hypothetical: 13 corpus sites call `SetJustifyV("CENTER")` meaning "middle" - /// (`FonzAppraiser` ×12, `Roid-Macros`), which parses and then erases the vertical axis. + /// A token with no bit on this axis (`SetJustifyH("TOP")`): the reference clears the axis and + /// raises nothing; corpus addons call `SetJustifyV("CENTER")` and hit this. Clears, - /// No entry matched. The caller raises its own `Usage:` string; it does not coerce. + /// No entry matched: the caller raises its `Usage:` string, never coerces. NoMatch, } -/// The ctor default — `CENTER | MIDDLE | 0x200`. Bit 9 is outside both axis masks and is read by -/// neither accessor nor the gx translator. `CSimpleFontString`'s **own** ctor writes it at -/// `0x770dd3`, so a FontString that has never been told otherwise reads `CENTER`/`MIDDLE`. +/// `CENTER | MIDDLE | 0x200`, as `CSimpleFontString`'s ctor writes it (`0x770dd3`); bit 9 is in +/// neither axis and no reader looks at it. const CTOR_DEFAULT: u32 = 0x212; -/// The justify **dword** — `CSimpleFont+0x54`, `CSimpleFontString+0x120`, `CSimpleEditBox`'s own — -/// with bits 0–2 horizontal and bits 3–5 vertical. -/// -/// Stored as the dword rather than a pair of resolved enums because **an axis with no bit set is a -/// real, reachable state** and a resolved enum cannot hold it: `SetJustifyH("TOP")` parses `0x08`, -/// contributes nothing to mask `0x07`, and erases the axis with no error raised. -/// -/// The two readers then disagree, faithfully, and that is the whole reason this type exists: -/// [`Justify::name_h`] (the Lua getter) answers `"UNKNOWN"`, while [`Justify::paint_h`] (the draw -/// path) answers `CENTER`. Anything that collapses the two loses one of them. +/// The justify dword (`CSimpleFont+0x54`, `CSimpleFontString+0x120`, `CSimpleEditBox`'s own), +/// bits 0–2 horizontal and 3–5 vertical. Kept raw because an axis with no bit set is reachable, +/// and there the getter answers `"UNKNOWN"` while the draw path centres. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Justify(pub u32); @@ -110,11 +52,9 @@ impl Default for Justify { } impl Justify { - /// Replace the horizontal bits — `(cur & !0x07) | (parsed & 0x07)`. pub fn set_h(&mut self, j: JustifyH) { self.0 = set_axis(self.0, H_MASK, bits_h(j)); } - /// Replace the vertical bits — `(cur & !0x38) | (parsed & 0x38)`. pub fn set_v(&mut self, j: JustifyV) { self.0 = set_axis(self.0, V_MASK, bits_v(j)); } @@ -122,46 +62,36 @@ impl Justify { pub fn clear_h(&mut self) { self.0 = set_axis(self.0, H_MASK, 0); } - /// Erase the vertical axis. pub fn clear_v(&mut self) { self.0 = set_axis(self.0, V_MASK, 0); } - /// `GetJustifyH`'s answer — `0x6f1a00`, so `"UNKNOWN"` for a cleared axis. + /// `GetJustifyH`'s answer (`0x79e5f0`) through `0x6f1a00`: `"UNKNOWN"` for a cleared axis. pub fn name_h(self) -> &'static str { name_of(self.0, H_MASK) } - /// `GetJustifyV`'s answer. + /// `GetJustifyV`'s answer (`0x79e7f0`). pub fn name_v(self) -> &'static str { name_of(self.0, V_MASK) } - /// What the glyphs actually draw at horizontally — the ui→gx translator `0x44d420`, whose one - /// call site image-wide is `0x772693`. - /// - /// **It is a priority ladder, not a switch, and the fall-through is `CENTER` — not `LEFT`.** - /// Each axis's result register is pre-set to `1` *between* the `test` and the `jcc` - /// (`0x44d4fb mov ebx,0x1`, then `0x44d516 je`), so an all-clear axis exits with `1` still in - /// it, and `1` is bit-identical to CENTER. A cleared axis is therefore visually - /// indistinguishable from the ctor default. - /// - /// **The trap, quoted from the finding:** the gx layer's `else` arms genuinely *are* LEFT and - /// BOTTOM, but both are unreachable from a FontString — `0x44d420` emits only `{0,1,2}` and - /// `0x5c1c30` rejects `>= 3`. *Mapping the ui bitmask onto the gx enum with a `0` default - /// inverts both axes.* + /// Where the glyphs draw horizontally: the translator `0x44d420` (called only from + /// `0x772693`) is a ladder whose result is pre-set to `1`, CENTER (`0x44d4fb`, before the + /// `je` at `0x44d516`), so a cleared axis draws centred. The gx enum's `else` arms, LEFT and + /// BOTTOM, are unreachable (`0x44d420` emits only 0 to 2, `0x5c1c30` rejects 3 and up): + /// mapping the bitmask onto that enum with a `0` default inverts both axes. pub fn paint_h(self) -> JustifyH { if self.0 & 0x04 != 0 { JustifyH::Right } else if self.0 & 0x01 != 0 { JustifyH::Left } else { - JustifyH::Center // 0x02 set, or NOTHING set — the pre-set `1` + JustifyH::Center // 0x02 set, or no bit set: the pre-set `1` } } - /// What the glyphs draw at vertically — the same ladder, `TOP > BOTTOM > MIDDLE`, falling - /// through to `MIDDLE`. `0x44d536`'s redundant `mov esi,0x1` on the MIDDLE arm is MSVC's own - /// tell that `default:` and `MIDDLE` share a constant. + /// The same ladder vertically, `TOP > BOTTOM > MIDDLE`, falling through to MIDDLE + /// (`0x44d536`). pub fn paint_v(self) -> JustifyV { if self.0 & 0x08 != 0 { JustifyV::Top @@ -189,8 +119,7 @@ fn bits_v(j: JustifyV) -> u32 { } } -/// `0x6f1990` — a linear scan of all six entries with `SStrCmpI`: **whole-string, -/// case-insensitive, and without trimming**, so `"left "` is a miss. +/// `0x6f1990`: `SStrCmpI` over the six entries, whole-string and untrimmed, so `"left "` misses. pub fn parse_bits(s: &str) -> Option { TOKENS .iter() @@ -198,7 +127,7 @@ pub fn parse_bits(s: &str) -> Option { .map(|(bits, _)| *bits) } -/// `0x6f1a00` — the **first** entry whose bit is set within `mask`, else the literal `UNKNOWN`. +/// `0x6f1a00`: the first entry whose bit is set within `mask`, else the literal `UNKNOWN`. pub fn name_of(bits: u32, mask: u32) -> &'static str { TOKENS .iter() @@ -206,14 +135,13 @@ pub fn name_of(bits: u32, mask: u32) -> &'static str { .map_or(UNKNOWN, |(_, name)| *name) } -/// Replace one axis's bits with `parsed`'s, masked to that axis — `0x79fc5d`'s -/// `(cur ^ parsed) & mask ^ cur`, i.e. `(cur & !mask) | (parsed & mask)`. This is the operation -/// that makes a cross-axis token *clear* rather than raise. +/// The setters' `(cur ^ parsed) & mask ^ cur` (`SetJustifyH` at `0x79fc5d`–`0x79fc64`), which is +/// why a cross-axis token clears rather than raises. pub fn set_axis(cur: u32, mask: u32, parsed: u32) -> u32 { (cur & !mask) | (parsed & mask) } -/// `SetJustifyH`'s argument, resolved against mask `0x07`. +/// `SetJustifyH`'s argument, against mask `0x07`. pub fn parse_h(s: &str) -> Set { match parse_bits(s) { None => Set::NoMatch, @@ -224,7 +152,7 @@ pub fn parse_h(s: &str) -> Set { } } -/// `SetJustifyV`'s argument, resolved against mask `0x38`. +/// `SetJustifyV`'s argument, against mask `0x38`. pub fn parse_v(s: &str) -> Set { match parse_bits(s) { None => Set::NoMatch, @@ -235,18 +163,17 @@ pub fn parse_v(s: &str) -> Set { } } -/// `GetJustifyH`'s answer for a resolved horizontal justification, through the same table. +/// `GetJustifyH`'s answer for a resolved justification. pub fn name_h(j: JustifyH) -> &'static str { name_of(bits_h(j), H_MASK) } -/// `GetJustifyV`'s answer for a resolved vertical justification, through the same table. pub fn name_v(j: JustifyV) -> &'static str { name_of(bits_v(j), V_MASK) } -/// The `Usage:` string `SetJustifyH` raises on a miss (`.rdata 0x87c77c`). The `%s` is spelled as -/// the widget type, matching the shipped convention in `script::font_block`. +/// The `Usage:` string `SetJustifyH` raises on a miss (`.rdata 0x87c77c`), the `%s` spelled as +/// the widget type. pub fn usage_h(widget: &str) -> mlua::Error { mlua::Error::runtime(format!("Usage: <{widget}>:SetJustifyH(\"justify\")")) } @@ -265,8 +192,7 @@ mod tests { assert_eq!(parse_h("LEFT"), Set::To(JustifyH::Left)); assert_eq!(parse_h("left"), Set::To(JustifyH::Left)); assert_eq!(parse_h("LeFt"), Set::To(JustifyH::Left)); - // `SStrCmpI` compares the whole string; the client does not trim, and neither do we — - // the Font-object path used to, which is the divergence this module exists to end. + // `SStrCmpI` compares the whole string, untrimmed. assert_eq!(parse_h("left "), Set::NoMatch); assert_eq!(parse_h(" left"), Set::NoMatch); assert_eq!(parse_h("leftmost"), Set::NoMatch); @@ -275,8 +201,6 @@ mod tests { #[test] fn an_unmatched_string_does_not_coerce_to_center() { - // The whole point of the raise: the reference tells the addon it passed nonsense, where we - // used to silently answer CENTER on both tables. assert_eq!(parse_h("MIDDLE_LEFT"), Set::NoMatch); assert_eq!(parse_v("CENTERED"), Set::NoMatch); } @@ -303,15 +227,12 @@ mod tests { #[test] fn the_formatter_is_first_bit_set_within_the_axis_else_unknown() { - // `0x6f1a00`'s two documented edges, neither reachable through our resolved enums but both - // part of the law being transcribed: no bit in the axis answers the literal UNKNOWN, and a - // dword with two bits set answers the *first* table entry rather than the largest. + // `0x6f1a00`: no bit in the axis answers UNKNOWN, two bits answer the first table entry. assert_eq!(name_of(0x00, H_MASK), "UNKNOWN"); assert_eq!(name_of(0x38, H_MASK), "UNKNOWN"); // vertical bits only assert_eq!(name_of(0x05, H_MASK), "LEFT"); // LEFT | RIGHT assert_eq!(name_of(0x30, V_MASK), "MIDDLE"); // MIDDLE | BOTTOM - // The ctor default `0x212` reads CENTER/MIDDLE, and bit 9 is invisible to both axes. - assert_eq!(name_of(0x212, H_MASK), "CENTER"); + assert_eq!(name_of(0x212, H_MASK), "CENTER"); // the ctor default; bit 9 is in no axis assert_eq!(name_of(0x212, V_MASK), "MIDDLE"); } } diff --git a/crates/benilla-ui/src/layout.rs b/crates/benilla-ui/src/layout.rs index 8ec33152..8c68f058 100644 --- a/crates/benilla-ui/src/layout.rs +++ b/crates/benilla-ui/src/layout.rs @@ -1,66 +1,37 @@ -//! The frame anchor/layout resolver — anchor graph → resolved rects (decision 0068). +//! The frame anchor resolver: the 1.12 client's `CLayoutFrame` rect resolution, transcribed +//! function by function, with the reference's operations in the reference's order. //! -//! This is a faithful transcription of the WoW 1.12.1 client's `CLayoutFrame` geometry resolver, -//! reverse-engineered bit-exact against `WoW.exe`. The leaf float kernels and the -//! recursion-guarded per-edge composition are transcribed from the reference, function by function -//! and verified bit-exact. Matching the reference's operations and their order — not "improving" -//! the arithmetic — is the goal. +//! The client computes on the x87 at 53-bit precision, which `f64` matches, and narrows to `f32` +//! only at its stores (each sub-edge before a `combine_*`, each final edge, the `width * scale` +//! span), so the arithmetic here runs in `f64` with an explicit `as f32` at exactly those sites. //! -//! ## Precision — why `f64` intermediates, not naive `f32` -//! -//! The client is an x87 binary: every intermediate lives in an FPU register at PC_53 (53-bit -//! mantissa = IEEE `f64` precision) and is narrowed to `f32` *only* at the documented `fstp m32` -//! store sites (each sub-edge result before a `combine_*`; each final edge into the out-rect; the -//! `width·scale` span). A leaf like `(right + left) * 0.5 + xOff·scale` therefore rounds **once** -//! (at the final store), whereas doing every operation in `f32` would round at each step and -//! diverge. So "the client is f32" is true only at the storage boundaries: to be bit-faithful the -//! chain of `+`/`-`/`*0.5` runs in `f64` (host-exact — every operand is `f32`-derived, so each op is -//! exact in `f64`) with an explicit `as f32` at exactly the sites the reference pins. -//! -//! ## Coordinate space -//! -//! Rects are stored in the client's own convention: `[bottom, left, top, right]` (frame-absolute -//! `+0x64/+0x68/+0x6c/+0x70`; `GetRect 0x768320` and the Lua `GetBottom/Left/Top/Right` -//! readers). WoW UI space is **y-up**: `top > bottom`, the screen root's rect is the -//! physical screen (the anchor chain bottoms out at the `CSimpleTop` root), and `width = right − -//! left`, `height = top − bottom`. Coordinates are screen pixels throughout; `scale` (layoutScale = -//! effective scale, `0x76ac90`) scales only the per-anchor offsets and the -//! `width/height` span. +//! Rects are `[bottom, left, top, right]` (frame `+0x64..+0x70`, `GetRect` `0x768320`) in screen +//! pixels, y-up. use std::collections::VecDeque; -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Constants (objdump-read from `WoW.exe`) -// ───────────────────────────────────────────────────────────────────────────────────────────── +// ── Constants ──────────────────────────────────────────────────────────────────────────────── -/// UNSET coordinate sentinel: `+Infinity` (the `.data` slot `0xcf550c`, runtime-initialized from -/// `[0x81d56c] = 0x7f800000`). "coordinate is set" ⇔ `x != +Inf`. Intermediates carry -/// this in `f64`; the `f32` narrowing preserves it (`+Inf as f32 == +Inf`). +/// The unset-coordinate sentinel, `+Inf` (`0xcf550c`, set at runtime from `0x81d56c`). const UNSET: f64 = f64::INFINITY; -/// `OnSizeChanged` trigger ε — `_DAT_008029d4` = `0x34800000` ≈ 2.384e-7 (applied in -/// `ApplyRect 0x76b580`). `OnSizeChanged(width,height)` fires iff `|Δwidth| ≥ ε` OR -/// `|Δheight| ≥ ε` between the old and new resolved rect. See [`size_changed`]. +/// The least width or height change that fires `OnSizeChanged`: 2^-22, bits `0x34800000` at +/// `0x8029d4`, tested in `ApplyRect` (`0x76b580`). pub const SIZE_EPS: f64 = 2.384_185_791_015_625e-7; -/// Recompute change threshold ε — `_DAT_0080c5c8` = `0x3727c5ac` ≈ 9.96e-6 (applied in -/// `recompute 0x768d20`): an edge must move at least this far vs the cached rect for a re-store + -/// `ApplyRect`. Benilla owns its own caching (the caller's dirty gate); this const records the client's -/// documented threshold for fidelity work that needs it. +/// The least edge move against the cached rect for the client to re-store it and `ApplyRect`, 1e-5 +/// (`0x80c5c8`, in `0x768d20`). Recorded, not applied: the caller's own dirty gate decides. pub const RESOLVE_EPS: f64 = f32::from_bits(0x3727_c5ac) as f64; -/// point-id → X column (table `0x81c3b8`..): -/// `{0,3,6}` (LEFT column) → 0, `{1,4,7}` (center) → 1, `{2,5,8}` (RIGHT column) → 2. +/// Point id to X column: left 0, center 1, right 2. const X_COL: [u8; 9] = [0, 1, 2, 0, 1, 2, 0, 1, 2]; -/// point-id → Y row: `{0,1,2}` (TOP row) → 0, `{3,4,5}` (center) → 1, `{6,7,8}` (BOTTOM row) → 2. +/// Point id to Y row: top 0, center 1, bottom 2. const Y_ROW: [u8; 9] = [0, 0, 0, 1, 1, 1, 2, 2, 2]; -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Public types -// ───────────────────────────────────────────────────────────────────────────────────────────── +// ── Public types ───────────────────────────────────────────────────────────────────────────── -/// The nine anchor points, with the client's exact discriminants (static tables -/// `0x81c3b8–0x81c3fc`): the id is the slot index in the client's `anchorPoints[9]`. +/// The nine anchor points; the discriminant is the client's point id, the slot in its +/// `anchorPoints[9]` (tables at `0x81c3b8..0x81c3fc`). #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Point { @@ -76,13 +47,11 @@ pub enum Point { } impl Point { - /// The point-id (0..=8), which is also the `anchorPoints[9]` slot index. #[inline] pub const fn id(self) -> u8 { self as u8 } - /// Build a [`Point`] from its client id, `None` if out of range. pub const fn from_id(id: u8) -> Option { Some(match id { 0 => Point::TopLeft, @@ -99,11 +68,10 @@ impl Point { } } -/// An opaque handle to an anchor target. This crate does not own the frame arena; the caller maps -/// handles to frames / external rects. (In the client this is the `CAnchor+0xc relativeTo` pointer.) +/// An anchor target, which the caller maps to a frame or an external rect (`CAnchor+0xc`). pub type Handle = u32; -/// A resolved rect in the client's `[bottom, left, top, right]` convention (screen pixels, y-up). +/// A resolved rect, `[bottom, left, top, right]` in screen pixels, y-up. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Rect { pub bottom: f32, @@ -113,7 +81,6 @@ pub struct Rect { } impl Rect { - /// Construct from the four edges in client order. pub const fn new(bottom: f32, left: f32, top: f32, right: f32) -> Rect { Rect { bottom, @@ -123,51 +90,39 @@ impl Rect { } } - /// `[bottom, left, top, right]` — the client's cached-rect layout (`+0x64..0x70`). #[inline] pub const fn to_array(self) -> [f32; 4] { [self.bottom, self.left, self.top, self.right] } - /// From `[bottom, left, top, right]`. #[inline] pub const fn from_array(a: [f32; 4]) -> Rect { Rect::new(a[0], a[1], a[2], a[3]) } - /// `right − left` (`GetWidth` semantics). #[inline] pub fn width(self) -> f32 { self.right - self.left } - /// `top − bottom` (`GetHeight` semantics; y-up). #[inline] pub fn height(self) -> f32 { self.top - self.bottom } } -/// One anchor of a frame — a `CAnchor` record -/// (`{xOff@4, yOff@8, relativeTo@0xc, relativePoint@0x10}`). `point` is the point *on this frame* -/// being pinned (its `anchorPoints[9]` slot); `relative_point` is the point *on the target* it -/// pins to. +/// One anchor, a client `CAnchor` (`xOff +4`, `yOff +8`, `relativeTo +0xc`, `relativePoint +0x10`): +/// `point` on this frame pins to `relative_point` on the target. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Anchor { - /// The point on *this* frame that is being anchored (its `anchorPoints[9]` slot index). pub point: Point, - /// The target frame/region this anchor is relative to (`CAnchor+0xc`). pub relative_to: Handle, - /// The point on the *target* this anchor pins to (`CAnchor+0x10 relativePoint`). pub relative_point: Point, - /// X offset (`CAnchor+4`), scaled by the requesting frame's `scale`. pub x_off: f32, - /// Y offset (`CAnchor+8`), scaled by `scale`. pub y_off: f32, } impl Anchor { - /// Convenience constructor. pub fn new( point: Point, relative_to: Handle, @@ -185,28 +140,24 @@ impl Anchor { } } -/// A frame's full layout input: its anchors, its optional explicit size, its scale, and the -/// clamp-to-screen flag + extents. Mirrors the client's geometry subobject fields consulted by the -/// resolver: `anchorPoints[9]` (`G+0x28`), `width` -/// (`G+0x50`), `height` (`G+0x54`), `layoutScale` (`G+0x58`), flags bit4 = clamp (`G+0x3c`). +/// A frame's layout input, the geometry fields the client's resolver reads: `anchorPoints[9]` +/// (`G+0x4`), `width` (`G+0x50`), `height` (`G+0x54`), `layoutScale` (`G+0x58`) and the clamp flag +/// (bit 4 of `G+0x3c`). #[derive(Clone, Debug, PartialEq)] pub struct LayoutInput { - /// The frame's anchors. Placed into the nine `anchorPoints` slots by their `point`; if two share - /// a `point`, the *last* wins (the client's `SetPoint 0x767c70` overwrites the slot). + /// Slotted by `point`, the later of two on one point winning (`SetPoint`, `0x767c70`). pub anchors: Vec, - /// Explicit width (`G+0x50`); **`0.0` ⇒ derive from opposing anchors** (the no-explicit-size - /// sentinel `DAT_007ffd74`). + /// `0.0` is the no-size sentinel (`0x7ffd74`): the axis derives from the anchors. pub width: f32, - /// Explicit height (`G+0x54`); `0.0` ⇒ derive from opposing anchors. pub height: f32, - /// `layoutScale` = effective scale (`parentScale · ownScale`, ε-gated; `0x76ac90`); the - /// px↔coord multiplier applied to offsets and the size span. Default `1.0`. + /// The effective scale, parent times own (`0x76ac90`), applied only to the anchor offsets and + /// the size span. pub scale: f32, - /// Clamp the assembled rect into `[0, extent]` per axis (`G` flags bit4; `assemble 0x767a20`). + /// Clamp the rect into `[0, extent]` on each axis. pub clamp: bool, - /// Screen X extent, only consulted when `clamp` (client `0x41ae60` return, default `0.8`). + /// The clamp's X extent; the client's default is `0.8` (`0x41ae60`). pub extent_x: f32, - /// Screen Y extent, only consulted when `clamp` (client `0x41ae70` return, default `0.6`). + /// The clamp's Y extent; the client's default is `0.6` (`0x41ae70`). pub extent_y: f32, } @@ -236,31 +187,18 @@ impl LayoutInput { } } -/// The outcome of resolving one frame's rect. #[derive(Clone, Copy, Debug, PartialEq)] pub enum ResolveOutcome { - /// Every edge resolved → the cached rect `[bottom, left, top, right]` (`assemble` returned 1). Resolved(Rect), - /// At least one edge stayed `+Inf` — under-constrained (`assemble 0x767a20` returns 0, caller - /// sets flags bit3 "resolution-failed"). + /// An edge stayed `+Inf`: the client's `assemble` returns 0 and its caller sets flags bit 3. Unresolvable, } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Leaf float kernels — the owned-fidelity math. Each returns its coordinate in `f64` (the -// un-narrowed x87 `st0` observable); callers narrow to `f32` at the documented store sites. -// ───────────────────────────────────────────────────────────────────────────────────────────── +// ── Leaf kernels: each returns the un-narrowed `f64` ───────────────────────────────────────── -/// `combineEdge 0x767440(center, opp, span)`: resolve one edge from the opposite -/// edge, the axis center, and the signed `size·scale` span, in that fallback order. -/// -/// - opposite known and non-zero span → `opp + span` (edge = opposite ± size·scale); -/// - else center known: non-zero span → `span·0.5 + center` (edge = center ± half-size); -/// else opposite known → `(center − opp) + center` (= 2·center − opposite, the mirror); -/// - else → `+Inf` (unresolvable). -/// -/// The `+Inf`/`0.0` sentinels are compared with IEEE equality, matching the binary's `fcomp` -/// against the `.data` slots (a NaN operand is unordered ⇒ "not equal to the sentinel"). +/// `combineEdge` (`0x767440`): an edge from the opposite edge and the signed `size * scale` span, +/// else from the axis center. The sentinels compare by IEEE equality, as the client's `fcomp` +/// does, so a NaN never matches one. #[allow(clippy::float_cmp)] pub fn combine_edge(center: f32, opp: f32, span: f32) -> f64 { let (c, o, s) = (f64::from(center), f64::from(opp), f64::from(span)); @@ -278,12 +216,7 @@ pub fn combine_edge(center: f32, opp: f32, span: f32) -> f64 { UNSET } -/// `combineCenter 0x7672d0(lo, hi, span)`: resolve an axis center from the low edge, -/// the high edge, and the signed `size·scale` span. -/// -/// - low known: high known → `(lo+hi)·0.5`; else non-zero span → `span·0.5 + lo`; -/// - else high known and non-zero span → `hi − span·0.5`; -/// - else → `+Inf`. +/// `combineCenter` (`0x7672d0`): an axis center from the low and high edges and the signed span. #[allow(clippy::float_cmp)] pub fn combine_center(lo: f32, hi: f32, span: f32) -> f64 { let (l, h, s) = (f64::from(lo), f64::from(hi), f64::from(span)); @@ -301,16 +234,9 @@ pub fn combine_center(lo: f32, hi: f32, span: f32) -> f64 { UNSET } -/// `CAnchor::ResolveX 0x7a2f90(scale)`: resolve this anchor's X coordinate against its -/// `relativeTo`'s cached rect. -/// -/// `rect` = the target's cached rect `[bottom, left, top, right]` when `GetRect` succeeded (cache -/// valid), `None` when it failed ⇒ `+Inf`. `mirror` = the target's geometry-vtable `+0x24` -/// normalize predicate (RTL/local-origin): when set, X edges are translated so `left → 0`, -/// `right → right − left` (each stored back as `f32`, the binary's `fstp m32`). For an ordinary -/// frame the predicate returns 0, so `mirror` is `false` (`0x46ff60 = xor eax,eax; ret`). -/// `coord = xOff·scale + edge` for the relativePoint's column -/// (center = mid-X + xOff·scale). `rel_point > 8` ⇒ `+Inf`. +/// `CAnchor::ResolveX` (`0x7a2f90`): the anchor's X against its target's cached `rect`, which is +/// `None` when `GetRect` fails. `mirror` is the target's normalize predicate (vtable `+0x24`, 0 for +/// an ordinary frame at `0x46ff60`), which shifts the edges to start at 0, each stored as `f32`. pub fn anchor_resolve_x( scale: f32, x_off: f32, @@ -337,9 +263,7 @@ pub fn anchor_resolve_x( } } -/// `CAnchor::ResolveY 0x7a3070(scale)` — the Y twin of [`anchor_resolve_x`]. `mirror` -/// normalizes the Y edges (`bottom → 0`, `top → top − bottom`); `coord = yOff·scale + edge` for the -/// relativePoint's row (center = mid-Y + yOff·scale). `rect` = `[bottom, left, top, right]`. +/// `CAnchor::ResolveY` (`0x7a3070`), the Y counterpart of [`anchor_resolve_x`]. pub fn anchor_resolve_y( scale: f32, y_off: f32, @@ -366,21 +290,16 @@ pub fn anchor_resolve_y( } } -/// The assembled (and possibly clamped) rect + whether every edge resolved. +/// [`assemble_rect`]'s result: `ok` when every edge resolved. #[derive(Clone, Copy, Debug, PartialEq)] pub struct AssembleRect { pub ok: bool, - /// `[bottom, left, top, right]` (meaningful only when `ok`). + /// `[bottom, left, top, right]`, meaningful only when `ok`. pub rect: [f32; 4], } -/// `assemble 0x767a20`: given the four resolved edge coordinates `[bottom, left, top, -/// right]` (each `+Inf` ⇒ unresolvable), fail if any is `+Inf`; else optionally clamp into -/// `[0, extent]` per axis. -/// -/// Clamp (geom flags bit4): a low edge `< 0` shifts to 0 carrying the span to the high edge; a high -/// edge past the screen extent shifts back carrying to the low edge, high edge = extent. Each store -/// is narrowed to `f32`. +/// `assemble` (`0x767a20`): fails if any edge is `+Inf`, else with `clamp` (flags bit 4) shifts +/// the rect into `[0, extent]` on each axis, low edge first, keeping its size. #[allow(clippy::float_cmp)] pub fn assemble_rect(edges: [f32; 4], clamp: bool, extent_x: f32, extent_y: f32) -> AssembleRect { let [mut bottom, mut left, mut top, mut right] = edges; @@ -417,9 +336,8 @@ pub fn assemble_rect(edges: [f32; 4], clamp: bool, extent_x: f32, extent_y: f32) } } -/// `OnSizeChanged` fire test (`ApplyRect 0x76b580`): fires iff `|Δwidth| ≥ [`SIZE_EPS`]` -/// OR `|Δheight| ≥ SIZE_EPS`, with `width = right − left`, `height = top − bottom`. Deltas are `f64` -/// of the `f32` edges (the binary's `fsub` of the `fld`'d rect components). +/// `ApplyRect`'s `OnSizeChanged` test (`0x76b580`): width or height moved by at least +/// [`SIZE_EPS`], the deltas taken in `f64` from the `f32` edges. pub fn size_changed(old: Rect, new: Rect) -> bool { let dw = (f64::from(new.right) - f64::from(new.left)) - (f64::from(old.right) - f64::from(old.left)); @@ -428,15 +346,9 @@ pub fn size_changed(old: Rect, new: Rect) -> bool { dw.abs() >= SIZE_EPS || dh.abs() >= SIZE_EPS } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// The composed resolver — recursion-guarded per-edge sequencing over the leaves above, -// bit-exact over the binary's `assemble 0x767a20` → six per-edge resolvers → anchor scans → leaf -// kernels. Adds NO new arithmetic — only integer control flow + the documented `fstp m32` `f32` -// narrowing at each site. -// ───────────────────────────────────────────────────────────────────────────────────────────── +// ── The composed resolver: `assemble` over six recursion-guarded edge resolvers ────────────── -/// A resolved anchor for the internal edge tree: like [`Anchor`] but with the target's rect already -/// looked up (`None` ⇒ target had no valid rect ⇒ `GetRect`-fail leg). +/// An [`Anchor`] with its target's rect looked up, `None` where `GetRect` would fail. #[derive(Clone, Copy)] struct AnchorRel { x_off: f32, @@ -446,8 +358,6 @@ struct AnchorRel { mirror: bool, } -/// Internal per-edge resolver input: the nine `anchorPoints` slots (by point-id) with rects -/// resolved, plus the size/scale/clamp fields. struct EdgeInput { anchors: [Option; 9], width: f32, @@ -458,7 +368,7 @@ struct EdgeInput { extent_y: f32, } -// recursion-guard bits (the binary's `G+0x28` bitfield). +// The recursion-guard bits of the client's `G+0x28`. const G_LEFT: u32 = 0x01; const G_TOP: u32 = 0x02; const G_RIGHT: u32 = 0x04; @@ -466,7 +376,7 @@ const G_BOTTOM: u32 = 0x08; const G_XC: u32 = 0x10; const G_YC: u32 = 0x20; -// per-edge point-id scan tables (`.rdata` 0x81c3b8..0x81c3f4). +// The point ids each edge scans (`.rdata` 0x81c3b8..0x81c3f4). const IDS_LEFT: [usize; 3] = [0, 3, 6]; const IDS_RIGHT: [usize; 3] = [2, 5, 8]; const IDS_TOP: [usize; 3] = [0, 1, 2]; @@ -474,8 +384,7 @@ const IDS_BOTTOM: [usize; 3] = [6, 7, 8]; const IDS_XC: [usize; 3] = [1, 4, 7]; const IDS_YC: [usize; 3] = [3, 4, 5]; -/// `anchorScanX 0x7671a0`: over the edge's three point-ids, the first present anchor whose -/// `ResolveX` is non-`+Inf` wins; else `+Inf`. +/// `anchorScanX` (`0x7671a0`): the first of the edge's anchors whose X resolves, else `+Inf`. #[allow(clippy::float_cmp)] fn anchor_scan_x(ids: &[usize; 3], f: &EdgeInput) -> f64 { for &id in ids { @@ -489,7 +398,7 @@ fn anchor_scan_x(ids: &[usize; 3], f: &EdgeInput) -> f64 { UNSET } -/// `anchorScanY 0x7671f0` — the Y twin. +/// `anchorScanY` (`0x7671f0`). #[allow(clippy::float_cmp)] fn anchor_scan_y(ids: &[usize; 3], f: &EdgeInput) -> f64 { for &id in ids { @@ -503,18 +412,10 @@ fn anchor_scan_y(ids: &[usize; 3], f: &EdgeInput) -> f64 { UNSET } -/// `width·scale` / `height·scale` — the per-edge resolvers' shared span step (`LEFT 0x7673d0` et -/// al.): operands are `f32` fields, the product narrowed to `f32` at the `fstp [esp]` site, -/// negated for LEFT/BOTTOM. -/// -/// **`size` is not always the authored field, and this is the trap** (decision 1349): each -/// caller's `call [eax+0x1c]` is **virtual**. A plain frame lands on -/// `0x768420 fld [G+0x50]` — the flat read this signature assumes — but a `CSimpleTexture` lands on -/// `0x770720` and a `CSimpleFontString` on `0x772930`, both of which substitute a **content-derived** -/// extent when the authored value is exactly `0.0` (the texture's own texel size, one texel per -/// FrameXML unit; the string's measured text, floored at one unit). benilla feeds this the authored -/// number only, so a zero-authored *texture* axis yields a zero span here where the client yields a -/// real one. 1349 §4 carries the change and its scope; do not read this kernel as the whole law. +/// The edge resolvers' span, `size * scale` narrowed to `f32`, negated for LEFT and BOTTOM. The +/// client reads `size` through a virtual getter: a frame's is the authored field (`0x768420`), but +/// a texture (`0x770720`) and a font string (`0x772930`) replace an authored `0.0` with their +/// content's extent, so a region's [`LayoutInput`] must already carry that extent. fn size_span(size: f32, scale: f32, negate: bool) -> f32 { let span = (f64::from(size) * f64::from(scale)) as f32; if negate { @@ -524,7 +425,7 @@ fn size_span(size: f32, scale: f32, negate: bool) -> f32 { } } -/// `LEFT 0x7673d0` — direct anchor scan, else `combineEdge(Xcenter, RIGHT, −width·scale)`. +/// LEFT (`0x7673d0`): its anchor scan, else `combineEdge(Xcenter, RIGHT, -width * scale)`. #[allow(clippy::float_cmp)] fn resolve_left(f: &EdgeInput, guard: &mut u32) -> f64 { if *guard & G_LEFT != 0 { @@ -544,7 +445,7 @@ fn resolve_left(f: &EdgeInput, guard: &mut u32) -> f64 { r } -/// `RIGHT 0x767540` — direct anchor scan, else `combineEdge(Xcenter, LEFT, +width·scale)`. +/// RIGHT (`0x767540`): its anchor scan, else `combineEdge(Xcenter, LEFT, width * scale)`. #[allow(clippy::float_cmp)] fn resolve_right(f: &EdgeInput, guard: &mut u32) -> f64 { if *guard & G_RIGHT != 0 { @@ -564,7 +465,7 @@ fn resolve_right(f: &EdgeInput, guard: &mut u32) -> f64 { r } -/// `TOP 0x7674d0` — direct anchor scan, else `combineEdge(Ycenter, BOTTOM, +height·scale)`. +/// TOP (`0x7674d0`): its anchor scan, else `combineEdge(Ycenter, BOTTOM, height * scale)`. #[allow(clippy::float_cmp)] fn resolve_top(f: &EdgeInput, guard: &mut u32) -> f64 { if *guard & G_TOP != 0 { @@ -584,7 +485,7 @@ fn resolve_top(f: &EdgeInput, guard: &mut u32) -> f64 { r } -/// `BOTTOM 0x7675b0` — direct anchor scan, else `combineEdge(Ycenter, TOP, −height·scale)`. +/// BOTTOM (`0x7675b0`): its anchor scan, else `combineEdge(Ycenter, TOP, -height * scale)`. #[allow(clippy::float_cmp)] fn resolve_bottom(f: &EdgeInput, guard: &mut u32) -> f64 { if *guard & G_BOTTOM != 0 { @@ -604,7 +505,7 @@ fn resolve_bottom(f: &EdgeInput, guard: &mut u32) -> f64 { r } -/// `Xcenter 0x767260` — direct anchor scan, else `combineCenter(LEFT, RIGHT, +width·scale)`. +/// Xcenter (`0x767260`): its anchor scan, else `combineCenter(LEFT, RIGHT, width * scale)`. #[allow(clippy::float_cmp)] fn resolve_xcenter(f: &EdgeInput, guard: &mut u32) -> f64 { if *guard & G_XC != 0 { @@ -624,7 +525,7 @@ fn resolve_xcenter(f: &EdgeInput, guard: &mut u32) -> f64 { r } -/// `Ycenter 0x767360` — direct anchor scan, else `combineCenter(BOTTOM, TOP, +height·scale)`. +/// Ycenter (`0x767360`): its anchor scan, else `combineCenter(BOTTOM, TOP, height * scale)`. #[allow(clippy::float_cmp)] fn resolve_ycenter(f: &EdgeInput, guard: &mut u32) -> f64 { if *guard & G_YC != 0 { @@ -644,12 +545,8 @@ fn resolve_ycenter(f: &EdgeInput, guard: &mut u32) -> f64 { r } -/// `assemble 0x767a20` composed: resolve all four edges (each `fst`'d to the `f32` out-rect), -/// returning [`ResolveOutcome::Unresolvable`] if any stayed `+Inf`, else the assembled (and, when -/// `clamp`, screen-clamped) rect. The four top-level resolves share a fresh guard; the binary -/// clears each edge's bit on return, so order is immaterial — we keep the binary's LEFT→BOTTOM→ -/// RIGHT→TOP order. The `+Inf` test is on the un-narrowed `f64` edge (the binary's `fcomp` before -/// the `fst`). +/// `assemble` (`0x767a20`) composed, resolving LEFT, BOTTOM, RIGHT, TOP in the client's order; the +/// `+Inf` test reads the un-narrowed `f64` edge, as the client's `fcomp` precedes its `fst`. #[allow(clippy::float_cmp)] fn resolve_edges(f: &EdgeInput) -> ResolveOutcome { let mut guard = 0u32; @@ -665,13 +562,8 @@ fn resolve_edges(f: &EdgeInput) -> ResolveOutcome { ResolveOutcome::Resolved(Rect::from_array(a.rect)) } -/// Resolve one frame's rect. `target_rect` supplies each anchor target's already-resolved rect -/// (`None` ⇒ the target has no valid rect ⇒ that anchor contributes `+Inf`, the `GetRect`-fail -/// leg). Anchors are placed into the nine `anchorPoints` slots by their `point`; on a duplicate the -/// later anchor wins (the client's `SetPoint` overwrite). -/// -/// This is the fidelity surface: it builds the client's `anchorPoints[9]` view and runs the -/// composed `assemble 0x767a20`. +/// Resolve one frame's rect; `target_rect` gives each anchor target's resolved rect, and `None` +/// makes that anchor `+Inf`, as a failed `GetRect` does. pub fn resolve_rect( frame: &LayoutInput, target_rect: impl FnMut(Handle) -> Option, @@ -679,9 +571,6 @@ pub fn resolve_rect( resolve_edges(&build_edge_input(frame, target_rect)) } -/// Build the client's `anchorPoints[9]` view + size/scale/clamp fields for `frame`, resolving each -/// anchor target's rect through `target_rect`. Shared by [`resolve_rect`] (all-or-nothing) and -/// [`resolve_rect_edges`] (per-edge). fn build_edge_input( frame: &LayoutInput, mut target_rect: impl FnMut(Handle) -> Option, @@ -693,8 +582,7 @@ fn build_edge_input( y_off: a.y_off, rel_point: a.relative_point.id() as u32, rel_rect: target_rect(a.relative_to).map(Rect::to_array), - // Ordinary frames' normalize predicate returns 0 (`0x46ff60`); RTL/mirror - // frames are not modeled here (the leaf `anchor_resolve_*` covers that leg). + // An ordinary frame's normalize predicate returns 0 (`0x46ff60`). mirror: false, }); } @@ -709,12 +597,8 @@ fn build_edge_input( } } -/// Resolve one frame's four edges, returning each as `Some(edge)` when the anchor graph + explicit -/// size determined it, or `None` when it stayed `+Inf` (unset). The per-edge view **regions** need: -/// a region inherits any edge its anchors + size don't pin from its owner frame (decision 0068's v1 -/// region model — see [`crate::script`]'s region resolution), so it must see *which* edges resolved, -/// not the all-or-nothing verdict [`resolve_rect`] gives. Order matches [`Rect`]: `[bottom, left, -/// top, right]`. No `assemble`/clamp is applied (regions never clamp to screen in v1). +/// Resolve one frame's edges, `[bottom, left, top, right]`, each `None` where it stayed unset; +/// `assemble` and its clamp are not applied. The region pass reads it axis by axis. #[allow(clippy::float_cmp)] pub fn resolve_rect_edges( frame: &LayoutInput, @@ -730,55 +614,27 @@ pub fn resolve_rect_edges( [cvt(bottom), cvt(left), cvt(top), cvt(right)] } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Graph driver — resolve a set of frames in dependency order (anchor targets before dependents), -// detecting cycles. This is benilla's orchestration, not client fidelity math: the client resolves -// lazily-on-read + per-frame-batched via a dependency-ordered deferred queue (`0x7680e0`, ordered -// by `0x7681b0`); we express the same dependency ordering as an explicit topological pass. -// -// ## Why a dense, reusable solver rather than a fresh map-backed graph -// -// The caller re-solves the whole widget set every round of its fixpoint, and the fixpoint runs -// every frame. A map-backed graph rebuilt per round paid, per round: one `LayoutInput` clone -// (heap `Vec`) per frame, a `BTreeMap` insert per node, and a `BTreeMap` probe per anchor -// target during the solve. Measured on the shipped default UI (1881 frames + 5478 regions, -// release): 1.36 ms per resolve, of which ~0.25 ms was graph construction and ~0.64 ms the solve -// itself — paid on a completely quiet frame. -// -// [`Handle`]s are the caller's own dense id space (benilla's `Model::next_id`, monotonic from 1 -// with frames and regions sharing the counter), so every per-node map collapses to an array index -// and every buffer can live across calls. `begin` clears only the slots the previous round -// touched, so a round costs O(nodes), never O(id space). Iteration order is kept **ascending by -// handle** — identical to the `BTreeMap` it replaces — because cycle members are resolved -// best-effort in that order and the result depends on it. -// ───────────────────────────────────────────────────────────────────────────────────────────── +// ── Graph driver ───────────────────────────────────────────────────────────────────────────── -/// A reusable dependency solver over a set of anchored frames plus *external* pre-resolved rects -/// (the screen root — the client's `CSimpleTop`, whose rect is the physical screen — and any fixed -/// targets, such as already-resolved regions). -/// -/// One solver is kept alive across frames so its buffers are reused. A round is: -/// [`begin`](Self::begin) → [`set_external`](Self::set_external) / -/// [`set_frame`](Self::set_frame) → [`solve`](Self::solve) → [`rect`](Self::rect). +/// Resolves anchored frames in dependency order, a driver of benilla's own: the client resolves +/// lazily through a dependency-ordered deferred queue (`0x7680e0`, ordered by `0x7681b0`). Handles +/// index its arrays directly, so they must be dense. A round: [`begin`](Self::begin), +/// [`set_external`](Self::set_external) and [`set_frame`](Self::set_frame), [`solve`](Self::solve). #[derive(Clone, Debug, Default)] pub struct LayoutSolver { - /// Per-handle layout input. Persistent storage: [`set_frame`](Self::set_frame) overwrites in - /// place so the anchors `Vec` keeps its allocation across rounds. + /// Overwritten in place, so each anchors `Vec` keeps its allocation. input: Vec, - /// Is `input[h]` a frame to solve this round? is_frame: Vec, - /// Per-handle rect: externals as seeded, frames as they resolve. `None` = not (yet) known. + /// Externals as seeded, frames as they resolve. rect: Vec>, - /// Kahn in-degree, and the reverse edges (each inner `Vec` is cleared, never freed). + /// Kahn in-degree and reverse edges; each inner `Vec` is cleared, never freed. indeg: Vec, dependents: Vec>, - /// Handles whose slots were written this round — `begin` resets exactly these. + /// The handles written this round, the ones `begin` resets. touched: Vec, - /// Is `h` already in `touched`? marked: Vec, - /// The frames to solve, ascending (the `BTreeMap` iteration order this replaces). + /// Ascending: cycle members resolve in this order, and the result depends on it. live: Vec, - /// Kahn scratch + output. queue: VecDeque, emitted: Vec, order: Vec, @@ -787,12 +643,10 @@ pub struct LayoutSolver { } impl LayoutSolver { - /// A new, empty solver. pub fn new() -> LayoutSolver { LayoutSolver::default() } - /// Grow the per-handle arrays to cover `h`. fn ensure(&mut self, h: Handle) { let need = h as usize + 1; if self.input.len() < need { @@ -806,7 +660,7 @@ impl LayoutSolver { } } - /// Mark `h` as written this round (so [`begin`](Self::begin) will reset its slot). + /// Record `h` for [`begin`](Self::begin) to reset. fn touch(&mut self, h: Handle) { let i = h as usize; if !self.marked[i] { @@ -815,7 +669,7 @@ impl LayoutSolver { } } - /// Start a round: reset every slot the previous round touched (O(nodes), not O(id space)). + /// Start a round, resetting only the slots the last round touched. pub fn begin(&mut self) { for &h in &self.touched { let i = h as usize; @@ -834,17 +688,15 @@ impl LayoutSolver { self.unresolvable.clear(); } - /// Seed a pre-resolved rect at `h` — an anchor target this solve does not compute (the screen - /// root, an already-resolved region). Also used to publish a rect mid-sweep so later nodes in - /// the same pass see it. + /// Seed a rect this solve does not compute, such as the screen root (`CSimpleTop`); also + /// publishes one mid-sweep for later nodes of the same pass. pub fn set_external(&mut self, h: Handle, rect: Rect) { self.ensure(h); self.touch(h); self.rect[h as usize] = Some(rect); } - /// Register a frame to solve, copying `src` into the solver's persistent slot (the anchors - /// `Vec` is refilled in place, so a steady round allocates nothing). + /// Register a frame to solve, copying `src` into its slot; a steady round allocates nothing. pub fn set_frame(&mut self, h: Handle, src: &LayoutInput) { self.ensure(h); self.touch(h); @@ -856,9 +708,7 @@ impl LayoutSolver { self.live.push(h); } - /// [`set_frame`](Self::set_frame) with `src`'s anchors replaced by exactly `anchor` — the - /// ScrollFrame child override (decision 0112), which otherwise cost a full input clone plus a - /// one-element `Vec` per scroll child per round. + /// [`set_frame`](Self::set_frame) with `anchor` as the only anchor: a ScrollFrame child's. pub fn set_frame_anchored(&mut self, h: Handle, src: &LayoutInput, anchor: Anchor) { self.ensure(h); self.touch(h); @@ -870,7 +720,6 @@ impl LayoutSolver { self.live.push(h); } - /// The non-anchor fields, copied verbatim. fn copy_fields(dst: &mut LayoutInput, src: &LayoutInput) { dst.width = src.width; dst.height = src.height; @@ -880,18 +729,13 @@ impl LayoutSolver { dst.extent_y = src.extent_y; } - /// Resolve every registered frame in dependency order (each anchor target before its - /// dependents), detecting cycles. Cycle members are still resolved best-effort, ascending by - /// handle, against whatever rects were available — matching the map-backed driver this - /// replaces. + /// Resolve every registered frame, anchor targets first; cycle members then resolve + /// best-effort, ascending by handle, against the rects known by then. pub fn solve(&mut self) { self.live.sort_unstable(); - // Frame→frame anchor edges. A dependency on an external or an unknown handle does not - // constrain ordering (its rect is fixed / missing, not produced by this pass); only - // frame→frame edges do. A self-anchor counts as an edge, so a self-referential frame is - // never emitted ⇒ reported as a cycle. Duplicate targets count once (an anchor slot set is - // at most 9, so the dedupe is a linear scan, not a set). + // Only frame-to-frame anchors order the pass. A self-anchor counts, so a self-anchored + // frame lands in `cycle`; a target named twice counts once. for idx in 0..self.live.len() { let h = self.live[idx]; let mut seen: [Handle; 9] = [0; 9]; @@ -936,7 +780,7 @@ impl LayoutSolver { self.resolve_one(h); } - // Frames never emitted are on (or behind) a cycle; resolve them best-effort, ascending. + // A frame never emitted is on or behind a cycle. for idx in 0..self.live.len() { let h = self.live[idx]; if !self.emitted[h as usize] { @@ -946,7 +790,6 @@ impl LayoutSolver { } } - /// Resolve one frame against the rects known so far, recording failure. fn resolve_one(&mut self, h: Handle) { let outcome = { let input = &self.input[h as usize]; @@ -959,20 +802,18 @@ impl LayoutSolver { } } - /// The rect known at `h`: an external as seeded, or a frame's resolved rect. `None` if the - /// handle is unknown or its frame was under-constrained. + /// The rect at `h`: an external as seeded, a frame as resolved. #[inline] pub fn rect(&self, h: Handle) -> Option { self.rect.get(h as usize).copied().flatten() } - /// Frames on (or behind) an anchor dependency cycle — no valid topological position. Ascending. + /// Frames on or behind an anchor cycle, ascending. pub fn cycle(&self) -> &[Handle] { &self.cycle } - /// Frames whose `assemble` returned unresolvable (under-constrained, or depending on an - /// unresolvable/missing target). May overlap [`cycle`](Self::cycle). + /// Frames that did not resolve; may overlap [`cycle`](Self::cycle). pub fn unresolvable(&self) -> &[Handle] { &self.unresolvable } @@ -982,10 +823,8 @@ impl LayoutSolver { mod tests { use super::*; - // Screen-root handle used across tests; its rect is the physical screen (y-up). const SCREEN: Handle = 0; fn screen_rect() -> Rect { - // [bottom, left, top, right] Rect::new(0.0, 0.0, 600.0, 800.0) } @@ -1007,12 +846,10 @@ mod tests { } // ── Leaf kernels ───────────────────────────────────────────────────────────────────────── - // Expected outputs are hand-computed from the formulas above — this crate has no `WoW.exe` - // emulator, so these are spec-derived, not binary-diffed. + // Expected values are hand-computed from the formulas, not taken from the client. #[test] fn combine_edge_legs() { - // (center, opp, span) assert_eq!(combine_edge(10.0, 50.0, 7.0), 57.0); // opp+span assert_eq!(combine_edge(10.0, f32::INFINITY, 7.0), 7.0 * 0.5 + 10.0); // span/2+center assert_eq!(combine_edge(10.0, 50.0, 0.0), (10.0 - 50.0) + 10.0); // 2c-opp = -30 @@ -1043,14 +880,13 @@ mod tests { #[test] fn anchor_resolve_x_columns() { let r = [3.0f32, 100.0, 480.0, 260.0]; // [bottom, left, top, right] - // relLeft, center, relRight (point-ids 0,1,2), scale 1, xOff 5 + // point ids 0, 1, 2 (left, center, right), xOff 5 assert_eq!(anchor_resolve_x(1.0, 5.0, 0, Some(r), false), 5.0 + 100.0); assert_eq!( anchor_resolve_x(1.0, 5.0, 1, Some(r), false), (260.0 + 100.0) * 0.5 + 5.0 ); assert_eq!(anchor_resolve_x(1.0, 5.0, 2, Some(r), false), 5.0 + 260.0); - // scale multiplies the offset assert_eq!( anchor_resolve_x(2.0, 5.0, 0, Some(r), false), 2.0 * 5.0 + 100.0 @@ -1112,7 +948,7 @@ mod tests { // ── Composed resolver ───────────────────────────────────────────────────────────────────── - /// A numeric oracle for `assemble 0x767a20`, confirmed against a running `WoW.exe`. + /// The rect the running reference client's `assemble` (`0x767a20`) produced. #[test] fn oracle_single_topleft_explicit_size() { let f = LayoutInput::sized( @@ -1126,16 +962,14 @@ mod tests { 200.0, 50.0, ); - // screen root [0,0,600,800] (probe uses root top=600, right=800) + // The reference run's screen root: top 600, right 800. let got = resolve_rect(&f, |_| Some(Rect::new(0.0, 0.0, 600.0, 800.0))); assert_rect(got, Rect::new(545.0, 10.0, 595.0, 210.0)); } #[test] fn single_anchor_all_nine_points_explicit_size() { - // For each own-point p, anchor p -> the same point on screen, xOff/yOff 0, w100 h40. - // The anchored corner/edge pins directly; the rest derive from size. We assert the pinned - // coordinate and the resulting width/height, computed by hand from the formulas above. + // Each point pinned to the same point on the screen: every leg must keep the 100x40 size. let screen = screen_rect(); for &p in &[ Point::TopLeft, @@ -1154,7 +988,6 @@ mod tests { ResolveOutcome::Resolved(r) => r, ResolveOutcome::Unresolvable => panic!("{p:?} unresolvable"), }; - // width/height always preserved assert_eq!(r.width().to_bits(), 100.0f32.to_bits(), "{p:?} width"); assert_eq!(r.height().to_bits(), 40.0f32.to_bits(), "{p:?} height"); } @@ -1190,7 +1023,6 @@ mod tests { #[test] fn scale_interaction() { - // scale 2 -> offsets and size span doubled. let f = LayoutInput { anchors: vec![Anchor::new( Point::TopLeft, @@ -1266,8 +1098,7 @@ mod tests { #[test] fn unset_sentinel_propagates() { - // Anchor to a target with no valid rect (GetRect fail) -> that edge is +Inf -> frame - // unresolvable (single anchor, so opposite edges can't derive). + // The one anchor's target has no rect, so its edges are +Inf and nothing else pins them. let f = LayoutInput::sized( vec![Anchor::new( Point::TopLeft, @@ -1330,7 +1161,6 @@ mod tests { s.solve(); assert!(s.cycle().contains(&A)); assert!(s.cycle().contains(&B)); - // neither can resolve (each depends on the other's rect, unavailable) assert!(s.unresolvable().contains(&A)); assert!(s.unresolvable().contains(&B)); } @@ -1355,18 +1185,15 @@ mod tests { #[test] fn size_changed_epsilon() { let r = Rect::new(0.0, 0.0, 40.0, 100.0); - // sub-epsilon width change -> not fired + // 1e-7 is under SIZE_EPS. let tiny = Rect::new(0.0, 0.0, 40.0, 100.0 + 1e-7); assert!(!size_changed(r, tiny)); - // supra-epsilon -> fired let big = Rect::new(0.0, 0.0, 40.0, 100.001); assert!(size_changed(r, big)); } - /// The solver's buffers live across rounds, so `begin` must leave NO trace of the previous - /// one — a stale rect, in-degree, dependent edge, or `is_frame` flag would silently corrupt - /// the next solve. Round 2 registers a *disjoint* frame set that anchors to a handle round 1 - /// used, and must see it as unknown (not round 1's rect). + /// The buffers live across rounds, so `begin` must leave nothing behind: round 2 anchors to a + /// handle only round 1 registered and must find it unknown. #[test] fn begin_leaves_no_state_from_the_previous_round() { const A: Handle = 1; @@ -1402,8 +1229,7 @@ mod tests { let a_first = s.rect(A).expect("A resolved in round 1"); assert!(s.rect(B).is_some()); - // Round 2: ONLY B, still anchored to A — but A is no longer registered and no external - // seeds it, so B must be unresolvable rather than reusing round 1's stale A rect. + // Round 2: only B, whose target A is no longer registered. s.begin(); s.set_external(SCREEN, screen_rect()); s.set_frame( @@ -1426,8 +1252,7 @@ mod tests { assert!(s.unresolvable().contains(&B)); assert!(s.cycle().is_empty(), "cycle list must be cleared too"); - // Round 3: the original pair again — identical results to round 1 (no accumulated - // in-degree or dependent edges from the rounds between). + // Round 3: the original pair again, which must match round 1. s.begin(); s.set_external(SCREEN, screen_rect()); s.set_frame( @@ -1460,9 +1285,8 @@ mod tests { assert!(s.unresolvable().is_empty()); } - /// A frame registered with the ScrollFrame override keeps its size/scale but drops its own - /// anchors for exactly the override (decision 0112) — and the slot's anchor `Vec` is reused, - /// so the previous round's anchors must not leak through. + /// The override keeps the size and scale but replaces the anchors, and the reused anchors `Vec` + /// must not leak the last round's. #[test] fn set_frame_anchored_replaces_only_the_anchors() { const A: Handle = 1; @@ -1486,8 +1310,7 @@ mod tests { bits(Rect::new(0.0, 0.0, 600.0, 800.0)) ); - // Overridden: the two authored anchors are gone, replaced by one TOPLEFT pin at (10, -5), - // so the explicit 100x40 size now decides the other two edges. + // Overridden: one TOPLEFT pin at (10, -5), so the 100x40 size decides the other edges. s.begin(); s.set_external(SCREEN, screen_rect()); s.set_frame_anchored( diff --git a/crates/benilla-ui/src/lib.rs b/crates/benilla-ui/src/lib.rs index 0bb5bbbc..4af31f01 100644 --- a/crates/benilla-ui/src/lib.rs +++ b/crates/benilla-ui/src/lib.rs @@ -1,25 +1,5 @@ -//! The engine-free core of benilla's UI engine (decision 0068): the WoW interface is data + a -//! runtime — TOC manifests, FrameXML documents, and Lua — and this crate owns everything about -//! that runtime which needs neither Bevy nor a GPU: parsing, template expansion, the anchor/layout -//! resolver, and the draw-order key. The Bevy side (rendering, input, the game-state API bindings) -//! lives in the app; the Lua VM is embedded by the app and calls back into this crate's model. -//! -//! Built currently: [`toc`] — the addon/FrameXML manifest parser; [`framexml`] — the FrameXML -//! document parser (order-preserving tree, template expansion, `$parent` substitution); -//! [`bindings_xml`] — the per-addon `Bindings.xml` parser (the third file format, feeding -//! [`script::keybind`]'s table); [`layout`] -//! — the anchor/layout resolver (anchor graph → resolved rects), transcribed bit-faithfully from -//! the reference; [`widget`] — the frame arena and its show/hide/strata/level/ -//! scale/alpha propagation mutations; [`order`] — the strata/draw-layer vocabulary, the packed -//! total-order key, and the visible-tree traversal that realizes the client's painter order; -//! [`script`] — the engine-free Lua host (mlua 5.1) binding the arena/layout/order model to the -//! FrameScript object model + WoW stdlib so FrameXML/addon Lua runs against it (decision 0068); -//! [`loader`] — the FrameXML loader that joins the two, walking a parsed document to materialize live -//! frames in a running [`script::UiScript`] by driving the object model exactly as an addon does; -//! [`markup`] — the single owner of WoW's inline-markup grammar (`|cAARRGGBB`, `|H…|h…|h`, `|n`, -//! `||`): the client's token decoder and the per-byte class map its cursor law is built on; -//! [`source`] — what a UI source file *is* before any parser sees it: bytes, BOM-stripped, decoded -//! to text only where a Rust parser forces it (decision 1193). +//! The engine-free core of benilla's UI: the `.toc`, FrameXML and `Bindings.xml` parsers, the +//! layout resolver, the frame arena, draw order and the Lua host FrameXML runs in; no Bevy, no GPU. pub mod bindings_xml; pub mod civil; diff --git a/crates/benilla-ui/src/loader/backdrop.rs b/crates/benilla-ui/src/loader/backdrop.rs index 673df8e9..9b22381a 100644 --- a/crates/benilla-ui/src/loader/backdrop.rs +++ b/crates/benilla-ui/src/loader/backdrop.rs @@ -5,11 +5,9 @@ use crate::framexml::Element; use super::{abs_value, children_named, color_of, Loader}; impl Loader<'_> { - /// `` + children ``/``/``/ - /// ``/`` (LoadXML `0x77e6c0`). Builds the same table Lua `SetBackdrop` - /// reads and installs it, then applies the two colors — the XML `` parser (`0x6f23d0`) - /// defaults a *present* element's missing channels to **black** (r=g=b=0, a=1 via - /// [`color_of`]), distinct from the ctor white a table `SetBackdrop` leaves. + /// `` (LoadXML `0x77e6c0`): the table Lua `SetBackdrop` takes, then the two colours. + /// A present `` defaults a missing channel to black, alpha 1 (`0x6f23d0`), not the + /// white a table `SetBackdrop` leaves. pub(super) fn apply_backdrop(&mut self, el: &Element, wrapper: &Table, dbg: &str) { let Some(bd) = children_named(el, "Backdrop").next() else { return; @@ -32,10 +30,8 @@ impl Loader<'_> { } } - /// Build the `SetBackdrop` table from a `` element: `bgFile`/`edgeFile`/`tile` attrs, - /// ``/`` scalars ([`abs_value`]), and `` (an `` - /// child or inline `left`/`right`/`top`/`bottom` attrs). Missing keys are simply absent — the - /// object-model `SetBackdrop` leaves the ctor default (tileSize 0, edgeSize 32, insets 0). + /// The `SetBackdrop` table for a ``. A key the element omits stays absent, so + /// `SetBackdrop` keeps the ctor's default (tileSize 0, edgeSize 32, insets 0). pub(super) fn build_backdrop_table(&self, bd: &Element) -> mlua::Result { let t = self.lua().create_table()?; if let Some(f) = bd.attr("bgFile") { diff --git a/crates/benilla-ui/src/loader/geometry.rs b/crates/benilla-ui/src/loader/geometry.rs index d7aa26af..84d3a756 100644 --- a/crates/benilla-ui/src/loader/geometry.rs +++ b/crates/benilla-ui/src/loader/geometry.rs @@ -6,33 +6,15 @@ use super::regions::FontAttrs; use super::{abs_dim, children_named, Loader}; impl Loader<'_> { - /// LoadXML attribute handling (`0x769820`): the subset the v1 object model exposes, plus a - /// warn-once for the attributes whose methods don't exist yet (a next-phase gap, not a workaround). + /// A frame element's LoadXML attributes and simple children (`0x769820`), and a model pane's + /// own (`0x76cac0`). pub(super) fn apply_attrs(&mut self, el: &Element, wrapper: &Table, dbg: &str) { if el.attr_bool("hidden") { self.call(wrapper, "Hide", (), dbg); } - // **An unrecognised `frameStrata=` WARNS and skips; it does not raise** (decision 2160). - // - // The two doors are genuinely different in the reference and this is the one that is - // quiet. `CSimpleFrame::LoadXML 0x769820` resolves the name through `0x6f17d0`, whose miss - // arm returns `eax = 0` without writing the caller's out-parameter (`0x6f17ff`); the miss - // leg then pushes `0x878658 "Frame %s: Unknown frame strata: %s"` at severity 1 into the - // document's `CStatus` sink (`0x7699a4 call [edx+0xc]`, which returns — all five `Add` - // implementations image-wide are raise-free) and **reconverges with the hit path at - // `0x7699ad`**, carrying straight on into `frameLevel` and the rest of the subtree. - // `SetFrameStrata 0x76a470` is called only on the hit arm (`0x769971`), so the frame keeps - // whatever stratum it already had — its template's, its parent's, or the ctor's MEDIUM - // (`[+0xc0] = 3` at `0x7690c2`). - // - // The **Lua** binding is the loud one and stays as it is: `0x774360`'s miss reaches - // `0x774456 call 0x6f4940` (`luaL_error`), whose callee chain `luaG_errormsg 0x6fc780` / - // `luaD_throw 0x6f5d80` contains no `ret` at all — the epilogue after it is dead code. - // - // Resolving here rather than letting `SetFrameStrata` raise is what reproduces that split: - // routed through `self.call`, a bad value became a `report.errors` row, which is a script - // error the player sees — and it cost `EQL3` its whole `EQL3_Log.xml`, on a `` (a draw-LAYER name) the real client shrugs at. + // An unknown `frameStrata=` is logged (`0x7699a4`) and skipped, not raised as the Lua + // binding would (`0x774360`): `SetFrameStrata` runs only on a hit (`0x769971`), so the + // frame keeps the strata it had. if let Some(strata) = el.attr("frameStrata") { if crate::script::object::strata_from_str(strata).is_some() { self.call(wrapper, "SetFrameStrata", strata.to_string(), dbg); @@ -52,20 +34,8 @@ impl Loader<'_> { self.call(wrapper, "SetAlpha", a, dbg); } } - // `scale=` on a MODEL pane is the model's own scale, not the frame's: `CSimpleModel:: - // LoadXML` (`0x76cac0`) writes it into `+0x3a0` at `76cb61` — the field `SetModelScale` - // writes — and raises `Frame %s: Invalid model scale: %s` at `76cb92` for `≤ 0` (a raise, - // not a clamp). Every `scale=` in the shipped - // FrameXML sits on a model pane (the cooldown indicator's 0.75, the autocast shine's - // 1.2/1.22, the pings' 0.4, the dressing room's 2.0), and until decision 2007 the loader - // read none of them. Whether the generic frame loader (`0x769820`) reads a `scale` - // attribute of its own is unconfirmed; a plain frame's `scale=` is left as it was. - // `file=` on a model pane is `SetModel` (`CSimpleModel::LoadXML` `0x76cac0` installs the - // file into the widget, resident or streaming — decision 2013). Until 2013 no XML-declared - // pane ever held a file: the loader read `file=` only to turn the cooldown indicator's - // pane into a native widget of ours (retired by 2019), and the pings, the shine and the - // item card were bare panes to the engine. + // A model pane's `file=` is `SetModel` (`0x76cac0`). let model_kind = super::model_kind_tag(&el.tag); if model_kind { if let Some(file) = el.attr("file") { @@ -73,6 +43,9 @@ impl Loader<'_> { self.call(wrapper, "SetModel", text, dbg); } } + // A model pane's `scale=` is the model's scale, the field `SetModelScale` writes + // (`0x76cb61`); a value of 0 or less is refused, never clamped (`0x76cb92`). A plain + // frame's `scale=` is ignored; whether `0x769820` reads one is untraced. if let Some(scale) = el.attr("scale") { if model_kind { match scale.trim().parse::() { @@ -84,12 +57,8 @@ impl Loader<'_> { } } } - // The model pane's own fog attributes and `` child (`CSimpleModel::LoadXML` - // `0x76cac0`). `fogNear`/`fogFar` are **clamped at `≥ 0`** here and only - // here (`76cbbb`-`76cbd2` / `76cbf3`-`76cc0a`: `0.0 fcomp value ; jne store ; else store - // 0.0`) — the Lua setters store raw. The `` child writes the packed colour AND - // arms the fog bit, so it is `SetFogColor` in every respect; nothing in XML touches the - // light. Decision 2027. + // A model pane's fog: `fogNear`/`fogFar` clamp at 0 here only (`0x76cbbb`, `0x76cbf3`), + // where the Lua setters store raw; `` is `SetFogColor`, fog bit included. if model_kind { for (attr, verb) in [("fogNear", "SetFogNear"), ("fogFar", "SetFogFar")] { if let Some(v) = el.attr(attr).and_then(|v| v.trim().parse::().ok()) { @@ -115,22 +84,14 @@ impl Loader<'_> { self.call(wrapper, "SetID", n, dbg); } } - // `clampedToScreen="true"` → SetClampedToScreen (`0x768cc0` — geometry flags bit4, - // the layout resolve's screen clamp; GameTooltip frames carry it by construction). + // `clampedToScreen`: the layout's screen clamp, geometry flag bit 4 (`0x768cc0`). if el.attr_bool("clampedToScreen") { self.call(wrapper, "SetClampedToScreen", true, dbg); } - // `enableMouse` lands as the real EnableMouse call (the hit test gates on it — - // pointer.rs): a ref frame authored mouse-blocking (StaticPopup, BlackoutWorld, the - // minimap cluster) must actually capture. The loader ignored it for a while after the - // native landed — the stale "v1 gap" note here — which left those frames click-through - // and the minimap deaf to its ping click (0434 phase 6c's root cause). if el.attr_bool("enableMouse") { self.call(wrapper, "EnableMouse", true, dbg); } - // `` (also accepted - // inline on the element) → SetHitRectInsets: the frame's MOUSE rect, inset from its - // resolved rect. Absent sides read 0, so a partial element insets only what it names. + // ``: the mouse rect, inset from the frame's; a side it omits is 0. if let Some(ins) = children_named(el, "HitRectInsets").next() { let src = children_named(ins, "AbsInset").next().unwrap_or(ins); let side = |k: &str| { @@ -145,70 +106,33 @@ impl Loader<'_> { dbg, ); } - // `…` → the resize - // quad (`0x769820`'s child-loop arm at `0x769baa`). Three clauses from the reference - // rather than from the shape of the element: - // - // · **Both pairs are written unconditionally once `` matches**, each side - // defaulting to `0` before its lookup — so a block carrying only `` RESETS - // the max pair to unbounded. That is why this writes both calls, never just the one it - // found. - // · `x` is the WIDTH and `y` the HEIGHT, through the same logical→internal transform - // `` uses — the same `abs_dim` reader, and the same inline-or-`AbsDimension` pair - // of forms. - // · `` and `` are consumed by the base-class `CLayoutFrame::LoadXML` - // *before* the child loop is entered at all, so they always precede this structurally - // whatever the document order — and neither path clamps, so an authored `` - // outside the authored bounds survives load unchanged. - // - // The shipped 1.12.1 FrameXML uses it exactly once (`FloatingChatFrame.xml:223`, - // min 296×75 / max 608×400) and never calls the Lua setters at all. + // `` (`0x769baa`): both pairs are always written, an absent one as 0, so a + // lone `` resets the max to unbounded. A `` outside them is not clamped. if let Some(rb) = children_named(el, "ResizeBounds").next() { for (tag, verb) in [("minResize", "SetMinResize"), ("maxResize", "SetMaxResize")] { let (x, y) = children_named(rb, tag).next().map_or((None, None), abs_dim); self.call(wrapper, verb, (x.unwrap_or(0.0), y.unwrap_or(0.0)), dbg); } } - // `movable`/`resizable` → SetMovable/SetResizable — the same flag word the methods write - // (`0x76a3c0` with mask 0x100 / 0x200; the XML loader `0x769820` calls that very setter). - // Both were in the gap list below until the movable family - // landed; leaving them there would have left every `movable="true"` reference window - // undraggable while `SetMovable` worked from Lua. + // `movable`/`resizable`: flag bits 0x100/0x200, via the setter Lua uses (`0x76a3c0`). if el.attr_bool("movable") { self.call(wrapper, "SetMovable", true, dbg); } if el.attr_bool("resizable") { self.call(wrapper, "SetResizable", true, dbg); } - // `toplevel` → SetToplevel — the third bit of that same flag word (`0x76a3c0` mask `0x1`, - // XML site `0x7698ec`), and the third attribute to - // graduate out of the gap list below for the same reason: the raise law is built - // (`script::object::toplevel`), so accepting the attribute now means the behaviour, not - // silence. 82 corpus addons and thirteen of our own frames declare it. + // `toplevel`: bit 0x1 of the same flag word (`0x7698ec`). if el.attr_bool("toplevel") { self.call(wrapper, "SetToplevel", true, dbg); } - // `enableKeyboard="true"` — the XML half of the flag, which enables BOTH key kinds - // (`0x769ae8`/`0x769af3`). The flag is real, and `script::keyboard`'s delivery - // walk is what reads it (1319). + // `enableKeyboard` enables both key kinds (`0x769ae8`, `0x769af3`). if el.attr_bool("enableKeyboard") { self.call(wrapper, "EnableKeyboard", true, dbg); } } - /// `` → SetWidth/SetHeight (`0x767800`). Accepts either `` - /// or the inline `` form; a dimension that's absent is left untouched (the client's - /// "0 = derive"). ALL `` children apply, in document order — template expansion appends the - /// instance's children after the template's ([`crate::framexml::expand`]), and the client simply - /// processes each child in turn, so an instance's own `` overwrites its template's. (Taking - /// only `.next()` here silently pinned every templated frame to the TEMPLATE's size; caught on - /// the quest log's Abandon button — 125×21 in the instance XML, 80×22 on screen.) - /// `` (or one with its own ``/``): the - /// frame's drag handle, built through the same `CreateTitleRegion` the Lua API exposes — so - /// the element and a later `frame:CreateTitleRegion()` name ONE object (XML site `0x769b2a` - /// is the same code path as `CreateTitleRegion 0x773910`, idempotent) — then laid out like - /// any region. - /// The stock `TutorialFrame.xml` declares one over its whole plate (1976). + /// ``: the drag handle, built through the idempotent `CreateTitleRegion` so the + /// element and a later Lua call name one object (`0x769b2a` takes `0x773910`'s path). pub(super) fn apply_title_region( &mut self, el: &Element, @@ -231,6 +155,8 @@ impl Loader<'_> { self.apply_region_layout(tr, ®ion, self_name, dbg, FontAttrs::Own); } + /// `` (`0x767800`). Every one applies in document order, and expansion puts an + /// instance's children after its template's, so the instance's own `` wins. pub(super) fn apply_size(&mut self, el: &Element, wrapper: &Table, dbg: &str) { for size in children_named(el, "Size") { let (x, y) = abs_dim(size); @@ -243,13 +169,9 @@ impl Loader<'_> { } } - /// `` → SetPoint per `` - /// (`0x767800`). `relativePoint` defaults to `point`; `relativeTo` is `$parent`-substituted - /// (`0x76c5b0`) and passed by name (the object model resolves it, falling back to the parent - /// when absent/unresolved); a missing `point` is skipped with a warning ("Invalid anchor - /// point"). - /// The `setAllPoints="true"` shorthand applies first, like the region path — a frame carrying - /// only the attribute (WorldMapFrame's chrome layers) pins TOPLEFT+BOTTOMRIGHT to its parent. + /// `` (`0x767800`), after the `setAllPoints` shorthand: `relativePoint` defaults to + /// `point`, and `relativeTo` expands `$parent` to the nearest named ancestor's name, else + /// `Top` (`0x76c5b0`). pub(super) fn apply_anchors( &mut self, el: &Element, @@ -289,8 +211,7 @@ impl Loader<'_> { args, dbg: dbg.to_string(), }; - // The XML path's own law, which resolves the name itself and defers a target - // the enclosing frame's subtree has not built yet (`Loader::apply_anchor`). + // Deferred while the enclosing subtree has not built its target yet. self.apply_anchor(d, true); } } diff --git a/crates/benilla-ui/src/loader/mod.rs b/crates/benilla-ui/src/loader/mod.rs index 3fd125c6..435bbff9 100644 --- a/crates/benilla-ui/src/loader/mod.rs +++ b/crates/benilla-ui/src/loader/mod.rs @@ -1,58 +1,12 @@ -//! The FrameXML **loader** (decision 0068): the join between the document layer ([`crate::framexml`]) -//! and the runtime ([`crate::script`]). It walks a parsed FrameXML document and *materializes* it — -//! turning ``/`