use super::error::{
CommandBaseKind, InvalidCommandName, RegisterCommandError, RegisterCommandErrorKind,
UnregisterCommandError, UnregisterCommandErrorKind, validate_name,
};
use super::registrar::{CommandRegistrar, UnlinksBeforeUnload};
use super::{
Client, ClientFilter, CommandAccess, CommandContext, CommandFlags, CommandHandler,
CommandResult, Invoker, route,
};
use crate::server::{Server, ServerBinding};
use sdk_raw::commands::{ConCommandHooks, ConCommandObject, is_registered};
use sdk_raw::vcall;
use std::cell::Cell;
use std::ffi::CStr;
use std::mem::offset_of;
use std::pin::Pin;
use std::ptr::NonNull;
type DispatchFn = unsafe fn(NonNull<CommandHeader>, &CommandContext<'_>) -> CommandResult;
const _: () = {
assert!(offset_of!(CommandHeader, object) == 0);
assert!(offset_of!(ConsoleCommand<()>, header) == 0);
};
static HOOKS: ConCommandHooks = ConCommandHooks {
dispatch: engine_dispatch,
};
#[repr(C)]
pub(super) struct CommandHeader {
object: ConCommandObject,
dispatch: DispatchFn,
name: &'static CStr,
help: &'static CStr,
access: CommandAccess,
client_filter: Option<ClientFilter>,
flags: CommandFlags,
binding: Cell<Option<ServerBinding>>,
}
impl CommandHeader {
pub(super) unsafe fn from_registered(
base: NonNull<sys::ConCommandBase>,
) -> Option<RegisteredCommand> {
let object = unsafe { ConCommandObject::from_registered(base, &HOOKS) }?;
Some(RegisteredCommand(object.cast()))
}
pub(super) const fn access(&self) -> CommandAccess {
self.access
}
pub(super) fn accepts_client(&self, server: Server<'_>, client: Client<'_>) -> bool {
self.access.allows_clients()
&& self
.client_filter
.is_none_or(|filter| (filter.accepts)(server, client))
}
pub(super) fn binding(&self) -> Option<ServerBinding> {
self.binding.get()
}
pub(super) fn current_flags(&self) -> CommandFlags {
CommandFlags::from_bits_retain(self.object.flags())
}
pub(super) const fn name(&self) -> &'static CStr {
self.name
}
unsafe fn prepare(&self, binding: ServerBinding, dll_identifier: sys::CVarDLLIdentifier_t) {
unsafe {
self.object
.prepare(self.name, self.help, self.flags.bits(), dll_identifier)
};
self.binding.set(Some(binding));
}
}
#[doc(alias("ConCommand"))]
#[repr(C)]
pub struct ConsoleCommand<H> {
header: CommandHeader,
handler: H,
}
impl<H: CommandHandler> ConsoleCommand<H> {
pub const fn new(name: &'static CStr, handler: H) -> Self {
match validate_name(name) {
Ok(()) => {}
Err(InvalidCommandName::Empty) => panic!("console command names cannot be empty"),
Err(InvalidCommandName::TooLong) => {
panic!("console command names cannot be longer than 63 bytes")
}
Err(InvalidCommandName::InvalidByte { .. }) => {
panic!("console command names may only contain ASCII letters, digits, and `_`")
}
Err(InvalidCommandName::Reserved) => {
panic!("the engine reserves this name for its own client commands")
}
}
Self {
header: CommandHeader {
object: ConCommandObject::new(&HOOKS),
dispatch: dispatch_erased::<H>,
name,
help: c"",
access: CommandAccess::Server,
client_filter: None,
flags: CommandFlags::NONE,
binding: Cell::new(None),
},
handler,
}
}
pub const fn access(mut self, access: CommandAccess) -> Self {
self.header.access = access;
self
}
pub const fn clients(mut self, filter: ClientFilter) -> Self {
self.header.client_filter = Some(filter);
self
}
pub const fn flags(mut self, flags: CommandFlags) -> Self {
self.header.flags = flags;
self
}
pub const fn help(mut self, help: &'static CStr) -> Self {
self.header.help = help;
self
}
pub fn register(
self: Pin<&'static Self>,
server: Server<'_>,
binding: ServerBinding,
registrar: &impl UnlinksBeforeUnload,
) -> Result<(), RegisterCommandError> {
unsafe { self.register_unmanaged(server, binding, registrar) }
}
pub unsafe fn register_unmanaged(
self: Pin<&'static Self>,
server: Server<'_>,
binding: ServerBinding,
registrar: &impl CommandRegistrar,
) -> Result<(), RegisterCommandError> {
let header = &self.get_ref().header;
let prepare = |dll_identifier| unsafe { header.prepare(binding, dll_identifier) };
unsafe {
register_base(
self.get_ref().as_base(),
header.name,
CommandBaseKind::Command,
server,
registrar,
prepare,
)
}
}
pub fn unregister(
&self,
server: Server<'_>,
registrar: &impl CommandRegistrar,
) -> Result<(), UnregisterCommandError> {
unsafe {
unregister_base(
self.as_base(),
self.header.name,
CommandBaseKind::Command,
server,
registrar,
)
}
}
}
impl<H> ConsoleCommand<H> {
pub const fn access_level(&self) -> CommandAccess {
self.header.access
}
pub fn allows(&self, server: Server<'_>, invoker: Invoker<'_>) -> bool {
match invoker {
Invoker::Server => self.header.access.allows_server(),
Invoker::Client(client) => self.header.accepts_client(server, client),
}
}
fn as_base(&self) -> NonNull<sys::ConCommandBase> {
ConCommandObject::as_base(NonNull::from(self).cast())
}
pub const fn client_filter(&self) -> Option<ClientFilter> {
self.header.client_filter
}
pub fn current_flags(&self, _server: Server<'_>) -> CommandFlags {
self.header.current_flags()
}
pub const fn handler(&self) -> &H {
&self.handler
}
#[doc(alias("GetHelpText"))]
pub const fn help_text(&self) -> &'static CStr {
self.header.help
}
#[doc(alias("GetName"))]
pub const fn name(&self) -> &'static CStr {
self.header.name
}
}
impl<H: std::fmt::Debug> std::fmt::Debug for ConsoleCommand<H> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConsoleCommand")
.field("name", &self.header.name)
.field("help", &self.header.help)
.field("access", &self.header.access)
.field("client_filter", &self.header.client_filter)
.field("flags", &self.header.flags)
.field("handler", &self.handler)
.finish_non_exhaustive()
}
}
unsafe impl<H: Sync> Sync for ConsoleCommand<H> {}
#[derive(Clone, Copy)]
pub(super) struct RegisteredCommand(NonNull<CommandHeader>);
impl RegisteredCommand {
pub(super) fn dispatch(self, context: &CommandContext<'_>) -> CommandResult {
unsafe { (self.dispatch)(self.0, context) }
}
}
impl std::ops::Deref for RegisteredCommand {
type Target = CommandHeader;
fn deref(&self) -> &CommandHeader {
unsafe { self.0.as_ref() }
}
}
unsafe fn dispatch_erased<H: CommandHandler>(
header: NonNull<CommandHeader>,
context: &CommandContext<'_>,
) -> CommandResult {
let command = unsafe { header.cast::<ConsoleCommand<H>>().as_ref() };
command.handler.dispatch(context)
}
unsafe fn engine_dispatch(object: NonNull<ConCommandObject>, command: NonNull<sys::CCommand>) {
let registered = RegisteredCommand(object.cast());
unsafe { route::dispatch_from_engine(registered, command) };
}
pub(super) unsafe fn register_base(
base: NonNull<sys::ConCommandBase>,
name: &'static CStr,
kind: CommandBaseKind,
server: Server<'_>,
registrar: &impl CommandRegistrar,
prepare: impl FnOnce(sys::CVarDLLIdentifier_t),
) -> Result<(), RegisterCommandError> {
let error = |error| RegisterCommandError::new(name, kind, error);
let cvar = server.cvar().map_err(|interface| error(interface.into()))?;
if unsafe { is_registered(base) } {
return Err(error(RegisterCommandErrorKind::AlreadyRegistered));
}
if let Some(existing) = cvar.find_command_base(name) {
let existing = if unsafe { vcall!(existing.as_ptr() => ConCommandBase_IsCommand()) } {
CommandBaseKind::Command
} else {
CommandBaseKind::Variable
};
return Err(error(RegisterCommandErrorKind::NameTaken(existing)));
}
prepare(cvar.allocate_dll_identifier());
unsafe { registrar.link(base) };
if unsafe { is_registered(base) } && cvar.find_command_base(name) == Some(base) {
return Ok(());
}
unsafe { registrar.unlink(base) };
Err(error(RegisterCommandErrorKind::NotLinked))
}
pub(super) unsafe fn unregister_base(
base: NonNull<sys::ConCommandBase>,
name: &'static CStr,
kind: CommandBaseKind,
server: Server<'_>,
registrar: &impl CommandRegistrar,
) -> Result<(), UnregisterCommandError> {
let error = |error| UnregisterCommandError::new(name, kind, error);
if !unsafe { is_registered(base) } {
return Err(error(UnregisterCommandErrorKind::NotRegistered));
}
let cvar = server.cvar().map_err(|interface| error(interface.into()))?;
unsafe { registrar.unlink(base) };
if unsafe { is_registered(base) } || cvar.find_command_base(name) == Some(base) {
return Err(error(UnregisterCommandErrorKind::StillLinked));
}
Ok(())
}