NTFS load order, a title-sorted AddOns list, and tooltips at the cursor
- addons load in the order NTFS lists a folder, case-insensitive with an underscore before the letters, the order the vanilla corpus was written against and the one that decides which addon provides a shared library (2178) - GetNumAddOns and the index form of every AddOn verb walk a title-sorted list that hides the Blizzard addons the server says to hide, and the character-select AddOns list is in title order (2175) - SetPoint and SetAllPoints raise the reference's own errors for a frame name that does not exist or an omitted relative frame instead of quietly anchoring to the parent, and an explicit nil means the screen root (2176) - a tooltip owned with ANCHOR_CURSOR follows the mouse, which pfUI's tooltip, xpbar and chat modules, pfQuest's browser and TipBuddy ask for; an omitted or unknown anchor is ANCHOR_LEFT (2176) - a $parent name handed to CreateFrame, CreateTexture or CreateFontString expands as it does in XML, so FonzAppraiser's thirty dropdowns and pfQuest's search icon get the names they asked for (2176) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
//! `SetPoint`/`SetAllPoints`' argument ladder — **one** transcription of `0x7a2540` / `0x7a2940`.
|
||||
//!
|
||||
//! The two bindings are `CScriptRegion` methods registered **once**, at `.data 0x87ca38` and
|
||||
//! `0x87ca40` inside the 19-record table `[0x87c9b8, 0x87ca50)` whose registrar `0x7a2e70` supplies
|
||||
//! base and count — so Frame, Texture, FontString and Model all run the *same* function (wow-re
|
||||
//! `system/ui/ui.md` §(b2), `scratch/name-string-widget-resolution.md` §3-4, ledger rows
|
||||
//! `0x7a2540`/`0x7a2940`). This module is that single function's argument half; what each caller
|
||||
//! keeps for itself is only the thing the reference reads off the receiver — the layout parent's
|
||||
//! default id, and where the anchor is stored.
|
||||
//!
|
||||
//! **Every failure leg RAISES**, which is the point of the module (decision 2176, closing 2105's
|
||||
//! "Named, not done"). `luaL_error` does not return: `luaG_errormsg [0x6fc780,0x6fc861)` and
|
||||
//! `luaD_throw [0x6f5d80,0x6f5da5)` hold zero `ret`s, and `luaD_throw`'s *elided* epilogue is the
|
||||
//! positive control that says so. There is no fallback and no silent no-op on any of them.
|
||||
//!
|
||||
//! The **XML** `<Anchor relativeTo=>` path is a different function and a different law — it
|
||||
//! resolves through the same `0x76c700` but reports `"Couldn't find relative frame: %s"` at
|
||||
//! severity 1 and **skips the anchor** (`0x767969 test ebx,ebx` → `0x76796d` → `jmp 0x7679dd`,
|
||||
//! past the `0x7679c8 call 0x767c70`). That half lives in `crate::loader::geometry`, which
|
||||
//! therefore resolves its own names and never reaches the ladder here.
|
||||
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::layout::Point;
|
||||
use crate::script::{Model, SCREEN};
|
||||
|
||||
use super::{decode_id, point_from_str, prefetch_named_target, resolve_named_target, NamedTarget};
|
||||
|
||||
/// What the reference prints where a widget has no name — the literal `0x84c7f0`, reached from
|
||||
/// `call [primary_vt+0x4]` (`GetName` → `[widget+0x98]`) returning NULL.
|
||||
pub(crate) const UNNAMED: &str = "<unnamed>";
|
||||
|
||||
/// `relativeTo`, decoded — the four tags `lua_type(L,3)` accepts (`0x7a25e6`–`0x7a2620`); anything
|
||||
/// else, **absent included** (`TNONE` = −1), is a Usage raise on the `SetPoint` side.
|
||||
pub(crate) enum RelTarget {
|
||||
/// A widget wrapper table (`0x7a26c9`: `rawgeti(3,0)` → `touserdata` → `+0x24`).
|
||||
///
|
||||
/// **The reference runs no `IsA` check on this arm** — unlike the `this` ladder and unlike the
|
||||
/// string arm, whose check lives inside `0x76c760` — so `add eax,0x24` is applied to whatever
|
||||
/// `lua_touserdata` returned and a table carrying any light userdata at `[0]` is accepted as a
|
||||
/// widget. That is a wild pointer, not a behaviour: [`resolve_rel_target`] takes the layout
|
||||
/// parent for a table that decodes to no live widget, and says so there.
|
||||
Wrapper(Table),
|
||||
/// A name string, already read out of `_G` with no `Model` guard alive ([`NamedTarget`]).
|
||||
Named(NamedTarget),
|
||||
/// An explicit `nil` — the **screen root** `ds:[0xcf0bd8]` (`0x7a2710`), *not* the parent. One
|
||||
/// stock site takes it (`UIParent.lua:1549`) and fourteen corpus sites do, every one of them
|
||||
/// meaning "the screen" (`pfUI`'s screenshot caption, `FixCTGroups`' cursor-space plate).
|
||||
Screen,
|
||||
/// A NUMBER in the `relativeTo` slot: `edi` stays 3 and the number is `offsetX` — the
|
||||
/// `SetPoint(point, x, y)` shorthand (`0x7a270e`). `relativeTo` keeps the layout parent's
|
||||
/// default (`0x76c6e0`: `[+0x9c]`'s layout sub-object, else the screen root).
|
||||
DefaultParent,
|
||||
}
|
||||
|
||||
/// `SetPoint`'s parsed arguments (`0x7a2540`), ready for the caller's own commit.
|
||||
pub(crate) struct PointArgs {
|
||||
pub(crate) point: Point,
|
||||
pub(crate) target: RelTarget,
|
||||
pub(crate) rel_point: Point,
|
||||
pub(crate) x: f32,
|
||||
pub(crate) y: f32,
|
||||
}
|
||||
|
||||
/// `Usage: %s:SetPoint(…)` — `luaL_error(0x87cc28)`, the string read out of the reference image.
|
||||
fn usage(who: &str) -> mlua::Error {
|
||||
mlua::Error::runtime(format!(
|
||||
"Usage: {who}:SetPoint(\"point\" [, region or nil] [, \"relativePoint\"] \
|
||||
[, offsetX, offsetY])"
|
||||
))
|
||||
}
|
||||
|
||||
/// `%s:SetPoint(): Unknown region point` — `0x87cd04`, raised by the 9-entry name scan `0x6f1840`
|
||||
/// for either the `point` or a `relativePoint` that is a string it rejects.
|
||||
fn unknown_point(who: &str) -> mlua::Error {
|
||||
mlua::Error::runtime(format!("{who}:SetPoint(): Unknown region point"))
|
||||
}
|
||||
|
||||
/// `%s:%s(): Couldn't find region named '%s'` — `0x87ccd4` for `SetPoint`, `0x87cd84` for
|
||||
/// `SetAllPoints`. **No fallback and no no-op**: this is the leg benilla answered with the parent
|
||||
/// plus a warning until 2176.
|
||||
pub(crate) fn no_such_region(who: &str, verb: &str, name: &str) -> mlua::Error {
|
||||
mlua::Error::runtime(format!(
|
||||
"{who}:{verb}(): Couldn't find region named '{name}'"
|
||||
))
|
||||
}
|
||||
|
||||
/// `%s:%s(): trying to anchor to itself` — `0x87cca8` / `0x87cd54`, the `[ebp-4] == this+0x24`
|
||||
/// compare at `0x7a2764` / `0x7a2aa8`.
|
||||
fn anchor_to_self(who: &str, verb: &str) -> mlua::Error {
|
||||
mlua::Error::runtime(format!("{who}:{verb}(): trying to anchor to itself"))
|
||||
}
|
||||
|
||||
/// `lua_isnumber 0x6f34d0` — a number, or a string Lua can read as one.
|
||||
fn as_number(lua: &Lua, v: &Value) -> Option<f32> {
|
||||
lua.coerce_number(v.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|n| n as f32)
|
||||
}
|
||||
|
||||
/// `lua_isstring 0x6f3510` — tag 4 **or** 3, so a number is a string here.
|
||||
fn as_string(lua: &Lua, v: &Value) -> Option<String> {
|
||||
match v {
|
||||
Value::String(_) | Value::Number(_) | Value::Integer(_) => lua
|
||||
.coerce_string(v.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|s| s.to_str().ok().map(|r| r.to_string())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `SetPoint(point [, relativeTo [, relativePoint]] [, x, y])` in the reference's own order:
|
||||
/// the two tag gates first (both Usage), then the point-name scan, then `relativePoint`, then the
|
||||
/// offsets. The order is observable — an unknown point with an *absent* `relativeTo` raises Usage,
|
||||
/// not "Unknown region point".
|
||||
///
|
||||
/// `who` names the receiver for the error strings, and `parent_base` is what a leading `$parent`
|
||||
/// expands to. Both are the caller's to compute, under a short `Model` borrow, **before** this
|
||||
/// runs: the `_G` read here is a `lua_gettable` and an `__index` on `_G` can call straight back
|
||||
/// into a binding ([`NamedTarget`]).
|
||||
pub(crate) fn parse_set_point(
|
||||
lua: &Lua,
|
||||
args: &MultiValue,
|
||||
who: &str,
|
||||
parent_base: &str,
|
||||
) -> mlua::Result<PointArgs> {
|
||||
// arg 2 = point. `lua_isstring(L,2)` false → `0x7a290d` Usage (`0x7a25d2`).
|
||||
let point_str = args.front().and_then(|v| as_string(lua, v));
|
||||
let Some(point_str) = point_str else {
|
||||
return Err(usage(who));
|
||||
};
|
||||
|
||||
// arg 3 = relativeTo. `lua_type(L,3)` ∈ {4 STRING, 5 TABLE, 0 NIL, 3 NUMBER}, else Usage —
|
||||
// and an **absent** argument is `TNONE` = −1, which fails all four (`0x7a25e6`–`0x7a2620`).
|
||||
let Some(rel_arg) = args.get(1) else {
|
||||
return Err(usage(who));
|
||||
};
|
||||
let target = match rel_arg {
|
||||
Value::String(s) => RelTarget::Named(match s.to_str() {
|
||||
Ok(n) => prefetch_named_target(lua, n.as_ref(), Some(parent_base)),
|
||||
Err(_) => NamedTarget::unreadable(),
|
||||
}),
|
||||
Value::Table(t) => RelTarget::Wrapper(t.clone()),
|
||||
Value::Nil => RelTarget::Screen,
|
||||
Value::Number(_) | Value::Integer(_) => RelTarget::DefaultParent,
|
||||
_ => return Err(usage(who)),
|
||||
};
|
||||
|
||||
// The point name, scanned by `0x6f1840` only now (`0x7a264a`) — after both tag gates.
|
||||
let point = point_from_str(&point_str).ok_or_else(|| unknown_point(who))?;
|
||||
|
||||
// `relativePoint` is seeded from `point` (`0x7a2697`) and read at index `edi`, which the
|
||||
// number arm above left at 3 and every other arm advanced to 4. **Only a STRING is consumed**
|
||||
// (`0x7a2810 lua_type == 4`, strictly — not `lua_isstring`); anything else leaves the seed and
|
||||
// the offsets are taken from here.
|
||||
let mut cur = if matches!(target, RelTarget::DefaultParent) {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let mut rel_point = point;
|
||||
if let Some(Value::String(s)) = args.get(cur) {
|
||||
let name = s.to_str()?;
|
||||
rel_point = point_from_str(name.as_ref()).ok_or_else(|| unknown_point(who))?;
|
||||
cur += 1;
|
||||
}
|
||||
|
||||
// **Both offsets, or neither** (`0x7a2888 je 0x7a28e6`): each is gated on `lua_isnumber`, and
|
||||
// one failing zeroes the pair rather than only itself.
|
||||
let (x, y) = match (
|
||||
args.get(cur).and_then(|v| as_number(lua, v)),
|
||||
args.get(cur + 1).and_then(|v| as_number(lua, v)),
|
||||
) {
|
||||
(Some(x), Some(y)) => (x, y),
|
||||
_ => (0.0, 0.0),
|
||||
};
|
||||
|
||||
Ok(PointArgs {
|
||||
point,
|
||||
target,
|
||||
rel_point,
|
||||
x,
|
||||
y,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse `SetAllPoints([relativeTo])` (`0x7a2940`) — **the same resolver, a different argument
|
||||
/// law**, and the three differences are per-binding facts rather than a family rule:
|
||||
///
|
||||
/// 1. An **omitted** argument is legal: it falls through to the layout parent with no raise, where
|
||||
/// `SetPoint` raises Usage.
|
||||
/// 2. The name path is gated on **`lua_isstring`** (`0x7a29ea`), tag 4 **or** 3 — so
|
||||
/// `f:SetAllPoints(42)` stringifies and looks up `_G["42"]`, where `SetPoint` tests
|
||||
/// `lua_type == 4` strictly and routes a number to the offset shorthand.
|
||||
/// 3. There is no `relativePoint` or offset parsing at all.
|
||||
pub(crate) fn parse_set_all_points(lua: &Lua, arg: Option<&Value>, parent_base: &str) -> RelTarget {
|
||||
match arg {
|
||||
Some(Value::Table(t)) => RelTarget::Wrapper(t.clone()),
|
||||
Some(Value::Nil) => RelTarget::Screen,
|
||||
// `lua_isstring` — a NUMBER takes the name path here, unlike `SetPoint`.
|
||||
Some(v) => match as_string(lua, v) {
|
||||
Some(n) => RelTarget::Named(prefetch_named_target(lua, &n, Some(parent_base))),
|
||||
None => RelTarget::DefaultParent,
|
||||
},
|
||||
None => RelTarget::DefaultParent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a parsed [`RelTarget`] into the layout id to anchor against, raising the reference's own
|
||||
/// errors. `me` is the receiver's own id — the self-anchor compare (`0x7a2764` / `0x7a2aa8`).
|
||||
///
|
||||
/// The **wrapper** arm takes `default_parent` where the reference would take a wild pointer (see
|
||||
/// [`RelTarget::Wrapper`]); every other arm is the reference's.
|
||||
pub(crate) fn resolve_rel_target(
|
||||
model: &Model,
|
||||
target: &RelTarget,
|
||||
who: &str,
|
||||
verb: &str,
|
||||
me: u32,
|
||||
default_parent: u32,
|
||||
) -> mlua::Result<u32> {
|
||||
let id = match target {
|
||||
RelTarget::Wrapper(t) => decode_id(t)
|
||||
.ok()
|
||||
.filter(|id| model.id_to_frame.contains_key(id) || model.id_to_region.contains_key(id))
|
||||
.unwrap_or(default_parent),
|
||||
RelTarget::Named(nt) => {
|
||||
resolve_named_target(model, nt).ok_or_else(|| no_such_region(who, verb, &nt.name))?
|
||||
}
|
||||
RelTarget::Screen => SCREEN,
|
||||
RelTarget::DefaultParent => default_parent,
|
||||
};
|
||||
if id == me {
|
||||
return Err(anchor_to_self(who, verb));
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// The **XML** `<Anchor relativeTo="name">` resolution — `CLayoutFrame::LoadXML 0x767800`'s own,
|
||||
/// which is a different law from the Lua ladder above and belongs to `crate::loader`.
|
||||
///
|
||||
/// `0x767964 call [eax+0x28]` is the same `0x76c700` the Lua string arm uses, so the *lookup* is
|
||||
/// identical; what differs is the miss. `0x767969 test ebx,ebx` → `0x76796d` pushes
|
||||
/// `0x878440 "Couldn't find relative frame: %s"` at severity 1 through the XML report's
|
||||
/// `[ecx+0xc]` and then `jmp 0x7679dd`, which lands **past** the `0x7679c8 call 0x767c70` — the
|
||||
/// anchor is reported and SKIPPED. It does not raise (there is no Lua state to unwind here) and it
|
||||
/// does not fall back to the parent, which is what benilla did until 2176.
|
||||
///
|
||||
/// `$parent` is already expanded by the caller, so no base is taken.
|
||||
pub(crate) fn resolve_xml_relative_to(lua: &Lua, name: &str) -> Option<Table> {
|
||||
// ONE `_G` read, and the wrapper it produced is the one handed back — re-reading `_G` to fetch
|
||||
// the table again would be a second `lua_gettable`, and an `__index` is allowed to answer
|
||||
// twice differently.
|
||||
let nt = prefetch_named_target(lua, name, None);
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
resolve_named_target(&model, &nt)?;
|
||||
// The wrapper the ladder's TABLE arm takes — resolved here so the binding never sees a name
|
||||
// from the XML path and so its raise can never reach a loading document.
|
||||
nt.wrapper.clone()
|
||||
}
|
||||
@@ -520,6 +520,20 @@ fn create_region(
|
||||
|
||||
let wrapper = region_wrapper(lua, id)?;
|
||||
if let Some(name) = name {
|
||||
// **A leading `$parent` in the NAME is expanded, here as in XML** — `CreateTexture
|
||||
// 0x773a20` and `CreateFontString 0x773c30` build a synthetic node carrying
|
||||
// `name=<the Lua string>` and read it back through the same `GetAttribute(node, "name")` →
|
||||
// `CScriptRegion::SetName 0x76c650` the XML path uses, and `0x76c691` is one of that
|
||||
// expander's two call sites (wow-re `name-string-widget-resolution.md` §5/§6, whose
|
||||
// `name=` reader census names `0x773ba1` and `0x773dc4` — the addresses *inside* these two
|
||||
// bindings). A region's `$parent` is its OWNER frame, so the walk starts there.
|
||||
//
|
||||
// `pfQuest/browser.lua:723` is `pfBrowser.input:CreateTexture("$parentSearchIcon",
|
||||
// "OVERLAY")`, and every named widget FonzAppraiser builds is this idiom.
|
||||
let name = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
crate::framexml::resolve_name(&name, &super::parent_token_base(&model, Some(owner)))
|
||||
};
|
||||
publish_global(lua, &name, &wrapper)?;
|
||||
// Publish into the region-name registry too (first-wins, the frame rule) — this is what
|
||||
// lets a sibling region's SetPoint name us as its `relativeTo` (see `resolve_target`).
|
||||
|
||||
@@ -3,40 +3,26 @@
|
||||
//! (`GetLeft`/`GetRight`/`GetTop`/`GetBottom`). Split out of [`super`] purely for size — see
|
||||
//! its module doc for the shared id/handle plumbing and method-table wiring.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::script::{Model, SCREEN};
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::{
|
||||
as_f32, decode_id, frame_handle_of, frame_parent_token_base, frame_wrapper, point_from_str,
|
||||
point_name, prefetch_named_target, resolve_named_target, NamedTarget,
|
||||
};
|
||||
use super::anchor_args::{parse_set_all_points, parse_set_point, resolve_rel_target, UNNAMED};
|
||||
use super::{frame_handle_of, frame_parent_token_base, frame_wrapper, point_name};
|
||||
|
||||
/// Phase 1 of a `relativeTo`/`SetAllPoints` target: when the argument is a **name string**, expand
|
||||
/// a leading `$parent` against `h`'s first named ancestor and read `_G` — all of it with **no**
|
||||
/// `Model` guard alive, because the reference's read is a gettable and an `__index` on `_G` can run
|
||||
/// Lua that calls straight back into a binding ([`NamedTarget`]).
|
||||
///
|
||||
/// Anything else needs no prefetch and answers `None`.
|
||||
fn prefetch_relative_to(
|
||||
lua: &Lua,
|
||||
arg: Option<&Value>,
|
||||
h: FrameHandle,
|
||||
) -> mlua::Result<Option<NamedTarget>> {
|
||||
let Some(Value::String(s)) = arg else {
|
||||
return Ok(None);
|
||||
};
|
||||
// A name we cannot even read names nothing; the caller's miss path reports it as before.
|
||||
let Ok(raw) = s.to_str() else {
|
||||
return Ok(Some(NamedTarget::unreadable()));
|
||||
};
|
||||
let base = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
frame_parent_token_base(&model, h)
|
||||
};
|
||||
Ok(Some(prefetch_named_target(lua, raw.as_ref(), Some(&base))))
|
||||
/// The two things the shared ladder needs off the receiver, read under **one short** `Model`
|
||||
/// borrow that is dropped before the `_G` read: the name the reference puts in its error strings
|
||||
/// (`GetName`, else the literal `"<unnamed>"`), and the name a leading `$parent` expands to.
|
||||
fn frame_ladder_context(lua: &Lua, h: FrameHandle) -> (String, String) {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let who = model
|
||||
.arena
|
||||
.frame(h)
|
||||
.and_then(|f| f.name.clone())
|
||||
.unwrap_or_else(|| UNNAMED.to_string());
|
||||
(who, frame_parent_token_base(&model, h))
|
||||
}
|
||||
|
||||
/// Populate `m`'s layout (anchor/size) methods (see the module doc).
|
||||
@@ -44,11 +30,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Layout: SetPoint / ClearAllPoints / SetWidth / SetHeight / GetWidth / GetHeight
|
||||
m.set(
|
||||
"SetPoint",
|
||||
lua.create_function(
|
||||
|lua, (this, p, a2, a3, a4, a5): (Table, String, Value, Value, Value, Value)| {
|
||||
set_point(lua, &this, &p, [a2, a3, a4, a5])
|
||||
},
|
||||
)?,
|
||||
lua.create_function(|lua, (this, rest): (Table, MultiValue)| set_point(lua, &this, &rest))?,
|
||||
)?;
|
||||
m.set(
|
||||
"ClearAllPoints",
|
||||
@@ -135,30 +117,17 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// the XML `setAllPoints="true"` behavior as a method (rf24 `0x767800`'s SetAllPoints path).
|
||||
m.set(
|
||||
"SetAllPoints",
|
||||
lua.create_function(|lua, (this, target): (Table, Value)| {
|
||||
lua.create_function(|lua, (this, rest): (Table, MultiValue)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
// The `_G` read runs before the guard — see `set_point` and `object::NamedTarget`.
|
||||
let named = prefetch_relative_to(lua, Some(&target), h)?;
|
||||
// `who`/`$parent` first, then the `_G` read, then the guard — see `set_point` and
|
||||
// `object::NamedTarget`.
|
||||
let (who, base) = frame_ladder_context(lua, h);
|
||||
let target = parse_set_all_points(lua, rest.front(), &base);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let rel_id: u32 = match &target {
|
||||
// Same id_to_region fallback as `set_point` above (and `region.rs`'s own
|
||||
// `resolve_target`, which already checked both) — a table wrapper may name a region.
|
||||
Value::Table(t) => decode_id(t)
|
||||
.ok()
|
||||
.filter(|id| {
|
||||
model.id_to_frame.contains_key(id) || model.id_to_region.contains_key(id)
|
||||
})
|
||||
.unwrap_or_else(|| default_parent_id(&mut model, h)),
|
||||
Value::String(_) => {
|
||||
// `_G[name]` — the client's one widget namespace, frames and regions alike
|
||||
// (`object::NamedTarget`). The real XML anchors frames to REGIONS too (gossip
|
||||
// option rows → the greeting FontString), and both publish there.
|
||||
let nt = named.as_ref().expect("a String argument is prefetched");
|
||||
let hit = resolve_named_target(&model, nt);
|
||||
hit.unwrap_or_else(|| default_parent_id(&mut model, h))
|
||||
}
|
||||
_ => default_parent_id(&mut model, h),
|
||||
};
|
||||
let me = model.frame_id(h);
|
||||
let parent = default_parent_id(&mut model, h);
|
||||
let rel_id: u32 =
|
||||
resolve_rel_target(&model, &target, &who, "SetAllPoints", me, parent)?;
|
||||
let pair = [
|
||||
Anchor::new(Point::TopLeft, rel_id, Point::TopLeft, 0.0, 0.0),
|
||||
Anchor::new(Point::BottomRight, rel_id, Point::BottomRight, 0.0, 0.0),
|
||||
@@ -369,91 +338,22 @@ fn size_read(model: &Model, h: FrameHandle, width: bool) -> f32 {
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// `SetPoint(point [, relativeTo [, relativePoint]] [, x, y])` (RF-0023/RF-0024). Defaults per the
|
||||
/// client: `relativeTo` = the frame's parent (the screen root if top-level), `relativePoint` =
|
||||
/// `point`. Overload disambiguated by argument *type* (a string/table after `point` is `relativeTo`;
|
||||
/// a number is the first offset).
|
||||
fn set_point(lua: &Lua, this: &Table, point: &str, rest: [Value; 4]) -> mlua::Result<()> {
|
||||
let point = point_from_str(point)
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("SetPoint: unknown point '{point}'")))?;
|
||||
/// `SetPoint(point [, relativeTo [, relativePoint]] [, x, y])` — `0x7a2540`, whose argument ladder
|
||||
/// (and whose five raises) live in [`super::anchor_args`] because the reference runs **one**
|
||||
/// function for frames and regions alike. What is this side's own is only what the reference reads
|
||||
/// off a *frame*: the layout parent's default id, and the anchor list the commit writes.
|
||||
fn set_point(lua: &Lua, this: &Table, args: &MultiValue) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
|
||||
// A name target is read out of `_G` FIRST, with no model guard alive — the reference's read is
|
||||
// a gettable, so an `__index` on `_G` can run Lua that calls back in (`object::NamedTarget`).
|
||||
let named = prefetch_relative_to(lua, rest.first(), h)?;
|
||||
let (who, base) = frame_ladder_context(lua, h);
|
||||
// The `_G` read inside the ladder runs with NO model guard alive — the reference's read is a
|
||||
// gettable, so an `__index` on `_G` can run Lua that calls back in (`object::NamedTarget`).
|
||||
let p = parse_set_point(lua, args, &who, &base)?;
|
||||
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
|
||||
// relativeTo: a table (wrapper) or a string (name), else default to the parent/screen. A table
|
||||
// wrapper may name a FRAME or a REGION (a Button anchored to a FontString by direct reference —
|
||||
// e.g. `button:SetPoint("TOPLEFT", someFontString, "BOTTOMLEFT", ...)` — is exactly as legal in
|
||||
// the real client as anchoring to a frame, and the string-branch below already accepts both), so
|
||||
// this must check id_to_region too, not just id_to_frame: an id that only lives in id_to_region
|
||||
// used to fail the frame-only filter and silently fall back to the default parent, misdirecting
|
||||
// the anchor with no error (caught via the quest-log reward rows, which anchor Buttons to
|
||||
// FontString regions by table reference — QuestLogFrame.xml's own comment on the port).
|
||||
let mut cursor = 0usize;
|
||||
let rel_to_id: u32 = match rest.first() {
|
||||
Some(Value::Table(t)) => {
|
||||
cursor = 1;
|
||||
decode_id(t)
|
||||
.ok()
|
||||
.filter(|id| {
|
||||
model.id_to_frame.contains_key(id) || model.id_to_region.contains_key(id)
|
||||
})
|
||||
.unwrap_or_else(|| default_parent_id(&mut model, h))
|
||||
}
|
||||
Some(Value::String(_)) => {
|
||||
cursor = 1;
|
||||
// `_G[name]`, frames and regions alike (see SetAllPoints above — same rationale), with
|
||||
// a leading `$parent` already expanded against this frame's first named ancestor.
|
||||
let nt = named.as_ref().expect("a String argument is prefetched");
|
||||
let hit = resolve_named_target(&model, nt);
|
||||
hit.unwrap_or_else(|| {
|
||||
// **The reference RAISES here** — `luaL_error(0x87ccd4, "%s:SetPoint(): Couldn't
|
||||
// find region named '%s'")`, no fallback and no no-op (`luaG_errormsg`/`luaD_throw`
|
||||
// contain no `ret`). Ours still falls back to the parent and warns, because we
|
||||
// cannot yet see how many of our own transcriptions and of the addon corpus that
|
||||
// raise would abort — the harness reports no engine warnings, which is the
|
||||
// instrument that has to come first (decision 2105, "Named, not done").
|
||||
//
|
||||
// The warning is worth keeping regardless: a *named* target that doesn't resolve is
|
||||
// almost always a bug, and this exact silence cost a hunt twice (QuestLogFrame's
|
||||
// reward rows, ItemTextFrame's scrollbar track).
|
||||
let who = model
|
||||
.arena
|
||||
.frame(h)
|
||||
.and_then(|f| f.name.clone())
|
||||
.unwrap_or_else(|| "<anonymous>".into());
|
||||
model.record_warning(format!(
|
||||
"SetPoint({who}): relativeTo '{}' does not resolve — anchored to the parent",
|
||||
nt.name
|
||||
));
|
||||
default_parent_id(&mut model, h)
|
||||
})
|
||||
}
|
||||
// An *explicit* `nil` relativeTo (`SetPoint(point, nil, relPoint, x, y)`) means "default
|
||||
// target" but still occupies its argument slot — consume it so the relativePoint/offsets
|
||||
// that follow line up. Only a leading *number* (the `SetPoint(point, x, y)` overload) or a
|
||||
// truly-absent arg leaves the cursor at 0.
|
||||
Some(Value::Nil) => {
|
||||
cursor = 1;
|
||||
default_parent_id(&mut model, h)
|
||||
}
|
||||
_ => default_parent_id(&mut model, h),
|
||||
};
|
||||
|
||||
// relativePoint: a string in the next slot, else defaults to `point`.
|
||||
let mut rel_point = point;
|
||||
if let Some(Value::String(s)) = rest.get(cursor) {
|
||||
if let Some(p) = s.to_str().ok().and_then(|n| point_from_str(n.as_ref())) {
|
||||
rel_point = p;
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let x = rest.get(cursor).map(as_f32).unwrap_or(0.0);
|
||||
let y = rest.get(cursor + 1).map(as_f32).unwrap_or(0.0);
|
||||
let me = model.frame_id(h);
|
||||
let parent = default_parent_id(&mut model, h);
|
||||
let rel_to_id = resolve_rel_target(&model, &p.target, &who, "SetPoint", me, parent)?;
|
||||
let (point, rel_point, x, y) = (p.point, p.rel_point, p.x, p.y);
|
||||
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
let new = Anchor::new(point, rel_to_id, rel_point, x, y);
|
||||
|
||||
Reference in New Issue
Block a user