diff --git a/crates/benilla-ui/src/script/object/anchor_args.rs b/crates/benilla-ui/src/script/object/anchor_args.rs index fade7e05..cea12199 100644 --- a/crates/benilla-ui/src/script/object/anchor_args.rs +++ b/crates/benilla-ui/src/script/object/anchor_args.rs @@ -1,6 +1,11 @@ //! The argument ladder of `SetPoint` (`0x7a2540`) and `SetAllPoints` (`0x7a2940`): the reference //! registers both once, in the Region method table (`0x87c9b8`), so every widget type runs the -//! same function. Every failure leg raises; none falls back to the parent or does nothing. +//! same function. Every failure leg raises; none falls back to the parent or does nothing — with +//! the single exception of an **absent** `relativeTo`, which `compat/` reads as the default parent +//! and whose reasoning sits at that leg in [`parse_set_point`]. It is written there rather than as +//! a wrapper in `compat/` precisely because of the sentence above: one function serves every +//! widget type, and three wrapped copies would break the identity `region_map.rs` maintains and +//! `tests::region_map` pins. use mlua::{Lua, MultiValue, Table, Value}; @@ -94,7 +99,30 @@ pub(crate) fn parse_set_point( }; let Some(rel_arg) = args.get(1) else { - return Err(usage(who)); + // **The one relaxation of this ladder, and it is `compat/`'s, not the reference's.** 1.12 + // raises `Usage:` here — the tag gate above accepts four tags and an absent argument is + // none of them — and later clients made `SetPoint("POINT")` mean the default parent at the + // same point with no offset. + // + // Allowed because the real `ClassicAPI.dll` writes that form in its own embedded Lua + // (`f:SetPoint("CENTER");`, `tx:SetPoint(q[4]);`), so on the client this build already + // announces itself as (`CLASSIC_API_VERSION`) it has to work, or ClassicAPI's own windows + // would not build. Same judgement, and the same inconsistency if withheld, as + // `compat/classic_api/stdlib_backport.rs`'s; a build dropping `compat/` wants this leg + // back as `Err(usage(who))` and nothing else. + // + // It resolves to exactly what the reference's own *three-argument* shorthand does + // (`RelTarget::DefaultParent`, `0x7a270e`): the default parent, `rel_point` defaulted to + // `point` below, and the offsets falling through to (0, 0) because there are none to read. + // So this adds no anchor shape — it spells one the ladder already answers. + let point = point_from_str(&point_str).ok_or_else(|| unknown_point(who))?; + return Ok(PointArgs { + point, + target: RelTarget::DefaultParent, + rel_point: point, + x: 0.0, + y: 0.0, + }); }; let target = match rel_arg { Value::String(s) => RelTarget::Named(match s.to_str() { diff --git a/crates/benilla-ui/src/script/tests/anchors.rs b/crates/benilla-ui/src/script/tests/anchors.rs index 6139bcd6..f2fb9768 100644 --- a/crates/benilla-ui/src/script/tests/anchors.rs +++ b/crates/benilla-ui/src/script/tests/anchors.rs @@ -126,22 +126,22 @@ fn setpoint_unresolved_name_raises() { assert!(s.take_warnings().is_empty()); } -/// The reference's other `SetPoint` raises: Usage (`0x87cc28`) when `relativeTo` is absent, since -/// `lua_type(L,3)` is `TNONE` at the gate (`0x7a25e6-0x7a2620`); "Unknown region point" -/// (`0x87cd04`, the 9-entry scan `0x6f1840`); "trying to anchor to itself" (`0x87cca8`, -/// `0x87cd54`). The type gates run before the point scan, so an unknown point with no -/// `relativeTo` answers Usage. +/// The reference's other `SetPoint` raises: "Unknown region point" (`0x87cd04`, the 9-entry scan +/// `0x6f1840`) and "trying to anchor to itself" (`0x87cca8`, `0x87cd54`); Usage (`0x87cc28`) for a +/// `relativeTo` whose `lua_type` is none of the four the gate takes +/// (`0x7a25e6-0x7a2620`). The type gates run before the point scan, so an unknown point with a +/// *bad* `relativeTo` answers Usage. +/// +/// **The absent-`relativeTo` leg is no longer one of them**: `object/anchor_args.rs` takes +/// `SetPoint("POINT")` as the default parent, because the real `ClassicAPI.dll` writes that form +/// itself. With no third argument there is no tag to gate, so an unknown point with no +/// `relativeTo` now reports the point rather than Usage — asserted below, since that ordering is +/// the thing the change moves. #[test] fn the_other_setpoint_raises() { let s = script(); s.run(r#"f = CreateFrame("Frame", "Ladder")"#).unwrap(); - let e = s - .run(r#"Ladder:SetPoint("CENTER")"#) - .unwrap_err() - .to_string(); - assert!(e.contains("Usage: Ladder:SetPoint(\"point\""), "{e}"); - let e = s .run(r#"Ladder:SetPoint("MIDDLE", nil, "MIDDLE", 0, 0)"#) .unwrap_err() @@ -152,7 +152,17 @@ fn the_other_setpoint_raises() { .run(r#"Ladder:SetPoint("MIDDLE")"#) .unwrap_err() .to_string(); - assert!(e.contains("Usage:"), "the tag gate runs first: {e}"); + assert!( + e.contains("Ladder:SetPoint(): Unknown region point"), + "nothing to tag-gate, so the point scan is what answers: {e}" + ); + + // A tag the gate does refuse still answers Usage, and still before the point scan. + let e = s + .run(r#"Ladder:SetPoint("MIDDLE", true)"#) + .unwrap_err() + .to_string(); + assert!(e.contains("Usage: Ladder:SetPoint(\"point\""), "{e}"); let e = s .run(r#"Ladder:SetPoint("CENTER", Ladder, "CENTER", 0, 0)"#) @@ -164,19 +174,49 @@ fn the_other_setpoint_raises() { ); } -/// `lua_type(L,3)` tells `LUA_TNIL` (0, the screen root) from `LUA_TNONE` (-1, Usage), so the -/// raise ladder must see the argument count Lua saw, not a padded or trimmed tuple. +/// `lua_type(L,3)` tells `LUA_TNIL` (0, the screen root) from `LUA_TNONE` (-1, which the +/// reference refuses and `compat/` reads as the default parent), so the ladder must see the +/// argument count Lua saw, not a padded or trimmed tuple. +/// +/// Both forms resolve now, and they must resolve to **different anchors** — that distinction is +/// the whole reason the count has to survive. A trailing nil is the screen root; an absent one is +/// the parent, which here is a 100x100 frame nowhere near the screen's corner. #[test] -fn a_trailing_nil_relative_to_is_present_where_an_absent_one_is_not() { - let s = script(); - s.run(r#"f = CreateFrame("Frame", "Trail")"#).unwrap(); - s.run(r#"Trail:SetPoint("CENTER", nil)"#).unwrap(); - assert_eq!(s.eval::("return Trail:GetNumPoints()").unwrap(), 1); - let e = s - .run(r#"Trail:SetPoint("CENTER")"#) - .unwrap_err() - .to_string(); - assert!(e.contains("Usage:"), "{e}"); +fn a_trailing_nil_relative_to_is_the_screen_where_an_absent_one_is_the_parent() { + let mut s = script(); + s.set_screen_size(800.0, 600.0); + s.run( + r#" + host = CreateFrame("Frame", "TrailHost") + TrailHost:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 300, 200) + TrailHost:SetWidth(100) TrailHost:SetHeight(100) + nilled = CreateFrame("Frame", "TrailNil", TrailHost) + TrailNil:SetWidth(10) TrailNil:SetHeight(10) + TrailNil:SetPoint("BOTTOMLEFT", nil) + absent = CreateFrame("Frame", "TrailAbsent", TrailHost) + TrailAbsent:SetWidth(10) TrailAbsent:SetHeight(10) + TrailAbsent:SetPoint("BOTTOMLEFT") + "#, + ) + .unwrap(); + s.resolve(); + assert_eq!( + s.eval::("return TrailNil:GetNumPoints()").unwrap(), + 1, + "a trailing nil is an argument, not an absence" + ); + assert_eq!( + s.eval::<(f32, f32)>("return TrailNil:GetLeft(), TrailNil:GetBottom()") + .unwrap(), + (0.0, 0.0), + "the explicit nil anchored to the screen root" + ); + assert_eq!( + s.eval::<(f32, f32)>("return TrailAbsent:GetLeft(), TrailAbsent:GetBottom()") + .unwrap(), + (300.0, 200.0), + "the absent one anchored to the parent, as the three-argument shorthand does" + ); } /// An explicit `nil` `relativeTo` is the screen root, not the parent (`0x7a2710` loads it from