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
+16 -17
View File
@@ -238,7 +238,7 @@ pub(crate) fn arm_entry_ui_load(world: &mut World) {
/// phase can write CVars — the character screen's AddOns panel is the live case, a *Load out of
/// date AddOns* click — and `sync_cvars` (1291) registers them onto whichever VM is in the world,
/// so this is the last moment that table exists. Folded into the persist state, the
/// `set_cvar_saved_base` + `register_cvars` pair in the load below starts the new VM at the
/// `set_cvar_saved_base` + `seed_cvars` pair in the load below starts the new VM at the
/// player's value. That is exactly the route `/reload` has always taken through
/// [`end_ui_session`]; 2226 makes the first login take it too.
///
@@ -483,15 +483,19 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
// gets a turn — so this edge would load a client with no CVar table at all. Nothing had ever
// noticed, because until now no interface file read a CVar at load.
//
// The two lines are `sync_cvars`'s own first two, in its own order, and the order is
// load-bearing: the saved config goes in FIRST so registration starts each key at the player's
// value rather than the factory one (1291) — reversed, a reload would quietly reset every
// knobless CVar to its default. `sync_cvars` still does its full knob-derived pass on the next
// `Update`; both calls are idempotent, and a re-register never clobbers a live value.
if let Some(persist) = world.get_resource::<crate::cvars::CvarPersist>() {
script.set_cvar_saved_base(persist.saved_base());
// The two lines are `sync_cvars`'s own seed, in its own order, and the order is
// load-bearing: the file's unclaimed entries go in FIRST so an addon's `RegisterCVar` starts
// its key at the player's value rather than the declared one (1291); then the registry's
// whole table at its live values (2303) — the store that outlived the last VM. `sync_cvars`
// still runs its own per-VM seed on the next `Update`; both are idempotent. A world with no
// registry (a bare test app) gets the registered defaults.
match world.get_resource::<crate::cvars::Cvars>() {
Some(cvars) => {
script.set_cvar_saved_base(cvars.orphans());
script.seed_cvars(cvars.vm_seed());
}
None => script.register_cvars(crate::cvars::registered_pairs()),
}
script.register_cvars(crate::cvars::registered_pairs());
// The realm name goes in BEFORE the UI loads, because `GetRealmName()` is read at addon file
// scope — `MyAddonDB[GetRealmName()] = …` is the corpus idiom, and 24 addons stop on it
// (decision 1195). The roster carries the auth realm-list entry this session connected to.
@@ -531,14 +535,9 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
// character select reached here inside the save debounce; the fold carries that click now, by
// the route the reload path always used. Absent both (a bare test world) is the registrar
// default: check ON.
let version_check = script
.cvar("checkAddonVersion")
.map(|v| v != "0")
.unwrap_or_else(|| {
world
.get_resource::<crate::cvars::CvarPersist>()
.is_none_or(crate::cvars::CvarPersist::addon_version_check)
});
let version_check = world
.get_resource::<crate::cvars::Cvars>()
.is_none_or(crate::cvars::Cvars::addon_version_check);
// **…and the rest of the same class** (decision 2241). Each of these was a per-VM claim in
// `Update`, which answers *which* VM and not *when inside its life* — and every one of them
// backs a Lua getter the load burst below reads:
+63 -11
View File
@@ -281,13 +281,31 @@ pub(crate) struct UiPassState {
#[derive(Resource, Default)]
pub(crate) struct PointerOverUiPanel(pub(crate) bool);
/// The player-UI FEED phase — every push into the VM that this frame's tick must see: the
/// snapshot feeds, the one-shot drains that `fire_event`, the per-VM seeds. Chained
/// `UiFeed` → [`UiInput`] → [`UiPaint`] and ordered after [`WorldStage::Net`] (decision 2304), so
/// a feed reads what this frame's packets did, and a handler its event fires reads what every
/// other feed pushed — the reference's own frame, where packet dispatch precedes every
/// `FrameScript_SignalEvent` and both precede `OnUpdate`. The failure the order closes was found
/// in the unit feed first ([`crate::ui_unit::UnitFeed`]'s doc): unordered, `apply_net_updates`
/// could land between two feeds, and a synchronous handler fired by the later one re-read the
/// earlier one's pre-mutation push. **No run condition on the set** — whether a feed may run
/// before the in-game interface exists is a judgement per feed (2232), and the ones that may not
/// ride `UnitFeed`, a gated sub-phase of this one. Every system holding the VM in `Update`
/// declares its side of the tick — this set, or `.after(UiInput)` — and
/// `game_plugins::schedule_tests` holds it on the built schedule.
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct UiFeed;
/// The player-UI PAINT pass — the quad walk ([`extract::paint_script`]), ordered after the world's
/// camera update so that world-anchored widgets are drawn from this frame's camera (decision 2168).
/// The last of the three UI phases (see [`UiFeed`]).
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct UiPaint;
/// The player-UI input pass — hit-testing + handler firing. Ordered before [`WorldStage::Input`] so
/// the [`PlayerUiHover`] it produces is folded into `PointerOverUi` before `player::control` reads it.
/// The player-UI input pass — the tick, hit-testing + handler firing. Ordered before
/// [`WorldStage::Input`] so the [`PlayerUiHover`] it produces is folded into `PointerOverUi` before
/// `player::control` reads it; the middle of the three UI phases (see [`UiFeed`]).
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct UiInput;
@@ -403,8 +421,31 @@ pub(crate) fn seam_scale(window_h: f32, ui_scale: f32) -> f32 {
/// tick → resolve → extract pipeline into [`UiQuads`], plus the input pass ([`feed_ui_input`]).
pub(crate) struct UiScriptPlugin;
/// `uiScale`'s change callback (decision 2303): the dial's own `[0.5, 1.5]` clamp.
pub(crate) fn on_cvar(ev: On<crate::cvars::CvarChanged>, mut scale: ResMut<UiScaleCvar>) {
if ev.is("uiScale") {
scale.0 = ev.num().clamp(0.5, 1.5);
}
}
/// `/console reloadUI` — the reference's own console command, and the same deferred rebuild
/// `ReloadUI()` and `/reload` queue (decision 1291): through the session seam, run by
/// [`run_pending_reload`] at the top of the next frame.
fn console_reload_ui(world: &mut World, _args: &str) -> Vec<String> {
match world.get_non_send_resource_mut::<UiScript>() {
Some(mut script) => {
script.queue_session_request(benilla_ui::script::SessionRequest::ReloadUi);
Vec::new()
}
None => vec!["reloadUI: no interface to reload".to_string()],
}
}
impl Plugin for UiScriptPlugin {
fn build(&self, app: &mut App) {
use crate::console::ConsoleCommandApp;
app.add_observer(on_cvar);
app.console_command("reloadUI", "Reload the interface.", console_reload_ui);
// **The quit root of the shutdown tail, on the exit edge** (decision 1528). It was
// `.add_systems(Update, shutdown_on_exit)` below, which cannot see the `AppExit` a player
// produces — the close button's is written in `PostUpdate` — so quitting from in-world
@@ -474,14 +515,26 @@ impl Plugin for UiScriptPlugin {
PreUpdate,
(run_pending_reload, lifecycle::run_pending_entry_load).chain(),
)
// `GetFramerate()`'s host half (decision 1195), before the VM ticks so an `OnUpdate`
// **The three UI phases, in order** (decision 2304): every push after the net
// drain, then the tick, then the paint. `UiFeed`'s own doc has the why; `UiInput`
// sits before the world's input stage so the hover it produces reaches the pointer
// arbiter in time (its doc).
.configure_sets(
Update,
(
UiFeed.after(WorldStage::Net),
UiInput.before(WorldStage::Input),
UiPaint,
)
.chain(),
)
// `GetFramerate()`'s host half (decision 1195), in the feed phase so an `OnUpdate`
// handler reads this frame's number rather than the previous one's.
.add_systems(Update, feed_framerate.before(UiInput))
.add_systems(Update, feed_framerate.in_set(UiFeed))
// `tick_script` resolves layout; `feed_ui_input` hit-tests against those rects, so they
// chain (also required because both take the single `NonSend` VM). The pair runs before
// `WorldStage::Input` so the hover result reaches the pointer arbiter in time. The input
// pass is in-world only (decision 0193): the character-select glue screen owns the
// pointer + keyboard there (its exit edge resets the latches this pass normally drives).
// chain (also required because both take the single `NonSend` VM). The input pass is
// in-world only (decision 0193): the character-select glue screen owns the pointer +
// keyboard there (its exit edge resets the latches this pass normally drives).
.init_resource::<UiPassState>()
.init_resource::<PointerOverUiPanel>()
.add_systems(
@@ -495,8 +548,7 @@ impl Plugin for UiScriptPlugin {
// The binding dispatch (0997) runs in this same set, after the feed: it
// reads the capture gate the feed just wrote, so a key a focused box
// consumed this frame never also fires a binding.
.before(crate::bindings::BindingSet)
.before(WorldStage::Input),
.before(crate::bindings::BindingSet),
)
// **The paint half, after the camera** (decision 2168): the quad walk runs once the
// world's own update has happened, so a widget anchored to the world — the nameplates,
@@ -527,8 +579,8 @@ impl Plugin for UiScriptPlugin {
app.add_systems(
Update,
demo_unit_feed
.in_set(UiFeed)
.after(UnitFeed)
.before(UiInput)
.run_if(capture_ui_active),
);
}