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:
@@ -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 —
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user