From 277deac06be0a775ad8656e020de05cf14fc6d3d Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 18 Sep 2026 14:31:51 +0100 Subject: [PATCH] =?UTF-8?q?MONKEY:=20lighting=20round=206=20=E2=80=94=20Ad?= =?UTF-8?q?vanced=20Graphics=20options=20page,=20moon=20shadows,=20ground-?= =?UTF-8?q?effect=20spell=20lights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Advanced Graphics page: Lighting Quality preset (Off/Low/Medium/High/Custom, one const table, Off = the original client's look), shadow rows moved from Graphics, Shadow Resolution, Moon Shadows, interior/torch/fire/spell/night rows; EN+RU strings; works in player builds. - Moon shadows: the single shadow rig re-aims at the moon at night (MoonShadowStrength 0.35); attenuates only the night sky term, never point lights; covers terrain, WMO exteriors, models and retained doodads; stateful hand-over for clock jumps; blob gated on a built silhouette; zero cost at strength 0. Signed fog_params.z carries +sun / -moon*strength. - Area spell lights: SpellLightMode::Area for Flamestrike, Rain of Fire, Consecration, Flare, Explosive Trap (school-based where the model says nothing); Blizzard/Hurricane/Volley/frost traps stay dark; repeating impacts deduped; benilla-extract spellvis prints the area block. 1121 world+formats tests; 8/8 WGSL probes; UI and world smoke scenes clean. Co-Authored-By: Claude Fable 5.1 --- crates/benilla-app/assets/ui/OptionsFrame.xml | 725 ++++++++++++++++-- crates/benilla-app/src/blob_shadow.rs | 97 ++- crates/benilla-app/src/character_shadow.rs | 26 + crates/benilla-app/src/cvars.rs | 428 ++++++++++- .../src/debug_panel/lighting_controls.rs | 28 +- crates/benilla-app/src/dynamic_interior.rs | 14 +- crates/benilla-app/src/entities.rs | 3 + .../benilla-app/src/entities/carried_light.rs | 139 +++- crates/benilla-app/src/entities/dest_fx.rs | 132 +++- .../src/entities/spell_fx/lifecycle.rs | 207 ++++- .../benilla-app/src/entities/spell_fx/mod.rs | 6 +- crates/benilla-app/src/shadow_core.rs | 137 +++- .../src/ui_script/options_tests.rs | 400 +++++++++- crates/benilla-app/src/video.rs | 21 +- crates/benilla-app/src/world_shadow.rs | 8 + .../src/shaders/shadow_hook.wgsl | 82 +- .../benilla-assets/src/shaders/terrain.wgsl | 36 +- crates/benilla-assets/src/shaders/wdl.wgsl | 2 +- .../benilla-assets/src/shaders/wow_model.wgsl | 33 +- .../src/bin/benilla-extract/spellvis.rs | 27 + crates/benilla-formats/src/fire_light.rs | 277 ++++++- crates/benilla-formats/src/lib.rs | 8 + .../src/lighting/global_light.rs | 475 +++++++++++- crates/benilla-world/src/lighting/mod.rs | 24 + .../benilla-world/src/shaders/static_gx.wgsl | 28 +- .../benilla-world/src/shaders/wow_effect.wgsl | 5 +- 26 files changed, 3180 insertions(+), 188 deletions(-) diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index d9d432eb..9e9cbae9 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -329,9 +329,12 @@ -- The static category tree (era Game tab, Classic tocs), in list order. Keys are the row -- name suffixes; the visible label doubles as the page title (the era does the same — -- Settings.RegisterCategory names both from one string). + -- MONKEY (advanced graphics): "AdvancedGraphics" joins the System group under Graphics — + -- the seat the era's own tree gives a page that refines the one above it, and the seat + -- that keeps the three shadow rows it inherits one row away from where they used to be. OPTIONS_CATEGORY_KEYS = { "Controls", "Interface", "ActionBars", "Combat", "Chat", "Keybindings", "Nameplates", - "Graphics", "Audio", + "Graphics", "AdvancedGraphics", "Audio", } -- The era-window scale (shared, director-owned — the 0219 WORLDMAP_WINDOWED_SCALE @@ -804,19 +807,265 @@ .. "which it could only do in exclusive fullscreen. benilla applies it to the " .. "finished image instead, so it works in both display modes." - BENILLA_TOOLTIP_CHARACTER_SHADOWS = - "Renders real dynamic silhouettes for players, NPCs, creatures, and mounts, replacing " - .. "the legacy oval blob. Matches Wrath's character-only shadow tier. This uses " - .. "additional GPU time and memory." + -- ══ MONKEY (advanced graphics): the Advanced Graphics page's own strings, EN + RU ═════ + -- + -- The page that gives a PLAYER the dynamic light and shadow system. Until it existed the + -- lanes were reachable only from `/console` and the native debug panel — and the panel + -- does not ship in a player build, so a player had no way in at all. Three rows move here + -- off the Graphics page (Real Character Shadows, World Shadows, Shadow Distance) with + -- their settings and their descriptions unchanged; the other twelve are new seats on + -- CVars this client has read for some time. + -- + -- **Every string on it is authored in both languages** (the project's own standing rule), + -- picked by `GetLocale()`, and there is no other localisation pattern in this window to + -- follow: every page above is 1.12 GlobalStrings, whose own file IS the localisation. So + -- the table is here, next to the page, rather than in a scheme of its own. + -- + -- TWO CAVEATS, both true of the client and not of this table: benilla's `GetLocale()` + -- answers `enUS` unconditionally today (it ships and reads enUS data only), so the ruRU + -- column is dormant until that changes; and 1.12's own `FRIZQT__.TTF` carries no Cyrillic + -- glyphs, so a Russian build needs a font that does before these read as words. Neither is + -- a reason to ship the page half-authored — both are one line away from being true, and a + -- string written later is a string written to a different standard. + -- + -- The VALUES the rows write are never localised: a dropdown entry carries the engine's own + -- string in `value` and the player's language in `text`, so `config.toml` and `/console` + -- read the same words on every install. + BENILLA_ADVGFX_STRINGS = { + enUS = { + page = "Advanced Graphics", + lightingQuality = "Lighting Quality", + qOff = "Off", + qLow = "Low", + qMedium = "Medium", + qHigh = "High", + qCustom = "Custom", + characterShadows = "Real Character Shadows", + worldShadows = "World Shadows", + shadowDistance = "Shadow Distance", + shadowResolution = "Shadow Resolution", + moonShadows = "Moon Shadows", + interiorLight = "Dynamic Interior Lighting", + interiorShadows = "Indoor Torch Shadows", + exteriorShadows = "Outdoor Torch Shadows", + torchSoft = "Torch Shadow Softness", + fireLight = "Fire Light Brightness", + fireFlicker = "Fire Flicker", + spellLights = "Spell Lights", + nightDarkness = "Night Darkness", + interiorDarkness = "Interior Darkness", + yards = "%d yd", + tips = { + LIGHTING_QUALITY = + "Sets character, world, indoor torch and outdoor torch shadows; dynamic " + .. "interior lighting; moon shadows; fire and spell brightness; fire flicker; " + .. "and night and interior darkness. Low, Medium and High also set shadow " + .. "resolution; Medium and High also set resident and moving torch shadow counts.\n\n" + .. "Off restores the original client's look: no synthesised fire lights, " + .. "night gain 1.0 and interior gain 1.0. All other settings, including " + .. "Shadow Distance and Torch Shadow Softness, stay as you set them. " + .. "Changing a preset member shows Custom when no preset matches.", + CHARACTER_SHADOWS = + "Renders real dynamic silhouettes for players, NPCs, creatures, and mounts, replacing " + .. "the legacy oval blob. Matches Wrath's character-only shadow tier. This uses " + .. "additional GPU time and memory.", + WORLD_SHADOWS = + "Casts real dynamic shadows from the static world (trees, buildings, and foliage) " + .. "following the sun, and switches off the baked terrain shadows they replace. " + .. "Independent of Real Character Shadows. This uses additional GPU time and memory.", + SHADOW_DISTANCE = + "How far realtime shadows are drawn, in yards. Higher covers more ground but spreads " + .. "the shadow map thinner (softer edges) and costs more. Applies on Apply.", + SHADOW_RESOLUTION = + "The edge of the sun's shadow map, in texels. Larger is sharper " + .. "everywhere the shadow distance reaches.\n\n" + .. "The cost is quadratic: 4096 draws four times the shadow pass of 2048 " + .. "and holds four times the depth texture.", + MOON_SHADOWS = + "How dark a shadow thrown by moonlight is allowed to get. A little keeps " + .. "the night from reading flat; a lot is stylised moonlight the original " + .. "client never had.\n\n" + .. "At 0 the moon casts nothing at all and the night renders exactly as " + .. "it did before this existed. It costs nothing by day.", + INTERIOR_LIGHT = + "Lights the inside of buildings from the lamps, candles and hearths that " + .. "actually stand in the room, instead of the lighting baked into the " + .. "walls.\n\n" + .. "Off restores the original client's baked interiors. On, it costs a " + .. "little GPU time per lit surface. Indoor Torch Shadows require it; " + .. "Outdoor Torch Shadows work independently.", + INTERIOR_SHADOWS = + "Lets the nearest indoor fires cast real shadows — a candle throws the " + .. "table's shadow across the floor.\n\n" + .. "Each promoted fire costs a shadow map of its own, which makes this " + .. "the most expensive row on the page. Needs Dynamic Interior Lighting.", + EXTERIOR_SHADOWS = + "The same for fires out in the open: campfires, braziers and lampposts " + .. "cast real shadows after dark.\n\n" + .. "It does nothing by day and does not require Dynamic Interior Lighting.", + TORCH_SOFT = + "How soft the edge of a torch shadow is where the object meets the " + .. "ground. Low is hard and crisp; high spreads the edge the way a real " + .. "flame does.\n\n" + .. "It costs no extra shadow maps — only a wider filter over the ones " + .. "already drawn.", + FIRE_LIGHT = + "How brightly campfires, torches, braziers and forges light the world " + .. "around them. These are lights benilla invents from the flame itself; " + .. "the original client's art carries none.\n\n" + .. "At 0 the invented fire lights are off and only the lights the artists " + .. "placed remain. Moving it costs nothing.", + FIRE_FLICKER = + "How strongly every flame's light breathes — candles quick and shallow, " + .. "bonfires slow and steady.\n\n" + .. "At 0 fires burn at a constant brightness, which is what they did " + .. "before this existed. It is computed on the processor and costs the " + .. "graphics card nothing.", + SPELL_LIGHTS = + "How brightly spells, missiles and impacts light the room around them — a " + .. "fireball's flash, an aura's glow.\n\n" + .. "Separate from Fire Light Brightness on purpose: calming a busy fight " + .. "should not dim the world's hearths. At 0 no spell throws light.", + NIGHT_DARKNESS = + "How dark the world gets outdoors after sunset. " + .. "1.0 = the original client night; lower = darker.\n\n" + .. "It never touches the fires — the point is a darker night around the " + .. "flame, not a dimmer flame. Inert while the sun is up, and free at any " + .. "setting.", + INTERIOR_DARKNESS = + "Scales interior fixture light and room ambient, including the light from " + .. "lamps and candles. Lower values make both dimmer.\n\n" + .. "1.0 leaves these inputs unchanged. Free at any setting.", + }, + }, + ruRU = { + page = "Расширенная графика", + lightingQuality = "Качество освещения", + qOff = "Выкл.", + qLow = "Низкое", + qMedium = "Среднее", + qHigh = "Высокое", + qCustom = "Своё", + characterShadows = "Настоящие тени персонажей", + worldShadows = "Тени мира", + shadowDistance = "Дальность теней", + shadowResolution = "Разрешение теней", + moonShadows = "Лунные тени", + interiorLight = "Динамическое освещение помещений", + interiorShadows = "Тени факелов в помещениях", + exteriorShadows = "Тени огней под открытым небом", + torchSoft = "Мягкость теней от огня", + fireLight = "Яркость огня", + fireFlicker = "Мерцание огня", + spellLights = "Свет от заклинаний", + nightDarkness = "Темнота ночи", + interiorDarkness = "Темнота помещений", + yards = "%d ярд.", + tips = { + LIGHTING_QUALITY = + "Задаёт тени персонажей, мира и огней внутри и снаружи помещений; " + .. "динамическое освещение помещений; лунные тени; яркость огня и заклинаний; " + .. "мерцание огня; темноту ночи и помещений. «Низкое», «Среднее» и «Высокое» " + .. "также задают разрешение теней; «Среднее» и «Высокое» — число карт теней " + .. "от огней и число огней с тенями движущихся объектов.\n\n" + .. "«Выкл.» возвращает вид оригинального клиента: без созданных из пламени " + .. "источников света, с коэффициентами яркости ночи и помещений 1.0. " + .. "Все остальные настройки, включая дальность теней и мягкость теней от огня, " + .. "сохраняют ваши значения. Изменение настройки набора показывает «Своё», " + .. "если ни один набор больше не совпадает.", + CHARACTER_SHADOWS = + "Рисует настоящие динамические силуэты игроков, НИП, существ и ездовых " + .. "животных вместо старого овального пятна. Соответствует уровню теней " + .. "персонажей из Wrath. Требует дополнительного времени и памяти " + .. "видеокарты.", + WORLD_SHADOWS = + "Заставляет статичный мир — деревья, здания и растительность — " + .. "отбрасывать настоящие динамические тени вслед за солнцем и отключает " + .. "запечённые тени ландшафта, которые они заменяют. Не зависит от " + .. "настоящих теней персонажей. Требует дополнительного времени и памяти " + .. "видеокарты.", + SHADOW_DISTANCE = + "Как далеко рисуются тени в реальном времени, в ярдах. Большее значение " + .. "покрывает больше земли, но растягивает карту теней тоньше (края " + .. "становятся мягче) и стоит дороже. Применяется по кнопке «Применить».", + SHADOW_RESOLUTION = + "Размер карты теней солнца в текселях. Чем больше, тем чётче тени на " + .. "всей дальности их прорисовки.\n\n" + .. "Стоимость растёт квадратично: 4096 — это вчетверо больше работы в " + .. "проходе теней и вчетверо больше видеопамяти под карту глубины, чем " + .. "2048.", + MOON_SHADOWS = + "Насколько тёмной может стать тень, отбрасываемая лунным светом. " + .. "Немного — и ночь перестаёт выглядеть плоской; много — это " + .. "стилизованная луна, какой в оригинальном клиенте не было.\n\n" + .. "При 0 луна не отбрасывает теней вовсе, и ночь выглядит так же, как " + .. "до появления этой возможности. Днём не стоит ничего.", + INTERIOR_LIGHT = + "Освещает помещения от ламп, свечей и очагов, которые действительно " + .. "стоят в комнате, а не от освещения, запечённого в стенах.\n\n" + .. "«Выкл.» возвращает запечённые интерьеры оригинального клиента. " + .. "Включённый режим стоит немного времени видеокарты на каждую " + .. "освещённую поверхность. Тени от огней внутри требуют этого режима; " + .. "тени от огней снаружи работают независимо.", + INTERIOR_SHADOWS = + "Позволяет ближайшим огням в помещении отбрасывать настоящие тени: " + .. "свеча кладёт тень стола на пол.\n\n" + .. "Каждый такой огонь стоит собственной карты теней, поэтому это самая " + .. "дорогая строка на странице. Требует динамического освещения " + .. "помещений.", + EXTERIOR_SHADOWS = + "То же для огней под открытым небом: костры, жаровни и фонари " + .. "отбрасывают настоящие тени после наступления темноты.\n\n" + .. "Днём не действует и не требует динамического освещения помещений.", + TORCH_SOFT = + "Насколько мягок край тени от огня там, где предмет касается пола. " + .. "Низкое значение — резкий край, высокое — размытый, как от " + .. "настоящего пламени.\n\n" + .. "Дополнительных карт теней не требует: это лишь более широкий фильтр " + .. "по уже нарисованным.", + FIRE_LIGHT = + "Насколько ярко костры, факелы, жаровни и горны освещают всё вокруг. " + .. "Эти источники benilla создаёт из самого пламени — в оригинальной " + .. "графике их нет.\n\n" + .. "При 0 придуманные огни выключены и остаются лишь источники, " + .. "расставленные художниками. Изменение значения ничего не стоит.", + FIRE_FLICKER = + "Насколько сильно «дышит» свет каждого пламени: свечи — часто и слабо, " + .. "костры — медленно и ровно.\n\n" + .. "При 0 огни горят с постоянной яркостью, как было до появления этой " + .. "возможности. Считается на процессоре и ничего не стоит видеокарте.", + SPELL_LIGHTS = + "Насколько ярко заклинания, снаряды и попадания освещают всё вокруг: " + .. "вспышка огненного шара, свечение ауры.\n\n" + .. "Отделено от яркости огня намеренно: успокоить шумный бой не должно " + .. "значить пригасить очаги во всём мире. При 0 заклинания не дают " + .. "света.", + NIGHT_DARKNESS = + "Насколько темно становится под открытым небом после заката. " + .. "1.0 — ночь оригинального клиента; ниже — темнее.\n\n" + .. "Огней не касается: смысл в том, чтобы ночь вокруг пламени была " + .. "темнее, а не само пламя тусклее. Пока солнце в небе, не действует, и " + .. "ничего не стоит при любом значении.", + INTERIOR_DARKNESS = + "Масштабирует свет светильников и фоновое освещение помещений, включая " + .. "свет ламп и свечей. Меньшие значения ослабляют и то и другое.\n\n" + .. "1.0 оставляет эти составляющие без изменений. Ничего не стоит при любом значении.", + }, + }, + } - BENILLA_TOOLTIP_WORLD_SHADOWS = - "Casts real dynamic shadows from the static world (trees, buildings, and foliage) " - .. "following the sun, and switches off the baked terrain shadows they replace. " - .. "Independent of Real Character Shadows. This uses additional GPU time and memory." + -- The selected column. `or` the English one so an unbuilt locale is a fallback rather + -- than fifteen rows of `nil` labels — 1.12's own GlobalStrings do the same thing by + -- shipping one file per locale with every key present. + BENILLA_ADVGFX = BENILLA_ADVGFX_STRINGS[GetLocale()] or BENILLA_ADVGFX_STRINGS.enUS - BENILLA_TOOLTIP_SHADOW_DISTANCE = - "How far realtime shadows are drawn, in yards. Higher covers more ground but spreads " - .. "the shadow map thinner (softer edges) and costs more. Applies on Okay/Apply." + -- A row's description resolves by GLOBAL NAME at hover time (`OptionsRow_ShowTooltip`), so + -- the selected column's copy is published under the key the row carries. Same `BENILLA_` + -- prefix the four rows above use, and for the same reason: the reference's + -- `OPTION_TOOLTIP_` namespace stays the reference's. + for key, text in pairs(BENILLA_ADVGFX.tips) do + setglobal("BENILLA_TOOLTIP_" .. key, text) + end OPTIONS_PAGE_ROWS = { -- Seven rows became sixteen at 2180, and the nine that arrived were not new @@ -851,9 +1100,25 @@ "RowUiScale", "RowFarclip", "RowWorldDetail", "RowBrightness", "RowWeatherDensity", "RowDisplayMode", "RowVerticalSync", - "RowCharacterShadows", "RowWorldShadows", "RowShadowDistance", "RowRenderScale", }, + -- MONKEY (advanced graphics): the dynamic light + shadow system's own page. Order is + -- the preset first (it writes the rest), then the SUN lanes, then the ROOM lane and + -- its torches, then the invented lights, then the two darkness dials — coarse to + -- fine, and each group's own master above the rows it gates. + -- + -- Real Character Shadows, World Shadows and Shadow Distance MOVED here from Graphics, + -- carrying their CVars and their descriptions unchanged: they belong with the rest of + -- 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 = { + "RowLightingQuality", + "RowCharacterShadows", "RowWorldShadows", "RowShadowDistance", + "RowShadowResolution", "RowMoonShadows", + "RowInteriorLight", "RowInteriorShadows", "RowExteriorShadows", "RowTorchSoft", + "RowFireLight", "RowFireFlicker", "RowSpellLights", + "RowNightDarkness", "RowInteriorDarkness", + }, -- Player Guild Names (2180) rides as a CHILD of Player Names, which is the -- reference's own relationship twice over: its CheckButton22 hangs off 21 with a +26 -- x indent, and UIOptionsFrame_UpdateDependencies l.702-708 greys 22 (and 23) from @@ -1030,7 +1295,7 @@ -- The write. A global's consumers are Lua and do not poll, so a uvar row also runs its -- `applyFunc` — the reference's own per-uvar side effects (SIMPLE_CHAT → FCF_Set_*, -- HIDE_PARTY_INTERFACE → RaidOptionsFrame_UpdatePartyFrames, the combat-text family → - -- CombatText_UpdateDisplayedMessages), moved from its batched Okay to our live write + -- CombatText_UpdateDisplayedMessages), moved from its batched confirmation to our live write -- because this window commits on the click (0961's grammar), not on a Save button. -- `cvarEvent` is a CVar row's counterpart to a global row's `applyFunc`, and it is the -- reference's own third SetCVar argument (decision 1140): the token the engine hands back @@ -1368,11 +1633,33 @@ return string.format("%d%%", math.floor((value - row.minv) / (row.maxv - row.minv) * 100 + 0.5)) end + -- MONKEY (advanced graphics): travel, READ BACKWARDS — the same share of the groove, + -- counted from the right-hand end. The two darkness dials are the case: their stored + -- unit is a GAIN (`nightGain`, `interiorGain`), so the darkest setting is the SMALLEST + -- number and sits at the left of the groove, while the row is named for the darkness + -- rather than the gain. Without this the readout would count UP as the world got + -- LIGHTER under a label that says "Darkness", which is exactly backwards; with it the + -- label, the thumb and the number all agree that further left is darker. The store + -- keeps the gain, unchanged — this is a readout, not a unit. + if row.fmt == "darkness" and row.minv and row.maxv and row.maxv > row.minv then + return string.format("%d%%", + math.floor((row.maxv - value) / (row.maxv - row.minv) * 100 + 0.5)) + end -- A row whose stored value is a FACTOR but whose meaning is a distance: show the -- distance (decision 1140 — cameraDistanceMaxFactor over the 15 yd base). The number -- the player is choosing is "how far out the wheel stops", not "times 1.4". if row.fmt == "yards" then - return string.format("%d yd", math.floor(value * 15 + 0.5)) + return string.format(BENILLA_ADVGFX.yards, math.floor(value * 15 + 0.5)) + end + -- MONKEY (advanced graphics): a row whose stored value ALREADY IS a distance in yards + -- — the realtime-shadow range. Its own format, because the one above it is a + -- CONVERSION and not a unit: `shadowDistance` rode `"yards"` from the day it was + -- seated, so an 80-yard cascade printed "1200 yd" and nothing caught it (no test read + -- that readout, and 1200 is not an absurd-looking number for a view distance). The + -- two formats differ by exactly the factor's 15 yd base, which is why they cannot be + -- one. + if row.fmt == "yd" then + return string.format(BENILLA_ADVGFX.yards, math.floor(value + 0.5)) end return string.format("%d%%", math.floor(value * 100 + 0.5)) end @@ -1533,6 +1820,47 @@ OptionsApply_Update() end + -- ── MONKEY (advanced graphics): the page that has to WATCH its settings ───────────── + -- Every other page in this window owns its rows outright: nothing but a click on the page + -- moves the CVars behind it while it is open, so reading them once on select is the whole + -- contract. The lighting rows are not like that, and deliberately so — they are the live + -- A/B knobs the whole system was tuned with, and there are three other ways to move one + -- with this page on screen: `/console set nightGain 1` in chat, the native lighting panel + -- in a dev build, and — the one that matters to a player — the Lighting Quality row above, + -- which does not write its members here at all. It writes ONE CVar; the host's preset + -- table answers by writing the other ten, and those land a frame or two later. + -- + -- So the page polls. A cheap signature of its own rows' values, four times a second, and a + -- refresh only when that string actually moves — which is never, on a page nobody is + -- touching. The alternative (an event) does not exist: a host-side CVar write reaches the + -- interface as a mirror update, not as `CVAR_UPDATE`, which the reference only ever fires + -- for a script's own `SetCVar` with a token. + -- + -- Safe under a drag, and not by luck: the value the refresh would write back is the value + -- the drag just wrote, `SetValue` fires nothing when it does not change the value, and the + -- `refreshing` guard stops a write even if it did. A staged (deferred) edit is safe too — + -- `OptionsPage_Refresh` shows `row.pending` ahead of the stored value. + BENILLA_ADVGFX_POLL = 0.25 + + function AdvGfx_Signature() + local out = "" + for _, rkey in ipairs(OPTIONS_PAGE_ROWS.AdvancedGraphics) do + local row = getglobal("BenillaOptionsFrameContainerBodyAdvancedGraphics" .. rkey) + out = out .. "|" .. (OptionsRow_Get(row) or "") + end + return out + end + + function AdvGfx_OnUpdate(page, elapsed) + page.since = (page.since or 0) + (elapsed or 0) + if page.since < BENILLA_ADVGFX_POLL then return end + page.since = 0 + local sig = AdvGfx_Signature() + if sig == page.sig then return end + page.sig = sig + OptionsPage_Refresh("AdvancedGraphics") + end + -- Per-page dependency rules. Audio's is the 1.12 rule, verbatim: Enable All Sound off -- greys Enable Ambience — and only it, of our rows -- (SoundOptionsFrame_UpdateDependencies disables buttons 2 and 4, Ambience and Error @@ -1575,6 +1903,27 @@ OptionsRow_SetEnabled(BenillaOptionsFrameContainerBodyCombatRowPetDamage, dmg) return end + if key == "AdvancedGraphics" then + -- MONKEY (advanced graphics): the page's own dependencies — the same shape the + -- reference's own rules have (a master's own switch greys what it gates), read + -- off the CVar docs rather than invented: `interiorShadows` "needs + -- interiorLight", the two shadow-rig rows size a cascade that is not rendered + -- with both sun lanes off, and the torch softness is the PCF radius on maps + -- neither torch lane is drawing. The CVars keep their values throughout — this + -- is a dependency, not a write, so turning a master back on finds its children + -- exactly where they were left. + local base = "BenillaOptionsFrameContainerBodyAdvancedGraphics" + local sun = GetCVar("characterShadows") ~= "0" + or GetCVar("worldShadows") ~= "0" + OptionsRow_SetEnabled(getglobal(base .. "RowShadowDistance"), sun) + OptionsRow_SetEnabled(getglobal(base .. "RowShadowResolution"), sun) + local interior = GetCVar("interiorLight") ~= "0" + OptionsRow_SetEnabled(getglobal(base .. "RowInteriorShadows"), interior) + local torch = (interior and GetCVar("interiorShadows") ~= "0") + or GetCVar("exteriorShadows") ~= "0" + OptionsRow_SetEnabled(getglobal(base .. "RowTorchSoft"), torch) + return + end if key == "Controls" then -- 1.12's own rule (l.715-719): the Auto-Follow Speed slider is dead while the -- following style is Never, because there is no follow to set a speed for. The @@ -2346,8 +2695,16 @@ OptionsCategoryRow_OnLoad(self, "Graphics", "Graphics") - + @@ -3099,48 +3456,18 @@ "OPTION_TOOLTIP_VERTICAL_SYNC") - + + - - OptionsRow_OnLoad(self, "characterShadows", "Real Character Shadows", - "BENILLA_TOOLTIP_CHARACTER_SHADOWS") - - - - - - - - - OptionsRow_OnLoad(self, "worldShadows", "World Shadows", - "BENILLA_TOOLTIP_WORLD_SHADOWS") - - - - - - - - - -- Realtime-shadow render distance (yd): the cascade - -- range + caster reach. 40-200, matching the CVar's - -- own clamp (SHADOW_DISTANCE_RANGE). DEFERRED to - -- Apply so a drag doesn't re-fit the cascade + recollect - -- casters on every tick (one rebuild on commit instead). - OptionsRow_OnLoad(self, "shadowDistance", "Shadow Distance", - "BENILLA_TOOLTIP_SHADOW_DISTANCE") - OptionsSliderRow_Setup(self, 40, 200, 10, "yards") - self.deferred = 1 - - - - - - - -- Render Scale (decision 1639) -- benilla's own -- row, no 1.12 counterpart: the reference renders @@ -3199,6 +3526,288 @@ + + + + + + + + + + + + + + -- The preset ladder. This row writes ONE CVar and + -- nothing else: the host's own table + -- (`cvars::LIGHTING_PRESETS`) answers the write by + -- setting the member rows, and re-derives this + -- row's value from them every frame — which is why + -- Custom is an entry the capsule can SHOW but + -- picking it writes nothing anyone keeps. + -- + -- The table is deliberately NOT here. Three other + -- ways reach the same ladder (the console, the + -- native lighting panel, a hand-edited + -- config.toml), and a copy carried by this window + -- would be a copy the other three cannot see. + -- The page's OnUpdate poll is what brings the + -- member rows' new values back onto the screen. + OptionsRow_OnLoad(self, "lightingQuality", + BENILLA_ADVGFX.lightingQuality, + "BENILLA_TOOLTIP_LIGHTING_QUALITY") + OptionsDropdownRow_Setup(self, { + { text = BENILLA_ADVGFX.qOff, value = "Off" }, + { text = BENILLA_ADVGFX.qLow, value = "Low" }, + { text = BENILLA_ADVGFX.qMedium, value = "Medium" }, + { text = BENILLA_ADVGFX.qHigh, value = "High" }, + { text = BENILLA_ADVGFX.qCustom, value = "Custom" }, + }) + + + + + + + + + OptionsRow_OnLoad(self, "characterShadows", + BENILLA_ADVGFX.characterShadows, + "BENILLA_TOOLTIP_CHARACTER_SHADOWS") + + + + + + + + + OptionsRow_OnLoad(self, "worldShadows", + BENILLA_ADVGFX.worldShadows, + "BENILLA_TOOLTIP_WORLD_SHADOWS") + + + + + + + + + -- Realtime-shadow render distance (yd): the cascade + -- range + caster reach. 40-200, matching the CVar's + -- own clamp (SHADOW_DISTANCE_RANGE). DEFERRED to + -- Apply so a drag doesn't re-fit the cascade + recollect + -- casters on every tick (one rebuild on commit instead). + OptionsRow_OnLoad(self, "shadowDistance", + BENILLA_ADVGFX.shadowDistance, + "BENILLA_TOOLTIP_SHADOW_DISTANCE") + -- `"yd"`, not `"yards"`: the value is already a + -- distance in yards, where the format beside it + -- multiplies a FACTOR by the 15 yd camera base. + -- This row rode the wrong one from the day it was + -- seated and printed "1200 yd" for an 80 yd + -- cascade. + OptionsSliderRow_Setup(self, 40, 200, 10, "yd") + self.deferred = 1 + + + + + + + + + -- The three stops the CVar's own snap accepts + -- (`shadow_core::SHADOW_MAP_SIZES`): a dropdown + -- rather than a slider because an off-ladder value + -- is not a weaker setting, it is one the renderer + -- rounds UP into a bigger and slower map than the + -- one that was asked for. The entry texts are the + -- numbers themselves — nothing to translate. + OptionsRow_OnLoad(self, "shadowMapSize", + BENILLA_ADVGFX.shadowResolution, + "BENILLA_TOOLTIP_SHADOW_RESOLUTION") + OptionsDropdownRow_Setup(self, { + { text = "1024", value = "1024" }, + { text = "2048", value = "2048" }, + { text = "4096", value = "4096" }, + }) + + + + + + + + + -- 0..1, the CVar's own clamp, and 0 is the FAITHFUL + -- null rather than a floor: every consumer guards + -- on it, so the left end of this groove is the + -- night the client rendered before the moon lane + -- existed. + OptionsRow_OnLoad(self, "moonShadowStrength", + BENILLA_ADVGFX.moonShadows, + "BENILLA_TOOLTIP_MOON_SHADOWS") + OptionsSliderRow_Setup(self, 0, 1, 0.05) + + + + + + + + + OptionsRow_OnLoad(self, "interiorLight", + BENILLA_ADVGFX.interiorLight, + "BENILLA_TOOLTIP_INTERIOR_LIGHT") + + + + + + + + + OptionsRow_OnLoad(self, "interiorShadows", + BENILLA_ADVGFX.interiorShadows, + "BENILLA_TOOLTIP_INTERIOR_SHADOWS") + + + + + + + + + OptionsRow_OnLoad(self, "exteriorShadows", + BENILLA_ADVGFX.exteriorShadows, + "BENILLA_TOOLTIP_EXTERIOR_SHADOWS") + + + + + + + + + -- 0.5..3, the CVar's own clamp verbatim: it is a + -- SCALE on the PCF tap radius at a contact, so the + -- readout is a percentage of the shipped radius + -- and 100% is what the lane was tuned at. + OptionsRow_OnLoad(self, "interiorShadowSoft", + BENILLA_ADVGFX.torchSoft, + "BENILLA_TOOLTIP_TORCH_SOFT") + OptionsSliderRow_Setup(self, 0.5, 3, 0.1) + + + + + + + + + -- Full 0..4 registry range, in five-percent steps. + OptionsRow_OnLoad(self, "fireLightGain", + BENILLA_ADVGFX.fireLight, + "BENILLA_TOOLTIP_FIRE_LIGHT") + OptionsSliderRow_Setup(self, 0, 4, 0.05) + + + + + + + + + -- 0..2, the CVar's own clamp verbatim: the + -- amplitudes are authored at 100%, and 200% is the + -- deliberate over-drive for judging the shape. + OptionsRow_OnLoad(self, "fireFlicker", + BENILLA_ADVGFX.fireFlicker, + "BENILLA_TOOLTIP_FIRE_FLICKER") + OptionsSliderRow_Setup(self, 0, 2, 0.05) + + + + + + + + + -- Full 0..4 registry range, in five-percent steps. + OptionsRow_OnLoad(self, "spellLightGain", + BENILLA_ADVGFX.spellLights, + "BENILLA_TOOLTIP_SPELL_LIGHTS") + OptionsSliderRow_Setup(self, 0, 4, 0.05) + + + + + + + + + -- 0.2..1.5, the CVar's own clamp verbatim: the + -- floor is 0.2 rather than 0 because a true 0 is + -- indistinguishable from a broken light pack, and + -- the "off" a player wants is the reference's own + -- night at gain 1. + -- + -- The readout is `darkness` — travel read from the + -- right — because the stored unit is a GAIN while + -- the row is named for the darkness: left is + -- darker, and this is what makes the number say so + -- too. The store keeps the gain, untouched. + OptionsRow_OnLoad(self, "nightGain", + BENILLA_ADVGFX.nightDarkness, + "BENILLA_TOOLTIP_NIGHT_DARKNESS") + OptionsSliderRow_Setup(self, 0.2, 1.5, 0.05, "darkness") + + + + + + + + + -- The room lane's own dim dial, same clamp and same + -- backwards readout as the night one above it. + OptionsRow_OnLoad(self, "interiorGain", + BENILLA_ADVGFX.interiorDarkness, + "BENILLA_TOOLTIP_INTERIOR_DARKNESS") + OptionsSliderRow_Setup(self, 0.2, 1.5, 0.05, "darkness") + + + + + + AdvGfx_OnUpdate(this, arg1) + + +