use sdk_raw::test_support::{mock_vtable, unexpected_call};
use sdk_raw::util::cstr::cstring_from_buffer;
use sdk_raw::util::printf::vsnprintf;
use sdk_raw::vcall;
use source_sdk_2013::commands::{
Client, ClientFilter, ClientRoute, CommandAccess, CommandBaseKind, CommandContext,
CommandError, CommandFlags, CommandFn, CommandHandler, CommandRegistrar, CommandResult,
ConsoleCommand, ConsoleVariable, Invoker, RegisterCommandError, RegisterCommandErrorKind,
UnlinksBeforeUnload, UnregisterCommandErrorKind, route_client_command,
};
use source_sdk_2013::interfaces::cvar::ConVarChange;
use source_sdk_2013::interfaces::{Cvar, ValveEngine};
use source_sdk_2013::test_support::edicts::edict_table;
use source_sdk_2013::test_support::interfaces::cvar::mock_base;
use source_sdk_2013::test_support::leak;
use source_sdk_2013::test_support::server::{export, mock_binding, mock_server};
use source_sdk_2013::{Module, Server};
use std::cell::{Cell, RefCell};
use std::ffi::{CStr, c_char, c_int};
use std::pin::Pin;
use std::ptr::{self, NonNull, null_mut};
thread_local! {
static REGISTRY: RefCell<Vec<*mut sys::ConCommandBase>> = const { RefCell::new(Vec::new()) };
static REFUSE_LINKS: Cell<bool> = const { Cell::new(false) };
static NEXT_IDENTIFIER: Cell<c_int> = const { Cell::new(7) };
static CHEATS: Cell<*mut sys::ConVar> = const { Cell::new(null_mut()) };
static CONSOLE: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
static CLIENT_PRINTS: RefCell<Vec<(c_int, String)>> = const { RefCell::new(Vec::new()) };
static SEEN: RefCell<Vec<Seen>> = const { RefCell::new(Vec::new()) };
static CHANGES: RefCell<Vec<Change>> = const { RefCell::new(Vec::new()) };
static GLOBAL_CALLBACKS: RefCell<Vec<sys::FnChangeCallback_t>> = const { RefCell::new(Vec::new()) };
static ANNOUNCED: Cell<usize> = const { Cell::new(0) };
static RAW_COMMAND: Cell<*mut sys::CCommand> = const { Cell::new(null_mut()) };
static CLIENT_EDICT: Cell<*mut sys::edict_t> = const { Cell::new(null_mut()) };
}
static SELF_REMOVING: ConsoleCommand<CommandFn> =
ConsoleCommand::new(c"sb_once", remove_self as CommandFn).access(CommandAccess::Everyone);
#[derive(Debug, PartialEq)]
struct Change {
name: String,
old: String,
old_float: f32,
new: String,
}
struct Host<'s>(Cvar<'s>);
unsafe impl CommandRegistrar for Host<'_> {
unsafe fn link(&self, command: NonNull<sys::ConCommandBase>) {
unsafe { self.0.link(command) };
}
unsafe fn unlink(&self, command: NonNull<sys::ConCommandBase>) {
unsafe { self.0.unlink(command) };
}
}
unsafe impl UnlinksBeforeUnload for Host<'_> {}
#[derive(Debug, PartialEq, Eq)]
struct Seen {
slot: Option<c_int>,
typed: String,
args: Vec<String>,
}
unsafe extern "C" fn allocate_dll_identifier(_: *mut sys::ICvar) -> c_int {
NEXT_IDENTIFIER.replace(NEXT_IDENTIFIER.get() + 1)
}
fn base_of<T>(object: &T) -> *mut sys::ConCommandBase {
ptr::from_ref(object).cast_mut().cast()
}
unsafe extern "C" fn call_global_change_callbacks(
_: *mut sys::ICvar,
var: *mut sys::ConVar,
old: *const c_char,
old_float: f32,
) {
let change = unsafe {
Change {
name: text(name_of(var.cast()).as_ptr()),
old: text(old),
old_float,
new: text((&raw const (*var).m_pszString).read()),
}
};
CHANGES.with_borrow_mut(|changes| changes.push(change));
if unsafe {
vcall!(var.cast::<sys::ConCommandBase>() => ConCommandBase_IsFlagSet(CommandFlags::NOTIFY.bits()))
} {
ANNOUNCED.set(ANNOUNCED.get() + 1);
}
let count = GLOBAL_CALLBACKS.with_borrow(Vec::len);
for index in 0..count {
let Some(callback) =
GLOBAL_CALLBACKS.with_borrow(|callbacks| callbacks.get(index).copied())
else {
break;
};
if let Some(callback) = callback {
unsafe { callback(&raw mut (*var)._base_1, old, old_float) };
}
}
}
#[test]
fn change_callbacks_get_the_interface_and_the_old_value() {
thread_local! {
static SEEN_BY_CALLBACK: RefCell<Vec<(*mut sys::IConVar, String, f32)>> =
const { RefCell::new(Vec::new()) };
}
unsafe extern "C" fn callback(var: *mut sys::IConVar, old: *const c_char, old_float: f32) {
let old = unsafe { text(old) };
SEEN_BY_CALLBACK.with_borrow_mut(|seen| seen.push((var, old, old_float)));
}
mock_engine();
let variable = leak_pinned(ConsoleVariable::new(c"sb_speed", c"1.5"));
register_variable(variable).unwrap();
let raw = base_of(variable.get_ref()).cast::<sys::ConVar>();
unsafe { (&raw mut (*raw).m_fnChangeCallback).write(Some(callback)) };
let scope = ();
let server = mock_server(&scope);
variable.set_float(server, 2.0);
let interface = unsafe { &raw mut (*raw)._base_1 };
assert_eq!(
SEEN_BY_CALLBACK.take(),
[(interface, "1.5".to_owned(), 1.5)]
);
assert_eq!(changes().len(), 1);
assert_eq!(variable.string(server).as_c_str(), c"2.000000");
}
fn changes() -> Vec<Change> {
CHANGES.take()
}
unsafe extern "C" fn client_printf(
_: *mut sys::IVEngineServer,
edict: *mut sys::edict_t,
message: *const c_char,
) {
let (index, message) = unsafe { (c_int::from((*edict)._base.m_EdictIndex), text(message)) };
CLIENT_PRINTS.with_borrow_mut(|prints| prints.push((index, message)));
}
fn client_prints() -> Vec<(c_int, String)> {
CLIENT_PRINTS.take()
}
#[test]
fn client_filters_narrow_the_clients_that_may_run_a_command() {
mock_engine();
let filtered = leak_pinned(
ConsoleCommand::new(c"sb_round_cancel", record)
.access(CommandAccess::Everyone)
.clients(ClientFilter {
name: "slot 1",
accepts: in_slot_1,
})
.flags(CommandFlags::HIDDEN)
.help(c"Cancels the round."),
);
let allowed = leak_pinned(
ConsoleCommand::new(c"sb_may_cancel", move |command: &CommandContext<'_>| {
command.reply(filtered.allows(command.server(), command.invoker()))?;
Ok(())
})
.access(CommandAccess::Everyone),
);
register(filtered).unwrap();
register(allowed).unwrap();
let mut table = edict_table(3, |_| false);
assert_eq!(
route(&raw mut table[2], "sb_round_cancel"),
ClientRoute::Handled
);
assert_eq!(
route(&raw mut table[1], "sb_round_cancel"),
ClientRoute::NotRouted
);
assert_eq!(
seen(),
[Seen {
slot: Some(1),
typed: "sb_round_cancel".to_owned(),
args: vec![],
}]
);
assert_eq!(client_prints(), [(2, "ran sb_round_cancel\n".to_owned())]);
engine_dispatch(filtered.get_ref(), &*tokenized("sb_round_cancel"));
assert_eq!(seen().len(), 1);
assert_eq!(console(), ["ran sb_round_cancel\n"]);
assert_eq!(
route(&raw mut table[2], "sb_may_cancel"),
ClientRoute::Handled
);
assert_eq!(
route(&raw mut table[1], "sb_may_cancel"),
ClientRoute::Handled
);
engine_dispatch(allowed.get_ref(), &*tokenized("sb_may_cancel"));
assert_eq!(
client_prints(),
[(2, "true\n".to_owned()), (1, "false\n".to_owned())]
);
assert_eq!(console(), ["true\n"]);
let scope = ();
let server = mock_server(&scope);
assert_eq!(filtered.access_level(), CommandAccess::Everyone);
assert_eq!(
filtered.client_filter().map(|filter| filter.name),
Some("slot 1")
);
assert_eq!(filtered.current_flags(server), CommandFlags::HIDDEN);
assert_eq!(filtered.help_text(), c"Cancels the round.");
assert!(allowed.client_filter().is_none());
}
#[test]
fn clients_need_cheats_for_cheat_commands() {
mock_engine();
let command = leak_pinned(
ConsoleCommand::new(c"sb_noclip", record)
.access(CommandAccess::Everyone)
.flags(CommandFlags::CHEAT | CommandFlags::HIDDEN),
);
register(command).unwrap();
let mut table = edict_table(2, |_| false);
assert_eq!(route(&raw mut table[1], "sb_noclip"), ClientRoute::Handled);
assert!(seen().is_empty());
assert_eq!(
client_prints(),
[(
1,
"Can't use cheat command sb_noclip in multiplayer, unless the server has sv_cheats set to 1.\n"
.to_owned()
)]
);
set_cheats(1);
assert_eq!(route(&raw mut table[1], "sb_noclip"), ClientRoute::Handled);
assert_eq!(seen().len(), 1);
}
#[test]
fn clients_reach_only_commands_they_may_run() {
mock_engine();
list_foreign(c"status", CommandBaseKind::Command);
let everyone =
leak_pinned(ConsoleCommand::new(c"sb_ping", record).access(CommandAccess::Everyone));
let clients =
leak_pinned(ConsoleCommand::new(c"sb_whoami", record).access(CommandAccess::Clients));
let server_only = leak_pinned(ConsoleCommand::new(c"sb_kick_all", record));
register(everyone).unwrap();
register(clients).unwrap();
register(server_only).unwrap();
let mut table = edict_table(4, |slot| slot == 3);
let mut route = |slot: usize, line: &str| route(&raw mut table[slot], line);
assert_eq!(route(2, "SB_Ping x"), ClientRoute::Handled);
assert_eq!(route(1, "sb_whoami"), ClientRoute::Handled);
assert_eq!(
seen(),
[
Seen {
slot: Some(1),
typed: "SB_Ping".to_owned(),
args: vec!["x".to_owned()],
},
Seen {
slot: Some(0),
typed: "sb_whoami".to_owned(),
args: vec![],
},
]
);
assert_eq!(
client_prints(),
[
(2, "ran sb_ping\n".to_owned()),
(1, "ran sb_whoami\n".to_owned())
]
);
assert_eq!(route(1, "sb_kick_all"), ClientRoute::NotRouted);
assert_eq!(route(1, "status"), ClientRoute::NotRouted);
assert_eq!(route(1, "sb_unknown"), ClientRoute::NotRouted);
assert_eq!(route(0, "sb_ping"), ClientRoute::NotRouted);
assert_eq!(route(3, "sb_ping"), ClientRoute::NotRouted);
let scope = ();
let server = mock_server(&scope);
everyone
.unregister(server, &server.cvar().unwrap())
.unwrap();
assert_eq!(route(1, "sb_ping"), ClientRoute::NotRouted);
assert!(seen().is_empty());
assert!(client_prints().is_empty());
assert!(console().is_empty());
}
fn console() -> Vec<String> {
CONSOLE.take()
}
unsafe extern "C" fn console_printf(_: *const sys::ICvar, format: *const c_char, arguments: ...) {
let message = if unsafe { CStr::from_ptr(format) } == c"%s" {
let mut buffer = [0; 1024];
unsafe { vsnprintf(&mut buffer, format, arguments) };
cstring_from_buffer(&buffer).into_string().unwrap()
} else {
"<format>".to_owned()
};
CONSOLE.with_borrow_mut(|console| console.push(message));
}
fn engine_dispatch<H>(command: &ConsoleCommand<H>, raw: *const sys::CCommand) {
let base = base_of(command);
unsafe {
let vtable = (&raw const (*base).vtable_)
.read()
.cast::<sys::ConCommand__bindgen_vtable>();
((*vtable).ConCommand_Dispatch)(base.cast(), raw);
}
}
#[test]
fn errors_and_panics_are_reported_to_the_invoker() {
mock_engine();
let failing = leak_pinned(ConsoleCommand::new(
c"sb_fail",
|command: &CommandContext<'_>| {
command.args().parse::<u8>(0)?;
Err(CommandError::usage("sb_fail <count>"))
},
));
let panicking = leak_pinned(
ConsoleCommand::new(c"sb_panic", |_: &CommandContext<'_>| -> CommandResult {
panic!("boom")
})
.access(CommandAccess::Everyone),
);
register(failing).unwrap();
register(panicking).unwrap();
engine_dispatch(failing.get_ref(), &*tokenized("sb_fail x"));
engine_dispatch(failing.get_ref(), &*tokenized("sb_fail 3"));
engine_dispatch(panicking.get_ref(), &*tokenized("sb_panic"));
assert_eq!(
console(),
[
"sb_fail: argument 1 (`x`) is not a valid u8\n",
"sb_fail: usage: sb_fail <count>\n",
"sb_panic: the command panicked: boom\n",
]
);
let mut table = edict_table(2, |_| false);
assert_eq!(route(&raw mut table[1], "sb_panic"), ClientRoute::Handled);
assert_eq!(console(), ["sb_panic: the command panicked: boom\n"]);
assert_eq!(
client_prints(),
[(1, "sb_panic: the command failed\n".to_owned())]
);
}
unsafe extern "C" fn find_command_base(
_: *mut sys::ICvar,
name: *const c_char,
) -> *mut sys::ConCommandBase {
let name = unsafe { CStr::from_ptr(name) }.to_bytes();
REGISTRY.with_borrow(|registry| {
registry
.iter()
.copied()
.find(|&base| {
unsafe { name_of(base) }
.to_bytes()
.eq_ignore_ascii_case(name)
})
.unwrap_or(null_mut())
})
}
unsafe extern "C" fn find_var(_: *mut sys::ICvar, name: *const c_char) -> *mut sys::ConVar {
let name = unsafe { CStr::from_ptr(name) };
if name == c"sv_cheats" {
return CHEATS.get();
}
REGISTRY.with_borrow(|registry| {
registry
.iter()
.copied()
.find(|&base| {
unsafe {
!vcall!(base => ConCommandBase_IsCommand())
&& name_of(base)
.to_bytes()
.eq_ignore_ascii_case(name.to_bytes())
}
})
.map_or(null_mut(), |base| base.cast())
})
}
unsafe extern "C" fn get_commands(_: *mut sys::ICvar) -> *mut sys::ConCommandBase {
REGISTRY.with_borrow(|registry| registry.first().copied().unwrap_or(null_mut()))
}
#[test]
fn handlers_can_leave_client_invocations_to_the_game() {
mock_engine();
let command = leak_pinned(
ConsoleCommand::new(c"jointeam", |command: &CommandContext<'_>| {
let Invoker::Client(client) = command.invoker() else {
return Err(CommandError::Unhandled);
};
if command.args().get(0) == Some(c"blue") {
return record(command);
}
SEEN.with_borrow_mut(|seen| {
seen.push(Seen {
slot: Some(client.slot()),
typed: "left to the game".to_owned(),
args: vec![],
})
});
Err(CommandError::Unhandled)
})
.access(CommandAccess::Everyone),
);
register(command).unwrap();
let mut table = edict_table(2, |_| false);
assert_eq!(
route(&raw mut table[1], "jointeam red"),
ClientRoute::NotRouted
);
assert_eq!(
route(&raw mut table[1], "jointeam blue"),
ClientRoute::Handled
);
assert_eq!(seen().len(), 2);
assert_eq!(client_prints(), [(1, "ran jointeam\n".to_owned())]);
engine_dispatch(command.get_ref(), &*tokenized("jointeam red"));
assert!(seen().is_empty());
assert!(console().is_empty());
}
#[test]
fn handlers_may_unregister_their_own_command() {
mock_engine();
register(Pin::static_ref(&SELF_REMOVING)).unwrap();
engine_dispatch(&SELF_REMOVING, &*tokenized("sb_once"));
assert!(listed().is_empty());
assert_eq!(console(), ["removed\n"]);
}
#[test]
fn handlers_with_state_dispatch_through_the_whole_command() {
mock_engine();
let greeting = String::from("hello");
let command = leak_pinned(
ConsoleCommand::new(
c"sb_greet",
move |command: &CommandContext<'_>| -> CommandResult {
command.reply(&greeting)?;
Ok(())
},
)
.access(CommandAccess::Everyone),
);
register(command).unwrap();
engine_dispatch(command.get_ref(), &*tokenized("sb_greet"));
assert_eq!(console(), ["hello\n"]);
let mut table = edict_table(2, |_| false);
assert_eq!(route(&raw mut table[1], "sb_greet"), ClientRoute::Handled);
assert_eq!(client_prints(), [(1, "hello\n".to_owned())]);
}
fn in_slot_1(_: Server<'_>, client: Client<'_>) -> bool {
client.slot() == 1
}
unsafe extern "C" fn install_global_change_callback(
_: *mut sys::ICvar,
callback: sys::FnChangeCallback_t,
) {
GLOBAL_CALLBACKS.with_borrow_mut(|callbacks| callbacks.push(callback));
}
fn leak_pinned<T>(object: T) -> Pin<&'static T> {
Pin::static_ref(Box::leak(Box::new(object)))
}
fn list_foreign(name: &'static CStr, kind: CommandBaseKind) -> *mut sys::ConCommandBase {
let next = REGISTRY.with_borrow(|registry| registry.first().copied().unwrap_or(null_mut()));
let base = leak(mock_base(name, kind, CommandFlags::NONE, next));
REGISTRY.with_borrow_mut(|registry| registry.insert(0, base));
base
}
fn listed() -> Vec<String> {
REGISTRY.with_borrow(|registry| {
registry
.iter()
.map(|&base| unsafe { name_of(base) }.to_str().unwrap().to_owned())
.collect()
})
}
fn mock_engine() {
let (cvar_vtable, engine_vtable) = unsafe {
(
mock_vtable::<sys::ICvar__bindgen_vtable>(unexpected_call as *const (), |vtable| {
(&raw mut (*vtable).ICvar_FindCommandBase).write(find_command_base);
(&raw mut (*vtable).ICvar_RegisterConCommand).write(register_con_command);
(&raw mut (*vtable).ICvar_UnregisterConCommand).write(unregister_con_command);
(&raw mut (*vtable).ICvar_AllocateDLLIdentifier).write(allocate_dll_identifier);
(&raw mut (*vtable).ICvar_FindVar).write(find_var);
(&raw mut (*vtable).ICvar_GetCommands).write(get_commands);
(&raw mut (*vtable).ICvar_ConsolePrintf).write(console_printf);
(&raw mut (*vtable).ICvar_InstallGlobalChangeCallback)
.write(install_global_change_callback);
(&raw mut (*vtable).ICvar_RemoveGlobalChangeCallback)
.write(remove_global_change_callback);
(&raw mut (*vtable).ICvar_CallGlobalChangeCallbacks)
.write(call_global_change_callbacks);
}),
mock_vtable::<sys::IVEngineServer__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IVEngineServer_ClientPrintf).write(client_printf);
},
),
)
};
CHEATS.set(leak(unsafe { std::mem::zeroed::<sys::ConVar>() }));
export(
Module::Engine,
Cvar::VERSION,
leak(sys::ICvar {
vtable_: Box::leak(cvar_vtable),
}),
);
export(
Module::Engine,
ValveEngine::VERSION,
leak(sys::IVEngineServer {
vtable_: Box::leak(engine_vtable),
}),
);
}
unsafe fn name_of(base: *mut sys::ConCommandBase) -> &'static CStr {
unsafe { CStr::from_ptr(vcall!(base => ConCommandBase_GetName())) }
}
#[test]
fn names_in_use_are_refused() {
mock_engine();
list_foreign(c"changelevel", CommandBaseKind::Command);
list_foreign(c"sv_cheats", CommandBaseKind::Variable);
let command = |name| leak_pinned(ConsoleCommand::new(name, record));
let kind = |result: Result<(), RegisterCommandError>| result.unwrap_err().kind().clone();
assert_eq!(
kind(register(command(c"changelevel"))),
RegisterCommandErrorKind::NameTaken(CommandBaseKind::Command)
);
assert_eq!(
kind(register(command(c"SV_CHEATS"))),
RegisterCommandErrorKind::NameTaken(CommandBaseKind::Variable)
);
register(command(c"sb_ping")).unwrap();
assert_eq!(
kind(register(command(c"SB_Ping"))),
RegisterCommandErrorKind::NameTaken(CommandBaseKind::Command)
);
assert_eq!(listed(), ["sb_ping", "sv_cheats", "changelevel"]);
}
#[test]
fn nested_invocations_keep_their_own_invoker_and_arguments() {
mock_engine();
let inner =
leak_pinned(ConsoleCommand::new(c"sb_inner", record).access(CommandAccess::Clients));
let outer = leak_pinned(ConsoleCommand::new(
c"sb_outer",
|command: &CommandContext<'_>| {
let raw = RAW_COMMAND.get();
unsafe {
(&raw mut (*raw).m_nArgc).write(1);
(&raw mut (*raw).m_pArgSBuffer).cast::<u8>().write(b'!');
}
assert_eq!(
route(CLIENT_EDICT.get(), "sb_inner nested"),
ClientRoute::Handled
);
record(command)
},
));
register(inner).unwrap();
register(outer).unwrap();
let mut table = edict_table(2, |_| false);
let raw = Box::into_raw(tokenized("sb_outer a b"));
CLIENT_EDICT.set(&raw mut table[1]);
RAW_COMMAND.set(raw);
engine_dispatch(outer.get_ref(), raw);
drop(unsafe { Box::from_raw(raw) });
assert_eq!(
seen(),
[
Seen {
slot: Some(0),
typed: "sb_inner".to_owned(),
args: vec!["nested".to_owned()],
},
Seen {
slot: None,
typed: "sb_outer".to_owned(),
args: vec!["a".to_owned(), "b".to_owned()],
},
]
);
}
#[test]
fn quiet_changes_are_not_announced() {
mock_engine();
let variable = leak_pinned(ConsoleVariable::new(c"sb_loud", c"0").flags(CommandFlags::NOTIFY));
let scope = ();
let server = mock_server(&scope);
register_variable(variable).unwrap();
let var = server.cvar().unwrap().find_var(c"sb_loud").unwrap();
var.set_string(c"1");
assert_eq!(ANNOUNCED.get(), 1);
var.set_string_quietly(c"2");
assert_eq!(ANNOUNCED.get(), 1);
assert_eq!(changes().len(), 2);
assert_eq!(var.int(), 2);
let base = base_of(variable.get_ref());
assert!(unsafe { vcall!(base => ConCommandBase_IsFlagSet(CommandFlags::NOTIFY.bits())) });
}
fn record(command: &CommandContext<'_>) -> CommandResult {
let slot = match command.invoker() {
Invoker::Server => None,
Invoker::Client(client) => Some(client.slot()),
};
let args = command.args();
SEEN.with_borrow_mut(|seen| {
seen.push(Seen {
slot,
typed: args.name().to_str().unwrap().to_owned(),
args: args
.iter()
.map(|arg| arg.to_str().unwrap().to_owned())
.collect(),
})
});
command.reply(format_args!("ran {}", command.name().to_str().unwrap()))?;
Ok(())
}
fn register<H: CommandHandler>(
command: Pin<&'static ConsoleCommand<H>>,
) -> Result<(), RegisterCommandError> {
let scope = ();
let server = mock_server(&scope);
command.register(server, mock_binding(), &Host(server.cvar().unwrap()))
}
unsafe extern "C" fn register_con_command(_: *mut sys::ICvar, base: *mut sys::ConCommandBase) {
unsafe {
if REFUSE_LINKS.get() || vcall!(base => ConCommandBase_IsRegistered()) {
return;
}
(&raw mut (*base).m_bRegistered).write(true);
(&raw mut (*base).m_pNext).write(get_commands(null_mut()));
}
REGISTRY.with_borrow_mut(|registry| registry.insert(0, base));
}
fn register_variable(variable: Pin<&'static ConsoleVariable>) -> Result<(), RegisterCommandError> {
let scope = ();
let server = mock_server(&scope);
variable.register(server, mock_binding(), &Host(server.cvar().unwrap()))
}
#[test]
fn registered_commands_and_variables_are_listed() {
mock_engine();
let command =
leak_pinned(ConsoleCommand::new(c"sb_ping", record).flags(CommandFlags::GAME_DLL));
let variable = leak_pinned(
ConsoleVariable::new(c"sb_rounds", c"3")
.flags(CommandFlags::GAME_DLL | CommandFlags::NOTIFY),
);
register(command).unwrap();
register_variable(variable).unwrap();
let scope = ();
let server = mock_server(&scope);
let cvar = server.cvar().unwrap();
assert_eq!(
cvar.command_bases()
.map(|base| (base.name(), base.kind(), base.flags()))
.collect::<Vec<_>>(),
[
(
c"sb_rounds",
CommandBaseKind::Variable,
CommandFlags::GAME_DLL | CommandFlags::NOTIFY
),
(c"sb_ping", CommandBaseKind::Command, CommandFlags::NONE),
]
);
let vars = cvar.vars().collect::<Vec<_>>();
assert_eq!(vars.len(), 1);
let var = vars[0];
assert_eq!(var.as_ptr().cast(), base_of(variable.get_ref()));
assert_eq!(var.default_string().as_c_str(), c"3");
assert!(var.is_default());
var.set_string(c"5");
assert!(!var.is_default());
var.set_string(c"3");
let string = unsafe { (&raw const (*var.as_ptr()).m_pszString).read() };
assert_ne!(string.cast_const(), variable.default_value().as_ptr());
assert!(var.is_default());
}
#[test]
fn registration_links_once_and_unregisters() {
mock_engine();
let other = leak_pinned(ConsoleCommand::new(c"sb_other", record));
let command = leak_pinned(ConsoleCommand::new(c"sb_ping", record).help(c"Replies."));
register(other).unwrap();
register(command).unwrap();
assert_eq!(listed(), ["sb_ping", "sb_other"]);
assert_eq!(
register(command).unwrap_err().to_string(),
"could not register console command `sb_ping`: it is already registered"
);
assert_eq!(listed(), ["sb_ping", "sb_other"]);
let base = base_of(command.get_ref());
unsafe {
assert!(vcall!(base => ConCommandBase_IsCommand()));
assert!(vcall!(base => ConCommandBase_IsRegistered()));
assert_eq!(name_of(base), c"sb_ping");
assert_eq!(
CStr::from_ptr(vcall!(base => ConCommandBase_GetHelpText())),
c"Replies."
);
assert_eq!(vcall!(base => ConCommandBase_GetDLLIdentifier()), 8);
}
let scope = ();
let server = mock_server(&scope);
let cvar = server.cvar().unwrap();
command.unregister(server, &cvar).unwrap();
assert_eq!(listed(), ["sb_other"]);
assert_eq!(
command.unregister(server, &cvar).unwrap_err().kind(),
&UnregisterCommandErrorKind::NotRegistered
);
register(command).unwrap();
assert_eq!(listed(), ["sb_ping", "sb_other"]);
let identifier = unsafe { vcall!(base => ConCommandBase_GetDLLIdentifier()) };
assert_eq!(identifier, 9);
}
fn remove_self(command: &CommandContext<'_>) -> CommandResult {
let server = command.server();
SELF_REMOVING
.unregister(server, &server.cvar()?)
.map_err(CommandError::other)?;
command.reply("removed")?;
Ok(())
}
unsafe extern "C" fn remove_global_change_callback(
_: *mut sys::ICvar,
callback: sys::FnChangeCallback_t,
) {
let address = |callback: sys::FnChangeCallback_t| callback.map(|callback| callback as usize);
GLOBAL_CALLBACKS.with_borrow_mut(|callbacks| {
if let Some(index) = callbacks
.iter()
.position(|&listed| address(listed) == address(callback))
{
callbacks.remove(index);
}
});
}
fn route(edict: *mut sys::edict_t, line: &str) -> ClientRoute {
unsafe {
route_client_command(
&mock_binding(),
NonNull::new(edict).unwrap(),
NonNull::from(&*tokenized(line)),
)
}
}
fn seen() -> Vec<Seen> {
SEEN.take()
}
fn set_cheats(value: c_int) {
unsafe { (&raw mut (*CHEATS.get()).m_nValue).write(value) };
}
unsafe fn text(pointer: *const c_char) -> String {
unsafe { CStr::from_ptr(pointer) }
.to_str()
.unwrap()
.to_owned()
}
#[test]
fn the_engine_dispatches_server_invocations() {
mock_engine();
let command =
leak_pinned(ConsoleCommand::new(c"sb_ping", record).access(CommandAccess::Everyone));
register(command).unwrap();
engine_dispatch(command.get_ref(), &*tokenized("SB_PING a b"));
assert_eq!(
seen(),
[Seen {
slot: None,
typed: "SB_PING".to_owned(),
args: vec!["a".to_owned(), "b".to_owned()],
}]
);
assert_eq!(console(), ["ran sb_ping\n"]);
engine_dispatch(command.get_ref(), ptr::null());
let mut malformed = tokenized("sb_ping");
malformed.m_nArgc = 0;
engine_dispatch(command.get_ref(), &*malformed);
assert!(seen().is_empty());
assert_eq!(
console(),
["sb_ping: the command has 0 arguments, outside 1 to 64\n"]
);
}
#[test]
fn the_engine_sets_variables_through_their_interface() {
mock_engine();
let variable = leak_pinned(ConsoleVariable::new(c"sb_bots", c"10").min(1.0).max(32.0));
let scope = ();
let server = mock_server(&scope);
register_variable(variable).unwrap();
let var = server.cvar().unwrap().find_var(c"sb_bots").unwrap();
let change = |old: &str, old_float, new: &str| Change {
name: "sb_bots".to_owned(),
old: old.to_owned(),
old_float,
new: new.to_owned(),
};
var.set_string(c"20");
assert_eq!((var.int(), var.float()), (20, 20.0));
assert_eq!(changes(), [change("10", 10.0, "20")]);
var.set_string(c"40");
assert_eq!(var.string().as_c_str(), c"32.000000");
assert_eq!(variable.int(server), 32);
assert_eq!(changes(), [change("20", 20.0, "32.000000")]);
var.set_float(0.5);
assert_eq!(var.string().as_c_str(), c"1.000000");
assert_eq!(var.int(), 1);
var.set_int(7);
assert_eq!(var.string().as_c_str(), c"7");
assert_eq!(
changes(),
[
change("32.000000", 32.0, "1.000000"),
change("1.000000", 1.0, "7")
]
);
var.set_int(7);
var.set_string(c"7");
var.set_float(7.0);
assert!(changes().is_empty());
var.set_string(c"many");
assert_eq!(var.string().as_c_str(), c"1.000000");
assert_eq!(changes(), [change("7", 7.0, "1.000000")]);
variable.revert(server);
assert_eq!(var.string().as_c_str(), c"10");
assert_eq!(changes(), [change("1.000000", 1.0, "10")]);
}
fn tokenized(line: &str) -> Box<sys::CCommand> {
let args = line.split(' ').collect::<Vec<_>>();
let args_start = if args.len() > 1 { args[0].len() + 1 } else { 0 };
sdk_raw::test_support::commands::tokenized(line, &args, args_start)
}
#[test]
fn unlisted_commands_are_reported() {
mock_engine();
REFUSE_LINKS.set(true);
let command = leak_pinned(ConsoleCommand::new(c"sb_ping", record));
let error = register(command).unwrap_err();
assert_eq!(error.kind(), &RegisterCommandErrorKind::NotLinked);
assert_eq!(error.name(), c"sb_ping");
assert!(listed().is_empty());
REFUSE_LINKS.set(false);
register(command).unwrap();
}
unsafe extern "C" fn unregister_con_command(_: *mut sys::ICvar, base: *mut sys::ConCommandBase) {
if !unsafe { vcall!(base => ConCommandBase_IsRegistered()) } {
return;
}
unsafe {
(&raw mut (*base).m_bRegistered).write(false);
(&raw mut (*base).m_pNext).write(null_mut());
}
REGISTRY.with_borrow_mut(|registry| {
let Some(position) = registry.iter().position(|&entry| entry == base) else {
return;
};
registry.remove(position);
if let Some(&previous) = position
.checked_sub(1)
.and_then(|index| registry.get(index))
{
let next = registry.get(position).copied().unwrap_or(null_mut());
unsafe { (&raw mut (*previous).m_pNext).write(next) };
}
});
}
#[test]
fn variables_hold_their_default_and_register_as_variables() {
mock_engine();
let variable =
leak_pinned(ConsoleVariable::new(c"sb_rounds", c" 12.5 rounds").help(c"Rounds."));
let scope = ();
let server = mock_server(&scope);
variable.set_float(server, f32::NEG_INFINITY);
assert_eq!(variable.int(server), c_int::MIN);
variable.set_int(server, 3);
assert_eq!(variable.string(server).as_c_str(), c"3");
assert!(changes().is_empty());
register_variable(variable).unwrap();
assert_eq!(listed(), ["sb_rounds"]);
let base = base_of(variable.get_ref());
unsafe {
assert!(!vcall!(base => ConCommandBase_IsCommand()));
assert!(vcall!(base => ConCommandBase_IsRegistered()));
assert_eq!(name_of(base), c"sb_rounds");
assert_eq!(
CStr::from_ptr(vcall!(base => ConCommandBase_GetHelpText())),
c"Rounds."
);
assert_eq!(vcall!(base => ConCommandBase_GetDLLIdentifier()), 7);
}
let var = server.cvar().unwrap().find_var(c"SB_Rounds").unwrap();
assert_eq!(var.name(), c"sb_rounds");
assert_eq!(var.int(), 3);
assert_eq!(var.string().as_c_str(), c"3");
assert_eq!(var.default_string().as_c_str(), c" 12.5 rounds");
let error = register_variable(variable).unwrap_err();
assert_eq!(error.base(), CommandBaseKind::Variable);
assert_eq!(
error.to_string(),
"could not register console variable `sb_rounds`: it is already registered"
);
assert_eq!(
register(leak_pinned(ConsoleCommand::new(c"sb_rounds", record)))
.unwrap_err()
.kind(),
&RegisterCommandErrorKind::NameTaken(CommandBaseKind::Variable)
);
variable
.unregister(server, &server.cvar().unwrap())
.unwrap();
assert!(listed().is_empty());
assert!(server.cvar().unwrap().find_var(c"sb_rounds").is_none());
}
#[test]
fn watches_see_this_crates_variables_change() {
thread_local! {
static WATCHED: RefCell<Vec<(String, *mut sys::ConVar, String, String)>> =
const { RefCell::new(Vec::new()) };
static ECHO: Cell<Option<Pin<&'static ConsoleVariable>>> = const { Cell::new(None) };
}
fn mirror(server: Server<'_>, change: ConVarChange<'_>) {
let var = change.var().unwrap();
let new = var.string();
WATCHED.with_borrow_mut(|watched| {
watched.push((
change.name().to_str().unwrap().to_owned(),
var.as_ptr(),
change.old_string().to_str().unwrap().to_owned(),
new.to_str().unwrap().to_owned(),
));
});
if change.name() == c"sb_speed" {
ECHO.get().unwrap().set_string(server, &new);
}
}
mock_engine();
let speed = leak_pinned(ConsoleVariable::new(c"sb_speed", c"1.5"));
let echo = leak_pinned(ConsoleVariable::new(c"sb_echo", c""));
let scope = ();
let server = mock_server(&scope);
register_variable(speed).unwrap();
register_variable(echo).unwrap();
ECHO.set(Some(echo));
let watch = server
.cvar()
.unwrap()
.watch_changes(mock_binding(), mirror)
.unwrap();
speed.set_float(server, 2.0);
assert_eq!(
WATCHED.take(),
[
(
"sb_speed".to_owned(),
base_of(speed.get_ref()).cast(),
"1.5".to_owned(),
"2.000000".to_owned()
),
(
"sb_echo".to_owned(),
base_of(echo.get_ref()).cast(),
String::new(),
"2.000000".to_owned()
),
]
);
assert_eq!(changes().len(), 2);
assert_eq!(echo.string(server).as_c_str(), c"2.000000");
watch.stop();
speed.set_float(server, 3.0);
assert!(WATCHED.take().is_empty());
assert_eq!(changes().len(), 1);
assert_eq!(echo.string(server).as_c_str(), c"2.000000");
}