use super::args::CommandArgs;
use super::object::{CommandHeader, RegisteredCommand};
use super::{Client, CommandContext, CommandError, CommandFlags, Invoker, line_from};
use crate::edicts::Edict;
use crate::interfaces::Cvar;
use crate::server::{Server, ServerBinding};
use sdk_raw::commands::CommandLine;
use sdk_raw::players::ABSOLUTE_PLAYER_LIMIT;
use std::any::Any;
use std::mem;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::NonNull;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[must_use]
pub enum ClientRoute {
Handled,
NotRouted,
}
impl ClientRoute {
pub const fn is_handled(self) -> bool {
matches!(self, Self::Handled)
}
}
fn cheats_allowed(cvar: Cvar<'_>) -> bool {
cvar.find_var(c"sv_cheats")
.is_some_and(|cheats| cheats.int() != 0)
}
pub(super) unsafe fn dispatch_from_engine(
header: RegisteredCommand,
command: NonNull<sys::CCommand>,
) {
let outcome = catch_unwind(AssertUnwindSafe(|| {
let Some(binding) = header.binding() else {
return;
};
let scope = ();
let server = unsafe { binding.server(&scope) };
let name = header.name().to_string_lossy();
let line = match unsafe { CommandLine::copy(command) } {
Ok(line) => line,
Err(error) => {
print_to_console(server, format_args!("{name}: {error}"));
return;
}
};
let context = CommandContext::new(
server,
CommandArgs::new(&line),
Invoker::Server,
header.name(),
);
if !header.access().allows_server() {
let _ = context.reply(format_args!("{name}: only players can use this command"));
return;
}
run(header, &context);
}));
if let Err(payload) = outcome {
drop_payload(payload);
}
}
pub(crate) fn drop_payload(payload: Box<dyn Any + Send>) {
if let Err(nested) = catch_unwind(AssertUnwindSafe(|| drop(payload))) {
mem::forget(nested);
}
}
fn panic_message(payload: &(dyn Any + Send)) -> &str {
payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("a non-string payload")
}
fn print_to_console(server: Server<'_>, message: std::fmt::Arguments<'_>) {
server.console_print(&line_from(message));
}
unsafe fn route(
server: Server<'_>,
edict: NonNull<sys::edict_t>,
command: NonNull<sys::CCommand>,
) -> ClientRoute {
let Ok(cvar) = server.cvar() else {
return ClientRoute::NotRouted;
};
let Ok(line) = (unsafe { CommandLine::copy(command) }) else {
return ClientRoute::NotRouted;
};
let args = CommandArgs::new(&line);
let Some(header) = cvar
.find_command_base(args.name())
.and_then(|found| unsafe { CommandHeader::from_registered(found) })
.filter(|header| header.access().allows_clients())
else {
return ClientRoute::NotRouted;
};
let edict = unsafe { Edict::from_raw(edict) };
if edict.is_free() || !(1..=ABSOLUTE_PLAYER_LIMIT).contains(&edict.index()) {
return ClientRoute::NotRouted;
}
let client = Client { edict };
if !header.accepts_client(server, client) {
return ClientRoute::NotRouted;
}
let context = CommandContext::new(server, args, Invoker::Client(client), header.name());
if header.current_flags().contains(CommandFlags::CHEAT) && !cheats_allowed(cvar) {
let _ = context.reply(format_args!(
"Can't use cheat command {} in multiplayer, unless the server has sv_cheats set to 1.",
header.name().to_string_lossy(),
));
return ClientRoute::Handled;
}
if run(header, &context) {
ClientRoute::NotRouted
} else {
ClientRoute::Handled
}
}
pub unsafe fn route_client_command(
binding: &ServerBinding,
edict: NonNull<sys::edict_t>,
command: NonNull<sys::CCommand>,
) -> ClientRoute {
let routed = catch_unwind(AssertUnwindSafe(|| {
let scope = ();
let server = unsafe { binding.server(&scope) };
unsafe { route(server, edict, command) }
}));
routed.unwrap_or_else(|payload| {
drop_payload(payload);
ClientRoute::NotRouted
})
}
fn run(header: RegisteredCommand, context: &CommandContext<'_>) -> bool {
let name = header.name().to_string_lossy();
let outcome = catch_unwind(AssertUnwindSafe(|| match header.dispatch(context) {
Ok(()) => false,
Err(CommandError::Unhandled) => true,
Err(error) => {
let _ = context.reply(format_args!("{name}: {error}"));
false
}
}));
let payload = match outcome {
Ok(unhandled) => return unhandled,
Err(payload) => payload,
};
print_to_console(
context.server(),
format_args!("{name}: the command panicked: {}", panic_message(&*payload)),
);
if matches!(context.invoker(), Invoker::Client(_)) {
let _ = context.reply(format_args!("{name}: the command failed"));
}
drop_payload(payload);
false
}