MONKEY (integration): merge graphics-programme-presets

Graphics Preset ladder + Render Distance (farclip ceiling 1497). Conflict in
options_tests: the Advanced page now has 33 rows (preset + render distance lead,
then the 31 programme rows). The three round-3 rows join the ladder:
ambientOcclusion 0/0/1/2/2, zoneSkyboxes 0/0/1/1/1, lampFog 0/0/1/2/2
(Classic/Low/Medium/High/Ultra); the preset tooltip names them (EN+RU).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-25 19:52:52 +01:00
co-authored by Claude Opus 5.5
6 changed files with 592 additions and 20 deletions
+45 -1
View File
@@ -38,9 +38,53 @@ behind each constant.
| Zone skyboxes | the painted sky a zone's `LightParams` names (clear-weather slot, storm slot lerped by the storm blend) for the living, fading in by the Light sphere falloff with the modern client's crossfade (ghost > WMO > zone); extended `LightSkybox.dbc` flags `0x1` day cycle, `0x2` keep sun/moon/stars/clouds, `0x4` fog-colour horizon cone, plus a celestial second model; every skybox now plays its bones, texture transforms, colour and alpha tracks (`benilla-world/src/skybox.rs`, `skybox_anim.rs`, `benilla-formats/src/light/skybox.rs`) | Advanced Graphics → Zone Skyboxes, cvar `zoneSkyboxes` (0 Off default, 1 On; High = 1), env `WOW_ZONE_SKYBOXES=0\|1` |
| Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` |
Players reach all of it from **Options -> Advanced Graphics** (a Lighting Quality preset Off / Low / Medium / High plus the individual rows; Off is the original client look). The dev build has a panel for all of it: **Ctrl+Shift+D → Lighting & shadows**, with Dim / Default /
Players reach all of it from **Options -> Advanced Graphics** (a Graphics Preset over everything, a Render Distance slider, a Lighting Quality preset Off / Low / Medium / High / Ultra plus the individual rows; Classic / Off is the original client look). The dev build has a panel for all of it: **Ctrl+Shift+D → Lighting & shadows**, with Dim / Default /
Bright presets.
## Graphics Preset
MONKEY (presets). The top row of Advanced Graphics, cvar `graphicsQuality`: Classic / Low / Medium /
High / Ultra, or Custom when the rows match none of them. The table is `GRAPHICS_PRESETS` in
`benilla-app/src/cvars.rs` (one line per governed row); choosing a rung writes each row through
the ordinary cvar path, and the label is re-derived every frame, so editing any governed row
(or any Lighting Quality member) shows Custom. `lightingQuality` is itself a governed row, so
every row the lighting ladder owns (`LIGHTING_PRESETS`: both sun lanes, shadow resolution, the
room and torch lanes, moon shadows, `waterQuality`, `volumetricFog`, `bloom`, `sunShafts`,
`colorGrading`, `foliageWind`, the fire/spell/lava gains, `nightGain`, `interiorGain`) follows it.
| row | Classic | Low | Medium | High | Ultra |
|---|---|---|---|---|---|
| `lightingQuality` | Off | Low | Medium | High | Ultra |
| `farclip` (Render Distance, yd) | 350 | 350 | 477 | 777 | 1497 |
| `skyQuality` | 0 | 1 | 1 | 2 | 2 |
| `skyDither` | 0 | 1 | 1 | 1 | 1 |
| `fogModel` | 0 | 1 | 1 | 1 | 1 |
| `rainSurfaces` | 0 | 1 | 1 | 1 | 1 |
| `torchTerrainShadows` | 0 | 0 | 0 | 1 | 1 |
| `daylightWindowSplit` | 0 | 1 | 1 | 1 | 1 |
| `ambientOcclusion` | 0 | 0 | 1 | 2 | 2 |
| `zoneSkyboxes` | 0 | 0 | 1 | 1 | 1 |
| `lampFog` | 0 | 0 | 1 | 2 | 2 |
What the lighting rungs set, beyond Off (everything off, gains 1.0) and High (the registered
defaults): Low = character shadows only at a 1024 map, no torch or moon shadows, bloom Low;
Medium = both sun lanes, indoor torch shadows (6 resident / 2 moving), moon shadows, bloom Low,
no sun shafts or colour grading; Ultra = High with a 4096 sun map, 16 / 8 torch maps, High water
and High volumetric fog. No rung decides `shadowDistance` or `interiorShadowSoft`.
**Default: High.** A player whose `config.toml` names no `graphicsQuality` is seeded to the High
column at startup (`Cvars::seed_graphics_preset`) on every row the file does not carry; the rows'
registered defaults stay their per-lane ones (`farclip` the reference's 350), and captures and
tests, which never seed, keep them. The Advanced page's Defaults button lands on High too.
**Render Distance** is `farclip`, the same cvar as the Graphics page's Terrain Distance (which
keeps the stock 177–777 slider). Its clamp (`view::FARCLIP_RANGE`) now reaches `FARCLIP_MAX` =
1497 yd, the 1.12 grid of 60 carried on. Terrain residency (`terrain_stream/window.rs`), the
model/WMO and particle walls (`view::within_farclip`) and the art-cache sweep radius (now 7 tiles,
`art_scope.rs`) all follow it; at 1497 the residency window is at most 7×7 ADT tiles against
777's 5×5, which is the memory bound. Past 777 only Modern fog (`fog_model::modern_fog_end`)
stretches the fog end with it; under Classic fog the zone's fog end still hides the extra reach.
The optional enhanced water (Water Quality, refraction, caustics, High reflections, lava glow)
is its own module, documented in `WATER.md`.
+109 -3
View File
@@ -492,6 +492,11 @@
enUS = {
page = "Advanced Graphics",
lightingQuality = "Lighting Quality",
-- MONKEY (presets): the Graphics Preset and Render Distance rows.
graphicsQuality = "Graphics Preset",
renderDistance = "Render Distance",
pClassic = "Classic",
qUltra = "Ultra",
-- MONKEY (volumetric fog): localised atmospheric quality row.
volumetricFog = "Volumetric Fog",
-- MONKEY (post): HDR excess + halo quality.
@@ -546,6 +551,24 @@
daylightWindowSplit = "City Window Daylight",
yards = "%d yd",
tips = {
-- MONKEY (presets)
GRAPHICS_QUALITY =
"Sets every graphics setting at once: Lighting Quality and everything it sets, "
.. "Render Distance, Sky Quality, Sky Dithering, Modern Fog, Wet Surfaces in Rain, "
.. "Terrain Blocks Torchlight, the daylight through city windows, Ambient Occlusion, Lamp Fog "
.. "and Zone Skyboxes.\n\n"
.. "Classic is the original client: every enhancement off and the original 350 yd "
.. "view distance. Low and Medium add the enhancements that cost almost nothing; High "
.. "is the recommended look; Ultra raises every quality to its maximum and the view "
.. "distance to 1497 yd, and costs the most.\n\n"
.. "Shadow Distance and Torch Shadow Softness stay as you set them. Changing any "
.. "setting the preset sets shows Custom.",
RENDER_DISTANCE =
"How far the detailed world is drawn, in yards: the same setting as Terrain "
.. "Distance on the Graphics page, with a longer reach. Past 777 yd, the original "
.. "client's limit, the extra distance shows through Modern Fog; with the original "
.. "fog the zone's own fog hides it.\n\n"
.. "Farther costs more memory, loading and GPU time.",
LIGHTING_QUALITY =
"Sets character, world, indoor torch and outdoor torch shadows; dynamic "
.. "interior lighting; moon shadows; volumetric fog; water quality; lava glow; fire and spell brightness; fire flicker; "
@@ -555,6 +578,8 @@
.. "night gain 1.0 and interior gain 1.0. All other settings, including "
.. "Shadow Distance and Torch Shadow Softness, stay as you set them. "
.. "Low, Medium and High use Enhanced water; mirror reflections stay opt-in. "
.. "Ultra also raises shadow resolution to 4096 and the torch shadow counts to 16 and 8, "
.. "and uses High water and High volumetric fog. "
.. "Changing a preset member shows Custom when no preset matches.",
-- MONKEY (volumetric fog): explain cost and the shadow-map dependency.
-- MONKEY (post): Off guarantees the pre-HDR image.
@@ -706,6 +731,11 @@
ruRU = {
page = "Расширенная графика",
lightingQuality = "Качество освещения",
-- MONKEY (presets): the Graphics Preset and Render Distance rows.
graphicsQuality = "Набор графики",
renderDistance = "Дальность прорисовки",
pClassic = "Классика",
qUltra = "Ультра",
-- MONKEY (post): Russian HDR excess + halo quality row.
bloom = "Свечение",
sunShafts = "Солнечные лучи",
@@ -760,6 +790,24 @@
daylightWindowSplit = "Дневной свет через окна города",
yards = "%d ярд.",
tips = {
-- MONKEY (presets)
GRAPHICS_QUALITY =
"Задаёт все настройки графики сразу: качество освещения и всё, что оно задаёт, "
.. "дальность прорисовки, качество неба, дизеринг неба, современный туман, мокрые "
.. "поверхности в дождь, заслонение огней рельефом, дневной свет в окнах городов, "
.. "затенение окружения, свечение фонарей в тумане и небосводы зон.\n\n"
.. "«Классика» — оригинальный клиент: все улучшения выключены, дальность обзора "
.. "прежние 350 ярдов. «Низкое» и «Среднее» добавляют почти бесплатные улучшения; "
.. "«Высокое» — рекомендуемый вид; «Ультра» поднимает всё качество до максимума, а "
.. "дальность — до 1497 ярдов, и стоит дороже всего.\n\n"
.. "Дальность теней и мягкость теней от огня сохраняют ваши значения. Изменение "
.. "любой настройки набора показывает «Своё».",
RENDER_DISTANCE =
"Как далеко рисуется подробный мир, в ярдах: та же настройка, что «Дальность "
.. "обзора» на странице «Графика», но с большим пределом. Дальше 777 ярдов, предела "
.. "оригинального клиента, дополнительное расстояние видно с современным туманом; "
.. "с оригинальным туманом его скрывает туман зоны.\n\n"
.. "Чем дальше, тем больше памяти, загрузки и работы видеокарты.",
LIGHTING_QUALITY =
"Задаёт тени персонажей, мира и огней внутри и снаружи помещений; "
.. "динамическое освещение помещений; лунные тени; объёмный туман; качество воды; свечение лавы; яркость огня и заклинаний; "
@@ -770,7 +818,9 @@
.. "источников света, с коэффициентами яркости ночи и помещений 1.0. "
.. "Все остальные настройки, включая дальность теней и мягкость теней от огня, "
.. "сохраняют ваши значения. «Низкое», «Среднее» и «Высокое» используют улучшенную воду; "
.. "зеркальные отражения включаются отдельно. Изменение настройки набора показывает «Своё», "
.. "зеркальные отражения включаются отдельно. «Ультра» также поднимает разрешение теней "
.. "до 4096 и число теней от огней до 16 и 8, а вода и объёмный туман — «Высокое». "
.. "Изменение настройки набора показывает «Своё», "
.. "если ни один набор больше не совпадает.",
-- MONKEY (post): localised bloom effect and Off contract.
BLOOM =
@@ -976,6 +1026,8 @@
-- the rig they share, and the Graphics page had become the place where a player found
-- three of eighteen shadow settings and no hint that the other fifteen existed.
AdvancedGraphics = {
-- MONKEY (presets): the ladder over every row below, then the view distance.
"RowGraphicsQuality", "RowRenderDistance",
-- MONKEY (volumetric fog): include in refresh, defaults and search.
"RowLightingQuality", "RowWaterQuality", "RowVolumetricFog",
-- MONKEY (sky): the sky tier row.
@@ -1615,7 +1667,20 @@
for _, rkey in ipairs(OPTIONS_PAGE_ROWS[key]) do
local row = getglobal(base .. rkey)
row.pending = nil
OptionsRow_Set(row, OptionsRow_Default(row))
if not row.preset then
OptionsRow_Set(row, OptionsRow_Default(row))
end
end
-- MONKEY (presets): a preset row goes last, so its default rung (the Graphics
-- Preset's High) is what the page lands on rather than its members' own defaults.
-- `SetCVar` queues only a moved value, so "Custom" (which the host applies as
-- nothing) goes first: the rung is then always a queued write, after the members.
for _, rkey in ipairs(OPTIONS_PAGE_ROWS[key]) do
local row = getglobal(base .. rkey)
if row.preset then
OptionsRow_Set(row, "Custom")
OptionsRow_Set(row, OptionsRow_Default(row))
end
end
OptionsPage_Refresh(key)
end
@@ -2744,11 +2809,51 @@
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Frame name="$parentRowLightingQuality" inherits="OptionsDropdownRowTemplate">
<!-- MONKEY (presets): the Graphics Preset. Like Lighting
Quality below it, ONE CVar: the host's
`cvars::GRAPHICS_PRESETS` writes the rows and
re-derives this one every frame. -->
<Frame name="$parentRowGraphicsQuality" inherits="OptionsDropdownRowTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-12"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-20" y="-12"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "graphicsQuality",
BENILLA_ADVGFX.graphicsQuality,
"BENILLA_TOOLTIP_GRAPHICS_QUALITY")
self.preset = 1
OptionsDropdownRow_Setup(self, {
{ text = BENILLA_ADVGFX.pClassic, value = "Classic" },
{ text = BENILLA_ADVGFX.qLow, value = "Low" },
{ text = BENILLA_ADVGFX.qMedium, value = "Medium" },
{ text = BENILLA_ADVGFX.qHigh, value = "High" },
{ text = BENILLA_ADVGFX.qUltra, value = "Ultra" },
{ text = BENILLA_ADVGFX.qCustom, value = "Custom" },
})
</OnLoad></Scripts>
</Frame>
<!-- MONKEY (presets): `farclip`, the Graphics page's Terrain
Distance, on the extended range (`view::FARCLIP_RANGE`,
177..1497 on the 1.12 grid of 60). Live like its
Graphics twin; both read the one CVar. -->
<Frame name="$parentRowRenderDistance" inherits="OptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowGraphicsQuality" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowGraphicsQuality" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "farclip",
BENILLA_ADVGFX.renderDistance,
"BENILLA_TOOLTIP_RENDER_DISTANCE")
OptionsSliderRow_Setup(self, 177, 1497, 60, "yd")
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowLightingQuality" inherits="OptionsDropdownRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowRenderDistance" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowRenderDistance" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
-- The preset ladder. This row writes ONE CVar and
-- nothing else: the host's own table
@@ -2773,6 +2878,7 @@
{ text = BENILLA_ADVGFX.qLow, value = "Low" },
{ text = BENILLA_ADVGFX.qMedium, value = "Medium" },
{ text = BENILLA_ADVGFX.qHigh, value = "High" },
{ text = BENILLA_ADVGFX.qUltra, value = "Ultra" },
{ text = BENILLA_ADVGFX.qCustom, value = "Custom" },
})
</OnLoad></Scripts>
+405 -4
View File
@@ -916,9 +916,22 @@ pub(crate) const REGISTERED: &[Registered] = &[
"lightingQuality",
"High",
"benilla's own: the preset ladder over the dynamic light + shadow rows — Off / Low / \
Medium / High, or Custom when the members match none of them; the reference has no \
Medium / High / Ultra, or Custom when the members match none of them; the reference has no \
realtime light or shadow system to preset",
),
// MONKEY (presets): the Graphics Preset — `lightingQuality`'s posture one level up, a NAME
// for every graphics row at once ([`GRAPHICS_PRESETS`]), re-derived every frame. "High" is
// what a player with no saved preset boots into: [`Cvars::seed_graphics_preset`] writes the
// High column over every row their `config.toml` does not carry, so the rows themselves keep
// their per-lane defaults (Classic-leaning, and `farclip` the reference's 350) and a capture
// or a test, which never seeds, keeps them too.
ours(
"graphicsQuality",
"High",
"benilla's own: the preset ladder over every graphics row — Classic / Low / Medium / High \
/ Ultra, or Custom when the rows match none of them; the reference's options have no \
such preset",
),
// MONKEY (volumetric fog): saved live tier; capture override stays session-only.
ours("volumetricFog", "1", "benilla's own: near-field volumetric fog, 0 Off / 1 Low / 2 High"),
// MONKEY (lampfog): opt-in point-light halos; High graphics preset value is 2.
@@ -1747,6 +1760,9 @@ impl Cvars {
if name.eq_ignore_ascii_case("lightingQuality") {
apply_lighting_preset(self, value);
}
if name.eq_ignore_ascii_case("graphicsQuality") {
apply_graphics_preset(self, value);
}
outcome
}
@@ -1758,9 +1774,38 @@ impl Cvars {
if name.eq_ignore_ascii_case("lightingQuality") {
apply_lighting_preset(self, value);
}
if name.eq_ignore_ascii_case("graphicsQuality") {
apply_graphics_preset(self, value);
}
outcome
}
/// MONKEY (presets): **the first boot's Graphics Preset** — a player whose `config.toml`
/// names no `graphicsQuality` (a new player, or one from before the ladder) gets the
/// [`GRAPHICS_DEFAULT`] column written over every governed row the file does not carry and the
/// session does not own. Rows the file carries are the player's and stay; the lighting rung
/// is left alone, because High on that ladder IS the registered defaults
/// (`the_high_preset_is_the_registered_defaults`). Returns how many rows moved.
pub(crate) fn seed_graphics_preset(&mut self) -> usize {
let carried = |cvars: &Self, k: &str| {
cvars.file.keys().any(|f| f.eq_ignore_ascii_case(k)) || cvars.is_session_owned(k)
};
if carried(self, "graphicsQuality") {
return 0;
}
let col = graphics_column(GRAPHICS_DEFAULT).expect("the default is a rung");
let mut moved = 0;
for (k, values) in GRAPHICS_PRESETS {
if k.eq_ignore_ascii_case("lightingQuality") || carried(self, k) {
continue;
}
if self.set(k, values[col]) == SetOutcome::Changed {
moved += 1;
}
}
moved
}
/// **The table follows a value the engine already applied** — a mirror, not a write: the
/// applied value moves, the config dirties, the VM's mirror learns it, and **no observer
/// fires**, because the knob is already there. For a second registered spelling of one knob
@@ -2180,6 +2225,35 @@ pub(crate) const LIGHTING_PRESETS: &[(&str, &[(&str, &str)])] = &[
("foliageWind", "2"),
],
),
// MONKEY (presets): High plus every lane at its maximum — the 4096 sun map, all sixteen
// resident torch maps with eight of them tracking movers, mirror-reflection water and the
// High fog march. Disjoint from High on `shadowMapSize`, so derivation never confuses them.
(
"Ultra",
&[
("characterShadows", "1"),
("worldShadows", "1"),
("shadowMapSize", "4096"),
("interiorLight", "1"),
("interiorShadows", "1"),
("interiorShadowCasters", "16"),
("interiorShadowDynamic", "8"),
("exteriorShadows", "1"),
("spellLightGain", "1"),
("waterQuality", "2"),
("volumetricFog", "2"),
("bloom", "2"),
("sunShafts", "1"),
("colorGrading", "1"),
("lavaLightGain", "1"),
("fireLightGain", "1"),
("nightGain", "0.45"),
("interiorGain", "0.5"),
("moonShadowStrength", "0.35"),
("fireFlicker", "1"),
("foliageWind", "2"),
],
),
];
/// What the ladder shows when the members match no preset. Not a preset: selecting it writes
@@ -2233,6 +2307,82 @@ fn lighting_quality(cvars: &mut Cvars) {
let derived = derive_lighting_quality(cvars);
// A derived label is a mirror, never another request to apply a preset.
cvars.mirror("lightingQuality", derived);
// MONKEY (presets): after the lighting label, which the graphics ladder reads as a member.
let graphics = derive_graphics_quality(cvars);
cvars.mirror("graphicsQuality", graphics);
}
// ─── MONKEY (presets): the Graphics Preset ladder ─────────────────────────────────────────────
/// The Graphics Preset's rungs, in the order [`derive_graphics_quality`] tries them and the
/// column order of [`GRAPHICS_PRESETS`].
pub(crate) const GRAPHICS_PRESET_NAMES: [&str; 5] = ["Classic", "Low", "Medium", "High", "Ultra"];
/// The rung a player with no saved preset boots into ([`Cvars::seed_graphics_preset`]).
pub(crate) const GRAPHICS_DEFAULT: &str = "High";
/// **The one place the Graphics Preset is written down**: one line per governed row, one column
/// per rung of [`GRAPHICS_PRESET_NAMES`]. `lightingQuality` is itself a row — writing its rung
/// writes [`LIGHTING_PRESETS`]' members — so every lighting, water, fog, post and wind row the
/// lighting ladder owns is governed here without a second copy. The other rows are disjoint from
/// that ladder, which is what lets both labels be right at once.
///
/// Classic is the reference client: every lane off, `farclip` at its registered 350. High is
/// each lane's documented High (LIGHTING.md, WATER.md); Ultra is High at every maximum, and the
/// only rung past the reference's 777 yd (`FARCLIP_RANGE`); Low and Medium keep the practically
/// free lanes (sky, dither, modern fog, wet surfaces) and leave the costly ones to High.
///
/// A row that lands later is ONE line here, and only once it is registered (an unregistered
/// member would never match, and the ladder would read Custom forever).
#[rustfmt::skip]
pub(crate) const GRAPHICS_PRESETS: &[(&str, [&str; 5])] = &[
// Classic Low Medium High Ultra
("lightingQuality", ["Off", "Low", "Medium", "High", "Ultra"]),
("farclip", ["350", "350", "477", "777", "1497"]),
("skyQuality", ["0", "1", "1", "2", "2"]),
("skyDither", ["0", "1", "1", "1", "1"]),
("fogModel", ["0", "1", "1", "1", "1"]),
("rainSurfaces", ["0", "1", "1", "1", "1"]),
("torchTerrainShadows", ["0", "0", "0", "1", "1"]),
("daylightWindowSplit", ["0", "1", "1", "1", "1"]),
// MONKEY (integration): the three rows that landed in round 3. Low keeps them off (AO and
// the lamp halos are GPU passes; the zone skybox is a look change), Medium takes the Low tiers.
("ambientOcclusion", ["0", "0", "1", "2", "2"]),
("zoneSkyboxes", ["0", "0", "1", "1", "1"]),
("lampFog", ["0", "0", "1", "2", "2"]),
];
/// The column of a rung, matched case-insensitively; `None` for `Custom` or anything else.
fn graphics_column(name: &str) -> Option<usize> {
GRAPHICS_PRESET_NAMES
.iter()
.position(|n| n.eq_ignore_ascii_case(name))
}
/// The rung every governed row currently spells, or [`LIGHTING_CUSTOM`] (the one "Custom" both
/// ladders show). Reads `lightingQuality`'s DERIVED label, so run it after that is mirrored.
pub(crate) fn derive_graphics_quality(cvars: &Cvars) -> &'static str {
for (col, name) in GRAPHICS_PRESET_NAMES.iter().enumerate() {
let matched = GRAPHICS_PRESETS
.iter()
.all(|(k, v)| cvars.get(k).is_some_and(|live| same_value(live, v[col])));
if matched {
return name;
}
}
LIGHTING_CUSTOM
}
/// Write one rung's column through [`Cvars::set`]; `false` (and nothing written) for an unknown
/// name, `Custom` included.
pub(crate) fn apply_graphics_preset(cvars: &mut Cvars, name: &str) -> bool {
let Some(col) = graphics_column(name) else {
return false;
};
for (k, values) in GRAPHICS_PRESETS {
cvars.set(k, values[col]);
}
true
}
/// **What the environment took for this session, and what it set it to** — read off the knobs
@@ -2327,6 +2477,11 @@ fn session_values(world: &World) -> Vec<(&'static str, Option<String>)> {
fn load_config(world: &mut World) {
let session = session_values(world);
let stored = stored_config();
let stored_kind = match &stored {
StoredConfig::Absent => StoredKind::Absent,
StoredConfig::Table(_) => StoredKind::Table,
StoredConfig::Bad(_) => StoredKind::Bad,
};
let events = {
let mut cvars = world.resource_mut::<Cvars>();
for (name, value) in session {
@@ -2348,6 +2503,11 @@ fn load_config(world: &mut World) {
}
StoredConfig::Table(table) => cvars.load_file(table),
}
// MONKEY (presets): a player's own run (never a capture, never a malformed file) boots
// into the default Graphics Preset over whatever its file leaves unsaid.
if seeds_graphics_preset(&stored_kind) {
cvars.seed_graphics_preset();
}
cvars.take_events()
};
for event in events {
@@ -2355,6 +2515,22 @@ fn load_config(world: &mut World) {
}
}
/// MONKEY (presets): which [`StoredConfig`] arm a boot took, kept past the table's move.
enum StoredKind {
Absent,
Table,
Bad,
}
/// MONKEY (presets): whether this boot seeds the default Graphics Preset — a run that reads and
/// saves a player's `config.toml` and found it absent or well-formed. A capture (hermetic, or on
/// an explicit fixture) keeps the registered defaults so its A/B stays exact, and a malformed
/// file is left for the player rather than papered over.
fn seeds_graphics_preset(kind: &StoredKind) -> bool {
let players_file = std::env::var_os("WOW_CAPTURE").is_none() && config_read_path().is_some();
players_file && matches!(kind, StoredKind::Absent | StoredKind::Table)
}
/// What the one read of `config.toml` found.
enum StoredConfig {
/// No file, no install, or a capture without an explicit fixture — use registered defaults.
@@ -3127,6 +3303,8 @@ mod tests {
for member in ["fireLightGain", "waterQuality", "volumetricFog", "lavaLightGain", "nightGain", "interiorGain"] {
let expected = if *name == "Off" {
if matches!(member, "fireLightGain" | "waterQuality" | "volumetricFog" | "lavaLightGain") { "0" } else { "1.0" }
} else if *name == "Ultra" && matches!(member, "waterQuality" | "volumetricFog") {
"2" // MONKEY (presets): Ultra's two maxima over the defaults.
} else {
cvars.default_of(member).unwrap()
};
@@ -3211,10 +3389,212 @@ mod tests {
let mut cvars = fresh_registry();
apply_lighting_preset(&mut cvars, "Medium");
assert!(!apply_lighting_preset(&mut cvars, LIGHTING_CUSTOM));
assert!(!apply_lighting_preset(&mut cvars, "Ultra"));
assert!(!apply_lighting_preset(&mut cvars, "Epic"));
assert_eq!(derive_lighting_quality(&cvars), "Medium");
}
// ── MONKEY (presets): the Graphics Preset ladder ──────────────────────────────────────────
/// Every governed row is registered and names no row the lighting ladder already owns — the
/// disjointness that lets both labels be right at once.
#[test]
fn graphics_rows_are_registered_and_disjoint_from_the_lighting_ladder() {
assert_eq!(GRAPHICS_PRESETS.len(), 11, "add the row count with the row");
let cvars = fresh_registry();
for (k, values) in GRAPHICS_PRESETS {
assert!(cvars.get(k).is_some(), "{k}: not registered");
for (rung, members) in LIGHTING_PRESETS {
assert!(
members.iter().all(|(m, _)| !m.eq_ignore_ascii_case(k)),
"{k} is also a member of lighting {rung}"
);
}
if *k == "lightingQuality" {
for v in values {
assert!(
LIGHTING_PRESETS.iter().any(|(n, _)| n == v),
"{v}: a lighting rung"
);
}
}
}
// The rungs are distinct columns, so no two can derive to the same name.
for a in 0..GRAPHICS_PRESET_NAMES.len() {
for b in a + 1..GRAPHICS_PRESET_NAMES.len() {
assert!(
GRAPHICS_PRESETS
.iter()
.any(|(_, v)| !same_value(v[a], v[b])),
"{} and {} are the same column",
GRAPHICS_PRESET_NAMES[a],
GRAPHICS_PRESET_NAMES[b]
);
}
}
}
/// **Preset → rows → the same preset**, from every rung to every rung, through the ordinary
/// `set` path — with the two rows no preset decides left where the player put them.
#[test]
fn every_graphics_preset_derives_back_to_its_own_name() {
for from in GRAPHICS_PRESET_NAMES {
for name in GRAPHICS_PRESET_NAMES {
let mut cvars = fresh_registry();
cvars.set("graphicsQuality", from);
cvars.set("shadowDistance", "120");
cvars.set("interiorShadowSoft", "2.5");
cvars.set("graphicsQuality", name);
lighting_quality(&mut cvars);
assert_eq!(derive_graphics_quality(&cvars), name, "{from} -> {name}");
assert_eq!(cvars.get("graphicsQuality"), Some(name));
let col = graphics_column(name).unwrap();
assert_eq!(
cvars.get("lightingQuality"),
Some(GRAPHICS_PRESETS[0].1[col])
);
assert_eq!(cvars.get("shadowDistance"), Some("120"));
assert_eq!(cvars.get("interiorShadowSoft"), Some("2.5"));
}
}
}
/// Classic is the reference client: the lighting ladder's Off, every programme lane off and
/// `farclip` at the reference's registered 350.
#[test]
fn the_classic_preset_is_every_lane_off_at_the_reference_view_distance() {
let mut cvars = fresh_registry();
assert!(apply_graphics_preset(&mut cvars, "Classic"));
assert_eq!(cvars.get("lightingQuality"), Some("Off"));
assert_eq!(cvars.get("farclip"), cvars.default_of("farclip"));
for (k, values) in GRAPHICS_PRESETS {
if !matches!(*k, "lightingQuality" | "farclip") {
assert!(same_value(values[0], "0"), "{k}: Classic leaves it on");
}
}
// Ultra is the one rung past the reference's 777, and inside the extended clamp.
let ultra = GRAPHICS_PRESETS
.iter()
.find(|(k, _)| *k == "farclip")
.unwrap()
.1[4];
let ultra: f32 = ultra.parse().unwrap();
assert!(ultra > 777.0 && FARCLIP_RANGE.contains(&ultra));
}
/// **Editing one governed row is Custom** — for every rung and every row of it, the lighting
/// rung included (moved by one of ITS members, as the page's lighting rows would).
#[test]
fn one_changed_graphics_row_derives_custom() {
for name in GRAPHICS_PRESET_NAMES {
let col = graphics_column(name).unwrap();
for (k, values) in GRAPHICS_PRESETS {
let mut cvars = fresh_registry();
apply_graphics_preset(&mut cvars, name);
let (row, moved) = if *k == "lightingQuality" {
let to = if values[col] == "Off" { "1" } else { "0" };
("characterShadows", to.to_string())
} else {
let v: f32 = values[col].parse().unwrap();
let to = if *k == "farclip" {
v + 60.0
} else if v == 0.0 {
1.0
} else {
0.0
};
(*k, to.to_string())
};
assert_eq!(cvars.set(row, &moved), SetOutcome::Changed, "{name}/{row}");
lighting_quality(&mut cvars);
assert_eq!(
cvars.get("graphicsQuality"),
Some(LIGHTING_CUSTOM),
"{name}/{row}"
);
// …and picking the rung again puts every row back.
assert!(apply_graphics_preset(&mut cvars, name));
lighting_quality(&mut cvars);
assert_eq!(
cvars.get("graphicsQuality"),
Some(name),
"{name}/{row} re-pick"
);
}
}
let mut cvars = fresh_registry();
assert!(!apply_graphics_preset(&mut cvars, LIGHTING_CUSTOM));
assert!(!apply_graphics_preset(&mut cvars, "Epic"));
}
/// **A new player boots into High**: the seed writes the High column over a registry with no
/// file, and the registered default of the label agrees, so it never reaches `config.toml`.
#[test]
fn a_fresh_player_is_seeded_to_the_high_graphics_preset() {
assert_eq!(
fresh_registry().default_of("graphicsQuality"),
Some(GRAPHICS_DEFAULT)
);
let mut cvars = fresh_registry();
assert!(cvars.seed_graphics_preset() > 0);
lighting_quality(&mut cvars);
assert_eq!(cvars.get("graphicsQuality"), Some("High"));
assert_eq!(cvars.get("lightingQuality"), Some("High"));
assert_eq!(cvars.get("farclip"), Some("777"));
assert!(
!cvars.compose().contains_key("graphicsQuality"),
"the default label is not saved"
);
// A second boot over what the first one saved seeds nothing.
let mut again = fresh_registry();
again.load_file(cvars.compose());
assert_eq!(again.seed_graphics_preset(), 0);
lighting_quality(&mut again);
assert_eq!(again.get("graphicsQuality"), Some("High"));
}
/// The seed never overrides the player: a saved preset stops it outright, a saved row keeps
/// its value, and a session-owned row (an env lever) is left to the session.
#[test]
fn the_graphics_seed_leaves_saved_and_session_rows_alone() {
let mut saved = fresh_registry();
saved.load_file(BTreeMap::from([(
"graphicsQuality".into(),
"Classic".into(),
)]));
assert_eq!(saved.seed_graphics_preset(), 0);
assert_eq!(saved.get("skyQuality"), saved.default_of("skyQuality"));
let mut row = fresh_registry();
row.load_file(BTreeMap::from([("skyQuality".into(), "1".into())]));
row.own_for_session("farclip", Some("500"));
row.seed_graphics_preset();
lighting_quality(&mut row);
assert_eq!(row.get("skyQuality"), Some("1"));
assert_eq!(row.get("farclip"), Some("500"));
assert_eq!(row.get("fogModel"), Some("1"));
assert_eq!(row.get("graphicsQuality"), Some(LIGHTING_CUSTOM));
}
/// Ultra's `farclip` survives the real observer (the extended clamp), and every governed
/// row's rung values do too.
#[test]
fn graphics_rows_pass_their_observers_unclamped() {
let mut app = cvar_app();
for (col, name) in GRAPHICS_PRESET_NAMES.iter().enumerate() {
let farclip = GRAPHICS_PRESETS
.iter()
.find(|(k, _)| *k == "farclip")
.unwrap()
.1[col];
apply(&mut app, "farclip", farclip);
assert_eq!(
res::<ViewDistance>(&app).farclip,
farclip.parse::<f32>().unwrap(),
"{name}"
);
}
}
/// Every member every rung names is a REGISTERED row — a typo'd member would otherwise make
/// its whole preset underivable (a `cvars.get` that answers `None` never matches), and the
/// only symptom would be a dropdown stuck on `Custom`.
@@ -3741,7 +4121,16 @@ mod tests {
assert!(text.contains("MusicVolume = \"0.75\""), "{text}");
assert!(!text.contains("MasterVolume"), "defaults stay out:\n{text}");
let back: LocalConfig = toml::from_str(&text).unwrap();
assert_eq!(back.cvars.len(), 1, "a diff, not a dump: {text}");
// MONKEY (presets): the file named no `graphicsQuality`, so the first boot seeded the
// High rungs over the governed rows; those are the only other entries, and the label
// itself, at its default, stays out.
let player: Vec<&String> = back
.cvars
.keys()
.filter(|k| !GRAPHICS_PRESETS.iter().any(|(g, _)| g.eq_ignore_ascii_case(k)))
.collect();
assert_eq!(player.len(), 1, "a diff, not a dump: {text}");
assert!(!text.contains("graphicsQuality"), "{text}");
std::fs::remove_dir_all(&tmp).ok();
}
@@ -4115,7 +4504,14 @@ mod tests {
strings.sort_unstable(); // the list is the claim, not where the rows sit in the table
assert_eq!(
strings,
vec!["gxApi", "gxResolution", "lightingQuality", "realmList", "realmName"]
vec![
"graphicsQuality",
"gxApi",
"gxResolution",
"lightingQuality",
"realmList",
"realmName"
]
);
let default_of = |name: &str| {
REGISTERED
@@ -4432,6 +4828,11 @@ mod tests {
"GameTooltip's binding-line gate, stock and pfUI (1316)",
),
("gxApi", "pfUI's system tooltip names the backend (2151)"),
(
"graphicsQuality",
"MONKEY (presets): the Advanced Graphics page's Graphics Preset row; the host \
writes and derives it here in cvars.rs",
),
(
"useUiScale",
"UIOptionsFrame.lua and OptionsFrame.lua branch on it to gate the uiScale slider",
@@ -2042,7 +2042,7 @@ fn volumetric_fog_dropdown_is_localised_and_live() {
s.run(&format!("function GetLocale() return '{locale}' end")).unwrap();
let mut s = harness_on(s);
s.run("ShowUIPanel(BenillaOptionsFrame) BenillaOptionsFrameCategoryListRowAdvancedGraphics:Click()").unwrap();
assert_eq!(s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[3]").unwrap(), "RowVolumetricFog");
assert_eq!(s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[5]").unwrap(), "RowVolumetricFog");
assert_eq!(s.eval::<String>(&format!("return {ADVGFX}RowVolumetricFogLabel:GetText()")).unwrap(), title);
assert_eq!(s.eval::<String>(&format!("return {ADVGFX}RowVolumetricFogDropdownText:GetText()")).unwrap(), labels[1]);
let _ = s.take_cvar_changes();
@@ -2164,10 +2164,10 @@ fn water_quality_writes_numeric_tiers_with_localised_labels() {
let mut s = harness_on(s);
s.run("ShowUIPanel(BenillaOptionsFrame) BenillaOptionsFrameCategoryListRowAdvancedGraphics:Click()").unwrap();
// MONKEY (volumetric fog): account for the atmosphere row after water.
// MONKEY (integration): all programme rows (sky, post, dither, fog, wet, wind, ao, lamp fog, window split, zone skyboxes) counted.
assert_eq!(s.eval::<usize>("return table.getn(OPTIONS_PAGE_ROWS.AdvancedGraphics)").unwrap(), 31);
assert_eq!(s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[2]").unwrap(), "RowWaterQuality");
assert_eq!(s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[21]").unwrap(), "RowLavaGlow");
// MONKEY (integration): Graphics Preset + Render Distance lead the page, then every programme row (sky, post, dither, fog, wet, wind, ao, lamp fog, window split, zone skyboxes): 33.
assert_eq!(s.eval::<usize>("return table.getn(OPTIONS_PAGE_ROWS.AdvancedGraphics)").unwrap(), 33);
assert_eq!(s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[4]").unwrap(), "RowWaterQuality");
assert_eq!(s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[23]").unwrap(), "RowLavaGlow");
assert_eq!(s.eval::<String>(&format!("return {ADVGFX}RowWaterQualityDropdownText:GetText()")).unwrap(), labels[1]);
assert_eq!(s.eval::<String>(&format!("return {ADVGFX}RowLavaGlowLabel:GetText()")).unwrap(), lava_label);
assert!(s.eval::<bool>("return BENILLA_TOOLTIP_WATER_QUALITY == BENILLA_ADVGFX.tips.WATER_QUALITY and BENILLA_TOOLTIP_LAVA_GLOW == BENILLA_ADVGFX.tips.LAVA_GLOW").unwrap());
@@ -3139,6 +3139,15 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
"BENILLA_TOOLTIP_LIGHTING_QUALITY",
"AdvancedGraphicsRowLightingQuality",
),
// MONKEY (presets)
(
"BENILLA_TOOLTIP_GRAPHICS_QUALITY",
"AdvancedGraphicsRowGraphicsQuality",
),
(
"BENILLA_TOOLTIP_RENDER_DISTANCE",
"AdvancedGraphicsRowRenderDistance",
),
(
"BENILLA_TOOLTIP_SHADOW_RESOLUTION",
"AdvancedGraphicsRowShadowResolution",
@@ -3249,7 +3258,8 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
// changing pages does not change this count, only its entry in BENILLA_OWNED. 81 -> 93.
// Water Quality and Lava Glow add two more: 93 -> 95.
// MONKEY (daylight: terrain torch casters): Terrain Blocks Torchlight. 95 -> 96.
assert_eq!(checked, 96, "every tipped row carries a live key");
// MONKEY (presets): Graphics Preset and Render Distance. +2.
assert_eq!(checked, 98, "every tipped row carries a live key");
assert_eq!(
untipped,
vec![
+5 -4
View File
@@ -17,8 +17,9 @@ use benilla_formats::TILE_SIZE;
/// Real-clock seconds between sweeps (housekeeping, not world time); also the stamp's resolution.
const SWEEP_SECS: f32 = 1.0;
/// Five ADT tiles (2667 yd), the smallest round tile multiple clear of [`radius_floor`].
const DEFAULT_RADIUS_YD: f32 = 5.0 * TILE_SIZE;
/// Seven ADT tiles (3733 yd), the smallest round tile multiple clear of [`radius_floor`]
/// (MONKEY (presets): five until `farclip`'s ceiling rose past 777).
const DEFAULT_RADIUS_YD: f32 = 7.0 * TILE_SIZE;
/// The far corner of the widest tile block the residency window keeps (at the `farclip` clamp's
/// maximum), plus a tile: a stamp is where the viewer stood, so no smaller radius is safe.
@@ -223,8 +224,8 @@ mod tests {
"default {DEFAULT_RADIUS_YD} must clear the floor {}",
radius_floor()
);
// (26 + 1 + 16) chunks each way at farclip 777, to the corner, plus a tile: ~2560 yd.
assert!((radius_floor() - 2560.0).abs() < 1.0, "{}", radius_floor());
// (47 + 1 + 16) chunks each way at farclip 1497, to the corner, plus a tile: ~3550 yd.
assert!((radius_floor() - 3550.3).abs() < 1.0, "{}", radius_floor());
}
/// `$WOW_ART_RADIUS` is unset in the test binary, as in every ordinary run.
+12 -2
View File
@@ -75,8 +75,18 @@ pub struct ViewDistance {
/// The settable range of [`ViewDistance::farclip`]: the reference's `farclip` clamp,
/// `[0x81021c]` = 177 to `[0x80fed8]` = 777 in its validate callback `0x688d40`; shared by the
/// CVar apply, the options row and `$WOW_FARCLIP`.
pub const FARCLIP_RANGE: std::ops::RangeInclusive<f32> = 177.0..=777.0;
/// CVar apply, the options rows and `$WOW_FARCLIP`.
///
/// MONKEY (presets) deviation: the ceiling is [`FARCLIP_MAX`], not 777. Modern fog
/// (`fog_model::modern_fog_end`) stretches the fog end past 777 with the view distance, so a
/// longer view is visible rather than fogged; the residency window and the model/particle walls
/// all follow `farclip`, and the ceiling is what bounds their memory (see [`FARCLIP_MAX`]).
pub const FARCLIP_RANGE: std::ops::RangeInclusive<f32> = 177.0..=FARCLIP_MAX;
/// MONKEY (presets): the extended `farclip` ceiling — the reference's slider grid (177 + n·60)
/// carried on to n = 22. The residency window there is ±47 chunks, at most 7×7 ADT tiles against
/// 777's 5×5: about twice the reference maximum's terrain, doodads and WMOs, and no more.
pub const FARCLIP_MAX: f32 = 1497.0;
/// The settable range of [`ViewDistance::nearclip`]: the bounds in the reference's `nearclip`
/// change callback `0x688d90`, `[0x8029d0]` = 0.01 and `[0x808300]` = 0.33.