plate text settles, mobs stop sinking, and select goes with Lua 5.1

- a nameplate's name and level stop popping a pixel every other step as the plate slides, and the whole plate no longer jumps 19 px to the top of the screen on a rounding tie; the chat bubble takes the same fix (2172)
- creatures and other players stop sinking through the ground while the world is still streaming in, where a Stormwind patroller had been 31 yd under its street: a body our world cannot yet answer for holds the server's pose (2174)
- select and the string metatable are gone, as in 1.12's Lua 5.0: pfUI's heal prediction parses its messages with the right format, BuffCheck2's own select wins its fallback, and a method call on a string raises the reference's own error (2171)
- the camera's water collision comes back out for now: with the pivot a hair under the plane, a surface swimmer's camera snapped in and out against the waterline, so the boom ignores water again (2173)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-11 21:03:05 +07:00
co-authored by Claude Fable 5.1
parent 70a8ad9d60
commit 5af3a15d0e
88 changed files with 1107 additions and 860 deletions
@@ -1866,6 +1866,7 @@ fn convert_entry(
shadow,
outline,
alpha_gradient,
world_seat,
} => {
// No atlas (no client data / Friz Quadrata unreadable — see `ui_text`'s startup
// system) means text simply doesn't render, same graceful-absence posture as a
@@ -1887,6 +1888,7 @@ fn convert_entry(
shadow,
outline,
alpha_gradient,
world_seat,
},
text::TextHost {
z: eq.z,
@@ -11,7 +11,7 @@ use benilla_ui::script::{EditBoxTextUi, FontShadow, JustifyH, JustifyV, Outline}
use benilla_ui::widget::FrameHandle;
use crate::ui_pass::{UiQuad, UvRect};
use crate::ui_text::{layout_text_quads, layout_text_quads_links, UiFontAtlas};
use crate::ui_text::{layout_text_quads, layout_text_quads_links, TextSeat, UiFontAtlas};
/// `WOW_TEXT_PROBE=1` — launch-time knob, read once (the check ran per Text quad per frame).
static TEXT_PROBE: std::sync::LazyLock<bool> =
@@ -29,6 +29,9 @@ pub(super) struct TextStyle {
pub shadow: Option<FontShadow>,
pub outline: Outline,
pub alpha_gradient: Option<(f32, f32)>,
/// The region's seat claim (`benilla_ui`'s `RegionData::world_seat`) — true for the V-plate's
/// name and level, which are rigid to a device-snapped overlay rather than to the UI grid.
pub world_seat: bool,
}
/// The host-loop context one Text quad draws under: the extracted quad's identity (z/alpha/
@@ -71,6 +74,14 @@ pub(super) fn emit(
link_spans: &mut Vec<(FrameHandle, benilla_ui::layout::Rect, String, String)>,
) {
let base_color = style.color.unwrap_or([1.0, 1.0, 1.0, 1.0]);
// Which pixel grid this block's top may land on (decision 2172): the interface's, or none —
// the WorldFrame overlays carry their own device-pixel seat and their text has to be rigid
// to it. The shadow pass below inherits it, like every other layout input.
let seat = if style.world_seat {
TextSeat::Exact
} else {
TextSeat::UiGrid
};
let spec = crate::ui_text::FontSpec {
path: style.font.as_deref(),
// The drawn px under the two size regimes × the 768-virtual scale × the owner's frame
@@ -249,6 +260,7 @@ pub(super) fn emit(
draw_justify,
host.z,
shadow_spec,
seat,
);
for q in &mut sq {
// Flatten rgb to the shadow color (markup tints ride the fill only); the alpha is
@@ -282,6 +294,7 @@ pub(super) fn emit(
},
host.z,
spec,
seat,
&mut spans,
);
for sp in spans {
@@ -308,6 +321,7 @@ pub(super) fn emit(
draw_justify,
host.z,
spec,
seat,
)
};
for q in &mut glyphs {
@@ -321,21 +335,43 @@ pub(super) fn emit(
// (`fontstring-vertical-placement.md`): compare `ink` against the law's `d + ascender` seat
// when hunting a vertical offset. Fill quads only (the shadow pass above would smear the
// bounds one px down-right).
if probe && !glyphs.is_empty() {
let vpl = style.world_seat && benilla_assets::trace::enabled_for("vpl");
if (probe || vpl) && !glyphs.is_empty() {
let (mut y0, mut y1) = (f32::MAX, f32::MIN);
for q in &glyphs {
y0 = y0.min(q.rect.min.y);
y1 = y1.max(q.rect.max.y);
}
info!(
"seat probe: top={:.2} ink=[{:.2}..{:.2}] (rel {:.2}..{:.2}) {:?}",
host.rect.min.y,
y0,
y1,
y0 - host.rect.min.y,
y1 - host.rect.min.y,
&draw_text[..draw_text.len().min(20)]
);
// **Where the plate's text actually inked**, on the same `vpl` tag the driver's `plate=`
// seat and the border's `paint=` line ride ([`crate::vplates`], decision 2168's
// measurement). The question the three lines answer together is not "does the text
// move?" — it is "does it move WITH the border?", which is a difference of two numbers
// per frame. Decision 2168 §5 named this line as the missing third; 2172 is what it
// found when it was built.
if vpl {
benilla_assets::trace::line(
"vpl",
&format!(
"text x={:.3} rect={:.3} ink={:.3} rel={:.3} txt={}",
host.rect.min.x,
host.rect.min.y,
y0,
y0 - host.rect.min.y,
draw_text.chars().take(16).collect::<String>()
),
);
}
if probe {
info!(
"seat probe: top={:.2} ink=[{:.2}..{:.2}] (rel {:.2}..{:.2}) {:?}",
host.rect.min.y,
y0,
y1,
y0 - host.rect.min.y,
y1 - host.rect.min.y,
draw_text.chars().take(20).collect::<String>()
);
}
}
out.extend(glyphs);
// The focused box's caret: a 1-px WHITE bar (the client's ctor `0xffffffff` caret texture —
+13 -12
View File
@@ -1236,9 +1236,10 @@ fn removing_a_member_names_them_in_the_confirm() {
/// 5.0→5.1 vararg swap.** The one place the window and the engine are coupled by *arity* rather
/// than by a name.
///
/// The reference walks the 5.0 vararg table with `for i = 1, arg.n`; ours walks
/// `for i = 1, select("#", ...)`. The two agree only while every one of the thirteen returns is
/// actually pushed — and with no rank loaded, ALL THIRTEEN ARE NIL (era booleans are `1`/`nil`).
/// The reference walks the 5.0 vararg table with `for i = 1, arg.n`, and that stock file is the
/// one this window loads, so the checkbox loop's bound *is* whatever the binding pushed. The two
/// agree only while every one of the thirteen returns is actually pushed — and with no rank
/// loaded, ALL THIRTEEN ARE NIL (era booleans are `1`/`nil`).
/// A binding that returned "as many values as are true" would look identical at every other
/// assertion in this file, load without an error, and leave checkboxes stale from the last rank.
/// So this one deliberately does NOT install the fixture: it asks the real `script::guild`.
@@ -1247,8 +1248,7 @@ fn the_rank_flags_binding_answers_thirteen_values_even_when_all_are_nil() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
assert_eq!(
s.eval::<i64>("return select(\"#\", GuildControlGetRankFlags())")
.unwrap(),
s.arity("GuildControlGetRankFlags()").unwrap(),
13,
"an unloaded buffer is all-nil, and every one of the thirteen must still be pushed — \
`GuildControlCheckboxUpdate` drives checkbox i off argument i and nothing else"
@@ -1256,9 +1256,12 @@ fn the_rank_flags_binding_answers_thirteen_values_even_when_all_are_nil() {
// …and each of them is nil, not `false`: the era boolean law, which `SetChecked` reads.
assert_eq!(
s.eval::<i64>(
"local n = 0 \
for i = 1, 13 do if select(i, GuildControlGetRankFlags()) ~= nil then n = n + 1 end end \
return n"
"local function count(...) \
local n = 0 \
for i = 1, 13 do if arg[i] ~= nil then n = n + 1 end end \
return n \
end \
return count(GuildControlGetRankFlags())"
)
.unwrap(),
0
@@ -1352,16 +1355,14 @@ fn the_roster_binding_answers_ten_values_and_the_tenth_is_status() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
assert_eq!(
s.eval::<i64>("return select(\"#\", GetGuildRosterInfo(1))")
.unwrap(),
s.arity("GetGuildRosterInfo(1)").unwrap(),
10,
"an out-of-range index still pushes all ten — the reference calls this with \
GetGuildRosterSelection(), which is 0 whenever nothing is selected, on every \
GuildStatus_Update pass before it ever checks `> 0`"
);
assert_eq!(
s.eval::<i64>("return select(\"#\", GetGuildRosterInfo(0))")
.unwrap(),
s.arity("GetGuildRosterInfo(0)").unwrap(),
10,
"…including index 0, the nothing-selected case the reference passes unguarded"
);
@@ -1140,8 +1140,11 @@ fn loot_row_awaiting_its_template_opens_clean() {
.unwrap();
assert_eq!((item.as_str(), quantity, quality), ("", 1, -1));
assert!(
s.eval::<bool>("return ITEM_QUALITY_COLORS[select(4, GetLootSlotInfo(1))] ~= nil")
.unwrap(),
s.eval::<bool>(
"local _, _, _, quality = GetLootSlotInfo(1) \
return ITEM_QUALITY_COLORS[quality] ~= nil"
)
.unwrap(),
"the cache-miss quality must be a real row of ITEM_QUALITY_COLORS"
);
@@ -326,11 +326,7 @@ fn get_ping_position_answers_two_numbers_always() {
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
assert_eq!(
s.eval::<i64>("return select('#', Minimap:GetPingPosition())")
.unwrap(),
2
);
assert_eq!(s.arity("Minimap:GetPingPosition()").unwrap(), 2);
assert_eq!(
s.eval::<f32>("return (Minimap:GetPingPosition())").unwrap(),
0.0,
@@ -4402,6 +4402,20 @@ const UNBACKED_REFERENCE_CVARS: &[(&str, &str)] = &[
one step further back than its guild twin: the rank byte streams, but the key's second \
index is a FACTION SIDE that `ui_unit` does not resolve for an arbitrary player yet",
),
(
"cameraWaterCollision",
"REVERTED 2026-09-10, and the row is back because the feature is not. Two consumers were \
transcribed off `pivot-height-glide.md` §5: (1) the collision sweep's class mask \
becomes `0x1f0171` instead of `0x100171` — the `0xf0000` nibble is the ADT liquid \
layers — and `0x511ad0`'s bands re-base the pivot floor/cap; (2) a band CROSSING \
arms a 5° pitch through the `+0xf4` channel. Built as (1)=the pivot corridor alone, \
on the reading that the arm stays liquid-blind. **That corridor is discontinuous**: at \
`excess = 2/9` the framing pivot drops 1.04 yd in one frame and pops back 0.29 yd at \
`5/9`, so entering or leaving water at head depth slams the camera — which is what \
the director saw. Whether the discontinuity is the reference's or a mis-transcription is \
a live §5 (2165). Registered \"1\", so benilla is the divergence while this row \
stands",
),
// **The SLIDERS' half is empty, and that is the point.** 2115 extended this census from
// `UIOptionsFrameCheckButtons` to `UIOptionsFrameSliders` and it caught exactly one row —
// `cameraYawMoveSpeed`, the MOUSE_LOOK_SPEED slider — which was not cosmetic:
+10 -4
View File
@@ -207,6 +207,12 @@ fn every_query_binding_answers_the_reference_s_return_arity() {
// A call that raises still says nothing about arity. The unit bindings are the reason:
// they gate their arguments and raise on a nil token (1834/1836), so they are called with
// a real token against the body seated above rather than with nothing.
//
// The count is taken with 5.0's own `arg.n`, for the reason [`KIND_HELPER`] states one
// gate down plus the one that settled it: `select` is 5.1's base library, not a 1.12
// global, and `lua50::install` removes it. Where the count is wanted on the HOST side we
// ask `UiScript::arity`; these four probes have to hold it INSIDE a `pcall`, and there
// the vararg's own `n` is the 1.12 spelling.
let call = if name.starts_with("Unit") {
format!(r#"{name}("player")"#)
} else {
@@ -214,7 +220,7 @@ fn every_query_binding_answers_the_reference_s_return_arity() {
};
let probe = format!(
"if type({name}) ~= 'function' then return -1 end \
local ok, n = pcall(function() return select('#', {call}) end) \
local ok, n = pcall(function() return (function(...) return arg.n end)({call}) end) \
if not ok then return -1 end return n"
);
let got: i64 = match s.eval(&probe) {
@@ -256,7 +262,7 @@ fn every_query_binding_answers_the_reference_s_return_arity() {
by_name.get(n).is_some_and(|rs| {
let want = rs[0].arity;
s.eval::<i64>(&format!(
"local ok, n = pcall(function() return select('#', {n}()) end) \
"local ok, n = pcall(function() return (function(...) return arg.n end)({n}()) end) \
if not ok then return -1 end return n"
))
.is_ok_and(|got| got >= 0 && got as usize == want)
@@ -374,7 +380,7 @@ fn the_base_library_answers_the_reference_s_return_arity_and_kinds() {
let name = &r.name;
let probe = format!(
"if type({name}) ~= 'function' then return -1 end \
local ok, n = pcall(function() return select('#', {name}({args})) end) \
local ok, n = pcall(function() return (function(...) return arg.n end)({name}({args})) end) \
if not ok then return -1 end return n"
);
let got: i64 = match s.eval(&probe) {
@@ -572,7 +578,7 @@ fn every_widget_method_answers_the_reference_s_return_arity() {
let probe = format!(
"if type({obj}.{name}) ~= 'function' then return -1 end \
local ok, n = pcall(function() return select('#', {obj}:{name}()) end) \
local ok, n = pcall(function() return (function(...) return arg.n end)({obj}:{name}()) end) \
if not ok then return -1 end return n"
);
let Ok(got) = s.eval::<i64>(&probe) else {
@@ -323,7 +323,7 @@ fn a_real_hunters_block_lists_exactly_what_the_reference_client_lists() {
}
// And the class lines still read as proficiencies despite their 300/300 descriptor.
assert_eq!(
s.eval::<i64>("return (select(7, GetSkillLineInfo(2)))")
s.eval::<i64>("local _,_,_,_,_,_,mx = GetSkillLineInfo(2) return mx")
.unwrap(),
1,
"Beast Mastery's skillMaxRank"
@@ -624,8 +624,11 @@ fn the_list_reaches_its_last_row_at_the_bars_end() {
// The knob dragged to the end: bar → SetVerticalScroll → <OnVerticalScroll> →
// FauxScrollFrame_OnVerticalScroll → SkillFrame_UpdateSkills.
s.run("SkillListScrollFrameScrollBar:SetValue(select(2, SkillListScrollFrameScrollBar:GetMinMaxValues()))")
.unwrap();
s.run(
"local _, hi = SkillListScrollFrameScrollBar:GetMinMaxValues() \
SkillListScrollFrameScrollBar:SetValue(hi)",
)
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.eval::<i64>("return FauxScrollFrame_GetOffset(SkillListScrollFrame)")
@@ -311,18 +311,10 @@ fn the_bindings_answer_the_reference_shape() {
Alliance Towers Controlled|||1|2|3",
"empty columns are empty strings, never nil"
);
assert_eq!(
s.eval::<i64>("return select('#', GetWorldStateUIInfo(1))")
.unwrap(),
10
);
assert_eq!(s.arity("GetWorldStateUIInfo(1)").unwrap(), 10);
// Out of range: ONE value, the number 0.
assert_eq!(
s.eval::<i64>("return select('#', GetWorldStateUIInfo(2))")
.unwrap(),
1
);
assert_eq!(s.arity("GetWorldStateUIInfo(2)").unwrap(), 1);
assert_eq!(s.eval::<i64>("return GetWorldStateUIInfo(2)").unwrap(), 0);
assert_eq!(
s.eval::<i64>("return GetWorldStateUIInfo(0)").unwrap(),