#[cfg(test)]
#[path = "../tests/commands/variable.rs"]
mod tests;
use super::CommandFlags;
use super::error::{
CommandBaseKind, InvalidCommandName, RegisterCommandError, UnregisterCommandError,
validate_name,
};
use super::object::{register_base, unregister_base};
use super::registrar::{CommandRegistrar, UnlinksBeforeUnload};
use super::route::drop_payload;
use crate::interfaces::Cvar;
use crate::server::{Server, ServerBinding};
use sdk_raw::commands::{
ConVarHooks, ConVarObject, format_float, format_int, is_registered, parse_float,
};
use std::cell::Cell;
use std::ffi::{CStr, CString, c_int};
use std::mem::offset_of;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::pin::Pin;
use std::ptr::NonNull;
const _: () = assert!(offset_of!(ConsoleVariable, object) == 0);
static HOOKS: ConVarHooks = ConVarHooks {
set_string: engine_set_string,
set_float: engine_set_float,
set_int: engine_set_int,
clamp: engine_clamp,
change_string: engine_change_string,
};
#[doc(alias("ConVar"))]
#[repr(C)]
pub struct ConsoleVariable {
object: ConVarObject,
name: &'static CStr,
help: &'static CStr,
flags: CommandFlags,
min: Option<f32>,
max: Option<f32>,
read_only: bool,
binding: Cell<Option<ServerBinding>>,
}
impl ConsoleVariable {
pub const fn new(name: &'static CStr, default: &'static CStr) -> Self {
match validate_name(name) {
Ok(()) => {}
Err(InvalidCommandName::Empty) => panic!("console variable names cannot be empty"),
Err(InvalidCommandName::TooLong) => {
panic!("console variable names cannot be longer than 63 bytes")
}
Err(InvalidCommandName::InvalidByte { .. }) => {
panic!("console variable names may only contain ASCII letters, digits, and `_`")
}
Err(InvalidCommandName::Reserved) => {
panic!("the engine reserves this name for its own client commands")
}
}
Self {
object: ConVarObject::new(default, &HOOKS),
name,
help: c"",
flags: CommandFlags::NONE,
min: None,
max: None,
read_only: false,
binding: Cell::new(None),
}
}
fn as_base(&self) -> NonNull<sys::ConCommandBase> {
ConVarObject::as_base(self.object_ptr())
}
#[doc(alias("GetBool"))]
pub fn bool(&self, server: Server<'_>) -> bool {
self.int(server) != 0
}
fn change_callbacks<'s>(&self, server: Option<Server<'s>>) -> Option<Cvar<'s>> {
if !unsafe { is_registered(self.as_base()) } {
return None;
}
server?.cvar().ok()
}
fn change_string(&self, callbacks: Option<Cvar<'_>>, value: CString, old_float: f32) {
let replaced = self.object.replace_string(value);
if let (true, Some(cvar)) = (replaced.changed(), callbacks) {
let object = self.object_ptr();
unsafe { ConVarObject::call_change_callback(object, replaced.old(), old_float) };
unsafe {
cvar.call_global_change_callbacks(
ConVarObject::as_var(object),
replaced.old(),
old_float,
)
};
}
}
fn clamp(&self, value: &mut f32) -> bool {
if let Some(min) = self.min
&& *value < min
{
*value = min;
return true;
}
if let Some(max) = self.max
&& *value > max
{
*value = max;
return true;
}
false
}
#[doc(alias("GetDefault"))]
pub const fn default_value(&self) -> &'static CStr {
self.object.default()
}
pub const fn flags(mut self, flags: CommandFlags) -> Self {
self.flags = flags;
self
}
#[doc(alias("GetFloat"))]
pub fn float(&self, _server: Server<'_>) -> f32 {
self.object.float()
}
pub const fn help(mut self, help: &'static CStr) -> Self {
self.help = help;
self
}
#[doc(alias("GetInt"))]
pub fn int(&self, _server: Server<'_>) -> c_int {
self.object.int()
}
pub const fn max(mut self, max: f32) -> Self {
self.max = Some(max);
self
}
pub const fn min(mut self, min: f32) -> Self {
self.min = Some(min);
self
}
#[doc(alias("GetName"))]
pub const fn name(&self) -> &'static CStr {
self.name
}
fn object_ptr(&self) -> NonNull<ConVarObject> {
NonNull::from(self).cast()
}
pub const fn read_only(mut self) -> Self {
self.read_only = true;
self
}
unsafe fn prepare(&self, binding: ServerBinding, dll_identifier: sys::CVarDLLIdentifier_t) {
unsafe {
ConVarObject::prepare(
self.object_ptr(),
self.name,
self.help,
self.flags.bits(),
self.min,
self.max,
dll_identifier,
)
};
self.binding.set(Some(binding));
}
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 variable = self.get_ref();
let prepare = |dll_identifier| unsafe { variable.prepare(binding, dll_identifier) };
unsafe {
register_base(
variable.as_base(),
variable.name,
CommandBaseKind::Variable,
server,
registrar,
prepare,
)
}
}
#[doc(alias("Revert"))]
pub fn revert(&self, server: Server<'_>) {
self.set_string(server, self.default_value());
}
#[doc(alias("SetValue"))]
pub fn set_float(&self, server: Server<'_>, value: f32) {
self.set_float_value(self.change_callbacks(Some(server)), value, false);
}
fn set_float_value(&self, callbacks: Option<Cvar<'_>>, value: f32, force: bool) {
if value == self.object.float() && !force {
return;
}
let mut value = value;
self.clamp(&mut value);
self.set_values(callbacks, value, value as c_int, || format_float(value));
}
#[doc(alias("SetValue"))]
pub fn set_int(&self, server: Server<'_>, value: c_int) {
self.set_int_value(self.change_callbacks(Some(server)), value);
}
fn set_int_value(&self, callbacks: Option<Cvar<'_>>, value: c_int) {
if value == self.object.int() {
return;
}
let mut float = value as f32;
let int = if self.clamp(&mut float) {
float as c_int
} else {
value
};
self.set_values(callbacks, float, int, || format_int(int));
}
#[doc(alias("SetValue"))]
pub fn set_string(&self, server: Server<'_>, value: &CStr) {
self.set_string_value(self.change_callbacks(Some(server)), Some(value));
}
fn set_string_value(&self, callbacks: Option<Cvar<'_>>, value: Option<&CStr>) {
let mut float = value.map_or(0.0, |value| parse_float(value.to_bytes()) as f32);
let clamped = self.clamp(&mut float);
self.set_values(callbacks, float, float as c_int, || match value {
_ if clamped => format_float(float),
Some(value) => value.to_owned(),
None => CString::default(),
});
}
fn set_values(
&self,
callbacks: Option<Cvar<'_>>,
float: f32,
int: c_int,
text: impl FnOnce() -> CString,
) {
let old_float = self.object.float();
self.object.set_numbers(float, int);
if !CommandFlags::from_bits_retain(self.object.flags())
.contains(CommandFlags::NEVER_AS_STRING)
{
self.change_string(callbacks, text(), old_float);
}
}
#[doc(alias("GetString"))]
pub fn string(&self, _server: Server<'_>) -> CString {
self.object.string()
}
pub fn unregister(
&self,
server: Server<'_>,
registrar: &impl CommandRegistrar,
) -> Result<(), UnregisterCommandError> {
unsafe {
unregister_base(
self.as_base(),
self.name,
CommandBaseKind::Variable,
server,
registrar,
)
}
}
}
impl std::fmt::Debug for ConsoleVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConsoleVariable")
.field("name", &self.name)
.field("default", &self.default_value())
.field("help", &self.help)
.field("flags", &self.flags)
.field("min", &self.min)
.field("max", &self.max)
.field("read_only", &self.read_only)
.finish_non_exhaustive()
}
}
unsafe impl Sync for ConsoleVariable {}
unsafe fn engine_change_string(variable: NonNull<ConVarObject>, value: &CStr, old_float: f32) {
unsafe {
set_from_engine(variable, |variable, callbacks| {
variable.change_string(callbacks, value.to_owned(), old_float)
})
};
}
unsafe fn engine_clamp(variable: NonNull<ConVarObject>, value: &mut f32) -> bool {
unsafe { from_engine(variable, |variable, _| variable.clamp(value)) }.unwrap_or(false)
}
unsafe fn engine_set_float(variable: NonNull<ConVarObject>, value: f32, force: bool) {
unsafe {
set_from_engine(variable, |variable, callbacks| {
variable.set_float_value(callbacks, value, force)
})
};
}
unsafe fn engine_set_int(variable: NonNull<ConVarObject>, value: c_int) {
unsafe {
set_from_engine(variable, |variable, callbacks| {
variable.set_int_value(callbacks, value)
})
};
}
unsafe fn engine_set_string(variable: NonNull<ConVarObject>, value: Option<&CStr>) {
unsafe {
set_from_engine(variable, |variable, callbacks| {
variable.set_string_value(callbacks, value)
})
};
}
unsafe fn from_engine<R>(
variable: NonNull<ConVarObject>,
f: impl FnOnce(&ConsoleVariable, Option<Server<'_>>) -> R,
) -> Option<R> {
let outcome = catch_unwind(AssertUnwindSafe(|| {
let variable = unsafe { variable.cast::<ConsoleVariable>().as_ref() };
let scope = ();
let server = variable
.binding
.get()
.map(|binding| unsafe { binding.server(&scope) });
f(variable, server)
}));
outcome.map_err(drop_payload).ok()
}
unsafe fn set_from_engine(
variable: NonNull<ConVarObject>,
f: impl FnOnce(&ConsoleVariable, Option<Cvar<'_>>),
) {
unsafe {
from_engine(variable, |variable, server| match variable.read_only {
true => {
if let Some(server) = server {
let name = variable.name.to_string_lossy();
server.console_print(&super::line_from(format_args!("{name} is read-only.")));
}
}
false => f(variable, variable.change_callbacks(server)),
})
};
}