Files
benilla-octo/crates/benilla-app/src/console.rs
T
SamandClaude Fable 5.1 edec5b2207 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>
2026-09-17 16:47:09 +07:00

413 lines
15 KiB
Rust

//! The in-game **console command registry** (decision 2303) — the reference's `ConsoleCommand`
//! table, host side: `ConsoleCommandRegister 0x63f9e0` over a `TSExplicitList<CONSOLECOMMAND>`,
//! with parse / register / lookup / execute (wow-re `console/console.md`, the upper band). A
//! subsystem registers its commands from its own plugin ([`ConsoleCommandApp::console_command`]),
//! the way each of the reference's registers into the one table, and `/console <line>` reaches
//! [`execute`] through the chat drain.
//!
//! **A CVar is a command too.** The reference's `CVar::Register` installs a per-CVar command
//! under the CVar's own name (`0x63dde0`): a bare name prints `CVar "%s" is "%s"`, a name with a
//! value sets it. So does this — over [`Cvars`] — after the registered commands, which is the
//! reference's own lookup order. The four commands `ConsoleVar.cpp`'s `Initialize` registers
//! (`set`, `cvar_reset`, `cvar_default`, `cvarlist`) are the built-ins [`ConsolePlugin`] adds,
//! plus `help`, which the reference's registry runtime owns.
//!
//! There is no console screen here — the drop-down that `` ` `` toggles in the reference — so a
//! command's output is a system line in the chat frame. That is the seam `/console` already
//! used; the registry is what the arm match behind it became.
use std::collections::BTreeMap;
use bevy::prelude::*;
use crate::cvars::{Cvars, SetOutcome};
/// A command's body: the whole world, the argument tail after the command name (trimmed), and
/// the lines to print in return.
pub(crate) type ConsoleHandler = fn(&mut World, &str) -> Vec<String>;
#[derive(Clone, Copy)]
struct ConsoleCommand {
/// The registered spelling, as `help` prints it.
name: &'static str,
help: &'static str,
run: ConsoleHandler,
}
/// The registry — lowercased name → command, in name order.
#[derive(Resource, Default)]
pub(crate) struct ConsoleCommands {
by_key: BTreeMap<String, ConsoleCommand>,
}
/// `app.console_command(name, help, handler)` — a subsystem registering its own command from
/// its plugin, the reference's `ConsoleCommandRegister`.
pub(crate) trait ConsoleCommandApp {
fn console_command(
&mut self,
name: &'static str,
help: &'static str,
run: ConsoleHandler,
) -> &mut Self;
}
impl ConsoleCommandApp for App {
fn console_command(
&mut self,
name: &'static str,
help: &'static str,
run: ConsoleHandler,
) -> &mut Self {
let mut table = self.world_mut().get_resource_or_init::<ConsoleCommands>();
let key = name.to_ascii_lowercase();
// Two registrations of one name is a wiring bug, never a runtime condition — and the
// reference's registry would silently shadow one with the other.
assert!(
!table.by_key.contains_key(&key),
"console command {name:?} registered twice"
);
table.by_key.insert(key, ConsoleCommand { name, help, run });
self
}
}
/// The built-ins: the reference's four `ConsoleVar.cpp` commands and `help`.
pub(crate) struct ConsolePlugin;
impl Plugin for ConsolePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ConsoleCommands>()
.console_command("set", "Set the value of a CVar.", set)
.console_command("cvar_reset", "Reset a CVar to its default.", cvar_default)
.console_command("cvar_default", "Reset a CVar to its default.", cvar_default)
.console_command(
"cvarlist",
"List the CVars, optionally matching a string.",
cvarlist,
)
.console_command("help", "List the console commands, or describe one.", help);
}
}
/// Run one console line — the reference's `ConsoleCommandExecute`: the first token is a command
/// name (case-insensitive), the rest is its argument tail. A registered command runs; else a
/// registered CVar answers as its per-CVar command does; else the line is reported as unknown.
/// Returns the lines to print.
///
/// A CVar write here is applied **at once** — its observers run before this returns — which is
/// the reference's shape (the callback runs inside `CVar::Set`), and what makes a typed
/// `/console farclip 500` visible on the next frame rather than the one after.
pub(crate) fn execute(world: &mut World, line: &str) -> Vec<String> {
let line = line.trim();
let (name, args) = line
.split_once(char::is_whitespace)
.map_or((line, ""), |(n, a)| (n, a.trim()));
if name.is_empty() {
return vec!["console: no command given (`help` lists the commands)".to_string()];
}
let command = world
.get_resource::<ConsoleCommands>()
.and_then(|t| t.by_key.get(&name.to_ascii_lowercase()).copied());
if let Some(command) = command {
return (command.run)(world, args);
}
if world
.get_resource::<Cvars>()
.is_some_and(|c| c.row(name).is_some())
{
return if args.is_empty() {
vec![print_cvar(world, name)]
} else {
set_cvar(world, name, args)
};
}
vec![format!(
"console: '{name}' is not a command or a CVar (`help` lists the commands)"
)]
}
/// `CVar "%s" is "%s"` — the per-CVar command's empty-argument arm (`0x63dde0`), with the
/// staged value beside it when one is waiting for the latch boundary.
fn print_cvar(world: &World, name: &str) -> String {
let cvars = world.resource::<Cvars>();
let Some(row) = cvars.row(name) else {
return format!("console: no CVar named '{name}'");
};
match &row.pending {
Some(staged) => format!(
"CVar \"{}\" is \"{}\" (\"{staged}\" staged until the next RestartGx)",
row.name, row.value
),
None => format!("CVar \"{}\" is \"{}\"", row.name, row.value),
}
}
/// A write through the registry, its observers run, and the outcome said back.
fn set_cvar(world: &mut World, name: &str, value: &str) -> Vec<String> {
let (outcome, events) = {
let mut cvars = world.resource_mut::<Cvars>();
let outcome = cvars.set(name, value);
(outcome, cvars.take_events())
};
for event in events {
world.trigger(event);
}
match outcome {
SetOutcome::Unknown => vec![format!("console: no CVar named '{name}'")],
SetOutcome::Refused => vec![format!(
"console: '{value}' is not a number, and {name} takes one"
)],
SetOutcome::Unchanged | SetOutcome::Changed => Vec::new(),
SetOutcome::Staged => vec![format!(
"{name} staged as \"{value}\" — applies at the next RestartGx"
)],
}
}
/// `set <name> <value>` — the reference's `0x63d500`. **One stated divergence:** the reference
/// registers an unknown name as a new category-5 record, invisible to Lua; ours refuses it,
/// because a row nothing reads is exactly what the registry's honest-tree rule exists to keep
/// out, and `config.toml` preserves an unknown key on its own.
fn set(world: &mut World, args: &str) -> Vec<String> {
let (name, value) = args
.split_once(char::is_whitespace)
.map_or((args, ""), |(n, v)| (n, v.trim()));
if name.is_empty() || value.is_empty() {
return vec!["usage: set <cvar> <value>".to_string()];
}
set_cvar(world, name, value)
}
/// `cvar_reset <name>` / `cvar_default <name>` — the reference's `0x63d590`/`0x63d640`. One
/// body for both: our rows carry no separate reset value, so the reset target is the default.
fn cvar_default(world: &mut World, args: &str) -> Vec<String> {
let name = args.split_whitespace().next().unwrap_or("");
if name.is_empty() {
return vec!["usage: cvar_default <cvar>".to_string()];
}
let default = world
.resource::<Cvars>()
.default_of(name)
.map(str::to_string);
let Some(default) = default else {
return vec![format!("console: no CVar named '{name}'")];
};
set_cvar(world, name, &default)
}
/// `cvarlist [match]` — the reference's `0x63d6f0`, in a format of our own: one line per row,
/// the value, the default when the value moved, the staged value when one is waiting, and
/// whether the session owns the row or an addon declared it.
fn cvarlist(world: &mut World, args: &str) -> Vec<String> {
let wanted = args.split_whitespace().next().map(str::to_ascii_lowercase);
let cvars = world.resource::<Cvars>();
let mut rows: Vec<_> = cvars
.rows()
.filter(|r| {
wanted
.as_deref()
.is_none_or(|w| r.name.to_ascii_lowercase().contains(w))
})
.collect();
rows.sort_by_key(|r| r.name.to_ascii_lowercase());
let mut out: Vec<String> = rows
.iter()
.map(|r| {
let mut line = format!("{} = \"{}\"", r.name, r.value);
if r.value != r.default {
line.push_str(&format!(" (default \"{}\")", r.default));
}
if let Some(staged) = &r.pending {
line.push_str(&format!(" [staged \"{staged}\"]"));
}
if cvars.is_session_owned(&r.name) {
line.push_str(" [session]");
}
if r.addon {
line.push_str(" [addon]");
}
line
})
.collect();
out.push(format!("{} CVar(s)", rows.len()));
out
}
/// `help [command]` — the registry's own listing.
fn help(world: &mut World, args: &str) -> Vec<String> {
let table = world.resource::<ConsoleCommands>();
let wanted = args.split_whitespace().next();
match wanted {
Some(name) => match table.by_key.get(&name.to_ascii_lowercase()) {
Some(c) => vec![format!("{} — {}", c.name, c.help)],
None => vec![format!("console: no command named '{name}'")],
},
None => {
let mut out: Vec<String> = table
.by_key
.values()
.map(|c| format!("{} — {}", c.name, c.help))
.collect();
out.push(
"…and every CVar by name: `<cvar>` prints it, `<cvar> <value>` sets it".into(),
);
out
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn console_app() -> App {
let mut app = App::new();
app.add_plugins(bevy::MinimalPlugins)
.init_resource::<Cvars>()
.add_plugins(ConsolePlugin);
app
}
fn run(app: &mut App, line: &str) -> Vec<String> {
execute(app.world_mut(), line)
}
/// **The lookup order is the reference's**: a registered command first, then the CVar's own
/// per-name command (bare name prints, a value sets), then unknown — case-insensitively at
/// every step, like the engine's own table lookup.
#[test]
fn a_line_is_a_command_then_a_cvar_then_unknown() {
let mut app = console_app();
assert_eq!(
run(&mut app, "MusicVolume"),
vec!["CVar \"MusicVolume\" is \"0.4\"".to_string()]
);
assert!(
run(&mut app, "musicvolume 0.7").is_empty(),
"a set is quiet"
);
assert_eq!(
app.world().resource::<Cvars>().get("MusicVolume"),
Some("0.7")
);
assert_eq!(
run(&mut app, "SET MusicVolume 0.2"),
Vec::<String>::new(),
"`set` is a registered command, matched case-insensitively"
);
assert_eq!(
app.world().resource::<Cvars>().get("MusicVolume"),
Some("0.2")
);
assert_eq!(
run(&mut app, "cvar_default musicVolume"),
Vec::<String>::new()
);
assert_eq!(
app.world().resource::<Cvars>().get("MusicVolume"),
Some("0.4")
);
assert_eq!(
run(&mut app, "nosuchthing 1"),
vec![
"console: 'nosuchthing' is not a command or a CVar (`help` lists the commands)"
.to_string()
]
);
assert_eq!(
run(&mut app, "set nosuchthing 1"),
vec!["console: no CVar named 'nosuchthing'".to_string()],
"the stated divergence: no record conjured at the console"
);
assert_eq!(
run(&mut app, "MusicVolume wat"),
vec!["console: 'wat' is not a number, and MusicVolume takes one".to_string()]
);
assert_eq!(
run(&mut app, " "),
vec!["console: no command given (`help` lists the commands)".to_string()]
);
}
/// A write to a latched row says it is staged, prints as staged, and the list shows it.
#[test]
fn a_latched_cvar_reports_its_stage() {
let mut app = console_app();
assert_eq!(
run(&mut app, "gxVSync 0"),
vec!["gxVSync staged as \"0\" — applies at the next RestartGx".to_string()]
);
assert_eq!(
run(&mut app, "gxvsync"),
vec!["CVar \"gxVSync\" is \"1\" (\"0\" staged until the next RestartGx)".to_string()]
);
let list = run(&mut app, "cvarlist gxv");
assert_eq!(
list,
vec![
"gxVSync = \"1\" [staged \"0\"]".to_string(),
"1 CVar(s)".to_string()
]
);
}
/// `cvarlist` marks a moved value with its default, a session-owned row, and an addon's.
#[test]
fn cvarlist_says_where_each_row_stands() {
let mut app = console_app();
{
let mut cvars = app.world_mut().resource_mut::<Cvars>();
cvars.set("farclip", "500");
cvars.own_for_session("uiScale", Some("1.2"));
cvars.learn_addon_row("myAddonKnob", "7");
}
let list = run(&mut app, "cvarlist");
assert!(
list.contains(&"farclip = \"500\" (default \"350\")".to_string()),
"{list:?}"
);
assert!(
list.contains(&"uiScale = \"1.2\" (default \"0.9\") [session]".to_string()),
"{list:?}"
);
assert!(
list.contains(&"myAddonKnob = \"7\" [addon]".to_string()),
"{list:?}"
);
assert!(list.last().unwrap().ends_with(" CVar(s)"));
let some = run(&mut app, "cvarlist FARCLIP");
assert_eq!(some.len(), 2, "matched case-insensitively: {some:?}");
}
/// `help` lists what is registered, and a subsystem's registration is one call.
#[test]
fn help_lists_the_registry_and_a_registration_is_one_call() {
fn shout(_: &mut World, args: &str) -> Vec<String> {
vec![format!("{}!", args.to_ascii_uppercase())]
}
let mut app = console_app();
app.console_command("shout", "Shout the argument back.", shout);
assert_eq!(
run(&mut app, "Shout hello there"),
vec!["HELLO THERE!".to_string()]
);
let listing = run(&mut app, "help");
assert!(listing.contains(&"shout — Shout the argument back.".to_string()));
assert!(listing.contains(&"set — Set the value of a CVar.".to_string()));
assert_eq!(
run(&mut app, "help cvarlist"),
vec!["cvarlist — List the CVars, optionally matching a string.".to_string()]
);
}
#[test]
#[should_panic(expected = "registered twice")]
fn a_name_registered_twice_is_a_wiring_bug() {
fn nothing(_: &mut World, _: &str) -> Vec<String> {
Vec::new()
}
let mut app = console_app();
app.console_command("set", "again", nothing);
}
}