The stock video window's boxes are registered and ffx, ffxGlow, ffxDeath and gxMaximize work; benilla's options gain Spell Detail; load failures go to FrameXML.log (#585)
**#549: the stock video window's CVars.** Its check boxes, `ffx` and `gxRefresh` are registered at the reference's defaults. `pixelShaders` and `specular` ship as `overridden` "1" (`hwDetect`, `0x639a60`), and `gxFixLag` ships "1".
- `ffx`, `ffxGlow` and `ffxDeath` gate the full-screen effect passes every frame, as the reference's gate does (`0x6cd890`; `CFFXGlow` reads `ffxGlow` at `0x6cc5a8`, `CFFXDeath` reads `ffxDeath` at `0x6cdf10`).
- `gxMaximize` is latched. At the `RestartGx` commit it makes a windowed client borderless over the monitor, as the reference's window rebuild does (`0x58cf10`).
- The rest are registered with no effect, and each row says why: `useWeatherShaders` (snow's fallback leg is a separate CPU draw), `lod`, `M2UseShaders`, `M2UsePixelShaders`, `movieSubtitle`, `gxTripleBuffer`, `gxCursor`, `gxRefresh`, `DesktopGamma`.
- `cvars.rs` is split into the runtime and the table.
**#329: Spell Detail.** benilla's options window gains 1.12's Spell Detail Level slider on the Graphics page, over `spellEffectLevel` on the stock 0-2 grid.
**#583: `FrameXML.log`.** Load failures now go to `benilla-config/Logs/FrameXML.log`, as the reference's go to `Logs\FrameXML.log`, and no longer to chat.
- The log uses the reference's lines ("Couldn't open", "Error loading", "Couldn't parse XML in") and its banners.
- A UI load that reports something rewrites the file, and a runtime `LoadAddOn` appends to it (`0x490187`, `0x46aac0`).
Verified:
- New tests for each switch, the maximize latch, every box's stored value with no unknown-CVar warning, the Spell Detail row and the log file's rewrite and append, each failing on main.
- A region dump of the options window shows the new row, and the rows below it moved 35 px, and nothing else.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run unticked Full Screen Glow in the stock window, which turned the glow pass off and saved `ffxGlow = "0"`, and a missing addon file produced its four log lines.
Built on macOS; no platform seam touched.
Fixes #549
Fixes #329
Fixes #583
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b8cb886210
commit
459e0e90c1
@@ -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" })
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- On the CVar, as 1.12's slider 8 lands: there is no
|
||||
SetspellEffectLevel global. -->
|
||||
<Frame name="$parentRowSpellDetail" inherits="BenillaOptionsSliderRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowWorldDetail" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowWorldDetail" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
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" })
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- An API row over GetGamma/SetGamma, whose unit is
|
||||
`1 - gamma`; writing `gamma` would invert the
|
||||
control. -->
|
||||
<Frame name="$parentRowBrightness" inherits="BenillaOptionsSliderRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowWorldDetail" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowWorldDetail" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowSpellDetail" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowSpellDetail" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
-- OptionsFrame_SetDefaults' default (l.444-445);
|
||||
|
||||
@@ -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<Item = (&'static str, &'static str)> {
|
||||
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<String>)> {
|
||||
"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::<FfxSwitches>(&app), on);
|
||||
apply(&mut app, "ffxGlow", "0");
|
||||
assert_eq!(*res::<FfxSwitches>(&app), FfxSwitches { glow: false, ..on });
|
||||
apply(&mut app, "ffxglow", "1");
|
||||
apply(&mut app, "ffxDeath", "0");
|
||||
assert_eq!(
|
||||
*res::<FfxSwitches>(&app),
|
||||
FfxSwitches { death: false, ..on }
|
||||
);
|
||||
apply(&mut app, "ffxDeath", "1");
|
||||
apply(&mut app, "ffx", "0");
|
||||
assert_eq!(
|
||||
*res::<FfxSwitches>(&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::<Cvars>()
|
||||
.set("gxMaximize", "1");
|
||||
assert_eq!(outcome, SetOutcome::Staged);
|
||||
let events = app.world_mut().resource_mut::<Cvars>().take_events();
|
||||
assert!(events.is_empty(), "a staged write fires no callback");
|
||||
assert!(!res::<VideoConfig>(&app).maximize);
|
||||
assert_eq!(app.world_mut().resource_mut::<Cvars>().commit_latched(), 1);
|
||||
let events = app.world_mut().resource_mut::<Cvars>().take_events();
|
||||
for event in events {
|
||||
app.world_mut().trigger(event);
|
||||
}
|
||||
assert!(res::<VideoConfig>(&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::<ZoomLimit>()
|
||||
.init_resource::<FollowConfig>()
|
||||
.init_resource::<VideoConfig>()
|
||||
.init_resource::<benilla_world::ffx_glow::FfxSwitches>()
|
||||
// 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",
|
||||
]
|
||||
@@ -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<Item = (&'static str, &'static str)> {
|
||||
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"),
|
||||
];
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 `"<Addon>/<file>: <error>"`, for the tests to assert empty.
|
||||
fn load(&self, script: &UiScript) -> Vec<String> {
|
||||
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
|
||||
/// `"<Addon>/<file>: <error>"`, for the tests to assert empty.
|
||||
fn load(&self, script: &UiScript, toc: &mut Status) -> Vec<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
/// 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<String> {
|
||||
let mut failures = Vec::new();
|
||||
// The `<Include>` / `<Script file=>` provider; `read` is the sandbox.
|
||||
let provider = |req: &str| -> Option<Vec<u8>> { self.read(req) };
|
||||
@@ -124,6 +138,7 @@ impl Addon {
|
||||
// Resolved once into the source's path space, for `read` and the loader alike.
|
||||
let path = benilla_ui::loader::join_ref(&self.prefix(), file);
|
||||
let Some(bytes) = self.read(&path) else {
|
||||
toc.report(status::FAILURE, status::missing(&path, is_lua(file)));
|
||||
let e = format!("{}/{file}: not found", self.name);
|
||||
// Severity follows whose manifest is wrong. Ours, or the core's (a shipped file, or
|
||||
// a `FrameXML.toc` row the player's chain lacks), is an ERROR. A player's addon is
|
||||
@@ -156,6 +171,7 @@ impl Addon {
|
||||
let doc = match benilla_ui::framexml::parse(&benilla_ui::source::decode(&bytes)) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
toc.report(status::FAILURE, status::unparsed(&path));
|
||||
let e = format!("{}/{file}: {e}", self.name);
|
||||
error!("ui_script: parsing {e}");
|
||||
// No dialog, as the reference only logs it (FrameXML.log); still retained.
|
||||
@@ -166,7 +182,12 @@ impl Addon {
|
||||
};
|
||||
// The loader resolves relative references against the document's own directory, and
|
||||
// names the file a raise came from.
|
||||
let report = benilla_ui::loader::load_in(script, &doc, &path, &provider);
|
||||
let mut report = benilla_ui::loader::load_in(script, &doc, &path, &provider);
|
||||
std::mem::take(&mut report.status).close_into(
|
||||
toc,
|
||||
script.framexml_debug(),
|
||||
status::file_banner(&path),
|
||||
);
|
||||
// `FrameXML_Debug` traces go to the log only: the reference files them at severity 0
|
||||
// (`0x6ee2bc`) in a per-document record whose surface is untraced.
|
||||
for t in &report.traces {
|
||||
@@ -904,7 +925,17 @@ impl Walk {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
self.failures.extend(addon.load(script));
|
||||
// The add-on's record: its `.toc`'s under that banner (`0x6eddc8`), then its own banner
|
||||
// (`0x51f464`), merged into the UI load's.
|
||||
let debug = script.framexml_debug();
|
||||
let mut toc = Status::default();
|
||||
self.failures.extend(addon.load(script, &mut toc));
|
||||
let mut own = Status::default();
|
||||
let toc_path = status::addon_toc_path(&addon.name);
|
||||
toc.close_into(&mut own, debug, status::toc_banner(&toc_path));
|
||||
let mut block = Status::default();
|
||||
own.close_into(&mut block, debug, status::addon_banner(&addon.name));
|
||||
script.report_load_status(block);
|
||||
// `Bindings.xml` (`0x51f400`), after the files whose functions a binding calls, through
|
||||
// the addon's own sandboxed reader; absent is normal.
|
||||
let bindings_xml = benilla_ui::loader::join_ref(&addon.prefix(), "Bindings.xml");
|
||||
@@ -1238,7 +1269,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let script = UiScript::new().unwrap();
|
||||
let failures = addon.load(&script);
|
||||
let failures = addon.load(&script, &mut Status::default());
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"a listed Bindings.xml costs log lines, not script errors: {failures:?}"
|
||||
@@ -1282,7 +1313,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let script = UiScript::new().unwrap();
|
||||
assert!(addon.load(&script).is_empty());
|
||||
assert!(addon.load(&script, &mut Status::default()).is_empty());
|
||||
|
||||
// The exact pattern the libraries run, on each file's own traceback.
|
||||
let folder = |global: &str| -> Option<String> {
|
||||
@@ -1403,7 +1434,7 @@ mod tests {
|
||||
};
|
||||
let mut script = UiScript::new().unwrap();
|
||||
script.set_screen_size(1024.0, 768.0);
|
||||
let failures = addon.load(&script);
|
||||
let failures = addon.load(&script, &mut Status::default());
|
||||
assert!(failures.is_empty(), "addon load errors: {failures:#?}");
|
||||
|
||||
assert_eq!(
|
||||
@@ -2129,6 +2160,51 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(home.parent().unwrap());
|
||||
}
|
||||
|
||||
/// A runtime `LoadAddOn`'s misses are one block appended to `Logs\FrameXML.log`
|
||||
/// (`0x46aac0`): the add-on's banner, its toc's, then each document by its own record, an
|
||||
/// included miss under its includer's banner (`0x6ee21c`).
|
||||
#[test]
|
||||
fn a_load_addon_appends_its_misses_as_one_block() {
|
||||
let _l = crate::local_state::test_env::ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CAPTURE");
|
||||
let (home, _h) = hermetic_root("lod-log");
|
||||
write_addon(
|
||||
&home,
|
||||
"Demand",
|
||||
"## Interface: 11200\n## LoadOnDemand: 1\nlate.xml\ngone.xml\ngone.lua\n",
|
||||
&[("late.xml", "<Ui><Include file=\"nope.xml\"/></Ui>")],
|
||||
);
|
||||
let mut script = UiScript::new().unwrap();
|
||||
script.set_screen_size(1024.0, 768.0);
|
||||
let _ = load_third_party(&mut script, None, &[], true);
|
||||
seat_stock_addon_reply(&mut script);
|
||||
assert!(script.take_load_log_writes().is_empty());
|
||||
script.run("LoadAddOn('Demand')").unwrap();
|
||||
assert_eq!(
|
||||
script.take_load_log_writes(),
|
||||
vec![benilla_ui::status::LogWrite::Append(
|
||||
[
|
||||
"Loading add-on Demand",
|
||||
"** Loading table of contents Interface\\AddOns\\Demand\\Demand.toc",
|
||||
"++ Loading file Interface\\AddOns\\Demand\\late.xml",
|
||||
"Couldn't open Interface\\AddOns\\Demand\\nope.xml",
|
||||
"Couldn't open Interface\\AddOns\\Demand\\gone.xml",
|
||||
"Error loading Interface\\AddOns\\Demand\\gone.lua",
|
||||
]
|
||||
.map(String::from)
|
||||
.to_vec()
|
||||
)]
|
||||
);
|
||||
script.run("LoadAddOn('Demand')").unwrap();
|
||||
assert!(
|
||||
script.take_load_log_writes().is_empty(),
|
||||
"an already loaded add-on reports nothing"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(home.parent().unwrap());
|
||||
}
|
||||
|
||||
/// A LoadOnDemand options package on a dependency the startup walk loaded (`/msbt`'s shape):
|
||||
/// without the walk's `mark_addon_loaded` stamp the gate answers `DEP_NOT_DEMAND_LOADED`.
|
||||
#[test]
|
||||
|
||||
@@ -8,16 +8,12 @@ use benilla_ui::script::{
|
||||
WorldStateUiView, ARROW_MODEL,
|
||||
};
|
||||
|
||||
/// The manifest's one warning. Deviation: `gxRefresh`, read at `OptionsFrame.lua:300`, is not
|
||||
/// registered, because nothing here can set a refresh rate.
|
||||
const MANIFEST_WARNINGS: [&str; 1] = ["unknown CVar 'gxRefresh' (not host-registered) — ignored"];
|
||||
|
||||
fn quiet(s: &UiScript) {
|
||||
assert!(s.errors().is_empty(), "script errors: {:#?}", s.errors());
|
||||
assert_eq!(
|
||||
s.warnings(),
|
||||
MANIFEST_WARNINGS,
|
||||
"host warnings beyond the manifest's own"
|
||||
assert!(
|
||||
s.warnings().is_empty(),
|
||||
"host warnings: {:#?}",
|
||||
s.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -557,6 +557,8 @@ pub(super) fn tick_script(
|
||||
for w in script.take_warnings() {
|
||||
warn!("ui_script: {w}");
|
||||
}
|
||||
// A runtime `LoadAddOn`'s record, appended to `Logs\FrameXML.log`.
|
||||
super::load_log::write(script.take_load_log_writes());
|
||||
|
||||
// ── The handover ────────────────────────────────────────────────────────────────────────
|
||||
*pass = super::UiPassState {
|
||||
|
||||
@@ -395,25 +395,10 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
},
|
||||
);
|
||||
});
|
||||
// Deviation: each failed addon file is reported in chat, one line apiece, because otherwise
|
||||
// an addon that does not load says nothing on screen: a failure that never raised reaches no
|
||||
// error handler, so the `/errors` log cannot hold it. Read from the engine's deduplicated
|
||||
// record after the whole load edge (handlers included); the VM is fresh, so every `Load` row
|
||||
// is this load's.
|
||||
if let Some(mut chat) = world.get_resource_mut::<crate::ui_chat::ChatLog>() {
|
||||
for row in script
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.filter(|d| d.kind == benilla_ui::script::diagnostics::DiagnosticKind::Load)
|
||||
{
|
||||
let head = row.message.lines().next().unwrap_or_default();
|
||||
// Queued: the chat feed drains `ChatLog` once the UI is up.
|
||||
chat.push_event(crate::ui_chat::ChatEvent::text_only(
|
||||
crate::ui_chat::ChatEventKind::System,
|
||||
format!("Addon load failure: {head}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
// The load's record goes to `Logs\FrameXML.log` (`0x490187`-`0x4901b6`), after any
|
||||
// `LoadAddOn` block the load ran; nothing reaches chat.
|
||||
script.finish_ui_load_log();
|
||||
super::load_log::write(script.take_load_log_writes());
|
||||
// `DAMAGE_TEXT_FONT` binds at the end of the load, after every addon's `ADDON_LOADED` (where
|
||||
// MikScrollingBattleText and pfUI assign it). The reference reads it eagerly, once per world
|
||||
// entry (`0x6c8470`, called at `0x401620` in `0x401570`, after the UI load at `0x401602`),
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! `Logs\FrameXML.log` (`0x842ecc`): the UI loads' records ([`benilla_ui::status`]), written a
|
||||
//! line at a time as the reference's log layer writes one (`0x65ac20`): the stamp
|
||||
//! `"%u/%u %02u:%02u:%02u.%03u "` (`0x866aa0`), the text, then `"\r\n"` (`0x82edfc`). Deviation:
|
||||
//! the file is `benilla-config/Logs/FrameXML.log`, since the install is read-only.
|
||||
|
||||
use std::io::Write as _;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::status::LogWrite;
|
||||
|
||||
/// The log's name, the reference's.
|
||||
const FILE: &str = "FrameXML.log";
|
||||
|
||||
/// Write the VM's drains in the order they were made. A drain with no lines opens nothing, so a
|
||||
/// clean UI load leaves the last load's file in place, as the reference's lazy open does
|
||||
/// (`0x65a930`).
|
||||
pub(super) fn write(writes: Vec<LogWrite>) {
|
||||
if writes.iter().all(|w| lines(w).is_empty()) {
|
||||
return;
|
||||
}
|
||||
let Some(dir) = crate::local_state::logs_dir() else {
|
||||
return; // hermetic capture, or no state folder
|
||||
};
|
||||
write_under(&dir, writes);
|
||||
}
|
||||
|
||||
fn lines(write: &LogWrite) -> &[String] {
|
||||
match write {
|
||||
LogWrite::Rewrite(lines) | LogWrite::Append(lines) => lines,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`write`] into `dir`.
|
||||
fn write_under(dir: &std::path::Path, writes: Vec<LogWrite>) {
|
||||
let path = dir.join(FILE);
|
||||
for w in writes {
|
||||
let (append, lines) = match w {
|
||||
LogWrite::Rewrite(lines) => (false, lines),
|
||||
LogWrite::Append(lines) => (true, lines),
|
||||
};
|
||||
if lines.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut body = String::new();
|
||||
for line in &lines {
|
||||
body.push_str(&crate::ui_chat::logging::stamp());
|
||||
body.push_str(" ");
|
||||
body.push_str(line);
|
||||
body.push_str("\r\n");
|
||||
}
|
||||
let written = std::fs::create_dir_all(dir).and_then(|()| {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.append(append)
|
||||
.truncate(!append)
|
||||
.open(&path)?;
|
||||
file.write_all(body.as_bytes())
|
||||
});
|
||||
if let Err(e) = written {
|
||||
warn!("ui_script: cannot write {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn read(dir: &std::path::Path) -> Vec<String> {
|
||||
std::fs::read_to_string(dir.join(FILE))
|
||||
.unwrap()
|
||||
.split("\r\n")
|
||||
.filter(|l| !l.is_empty())
|
||||
// `M/D HH:MM:SS.mmm` and two spaces.
|
||||
.map(|l| l.splitn(3, ' ').nth(2).unwrap().trim_start().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A UI load's drain replaces the file, a `LoadAddOn` drain appends, and an empty UI drain
|
||||
/// leaves the last file standing.
|
||||
#[test]
|
||||
fn a_load_rewrites_a_load_addon_appends_and_a_clean_load_touches_nothing() {
|
||||
let dir = std::env::temp_dir().join(format!("benilla-framexml-log-{}", std::process::id()));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
write_under(&dir, vec![LogWrite::Rewrite(vec!["first load".into()])]);
|
||||
write_under(&dir, vec![LogWrite::Rewrite(vec!["second load".into()])]);
|
||||
write_under(&dir, vec![LogWrite::Append(vec!["on demand".into()])]);
|
||||
assert_eq!(read(&dir), ["second load", "on demand"]);
|
||||
write_under(&dir, vec![LogWrite::Rewrite(vec![])]);
|
||||
assert_eq!(read(&dir), ["second load", "on demand"]);
|
||||
let raw = std::fs::read_to_string(dir.join(FILE)).unwrap();
|
||||
let first = raw.split("\r\n").next().unwrap();
|
||||
let (stamp, text) = first.split_at(first.find(" ").unwrap());
|
||||
assert_eq!(text, " second load");
|
||||
let (date, time) = stamp.split_once(' ').unwrap();
|
||||
assert!(date.split('/').all(|p| p.parse::<u32>().is_ok()), "{date}");
|
||||
assert_eq!(time.len(), "HH:MM:SS.mmm".len(), "{time}");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
@@ -171,9 +171,22 @@ pub(super) fn load_core(script: &UiScript) -> Vec<String> {
|
||||
);
|
||||
error!("ui_script: {e}");
|
||||
script.report_load_failure(&e);
|
||||
// Into the UI load's own record, before any banner (`0x6edc3f`).
|
||||
let mut log = benilla_ui::status::Status::default();
|
||||
log.report(
|
||||
benilla_ui::status::FAILURE,
|
||||
benilla_ui::status::missing(reference_ui::TOC, false),
|
||||
);
|
||||
script.report_load_status(log);
|
||||
return vec![e];
|
||||
};
|
||||
core.load_files(script, &core.toc.files)
|
||||
let mut toc = benilla_ui::status::Status::default();
|
||||
let failures = core.load_files_into(script, &core.toc.files, &mut toc);
|
||||
let mut log = benilla_ui::status::Status::default();
|
||||
let banner = benilla_ui::status::toc_banner(reference_ui::TOC);
|
||||
toc.close_into(&mut log, script.framexml_debug(), banner);
|
||||
script.report_load_status(log);
|
||||
failures
|
||||
}
|
||||
|
||||
/// Every [`LAYER_MANIFEST`] entry, in order, after the whole core: the layer loads as more of the
|
||||
|
||||
@@ -18,6 +18,7 @@ pub(crate) mod extract;
|
||||
mod input;
|
||||
#[cfg(test)]
|
||||
mod layer_tests;
|
||||
mod load_log;
|
||||
mod manifest;
|
||||
|
||||
/// The stock FrameXML this client runs off the player's own patch chain; its header is the rule.
|
||||
|
||||
@@ -1272,6 +1272,56 @@ fn the_world_detail_slider_writes_the_cvar_and_the_readout_names_its_stop() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// 1.12's slider 8, Spell Detail Level (`OptionsFrame.lua:32`), over `spellEffectLevel` on its
|
||||
/// 0..2 grid, labelled and tipped from the stock GlobalStrings.
|
||||
#[test]
|
||||
fn the_spell_detail_slider_reads_and_writes_the_spell_effect_level() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
const ROW: &str = "BenillaOptionsFrameContainerBodyGraphicsRowSpellDetail";
|
||||
let mut s = audio_harness();
|
||||
s.set_cvar_host("spellEffectLevel", "1");
|
||||
let mut s = harness_on(s);
|
||||
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
|
||||
s.run("BenillaOptionsFrameCategoryListRowGraphics:Click()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return {ROW}Label:GetText()"))
|
||||
.unwrap(),
|
||||
s.eval::<String>("return SPELL_DETAIL").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return {ROW}.tip")).unwrap(),
|
||||
"OPTION_TOOLTIP_SPELL_DETAIL"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(&format!(
|
||||
"return {ROW}ControlSlider:GetValue() == 1 and {ROW}ControlValue:GetText() == \"Medium\""
|
||||
))
|
||||
.unwrap());
|
||||
assert!(
|
||||
s.take_cvar_changes().is_empty(),
|
||||
"reading the CVar on select must not write it back"
|
||||
);
|
||||
// The reference's grid (`OptionsFrameSliders[8]`): 0 to 2, one apart.
|
||||
assert!(s
|
||||
.eval::<bool>(&format!(
|
||||
"local lo, hi = {ROW}ControlSlider:GetMinMaxValues() \
|
||||
return lo == 0 and hi == 2 and {ROW}ControlSlider:GetValueStep() == 1"
|
||||
))
|
||||
.unwrap());
|
||||
s.run(&format!("{ROW}ControlSlider:SetValue(0)")).unwrap();
|
||||
assert_eq!(
|
||||
s.take_cvar_changes(),
|
||||
vec![("spellEffectLevel".to_string(), "0".to_string())]
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return {ROW}ControlValue:GetText()"))
|
||||
.unwrap(),
|
||||
"Low"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_nameplates_page_toggles_the_unit_name_cvars() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -2231,9 +2281,9 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
|
||||
);
|
||||
checked += 1;
|
||||
}
|
||||
// 81 rows less the three untipped below; four of the 78 carry a `BENILLA_` key, and a dropdown
|
||||
// 82 rows less the three untipped below; four of the 79 carry a `BENILLA_` key, and a dropdown
|
||||
// row is checked on the key it wears at rest.
|
||||
assert_eq!(checked, 78, "every tipped row carries a live key");
|
||||
assert_eq!(checked, 79, "every tipped row carries a live key");
|
||||
assert_eq!(
|
||||
untipped,
|
||||
vec![
|
||||
@@ -2329,8 +2379,8 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
|
||||
s.errors()
|
||||
);
|
||||
}
|
||||
// Every tipped row: the same 78 the key census counts.
|
||||
assert_eq!(raised, 78, "every row but Auto Loot raises a description");
|
||||
// Every tipped row: the same 79 the key census counts.
|
||||
assert_eq!(raised, 79, "every row but Auto Loot raises a description");
|
||||
}
|
||||
|
||||
/// 1.12's AdvancedOptionsCombatText box as saved-global rows: a click writes the global, never a
|
||||
|
||||
@@ -62,6 +62,37 @@ fn slider(s: &UiScript, i: usize) -> f64 {
|
||||
fn assert_clean(s: &UiScript, step: &str) {
|
||||
let errors = s.errors();
|
||||
assert!(errors.is_empty(), "{step} raised: {errors:#?}");
|
||||
let unknown: Vec<String> = s
|
||||
.warnings()
|
||||
.into_iter()
|
||||
.filter(|w| w.contains("unknown CVar"))
|
||||
.collect();
|
||||
assert!(
|
||||
unknown.is_empty(),
|
||||
"{step} touched an unregistered CVar: {unknown:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Every box's `(cvar, stored value, checked)`, over the window's own table
|
||||
/// (`OptionsFrameCheckButtons`, `OptionsFrame.lua:5-22`); the stored value is `"nil"` when absent.
|
||||
fn boxes(s: &UiScript) -> Vec<(String, String, bool, bool)> {
|
||||
s.eval::<Vec<String>>(
|
||||
r#"local out = {}
|
||||
for _, v in OptionsFrameCheckButtons do
|
||||
local b = getglobal("OptionsFrameCheckButton"..v.index)
|
||||
local bit = function(x) if x then return "1" else return "0" end end
|
||||
table.insert(out, v.cvar.."|"..(GetCVar(v.cvar) or "nil").."|"..bit(b:GetChecked())
|
||||
.."|"..bit(b:IsEnabled() == 1))
|
||||
end
|
||||
return out"#,
|
||||
)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let f: Vec<&str> = row.split('|').collect();
|
||||
(f[0].to_string(), f[1].to_string(), f[2] == "1", f[3] == "1")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Open, walk every box and slider, Okay; reopen and Defaults; reopen, move the gamma and Cancel.
|
||||
@@ -94,6 +125,18 @@ fn the_stock_video_window_opens_saves_restores_defaults_and_cancels_clean() {
|
||||
"`DesktopGamma` registers \"0\" (`0x402d4d`)"
|
||||
);
|
||||
|
||||
// Every box shows its CVar's stored value: all eighteen are registered, and an enabled box is
|
||||
// ticked exactly when its value is "1" (`OptionsFrame_Load`'s `EnableCheckBox(button, 1,
|
||||
// GetCVar(...))`).
|
||||
let at_open = boxes(&s);
|
||||
assert_eq!(at_open.len(), 18);
|
||||
for (cvar, value, checked, enabled) in &at_open {
|
||||
assert!(value == "0" || value == "1", "{cvar} stores {value:?}");
|
||||
if *enabled {
|
||||
assert_eq!(*checked, value == "1", "{cvar}'s box shows its value");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Every box, then every slider to a position off its default. ──
|
||||
for i in 1..=18 {
|
||||
s.run(&format!("OptionsFrameCheckButton{i}:Click()"))
|
||||
@@ -132,6 +175,36 @@ fn the_stock_video_window_opens_saves_restores_defaults_and_cancels_clean() {
|
||||
assert_eq!(cvar(&s, "gamma").as_deref(), Some("0.500000"));
|
||||
// The Use Desktop Gamma box was ticked, `_Save` wrote "1" and the recorded value follows it.
|
||||
assert_eq!(cvar(&s, "desktopGamma").as_deref(), Some("1"));
|
||||
// `_Save` stored every box as it was left, and each moved box kept its click.
|
||||
let saved = boxes(&s);
|
||||
for (cvar, value, checked, _) in &saved {
|
||||
assert_eq!(
|
||||
value,
|
||||
if *checked { "1" } else { "0" },
|
||||
"{cvar} kept its box"
|
||||
);
|
||||
}
|
||||
let moved: Vec<&str> = at_open
|
||||
.iter()
|
||||
.zip(&saved)
|
||||
.filter(|(a, b)| a.1 != b.1)
|
||||
.map(|(a, _)| a.0.as_str())
|
||||
.collect();
|
||||
for cvar in [
|
||||
"ffxGlow",
|
||||
"specular",
|
||||
"movieSubtitle",
|
||||
"lod",
|
||||
"gxCursor",
|
||||
"useWeatherShaders",
|
||||
] {
|
||||
assert!(
|
||||
moved.contains(&cvar),
|
||||
"{cvar} was clicked and saved: {moved:?}"
|
||||
);
|
||||
}
|
||||
// Enable All Shaders is copied into `ffx` (`OptionsFrame.lua:202-204`).
|
||||
assert_eq!(cvar(&s, "ffx"), cvar(&s, "pixelShaders"));
|
||||
|
||||
// ── Defaults: `OptionsFrame_SetDefaults` reads `GetCVarDefault` for `smallCull`,
|
||||
// `shadowLevel` and `baseMip` (`:431-443`) and ends in `RestoreVideoDefaults`. ──
|
||||
|
||||
@@ -867,7 +867,7 @@ fn the_login_one_shots_wait_for_the_in_game_ui() {
|
||||
|
||||
/// An addon that fails to load without raising (a `.toc` naming a file the package lacks) reaches
|
||||
/// no Lua error handler, so the `/errors` log (which collects off the handler) does not hold it:
|
||||
/// the engine's record keeps it, and a chat line names it.
|
||||
/// the engine's record keeps it, and `Logs\FrameXML.log` names it as the reference's does.
|
||||
#[test]
|
||||
fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -880,7 +880,7 @@ fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() {
|
||||
std::fs::create_dir_all(&dir).expect("addon dir");
|
||||
std::fs::write(
|
||||
dir.join("AaMissing.toc"),
|
||||
"## Interface: 11200\nBossnames\\BossNames.xml\n",
|
||||
"## Interface: 11200\nBossnames\\BossNames.xml\nDatabase\\hash.lua\n",
|
||||
)
|
||||
.expect("toc");
|
||||
// …and no such file is written.
|
||||
@@ -941,14 +941,29 @@ fn an_addon_that_fails_to_load_without_raising_is_readable_in_the_error_log() {
|
||||
through `_ERRORMESSAGE` and through every addon handler that replaces it"
|
||||
);
|
||||
|
||||
// 4. Deviation: a chat line names the failure, where the reference stays silent, because
|
||||
// otherwise it reaches no screen.
|
||||
// 4. Nothing in chat, and the reference's lines in `benilla-config/Logs/FrameXML.log`, each
|
||||
// stamped `M/D HH:MM:SS.mmm` and ended `\r\n`: the add-on's banner, its toc's, then each
|
||||
// miss by its arm (`0x6edaa0`, `0x704bc0`). The clean core and probe add nothing.
|
||||
let lines = world.resource::<crate::ui_chat::ChatLog>().pending_lines();
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l.starts_with("Addon load failure: ") && l.contains("AaMissing")),
|
||||
"world entry queued the failure's line: {lines:?}"
|
||||
!lines.iter().any(|l| l.contains("AaMissing")),
|
||||
"no chat line names the failure: {lines:?}"
|
||||
);
|
||||
let log = std::fs::read_to_string(tmp.join("benilla-config/Logs/FrameXML.log"))
|
||||
.expect("the load wrote FrameXML.log");
|
||||
let texts: Vec<&str> = log
|
||||
.split_terminator("\r\n")
|
||||
.map(|l| l.split_once(" ").expect("a stamp, then two spaces").1)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
texts,
|
||||
[
|
||||
"Loading add-on AaMissing",
|
||||
"** Loading table of contents Interface\\AddOns\\AaMissing\\AaMissing.toc",
|
||||
"Couldn't open Interface\\AddOns\\AaMissing\\Bossnames\\BossNames.xml",
|
||||
"Error loading Interface\\AddOns\\AaMissing\\Database\\hash.lua",
|
||||
],
|
||||
"{log}"
|
||||
);
|
||||
|
||||
drop(world);
|
||||
@@ -1189,10 +1204,7 @@ fn a_clean_world_entry_raises_only_the_warnings_we_have_named() {
|
||||
|
||||
// Deviation: the `OnInputLanguageChanged` script slot (`ChatFrame.xml:121`, the IME language
|
||||
// indicator) is refused, because benilla has no IME to fire it.
|
||||
// Deviation: `gxRefresh`, read by stock `OptionsFrameRefreshDropDown_OnLoad`
|
||||
// (`OptionsFrame.lua:300`), is not registered, because no target offers the exclusive mode-set
|
||||
// a refresh rate needs; `GetRefreshRates` answers the reference's no-rates sentinel.
|
||||
const KNOWN: [&str; 2] = ["OnInputLanguageChanged", "unknown CVar 'gxRefresh'"];
|
||||
const KNOWN: [&str; 1] = ["OnInputLanguageChanged"];
|
||||
|
||||
let unexpected: Vec<String> = script
|
||||
.diagnostics()
|
||||
|
||||
+122
-17
@@ -86,11 +86,20 @@ pub(crate) fn parse_resolution(value: &str) -> Option<UVec2> {
|
||||
(size.x > 0 && size.y > 0).then_some(size)
|
||||
}
|
||||
|
||||
/// The `WindowMode` a display mode means, on a given monitor.
|
||||
pub(crate) fn window_mode(display: DisplayMode, monitor: MonitorSelection) -> WindowMode {
|
||||
match display {
|
||||
DisplayMode::Fullscreen => WindowMode::BorderlessFullscreen(monitor),
|
||||
DisplayMode::Windowed => WindowMode::Windowed,
|
||||
/// The `WindowMode` a display mode means, on a given monitor. `maximize` is `gxMaximize`, which
|
||||
/// counts only while windowed: the reference's window rebuild (`0x58cf10`) gives a windowed,
|
||||
/// maximized window the popup style `0x90000000`, no caption and no border, sized to the screen
|
||||
/// (`GetSystemMetrics` 0 and 1) at its origin, which is a borderless window over the monitor.
|
||||
pub(crate) fn window_mode(
|
||||
display: DisplayMode,
|
||||
maximize: bool,
|
||||
monitor: MonitorSelection,
|
||||
) -> WindowMode {
|
||||
match (display, maximize) {
|
||||
(DisplayMode::Fullscreen, _) | (DisplayMode::Windowed, true) => {
|
||||
WindowMode::BorderlessFullscreen(monitor)
|
||||
}
|
||||
(DisplayMode::Windowed, false) => WindowMode::Windowed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,14 +107,17 @@ pub(crate) fn window_mode(display: DisplayMode, monitor: MonitorSelection) -> Wi
|
||||
/// does not flash windowed until `Startup`. `MonitorSelection::Primary`, since `Current` has no
|
||||
/// answer before the window exists (`bevy_winit::select_monitor`).
|
||||
pub(crate) fn boot_window_mode() -> WindowMode {
|
||||
let display = if windowed_env() {
|
||||
DisplayMode::Windowed
|
||||
let flag = |name| crate::cvars::boot_cvar(name).and_then(|v| v.parse::<f32>().ok());
|
||||
// A run that sizes its own window owns both rows for the session.
|
||||
let (display, maximize) = if windowed_env() {
|
||||
(DisplayMode::Windowed, false)
|
||||
} else {
|
||||
crate::cvars::boot_cvar("gxWindow")
|
||||
.and_then(|v| v.parse::<f32>().ok())
|
||||
.map_or_else(DisplayMode::default, display_from_flag)
|
||||
(
|
||||
flag("gxWindow").map_or_else(DisplayMode::default, display_from_flag),
|
||||
flag("gxMaximize").is_some_and(|v| v != 0.0),
|
||||
)
|
||||
};
|
||||
window_mode(display, MonitorSelection::Primary)
|
||||
window_mode(display, maximize, MonitorSelection::Primary)
|
||||
}
|
||||
|
||||
/// The windowed size the primary window is born at, `gxResolution`, read as [`boot_window_mode`]
|
||||
@@ -122,6 +134,8 @@ pub(crate) fn boot_windowed_size() -> UVec2 {
|
||||
pub(crate) struct VideoConfig {
|
||||
pub(crate) vsync: bool,
|
||||
pub(crate) display: DisplayMode,
|
||||
/// `gxMaximize`: a windowed window fills the monitor, borderless ([`window_mode`]).
|
||||
pub(crate) maximize: bool,
|
||||
/// The windowed size, `gxResolution`. Kept while fullscreen so leaving it can restore it.
|
||||
pub(crate) windowed: UVec2,
|
||||
}
|
||||
@@ -135,6 +149,7 @@ impl Default for VideoConfig {
|
||||
} else {
|
||||
DisplayMode::default()
|
||||
},
|
||||
maximize: false,
|
||||
windowed: DEFAULT_WINDOWED,
|
||||
}
|
||||
}
|
||||
@@ -165,6 +180,7 @@ pub(crate) fn on_cvar(
|
||||
mut weather: ResMut<benilla_world::weather::WeatherState>,
|
||||
particles: Option<ResMut<benilla_world::particles::ParticleTuning>>,
|
||||
mut spell_effect_level: ResMut<SpellEffectLevel>,
|
||||
mut ffx: ResMut<benilla_world::ffx_glow::FfxSwitches>,
|
||||
mut console: Option<ResMut<crate::console::ConsoleEcho>>,
|
||||
mut cvars: ResMut<crate::cvars::Cvars>,
|
||||
) {
|
||||
@@ -179,6 +195,19 @@ pub(crate) fn on_cvar(
|
||||
"gxvsync" => cfg.vsync = ev.flag(),
|
||||
// The reference's polarity: `1` is windowed (the row is "Windowed Mode").
|
||||
"gxwindow" => cfg.display = display_from_flag(v),
|
||||
// Latched like `gxWindow`: the commit is `RestartGx`, which rebuilds the window
|
||||
// (`0x58cf10` reads `+0x09`); `apply_window_mode` is that rebuild.
|
||||
"gxmaximize" => cfg.maximize = ev.flag(),
|
||||
// The FFX pass's three switches, read every frame by the reference (`0x6cd8a6`, `0x6cc5a8`,
|
||||
// `0x6cdf10`), so a write shows on the next frame.
|
||||
"ffx" | "ffxglow" | "ffxdeath" => {
|
||||
match ev.key().as_str() {
|
||||
"ffx" => ffx.master = ev.flag(),
|
||||
"ffxglow" => ffx.glow = ev.flag(),
|
||||
_ => ffx.death = ev.flag(),
|
||||
}
|
||||
info!("video: full-screen effects {:?}", *ffx);
|
||||
}
|
||||
"farclip" => view.farclip = v.clamp(*FARCLIP_RANGE.start(), *FARCLIP_RANGE.end()),
|
||||
// Clamped, where the reference refuses an out-of-range write and keeps the value
|
||||
// (`0x688d90` echoes "NearClip must be in range 0.01 - 0.33" and returns 0).
|
||||
@@ -586,7 +615,7 @@ fn apply_window_mode(
|
||||
let Ok(mut window) = windows.single_mut() else {
|
||||
return;
|
||||
};
|
||||
let want = window_mode(cfg.display, MonitorSelection::Current);
|
||||
let want = window_mode(cfg.display, cfg.maximize, MonitorSelection::Current);
|
||||
let already = matches!(
|
||||
(&window.mode, &want),
|
||||
(WindowMode::Windowed, WindowMode::Windowed)
|
||||
@@ -601,13 +630,21 @@ fn apply_window_mode(
|
||||
// Leaving fullscreen hands the size back: entering it overwrote `window.resolution` with the
|
||||
// monitor's (`bevy_window`'s documented behaviour). Both writes land in one frame, as
|
||||
// `bevy_winit::changed_windows` applies `mode` before `resolution`.
|
||||
if cfg.display == DisplayMode::Windowed {
|
||||
if want == WindowMode::Windowed {
|
||||
window
|
||||
.resolution
|
||||
.set(cfg.windowed.x as f32, cfg.windowed.y as f32);
|
||||
}
|
||||
window.mode = want;
|
||||
info!("video: display mode {:?} ({want:?})", cfg.display);
|
||||
info!(
|
||||
"video: display mode {:?}{} ({want:?})",
|
||||
cfg.display,
|
||||
if cfg.maximize && cfg.display == DisplayMode::Windowed {
|
||||
", maximized"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -633,11 +670,11 @@ mod tests {
|
||||
fn the_default_is_borderless_fullscreen_not_exclusive() {
|
||||
assert_eq!(DisplayMode::default(), DisplayMode::Fullscreen);
|
||||
assert!(matches!(
|
||||
window_mode(DisplayMode::Fullscreen, MonitorSelection::Primary),
|
||||
window_mode(DisplayMode::Fullscreen, false, MonitorSelection::Primary),
|
||||
WindowMode::BorderlessFullscreen(_)
|
||||
));
|
||||
assert_eq!(
|
||||
window_mode(DisplayMode::Windowed, MonitorSelection::Primary),
|
||||
window_mode(DisplayMode::Windowed, false, MonitorSelection::Primary),
|
||||
WindowMode::Windowed
|
||||
);
|
||||
}
|
||||
@@ -657,6 +694,7 @@ mod tests {
|
||||
app.insert_resource(VideoConfig {
|
||||
vsync: true,
|
||||
display: DisplayMode::Fullscreen,
|
||||
maximize: false,
|
||||
windowed: UVec2::new(1024, 768),
|
||||
})
|
||||
// Seated by hand: this test runs the one system, not the plugin.
|
||||
@@ -671,7 +709,7 @@ mod tests {
|
||||
.entity_mut(win)
|
||||
.get_mut::<Window>()
|
||||
.unwrap()
|
||||
.mode = window_mode(DisplayMode::Fullscreen, MonitorSelection::Primary);
|
||||
.mode = window_mode(DisplayMode::Fullscreen, false, MonitorSelection::Primary);
|
||||
|
||||
app.update();
|
||||
assert!(
|
||||
@@ -708,6 +746,73 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
/// `gxMaximize` counts only while windowed, and there it is the reference's popup over the
|
||||
/// whole screen (`0x58cf10`: style `0x90000000`, `GetSystemMetrics` 0 and 1).
|
||||
#[test]
|
||||
fn a_maximized_window_is_borderless_over_the_monitor() {
|
||||
let m = MonitorSelection::Current;
|
||||
assert_eq!(
|
||||
window_mode(DisplayMode::Windowed, true, m),
|
||||
WindowMode::BorderlessFullscreen(m)
|
||||
);
|
||||
assert_eq!(
|
||||
window_mode(DisplayMode::Windowed, false, m),
|
||||
WindowMode::Windowed
|
||||
);
|
||||
assert_eq!(
|
||||
window_mode(DisplayMode::Fullscreen, true, m),
|
||||
WindowMode::BorderlessFullscreen(m),
|
||||
"fullscreen ignores it"
|
||||
);
|
||||
}
|
||||
|
||||
/// The `RestartGx` rebuild applies a committed `gxMaximize`, and un-maximizing hands the
|
||||
/// windowed size back.
|
||||
#[test]
|
||||
fn the_restart_rebuild_applies_a_committed_maximize() {
|
||||
let mut app = App::new();
|
||||
app.insert_resource(VideoConfig {
|
||||
vsync: true,
|
||||
display: DisplayMode::Windowed,
|
||||
maximize: false,
|
||||
windowed: UVec2::new(1024, 768),
|
||||
})
|
||||
.init_resource::<GxRestarts>()
|
||||
.add_systems(Update, apply_window_mode);
|
||||
let win = app
|
||||
.world_mut()
|
||||
.spawn((Window::default(), PrimaryWindow))
|
||||
.id();
|
||||
app.update();
|
||||
assert_eq!(
|
||||
app.world().entity(win).get::<Window>().unwrap().mode,
|
||||
WindowMode::Windowed
|
||||
);
|
||||
// The commit's observer write, then the restart the stock Okay asks for.
|
||||
app.world_mut().resource_mut::<VideoConfig>().maximize = true;
|
||||
app.world_mut().resource_mut::<GxRestarts>().0 += 1;
|
||||
app.update();
|
||||
assert!(matches!(
|
||||
app.world().entity(win).get::<Window>().unwrap().mode,
|
||||
WindowMode::BorderlessFullscreen(MonitorSelection::Current)
|
||||
));
|
||||
app.world_mut()
|
||||
.entity_mut(win)
|
||||
.get_mut::<Window>()
|
||||
.unwrap()
|
||||
.resolution
|
||||
.set(2560.0, 1440.0);
|
||||
app.world_mut().resource_mut::<VideoConfig>().maximize = false;
|
||||
app.world_mut().resource_mut::<GxRestarts>().0 += 1;
|
||||
app.update();
|
||||
let w = app.world().entity(win).get::<Window>().unwrap();
|
||||
assert_eq!(w.mode, WindowMode::Windowed);
|
||||
assert_eq!(
|
||||
(w.resolution.width(), w.resolution.height()),
|
||||
(1024.0, 768.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gxresolution_parses_the_reference_spelling() {
|
||||
assert_eq!(parse_resolution("1280x800"), Some(UVec2::new(1280, 800)));
|
||||
|
||||
@@ -104,6 +104,7 @@ const PUBLISHED: &[(&str, &str)] = &[
|
||||
("doodad_anim::spawn_anim_host", "record"),
|
||||
("ffx_glow::FfxBackdrop", "record"),
|
||||
("ffx_glow::FfxGlow", "record"),
|
||||
("ffx_glow::FfxSwitches", "record"),
|
||||
("ffx_glow::GlueFfx", "record"),
|
||||
("final_pass::FinalPassTarget", "record"),
|
||||
("instance_tint::InstanceTintMirrors", "record"),
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod messages;
|
||||
pub mod order;
|
||||
pub mod script;
|
||||
pub mod source;
|
||||
pub mod status;
|
||||
pub mod strings;
|
||||
pub mod toc;
|
||||
pub mod widget;
|
||||
|
||||
@@ -14,6 +14,7 @@ use mlua::{Function, ObjectLike, Table, Value};
|
||||
|
||||
use crate::framexml::{self, Element, ParsedDocument, ScriptRef, TopLevel};
|
||||
use crate::script::{FontObject, JustifyH, JustifyV, Outline, UiScript};
|
||||
use crate::status;
|
||||
|
||||
mod backdrop;
|
||||
mod geometry;
|
||||
@@ -39,6 +40,11 @@ pub struct LoadReport {
|
||||
/// Trace lines, only while `FrameXML_Debug` is on: the reference's flag (`[0xceea30]`) boots 0
|
||||
/// and its loader traces gate on `flag > 0` (`0x6ee298`). A trace is severity 0, not a warning.
|
||||
pub traces: Vec<String>,
|
||||
/// The document's own load status, the lines `0x6ede10` reports into its record for
|
||||
/// `Logs\FrameXML.log`: the traces, each `<Include>` or `<Script file=>` that did not open or
|
||||
/// parse, and each included document's own record under its banner. The caller closes it
|
||||
/// under the document's banner ([`crate::status::Status::close_into`]).
|
||||
pub status: crate::status::Status,
|
||||
}
|
||||
|
||||
/// Materialize a parsed FrameXML document into live frames in `script`, in document order
|
||||
@@ -68,11 +74,24 @@ pub fn load_into(
|
||||
doc: &ParsedDocument,
|
||||
path: &str,
|
||||
files: &dyn Fn(&str) -> Option<Vec<u8>>,
|
||||
) -> LoadReport {
|
||||
load_into_under(lua, doc, path, files, "")
|
||||
}
|
||||
|
||||
/// [`load_into`] for a provider whose path space sits under `status_root` in the install's, so
|
||||
/// the status lines name the install path (`LoadAddOn` reads under `Interface\AddOns\`).
|
||||
pub fn load_into_under(
|
||||
lua: &mlua::Lua,
|
||||
doc: &ParsedDocument,
|
||||
path: &str,
|
||||
files: &dyn Fn(&str) -> Option<Vec<u8>>,
|
||||
status_root: &str,
|
||||
) -> LoadReport {
|
||||
let mut loader = Loader {
|
||||
lua,
|
||||
files,
|
||||
path: path.to_string(),
|
||||
status_root,
|
||||
report: LoadReport::default(),
|
||||
warned: HashSet::new(),
|
||||
deferred_anchors: Vec::new(),
|
||||
@@ -98,6 +117,7 @@ pub fn apply_template(lua: &mlua::Lua, wrapper: &Table, kind: &str, template: &s
|
||||
lua,
|
||||
files: &no_files,
|
||||
path: String::new(),
|
||||
status_root: "",
|
||||
report: LoadReport::default(),
|
||||
warned: HashSet::new(),
|
||||
deferred_anchors: Vec::new(),
|
||||
@@ -196,11 +216,16 @@ pub(super) fn model_kind_tag(tag: &str) -> bool {
|
||||
.any(|k| k.eq_ignore_ascii_case(tag))
|
||||
}
|
||||
|
||||
/// The `.lua` test `0x6ede10` opens with: the last `.` anywhere in the resolved path, not the
|
||||
/// basename's (`strrchr`), compared case-insensitively to `".lua"` (`0x8710c8`, via `0x64a4c0`).
|
||||
fn has_lua_suffix(path: &str) -> bool {
|
||||
path.rsplit_once('.')
|
||||
.is_some_and(|(_, ext)| ext.eq_ignore_ascii_case("lua"))
|
||||
impl Loader<'_> {
|
||||
/// A provider path as a status line names it: under [`Loader::status_root`], `\`-separated.
|
||||
fn status_path(&self, path: &str) -> String {
|
||||
status::install_path(&format!("{}{path}", self.status_root))
|
||||
}
|
||||
|
||||
/// `FrameXML_Debug` above 0, read where the reference reads it, at each close.
|
||||
fn debug(&self) -> bool {
|
||||
self.model().framexml_debug.get() > 0
|
||||
}
|
||||
}
|
||||
|
||||
/// The directory part of a provider path, `""` for a bare name.
|
||||
@@ -219,6 +244,8 @@ struct Loader<'a> {
|
||||
/// The current document's path: its directory resolves relative references, the whole path
|
||||
/// names inline `<Script>` chunks. Swapped for each nested include and restored after.
|
||||
pub(super) path: String,
|
||||
/// What the provider's path space sits under in the install's, for the status lines.
|
||||
pub(super) status_root: &'a str,
|
||||
// The template and font registries live on the `Model` and persist across loads: the
|
||||
// reference's template table is global (`0x6ee500`), so `MerchantFrame.xml` inherits a template
|
||||
// `CharacterFrameTemplates.xml` registered. Fonts are their own namespace, stored flattened.
|
||||
@@ -329,10 +356,15 @@ impl Loader<'_> {
|
||||
.push(format!("<Script file=\"{path}\">: {e}"));
|
||||
}
|
||||
}
|
||||
None => self.report.missing_files.push(format!(
|
||||
"<Script file=\"{path}\">: no provider hit for \"{joined}\"; \
|
||||
every handler in it is missing"
|
||||
)),
|
||||
None => {
|
||||
// The chunk arm (`0x704bc0`), whatever the extension.
|
||||
let line = status::missing(&self.status_path(&joined), true);
|
||||
self.report.status.report(status::FAILURE, line);
|
||||
self.report.missing_files.push(format!(
|
||||
"<Script file=\"{path}\">: no provider hit for \"{joined}\"; \
|
||||
every handler in it is missing"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
TopLevel::Script(ScriptRef::Inline { body, line }) => {
|
||||
@@ -370,6 +402,9 @@ impl Loader<'_> {
|
||||
pub(super) fn do_include(&mut self, path: &str) {
|
||||
let joined = join_ref(self.base(), path);
|
||||
let Some(bytes) = (self.files)(&joined) else {
|
||||
// Reported into this document's record, before the included one has its own.
|
||||
let line = status::missing(&self.status_path(&joined), status::runs_as_lua(&joined));
|
||||
self.report.status.report(status::FAILURE, line);
|
||||
self.report.missing_files.push(format!(
|
||||
"<Include file=\"{path}\">: no provider hit for \"{joined}\"; the whole \
|
||||
document it names is missing"
|
||||
@@ -380,7 +415,7 @@ impl Loader<'_> {
|
||||
// which dispatches on the resolved path's extension, never its content: a `.lua` suffix
|
||||
// (`0x6edee6`-`0x6edf0f`) runs the file as a chunk (`0x704bc0`), anything else parses as
|
||||
// XML. A `.toc` line goes through the same routine.
|
||||
if has_lua_suffix(&joined) {
|
||||
if status::runs_as_lua(&joined) {
|
||||
if let Err(e) = self.run(crate::source::chunk(&bytes), &joined) {
|
||||
self.report
|
||||
.errors
|
||||
@@ -392,15 +427,24 @@ impl Loader<'_> {
|
||||
match framexml::parse(&crate::source::decode(&bytes)) {
|
||||
Ok(sub) => {
|
||||
self.report.warnings.extend(sub.warnings.iter().cloned());
|
||||
// The included document resolves and names against its own path until it is done.
|
||||
// The included document resolves and names against its own path until it is done,
|
||||
// and reports into a record of its own, closed under its banner (`0x6ee21c`).
|
||||
let outer = std::mem::replace(&mut self.path, joined.clone());
|
||||
let outer_status = std::mem::take(&mut self.report.status);
|
||||
self.load_doc(&sub);
|
||||
self.path = outer;
|
||||
let own = std::mem::replace(&mut self.report.status, outer_status);
|
||||
let banner = status::file_banner(&self.status_path(&joined));
|
||||
let debug = self.debug();
|
||||
own.close_into(&mut self.report.status, debug, banner);
|
||||
}
|
||||
Err(e) => {
|
||||
let line = status::unparsed(&self.status_path(&joined));
|
||||
self.report.status.report(status::FAILURE, line);
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("<Include file=\"{path}\">: {e}"))
|
||||
}
|
||||
Err(e) => self
|
||||
.report
|
||||
.errors
|
||||
.push(format!("<Include file=\"{path}\">: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,10 +628,10 @@ impl Loader<'_> {
|
||||
|
||||
// One of the reference's five loader traces: `Instantiate 0x6ee280`'s `"-- Creating %s
|
||||
// named %s"` (`0x871154`), gated `flag > 0` at `0x6ee298`; the first `%s` is the tag.
|
||||
if self.model().framexml_debug.get() > 0 {
|
||||
self.report
|
||||
.traces
|
||||
.push(format!("-- Creating {} named {dbg_name}", el.tag));
|
||||
if self.debug() {
|
||||
let trace = format!("-- Creating {} named {dbg_name}", el.tag);
|
||||
self.report.status.report(status::TRACE, trace.clone());
|
||||
self.report.traces.push(trace);
|
||||
}
|
||||
|
||||
// `$parent` for its contents; a nameless frame passes its nearest named ancestor on.
|
||||
|
||||
@@ -11,6 +11,7 @@ use mlua::{Lua, MultiValue, Value};
|
||||
use super::addon_gate::{can_load, GateRow, Verdict};
|
||||
use super::binding_abi::flag;
|
||||
use super::Model;
|
||||
use crate::status::Status;
|
||||
|
||||
/// Reads a chain-sourced addon's file by its path inside the patch chain.
|
||||
pub type AddonChainReader = Box<dyn Fn(&str) -> Option<Vec<u8>>>;
|
||||
@@ -384,7 +385,23 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua_str(lua, "MISSING")?,
|
||||
]));
|
||||
};
|
||||
match load_addon(lua, i) {
|
||||
// One record for the call and the calls inside it (`ds:0xb4e3e0` counts them,
|
||||
// `0x48ea85`/`0x48ea9d`), drained when the outermost returns (`0x48eab4`).
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.load_log
|
||||
.demand_depth += 1;
|
||||
let result = load_addon(lua, i);
|
||||
{
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let log = &mut model.load_log;
|
||||
log.demand_depth -= 1;
|
||||
if log.demand_depth == 0 && !log.demand.is_empty() {
|
||||
let lines = log.demand.take_lines();
|
||||
log.writes.push(crate::status::LogWrite::Append(lines));
|
||||
}
|
||||
}
|
||||
match result {
|
||||
Ok(()) => Ok(MultiValue::from_vec(vec![Value::Integer(1), Value::Nil])),
|
||||
Err(reason) => Ok(MultiValue::from_vec(vec![
|
||||
Value::Nil,
|
||||
@@ -477,7 +494,18 @@ fn load_addon(lua: &Lua, i: usize) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
run_files(lua, &name, &read, &files);
|
||||
let mut toc = crate::status::Status::default();
|
||||
run_files(lua, &name, &read, &files, &mut toc);
|
||||
// The toc's record under its banner, then the add-on's (`0x6eddc8`, `0x51f464`).
|
||||
let debug = framexml_debug(lua);
|
||||
let mut own = crate::status::Status::default();
|
||||
let toc_path = crate::status::addon_toc_path(&name);
|
||||
toc.close_into(&mut own, debug, crate::status::toc_banner(&toc_path));
|
||||
{
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let banner = crate::status::addon_banner(&name);
|
||||
own.close_into(&mut model.load_log.demand, debug, banner);
|
||||
}
|
||||
// `Bindings.xml` loads between the files and the saved variables.
|
||||
load_bindings(lua, &name, &read);
|
||||
load_saved_variables(lua, i);
|
||||
@@ -555,22 +583,38 @@ fn load_saved_variables(lua: &Lua, i: usize) {
|
||||
/// player's addon, the patch chain for a Blizzard LoadOnDemand one.
|
||||
type Reader = Box<dyn Fn(&str) -> Option<Vec<u8>>>;
|
||||
|
||||
/// `FrameXML_Debug` above 0, which puts every banner in the load log.
|
||||
fn framexml_debug(lua: &Lua) -> bool {
|
||||
lua.app_data_ref::<Model>()
|
||||
.expect("model")
|
||||
.framexml_debug
|
||||
.get()
|
||||
> 0
|
||||
}
|
||||
|
||||
/// Where the reader's paths sit in the install's, for the load log's lines.
|
||||
const ADDONS_ROOT: &str = "Interface/AddOns/";
|
||||
|
||||
/// Run the addon's `.toc` files in order, by the startup walk's rules: `.lua` is a chunk,
|
||||
/// anything else FrameXML, each path resolved against the including file's directory.
|
||||
fn run_files(lua: &Lua, name: &str, read: &Reader, files: &[String]) {
|
||||
/// anything else FrameXML, each path resolved against the including file's directory. `toc` is
|
||||
/// the `.toc`'s load record (`0x6edb90`).
|
||||
fn run_files(lua: &Lua, name: &str, read: &Reader, files: &[String], toc: &mut Status) {
|
||||
let provider = |req: &str| -> Option<Vec<u8>> { read(req) };
|
||||
for file in files {
|
||||
let path = crate::loader::join_ref(name, file);
|
||||
let install = format!("{ADDONS_ROOT}{path}");
|
||||
let lua_file = crate::status::runs_as_lua(&path);
|
||||
let Some(bytes) = read(&path) else {
|
||||
// The reference logs `Couldn't open %s` and carries on, so the addon still loads and
|
||||
// `IsAddOnLoaded` answers 1; the miss is warned, never a script error.
|
||||
toc.report(
|
||||
crate::status::FAILURE,
|
||||
crate::status::missing(&install, lua_file),
|
||||
);
|
||||
load_miss(lua, name, file);
|
||||
continue;
|
||||
};
|
||||
if std::path::Path::new(file)
|
||||
.extension()
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("lua"))
|
||||
{
|
||||
if lua_file {
|
||||
if let Err(e) = run_chunk(lua, &bytes, &crate::script::addon_chunk_name(name, file)) {
|
||||
log_error(lua, &format!("{name}/{file}: {e}"));
|
||||
}
|
||||
@@ -581,11 +625,14 @@ fn run_files(lua: &Lua, name: &str, read: &Reader, files: &[String]) {
|
||||
Err(e) => {
|
||||
// A document that will not parse is a load failure, not a raise: the reference
|
||||
// logs `Couldn't parse XML in %s` (`0x846fd8`) to `FrameXML.log` and goes on.
|
||||
toc.report(crate::status::FAILURE, crate::status::unparsed(&install));
|
||||
load_failure(lua, &format!("{name}/{file}: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let report = crate::loader::load_into(lua, &doc, &path, &provider);
|
||||
let report = crate::loader::load_into_under(lua, &doc, &path, &provider, ADDONS_ROOT);
|
||||
let banner = crate::status::file_banner(&install);
|
||||
report.status.close_into(toc, framexml_debug(lua), banner);
|
||||
// Warnings take the `<Addon>/<file>` prefix; a bare loader warning names no document.
|
||||
for w in report.warnings {
|
||||
crate::script::diagnostics::record_warning(lua, &format!("{name}/{file}: {w}"));
|
||||
|
||||
@@ -117,6 +117,32 @@ impl super::UiScript {
|
||||
record_load_failure(&self.lua, msg);
|
||||
}
|
||||
|
||||
/// Whether `FrameXML_Debug` is above 0, which puts every banner in the load log.
|
||||
pub fn framexml_debug(&self) -> bool {
|
||||
self.model_ref().framexml_debug.get() > 0
|
||||
}
|
||||
|
||||
/// Merge one closed level of a UI load (the core's `.toc`, an add-on) into the load's record.
|
||||
pub fn report_load_status(&self, status: crate::status::Status) {
|
||||
self.model_mut().load_log.ui.merge(status);
|
||||
}
|
||||
|
||||
/// The UI load is over: its record is drained (`0x490187`), after any `LoadAddOn` block it
|
||||
/// ran.
|
||||
pub fn finish_ui_load_log(&self) {
|
||||
let mut model = self.model_mut();
|
||||
let lines = model.load_log.ui.take_lines();
|
||||
model
|
||||
.load_log
|
||||
.writes
|
||||
.push(crate::status::LogWrite::Rewrite(lines));
|
||||
}
|
||||
|
||||
/// The drains made since the last call, in order, for the host to write.
|
||||
pub fn take_load_log_writes(&mut self) -> Vec<crate::status::LogWrite> {
|
||||
std::mem::take(&mut self.model_mut().load_log.writes)
|
||||
}
|
||||
|
||||
/// Retain a warning the host has already logged (a loader warning with its `<Addon>/<file>`
|
||||
/// prefix). Queuing it on `Model::warnings` too would log it twice; an engine-raised warning
|
||||
/// goes through `Model::record_warning`, which does both.
|
||||
|
||||
@@ -102,6 +102,8 @@ pub(crate) struct Model {
|
||||
/// `FrameXML_Debug`'s flag (`0x488440` over `[0xceea30]`, boots 0); the loader's trace lines
|
||||
/// print while it is greater than 0 (`0x6ee298`), so it is signed.
|
||||
pub(crate) framexml_debug: std::cell::Cell<i32>,
|
||||
/// The loads' records for `Logs\FrameXML.log` and the drains the host has yet to write.
|
||||
pub(crate) load_log: crate::status::LoadLog,
|
||||
/// The FrameXML font registry, a namespace apart from templates (a font inherits only a font).
|
||||
pub(crate) framexml_fonts:
|
||||
std::cell::RefCell<std::collections::HashMap<String, crate::framexml::Element>>,
|
||||
@@ -1019,6 +1021,7 @@ impl Model {
|
||||
addons_saved_character: None,
|
||||
framexml_templates: Default::default(),
|
||||
framexml_debug: Default::default(),
|
||||
load_log: Default::default(),
|
||||
framexml_fonts: Default::default(),
|
||||
arena: WidgetArena::new(),
|
||||
layout_inputs: HashMap::new(),
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
//! The reference's load status (`CStatus`, `Status.cpp`, entries `{text, severity}`): each level of
|
||||
//! a UI load (the add-on, its `.toc`, each XML document) reports into a record of its own, which is
|
||||
//! merged into its caller's when it closes and drained, line by line, to `Logs\FrameXML.log`
|
||||
//! (`0x842ecc`) at the load's end.
|
||||
|
||||
/// A trace, printed only under `FrameXML_Debug` (`"-- Creating %s named %s"`, `0x6ee2bc`).
|
||||
pub const TRACE: u8 = 0;
|
||||
/// A file-level failure: a file that did not open or did not parse (`0x6edad4`, `0x704c15`).
|
||||
pub const FAILURE: u8 = 2;
|
||||
|
||||
/// One level's record: its lines in order and the running maximum severity (`+0x10`, which the
|
||||
/// append `0x41a000` and the prepend `0x419ed0` raise, never a count).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Status {
|
||||
lines: Vec<String>,
|
||||
max: u8,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Append a line (`0x41a000`).
|
||||
pub fn report(&mut self, severity: u8, text: impl Into<String>) {
|
||||
self.lines.push(text.into());
|
||||
self.max = self.max.max(severity);
|
||||
}
|
||||
|
||||
/// Merge `other` in after this record's own lines, as `0x41a100` re-reports each entry.
|
||||
pub fn merge(&mut self, other: Status) {
|
||||
self.lines.extend(other.lines);
|
||||
self.max = self.max.max(other.max);
|
||||
}
|
||||
|
||||
/// Close one level into its caller: its banner goes first (a prepend at severity 0) when
|
||||
/// `debug` is on or the level reported anything above a trace (`jg` on `FrameXML_Debug`, else
|
||||
/// `jle` on the maximum, `0x6eddb6`/`0x6eddbb`, `0x6ee207`, `0x51f44f`), then the merge.
|
||||
pub fn close_into(mut self, parent: &mut Status, debug: bool, banner: String) {
|
||||
if debug || self.max > TRACE {
|
||||
self.lines.insert(0, banner);
|
||||
}
|
||||
parent.merge(self);
|
||||
}
|
||||
|
||||
/// The lines, in the order the drain writes them.
|
||||
pub fn lines(&self) -> &[String] {
|
||||
&self.lines
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.lines.is_empty()
|
||||
}
|
||||
|
||||
/// The lines, leaving the record empty.
|
||||
pub fn take_lines(&mut self) -> Vec<String> {
|
||||
self.max = TRACE;
|
||||
std::mem::take(&mut self.lines)
|
||||
}
|
||||
}
|
||||
|
||||
/// A path as the reference reports it: install-relative and `\`-separated.
|
||||
pub fn install_path(path: &str) -> String {
|
||||
path.replace('/', "\\")
|
||||
}
|
||||
|
||||
/// Whether the load-one-file routine (`0x6ede10`) runs `path` as a chunk: the text from its last
|
||||
/// `.` equals `.lua` (`0x8710c8`) under `_strnicmp` (`0x6edeea`-`0x6edf05`).
|
||||
pub fn runs_as_lua(path: &str) -> bool {
|
||||
path.rfind('.')
|
||||
.is_some_and(|i| path[i..].eq_ignore_ascii_case(".lua"))
|
||||
}
|
||||
|
||||
/// A file that did not open: `"Error loading %s"` (`0x872e50`) from the chunk arm (`0x704bc0`),
|
||||
/// else `"Couldn't open %s"` (`0x846ff4`) from the XML arm (`0x6edaa0`).
|
||||
pub fn missing(path: &str, lua: bool) -> String {
|
||||
if lua {
|
||||
format!("Error loading {}", install_path(path))
|
||||
} else {
|
||||
format!("Couldn't open {}", install_path(path))
|
||||
}
|
||||
}
|
||||
|
||||
/// A document that opened and did not parse (`0x846fd8`, `0x6edb11`).
|
||||
pub fn unparsed(path: &str) -> String {
|
||||
format!("Couldn't parse XML in {}", install_path(path))
|
||||
}
|
||||
|
||||
/// An XML document's banner (`0x87100c`, `0x6ee21c`).
|
||||
pub fn file_banner(path: &str) -> String {
|
||||
format!("++ Loading file {}", install_path(path))
|
||||
}
|
||||
|
||||
/// A `.toc`'s banner (`0x870fec`, `0x6eddc8`).
|
||||
pub fn toc_banner(path: &str) -> String {
|
||||
format!("** Loading table of contents {}", install_path(path))
|
||||
}
|
||||
|
||||
/// An add-on's banner (`0x8539fc`, `0x51f464`).
|
||||
pub fn addon_banner(name: &str) -> String {
|
||||
format!("Loading add-on {name}")
|
||||
}
|
||||
|
||||
/// Where an add-on's `.toc` is, as its banner names it: `Interface\AddOns\<name>\<name>.toc`.
|
||||
pub fn addon_toc_path(name: &str) -> String {
|
||||
format!("Interface\\AddOns\\{name}\\{name}.toc")
|
||||
}
|
||||
|
||||
/// One drain of a load's record into `Logs\FrameXML.log`, in the order the loads ended.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LogWrite {
|
||||
/// A UI load's drain (`UI_Init`, `0x490187`-`0x4901b6`): the log opens in mode 0 (`0x48ff06`),
|
||||
/// created anew (`CREATE_ALWAYS`, `0x65a1c0`) at its first line (`0x65a930`), so a load that
|
||||
/// reported nothing leaves the file as it was.
|
||||
Rewrite(Vec<String>),
|
||||
/// A runtime `LoadAddOn`'s drain (`0x46aac0`): mode 4 (`0x48ea4e`), opened (`OPEN_ALWAYS`)
|
||||
/// and appended to at its first line.
|
||||
Append(Vec<String>),
|
||||
}
|
||||
|
||||
/// A VM's two load records and the drains they have made. `LoadAddOn` keeps its own record
|
||||
/// (`ds:0xb4e3dc`), refcounted (`ds:0xb4e3e0`) so a nested call writes one block, and drained when
|
||||
/// the count returns to zero (`0x48eab4`), even inside a UI load, whose own drain comes last.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct LoadLog {
|
||||
pub(crate) ui: Status,
|
||||
pub(crate) demand: Status,
|
||||
pub(crate) demand_depth: u32,
|
||||
pub(crate) writes: Vec<LogWrite>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The install's own log shape (the reference's `Logs\FrameXML.log`, `FrameXML_Debug` 0): the
|
||||
/// add-on banner first, then the toc's, then the miss, and a later document with a message
|
||||
/// under its own banner; a clean document says nothing.
|
||||
#[test]
|
||||
fn banners_frame_only_what_reported_something() {
|
||||
let mut root = Status::default();
|
||||
let mut addon = Status::default();
|
||||
let mut toc = Status::default();
|
||||
toc.report(FAILURE, missing("Interface/AddOns/A/Missing.xml", false));
|
||||
let mut clean = Status::default();
|
||||
clean.report(TRACE, "-- Creating Frame named X");
|
||||
clean.close_into(&mut toc, false, file_banner("Interface/AddOns/A/Clean.xml"));
|
||||
let mut broken = Status::default();
|
||||
broken.report(FAILURE, missing("Interface/AddOns/A/b.lua", true));
|
||||
broken.close_into(&mut toc, false, file_banner("Interface/AddOns/A/Core.xml"));
|
||||
toc.close_into(&mut addon, false, toc_banner(&addon_toc_path("A")));
|
||||
addon.close_into(&mut root, false, addon_banner("A"));
|
||||
let mut quiet = Status::default();
|
||||
Status::default().close_into(&mut quiet, false, toc_banner(&addon_toc_path("B")));
|
||||
quiet.close_into(&mut root, false, addon_banner("B"));
|
||||
assert_eq!(
|
||||
root.lines(),
|
||||
[
|
||||
"Loading add-on A",
|
||||
"** Loading table of contents Interface\\AddOns\\A\\A.toc",
|
||||
"Couldn't open Interface\\AddOns\\A\\Missing.xml",
|
||||
"-- Creating Frame named X",
|
||||
"++ Loading file Interface\\AddOns\\A\\Core.xml",
|
||||
"Error loading Interface\\AddOns\\A\\b.lua",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Under `FrameXML_Debug` every level prints its banner, reported or not.
|
||||
#[test]
|
||||
fn debug_prints_every_banner() {
|
||||
let mut root = Status::default();
|
||||
Status::default().close_into(&mut root, true, file_banner("x.xml"));
|
||||
assert_eq!(root.lines(), ["++ Loading file x.xml"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_chunk_arm_is_the_lua_extension_alone() {
|
||||
assert!(runs_as_lua("Interface/AddOns/A/core.LUA"));
|
||||
assert!(!runs_as_lua("Interface/AddOns/A/core.lua.xml"));
|
||||
assert!(!runs_as_lua("Interface/AddOns/A/core"));
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,40 @@ pub struct FfxGlowGain(pub f32);
|
||||
#[derive(Resource, Clone, Default, ExtractResource)]
|
||||
pub struct FfxDeathFade(pub f32);
|
||||
|
||||
/// The three switches the reference's FFX pass reads every frame, as the `ffx`, `ffxGlow` and
|
||||
/// `ffxDeath` CVars' integers (each registered "1" with the echo stub `0x6cde30`, so nothing is
|
||||
/// cached). The begin/end pair (`0x6cd890`/`0x6cda70`) runs the active pass only while `ffx`
|
||||
/// (`[0xce8a04]`, `0x6cd8a6`/`0x6cda76`) is on and the pass's own enable (vtable `+4`) says so:
|
||||
/// `CFFXGlow` (`0x6cc5a0`) reads `ffxGlow` (`0x6cc5a8`), `CFFXDeath` (`0x6cded0`) reads `ffxDeath`
|
||||
/// (`0x6cdf10`). A pass that does not run is not replaced by the other one: the slot holds one.
|
||||
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, ExtractResource)]
|
||||
pub struct FfxSwitches {
|
||||
/// `ffx`, the master.
|
||||
pub master: bool,
|
||||
/// `ffxGlow`: the glow pass, the drunk and underwater haze and the underwater warp with it.
|
||||
pub glow: bool,
|
||||
/// `ffxDeath`: the ghost's death pass.
|
||||
pub death: bool,
|
||||
}
|
||||
|
||||
impl Default for FfxSwitches {
|
||||
/// Each registers "1".
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
master: true,
|
||||
glow: true,
|
||||
death: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FfxSwitches {
|
||||
/// Whether the pass the slot holds runs: the death pass while a ghost, else the glow pass.
|
||||
fn runs(self, death_pass: bool) -> bool {
|
||||
self.master && if death_pass { self.death } else { self.glow }
|
||||
}
|
||||
}
|
||||
|
||||
/// The glue screens' FFX state. The select build's tail (`0x472fba`–`0x473007`) forks on the
|
||||
/// selected record's `CHARSELECT+0xfc & 0x2000` (`0x472fd9`), installs (`0x6cde60`) the glue
|
||||
/// death or glow pass and pins `LightParams.glow` (`[0x6d48b0()+0x110]`) to a constant: both
|
||||
@@ -254,10 +288,11 @@ fn sync_wave(
|
||||
}
|
||||
|
||||
/// Whether a view draws the warped combine: the WorldFrame pair (the glue pair reaches the same
|
||||
/// render, but the guard's in-world half `0x467d00` never trips there), [`FfxWave::active`], and
|
||||
/// no ghost, whose `CFFXDeath::Render` (`0x6cdf20`) has one list and no guard.
|
||||
fn wave_armed(state: FfxState, wave: FfxWave, death: f32) -> bool {
|
||||
matches!(state, FfxState::Player) && wave.active && death == 0.0
|
||||
/// render, but the guard's in-world half `0x467d00` never trips there), [`FfxWave::active`], no
|
||||
/// ghost, whose `CFFXDeath::Render` (`0x6cdf20`) has one list and no guard, and a glow pass that
|
||||
/// runs ([`FfxSwitches`]).
|
||||
fn wave_armed(state: FfxState, wave: FfxWave, world: FfxPassState) -> bool {
|
||||
matches!(state, FfxState::Player) && wave.active && world.death == 0.0 && world.runs
|
||||
}
|
||||
|
||||
/// [`FfxGlowGain`]: the zone's `LightParams.glow` in world (its default 0.5 while no lighting
|
||||
@@ -697,6 +732,8 @@ fn dither_armed() -> f32 {
|
||||
|
||||
/// One view's combine uniform: the zone glow × [`FfxGlow::gain_scale`], the death gate and haze of
|
||||
/// its pass pair, the dither arm, then the wave phases, written every frame so the clock runs free.
|
||||
/// A pair whose pass is switched off ([`FfxSwitches`]) leaves the scene as it was drawn: no glow,
|
||||
/// no haze, no death combine, only the decode.
|
||||
fn combine_uniform(
|
||||
zone_gain: f32,
|
||||
world: FfxPassState,
|
||||
@@ -709,8 +746,13 @@ fn combine_uniform(
|
||||
FfxState::Glue => glue,
|
||||
FfxState::None => FfxPassState::INERT,
|
||||
};
|
||||
let (gain, feed) = if feed.runs {
|
||||
(zone_gain * glow.gain_scale, feed)
|
||||
} else {
|
||||
(0.0, FfxPassState::INERT)
|
||||
};
|
||||
[
|
||||
zone_gain * glow.gain_scale,
|
||||
gain,
|
||||
feed.death,
|
||||
feed.haze,
|
||||
dither_armed(),
|
||||
@@ -721,28 +763,48 @@ fn combine_uniform(
|
||||
]
|
||||
}
|
||||
|
||||
/// One pass pair's live death gate and haze mix.
|
||||
/// One pass pair's live death gate and haze mix, and whether the pass its slot holds runs.
|
||||
#[derive(Clone, Copy)]
|
||||
struct FfxPassState {
|
||||
death: f32,
|
||||
haze: f32,
|
||||
runs: bool,
|
||||
}
|
||||
|
||||
impl FfxPassState {
|
||||
fn world(death: f32, haze: f32) -> Self {
|
||||
Self { death, haze }
|
||||
Self {
|
||||
death,
|
||||
haze,
|
||||
runs: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Death only: the reference packs the haze into `primary.z` of the glow combine (`0x6cb020`)
|
||||
/// from the in-world player's state, which a glue screen does not have.
|
||||
fn glue(death: f32) -> Self {
|
||||
Self { death, haze: 0.0 }
|
||||
Self {
|
||||
death,
|
||||
haze: 0.0,
|
||||
runs: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The pair under the three switches: the glue pair is built by the same constructors
|
||||
/// (`0x46a723`/`0x46a752`) and reads the same records, and its paint calls the same begin
|
||||
/// (`0x46fad3`).
|
||||
fn switched(self, switches: FfxSwitches) -> Self {
|
||||
Self {
|
||||
runs: switches.runs(self.death > 0.0),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// What a view running neither pair reads: a bake.
|
||||
const INERT: Self = Self {
|
||||
death: 0.0,
|
||||
haze: 0.0,
|
||||
runs: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -792,10 +854,9 @@ impl ViewNode for FfxGlowNode {
|
||||
let render_device = render_context.render_device().clone();
|
||||
let diagnostics = render_context.diagnostic_recorder();
|
||||
|
||||
// The combines read the blur only through `w` (`x`) and the haze (`z`); with both zero the
|
||||
// filter passes are skipped, and the stale, finite ¼-res target is multiplied by zero.
|
||||
let blur_read = uniform[0] != 0.0 || uniform[2] != 0.0;
|
||||
if blur_read {
|
||||
// With both zero the filter passes are skipped, and the stale, finite ¼-res target is
|
||||
// multiplied by zero.
|
||||
if blur_read(&uniform) {
|
||||
let layout_filter = pipeline_cache.get_bind_group_layout(&pipelines.layout_filter);
|
||||
// Per frame: a `Write` view flips its main texture per call ([`FfxGlowTextures`]).
|
||||
let down_bind = render_device.create_bind_group(
|
||||
@@ -895,18 +956,29 @@ impl ViewNode for FfxGlowNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a combine reads the blur: only through `w` (`x`) and the haze (`z`), so with both zero
|
||||
/// the three filter passes (Box4, Gauss4 H and V) do not run.
|
||||
fn blur_read(uniform: &[f32; 8]) -> bool {
|
||||
uniform[0] != 0.0 || uniform[2] != 0.0
|
||||
}
|
||||
|
||||
/// One view's combine uniform and whether its underwater entry is armed, for both combine nodes.
|
||||
fn live_combine(world: &World, glow: &FfxGlow) -> ([f32; 8], bool) {
|
||||
let death = world.resource::<FfxDeathFade>().0;
|
||||
let switches = *world.resource::<FfxSwitches>();
|
||||
let wave = *world.resource::<FfxWave>();
|
||||
let player = FfxPassState::world(
|
||||
world.resource::<FfxDeathFade>().0,
|
||||
world.resource::<FfxHazeMix>().0,
|
||||
)
|
||||
.switched(switches);
|
||||
let uniform = combine_uniform(
|
||||
world.resource::<FfxGlowGain>().0,
|
||||
FfxPassState::world(death, world.resource::<FfxHazeMix>().0),
|
||||
FfxPassState::glue(world.resource::<GlueFfx>().death()),
|
||||
player,
|
||||
FfxPassState::glue(world.resource::<GlueFfx>().death()).switched(switches),
|
||||
glow,
|
||||
wave,
|
||||
);
|
||||
(uniform, wave_armed(glow.state, wave, death))
|
||||
(uniform, wave_armed(glow.state, wave, player))
|
||||
}
|
||||
|
||||
/// The render-world views an [`FfxBackdrop`] claims this frame; a claimed view has no combine.
|
||||
@@ -1090,6 +1162,7 @@ impl Plugin for FfxGlowPlugin {
|
||||
.init_resource::<GlueFfx>()
|
||||
.init_resource::<FfxHazeMix>()
|
||||
.init_resource::<FfxWave>()
|
||||
.init_resource::<FfxSwitches>()
|
||||
.add_plugins((
|
||||
ExtractComponentPlugin::<FfxGlow>::default(),
|
||||
ExtractComponentPlugin::<FfxBackdrop>::default(),
|
||||
@@ -1098,6 +1171,7 @@ impl Plugin for FfxGlowPlugin {
|
||||
ExtractResourcePlugin::<GlueFfx>::default(),
|
||||
ExtractResourcePlugin::<FfxHazeMix>::default(),
|
||||
ExtractResourcePlugin::<FfxWave>::default(),
|
||||
ExtractResourcePlugin::<FfxSwitches>::default(),
|
||||
))
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -1152,6 +1226,10 @@ mod tests {
|
||||
fn living_glue() -> FfxPassState {
|
||||
FfxPassState::glue(0.0)
|
||||
}
|
||||
/// The world pair, live: alive, sober and dry.
|
||||
fn alive() -> FfxPassState {
|
||||
FfxPassState::world(0.0, 0.0)
|
||||
}
|
||||
|
||||
/// Checked as one table, so a new preset cannot quietly join the wrong side.
|
||||
#[test]
|
||||
@@ -1301,27 +1379,139 @@ mod tests {
|
||||
};
|
||||
let dry = FfxWave::default();
|
||||
assert!(
|
||||
wave_armed(FfxState::Player, wet, 0.0),
|
||||
wave_armed(FfxState::Player, wet, alive()),
|
||||
"in-world, submerged, alive — the reference walks its second list"
|
||||
);
|
||||
assert!(
|
||||
!wave_armed(FfxState::Player, dry, 0.0),
|
||||
!wave_armed(FfxState::Player, dry, alive()),
|
||||
"a dry camera keeps the first list"
|
||||
);
|
||||
assert!(
|
||||
!wave_armed(FfxState::Player, wet, 1.0),
|
||||
!wave_armed(FfxState::Player, wet, ghost_world()),
|
||||
"a ghost runs CFFXDeath, which owns one list and no guard"
|
||||
);
|
||||
assert!(
|
||||
!wave_armed(FfxState::Glue, wet, 0.0),
|
||||
!wave_armed(FfxState::Glue, wet, alive()),
|
||||
"the glue pair reaches the same Render, but 0x467d00 gates it out of the swap"
|
||||
);
|
||||
assert!(
|
||||
!wave_armed(FfxState::None, wet, 0.0),
|
||||
!wave_armed(FfxState::None, wet, alive()),
|
||||
"a bake runs neither pair"
|
||||
);
|
||||
}
|
||||
|
||||
/// The underwater, drunk world the glow pass does the most for.
|
||||
fn wet() -> FfxWave {
|
||||
FfxWave {
|
||||
phase1: 0.0,
|
||||
phase2: 0.0,
|
||||
active: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// `ffxGlow` 0: `CFFXGlow`'s enable (`0x6cc5a8`) fails, so the begin/end pair runs no pass. The
|
||||
/// combine is the scene alone, the three filter passes are skipped, the warp never arms, and
|
||||
/// the death pass, glue pair included, still runs.
|
||||
#[test]
|
||||
fn glow_off_drops_the_glow_pass_and_only_it() {
|
||||
let off = FfxSwitches {
|
||||
glow: false,
|
||||
..FfxSwitches::default()
|
||||
};
|
||||
let drunk = FfxPassState::world(0.0, 1.0).switched(off);
|
||||
let u = combine_uniform(
|
||||
0.5,
|
||||
drunk,
|
||||
living_glue().switched(off),
|
||||
&FfxGlow::WORLD,
|
||||
wet(),
|
||||
);
|
||||
assert_eq!(
|
||||
u[..3],
|
||||
[0.0, 0.0, 0.0],
|
||||
"no glow add, no haze, no death gate"
|
||||
);
|
||||
assert!(!blur_read(&u), "Box4 and both Gauss4 passes are skipped");
|
||||
assert!(
|
||||
!wave_armed(FfxState::Player, wet(), drunk),
|
||||
"the underwater warp is the glow pass's second list"
|
||||
);
|
||||
let glue = combine_uniform(
|
||||
0.5,
|
||||
drunk,
|
||||
living_glue().switched(off),
|
||||
&FfxGlow::GLUE_SCENE,
|
||||
wet(),
|
||||
);
|
||||
assert_eq!(
|
||||
glue[..3],
|
||||
[0.0, 0.0, 0.0],
|
||||
"the glue glow reads the same record"
|
||||
);
|
||||
let ghost = ghost_world().switched(off);
|
||||
assert_eq!(
|
||||
combine_uniform(0.5, ghost, living_glue(), &FfxGlow::WORLD, wet())[..3],
|
||||
[0.5, 1.0, 0.0],
|
||||
"a ghost's death pass does not read ffxGlow"
|
||||
);
|
||||
}
|
||||
|
||||
/// `ffxDeath` 0: the ghost's slot holds `CFFXDeath`, whose enable (`0x6cdf10`) fails, and the
|
||||
/// glow pass is not put back in its place, so the ghost sees the plain scene.
|
||||
#[test]
|
||||
fn death_off_drops_the_death_pass_without_restoring_the_glow() {
|
||||
let off = FfxSwitches {
|
||||
death: false,
|
||||
..FfxSwitches::default()
|
||||
};
|
||||
let u = combine_uniform(
|
||||
0.5,
|
||||
ghost_world().switched(off),
|
||||
living_glue().switched(off),
|
||||
&FfxGlow::WORLD,
|
||||
FfxWave::default(),
|
||||
);
|
||||
assert_eq!(u[..3], [0.0, 0.0, 0.0]);
|
||||
assert!(!blur_read(&u));
|
||||
assert_eq!(
|
||||
combine_uniform(
|
||||
0.5,
|
||||
alive().switched(off),
|
||||
living_glue(),
|
||||
&FfxGlow::WORLD,
|
||||
FfxWave::default()
|
||||
)[..3],
|
||||
[0.5, 0.0, 0.0],
|
||||
"the living keep their glow"
|
||||
);
|
||||
let glue_ghost = FfxPassState::glue(1.0).switched(off);
|
||||
assert_eq!(
|
||||
combine_uniform(0.5, alive(), glue_ghost, &FfxGlow::GLUE_SCENE, wet())[..3],
|
||||
[0.0, 0.0, 0.0],
|
||||
"a ghost roster row loses its death combine too"
|
||||
);
|
||||
}
|
||||
|
||||
/// `ffx` 0 (`0x6cd8a6`): neither pass runs, on either pair.
|
||||
#[test]
|
||||
fn the_master_off_drops_both_passes() {
|
||||
let off = FfxSwitches {
|
||||
master: false,
|
||||
..FfxSwitches::default()
|
||||
};
|
||||
for (feed, what) in [(alive(), "alive"), (ghost_world(), "a ghost")] {
|
||||
let feed = feed.switched(off);
|
||||
let u = combine_uniform(0.5, feed, living_glue(), &FfxGlow::WORLD, wet());
|
||||
assert_eq!(u[..3], [0.0, 0.0, 0.0], "{what}");
|
||||
assert!(!blur_read(&u), "{what}");
|
||||
assert!(!wave_armed(FfxState::Player, wet(), feed), "{what}");
|
||||
}
|
||||
let on = alive().switched(FfxSwitches::default());
|
||||
let u = combine_uniform(0.5, on, living_glue(), &FfxGlow::WORLD, wet());
|
||||
assert!(blur_read(&u), "all three on, the glow pass runs");
|
||||
assert!(wave_armed(FfxState::Player, wet(), on));
|
||||
}
|
||||
|
||||
/// Dry, the uniform's first row is the plain combine's and the node binds the dry pipeline.
|
||||
#[test]
|
||||
fn a_dry_frame_pays_nothing_for_the_warp() {
|
||||
@@ -1343,7 +1533,7 @@ mod tests {
|
||||
"the combine lane is exactly what it was before the wave lane existed"
|
||||
);
|
||||
assert!(
|
||||
!wave_armed(FfxState::Player, live, 0.0),
|
||||
!wave_armed(FfxState::Player, live, alive()),
|
||||
"and the node binds the unwarped pipeline"
|
||||
);
|
||||
}
|
||||
|
||||
+12
-12
@@ -109,7 +109,7 @@
|
||||
- **benilla-mpq** (lib.rs): crypto
|
||||
- **benilla-protocol** (lib.rs): auth events guid messages wire world
|
||||
- **benilla-srp** (lib.rs): vanilla_header
|
||||
- **benilla-ui** (lib.rs): bindings_xml civil framexml justify layout loader markup messages order script source strings toc widget
|
||||
- **benilla-ui** (lib.rs): bindings_xml civil framexml justify layout loader markup messages order script source status strings toc widget
|
||||
- **benilla-visual** (lib.rs): relight
|
||||
- **benilla-world** (lib.rs): art_scope assets bgwin billboard boot build_id clouds clutter collision decal dev_state doodad_anim entity_shade exterior_cull ffx_glow final_pass frame_pace ground_fx instance_tint interact interior lighting liquid log_ring mac_quit map_proj mat_anim_table mesh_tag model_fade model_forms model_render modkeys particles ribbons ride_frame rig_anim rig_palette rig_rider schedule shaders sky sky_order skybox static_gx static_merge straddle sun surface terrain_stream thread_qos view vis_chain water_fx wdl weather wmo_portal world_census world_map world_plugins world_point world_unit worldview zfill
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
- `WOW_AMBIGUITY_DUMP` — benilla-app/src/game_plugins.rs
|
||||
- `WOW_ANIM_COST` — benilla-app/src/creature_anim/driver.rs
|
||||
- `WOW_ANIM_PARK_ALL` — benilla-app/src/creature_anim/lod.rs
|
||||
- `WOW_ANISO` — benilla-app/src/cvars.rs, benilla-assets/src/tex_filter.rs
|
||||
- `WOW_ANISO` — benilla-app/src/cvars/mod.rs, benilla-assets/src/tex_filter.rs
|
||||
- `WOW_API_DUMP` — benilla-app/tests/world_api_wall.rs
|
||||
- `WOW_ARCH_CENSUS` — benilla-app/src/perf/mod.rs
|
||||
- `WOW_ART_RADIUS` — benilla-world/src/art_scope.rs
|
||||
@@ -174,7 +174,7 @@
|
||||
- `WOW_BUBBLE_TRACE` — benilla-app/src/chat_bubble.rs
|
||||
- `WOW_CAM_CHANGED` — benilla-app/src/perf/mod.rs
|
||||
- `WOW_CAM_DUMP` — benilla-app/src/player/camera.rs
|
||||
- `WOW_CAPTURE` — benilla-app/src/capture/mod.rs, benilla-app/src/capture/scenarios.rs, benilla-app/src/char_select/addons.rs, benilla-app/src/cvars.rs, benilla-app/src/lib.rs, benilla-app/src/local_state.rs, benilla-app/src/login/mod.rs, benilla-app/src/name_persist.rs, benilla-app/src/net.rs, benilla-app/src/player/camera_saved.rs, benilla-app/src/run_mode.rs, benilla-app/src/sound/mod.rs, benilla-app/src/ui_chat/settings.rs, benilla-app/src/ui_chat/tests.rs, benilla-app/src/ui_macro/tests.rs, benilla-app/src/ui_saved.rs, benilla-app/src/ui_script/addons.rs, benilla-app/src/ui_script/layer_tests.rs, benilla-app/src/ui_script/mod.rs, benilla-app/src/ui_script/world_entry_tests.rs, benilla-app/src/video.rs, benilla-world/src/bgwin.rs, benilla-world/src/clouds/mod.rs, benilla-world/src/dev_state.rs
|
||||
- `WOW_CAPTURE` — benilla-app/src/capture/mod.rs, benilla-app/src/capture/scenarios.rs, benilla-app/src/char_select/addons.rs, benilla-app/src/cvars/mod.rs, benilla-app/src/lib.rs, benilla-app/src/local_state.rs, benilla-app/src/login/mod.rs, benilla-app/src/name_persist.rs, benilla-app/src/net.rs, benilla-app/src/player/camera_saved.rs, benilla-app/src/run_mode.rs, benilla-app/src/sound/mod.rs, benilla-app/src/ui_chat/settings.rs, benilla-app/src/ui_chat/tests.rs, benilla-app/src/ui_macro/tests.rs, benilla-app/src/ui_saved.rs, benilla-app/src/ui_script/addons.rs, benilla-app/src/ui_script/layer_tests.rs, benilla-app/src/ui_script/mod.rs, benilla-app/src/ui_script/world_entry_tests.rs, benilla-app/src/video.rs, benilla-world/src/bgwin.rs, benilla-world/src/clouds/mod.rs, benilla-world/src/dev_state.rs
|
||||
- `WOW_CAPTURE_AGE` — benilla-app/src/capture/mod.rs
|
||||
- `WOW_CAPTURE_DEADLINE` — benilla-app/src/capture/mod.rs
|
||||
- `WOW_CAPTURE_OUT` — benilla-app/src/capture/mod.rs
|
||||
@@ -182,7 +182,7 @@
|
||||
- `WOW_CAPTURE_STABLE` — benilla-app/src/capture/mod.rs
|
||||
- `WOW_CAPTURE_UI` — benilla-app/src/capture/scenarios.rs, benilla-app/src/ui_script/mod.rs, benilla-app/src/video.rs
|
||||
- `WOW_CAST_TRACE` — benilla-app/src/net.rs
|
||||
- `WOW_CHAR` — benilla-app/src/capture/probe_mail.rs, benilla-app/src/char_select/mod.rs, benilla-app/src/cvars.rs, benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/realm_select/smoke.rs, benilla-app/src/run_mode.rs
|
||||
- `WOW_CHAR` — benilla-app/src/capture/probe_mail.rs, benilla-app/src/char_select/mod.rs, benilla-app/src/cvars/mod.rs, benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/realm_select/smoke.rs, benilla-app/src/run_mode.rs
|
||||
- `WOW_CHARCREATE_DIALS` — benilla-app/src/char_create/mod.rs
|
||||
- `WOW_CHARCREATE_NAME` — benilla-app/src/char_create/mod.rs
|
||||
- `WOW_CHARCREATE_PICK` — benilla-app/src/capture/mod.rs, benilla-app/src/char_create/mod.rs
|
||||
@@ -190,7 +190,7 @@
|
||||
- `WOW_CHARCREATE_SHOT_OUT` — benilla-app/src/char_create/mod.rs
|
||||
- `WOW_CHARSELECT_ADDONS` — benilla-app/src/char_select/mod.rs
|
||||
- `WOW_CHARSELECT_DIALOG` — benilla-app/src/char_select/mod.rs
|
||||
- `WOW_CHARSELECT_PICK` — benilla-app/src/char_select/mod.rs, benilla-app/src/cvars.rs
|
||||
- `WOW_CHARSELECT_PICK` — benilla-app/src/char_select/mod.rs, benilla-app/src/cvars/mod.rs
|
||||
- `WOW_CHARSELECT_SHOT` — benilla-world/src/bgwin.rs
|
||||
- `WOW_CHARSELECT_SHOT_AT` — benilla-app/src/char_select/mod.rs
|
||||
- `WOW_CHARSELECT_SHOT_OUT` — benilla-app/src/char_select/mod.rs
|
||||
@@ -202,7 +202,7 @@
|
||||
- `WOW_CLOUD_DUMP` — benilla-world/src/clouds/mod.rs
|
||||
- `WOW_CLUSTERS` — benilla-app/src/player/setup.rs
|
||||
- `WOW_CLUTTER_ALPHA` — benilla-world/src/clutter.rs
|
||||
- `WOW_CLUTTER_DENSITY` — benilla-app/src/cvars.rs, benilla-app/src/lib.rs, benilla-world/src/clutter.rs
|
||||
- `WOW_CLUTTER_DENSITY` — benilla-app/src/cvars/mod.rs, benilla-app/src/lib.rs, benilla-world/src/clutter.rs
|
||||
- `WOW_CLUTTER_FADE` — benilla-world/src/clutter.rs
|
||||
- `WOW_COLUMN_COST` — benilla-assets/src/column_grid.rs
|
||||
- `WOW_CPU_CENSUS` — benilla-app/src/perf/census.rs
|
||||
@@ -229,7 +229,7 @@
|
||||
- `WOW_EMIT_DUMP` — benilla-world/src/particles/emitdump.rs
|
||||
- `WOW_ENTITY_CENSUS` — benilla-app/src/capture/probes/world_census.rs, benilla-app/src/dev.rs
|
||||
- `WOW_FADE_NEAR` — benilla-world/src/terrain_stream/spawn/mod.rs
|
||||
- `WOW_FARCLIP` — benilla-app/src/cvars.rs, benilla-world/src/view.rs
|
||||
- `WOW_FARCLIP` — benilla-app/src/cvars/mod.rs, benilla-world/src/view.rs
|
||||
- `WOW_FEED_COST` — benilla-app/src/ui_items/feed.rs
|
||||
- `WOW_FEED_GATE_CHECK` — benilla-app/src/ui_script/gate.rs, benilla-world/src/bgwin.rs
|
||||
- `WOW_FEED_GATE_TRACE` — benilla-app/src/ui_script/gate.rs, benilla-world/src/bgwin.rs
|
||||
@@ -274,7 +274,7 @@
|
||||
- `WOW_GX_FADE_TRACE` — benilla-world/src/static_gx/cull.rs
|
||||
- `WOW_GX_PERF` — benilla-world/src/static_gx/mod.rs
|
||||
- `WOW_HIT_COST` — benilla-app/src/ui_script/input.rs
|
||||
- `WOW_HOST` — benilla-app/src/cvars.rs, benilla-app/src/realmlist.rs
|
||||
- `WOW_HOST` — benilla-app/src/cvars/mod.rs, benilla-app/src/realmlist.rs
|
||||
- `WOW_HOVER_LOG` — benilla-app/src/hover_log.rs
|
||||
- `WOW_HOVER_LOG_REPORT` — benilla-app/src/lib.rs
|
||||
- `WOW_HOVER_PROBE` — benilla-app/src/target/hover_probe.rs
|
||||
@@ -325,7 +325,7 @@
|
||||
- `WOW_MOVE_SPEED` — benilla-app/src/player/setup.rs
|
||||
- `WOW_MOVE_TRACE` — benilla-assets/src/trace.rs
|
||||
- `WOW_MOVE_TRACE_TAGS` — benilla-assets/src/trace.rs
|
||||
- `WOW_MSAA` — benilla-app/src/cvars.rs, benilla-world/src/view.rs
|
||||
- `WOW_MSAA` — benilla-app/src/cvars/mod.rs, benilla-world/src/view.rs
|
||||
- `WOW_MT_EXTRACT` — benilla-app/src/lib.rs
|
||||
- `WOW_MT_POSTUPDATE` — benilla-app/src/lib.rs
|
||||
- `WOW_MT_UPDATE` — benilla-app/src/lib.rs
|
||||
@@ -444,7 +444,7 @@
|
||||
- `WOW_REMOTE_FLAT` — benilla-app/src/net/motion/remote.rs
|
||||
- `WOW_REMOTE_IDLE_GATE` — benilla-app/src/net/motion/remote.rs
|
||||
- `WOW_REMOTE_SNAP` — benilla-app/src/net/motion/remote.rs
|
||||
- `WOW_RENDER_SCALE` — benilla-app/src/cvars.rs, benilla-app/src/world_backdrop.rs
|
||||
- `WOW_RENDER_SCALE` — benilla-app/src/cvars/mod.rs, benilla-app/src/world_backdrop.rs
|
||||
- `WOW_RES_CENSUS` — benilla-app/src/perf/census.rs
|
||||
- `WOW_RESIZE` — benilla-app/src/capture/mod.rs
|
||||
- `WOW_REVEAL` — benilla-app/src/capture/probes/reveal.rs, benilla-app/src/dev.rs
|
||||
@@ -479,7 +479,7 @@
|
||||
- `WOW_TILE_UNLOAD` — benilla-world/src/assets/mod.rs
|
||||
- `WOW_TINT_PROBE` — benilla-app/src/aura_visual.rs
|
||||
- `WOW_TRAIL_CENSUS` — benilla-app/src/capture/probes/trail_census.rs, benilla-app/src/dev.rs
|
||||
- `WOW_TRILINEAR` — benilla-app/src/cvars.rs, benilla-assets/src/tex_filter.rs
|
||||
- `WOW_TRILINEAR` — benilla-app/src/cvars/mod.rs, benilla-assets/src/tex_filter.rs
|
||||
- `WOW_TWIST_GAP` — benilla-app/src/player/body_pose.rs
|
||||
- `WOW_UI_COST` — benilla-app/src/ui_script/extract/mod.rs
|
||||
- `WOW_UI_DIFF` — benilla-app/src/ui_pass.rs, benilla-app/src/ui_script/extract/mod.rs
|
||||
@@ -487,7 +487,7 @@
|
||||
- `WOW_UI_HANDLERS` — benilla-ui/src/script/handler_prof.rs
|
||||
- `WOW_UI_PICK` — benilla-app/src/ui_script/extract/mod.rs
|
||||
- `WOW_UI_PROBE` — benilla-app/src/ui_pass.rs
|
||||
- `WOW_UI_SCALE` — benilla-app/src/cvars.rs, benilla-app/src/ui_script/mod.rs
|
||||
- `WOW_UI_SCALE` — benilla-app/src/cvars/mod.rs, benilla-app/src/ui_script/mod.rs
|
||||
- `WOW_UI_SPLICE_VERIFY` — benilla-app/src/ui_script/extract/mod.rs
|
||||
- `WOW_UISCRIPT_DUMP` — benilla-app/tests/ui_script_wall.rs
|
||||
- `WOW_UNATTENDED` — benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/run_mode.rs, benilla-world/src/bgwin.rs
|
||||
|
||||
Reference in New Issue
Block a user