use super::error::{
CommandBaseKind, InvalidCommandName, RegisterCommandError, RegisterCommandErrorKind,
UnregisterCommandError, UnregisterCommandErrorKind, validate_name,
};
use super::registrar::{CommandRegistrar, UnlinksBeforeUnload};
use super::{CommandAccess, CommandContext, CommandFlags, CommandHandler, CommandResult, route};
use crate::abi::CppDestructors;
use crate::ffi::{NotThreadSafe, vcall};
use crate::server::{Server, ServerBinding};
use std::cell::{Cell, UnsafeCell};
use std::ffi::{CStr, c_char, c_int, c_void};
use std::marker::{PhantomData, PhantomPinned};
use std::mem::{MaybeUninit, offset_of, size_of};
use std::pin::Pin;
use std::ptr::{self, NonNull};
type DispatchFn = unsafe fn(NonNull<CommandHeader>, &CommandContext<'_>) -> CommandResult;
const _: () = {
assert!(offset_of!(CommandHeader, raw) == 0);
assert!(offset_of!(ConsoleCommand<()>, header) == 0);
assert!(offset_of!(sys::ConCommand, _base) == 0);
};
const _: () = {
use sys::ConCommand__bindgen_vtable as Engine;
assert!(size_of::<ConCommandVtable>() == size_of::<Engine>());
assert!(offset_of!(ConCommandVtable, is_command) == offset_of!(Engine, ConCommand_IsCommand));
assert!(offset_of!(ConCommandVtable, is_flag_set) == offset_of!(Engine, ConCommand_IsFlagSet));
assert!(offset_of!(ConCommandVtable, add_flags) == offset_of!(Engine, ConCommand_AddFlags));
assert!(offset_of!(ConCommandVtable, get_name) == offset_of!(Engine, ConCommand_GetName));
assert!(
offset_of!(ConCommandVtable, get_help_text) == offset_of!(Engine, ConCommand_GetHelpText)
);
assert!(
offset_of!(ConCommandVtable, is_registered) == offset_of!(Engine, ConCommand_IsRegistered)
);
assert!(
offset_of!(ConCommandVtable, get_dll_identifier)
== offset_of!(Engine, ConCommand_GetDLLIdentifier)
);
assert!(offset_of!(ConCommandVtable, create_base) == offset_of!(Engine, ConCommand_CreateBase));
assert!(offset_of!(ConCommandVtable, init) == offset_of!(Engine, ConCommand_Init));
assert!(
offset_of!(ConCommandVtable, auto_complete_suggest)
== offset_of!(Engine, ConCommand_AutoCompleteSuggest)
);
assert!(
offset_of!(ConCommandVtable, can_auto_complete)
== offset_of!(Engine, ConCommand_CanAutoComplete)
);
assert!(offset_of!(ConCommandVtable, dispatch) == offset_of!(Engine, ConCommand_Dispatch));
};
const _: () = assert!(size_of::<VtablePage>() == 4096);
static VTABLE: VtablePage = VtablePage(UnsafeCell::new(ConCommandVtable {
destructors: CppDestructors::new_noop(),
is_command,
is_flag_set,
add_flags,
get_name,
get_help_text,
is_registered,
get_dll_identifier,
create_base,
init,
auto_complete_suggest,
can_auto_complete,
dispatch,
}));
#[repr(C)]
pub(super) struct CommandHeader {
raw: UnsafeCell<sys::ConCommand>,
dispatch: DispatchFn,
name: &'static CStr,
help: &'static CStr,
access: CommandAccess,
flags: CommandFlags,
binding: Cell<Option<ServerBinding>>,
dll_identifier: Cell<sys::CVarDLLIdentifier_t>,
_pinned: PhantomPinned,
_not_thread_safe: NotThreadSafe,
}
impl CommandHeader {
pub(super) unsafe fn from_registered(
base: NonNull<sys::ConCommandBase>,
) -> Option<RegisteredCommand> {
let vtable_pointer = unsafe { (&raw const (*base.as_ptr()).vtable_).read() };
(vtable_pointer.cast::<c_void>() == vtable().cast()).then(|| RegisteredCommand(base.cast()))
}
unsafe fn from_this(this: *const sys::ConCommand) -> RegisteredCommand {
RegisteredCommand(unsafe { NonNull::new_unchecked(this.cast_mut().cast()) })
}
pub(super) const fn access(&self) -> CommandAccess {
self.access
}
pub(super) fn binding(&self) -> Option<ServerBinding> {
self.binding.get()
}
pub(super) fn current_flags(&self) -> c_int {
unsafe { (&raw const (*self.raw.get())._base.m_nFlags).read() }
}
fn is_registered(&self) -> bool {
unsafe { (&raw const (*self.raw.get())._base.m_bRegistered).read() }
}
pub(super) const fn name(&self) -> &'static CStr {
self.name
}
fn prepare(&self, binding: ServerBinding, dll_identifier: sys::CVarDLLIdentifier_t) {
let base = self.raw.get().cast::<sys::ConCommandBase>();
unsafe {
(&raw mut (*base).vtable_).write(vtable().cast());
(&raw mut (*base).m_pNext).write(ptr::null_mut());
(&raw mut (*base).m_pszName).write(self.name.as_ptr());
(&raw mut (*base).m_pszHelpString).write(self.help.as_ptr());
(&raw mut (*base).m_nFlags).write(self.flags.bits());
}
self.binding.set(Some(binding));
self.dll_identifier.set(dll_identifier);
}
}
#[repr(C)]
struct ConCommandVtable {
destructors: CppDestructors,
is_command: unsafe extern "C" fn(this: *const sys::ConCommand) -> bool,
is_flag_set: unsafe extern "C" fn(this: *const sys::ConCommand, flag: c_int) -> bool,
add_flags: unsafe extern "C" fn(this: *mut sys::ConCommand, flags: c_int),
get_name: unsafe extern "C" fn(this: *const sys::ConCommand) -> *const c_char,
get_help_text: unsafe extern "C" fn(this: *const sys::ConCommand) -> *const c_char,
is_registered: unsafe extern "C" fn(this: *const sys::ConCommand) -> bool,
get_dll_identifier:
unsafe extern "C" fn(this: *const sys::ConCommand) -> sys::CVarDLLIdentifier_t,
create_base: unsafe extern "C" fn(
this: *mut sys::ConCommand,
name: *const c_char,
help: *const c_char,
flags: c_int,
),
init: unsafe extern "C" fn(this: *mut sys::ConCommand),
auto_complete_suggest: unsafe extern "C" fn(
this: *mut sys::ConCommand,
partial: *const c_char,
commands: *mut c_void,
) -> c_int,
can_auto_complete: unsafe extern "C" fn(this: *mut sys::ConCommand) -> bool,
dispatch: unsafe extern "C" fn(this: *mut sys::ConCommand, command: *const sys::CCommand),
}
#[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 {
raw: UnsafeCell::new(unsafe { MaybeUninit::zeroed().assume_init() }),
dispatch: dispatch_erased::<H>,
name,
help: c"",
access: CommandAccess::Server,
flags: CommandFlags::NONE,
binding: Cell::new(None),
dll_identifier: Cell::new(0),
_pinned: PhantomPinned,
_not_thread_safe: PhantomData,
},
handler,
}
}
pub const fn access(mut self, access: CommandAccess) -> Self {
self.header.access = access;
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 error = |kind| RegisterCommandError::new(header.name, kind);
let cvar = server.cvar().map_err(|interface| error(interface.into()))?;
if header.is_registered() {
return Err(error(RegisterCommandErrorKind::AlreadyRegistered));
}
if let Some(existing) = cvar.find_command_base(header.name) {
let kind = if unsafe { vcall!(existing.as_ptr() => ConCommandBase_IsCommand()) } {
CommandBaseKind::Command
} else {
CommandBaseKind::Variable
};
return Err(error(RegisterCommandErrorKind::NameTaken(kind)));
}
header.prepare(binding, cvar.allocate_dll_identifier());
let command = self.get_ref().as_base();
unsafe { registrar.link(command) };
if header.is_registered() && cvar.find_command_base(header.name) == Some(command) {
return Ok(());
}
unsafe { registrar.unlink(command) };
Err(error(RegisterCommandErrorKind::NotLinked))
}
pub fn unregister(
&self,
server: Server<'_>,
registrar: &impl CommandRegistrar,
) -> Result<(), UnregisterCommandError> {
let header = &self.header;
let error = |kind| UnregisterCommandError::new(header.name, kind);
if !header.is_registered() {
return Err(error(UnregisterCommandErrorKind::NotRegistered));
}
let cvar = server.cvar().map_err(|interface| error(interface.into()))?;
let command = self.as_base();
unsafe { registrar.unlink(command) };
if header.is_registered() || cvar.find_command_base(header.name) == Some(command) {
return Err(error(UnregisterCommandErrorKind::StillLinked));
}
Ok(())
}
}
impl<H> ConsoleCommand<H> {
fn as_base(&self) -> NonNull<sys::ConCommandBase> {
NonNull::from(self).cast()
}
pub const fn handler(&self) -> &H {
&self.handler
}
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("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() }
}
}
#[repr(C, align(4096))]
struct VtablePage(UnsafeCell<ConCommandVtable>);
unsafe impl Sync for VtablePage {}
unsafe extern "C" fn add_flags(this: *mut sys::ConCommand, flags: c_int) {
unsafe {
let field = &raw mut (*this)._base.m_nFlags;
field.write(field.read() | (flags & !CommandFlags::GAME_DLL));
}
}
unsafe extern "C" fn auto_complete_suggest(
_this: *mut sys::ConCommand,
_partial: *const c_char,
_commands: *mut c_void,
) -> c_int {
0
}
unsafe extern "C" fn can_auto_complete(_this: *mut sys::ConCommand) -> bool {
false
}
unsafe extern "C" fn create_base(
_this: *mut sys::ConCommand,
_name: *const c_char,
_help: *const c_char,
_flags: c_int,
) {
}
unsafe extern "C" fn dispatch(this: *mut sys::ConCommand, command: *const sys::CCommand) {
let registered = unsafe { CommandHeader::from_this(this) };
unsafe { route::dispatch_from_engine(registered, command) };
}
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 extern "C" fn get_dll_identifier(this: *const sys::ConCommand) -> sys::CVarDLLIdentifier_t {
unsafe { CommandHeader::from_this(this) }
.dll_identifier
.get()
}
unsafe extern "C" fn get_help_text(this: *const sys::ConCommand) -> *const c_char {
unsafe { (&raw const (*this)._base.m_pszHelpString).read() }
}
unsafe extern "C" fn get_name(this: *const sys::ConCommand) -> *const c_char {
unsafe { (&raw const (*this)._base.m_pszName).read() }
}
unsafe extern "C" fn init(_this: *mut sys::ConCommand) {}
unsafe extern "C" fn is_command(_this: *const sys::ConCommand) -> bool {
true
}
unsafe extern "C" fn is_flag_set(this: *const sys::ConCommand, flag: c_int) -> bool {
let flags = unsafe { (&raw const (*this)._base.m_nFlags).read() };
flags & flag & !CommandFlags::GAME_DLL != 0
}
unsafe extern "C" fn is_registered(this: *const sys::ConCommand) -> bool {
unsafe { (&raw const (*this)._base.m_bRegistered).read() }
}
fn vtable() -> *const ConCommandVtable {
VTABLE.0.get()
}