the world shrinks without the UI, and a party member keeps their face

- `renderScale`: the world renders into its off-screen target at a fraction — or a multiple — of the window while text, icons and frame art stay native; the logical viewport comes out arithmetically unchanged, so picking cannot see the scale at all, and every hand-written sampler takes the `log2(scale)` mip bias below 100 % and none above it, where the sharper mip is what makes supersampling work — measured here at 26x the world pixels for +0.02 ms, and reachable as a Render Scale row on the Graphics page (50–200 %), staged to Apply so a dragged slider rebuilds the world target once instead of thirty times a sweep
- a party member who walks out of range keeps their numbers and their face (B334): the client asks — `CMSG_REQUEST_PARTY_MEMBER_STATS` goes out on the despawn edge and for a roster member gained unseen, the same two edges the reference sends from — the record is snapshotted whole from the live descriptor at the moment the object deactivates, a never-seen member seats at the reference's 1/1 placeholder rather than an empty bar, and the raw-to-display divide happens at the read, so an out-of-range warrior stops reading ten times the rage; the portrait binds a guid-keyed bake cache built as a handover — the booth's own target image moves into the cache as its member streams out, so the face freezes with no copy and no flicker — and the self-review adds that a changed occupant is a re-bake and the record's dead/ghost bits are read
- a slider row's number sits on its label's line (B232): the readout hung off the control frame and inherited a +3 art nudge that exists to seat the groove texture — four pixels off on an eleven-pixel cap height at the reporter's geometry — so the nudge moves onto the two frames that draw the art, across eight rows on three pages, and the bar itself does not move a pixel; `$WOW_DPI` renders on a player's pixel grid, so a 1x reporter's screenshot and a capture from this 2x machine compare pixel-for-pixel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-27 15:15:06 +07:00
co-authored by Claude Fable 5
parent 6b2a75e8a0
commit 72ca69ef5d
24 changed files with 1525 additions and 142 deletions
@@ -604,6 +604,60 @@ fn a_track_press_seats_the_thumb_and_keeps_dragging() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **A slider row's readout sits on its label's line.** The row shows one setting as two
/// `GameFontNormal` strings — the name at the left, the value at the right — and a reader takes
/// them as a single line; ours were **3 units apart** at every UI scale (≈4 px on a maximised
/// 1440p window, which is where B232 photographed them).
///
/// The cause is a nudge that leaked from the art onto the text: the readout hangs off
/// `$parentControl`, and the control was seated at the row's `CENTER(-80, +3)` — the +3 lifting
/// the 17-tall groove and its 32 px thumb inside the 26-tall row. Anything anchored to the
/// control inherited a seat that exists for a texture. The nudge now lives on the two art frames
/// it was for (`$parentGroove`, `$parentSlider`), so the bar does not move by a pixel and the
/// number falls onto the label's line — asserted here on both halves, over every slider row the
/// window has: a label/value line, and the groove still riding 3 units high of the row.
#[test]
fn a_slider_rows_readout_sits_on_its_labels_line() {
let mut s = harness_on(audio_harness());
s.run("ShowUIPanel(OptionsFrame)").unwrap();
for (page, rows) in [
("Controls", &["RowMouseSpeed", "RowMaxCameraDistance"][..]),
(
"Audio",
&["RowMaster", "RowSound", "RowMusic", "RowAmbience"][..],
),
("Graphics", &["RowUiScale", "RowFarclip"][..]),
] {
s.run(&format!("OptionsFrameCategoryListRow{page}:Click()"))
.unwrap();
s.resolve(); // seat the rects before reading them
let mid = |frame: &str| -> f32 {
let top: f32 = s.eval(&format!("return {frame}:GetTop()")).unwrap();
let bottom: f32 = s.eval(&format!("return {frame}:GetBottom()")).unwrap();
(top + bottom) * 0.5
};
for row in rows {
let base = format!("OptionsFrameContainerBody{page}{row}");
// The two strings are the assertion: same font, same row, one line. (Both are
// regions, so both rects arrive in the same space — the comparison needs no scale.)
let (label, value) = (
mid(&format!("{base}Label")),
mid(&format!("{base}ControlValue")),
);
assert!(
(label - value).abs() < 0.01,
"{base}: the readout sits {:.2} off its label's line",
value - label
);
// …and the art it used to ride is exactly where it was: 3 units high of the row.
assert!(
(mid(&format!("{base}ControlGroove")) - mid(&base) - 3.0).abs() < 0.01,
"{base}: the groove left its seat"
);
}
}
}
/// The Audio harness: the real registered CVar set on the table before the XML loads, exactly
/// the app's boot order (register → seed → load → select).
fn audio_harness() -> UiScript {
@@ -1337,6 +1391,89 @@ fn the_ui_scale_slider_defers_to_the_apply_button() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **Render Scale** (decision 1639) — benilla's own Graphics row, seated directly above
/// Multisampling because above 100 % it *is* antialiasing, on a client whose `gxMultisample`
/// ships off (1629).
///
/// Three things this pins, each of which was a real choice:
///
/// - **It reads the CVar and shows a percentage.** 1.0 must render as "100%", because the whole
/// point of the row is that the number in front of the player means something without a manual.
/// - **It is DEFERRED.** Committing live would rebuild the world's render target on every drag
/// tick — tens of megabytes, thirty times across one sweep of the handle. Staging to Apply makes
/// that exactly one rebuild. (`uiScale` above it defers for a different reason; the flag is the
/// same.)
/// - **It is not mute.** Every other tooltip on this window resolves an `OPTION_TOOLTIP_*` out of
/// 1.12's GlobalStrings, and a benilla row with no counterpart there has so far gone silent (the
/// nameplate pair). This one carries a description under a `BENILLA_` prefix — the reference's
/// namespace stays the reference's — because the dial's first reviewer said outright that they
/// could not tell what it did.
#[test]
fn the_render_scale_row_shows_a_percentage_and_defers_to_apply() {
let mut s = audio_harness();
s.set_cvar_host("renderScale", "1");
let mut s = harness_on(s);
s.run("ShowUIPanel(OptionsFrame)").unwrap();
s.run("OptionsFrameCategoryListRowGraphics:Click()")
.unwrap();
assert_eq!(
s.eval::<String>("return OptionsFrameContainerBodyGraphicsRowRenderScaleLabel:GetText()")
.unwrap(),
"Render Scale"
);
assert_eq!(
s.eval::<String>(
"return OptionsFrameContainerBodyGraphicsRowRenderScaleControlValue:GetText()"
)
.unwrap(),
"100%",
"off has to read as 100%, not as 1"
);
// The player's range is 50–200 %, narrower than the CVar's own clamp (0.25–4.0, which leaves
// room for the supersampling instrument). A row that offered the whole clamp would put 400 %
// in front of someone who only wanted their frame rate back.
assert!(s
.eval::<bool>(
"local lo, hi = OptionsFrameContainerBodyGraphicsRowRenderScaleControlSlider:GetMinMaxValues() return math.abs(lo - 0.5) < 0.0001 and math.abs(hi - 2.0) < 0.0001"
)
.unwrap());
// Not mute: the row resolves a description, unlike the other benilla-own rows.
assert!(s
.eval::<bool>("return BENILLA_TOOLTIP_RENDER_SCALE ~= nil")
.unwrap());
let _ = s.take_cvar_changes();
// The move stages and shows Apply; it does NOT rebuild the render target.
s.run("OptionsFrameContainerBodyGraphicsRowRenderScaleControlSlider:SetValue(0.75)")
.unwrap();
assert_eq!(
s.eval::<String>(
"return OptionsFrameContainerBodyGraphicsRowRenderScaleControlValue:GetText()"
)
.unwrap(),
"75%"
);
assert!(
s.take_cvar_changes().is_empty(),
"a deferred row must not write the CVar on the move"
);
assert!(s
.eval::<bool>("return OptionsFrameApplyButton:IsVisible()")
.unwrap());
// Apply commits once.
s.run("OptionsFrameApplyButton:Click()").unwrap();
assert_eq!(
s.take_cvar_changes(),
vec![("renderScale".to_string(), "0.75".to_string())]
);
assert!(!s
.eval::<bool>("return OptionsFrameApplyButton:IsVisible()")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Pending edits are PANEL-wide like era's modified table: they survive a category switch
/// (the row redisplays the pending value on return, era's GetValue-returns-pending) and die
/// only when the window hides — the reopened window reads the committed truth.
@@ -1996,6 +2133,23 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
untipped.push(row.to_string());
continue;
}
// The one deliberate exception (decision 1639). Render Scale has no 1.12 counterpart
// and so no `OPTION_TOOLTIP_*` to resolve, but it is the row that most needs a
// description — its first reviewer said outright they could not tell what it did. It
// carries one under a `BENILLA_` prefix so the reference's namespace stays the
// reference's, which is exactly what this guard is here to protect. Everything the
// guard was built to catch — an invented or typo'd `OPTION_TOOLTIP_` key that silently
// resolves to nothing — is untouched by the carve-out.
if key == "BENILLA_TOOLTIP_RENDER_SCALE" {
assert_eq!(
row, "GraphicsRowRenderScale",
"{row}: not this row's string"
);
let text: String = s.eval(&format!("return {key}")).unwrap();
assert!(!text.is_empty(), "{row}: {key} resolves to nothing");
checked += 1;
continue;
}
assert!(
key.starts_with("OPTION_TOOLTIP_"),
"{row}: {key} is not a 1.12 option-tooltip key"
@@ -2024,7 +2178,9 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
// bar as the other two. Camera Following Style is counted on the key it wears at rest (Smart's
// OPTION_TOOLTIP_CAMERA1) and Show When on its own (Always's OPTION_TOOLTIP_TARGETOFTARGET5);
// their other entries ride the same census as the selection moves.
assert_eq!(checked, 55, "every tipped row carries a live 1.12 key");
// 55 of the 56 are 1.12's own; the 56th is Render Scale, whose description is benilla's
// (1639) and whose carve-out is above.
assert_eq!(checked, 56, "every tipped row carries a live key");
assert_eq!(
untipped,
vec![
@@ -2129,7 +2285,9 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
// (the lock 1136, Always Show
// ActionBars 1500), the Chat page's 1 (Remove Chat Hover Delay, 1589) and 6 API rows (Show
// Cloak / Show Helm, 1472; the four multibar switches, 1500).
assert_eq!(raised, 55, "every row but Auto Loot has a 1.12 description");
// …plus the Graphics page's Render Scale (1639), the one row whose description is
// benilla's own rather than a 1.12 GlobalString — see the tooltip-key guard above.
assert_eq!(raised, 56, "every row but Auto Loot raises a description");
}
/// The **Combat page** (decision 1134) — the first rows in this window whose store is a