settings live in one registry, and the net drain starts to peel

- the CVars are one engine-side table that outlives every interface reload: a change fires its observer, the eight latched rows wait for RestartGx, and /console gains set, cvar_reset, cvar_default, cvarlist and help (2303)
- every push into the interface runs in one feed phase after the net drain and before the tick, held by a structural test on the built schedule (2304)
- subsystems own their packet handlers, dispatched in packet order; the auction house is the first family out of the 273-arm drain (2305)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-17 16:47:09 +07:00
co-authored by Claude Fable 5.1
parent 6019703a14
commit aa207b4050
117 changed files with 3844 additions and 2067 deletions
+14 -8
View File
@@ -138,9 +138,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// ConsoleExec("name value") — the console line `/console` hands over. A registered CVar is
// written the way `SetCVar` writes it (same store, same change queue, no CVAR_UPDATE token);
// anything else is a console command the app owns.
// ConsoleExec("name value") — the console line `/console` hands over. A registered CVar
// WITH a value is written the way `SetCVar` writes it (same store, same change queue, no
// CVAR_UPDATE token), synchronously, so a script reading it back on the next line sees the
// write. Everything else — a command name, or a bare CVar name, which the reference's
// per-CVar console command answers with `CVar "%s" is "%s"` (`0x63dde0`) — is a line for
// the host's command registry (decision 2303).
g.set(
"ConsoleExec",
lua.create_function(|lua, line: Option<String>| {
@@ -152,10 +155,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
.map_or((line.as_str(), ""), |(n, v)| (n, v.trim()));
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let is_cvar = model.cvars.contains_key(&name.to_ascii_lowercase());
if is_cvar {
if !value.is_empty() {
super::cvars::write_cvar(&mut model, name, value.to_string(), None);
}
if is_cvar && !value.is_empty() {
super::cvars::write_cvar(&mut model, name, value.to_string(), None);
} else {
model.console_lines.push(line.clone());
}
@@ -251,7 +252,12 @@ mod tests {
"0.8",
"the CVar store is the same one SetCVar writes, matched case-insensitively"
);
assert_eq!(s.take_console_lines(), vec!["reloadui".to_string()]);
// A bare CVar name is the host's to answer (2303: the reference prints its value from
// the per-CVar console command), so it rides the command lane like `reloadui`.
assert_eq!(
s.take_console_lines(),
vec!["reloadui".to_string(), "uiScale".to_string()]
);
assert_eq!(
s.take_cvar_changes(),
vec![("uiScale".to_string(), "0.8".to_string())],
+229 -35
View File
@@ -36,6 +36,26 @@ pub(crate) struct CvarSlot {
pub name: String,
pub value: String,
pub default: String,
/// The reference's flag bit1 (`rec+0x1c & 0x2`, decision 2303): a write lands in
/// [`Self::pending`] instead of [`Self::value`], so `GetCVar` keeps answering the applied
/// value until the host commits the latch (`CVar::Update 0x63e060` — for the `gx*` rows,
/// inside `RestartGx`). Seeded by the host ([`super::UiScript::seed_cvars`]); a row the VM
/// registers on its own is never latched, exactly like the console `set` command's create
/// path in the reference.
pub latched: bool,
/// The staged value of a latched row — what the host will commit at the boundary, and what
/// it is told about through the change queue the moment it is written.
pub pending: Option<String>,
}
/// One row of the host's registry, as the VM's mirror is seeded from it (decision 2303): the
/// registered spelling, the **applied** value, the registered default, and the latch flag.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SeededCvar {
pub name: String,
pub value: String,
pub default: String,
pub latched: bool,
}
/// One device-supported multisample format, as the Video options dropdown shows it.
@@ -173,6 +193,8 @@ impl super::UiScript {
name: name.to_string(),
value,
default: default.to_string(),
latched: false,
pending: None,
},
);
}
@@ -180,14 +202,44 @@ impl super::UiScript {
}
}
/// Host-side write (loaded config, env override): sets the value WITHOUT queueing a change
/// event — the host is the caller, an echo would re-dirty the file it just read. Unknown
/// names warn once, same posture as the Lua side.
/// Seed the mirror from the host's registry (decision 2303): every row's spelling, applied
/// value, default and latch flag, creating or overwriting the slot. This is what a fresh VM
/// gets at claim time and at the world-entry edge — the host's table is the store that
/// outlives the VM, so the mirror starts wherever it stands, and a re-seed of a live table
/// is the host re-asserting its truth (a staged value is dropped with it: the host holds
/// the pending copy and pushes it back through the outbox if it still stands).
///
/// `&self` for [`Self::register_cvars`]'s reason — the interface loader seeds from a
/// `&UiScript` before the stock files read their first CVar (decision 2115).
pub fn seed_cvars(&self, rows: impl IntoIterator<Item = SeededCvar>) {
let mut model = self.model_mut();
for row in rows {
let key = row.name.to_ascii_lowercase();
model.cvars.insert(
key,
CvarSlot {
name: row.name,
value: row.value,
default: row.default,
latched: row.latched,
pending: None,
},
);
}
}
/// Host-side write (a loaded value, a committed latch, the host registry's outbox): sets
/// the value WITHOUT queueing a change event — the host is the caller, an echo would
/// re-dirty the file it just read — and clears a staged value, because a host write IS the
/// commit. Unknown names warn once, same posture as the Lua side.
pub fn set_cvar_host(&mut self, name: &str, value: &str) {
let mut model = self.model_mut();
let key = name.to_ascii_lowercase();
match model.cvars.get_mut(&key) {
Some(slot) => slot.value = value.to_string(),
Some(slot) => {
slot.value = value.to_string();
slot.pending = None;
}
None => warn_unknown(&mut model, name),
}
}
@@ -217,6 +269,14 @@ impl super::UiScript {
std::mem::take(&mut self.model_mut().cvar_changes)
}
/// Drain the `(name, default)` rows an addon's `RegisterCVar` created since the last call
/// (decision 2303) — the host's cue to give each one a row in its own registry, which is the
/// store that survives this VM. In the reference an addon's registration lands in the same
/// engine-side table as the client's own; this is how ours does.
pub fn take_cvar_registrations(&mut self) -> Vec<(String, String)> {
std::mem::take(&mut self.model_mut().cvar_registrations)
}
/// A native surface's write — [`set_from_engine`]'s public face: sets the value AND queues
/// the change like a Lua `SetCVar`, so the host's sync dirties the config file. The glue
/// AddOns screen's *Load out of date AddOns* checkbox is the caller (decision 1293): a
@@ -297,15 +357,38 @@ impl super::UiScript {
/// code, not UI content, so a miss means this build's host doesn't back the var (a bare test VM) —
/// and the registered set is welded to the code truths by `crate::cvars`'s own test.
pub(super) fn set_from_engine(model: &mut Model, name: &str, value: String) {
let Some(slot) = model.cvars.get_mut(&name.to_ascii_lowercase()) else {
return;
};
if slot.value == value {
return;
store(model, &name.to_ascii_lowercase(), value);
}
/// **The one store every write goes through** — Lua's `SetCVar`, `ConsoleExec`, and the engine
/// writes above. Returns the registered spelling when the write moved something (and queued it
/// for the host), `None` for an unknown key or a write that changed nothing.
///
/// A **latched** slot (decision 2303) takes the write as its staged value and leaves `value`
/// alone, which is the reference's `Set 0x63df50`: `latchedValue` gets the string, `InternalSet`
/// does not run, and `GetCVar` keeps answering what is applied. Writing a latched slot back to
/// its applied value clears the stage instead of staging a no-op. Either way the host hears it
/// through the change queue — it holds the pending copy and commits it at the boundary.
fn store(model: &mut Model, key: &str, value: String) -> Option<String> {
let slot = model.cvars.get_mut(key)?;
if slot.latched {
let staged = (value != slot.value).then_some(value.clone());
if slot.pending == staged && staged.is_some() {
return None;
}
if staged.is_none() && slot.pending.is_none() {
return None;
}
slot.pending = staged;
} else {
if slot.value == value {
return None;
}
slot.value = value.clone();
}
slot.value = value.clone();
let registered = slot.name.clone();
model.cvar_changes.push((registered, value));
model.cvar_changes.push((registered.clone(), value));
Some(registered)
}
/// Push the warn-once for an unknown CVar name into the model's warning stream.
@@ -319,26 +402,19 @@ pub(super) fn write_cvar(
token: Option<String>,
) -> bool {
let key = name.to_ascii_lowercase();
match model.cvars.get_mut(&key) {
Some(slot) => {
if slot.value != value {
slot.value = value.clone();
let reg_name = slot.name.clone();
model.cvar_changes.push((reg_name, value.clone()));
if let Some(token) = token {
model.pending_events.push((
"CVAR_UPDATE".to_string(),
vec![ScriptValue::Str(token), ScriptValue::Str(value)],
));
}
}
true
}
None => {
warn_unknown(model, name);
false
if !model.cvars.contains_key(&key) {
warn_unknown(model, name);
return false;
}
if store(model, &key, value.clone()).is_some() {
if let Some(token) = token {
model.pending_events.push((
"CVAR_UPDATE".to_string(),
vec![ScriptValue::Str(token), ScriptValue::Str(value)],
));
}
}
true
}
fn warn_unknown(model: &mut Model, name: &str) {
@@ -395,11 +471,22 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// saved value survives the VM being replaced — the declared value stays the DEFAULT,
// so the saver still knows what "moved off default" means for this key.
let saved = model.cvars_saved_base.get(&key).cloned();
model.cvars.entry(key).or_insert(CvarSlot {
name,
value: saved.unwrap_or_else(|| value.clone()),
default: value,
});
if model.cvars.contains_key(&key) {
return Ok(());
}
// The host learns the row through its own queue (decision 2303): an addon-declared
// CVar gets a row in the host's registry, which is the store that survives this VM.
model.cvar_registrations.push((name.clone(), value.clone()));
model.cvars.insert(
key,
CvarSlot {
name,
value: saved.unwrap_or_else(|| value.clone()),
default: value,
latched: false,
pending: None,
},
);
Ok(())
})?,
)?;
@@ -1133,7 +1220,7 @@ fn install_nameplate_verbs(lua: &Lua) -> mlua::Result<()> {
#[cfg(test)]
mod tests {
use super::MultisampleFormat;
use super::{MultisampleFormat, SeededCvar};
use crate::script::UiScript;
fn script_with_volume() -> UiScript {
@@ -1170,6 +1257,113 @@ mod tests {
assert!(s.take_cvar_changes().is_empty());
}
/// **A latched row stages the write and keeps answering the applied value** (decision 2303)
/// — the reference's `Set 0x63df50` on flag bit1: `latchedValue` takes the string, `InternalSet`
/// never runs, so `GetCVar` (which reads `rec+0x20`) answers the old value until
/// `CVar::Update 0x63e060` commits it. The host still hears every staged write through the
/// change queue (it holds the pending copy), and its commit arrives as a host write, which
/// clears the stage. Writing the applied value back over a stage clears the stage too.
#[test]
fn a_latched_row_stages_the_write_until_the_host_commits_it() {
let mut s = UiScript::new().unwrap();
s.seed_cvars([
SeededCvar {
name: "gxVSync".into(),
value: "1".into(),
default: "1".into(),
latched: true,
},
SeededCvar {
name: "MusicVolume".into(),
value: "0.4".into(),
default: "0.4".into(),
latched: false,
},
]);
s.run(r#"SetCVar("gxVSync", 0)"#).unwrap();
assert_eq!(
s.eval::<String>(r#"return GetCVar("gxVSync")"#).unwrap(),
"1",
"the applied value stands until the boundary"
);
assert_eq!(
s.take_cvar_changes(),
vec![("gxVSync".to_string(), "0".to_string())],
"the host hears the staged write"
);
// Staging the same value again is quiet; staging the applied value back clears the stage
// and is reported, so the host can drop its pending copy too.
s.run(r#"SetCVar("gxVSync", "0")"#).unwrap();
assert!(s.take_cvar_changes().is_empty());
s.run(r#"SetCVar("gxVSync", "1")"#).unwrap();
assert_eq!(
s.take_cvar_changes(),
vec![("gxVSync".to_string(), "1".to_string())]
);
assert!(
s.take_cvar_changes().is_empty(),
"nothing staged, nothing to say"
);
// The commit is a host write: the value moves, the stage clears, no echo.
s.run(r#"SetCVar("gxVSync", 0)"#).unwrap();
s.take_cvar_changes();
s.set_cvar_host("gxVSync", "0");
assert_eq!(
s.eval::<String>(r#"return GetCVar("gxVSync")"#).unwrap(),
"0"
);
assert!(s.take_cvar_changes().is_empty());
// An unlatched sibling is untouched by any of this.
s.run(r#"SetCVar("MusicVolume", 0.7)"#).unwrap();
assert_eq!(
s.eval::<String>(r#"return GetCVar("MusicVolume")"#)
.unwrap(),
"0.7"
);
}
/// **An addon's `RegisterCVar` reaches the host** (decision 2303): the row it creates is
/// reported once, with the declared default, so the host's registry — the store that
/// outlives this VM — can carry it. A re-declaration of a live name is the no-op it always
/// was, and reports nothing.
#[test]
fn an_addon_registration_is_reported_to_the_host_once() {
let mut s = UiScript::new().unwrap();
s.run(r#"RegisterCVar("myAddonKnob", "7")"#).unwrap();
s.run(r#"RegisterCVar("myAddonKnob", "9")"#).unwrap();
assert_eq!(
s.take_cvar_registrations(),
vec![("myAddonKnob".to_string(), "7".to_string())]
);
assert!(s.take_cvar_registrations().is_empty(), "drained");
assert_eq!(
s.eval::<String>(r#"return GetCVar("myAddonKnob")"#)
.unwrap(),
"7"
);
}
/// **A bare CVar name through `ConsoleExec` is the host's to answer** (decision 2303): the
/// reference's per-CVar console command prints `CVar "%s" is "%s"` on an empty argument
/// (`0x63dde0`), and that printing lives host-side with the rest of the command registry.
/// A name WITH a value is still written synchronously, so the next Lua line reads it back.
#[test]
fn console_exec_writes_a_valued_cvar_and_hands_a_bare_name_to_the_host() {
let mut s = script_with_volume();
s.run(r#"ConsoleExec("MusicVolume 0.2")"#).unwrap();
assert_eq!(
s.eval::<String>(r#"return GetCVar("MusicVolume")"#)
.unwrap(),
"0.2"
);
assert!(
s.take_console_lines().is_empty(),
"a valued write is consumed here"
);
s.run(r#"ConsoleExec("MusicVolume")"#).unwrap();
assert_eq!(s.take_console_lines(), vec!["MusicVolume".to_string()]);
}
/// **`SetCVar`'s third argument is the `CVAR_UPDATE` token** (decision 1140) — the whole
/// mechanism behind that event. 1.12's options panels pass their CheckButtons table KEY
/// (`SetCVar(value.cvar, value.value, index)`), an uppercase display name, and the engine
+1 -1
View File
@@ -201,7 +201,7 @@ pub use cursor::{
CursorPetAction, CursorSpell, CursorStablePet, EnchantConfirm, WorldPick, EQUIPMENT_BAG,
};
pub use cvars::{
MultisampleFormat, ScreenResolution, VideoCaps, CVAR_FRILL_DENSITY, CVAR_GAMMA,
MultisampleFormat, ScreenResolution, SeededCvar, VideoCaps, CVAR_FRILL_DENSITY, CVAR_GAMMA,
CVAR_NAMEPLATE_ENEMIES, CVAR_NAMEPLATE_FRIENDS, CVAR_WORLD_DETAIL, VIDEO_DEFAULT_CVARS,
WORLD_DETAIL_STOPS,
};
+4
View File
@@ -789,6 +789,9 @@ pub(crate) struct Model {
/// `(registered name, new value)` per Lua `SetCVar` since the app's last
/// [`super::UiScript::take_cvar_changes`] drain — the knob-sync + config-dirty cue.
pub(crate) cvar_changes: Vec<(String, String)>,
/// `(name, default)` per addon `RegisterCVar` that created a slot, since the host's last
/// [`super::UiScript::take_cvar_registrations`] drain (decision 2303).
pub(crate) cvar_registrations: Vec<(String, String)>,
/// Unknown CVar names already warned about (warn-once, the era-atlas-miss posture).
pub(crate) cvars_warned: HashSet<String>,
@@ -2020,6 +2023,7 @@ impl Model {
cvars: HashMap::new(),
cvars_saved_base: HashMap::new(),
cvar_changes: Vec::new(),
cvar_registrations: Vec::new(),
cvars_warned: HashSet::new(),
multisample_formats: Vec::new(),
screen_resolutions: Vec::new(),