diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 924a22bd..3c13d6d0 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -449,7 +449,7 @@ "RowBackgroundSound", }, Graphics = { - "RowUiScale", "RowFarclip", "RowWorldDetail", "RowBrightness", + "RowUiScale", "RowFarclip", "RowWorldDetail", "RowSpellDetail", "RowBrightness", "RowWeatherDensity", "RowDisplayMode", "RowVerticalSync", "RowRenderScale", }, @@ -593,10 +593,10 @@ -- UIOptionsFrame.lua:193-227 re-applies saved uvars, so this window has no load ladder. -- OptionsFrameSliders is the stock table (OptionsFrame.lua:24-34), which addons write. - -- Holes at 4, 5, 7 and 8, so the OnShow loop uses the pairs form: ipairs would stop at 3. + -- Holes at 4, 5 and 7, so the OnShow loop uses the pairs form: ipairs would stop at 3. BENILLA_GRAPHICS_SLIDER_ROWS = { "RowUiScale", "RowFarclip", "RowWorldDetail", - [6] = "RowBrightness", [9] = "RowWeatherDensity", + [6] = "RowBrightness", [8] = "RowSpellDetail", [9] = "RowWeatherDensity", } -- SetMinMaxValues re-clamps and fires OnValueChanged, which must not write back. @@ -1877,13 +1877,31 @@ { "Low", "Medium", "High" }) + + + + + + + + BenillaOptionsRow_OnLoad(this, "spellEffectLevel", SPELL_DETAIL, + "OPTION_TOOLTIP_SPELL_DETAIL") + -- Our names for the handler's emission scalars + -- 0.33, 0.66 and 1.0 (`0x689510`). + BenillaOptionsSliderRow_Setup(this, OptionsFrameSliders[8].minValue, + OptionsFrameSliders[8].maxValue, + OptionsFrameSliders[8].valueStep, nil, + { "Low", "Medium", "High" }) + + - - + + -- OptionsFrame_SetDefaults' default (l.444-445); diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars/mod.rs similarity index 72% rename from crates/benilla-app/src/cvars.rs rename to crates/benilla-app/src/cvars/mod.rs index e0e1a1fd..daca3784 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars/mod.rs @@ -5,7 +5,7 @@ //! `GetCVar`/`SetCVar` ([`benilla_ui::script::UiScript::seed_cvars`]) whose writes queue back here. //! //! - [`REGISTERED`] holds only vars something reads, a host knob or a Lua consumer. A row's default -//! is the reference's, and [`Registered::reference`] says where it stands against it. +//! is the reference's, and [`table::Registered::reference`] says where it stands against it. //! - The change callback is a Bevy observer on [`CvarChanged`], beside the knob it writes; the //! registry applies nothing itself. //! - A latched row's write is staged in [`Row::pending`] until [`Cvars::commit_latched`], the @@ -27,634 +27,10 @@ use bevy::prelude::*; use crate::ui_script::VmMemo; use benilla_ui::script::{SeededCvar, UiScript}; -/// One host-backed CVar: its name, benilla's default, and where that default stands against the -/// reference's ([`Reference`]). -pub(crate) struct Registered { - /// The registered name, in the reference's own spelling. - pub(crate) name: &'static str, - /// What a fresh `benilla-config` runs at, and what `GetCVar` answers until the player moves it. - pub(crate) default: &'static str, - /// Where `default` stands against the reference's. Read only by the tests: it records the - /// reference, never a value this client acts on. - #[allow(dead_code)] - pub(crate) reference: Reference, - /// Registered with flag bit1 (`rec+0x1c & 0x2`, `flags` 2 or 3 at the register site): a write - /// is staged in [`Row::pending`] until [`Cvars::commit_latched`]. - pub(crate) latched: bool, -} - -impl Registered { - /// Mark the row latched. - pub(crate) const fn latched(self) -> Self { - Self { - latched: true, - ..self - } - } -} - -/// benilla's default against the reference's: the string the reference's `CVar::Register` -/// (`0x63db90`) passes for the name, or, for a setting 1.12 keeps in FrameXML, the value -/// `UIOptionsFrame.lua` boots it at. Not a `Config.wtf` line (`SaveConfig 0x63d980` writes only -/// values off their default), and not always what a fresh install runs at: `hwDetect` rewrites -/// sixteen video CVars from `VideoHardware.dbc` before the first frame ([`Reference::Overridden`]). -/// Each row's register site or FrameXML line is cited above it. -#[allow(dead_code)] // read only by the tests -pub(crate) enum Reference { - /// The reference registers this default and benilla ships it; the test compares the two. - Same(&'static str), - /// The reference registers `registered`, but its own boot code overwrites it before the first - /// frame; `default` is where that lands, and `why` is the override. - Overridden { - registered: &'static str, - why: &'static str, - }, - /// The reference ships `value` and benilla ships another; `why` is the reason. - Deviates { - value: &'static str, - why: &'static str, - }, - /// No reference setting to match: benilla's own knob, or a later-era name for something 1.12 - /// never made settable. `why` says which, and what the reference does instead. - Ours(&'static str), -} - -/// A row whose default is the reference's own registered string. -const fn same(name: &'static str, default: &'static str) -> Registered { - Registered { - name, - default, - reference: Reference::Same(default), - latched: false, - } -} - -/// A row whose default follows the reference's boot-time override of its registered string. -const fn overridden( - name: &'static str, - default: &'static str, - registered: &'static str, - why: &'static str, -) -> Registered { - Registered { - name, - default, - reference: Reference::Overridden { registered, why }, - latched: false, - } -} - -/// A row that ships something other than the reference's `value`, for `why`. -const fn deviates( - name: &'static str, - default: &'static str, - value: &'static str, - why: &'static str, -) -> Registered { - Registered { - name, - default, - reference: Reference::Deviates { value, why }, - latched: false, - } -} - -/// A row the reference has no counterpart for. -const fn ours(name: &'static str, default: &'static str, why: &'static str) -> Registered { - Registered { - name, - default, - reference: Reference::Ours(why), - latched: false, - } -} - -/// The table as the script VM's registrar wants it: `(name, default)` pairs in table order. -pub(crate) fn registered_pairs() -> impl Iterator { - REGISTERED.iter().map(|r| (r.name, r.default)) -} - -/// The host-backed CVars, one row per knob that has a reader: a host knob, or a Lua consumer -/// such as the stock Video Options window's verbs. -/// -/// Not registered for want of a reader, though pfUI's `hdgraphic` writes them: `lodDist` -/// (`0x688524`, "100.0", read at `0x6afb1d` for the doodad LOD swap), `footstepBias` (`0x6888b4`, -/// "0.125", read at `0x68fcb6`), `mapObjLightLOD` (`0x6886ec`, "0") and `SkyCloudLOD` (`0x6d1d33`, -/// "0"). `DistCull` (`0x688570`) has no reader in the reference either. `maxLOD` is no 1.12 CVar. -pub(crate) const REGISTERED: &[Registered] = &[ - // `realmName` (`0x83f2d0`): registered `""` (`0x882748`), help "Last realm connected to" - // (`0x85d684`); the client builds its SavedVariables path from it (`0x5ab7d0`). Written from - // the session's realm when addons load. `Ace/AceState.lua:27` trims it at - // PLAYER_ENTERING_WORLD, so a nil breaks every Ace addon. - same("realmName", ""), - // The logon server address (register site `0x5ab6a6`), a string row judged by - // `realmlist::on_cvar`. - deviates( - crate::realmlist::CVAR_REALMLIST, - crate::realmlist::DEFAULT_REALMLIST, - "us.logon.worldofwarcraft.com:3724", - "that host has not resolved since 2019, so shipping it makes every first launch a \ - DNS failure; benilla dials the machine it is running on", - ), - // `autoClearAFK` (`0x5e24d4`, "1" `0x82e748`, handle `[0xc4d68c]` set at `0x5e24ef`, read at - // `0x5eb84b`) gates five implicit AFK clears: any chat send but type `0x14`, Jump, - // forward/back, strafe and turn (`0x513d36`/`0x514e23`/`0x514f0b`/`0x514fca`). Off, the clear - // does nothing at all: no echo, no mirror write, no packet. - same("autoClearAFK", "1"), - same("MasterVolume", "1"), - same("SoundVolume", "1"), - same("MusicVolume", "0.4"), - same("AmbienceVolume", "0.6"), - // Registered "1" at `0x45737a`/`0x45739b`/`0x460a9d`. `MasterSoundEffects` is the Enable All - // Sound box (`SoundOptionsFrame.lua:6`), which pauses the whole sound engine, not an SFX - // toggle. - same("MasterSoundEffects", "1"), - same("EnableMusic", "1"), - same("EnableAmbience", "1"), - // The race/sex refusal voice lines (`0x457877`), `SoundOptionsFrame.lua:3`; the master enable - // greys it. - same("EnableErrorSpeech", "1"), - // Not a 1.12 CVar or checkbox; the later-era name. The reference mutes on losing focus, music - // included (`WM_ACTIVATE` to `0x7a4860`'s `FSOUND_SetMute(-3, active ? 0 : 1)`), so "0" is its - // behaviour. The knob is `SoundConfig::background_sound`. - same("Sound_EnableSoundWhenGameIsInBG", "0"), - // Registered "1" (`0x4573be`); `SoundConfig::reverb` carries the evidence for shipping "0". - deviates( - "SoundReverb", - "0", - "1", - "the reference's reverb is EAX-over-hardware, and that hardware has not existed since \ - Vista, so \"1\" would ship audio the real client has never actually produced on any \ - machine a player runs today", - ), - // FMOD 3's mix-ahead buffer in ms (`0x4571ca`, flags 2: read once at sound init); here it sizes - // the render thread's ring ahead of the IO callback (`sound::output`). `0x457520` registers - // "50" or "100" by a host probe (`0x835e10`/`0x835e0c`), which one on a current machine - // untraced; ours is the larger, since the depth has to hide a whole stalled IO cycle. - same("SoundBufferSize", "100").latched(), - // benilla's own, not a 1.12 CVar. Deviation: the reference clips at full scale and has no - // headroom mechanism (its SFX-bus duck `0x457960` is a sidechain armed only by server-pushed - // voice lines); benilla limits because every SFX is mastered to full scale and overlapping - // kits clip. - ours( - "SoundOutputLimiter", - "1", - "benilla's own — the reference sums at full scale and clips; every WoW SFX is mastered \ - to full scale, so overlapping kits need a limiter to keep from distorting", - ), - overridden( - "uiScale", - "0.9", - "1.0", - "a fresh reference client never consults this CVar: `useUiScale` registers \"0\" \ - (`0x48fce4`), and the OFF leg `0x492f70` computes clamp(768/height, 0.9, 1.0) instead — \ - 0.9 at 854 px tall and up, which is every window we ship against. It is 1.0 at 768 and \ - below, where our flat 0.9 does diverge; `ui_script::DEFAULT_UI_SCALE` carries that. \ - See `useUiScale` below, whose row this one used to say did not exist", - ), - // `useUiScale` (`0x8430c0`), the switch the `uiScale` override gates on: - // `ContainerFrame.lua:483` and `UIDropDownMenu.lua:525` branch on it and `OptionsFrame.lua:13` - // gives it a checkbox. - same("useUiScale", "0"), - same("farclip", "350"), - // `nearclip` (`0x68867a`: name `0x84ffb0`, default `0x84fb48` "0.1", flags 1, callback - // `0x688d90`, record `[0xc7f348]`). The camera re-reads the record every frame: `0x511bc0` - // (sole caller `0x483094`) stamps `[cam+0x38]` from it through the handle `[0xbe1078]` that - // `0x50b728` caches, overwriting the ctor's 1/9 (`0x3de38e39`); - // `benilla_world::view::stamp_near_clip` is that. - // The callback's derived global `[0xc7b480]` has no reader. pfUI's `hdgraphic` writes - // 0.06..0.30, inside `[0.01, 0.33]`. - same("nearclip", "0.1"), - // `deselectOnClick` and `mouseInvertPitch` are 1.12's own (`UIOptionsFrame.lua:8,4`); - // `autoLootDefault` is the later-era name, 1.12 having only the shift gesture. - same("deselectOnClick", "1"), - // `BlockTrades` (`0x842fbc`), `UIOptionsFrame.lua:11`: the refusal leg `0x4bf7bc` fires only - // when it is set. The knob is [`crate::ui_trade::BlockTrades`]. - same("BlockTrades", "0"), - // `autoSelfCast` (register site `0x6e731d`, record `[0xceac34]`, read at `0x6e53d7`; `0x870dc0` - // is its name string): a friendly cast that binds nothing falls back to the caster. - // `TOGGLEAUTOSELFCAST` toggles it. - same("autoSelfCast", "0"), - // The five saved camera views and the live index, at the reference's names and default strings; - // owned by [`crate::player::camera_view`]. Registered so a `SaveView` persists. - same( - crate::player::camera_view::CVAR_ACTIVE_VIEW, - crate::player::camera_view::ACTIVE_VIEW_DEFAULT, - ), - same( - crate::player::camera_view::VIEW_CVARS[0][0], - crate::player::camera_view::VIEW_DEFAULTS[0][0], - ), - same( - crate::player::camera_view::VIEW_CVARS[0][1], - crate::player::camera_view::VIEW_DEFAULTS[0][1], - ), - same( - crate::player::camera_view::VIEW_CVARS[0][2], - crate::player::camera_view::VIEW_DEFAULTS[0][2], - ), - same( - crate::player::camera_view::VIEW_CVARS[1][0], - crate::player::camera_view::VIEW_DEFAULTS[1][0], - ), - same( - crate::player::camera_view::VIEW_CVARS[1][1], - crate::player::camera_view::VIEW_DEFAULTS[1][1], - ), - same( - crate::player::camera_view::VIEW_CVARS[1][2], - crate::player::camera_view::VIEW_DEFAULTS[1][2], - ), - same( - crate::player::camera_view::VIEW_CVARS[2][0], - crate::player::camera_view::VIEW_DEFAULTS[2][0], - ), - same( - crate::player::camera_view::VIEW_CVARS[2][1], - crate::player::camera_view::VIEW_DEFAULTS[2][1], - ), - same( - crate::player::camera_view::VIEW_CVARS[2][2], - crate::player::camera_view::VIEW_DEFAULTS[2][2], - ), - same( - crate::player::camera_view::VIEW_CVARS[3][0], - crate::player::camera_view::VIEW_DEFAULTS[3][0], - ), - same( - crate::player::camera_view::VIEW_CVARS[3][1], - crate::player::camera_view::VIEW_DEFAULTS[3][1], - ), - same( - crate::player::camera_view::VIEW_CVARS[3][2], - crate::player::camera_view::VIEW_DEFAULTS[3][2], - ), - same( - crate::player::camera_view::VIEW_CVARS[4][0], - crate::player::camera_view::VIEW_DEFAULTS[4][0], - ), - same( - crate::player::camera_view::VIEW_CVARS[4][1], - crate::player::camera_view::VIEW_DEFAULTS[4][1], - ), - same( - crate::player::camera_view::VIEW_CVARS[4][2], - crate::player::camera_view::VIEW_DEFAULTS[4][2], - ), - same("mouseInvertPitch", "0"), - ours( - "autoLootDefault", - "0", - "1.12 has no auto-loot CVar at all — vanilla offers only the shift gesture, so OFF \ - IS the reference's own behaviour; the spelling is era's", - ), - // The overhead-name gates, registered at `0x6c7470` into mask `0xce8720`: `UnitNamePlayer` - // (`0x86c694`) "1" (`0x82e748`), `UnitNameNPC` (`0x86c6a4`) and `UnitNameOwn` (`0x86c6b0`) "0" - // (`0x82e570`). - same("UnitNamePlayer", "1"), - same("UnitNameNPC", "0"), - same("UnitNameOwn", "0"), - // `UnitNamePlayerGuild` (`0x86c680`, "1", mask bit `0x10`) is not a show gate: `ShouldShowName` - // (`0x6070a0`) reads bits `0x1/0x2/0x4`, and this gates the `"\n<%s>"` guild line at - // `0x609085`. `UnitNamePlayerPVPTitle` (bit `0x20`, "1") has no row: nothing here draws the - // rank prefix. - same("UnitNamePlayerGuild", "1"), - // `WorldDetail` is no 1.12 CVar but the `GetWorldDetail`/`SetWorldDetail` verb name - // (`OptionsFrame.lua:27`); stops 0/1/2 are `frillDensity` 16/32/48. `SetWorldDetail 0x488dd0` - // also writes `SmallCull` {0.07, 0.04, 0.01}, and `GetWorldDetail` reads only `SmallCull`, - // whose registered 0.04 is stop 1: the reference's slider boots at Medium. - same("WorldDetail", "1"), - // `SmallCull` (`0x68854a`: name `0x8696f8`, default `0x869718` "0.04", flags 1, callback - // `0x688b10`, record `[0xc7f330]`). The callback refuses outside [0.001, 2.0] and stores into - // `[0x868620]`, which nothing reads: no mechanism here either. Its readers are Lua ones: - // `SetWorldDetail` writes it per stop, and `OptionsFrame_SetDefaults` reads its default - // (`OptionsFrame.lua:431`). `hwDetect` sets 0.04 on the fallback row a modern GPU reaches. - same(benilla_ui::script::CVAR_SMALL_CULL, "0.04"), - // `frillDensity` (`0x68862e`: name `0x8423d8`, default `0x864644` "16", flags 1, callback - // `0x688de0`, record `[0xc7f2f4]`): detail-doodad cells visited per chunk, clamped to [1, 256] - // and handed through `0x6725a0` to `[0xc7b494]`, the bound of the scatter loop at - // `0x6bfcfb`/`0x6bff1c`. One knob with `WorldDetail`, each keeping its own clamp. `hwDetect` - // (`0x639a60`) sets it from `VideoHardware.dbc` field `+0x18`, 24 on videoID 170. pfUI's - // `hdgraphic` reads `GetCVar("frillDensity") > 48` and writes up to 256. - deviates( - "frillDensity", - "32", - "16", - "the reference's registered 16 is stop 0 and its post-`hwDetect` 24 is on no \ - stop at all, so every stop diverges; Medium (32) is the nearest one no sparser than a \ - fresh install, and erring sparse is the worse failure for ground cover", - ), - // ── Combat log display ranges, in yards ────────────────────────────────────────────────────── - // - // Registered by `0x626d00` from the `{name, default}` pairs at `0x8629e0`, read as the record's - // float (`+0x24`). Classes 0 and 1, you and your pet, have no CVar there, only the `100000.0` - // sentinel. - same("CombatLogRangeParty", "50"), - same("CombatLogRangePartyPet", "50"), - same("CombatLogRangeFriendlyPlayers", "50"), - same("CombatLogRangeFriendlyPlayersPets", "50"), - same("CombatLogRangeHostilePlayers", "50"), - same("CombatLogRangeHostilePlayersPets", "50"), - same("CombatLogRangeCreature", "30"), - // Outside that table (`0x626d5f`, "60" at `0x862e14`): `0x62c160` reads it first and falls back - // to the per-class range only when the lookup fails. - same(crate::ui_chat::combat::DEATH_LOG_RANGE_CVAR, "60"), - // ── Floating combat text ───────────────────────────────────────────────────────────────────── - // - // `CombatDamage` (`0x6032df`, record `[0xc4d944]`) is the master: its two readers, the word - // emitter `0x607140` and the number emitter `0x6128b0`, both return at "0", so nothing floats. - // The `Pet*` rows gate only the owned-by-you branch; the stock Pet Melee Damage box writes both - // (`UIOptionsFrame.lua:335-336`). - same("CombatDamage", "1"), - same("PetMeleeDamage", "1"), - same("PetSpellDamage", "1"), - // `CombatLogPeriodicSpells` (`0x6033b3`): the handle is discarded and every use looks it up by - // name; read as the record's int. - same(crate::ui_chat::combat::LOG_PERIODIC_CVAR, "1"), - // ── Sound-panel check buttons ──────────────────────────────────────────────────────────────── - // - // Category-7 registrations that keep no handle: the reference looks each up by name at use. - // - // `SoundListenerAtCharacter` (`0x457890`): the listener at the character, else at the camera - // (`update_audio_listener`). - same("SoundListenerAtCharacter", "1"), - // `EmoteSounds` (`0x4573b9`): the received text-emote voice kit, and only that. - same("EmoteSounds", "1"), - // `SoundZoneMusicNoDelay` (`0x4578b3`): `next_track_time`'s immediate path. - same("SoundZoneMusicNoDelay", "0"), - // `assistAttack` (`0x48fc50`, record `[0xb4d8f8]`): `/assist` also starts the swing. The "3" - // beside it is the next registration's, `minimapZoom`: stock `/assist` selects without - // swinging. - same("assistAttack", "0"), - // ── Mouse-look speed, per axis ─────────────────────────────────────────────────────────────── - // - // `cameraYawMoveSpeed` is the MOUSE_LOOK_SPEED slider (`UIOptionsFrame.lua:89`); a nil there - // raises in `slider:SetValue` (`0x790980`) and stops `UIOptionsFrame_Load`. The stock Save - // writes `cameraPitchMoveSpeed` as half of it (`:355-356`). The reference integrates - // OS-accelerated pixels where we take raw device deltas, so the unit factor lives in - // `camera::LOOK_YAW_PER_SPEED` and these defaults stay the reference's. The validator - // `0x50c000` → `0x50b330` rejects values outside [0.1, 360] rather than clamping, and - // `player::camera::on_cvar` does the same. - same("cameraYawMoveSpeed", "180"), - same("cameraPitchMoveSpeed", "90"), - // MOUSE_SENSITIVITY (`UIOptionsFrame.lua:87`): FrameXML's spelling of the binary's `mouseSpeed` - // (`0x402c7b`); lookups are case-insensitive (`CVar::Lookup 0x63de30`), here as there. The - // reference's default is `SPI_GETMOUSESPEED × 0.1`, "1.0" on stock Windows, and its record - // `[0x882704]` has no reader: the slider sets the OS pointer speed (`0x402ec0`, [0.1, 2.0]). - // The default matches; Deviation: here the dial multiplies the camera's own rate, because - // benilla does not change the OS pointer speed, a system-wide setting. - same("mousespeed", "1"), - // The zoom-out limit is `cameraDistanceMax × cameraDistanceMaxFactor`, held to [0, 50] - // (`0x5112d6`). `cameraDistanceMax` (`0x50beb2`, default `0x84fbd0`) has no panel row; its - // validator `0x50b310` refuses a value outside [0, 50], and `player::camera::on_cvar` does the - // same. The factor is MAX_FOLLOW_DIST (`UIOptionsFrame.lua:90`), registered "1.0" (`0x82e92c`) - // with no validator: the slider's 1 to 2 is the slider's, not the CVar's. - same("cameraDistanceMax", "15.0"), - same("cameraDistanceMaxFactor", "1"), - // `cameraSmoothStyle` (`0x50ba92`, default `[0x84f4f4]` "1"), the auto-return behind the - // character. The engine's enum is 0 Never, 1 Smart, 2 Always, as the stock dropdown writes it - // (`UIOptionsFrame.lua:525,536,547`); 3, the Never entry's position, is the validator's upper - // bound (`0x50b330(v, 0, 3)`). See `FollowStyle`. - same("cameraSmoothStyle", "1"), - // Read instead of `cameraSmoothStyle` while the state mask holds Track or Fear; no panel row. - same("cameraSmoothTrackingStyle", "1"), - // AUTO_FOLLOW_SPEED (`UIOptionsFrame.lua:88`), deg/s, registered "180.0" (`[0xbe1070]`): it - // sets the transition's duration (`|dyaw| / rate * factor`), an average rate, not a slew. The - // knob clamps it to `FOLLOW_SPEED_RANGE`. The stock Save also writes `cameraPitchSmoothSpeed` - // at a quarter (`:353`), unregistered here: `FollowRig` has one rate. - same("cameraYawSmoothSpeed", "180"), - // `cameraPivot` `[0xbe10a4]` "1" (`0x50bda3`), smart pivot: gate `0x510690`, routing - // `0x50fee0`, release `0x5107f0`; ours is `player::camera_dynamics::SmartPivot`. - same("cameraPivot", "1"), - // Read by the routing (`0x50fff5`/`0x510004`) in radians of camera rotation, so they carry to - // benilla's raw-device units unchanged. - same("cameraPivotDXMax", "0.05"), - same("cameraPivotDYMin", "0"), - // The pitch bias's ease-back once the pivot lets go, deg/s (`[0xbe0fc8]`; `0x512a50` divides - // |Δ| by rate · π/180 for the duration); no panel row. - same("cameraTargetSmoothSpeed", "90"), - // `cameraWaterCollision` `[0xbe1088]` "1" (`0x50bd63`, `0x82e748`): one register, two consumers - // that must ship together. `0x50e5ec` builds it; its `0xf0000` nibble joins the trace mask of - // all three `0x50e570` queries (reaching `0x69cc13`), and `0x50e629` tests it to lift the sweep - // origin to `surface + 2/9`. Ours: `benilla_world::collision::camera_filter` and - // `player::camera_water`. - same("cameraWaterCollision", "1"), - // `cameraTerrainTilt` `[0xbe0fd4]` "0" (`0x50bcfd`), Follow Terrain: probe and staircase - // `0x50d900`, arm `0x50dbc0`; ours is `player::camera_dynamics::TerrainTilt`. - same("cameraTerrainTilt", "0"), - // Rate in deg/s (`[0xbe0fc0]`), duration bounds in seconds (`[0xbe1050]`/`[0xbe1054]`). The 3 s - // floor always binds (20° at 7.5°/s is 2.67 s), so the camera leans rather than tracks. - same("cameraGroundSmoothSpeed", "7.5"), - same("cameraTerrainTiltTimeMin", "3"), - same("cameraTerrainTiltTimeMax", "10"), - // `cameraBobbing` `[0xbe10c0]` "0" (`0x50b76d`), head bob: kernel `0x511920`, gate `0x5105e0`; - // ours is `player::camera_dynamics::HeadBob`. - same("cameraBobbing", "0"), - // Amplitudes in the CVar's units, scaled by 1/36 (`[0x7ff9d0]`) to yards. - // `cameraBobbingSmoothSpeed` is the decay rate, read only in the disarm `0x51113a`, which - // divides the largest component by it for the ramp's duration (~0.069 s at these defaults). - same("cameraBobbingLRAmplitude", "2"), - same("cameraBobbingUDAmplitude", "2"), - same("cameraBobbingFrequency", "0.8"), - same("cameraBobbingSmoothSpeed", "0.8"), - // `statusBarText` (`0x48fc34`, record `[0xb4d904]`, no engine reader), read by - // `TextStatusBar.lua:47,97`: "0" shows the numbers on hover only. - same("statusBarText", "0"), - // Enhanced Tooltips (`UIOptionsFrame.lua:15`), registered "1" at `0x48fddd` (`0x82e748`); read - // only by the stock interface. - same("UberTooltips", "1"), - // Registered at `0x603280`: `ChatBubbles` "1", `ChatBubblesParty` "0". - same("ChatBubbles", "1"), - same("ChatBubblesParty", "0"), - // Registered "1" (`0x82e748`), category 4. `profanityFilter` (`0x402e68`, name `0x82e7f4`, - // callback `0x403570`) masks `ChatProfanity.dbc` spans inside the shared masker `0x4a1a66`, - // covering all thirteen call sites; `spamFilter` (`0x402e8e`, name `0x82e7d4`, callback - // `0x4035b0`) silently drops a line matching `SpamMessages.dbc`. The knob is - // [`crate::text_filter::TextFilterSwitches`]. - same("profanityFilter", "1"), - same("spamFilter", "1"), - // Registered by `CGlueMgr::EnterWorld` (`0x46b633` `gameTip` "0", `0x46b658` `showGameTips` - // "1"), category 5. `gameTip` is the cursor and holds the next tip, not the one on screen; - // `crate::game_tip` advances it. - same("gameTip", "0"), - same("showGameTips", "1"), - // `showLootSpam` (`0x48fd1c`, name `0x8430a0`, "1" `0x82e748`, record `0xb4e2bc`): off, group - // loot-roll lines are hidden and only the winner shows. Its three readers are the roll-line - // composers; the knob is [`crate::ui_loot::LootConfig::show_loot_spam`]. - same("showLootSpam", "1"), - // `guildMemberNotify` (`0x5e24c7`, "0" `0x82e570`, record `0xc4d3c4`): guildmate log on/off - // lines, read only in `SMSG_GUILD_EVENT`'s handler. The knob is - // [`crate::ui_guild::GuildMemberNotify`]. - same("guildMemberNotify", "0"), - // Both registered "3" (`0x48fc6c`, `0x48fc88`); the minimap's +/- buttons write them through - // `Minimap:SetZoom`. The knob is [`crate::minimap::MinimapZoom`]. - same("minimapZoom", "3"), - same("minimapInsideZoom", "3"), - // Load out of date AddOns, inverted (`0x402c3b`): "1" enforces the check. Read by the load walk - // ([`Cvars::addon_version_check`]) and live by the VM's gate. - same("checkAddonVersion", "1"), - // `gxApi` (`0x63a833`: name `0x842a64`, default `0x864f7c` "direct3d", flags 3, callback - // `0x63b030`, record `[0xc4ea94]`): the reference builds D3D9 unless it reads "OpenGl" - // (`0x63a3c4`, `0x842a5c`). Here it reports the wgpu backend (`wgpu::Backend::to_str`), pushed - // from `RenderAdapterInfo` and owned by the session, so it is never persisted. pfUI's - // `panel.lua:185` concatenates it. - deviates( - "gxApi", - "", - "direct3d", - "descriptive, not a selector — benilla renders through wgpu, which has no D3D9 \ - backend and no chooser; the value is the live adapter's own and is never persisted", - ) - .latched(), - // `gxVSync` (`0x63a859`, "1", flags 3), `OptionsFrame.lua:9`. The knob is - // [`crate::video::VideoConfig::vsync`], which the window's present mode follows at the - // `RestartGx` commit. `$WOW_NOVSYNC=1` overrides it for the session. - same("gxVSync", "1").latched(), - // `gxWindow` (`0x63a889`): "0" on enUS; zhCN registers "1", as it does `gxMaximize` - // (`0x63a8e0`), and koKR `AutoInteract` (`0x603390`). The knob is - // [`crate::video::VideoConfig::display`]. Deviation: "0" raises a borderless fullscreen window - // instead of mode-setting the display, because Wayland and macOS offer no mode-set and X11's - // leaves the desktop changed after a crash ([`crate::video`]). - same("gxWindow", "0").latched(), - // `gxResolution`, a string row parsed by `video::on_cvar`. Here it is only the windowed size: - // fullscreen is the monitor's own and no mode list is offered. - deviates( - "gxResolution", - "1600x900", - "640x480", - "narrowed to the WINDOWED size only — fullscreen is the monitor's own and we expose \ - no mode list, and 640x480 is not a window anyone would ship a client at", - ) - .latched(), - // benilla's own: a body pane's doll renders at half the frame rate while the pane is open; the - // reference draws it in the main pass. The knob is [`crate::portrait::PaneRate`]. - ours( - "boothHalfRate", - "1", - "benilla's own — the reference draws its doll inside the main pass and has no \ - second view to rate-limit", - ), - // `gxMultisample` (`0x63a950`, flags 3). The reference formats its default from field 21 of the - // `VideoHardware.dbc` row `DetectHardware` (`0x641260`) matches, which is 1, no multisampling, - // on every fallback row a modern GPU reaches. The knob is [`benilla_world::view::MsaaSetting`], - // read once at the camera's spawn; `$WOW_MSAA` overrides it for the session. - same("gxMultisample", "1").latched(), - // `GetCurrentMultisampleFormat 0x48c580` looks up all three of the Video dropdown's values by - // name, so these must exist. They describe the swapchain's own pair and steer nothing; - // `SetMultisampleFormat` writes them as `0x48c640` does. - deviates( - "gxColorBits", - "32", - "16", - "these describe, they do not steer — the pair is our swapchain's own, and every \ - format `MsaaFormats` publishes carries it", - ) - .latched(), - deviates( - "gxDepthBits", - "32", - "16", - "as `gxColorBits` — the depth half of the same descriptive pair", - ) - .latched(), - // `trilinear` and `anisotropic`, over `benilla_assets::TexFilterSetting` (`tex_filter.rs` - // carries the derivation). A change applies at the next launch, since a sampler is baked into - // each texture at load; the reference's UI says "enabled upon restart". - // `$WOW_TRILINEAR`/`$WOW_ANISO` override for the session. - // - // `trilinear` registers "0", but `hwDetect` runs `DetectHardware 0x641260` and sets it from the - // matched `VideoHardware.dbc` row before the first frame; the fallback rows an unlisted GPU - // reaches are 168/169/170, and 169 and 170 give 1. The reference install's `gx.log` resolves to - // videoID 170. - overridden( - "trilinear", - "1", - "0", - "`hwDetect` sets it from `VideoHardware.dbc` field 9 before the first frame, and \ - that field is 1 on both fallback rows an unlisted modern GPU can reach — measured on the \ - reference's own `Logs/gx.log` (`videoID: 170`)", - ), - // Not one of `hwDetect`'s sixteen (`[0x639a60, 0x639b80)` never reads `0xc7f2e4`), so the - // registered "1", off, stands. - same("anisotropic", "1"), - // The four video CVars the stock window's verbs read and write (`benilla_ui` `video_pairs`); - // benilla has no mechanism behind any of them. `shadowLevel` (`0x6885bc`, "1", flags 1, - // callback `0x688c10`, record `[0xc7f350]`) is behind `Get/SetTerrainMip` as `1 − level`: the - // callback refuses above 1 and prints "Shadow mip level changed upon restart.", and the - // terrain's shadow-map mip (`0x66f877`) reads it at load. `hwDetect` leaves it at 1. - same("shadowLevel", "1"), - // `doodadAnim` (`0x6884d8`, "1", flags 1, callback `0x688a10`, record `[0xc7f354]`), behind - // `Get/SetDoodadAnim`: the callback toggles bit `0x8000` of `[0xc7b2a4]` and calls `0x6953d0`, - // a bare `ret`, so no animation reads it. `hwDetect` sets 0 or 1 by CPU tier. - same("doodadAnim", "1"), - // `texLodBias` (`0x6885e2`, "0.0" at `0x84fad4`, flags 1, callback `0x688c90`, record - // `[0xc7f2f8]`), behind `Get/SetTexLodBias`: the callback refuses outside [-1.0, 1.0] and its - // sink `0x672640` is `ret 4`. - same("texLodBias", "0.0"), - // `baseMip` (`0x6887aa`, "0", flags 1, callback `0x689090`, record `[0xc7f2f0]`), behind - // `Get/SetBaseMip` as `1 − level`: the callback refuses outside [0, 1] ("BaseMip must be 0 or - // 1") and sets the device's first uploaded mip level (`0x589bf0`, read at `0x59f6ce`). benilla - // uploads every level; the write is kept and has no effect. `hwDetect` leaves it at 0. - same("baseMip", "0"), - // `spellEffectLevel` (`0x688900`: name `0x869324`, default `0x843068` "2", flags 1, callback - // `0x689510`, record `[0xc7f2a4]`), the Spell Detail slider (`OptionsFrame.lua:32`). The - // callback's two effects here: the particle emission scalar ([`crate::video::on_cvar`]) and - // the dynamic-object shard rate, read at each emitter's spawn (`crate::entities`). Its third, - // skipping one M2-scene ground decal below level 2 (`0x672b3a`), has no counterpart: benilla - // does not draw that projection. - same("spellEffectLevel", "2"), - // Weather Intensity (`OptionsFrame.lua:33`), registered "2" (`0x67b806`, flags 0, callback - // `0x67b870`, name `0x8685ac`). The reader is `benilla_world::weather::WeatherState`, which - // scales the precipitation spawn rate by `0x67b870`'s table {0.1, 0.33, 0.66, 1.0}; rendering - // only. - deviates( - "weatherDensity", - "3", - "2", - "every precipitation rate in `benilla-world`'s own precipitation module was \ - derived and graded against the reference install's own apitrace captures, and that \ - install runs \ - `SET weatherDensity \"3\"` (K = 1.0) — so 3 is the value a benilla-vs-reference \ - side-by-side is correct at, and the registered 2 would thin every rate to 0.66 against \ - the only client we compare with. The slider is how a player takes it back down", - ), - // `gamma` (`0x402d70`: name `0x82e924` "Gamma", default `0x82e92c` "1.0", flags 0, callback - // `0x4034d0`). The reference uploads `pow(i/255, gamma)` (`0x591680`) through - // `SetDeviceGammaRamp`, except when windowed (`byte[dev+0x20b]`). Deviation: benilla, which - // has no exclusive mode, applies the same curve in the composite pass - // ([`crate::ui_gamma::DisplayGamma`]), because the skipped upload would make a slider that - // moves no pixel. Spelled "1.000000" because `SetGamma 0x4891f0` formats with `"%f"`, and - // Restore Defaults must compare equal to the default; [`sync_cvars`] seeds it the same way. - same("gamma", "1.000000"), - // `DesktopGamma` (`0x402d4d`: name `0x82e930`, default "0" `0x82e570`, flags 0, callback - // `0x403500`, record `[0x8826d4]`), the stock window's Use Desktop Gamma box, which it also - // sets whenever Windowed Mode is ticked (`OptionsFrame.lua:399-408`). On 1 the callback puts - // the desktop's own ramp back, and `gamma`'s callback (`0x4034d0`) uploads only while this - // reads 0. Here nothing reads it: [`crate::ui_gamma::DisplayGamma`] follows `gamma` alone. - same("DesktopGamma", "0"), - // benilla's own: the world renders at `window × this` while the UI stays native. The knob is - // [`crate::world_backdrop::RenderScale`], clamped to `RENDER_SCALE_RANGE`; at "1" nothing is - // resampled. `$WOW_RENDER_SCALE` overrides it for the session. - ours( - "renderScale", - "1", - "benilla's own — the reference has no off-screen buffer to hang a resolution dial \ - on; its nearest equivalent, `gxResolution`, drops the interface with the world", - ), - // benilla's own: `/console fpsJournal 1` appends a per-second row of position, frame cost and - // per-pass GPU time to `benilla-config/Diagnostics/fps-journal.csv`. The knob is - // [`crate::perf::FpsJournalSetting`]. - ours( - "fpsJournal", - "0", - "benilla's own — 1.12 has no player-side perf log; its nearest thing is the \ - Ctrl+R framerate label, a number with no file behind it", - ), - // `lastCharacterIndex` (`0x402d93`, "0" `0x82e570`, category 4, handle `[0x882674]`), help - // "Last character selected": a 0-based row (the selection cell `[0x83856c]` under `"%d"`), so - // "0" is the first character. It mirrors [`crate::char_select::Roster::pending_index`]. - same(crate::char_select::CVAR_LAST_CHARACTER, "0"), -]; +mod table; +#[cfg(test)] +use table::Reference; +pub(crate) use table::{registered_pairs, REGISTERED}; /// `config.toml`: a `[cvars]` table of `Name = "value"` strings, sorted so every save is stable. #[derive(serde::Serialize, serde::Deserialize, Default)] @@ -1197,6 +573,7 @@ fn session_values(world: &World) -> Vec<(&'static str, Option)> { "gxWindow", v.map(|c| flag(c.display == crate::video::DisplayMode::Windowed)), )); + out.push(("gxMaximize", v.map(|c| flag(c.maximize)))); out.push(( "gxResolution", v.map(|c| format!("{}x{}", c.windowed.x, c.windowed.y)), @@ -1728,6 +1105,53 @@ mod tests { assert_eq!(d["renderScale"], 1.0); } + /// The FFX pass's three switches reach it one by one (`ffx`, `ffxGlow`, `ffxDeath`). + #[test] + fn the_ffx_switches_reach_the_pass() { + use benilla_world::ffx_glow::FfxSwitches; + let mut app = cvar_app(); + let on = FfxSwitches::default(); + assert_eq!(*res::(&app), on); + apply(&mut app, "ffxGlow", "0"); + assert_eq!(*res::(&app), FfxSwitches { glow: false, ..on }); + apply(&mut app, "ffxglow", "1"); + apply(&mut app, "ffxDeath", "0"); + assert_eq!( + *res::(&app), + FfxSwitches { death: false, ..on } + ); + apply(&mut app, "ffxDeath", "1"); + apply(&mut app, "ffx", "0"); + assert_eq!( + *res::(&app), + FfxSwitches { + master: false, + ..on + } + ); + } + + /// `gxMaximize` registers latched (flags 3): a write is staged and moves nothing until the + /// `RestartGx` commit, where the window knob takes it. + #[test] + fn gx_maximize_waits_for_the_restart_commit() { + let mut app = cvar_app(); + let outcome = app + .world_mut() + .resource_mut::() + .set("gxMaximize", "1"); + assert_eq!(outcome, SetOutcome::Staged); + let events = app.world_mut().resource_mut::().take_events(); + assert!(events.is_empty(), "a staged write fires no callback"); + assert!(!res::(&app).maximize); + assert_eq!(app.world_mut().resource_mut::().commit_latched(), 1); + let events = app.world_mut().resource_mut::().take_events(); + for event in events { + app.world_mut().trigger(event); + } + assert!(res::(&app).maximize, "the commit applies it"); + } + #[test] fn the_observers_apply_every_arm() { let mut app = cvar_app(); @@ -2200,6 +1624,7 @@ mod tests { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() // Literal, not Default: RenderScale::default() reads $WOW_RENDER_SCALE. .insert_resource(RenderScale(1.0)) // Literal: `Realmlist::default()` reads `$WOW_HOST`. @@ -2734,6 +2159,29 @@ mod tests { "DesktopGamma", "OptionsFrame.lua's Use Desktop Gamma box, recorded at open and put back on close", ), + ( + "pixelShaders", + "OptionsFrame.lua's Enable All Shaders box, which its Okay copies into `ffx`", + ), + ("specular", "OptionsFrame.lua's Terrain Highlights box"), + ( + "M2UseShaders", + "OptionsFrame.lua's Vertex Animation Shaders box", + ), + ("M2UsePixelShaders", "OptionsFrame.lua's Phong Shading box"), + ("lod", "OptionsFrame.lua's World LOD box"), + ( + "movieSubtitle", + "OptionsFrame.lua's Cinematic Subtitles box", + ), + ( + "useWeatherShaders", + "OptionsFrame.lua's Weather Shaders box", + ), + ("gxTripleBuffer", "OptionsFrame.lua's Triple Buffering box"), + ("gxCursor", "OptionsFrame.lua's Hardware Cursor box"), + ("gxFixLag", "OptionsFrame.lua's Fix Input Lag box"), + ("gxRefresh", "OptionsFrame.lua's refresh-rate dropdown"), ]; let app_src = crate::test_support::src_dir(); let ui_src = app_src @@ -2748,7 +2196,7 @@ mod tests { .chain(crate::test_support::rust_files(&ui_src)) { let rel = file.to_string_lossy().replace('\\', "/"); - if rel.ends_with("/cvars.rs") && rel.contains("benilla-app") || rel.contains("tests") { + if rel.contains("benilla-app/src/cvars/") || rel.contains("tests") { continue; } let text = std::fs::read_to_string(&file).expect("source is readable"); @@ -2800,9 +2248,14 @@ mod tests { "SoundBufferSize", "gxApi", "gxColorBits", + "gxCursor", "gxDepthBits", + "gxFixLag", + "gxMaximize", "gxMultisample", + "gxRefresh", "gxResolution", + "gxTripleBuffer", "gxVSync", "gxWindow", ] diff --git a/crates/benilla-app/src/cvars/table.rs b/crates/benilla-app/src/cvars/table.rs new file mode 100644 index 00000000..59a22702 --- /dev/null +++ b/crates/benilla-app/src/cvars/table.rs @@ -0,0 +1,718 @@ +//! The registry's table: every host-backed CVar, its default, and that default's standing against +//! the reference's registered one ([`Reference`]). The runtime that holds the live values is the +//! parent module. + +/// One host-backed CVar: its name, benilla's default, and where that default stands against the +/// reference's ([`Reference`]). +pub(crate) struct Registered { + /// The registered name, in the reference's own spelling. + pub(crate) name: &'static str, + /// What a fresh `benilla-config` runs at, and what `GetCVar` answers until the player moves it. + pub(crate) default: &'static str, + /// Where `default` stands against the reference's. Read only by the tests: it records the + /// reference, never a value this client acts on. + #[allow(dead_code)] + pub(crate) reference: Reference, + /// Registered with flag bit1 (`rec+0x1c & 0x2`, `flags` 2 or 3 at the register site): a write + /// is staged in [`super::Row::pending`] until [`super::Cvars::commit_latched`]. + pub(crate) latched: bool, +} + +impl Registered { + /// Mark the row latched. + pub(crate) const fn latched(self) -> Self { + Self { + latched: true, + ..self + } + } +} + +/// benilla's default against the reference's: the string the reference's `CVar::Register` +/// (`0x63db90`) passes for the name, or, for a setting 1.12 keeps in FrameXML, the value +/// `UIOptionsFrame.lua` boots it at. Not a `Config.wtf` line (`SaveConfig 0x63d980` writes only +/// values off their default), and not always what a fresh install runs at: `hwDetect` rewrites +/// sixteen video CVars from `VideoHardware.dbc` before the first frame ([`Reference::Overridden`]). +/// Each row's register site or FrameXML line is cited above it. +#[allow(dead_code)] // read only by the tests +pub(crate) enum Reference { + /// The reference registers this default and benilla ships it; the test compares the two. + Same(&'static str), + /// The reference registers `registered`, but its own boot code overwrites it before the first + /// frame; `default` is where that lands, and `why` is the override. + Overridden { + registered: &'static str, + why: &'static str, + }, + /// The reference ships `value` and benilla ships another; `why` is the reason. + Deviates { + value: &'static str, + why: &'static str, + }, + /// No reference setting to match: benilla's own knob, or a later-era name for something 1.12 + /// never made settable. `why` says which, and what the reference does instead. + Ours(&'static str), +} + +/// A row whose default is the reference's own registered string. +const fn same(name: &'static str, default: &'static str) -> Registered { + Registered { + name, + default, + reference: Reference::Same(default), + latched: false, + } +} + +/// A row whose default follows the reference's boot-time override of its registered string. +const fn overridden( + name: &'static str, + default: &'static str, + registered: &'static str, + why: &'static str, +) -> Registered { + Registered { + name, + default, + reference: Reference::Overridden { registered, why }, + latched: false, + } +} + +/// A row that ships something other than the reference's `value`, for `why`. +const fn deviates( + name: &'static str, + default: &'static str, + value: &'static str, + why: &'static str, +) -> Registered { + Registered { + name, + default, + reference: Reference::Deviates { value, why }, + latched: false, + } +} + +/// A row the reference has no counterpart for. +const fn ours(name: &'static str, default: &'static str, why: &'static str) -> Registered { + Registered { + name, + default, + reference: Reference::Ours(why), + latched: false, + } +} + +/// The table as the script VM's registrar wants it: `(name, default)` pairs in table order. +pub(crate) fn registered_pairs() -> impl Iterator { + REGISTERED.iter().map(|r| (r.name, r.default)) +} + +/// The host-backed CVars, one row per knob that has a reader: a host knob, or a Lua consumer +/// such as the stock Video Options window's verbs. +/// +/// Not registered for want of a reader, though pfUI's `hdgraphic` writes them: `lodDist` +/// (`0x688524`, "100.0", read at `0x6afb1d` for the doodad LOD swap), `footstepBias` (`0x6888b4`, +/// "0.125", read at `0x68fcb6`), `mapObjLightLOD` (`0x6886ec`, "0") and `SkyCloudLOD` (`0x6d1d33`, +/// "0"). `DistCull` (`0x688570`) has no reader in the reference either. `maxLOD` is no 1.12 CVar. +pub(crate) const REGISTERED: &[Registered] = &[ + // `realmName` (`0x83f2d0`): registered `""` (`0x882748`), help "Last realm connected to" + // (`0x85d684`); the client builds its SavedVariables path from it (`0x5ab7d0`). Written from + // the session's realm when addons load. `Ace/AceState.lua:27` trims it at + // PLAYER_ENTERING_WORLD, so a nil breaks every Ace addon. + same("realmName", ""), + // The logon server address (register site `0x5ab6a6`), a string row judged by + // `realmlist::on_cvar`. + deviates( + crate::realmlist::CVAR_REALMLIST, + crate::realmlist::DEFAULT_REALMLIST, + "us.logon.worldofwarcraft.com:3724", + "that host has not resolved since 2019, so shipping it makes every first launch a \ + DNS failure; benilla dials the machine it is running on", + ), + // `autoClearAFK` (`0x5e24d4`, "1" `0x82e748`, handle `[0xc4d68c]` set at `0x5e24ef`, read at + // `0x5eb84b`) gates five implicit AFK clears: any chat send but type `0x14`, Jump, + // forward/back, strafe and turn (`0x513d36`/`0x514e23`/`0x514f0b`/`0x514fca`). Off, the clear + // does nothing at all: no echo, no mirror write, no packet. + same("autoClearAFK", "1"), + same("MasterVolume", "1"), + same("SoundVolume", "1"), + same("MusicVolume", "0.4"), + same("AmbienceVolume", "0.6"), + // Registered "1" at `0x45737a`/`0x45739b`/`0x460a9d`. `MasterSoundEffects` is the Enable All + // Sound box (`SoundOptionsFrame.lua:6`), which pauses the whole sound engine, not an SFX + // toggle. + same("MasterSoundEffects", "1"), + same("EnableMusic", "1"), + same("EnableAmbience", "1"), + // The race/sex refusal voice lines (`0x457877`), `SoundOptionsFrame.lua:3`; the master enable + // greys it. + same("EnableErrorSpeech", "1"), + // Not a 1.12 CVar or checkbox; the later-era name. The reference mutes on losing focus, music + // included (`WM_ACTIVATE` to `0x7a4860`'s `FSOUND_SetMute(-3, active ? 0 : 1)`), so "0" is its + // behaviour. The knob is `SoundConfig::background_sound`. + same("Sound_EnableSoundWhenGameIsInBG", "0"), + // Registered "1" (`0x4573be`); `SoundConfig::reverb` carries the evidence for shipping "0". + deviates( + "SoundReverb", + "0", + "1", + "the reference's reverb is EAX-over-hardware, and that hardware has not existed since \ + Vista, so \"1\" would ship audio the real client has never actually produced on any \ + machine a player runs today", + ), + // FMOD 3's mix-ahead buffer in ms (`0x4571ca`, flags 2: read once at sound init); here it sizes + // the render thread's ring ahead of the IO callback (`sound::output`). `0x457520` registers + // "50" or "100" by a host probe (`0x835e10`/`0x835e0c`), which one on a current machine + // untraced; ours is the larger, since the depth has to hide a whole stalled IO cycle. + same("SoundBufferSize", "100").latched(), + // benilla's own, not a 1.12 CVar. Deviation: the reference clips at full scale and has no + // headroom mechanism (its SFX-bus duck `0x457960` is a sidechain armed only by server-pushed + // voice lines); benilla limits because every SFX is mastered to full scale and overlapping + // kits clip. + ours( + "SoundOutputLimiter", + "1", + "benilla's own — the reference sums at full scale and clips; every WoW SFX is mastered \ + to full scale, so overlapping kits need a limiter to keep from distorting", + ), + overridden( + "uiScale", + "0.9", + "1.0", + "a fresh reference client never consults this CVar: `useUiScale` registers \"0\" \ + (`0x48fce4`), and the OFF leg `0x492f70` computes clamp(768/height, 0.9, 1.0) instead — \ + 0.9 at 854 px tall and up, which is every window we ship against. It is 1.0 at 768 and \ + below, where our flat 0.9 does diverge; `ui_script::DEFAULT_UI_SCALE` carries that. \ + See `useUiScale` below, whose row this one used to say did not exist", + ), + // `useUiScale` (`0x8430c0`), the switch the `uiScale` override gates on: + // `ContainerFrame.lua:483` and `UIDropDownMenu.lua:525` branch on it and `OptionsFrame.lua:13` + // gives it a checkbox. + same("useUiScale", "0"), + same("farclip", "350"), + // `nearclip` (`0x68867a`: name `0x84ffb0`, default `0x84fb48` "0.1", flags 1, callback + // `0x688d90`, record `[0xc7f348]`). The camera re-reads the record every frame: `0x511bc0` + // (sole caller `0x483094`) stamps `[cam+0x38]` from it through the handle `[0xbe1078]` that + // `0x50b728` caches, overwriting the ctor's 1/9 (`0x3de38e39`); + // `benilla_world::view::stamp_near_clip` is that. + // The callback's derived global `[0xc7b480]` has no reader. pfUI's `hdgraphic` writes + // 0.06..0.30, inside `[0.01, 0.33]`. + same("nearclip", "0.1"), + // `deselectOnClick` and `mouseInvertPitch` are 1.12's own (`UIOptionsFrame.lua:8,4`); + // `autoLootDefault` is the later-era name, 1.12 having only the shift gesture. + same("deselectOnClick", "1"), + // `BlockTrades` (`0x842fbc`), `UIOptionsFrame.lua:11`: the refusal leg `0x4bf7bc` fires only + // when it is set. The knob is [`crate::ui_trade::BlockTrades`]. + same("BlockTrades", "0"), + // `autoSelfCast` (register site `0x6e731d`, record `[0xceac34]`, read at `0x6e53d7`; `0x870dc0` + // is its name string): a friendly cast that binds nothing falls back to the caster. + // `TOGGLEAUTOSELFCAST` toggles it. + same("autoSelfCast", "0"), + // The five saved camera views and the live index, at the reference's names and default strings; + // owned by [`crate::player::camera_view`]. Registered so a `SaveView` persists. + same( + crate::player::camera_view::CVAR_ACTIVE_VIEW, + crate::player::camera_view::ACTIVE_VIEW_DEFAULT, + ), + same( + crate::player::camera_view::VIEW_CVARS[0][0], + crate::player::camera_view::VIEW_DEFAULTS[0][0], + ), + same( + crate::player::camera_view::VIEW_CVARS[0][1], + crate::player::camera_view::VIEW_DEFAULTS[0][1], + ), + same( + crate::player::camera_view::VIEW_CVARS[0][2], + crate::player::camera_view::VIEW_DEFAULTS[0][2], + ), + same( + crate::player::camera_view::VIEW_CVARS[1][0], + crate::player::camera_view::VIEW_DEFAULTS[1][0], + ), + same( + crate::player::camera_view::VIEW_CVARS[1][1], + crate::player::camera_view::VIEW_DEFAULTS[1][1], + ), + same( + crate::player::camera_view::VIEW_CVARS[1][2], + crate::player::camera_view::VIEW_DEFAULTS[1][2], + ), + same( + crate::player::camera_view::VIEW_CVARS[2][0], + crate::player::camera_view::VIEW_DEFAULTS[2][0], + ), + same( + crate::player::camera_view::VIEW_CVARS[2][1], + crate::player::camera_view::VIEW_DEFAULTS[2][1], + ), + same( + crate::player::camera_view::VIEW_CVARS[2][2], + crate::player::camera_view::VIEW_DEFAULTS[2][2], + ), + same( + crate::player::camera_view::VIEW_CVARS[3][0], + crate::player::camera_view::VIEW_DEFAULTS[3][0], + ), + same( + crate::player::camera_view::VIEW_CVARS[3][1], + crate::player::camera_view::VIEW_DEFAULTS[3][1], + ), + same( + crate::player::camera_view::VIEW_CVARS[3][2], + crate::player::camera_view::VIEW_DEFAULTS[3][2], + ), + same( + crate::player::camera_view::VIEW_CVARS[4][0], + crate::player::camera_view::VIEW_DEFAULTS[4][0], + ), + same( + crate::player::camera_view::VIEW_CVARS[4][1], + crate::player::camera_view::VIEW_DEFAULTS[4][1], + ), + same( + crate::player::camera_view::VIEW_CVARS[4][2], + crate::player::camera_view::VIEW_DEFAULTS[4][2], + ), + same("mouseInvertPitch", "0"), + ours( + "autoLootDefault", + "0", + "1.12 has no auto-loot CVar at all — vanilla offers only the shift gesture, so OFF \ + IS the reference's own behaviour; the spelling is era's", + ), + // The overhead-name gates, registered at `0x6c7470` into mask `0xce8720`: `UnitNamePlayer` + // (`0x86c694`) "1" (`0x82e748`), `UnitNameNPC` (`0x86c6a4`) and `UnitNameOwn` (`0x86c6b0`) "0" + // (`0x82e570`). + same("UnitNamePlayer", "1"), + same("UnitNameNPC", "0"), + same("UnitNameOwn", "0"), + // `UnitNamePlayerGuild` (`0x86c680`, "1", mask bit `0x10`) is not a show gate: `ShouldShowName` + // (`0x6070a0`) reads bits `0x1/0x2/0x4`, and this gates the `"\n<%s>"` guild line at + // `0x609085`. `UnitNamePlayerPVPTitle` (bit `0x20`, "1") has no row: nothing here draws the + // rank prefix. + same("UnitNamePlayerGuild", "1"), + // `WorldDetail` is no 1.12 CVar but the `GetWorldDetail`/`SetWorldDetail` verb name + // (`OptionsFrame.lua:27`); stops 0/1/2 are `frillDensity` 16/32/48. `SetWorldDetail 0x488dd0` + // also writes `SmallCull` {0.07, 0.04, 0.01}, and `GetWorldDetail` reads only `SmallCull`, + // whose registered 0.04 is stop 1: the reference's slider boots at Medium. + same("WorldDetail", "1"), + // `SmallCull` (`0x68854a`: name `0x8696f8`, default `0x869718` "0.04", flags 1, callback + // `0x688b10`, record `[0xc7f330]`). The callback refuses outside [0.001, 2.0] and stores into + // `[0x868620]`, which nothing reads: no mechanism here either. Its readers are Lua ones: + // `SetWorldDetail` writes it per stop, and `OptionsFrame_SetDefaults` reads its default + // (`OptionsFrame.lua:431`). `hwDetect` sets 0.04 on the fallback row a modern GPU reaches. + same(benilla_ui::script::CVAR_SMALL_CULL, "0.04"), + // `frillDensity` (`0x68862e`: name `0x8423d8`, default `0x864644` "16", flags 1, callback + // `0x688de0`, record `[0xc7f2f4]`): detail-doodad cells visited per chunk, clamped to [1, 256] + // and handed through `0x6725a0` to `[0xc7b494]`, the bound of the scatter loop at + // `0x6bfcfb`/`0x6bff1c`. One knob with `WorldDetail`, each keeping its own clamp. `hwDetect` + // (`0x639a60`) sets it from `VideoHardware.dbc` field `+0x18`, 24 on videoID 170. pfUI's + // `hdgraphic` reads `GetCVar("frillDensity") > 48` and writes up to 256. + deviates( + "frillDensity", + "32", + "16", + "the reference's registered 16 is stop 0 and its post-`hwDetect` 24 is on no \ + stop at all, so every stop diverges; Medium (32) is the nearest one no sparser than a \ + fresh install, and erring sparse is the worse failure for ground cover", + ), + // ── Combat log display ranges, in yards ────────────────────────────────────────────────────── + // + // Registered by `0x626d00` from the `{name, default}` pairs at `0x8629e0`, read as the record's + // float (`+0x24`). Classes 0 and 1, you and your pet, have no CVar there, only the `100000.0` + // sentinel. + same("CombatLogRangeParty", "50"), + same("CombatLogRangePartyPet", "50"), + same("CombatLogRangeFriendlyPlayers", "50"), + same("CombatLogRangeFriendlyPlayersPets", "50"), + same("CombatLogRangeHostilePlayers", "50"), + same("CombatLogRangeHostilePlayersPets", "50"), + same("CombatLogRangeCreature", "30"), + // Outside that table (`0x626d5f`, "60" at `0x862e14`): `0x62c160` reads it first and falls back + // to the per-class range only when the lookup fails. + same(crate::ui_chat::combat::DEATH_LOG_RANGE_CVAR, "60"), + // ── Floating combat text ───────────────────────────────────────────────────────────────────── + // + // `CombatDamage` (`0x6032df`, record `[0xc4d944]`) is the master: its two readers, the word + // emitter `0x607140` and the number emitter `0x6128b0`, both return at "0", so nothing floats. + // The `Pet*` rows gate only the owned-by-you branch; the stock Pet Melee Damage box writes both + // (`UIOptionsFrame.lua:335-336`). + same("CombatDamage", "1"), + same("PetMeleeDamage", "1"), + same("PetSpellDamage", "1"), + // `CombatLogPeriodicSpells` (`0x6033b3`): the handle is discarded and every use looks it up by + // name; read as the record's int. + same(crate::ui_chat::combat::LOG_PERIODIC_CVAR, "1"), + // ── Sound-panel check buttons ──────────────────────────────────────────────────────────────── + // + // Category-7 registrations that keep no handle: the reference looks each up by name at use. + // + // `SoundListenerAtCharacter` (`0x457890`): the listener at the character, else at the camera + // (`update_audio_listener`). + same("SoundListenerAtCharacter", "1"), + // `EmoteSounds` (`0x4573b9`): the received text-emote voice kit, and only that. + same("EmoteSounds", "1"), + // `SoundZoneMusicNoDelay` (`0x4578b3`): `next_track_time`'s immediate path. + same("SoundZoneMusicNoDelay", "0"), + // `assistAttack` (`0x48fc50`, record `[0xb4d8f8]`): `/assist` also starts the swing. The "3" + // beside it is the next registration's, `minimapZoom`: stock `/assist` selects without + // swinging. + same("assistAttack", "0"), + // ── Mouse-look speed, per axis ─────────────────────────────────────────────────────────────── + // + // `cameraYawMoveSpeed` is the MOUSE_LOOK_SPEED slider (`UIOptionsFrame.lua:89`); a nil there + // raises in `slider:SetValue` (`0x790980`) and stops `UIOptionsFrame_Load`. The stock Save + // writes `cameraPitchMoveSpeed` as half of it (`:355-356`). The reference integrates + // OS-accelerated pixels where we take raw device deltas, so the unit factor lives in + // `camera::LOOK_YAW_PER_SPEED` and these defaults stay the reference's. The validator + // `0x50c000` → `0x50b330` rejects values outside [0.1, 360] rather than clamping, and + // `player::camera::on_cvar` does the same. + same("cameraYawMoveSpeed", "180"), + same("cameraPitchMoveSpeed", "90"), + // MOUSE_SENSITIVITY (`UIOptionsFrame.lua:87`): FrameXML's spelling of the binary's `mouseSpeed` + // (`0x402c7b`); lookups are case-insensitive (`CVar::Lookup 0x63de30`), here as there. The + // reference's default is `SPI_GETMOUSESPEED × 0.1`, "1.0" on stock Windows, and its record + // `[0x882704]` has no reader: the slider sets the OS pointer speed (`0x402ec0`, [0.1, 2.0]). + // The default matches; Deviation: here the dial multiplies the camera's own rate, because + // benilla does not change the OS pointer speed, a system-wide setting. + same("mousespeed", "1"), + // The zoom-out limit is `cameraDistanceMax × cameraDistanceMaxFactor`, held to [0, 50] + // (`0x5112d6`). `cameraDistanceMax` (`0x50beb2`, default `0x84fbd0`) has no panel row; its + // validator `0x50b310` refuses a value outside [0, 50], and `player::camera::on_cvar` does the + // same. The factor is MAX_FOLLOW_DIST (`UIOptionsFrame.lua:90`), registered "1.0" (`0x82e92c`) + // with no validator: the slider's 1 to 2 is the slider's, not the CVar's. + same("cameraDistanceMax", "15.0"), + same("cameraDistanceMaxFactor", "1"), + // `cameraSmoothStyle` (`0x50ba92`, default `[0x84f4f4]` "1"), the auto-return behind the + // character. The engine's enum is 0 Never, 1 Smart, 2 Always, as the stock dropdown writes it + // (`UIOptionsFrame.lua:525,536,547`); 3, the Never entry's position, is the validator's upper + // bound (`0x50b330(v, 0, 3)`). See `FollowStyle`. + same("cameraSmoothStyle", "1"), + // Read instead of `cameraSmoothStyle` while the state mask holds Track or Fear; no panel row. + same("cameraSmoothTrackingStyle", "1"), + // AUTO_FOLLOW_SPEED (`UIOptionsFrame.lua:88`), deg/s, registered "180.0" (`[0xbe1070]`): it + // sets the transition's duration (`|dyaw| / rate * factor`), an average rate, not a slew. The + // knob clamps it to `FOLLOW_SPEED_RANGE`. The stock Save also writes `cameraPitchSmoothSpeed` + // at a quarter (`:353`), unregistered here: `FollowRig` has one rate. + same("cameraYawSmoothSpeed", "180"), + // `cameraPivot` `[0xbe10a4]` "1" (`0x50bda3`), smart pivot: gate `0x510690`, routing + // `0x50fee0`, release `0x5107f0`; ours is `player::camera_dynamics::SmartPivot`. + same("cameraPivot", "1"), + // Read by the routing (`0x50fff5`/`0x510004`) in radians of camera rotation, so they carry to + // benilla's raw-device units unchanged. + same("cameraPivotDXMax", "0.05"), + same("cameraPivotDYMin", "0"), + // The pitch bias's ease-back once the pivot lets go, deg/s (`[0xbe0fc8]`; `0x512a50` divides + // |Δ| by rate · π/180 for the duration); no panel row. + same("cameraTargetSmoothSpeed", "90"), + // `cameraWaterCollision` `[0xbe1088]` "1" (`0x50bd63`, `0x82e748`): one register, two consumers + // that must ship together. `0x50e5ec` builds it; its `0xf0000` nibble joins the trace mask of + // all three `0x50e570` queries (reaching `0x69cc13`), and `0x50e629` tests it to lift the sweep + // origin to `surface + 2/9`. Ours: `benilla_world::collision::camera_filter` and + // `player::camera_water`. + same("cameraWaterCollision", "1"), + // `cameraTerrainTilt` `[0xbe0fd4]` "0" (`0x50bcfd`), Follow Terrain: probe and staircase + // `0x50d900`, arm `0x50dbc0`; ours is `player::camera_dynamics::TerrainTilt`. + same("cameraTerrainTilt", "0"), + // Rate in deg/s (`[0xbe0fc0]`), duration bounds in seconds (`[0xbe1050]`/`[0xbe1054]`). The 3 s + // floor always binds (20° at 7.5°/s is 2.67 s), so the camera leans rather than tracks. + same("cameraGroundSmoothSpeed", "7.5"), + same("cameraTerrainTiltTimeMin", "3"), + same("cameraTerrainTiltTimeMax", "10"), + // `cameraBobbing` `[0xbe10c0]` "0" (`0x50b76d`), head bob: kernel `0x511920`, gate `0x5105e0`; + // ours is `player::camera_dynamics::HeadBob`. + same("cameraBobbing", "0"), + // Amplitudes in the CVar's units, scaled by 1/36 (`[0x7ff9d0]`) to yards. + // `cameraBobbingSmoothSpeed` is the decay rate, read only in the disarm `0x51113a`, which + // divides the largest component by it for the ramp's duration (~0.069 s at these defaults). + same("cameraBobbingLRAmplitude", "2"), + same("cameraBobbingUDAmplitude", "2"), + same("cameraBobbingFrequency", "0.8"), + same("cameraBobbingSmoothSpeed", "0.8"), + // `statusBarText` (`0x48fc34`, record `[0xb4d904]`, no engine reader), read by + // `TextStatusBar.lua:47,97`: "0" shows the numbers on hover only. + same("statusBarText", "0"), + // Enhanced Tooltips (`UIOptionsFrame.lua:15`), registered "1" at `0x48fddd` (`0x82e748`); read + // only by the stock interface. + same("UberTooltips", "1"), + // Registered at `0x603280`: `ChatBubbles` "1", `ChatBubblesParty` "0". + same("ChatBubbles", "1"), + same("ChatBubblesParty", "0"), + // Registered "1" (`0x82e748`), category 4. `profanityFilter` (`0x402e68`, name `0x82e7f4`, + // callback `0x403570`) masks `ChatProfanity.dbc` spans inside the shared masker `0x4a1a66`, + // covering all thirteen call sites; `spamFilter` (`0x402e8e`, name `0x82e7d4`, callback + // `0x4035b0`) silently drops a line matching `SpamMessages.dbc`. The knob is + // [`crate::text_filter::TextFilterSwitches`]. + same("profanityFilter", "1"), + same("spamFilter", "1"), + // Registered by `CGlueMgr::EnterWorld` (`0x46b633` `gameTip` "0", `0x46b658` `showGameTips` + // "1"), category 5. `gameTip` is the cursor and holds the next tip, not the one on screen; + // `crate::game_tip` advances it. + same("gameTip", "0"), + same("showGameTips", "1"), + // `showLootSpam` (`0x48fd1c`, name `0x8430a0`, "1" `0x82e748`, record `0xb4e2bc`): off, group + // loot-roll lines are hidden and only the winner shows. Its three readers are the roll-line + // composers; the knob is [`crate::ui_loot::LootConfig::show_loot_spam`]. + same("showLootSpam", "1"), + // `guildMemberNotify` (`0x5e24c7`, "0" `0x82e570`, record `0xc4d3c4`): guildmate log on/off + // lines, read only in `SMSG_GUILD_EVENT`'s handler. The knob is + // [`crate::ui_guild::GuildMemberNotify`]. + same("guildMemberNotify", "0"), + // Both registered "3" (`0x48fc6c`, `0x48fc88`); the minimap's +/- buttons write them through + // `Minimap:SetZoom`. The knob is [`crate::minimap::MinimapZoom`]. + same("minimapZoom", "3"), + same("minimapInsideZoom", "3"), + // Load out of date AddOns, inverted (`0x402c3b`): "1" enforces the check. Read by the load walk + // ([`Cvars::addon_version_check`]) and live by the VM's gate. + same("checkAddonVersion", "1"), + // `gxApi` (`0x63a833`: name `0x842a64`, default `0x864f7c` "direct3d", flags 3, callback + // `0x63b030`, record `[0xc4ea94]`): the reference builds D3D9 unless it reads "OpenGl" + // (`0x63a3c4`, `0x842a5c`). Here it reports the wgpu backend (`wgpu::Backend::to_str`), pushed + // from `RenderAdapterInfo` and owned by the session, so it is never persisted. pfUI's + // `panel.lua:185` concatenates it. + deviates( + "gxApi", + "", + "direct3d", + "descriptive, not a selector — benilla renders through wgpu, which has no D3D9 \ + backend and no chooser; the value is the live adapter's own and is never persisted", + ) + .latched(), + // `gxVSync` (`0x63a859`, "1", flags 3), `OptionsFrame.lua:9`. The knob is + // [`crate::video::VideoConfig::vsync`], which the window's present mode follows at the + // `RestartGx` commit. `$WOW_NOVSYNC=1` overrides it for the session. + same("gxVSync", "1").latched(), + // `gxWindow` (`0x63a889`): "0" on enUS; zhCN registers "1", as it does `gxMaximize` + // (`0x63a8e0`), and koKR `AutoInteract` (`0x603390`). The knob is + // [`crate::video::VideoConfig::display`]. Deviation: "0" raises a borderless fullscreen window + // instead of mode-setting the display, because Wayland and macOS offer no mode-set and X11's + // leaves the desktop changed after a crash ([`crate::video`]). + same("gxWindow", "0").latched(), + // `gxResolution`, a string row parsed by `video::on_cvar`. Here it is only the windowed size: + // fullscreen is the monitor's own and no mode list is offered. + deviates( + "gxResolution", + "1600x900", + "640x480", + "narrowed to the WINDOWED size only — fullscreen is the monitor's own and we expose \ + no mode list, and 640x480 is not a window anyone would ship a client at", + ) + .latched(), + // benilla's own: a body pane's doll renders at half the frame rate while the pane is open; the + // reference draws it in the main pass. The knob is [`crate::portrait::PaneRate`]. + ours( + "boothHalfRate", + "1", + "benilla's own — the reference draws its doll inside the main pass and has no \ + second view to rate-limit", + ), + // `gxMultisample` (`0x63a950`, flags 3). The reference formats its default from field 21 of the + // `VideoHardware.dbc` row `DetectHardware` (`0x641260`) matches, which is 1, no multisampling, + // on every fallback row a modern GPU reaches. The knob is [`benilla_world::view::MsaaSetting`], + // read once at the camera's spawn; `$WOW_MSAA` overrides it for the session. + same("gxMultisample", "1").latched(), + // `GetCurrentMultisampleFormat 0x48c580` looks up all three of the Video dropdown's values by + // name, so these must exist. They describe the swapchain's own pair and steer nothing; + // `SetMultisampleFormat` writes them as `0x48c640` does. + deviates( + "gxColorBits", + "32", + "16", + "these describe, they do not steer — the pair is our swapchain's own, and every \ + format `MsaaFormats` publishes carries it", + ) + .latched(), + deviates( + "gxDepthBits", + "32", + "16", + "as `gxColorBits` — the depth half of the same descriptive pair", + ) + .latched(), + // `trilinear` and `anisotropic`, over `benilla_assets::TexFilterSetting` (`tex_filter.rs` + // carries the derivation). A change applies at the next launch, since a sampler is baked into + // each texture at load; the reference's UI says "enabled upon restart". + // `$WOW_TRILINEAR`/`$WOW_ANISO` override for the session. + // + // `trilinear` registers "0", but `hwDetect` runs `DetectHardware 0x641260` and sets it from the + // matched `VideoHardware.dbc` row before the first frame; the fallback rows an unlisted GPU + // reaches are 168/169/170, and 169 and 170 give 1. The reference install's `gx.log` resolves to + // videoID 170. + overridden( + "trilinear", + "1", + "0", + "`hwDetect` sets it from `VideoHardware.dbc` field 9 before the first frame, and \ + that field is 1 on both fallback rows an unlisted modern GPU can reach — measured on the \ + reference's own `Logs/gx.log` (`videoID: 170`)", + ), + // Not one of `hwDetect`'s sixteen (`[0x639a60, 0x639b80)` never reads `0xc7f2e4`), so the + // registered "1", off, stands. + same("anisotropic", "1"), + // The four video CVars the stock window's verbs read and write (`benilla_ui` `video_pairs`); + // benilla has no mechanism behind any of them. `shadowLevel` (`0x6885bc`, "1", flags 1, + // callback `0x688c10`, record `[0xc7f350]`) is behind `Get/SetTerrainMip` as `1 − level`: the + // callback refuses above 1 and prints "Shadow mip level changed upon restart.", and the + // terrain's shadow-map mip (`0x66f877`) reads it at load. `hwDetect` leaves it at 1. + same("shadowLevel", "1"), + // `doodadAnim` (`0x6884d8`, "1", flags 1, callback `0x688a10`, record `[0xc7f354]`), behind + // `Get/SetDoodadAnim`: the callback toggles bit `0x8000` of `[0xc7b2a4]` and calls `0x6953d0`, + // a bare `ret`, so no animation reads it. `hwDetect` sets 0 or 1 by CPU tier. + same("doodadAnim", "1"), + // `texLodBias` (`0x6885e2`, "0.0" at `0x84fad4`, flags 1, callback `0x688c90`, record + // `[0xc7f2f8]`), behind `Get/SetTexLodBias`: the callback refuses outside [-1.0, 1.0] and its + // sink `0x672640` is `ret 4`. + same("texLodBias", "0.0"), + // `baseMip` (`0x6887aa`, "0", flags 1, callback `0x689090`, record `[0xc7f2f0]`), behind + // `Get/SetBaseMip` as `1 − level`: the callback refuses outside [0, 1] ("BaseMip must be 0 or + // 1") and sets the device's first uploaded mip level (`0x589bf0`, read at `0x59f6ce`). benilla + // uploads every level; the write is kept and has no effect. `hwDetect` leaves it at 0. + same("baseMip", "0"), + // `spellEffectLevel` (`0x688900`: name `0x869324`, default `0x843068` "2", flags 1, callback + // `0x689510`, record `[0xc7f2a4]`), the Spell Detail slider (`OptionsFrame.lua:32`). The + // callback's two effects here: the particle emission scalar ([`crate::video::on_cvar`]) and + // the dynamic-object shard rate, read at each emitter's spawn (`crate::entities`). Its third, + // skipping one M2-scene ground decal below level 2 (`0x672b3a`), has no counterpart: benilla + // does not draw that projection. + same("spellEffectLevel", "2"), + // Weather Intensity (`OptionsFrame.lua:33`), registered "2" (`0x67b806`, flags 0, callback + // `0x67b870`, name `0x8685ac`). The reader is `benilla_world::weather::WeatherState`, which + // scales the precipitation spawn rate by `0x67b870`'s table {0.1, 0.33, 0.66, 1.0}; rendering + // only. + deviates( + "weatherDensity", + "3", + "2", + "every precipitation rate in `benilla-world`'s own precipitation module was \ + derived and graded against the reference install's own apitrace captures, and that \ + install runs \ + `SET weatherDensity \"3\"` (K = 1.0) — so 3 is the value a benilla-vs-reference \ + side-by-side is correct at, and the registered 2 would thin every rate to 0.66 against \ + the only client we compare with. The slider is how a player takes it back down", + ), + // `gamma` (`0x402d70`: name `0x82e924` "Gamma", default `0x82e92c` "1.0", flags 0, callback + // `0x4034d0`). The reference uploads `pow(i/255, gamma)` (`0x591680`) through + // `SetDeviceGammaRamp`, except when windowed (`byte[dev+0x20b]`). Deviation: benilla, which + // has no exclusive mode, applies the same curve in the composite pass + // ([`crate::ui_gamma::DisplayGamma`]), because the skipped upload would make a slider that + // moves no pixel. Spelled "1.000000" because `SetGamma 0x4891f0` formats with `"%f"`, and + // Restore Defaults must compare equal to the default; [`sync_cvars`] seeds it the same way. + same("gamma", "1.000000"), + // `DesktopGamma` (`0x402d4d`: name `0x82e930`, default "0" `0x82e570`, flags 0, callback + // `0x403500`, record `[0x8826d4]`), the stock window's Use Desktop Gamma box, which it also + // sets whenever Windowed Mode is ticked (`OptionsFrame.lua:399-408`). On 1 the callback puts + // the desktop's own ramp back, and `gamma`'s callback (`0x4034d0`) uploads only while this + // reads 0. No effect here: [`crate::ui_gamma::DisplayGamma`] follows `gamma` alone, because + // the stock window ticks this box whenever Windowed Mode is (`:399-408`), so honouring it would + // drop the gamma of every windowed player who opens that window. + same("DesktopGamma", "0"), + // ── The stock Video Options window's other boxes (`OptionsFrame.lua:5-22`) ─────────────────── + // + // `ffx` (`0x6ccfe3`: name `0x86f2ac`, record `[0xce8a04]`), `ffxGlow` (`0x6cc17d`, name + // `0x86f200`) and `ffxDeath` (`0x6cc6d1`, name `0x86f238`): each "1", flags 1, with the echo stub + // `0x6cde30` as callback, and each record's integer read every frame. The FFX begin/end pair + // (`0x6cd890`/`0x6cda70`) runs the active pass only while `ffx` (`0x6cd8a6`), `pixelShaders` + // (`0x6cd8b6`) and that pass's own enable hold: `CFFXGlow` (`0x6cc5a0`) reads `ffxGlow` at + // `0x6cc5a8`, `CFFXDeath` (`0x6cded0`) reads `ffxDeath` at `0x6cdf10`. The stock Okay writes + // `ffx` as Enable All Shaders (`OptionsFrame.lua:202-204`). The knob is + // [`benilla_world::ffx_glow::FfxSwitches`]. + same("ffx", "1"), + same("ffxGlow", "1"), + same("ffxDeath", "1"), + // `pixelShaders` (`0x688712`: name `0x869560`, "0", flags 1, callback `0x688f40`, record + // `[0xc7f360]`) and `specular` (`0x6886a0`: name `0x8695d8`, "0", flags 1, callback `0x688e20`, + // record `[0xc7f2cc]`), Enable All Shaders and Terrain Highlights. Both callbacks act only on a + // device whose caps field `[0x58a230()+0x98]` is above -1, setting bits 28 and 27 of the render + // flags `[0xc7b2a4]`, which gate the shader negotiation `0x6941f0` for terrain, water and + // models; `pixelShaders` also gates the FFX pass (`0x6cd8b6`). No effect here: benilla always + // draws the shader leg (`liquid.wgsl` records it), and the stock Okay carries this box into + // `ffx`, which is wired. + overridden( + "pixelShaders", + "1", + "0", + "`hwDetect` (`0x639a60`) sets it from `VideoHardware.dbc` field 11 before the first frame, \ + and that field is non-zero on fallback row 170, the one a modern GPU reaches \ + (`Logs/gx.log`: `videoID: 170`)", + ), + overridden( + "specular", + "1", + "0", + "`hwDetect` sets it from the same field 11 as `pixelShaders`, so row 170 gives 1", + ), + // `M2UseShaders` (`0x4028e9`, name `0x82e67c`, "1") and `M2UsePixelShaders` (`0x40290c`, name + // `0x82e644`, "0"), flags 0, no callback: their registering function reads six of its eight + // `M2*` records at `0x40297f`-`0x4029dd` into the M2 renderer's feature word `[0xceefb0]` + // (`0x706dd0`), once at startup, hence the stock tooltip's logout requirement. No effect here: + // benilla's M2 draw has one skinning and lighting path, chosen by no switch. + same("M2UseShaders", "1"), + same("M2UsePixelShaders", "0"), + // `lod` (`0x68848c`: name `0x8697d0`, "1", flags 1, callback `0x688920`, record `[0xc7f314]`), + // World LOD: bit 2 of `[0xc7b2a4]`, which gates the doodad LOD distance test at `0x6afb11`. No + // effect here: benilla has no doodad LOD swap, as `lodDist` above has no row. + same("lod", "1"), + // `movieSubtitle` (`0x402c18`: name `0x82e9d8`, "0", flags 0, record `[0x882640]`), Cinematic + // Subtitles: the pre-rendered movies' subtitles, read by the glue's `GetMovieSubtitles` + // (`0x46dbc0`). No effect here: benilla plays no pre-rendered movie. + same("movieSubtitle", "0"), + // `useWeatherShaders` (`0x67b81d`: name `0x868598`, "1", flags 0, no callback, record + // `[0xc7b588]`), Weather Shaders: read in each precipitation effect's constructor (`0x67480f`, + // `0x6775a7`, `0x679274`), which clears the effect's shader flag at 0. No effect here: rain's + // two legs are one draw with other scalars, but snow's fixed-function leg (`0x678960`) is a + // CPU-built triangle per flake where the shader leg (`0x678610`) is a point sprite, a second + // draw path `benilla_world::weather` does not have, so the shader leg always runs. + same("useWeatherShaders", "1"), + // The latched `gx*` rows the stock window's boxes and refresh dropdown write (flags 3, committed + // by `RestartGx`), each callback storing one field of the device-format record `0xc4eb98`. + // + // `gxMaximize` (`0x63a8e0`: name `0x864edc`, "0", "1" on zhCN like `gxWindow`, callback + // `0x63b1f0` → `+0x09` `[0xc4eba1]`): the window rebuild `0x58cf10` makes a windowed, maximized + // window a borderless popup (style `0x90000000`) at the screen's full size (`GetSystemMetrics` + // 0 and 1). The knob is [`crate::video::VideoConfig::maximize`]. + same("gxMaximize", "0").latched(), + // `gxTripleBuffer` (`0x63a80d`: name `0x864f88`, "0", callback `0x63afe0` → `+0x18`), whose + // callback refuses any value but 0 or 1 ("TripleBuffer must be 0 or 1", `0x8650b8`), a range + // this registry has no way to refuse with. No effect here: wgpu's present modes offer no + // triple-buffer choice. + same("gxTripleBuffer", "0").latched(), + // `gxCursor` (`0x63a906`: name `0x864eb8`, "1", callback `0x63b220` → `+0x05` `[0xc4eb9d]`), + // Hardware Cursor. No effect here: benilla always draws the OS cursor (`crate::cursor`) and has + // no software cursor to fall back to. + same("gxCursor", "1").latched(), + // `gxFixLag` (`0x63a9c1`: name `0x864e40`, callback `0x63b310` → `+0x06` `[0xc4eb9e]`), Fix + // Input Lag: its default is `"%d"` of `VideoHardware.dbc` field 20 (`[[0xc4e6e4]+0x50]`) of the + // row `DetectHardware` matched, 1 on all three fallback rows (168, 169, 170) a modern GPU can + // reach. No effect here: benilla has no such switch in its frame pacing. + same("gxFixLag", "1").latched(), + // `gxRefresh` (`0x63a7e7`: name `0x864fa8`, "75", callback `0x63aed0` → `+0x28`), the refresh + // dropdown (`OptionsFrame.lua:242-245`, `:300`); the callback accepts only the eleven rates at + // `0x8645e0` (60 to 200). No effect here: fullscreen is borderless at the monitor's own rate, + // and `GetRefreshRates` answers the reference's no-rates sentinel, which disables the dropdown. + same("gxRefresh", "75").latched(), + // benilla's own: the world renders at `window × this` while the UI stays native. The knob is + // [`crate::world_backdrop::RenderScale`], clamped to `RENDER_SCALE_RANGE`; at "1" nothing is + // resampled. `$WOW_RENDER_SCALE` overrides it for the session. + ours( + "renderScale", + "1", + "benilla's own — the reference has no off-screen buffer to hang a resolution dial \ + on; its nearest equivalent, `gxResolution`, drops the interface with the world", + ), + // benilla's own: `/console fpsJournal 1` appends a per-second row of position, frame cost and + // per-pass GPU time to `benilla-config/Diagnostics/fps-journal.csv`. The knob is + // [`crate::perf::FpsJournalSetting`]. + ours( + "fpsJournal", + "0", + "benilla's own — 1.12 has no player-side perf log; its nearest thing is the \ + Ctrl+R framerate label, a number with no file behind it", + ), + // `lastCharacterIndex` (`0x402d93`, "0" `0x82e570`, category 4, handle `[0x882674]`), help + // "Last character selected": a 0-based row (the selection cell `[0x83856c]` under `"%d"`), so + // "0" is the first character. It mirrors [`crate::char_select::Roster::pending_index`]. + same(crate::char_select::CVAR_LAST_CHARACTER, "0"), +]; diff --git a/crates/benilla-app/src/ui_chat.rs b/crates/benilla-app/src/ui_chat.rs index 4931d04f..02a527a3 100644 --- a/crates/benilla-app/src/ui_chat.rs +++ b/crates/benilla-app/src/ui_chat.rs @@ -30,7 +30,7 @@ mod input; /// The language gate: the exemptions and fluency lookup behind the chat garble. mod language; /// `LoggingChat`/`LoggingCombat`: the two log files `/chatlog` and `/combatlog` toggle. -mod logging; +pub(crate) mod logging; mod net; #[cfg(test)] mod notice_tests; diff --git a/crates/benilla-app/src/ui_chat/logging.rs b/crates/benilla-app/src/ui_chat/logging.rs index 1cfe12b8..ddc10ba3 100644 --- a/crates/benilla-app/src/ui_chat/logging.rs +++ b/crates/benilla-app/src/ui_chat/logging.rs @@ -71,8 +71,9 @@ impl ChatLogFiles { } } -/// `M/D HH:MM:SS.mmm` of now, UTC. -fn stamp() -> String { +/// `M/D HH:MM:SS.mmm` of now, UTC: the reference's log-line stamp (`"%u/%u %02u:%02u:%02u.%03u "`, +/// `0x866aa0`, from `GetLocalTime` at `0x65a871`), which `crate::ui_script::load_log` shares. +pub(crate) fn stamp() -> String { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); diff --git a/crates/benilla-app/src/ui_script/addons.rs b/crates/benilla-app/src/ui_script/addons.rs index 634f0416..81fba5a6 100644 --- a/crates/benilla-app/src/ui_script/addons.rs +++ b/crates/benilla-app/src/ui_script/addons.rs @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf}; use bevy::prelude::*; use benilla_ui::script::{EnableHash, ScriptValue, UiScript}; +use benilla_ui::status::{self, Status}; use benilla_ui::toc::Toc; use super::content; @@ -107,16 +108,29 @@ impl Addon { } } - /// Load this addon's `.toc`-listed files, in listed order. Each error is logged as it happens - /// and also returned, tagged `"/: "`, for the tests to assert empty. - fn load(&self, script: &UiScript) -> Vec { - self.load_files(script, &self.toc.files) + /// Load this addon's `.toc`-listed files, in listed order, reporting into `toc`, the `.toc`'s + /// load record. Each error is logged as it happens and also returned, tagged + /// `"/: "`, for the tests to assert empty. + fn load(&self, script: &UiScript, toc: &mut Status) -> Vec { + self.load_files_into(script, &self.toc.files, toc) + } + + /// [`Addon::load_files_into`] with the load record dropped. + pub(super) fn load_files(&self, script: &UiScript, files: &[String]) -> Vec { + self.load_files_into(script, files, &mut Status::default()) } /// [`Addon::load`] over an explicit slice. A manifest lists both kinds of file (the /// reference's `FrameXML.toc` opens with `GlobalStrings.lua`): a `.lua` runs as a chunk in the - /// shared state, anything else is parsed as FrameXML and materialized. - pub(super) fn load_files(&self, script: &UiScript, files: &[String]) -> Vec { + /// shared state, anything else is parsed as FrameXML and materialized. What the reference's + /// `.toc` runner (`0x6edb90`) reports goes into `toc`: a file that did not open or parse, and + /// each document's own record under its banner. + pub(super) fn load_files_into( + &self, + script: &UiScript, + files: &[String], + toc: &mut Status, + ) -> Vec { let mut failures = Vec::new(); // The `` / `