diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index 2bea1780..2bb70146 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -420,6 +420,19 @@
.. "Below 100% trades sharpness for frame rate; above 100% is supersampling, which "
.. "smooths edges at the cost of frame rate. Applies when you click Apply."
+ -- 1.12's own OPTION_TOOLTIP_ANISOTROPIC describes a four-stop slider that takes
+ -- effect on restart. Ours is neither: the groove runs the CVar's own 1-16 (the
+ -- reference's callback parses that range, `0x689110`; only its *slider* stopped at 4),
+ -- and it commits in place on Apply. So the text is ours.
+ BENILLA_TOOLTIP_ANISOTROPIC =
+ "Sharpens ground and wall texture seen at a grazing angle - the road running away "
+ .. "from you, a floor stretching into a room.\n\n"
+ .. "Off is what the original client shipped, which smears that texture to mush a few "
+ .. "yards out. 16x costs about a millisecond of graphics time and nothing at all on "
+ .. "the processor, so it is close to free on any modern card.\n\n"
+ .. "Each texture is sampled the way it was when it loaded, so this takes effect the "
+ .. "next time you start the game."
+
-- 1.12 has no such checkbox.
BENILLA_TOOLTIP_BACKGROUND_SOUND =
"Keeps the game's sound playing while you are working in another window.\n\n"
@@ -1051,7 +1064,8 @@
},
Graphics = {
"RowUiScale", "RowFarclip", "RowWorldDetail", "RowBrightness",
- "RowWeatherDensity",
+ -- GFX (anisotropic): index 7 of OptionsFrameSliders, in that table's own order.
+ "RowAnisotropic", "RowWeatherDensity",
"RowDisplayMode", "RowResolution", "RowVerticalSync",
"RowRenderScale",
},
@@ -1257,7 +1271,15 @@
{ text = WEATHER_DETAIL, func = "weatherDensity", minValue = 0, maxValue = 3, valueStep = 1, tooltipText = OPTION_TOOLTIP_WEATHER_DETAIL },
}
- -- Holes at 4, 5, 7 and 8, so the OnShow loop uses the pairs form: ipairs would stop at 3.
+ -- The rows whose bounds ARE the stock table's, re-applied on show so an addon's write to
+ -- OptionsFrameSliders lands. Holes at 4, 5, 7 and 8, so the OnShow loop uses the pairs
+ -- form: ipairs would stop at 3.
+ --
+ -- 7 (ANISOTROPIC) is a seated row but deliberately not listed: the stock entry caps at 4,
+ -- which was the reference *slider's* limit and not the CVar's (its callback parses 1-16,
+ -- `0x689110`), and High and Ultra both ask for 16. So that row carries its own literal
+ -- bounds, as `RowRenderScale` does for a CVar the table never had — and the table above
+ -- stays verbatim for any addon that reads it.
BENILLA_GRAPHICS_SLIDER_ROWS = {
"RowUiScale", "RowFarclip", "RowWorldDetail",
[6] = "RowBrightness", [9] = "RowWeatherDensity",
@@ -1430,6 +1452,14 @@
if row.fmt == "yd" then
return string.format(BENILLA_ADVGFX.yards, math.floor(value + 0.5))
end
+ -- The anisotropy groove. 1 is not "1x", it is the feature OFF (`tex_filter.rs`:
+ -- the reference writes filter mode 5 only at 2 and above), and a player reading "1x"
+ -- next to "2x" would take it for the weakest setting rather than for none.
+ if row.fmt == "aniso" then
+ local n = math.floor(value + 0.5)
+ if n <= 1 then return "Off" end
+ return string.format("%dx", n)
+ end
return string.format("%d%%", math.floor(value * 100 + 0.5))
end
@@ -2792,12 +2822,48 @@
OptionsFrameSliders[6].valueStep, "travel")
+
+
+
+
+
+
+ -- Index 7 of OptionsFrameSliders, seated where the
+ -- reference's own slider order puts it: after
+ -- Brightness, before Weather Intensity. Before this
+ -- the row was a hole, so the only way to anisotropy
+ -- was `/console anisotropic 16` — and a player on a
+ -- Custom graphics preset never met it at all, since
+ -- the preset ladder is what seeds the value.
+ OptionsRow_OnLoad(self, "anisotropic", "Anisotropic Filtering",
+ "BENILLA_TOOLTIP_ANISOTROPIC")
+ -- 1 to 16, the CVar's own range (`ANISO_RANGE`),
+ -- not the stock table's 1-4 — see
+ -- BENILLA_GRAPHICS_SLIDER_ROWS. Step 1 rather than
+ -- a power-of-two ladder because the value is a
+ -- MAXIMUM the driver is free to round down: an
+ -- off-ladder stop is a weaker setting, never a
+ -- wrong one, which is the test `RowShadowResolution`
+ -- fails and so takes a dropdown instead.
+ OptionsSliderRow_Setup(self, 1, 16, 1, "aniso")
+ -- DEFERRED, as the two rows below it are, because
+ -- the write does not reach a texture that is
+ -- already resident: a sampler is baked into each
+ -- `Image` at load (`tex_filter.rs`), which is why
+ -- the reference's own UI says "enabled upon
+ -- restart". Apply is therefore the honest commit
+ -- point, and a drag does not write a CVar on every
+ -- step of the groove that only the next launch
+ -- reads.
+ self.deferred = 1
+
+
-
-
+
+
OptionsRow_OnLoad(self, "weatherDensity", "Weather Intensity",
diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs
index ca47d517..d751ae65 100644
--- a/crates/benilla-app/src/ui_script/options_tests.rs
+++ b/crates/benilla-app/src/ui_script/options_tests.rs
@@ -1704,6 +1704,71 @@ fn the_render_scale_row_shows_a_percentage_and_defers_to_apply() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
+/// GFX (anisotropic): the row that was a hole. Three things it has to get right — the groove
+/// reaches the CVar's own 16 and not the stock table's 4, `1` reads as "Off" and not as "1x", and
+/// the write waits for Apply (committing re-samples every resident texture, which a drag would
+/// pay on every step).
+#[test]
+fn the_anisotropic_row_reaches_sixteen_reads_off_at_one_and_defers_to_apply() {
+ benilla_formats::wow_data_or_skip!();
+ let mut s = audio_harness();
+ s.set_cvar_host("anisotropic", "1");
+ let mut s = harness_on(s);
+ s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
+ s.run("BenillaOptionsFrameCategoryListRowGraphics:Click()")
+ .unwrap();
+ let row = "BenillaOptionsFrameContainerBodyGraphicsRowAnisotropic";
+
+ assert_eq!(
+ s.eval::(&format!("return {row}Label:GetText()"))
+ .unwrap(),
+ "Anisotropic Filtering"
+ );
+ assert_eq!(
+ s.eval::(&format!("return {row}ControlValue:GetText()"))
+ .unwrap(),
+ "Off",
+ "1 is the feature off, not its weakest stop"
+ );
+ // The CVar's own `ANISO_RANGE`, not `OptionsFrameSliders[7]`'s 1-4, which was the reference
+ // slider's ceiling and is below both the High and the Ultra rung.
+ assert!(s
+ .eval::(&format!(
+ "local lo, hi = {row}ControlSlider:GetMinMaxValues() \
+ return lo == 1 and hi == 16"
+ ))
+ .unwrap());
+ assert!(s
+ .eval::("return BENILLA_TOOLTIP_ANISOTROPIC ~= nil")
+ .unwrap());
+ // The stock table stays verbatim for any addon that reads it.
+ assert!(s
+ .eval::("return OptionsFrameSliders[7].maxValue == 4")
+ .unwrap());
+ let _ = s.take_cvar_changes();
+
+ s.run(&format!("{row}ControlSlider:SetValue(16)")).unwrap();
+ assert_eq!(
+ s.eval::(&format!("return {row}ControlValue:GetText()"))
+ .unwrap(),
+ "16x"
+ );
+ assert!(
+ s.take_cvar_changes().is_empty(),
+ "a deferred row must not write the CVar on the move"
+ );
+ assert!(s
+ .eval::("return BenillaOptionsFrameApplyButton:IsVisible()")
+ .unwrap());
+
+ s.run("BenillaOptionsFrameApplyButton:Click()").unwrap();
+ assert_eq!(
+ s.take_cvar_changes(),
+ vec![("anisotropic".to_string(), "16".to_string())]
+ );
+ assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+}
+
/// Pending edits are panel-wide, as the era's modified table: a category switch keeps them, and
/// hiding the window discards them.
#[test]
@@ -3125,6 +3190,9 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
),
("BENILLA_TOOLTIP_LAVA_GLOW", "AdvancedGraphicsRowLavaGlow"),
("BENILLA_TOOLTIP_RENDER_SCALE", "GraphicsRowRenderScale"),
+ // GFX (anisotropic): ours, because 1.12's own text describes a four-stop slider
+ // that needs a restart and this row is neither.
+ ("BENILLA_TOOLTIP_ANISOTROPIC", "GraphicsRowAnisotropic"),
("BENILLA_TOOLTIP_DISPLAY_MODE", "GraphicsRowDisplayMode"),
// 1.12 shipped the resolution dropdown with no tooltip at all, so there is no
// `OPTION_TOOLTIP_*` for it to borrow; benilla's own text explains the restart.
@@ -3260,7 +3328,9 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
// lava, fog, daylight, Graphics Preset, Render Distance, ...) add 20. 78 -> 98.
// MONKEY (post / dither / fog / wet / wind / volumetric light): their rows add 17, and the
// Graphics page's resolution dropdown one more. 98 -> 116.
- assert_eq!(checked, 116, "every tipped row carries a live key");
+ // GFX (anisotropic): the Graphics page's own anisotropy slider, seated where the reference's
+ // slider table already declared it. 116 -> 117.
+ assert_eq!(checked, 117, "every tipped row carries a live key");
assert_eq!(
untipped,
vec![
@@ -3364,7 +3434,8 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
// per-row; the total is here so a row losing its key is a failure rather than a smaller pass.
// MONKEY (graphics programme): the Advanced Graphics page's described rows add 19. 78 -> 97.
// MONKEY (post / dither / fog / wet / wind / volumetric light / resolution): 97 -> 116.
- assert_eq!(raised, 116, "every row but Auto Loot raises a description");
+ // GFX (anisotropic): the Graphics page's anisotropy slider. 116 -> 117.
+ assert_eq!(raised, 117, "every row but Auto Loot raises a description");
}
/// 1.12's AdvancedOptionsCombatText box as saved-global rows: a click writes the global, never a