diff --git a/LIGHTING.md b/LIGHTING.md
index c1ccefe1..976f6921 100644
--- a/LIGHTING.md
+++ b/LIGHTING.md
@@ -34,9 +34,53 @@ behind each constant.
| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), capture override `WOW_FOLIAGE_WIND`; field and benders in `benilla-world/src/wind/`. The wave phase is the CPU-integrated travel (speed integrated over time, wrapped seamlessly), and the waves' spatial term uses the fixed profile heading; the veer turns only the bend. Tree exile copies sway through material marker bit 14 (`FOLIAGE_WIND_MARKER`). Capture offset `WOW_CAPTURE_WIND_T` (s). Known: shadow casters (world-shadow proxies, `torch_depth.wgsl`) do not apply the sway, so leaves sample a static shadow; a future world-camera DepthPrepass must call the same offsets |
| 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` (TODO, not yet registered) | 0 | 0 | 1 | 2 | 2 |
+| `zoneSkyboxes` (TODO, not yet registered) | 0 | 1 | 1 | 1 | 1 |
+| `lampFog` (TODO, not yet registered) | 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`.
diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index e824d122..3f81a4dd 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -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.
@@ -538,6 +543,23 @@
windTrees = "Grass + Trees",
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 and the daylight through city windows.\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; "
@@ -547,6 +569,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.
@@ -673,6 +697,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 = "Солнечные лучи",
@@ -719,6 +748,23 @@
windTrees = "Трава + деревья",
yards = "%d ярд.",
tips = {
+ -- MONKEY (presets)
+ GRAPHICS_QUALITY =
+ "Задаёт все настройки графики сразу: качество освещения и всё, что оно задаёт, "
+ .. "дальность прорисовки, качество неба, дизеринг неба, современный туман, мокрые "
+ .. "поверхности в дождь, заслонение огней рельефом и дневной свет в окнах городов.\n\n"
+ .. "«Классика» — оригинальный клиент: все улучшения выключены, дальность обзора "
+ .. "прежние 350 ярдов. «Низкое» и «Среднее» добавляют почти бесплатные улучшения; "
+ .. "«Высокое» — рекомендуемый вид; «Ультра» поднимает всё качество до максимума, а "
+ .. "дальность — до 1497 ярдов, и стоит дороже всего.\n\n"
+ .. "Дальность теней и мягкость теней от огня сохраняют ваши значения. Изменение "
+ .. "любой настройки набора показывает «Своё».",
+ RENDER_DISTANCE =
+ "Как далеко рисуется подробный мир, в ярдах: та же настройка, что «Дальность "
+ .. "обзора» на странице «Графика», но с большим пределом. Дальше 777 ярдов, предела "
+ .. "оригинального клиента, дополнительное расстояние видно с современным туманом; "
+ .. "с оригинальным туманом его скрывает туман зоны.\n\n"
+ .. "Чем дальше, тем больше памяти, загрузки и работы видеокарты.",
LIGHTING_QUALITY =
"Задаёт тени персонажей, мира и огней внутри и снаружи помещений; "
.. "динамическое освещение помещений; лунные тени; объёмный туман; качество воды; свечение лавы; яркость огня и заклинаний; "
@@ -729,7 +775,9 @@
.. "источников света, с коэффициентами яркости ночи и помещений 1.0. "
.. "Все остальные настройки, включая дальность теней и мягкость теней от огня, "
.. "сохраняют ваши значения. «Низкое», «Среднее» и «Высокое» используют улучшенную воду; "
- .. "зеркальные отражения включаются отдельно. Изменение настройки набора показывает «Своё», "
+ .. "зеркальные отражения включаются отдельно. «Ультра» также поднимает разрешение теней "
+ .. "до 4096 и число теней от огней до 16 и 8, а вода и объёмный туман — «Высокое». "
+ .. "Изменение настройки набора показывает «Своё», "
.. "если ни один набор больше не совпадает.",
-- MONKEY (post): localised bloom effect and Off contract.
BLOOM =
@@ -912,6 +960,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.
@@ -1543,7 +1593,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
@@ -2672,11 +2735,51 @@
-
+
+
+
+ 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" },
+ })
+
+
+
+
+
+
+
+
+
+ OptionsRow_OnLoad(self, "farclip",
+ BENILLA_ADVGFX.renderDistance,
+ "BENILLA_TOOLTIP_RENDER_DISTANCE")
+ OptionsSliderRow_Setup(self, 177, 1497, 60, "yd")
+
+
+
+
+
+
+
-- The preset ladder. This row writes ONE CVar and
-- nothing else: the host's own table
@@ -2701,6 +2804,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" },
})
diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs
index 51fde75e..2131ce86 100644
--- a/crates/benilla-app/src/cvars.rs
+++ b/crates/benilla-app/src/cvars.rs
@@ -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 (p0 skyDither): the FFXGlow combine's deband dither (was env WOW_DITHER only).
@@ -1733,6 +1746,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
}
@@ -1744,9 +1760,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
@@ -2166,6 +2211,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
@@ -2219,6 +2293,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; the three pending ones sit commented with their
+/// intended columns until the rows they name are 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"]),
+ // TODO(presets): uncomment each as its row lands from the parallel merge.
+ // ("ambientOcclusion", ["0", "0", "1", "2", "2"]),
+ // ("zoneSkyboxes", ["0", "1", "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 {
+ 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
@@ -2313,6 +2463,11 @@ fn session_values(world: &World) -> Vec<(&'static str, Option)> {
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::();
for (name, value) in session {
@@ -2334,6 +2489,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 {
@@ -2341,6 +2501,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.
@@ -3108,6 +3284,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()
};
@@ -3192,10 +3370,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(), 8, "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::(&app).farclip,
+ farclip.parse::().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`.
@@ -3722,7 +4102,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();
}
@@ -4096,7 +4485,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
@@ -4413,6 +4809,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",
diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs
index 41062c64..8c2863c2 100644
--- a/crates/benilla-app/src/ui_script/options_tests.rs
+++ b/crates/benilla-app/src/ui_script/options_tests.rs
@@ -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::("return OPTIONS_PAGE_ROWS.AdvancedGraphics[3]").unwrap(), "RowVolumetricFog");
+ assert_eq!(s.eval::("return OPTIONS_PAGE_ROWS.AdvancedGraphics[5]").unwrap(), "RowVolumetricFog");
assert_eq!(s.eval::(&format!("return {ADVGFX}RowVolumetricFogLabel:GetText()")).unwrap(), title);
assert_eq!(s.eval::(&format!("return {ADVGFX}RowVolumetricFogDropdownText:GetText()")).unwrap(), labels[1]);
let _ = s.take_cvar_changes();
@@ -2067,9 +2067,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.
- assert_eq!(s.eval::("return table.getn(OPTIONS_PAGE_ROWS.AdvancedGraphics)").unwrap(), 18);
- assert_eq!(s.eval::("return OPTIONS_PAGE_ROWS.AdvancedGraphics[2]").unwrap(), "RowWaterQuality");
- assert_eq!(s.eval::("return OPTIONS_PAGE_ROWS.AdvancedGraphics[16]").unwrap(), "RowLavaGlow");
+ // MONKEY (presets): Graphics Preset and Render Distance lead the page: 27 -> 29.
+ assert_eq!(s.eval::("return table.getn(OPTIONS_PAGE_ROWS.AdvancedGraphics)").unwrap(), 29);
+ assert_eq!(s.eval::("return OPTIONS_PAGE_ROWS.AdvancedGraphics[4]").unwrap(), "RowWaterQuality");
+ assert_eq!(s.eval::("return OPTIONS_PAGE_ROWS.AdvancedGraphics[23]").unwrap(), "RowLavaGlow");
assert_eq!(s.eval::(&format!("return {ADVGFX}RowWaterQualityDropdownText:GetText()")).unwrap(), labels[1]);
assert_eq!(s.eval::(&format!("return {ADVGFX}RowLavaGlowLabel:GetText()")).unwrap(), lava_label);
assert!(s.eval::("return BENILLA_TOOLTIP_WATER_QUALITY == BENILLA_ADVGFX.tips.WATER_QUALITY and BENILLA_TOOLTIP_LAVA_GLOW == BENILLA_ADVGFX.tips.LAVA_GLOW").unwrap());
@@ -3033,6 +3034,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",
@@ -3143,7 +3153,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![