#[cfg(test)]
#[path = "../../tests/interfaces/cvar.rs"]
mod tests;
use crate::NotThreadSafe;
use crate::commands::{CommandBaseKind, CommandFlags, drop_payload};
use crate::server::{Server, ServerBinding};
use sdk_raw::util::cstr::{borrow_cstr, copy_cstr};
use sdk_raw::vcall;
use std::cell::Cell;
use std::ffi::{CStr, CString, c_char, c_int};
use std::iter::FusedIterator;
use std::marker::PhantomData;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::NonNull;
const MAX_LISTED: usize = 1 << 16;
thread_local! {
static WATCHING: Cell<Option<Watching>> = const { Cell::new(None) };
}
interface! {
#[doc(alias("ICvar"))]
pub struct Cvar(sys::ICvar) = Engine sdk_raw::interfaces::cvar::VERSION;
}
pub type ConVarChangeFn = for<'s> fn(server: Server<'s>, change: ConVarChange<'s>);
#[doc(alias("ConCommandBase"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CommandBase<'s> {
raw: NonNull<sys::ConCommandBase>,
kind: CommandBaseKind,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> CommandBase<'s> {
pub const fn as_ptr(self) -> *mut sys::ConCommandBase {
self.raw.as_ptr()
}
pub const fn as_var(self) -> Option<ConVar<'s>> {
match self.kind {
CommandBaseKind::Command => None,
CommandBaseKind::Variable => Some(unsafe { ConVar::from_raw(self.raw.cast()) }),
}
}
#[doc(alias("GetFlags", "IsFlagSet"))]
pub fn flags(self) -> CommandFlags {
if let Some(var) = self.as_var() {
return var.flags();
}
CommandFlags::from_bits_retain(unsafe { (&raw const (*self.as_ptr()).m_nFlags).read() })
}
#[doc(alias("IsCommand"))]
pub const fn kind(self) -> CommandBaseKind {
self.kind
}
#[doc(alias("GetName"))]
pub fn name(self) -> &'s CStr {
unsafe { borrow_cstr((&raw const (*self.as_ptr()).m_pszName).read()) }.unwrap_or_default()
}
#[doc(alias("GetHelpText"))]
pub fn help_text(self) -> &'s CStr {
if let Some(var) = self.as_var() {
return var.help_text();
}
unsafe { borrow_cstr((&raw const (*self.as_ptr()).m_pszHelpString).read()) }
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
pub struct CommandBases<'s> {
listed: std::vec::IntoIter<CommandBase<'s>>,
}
impl DoubleEndedIterator for CommandBases<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.listed.next_back()
}
}
impl ExactSizeIterator for CommandBases<'_> {}
impl FusedIterator for CommandBases<'_> {}
impl<'s> Iterator for CommandBases<'s> {
type Item = CommandBase<'s>;
fn next(&mut self) -> Option<Self::Item> {
self.listed.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.listed.size_hint()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConVar<'s> {
raw: NonNull<sys::ConVar>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> ConVar<'s> {
pub(crate) const unsafe fn from_raw(raw: NonNull<sys::ConVar>) -> Self {
Self {
raw,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
pub const fn as_ptr(self) -> *mut sys::ConVar {
self.raw.as_ptr()
}
#[doc(alias("GetDefault"))]
pub fn default_string(self) -> CString {
unsafe { copy_cstr((&raw const (*self.parent()).m_pszDefaultValue).read()) }
.unwrap_or_default()
}
#[doc(alias("GetMin", "GetMax"))]
pub fn bounds(self) -> (Option<f32>, Option<f32>) {
let parent = self.parent();
unsafe {
(
(&raw const (*parent).m_bHasMin)
.read()
.then(|| (&raw const (*parent).m_fMinVal).read()),
(&raw const (*parent).m_bHasMax)
.read()
.then(|| (&raw const (*parent).m_fMaxVal).read()),
)
}
}
#[doc(alias("GetHelpText"))]
pub fn help_text(self) -> &'s CStr {
unsafe { borrow_cstr((&raw const (*self.parent())._base.m_pszHelpString).read()) }
.unwrap_or_default()
}
#[doc(alias("GetFlags", "IsFlagSet"))]
pub fn flags(self) -> CommandFlags {
CommandFlags::from_bits_retain(unsafe {
(&raw const (*self.parent())._base.m_nFlags).read()
})
}
#[doc(alias("GetFloat"))]
pub fn float(self) -> f32 {
unsafe { (&raw const (*self.parent()).m_fValue).read() }
}
#[doc(alias("GetInt"))]
pub fn int(self) -> c_int {
unsafe { (&raw const (*self.parent()).m_nValue).read() }
}
fn interface(self) -> *mut sys::IConVar {
unsafe { &raw mut (*self.as_ptr())._base_1 }
}
pub fn is_default(self) -> bool {
let parent = self.parent();
unsafe {
let current = borrow_cstr((&raw const (*parent).m_pszString).read());
let default = borrow_cstr((&raw const (*parent).m_pszDefaultValue).read());
current.unwrap_or_default() == default.unwrap_or_default()
}
}
#[doc(alias("GetName"))]
pub fn name(self) -> &'s CStr {
unsafe { borrow_cstr((&raw const (*self.as_ptr())._base.m_pszName).read()) }
.unwrap_or_default()
}
fn parent(self) -> *mut sys::ConVar {
let parent = unsafe { (&raw const (*self.as_ptr()).m_pParent).read() };
if parent.is_null() {
self.as_ptr()
} else {
parent
}
}
pub fn quietly<R>(self, f: impl FnOnce() -> R) -> R {
struct Announce {
flags: *mut c_int,
announced: c_int,
}
impl Drop for Announce {
fn drop(&mut self) {
unsafe { self.flags.write(self.flags.read() | self.announced) };
}
}
let notify = CommandFlags::NOTIFY.bits();
let flags = unsafe { &raw mut (*self.parent())._base.m_nFlags };
let announced = unsafe { flags.read() } & notify;
unsafe { flags.write(flags.read() & !notify) };
let _announce = Announce { flags, announced };
f()
}
#[doc(alias("SetValue"))]
pub fn set_float(self, value: f32) {
unsafe { vcall!(self.interface() => IConVar_SetValue1(value)) };
}
#[doc(alias("SetValue"))]
pub fn set_int(self, value: c_int) {
unsafe { vcall!(self.interface() => IConVar_SetValue2(value)) };
}
#[doc(alias("SetValue"))]
pub fn set_string(self, value: &CStr) {
unsafe { vcall!(self.interface() => IConVar_SetValue(value.as_ptr())) };
}
pub fn set_string_quietly(self, value: &CStr) {
self.quietly(|| self.set_string(value));
}
#[doc(alias("GetString"))]
pub fn string(self) -> CString {
unsafe { copy_cstr((&raw const (*self.parent()).m_pszString).read()) }.unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ConVarChange<'s> {
name: &'s CStr,
var: Option<ConVar<'s>>,
old_string: &'s CStr,
old_float: f32,
_not_thread_safe: NotThreadSafe,
}
impl<'s> ConVarChange<'s> {
#[doc(alias("GetName"))]
pub const fn name(self) -> &'s CStr {
self.name
}
pub const fn old_float(self) -> f32 {
self.old_float
}
pub const fn old_string(self) -> &'s CStr {
self.old_string
}
pub const fn var(self) -> Option<ConVar<'s>> {
self.var
}
}
#[must_use = "dropping the watch stops it"]
#[derive(Debug)]
pub struct ConVarWatch {
cvar: NonNull<sys::ICvar>,
installed: sys::FnChangeCallback_t,
_not_thread_safe: NotThreadSafe,
}
impl ConVarWatch {
#[doc(alias("RemoveGlobalChangeCallback"))]
pub fn stop(self) {
drop(self);
}
}
impl Drop for ConVarWatch {
fn drop(&mut self) {
unsafe { vcall!(self.cvar.as_ptr() => ICvar_RemoveGlobalChangeCallback(self.installed)) };
WATCHING.set(None);
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConVarWatchError {
#[error("console variable changes are already being watched")]
AlreadyWatching,
}
impl<'s> Cvar<'s> {
#[doc(alias("AllocateDLLIdentifier"))]
pub(crate) fn allocate_dll_identifier(self) -> sys::CVarDLLIdentifier_t {
unsafe { vcall!(self.as_ptr() => ICvar_AllocateDLLIdentifier()) }
}
#[doc(alias("CallGlobalChangeCallbacks"))]
pub(crate) unsafe fn call_global_change_callbacks(
self,
var: NonNull<sys::ConVar>,
old_value: &CStr,
old_float: f32,
) {
unsafe {
vcall!(self.as_ptr() => ICvar_CallGlobalChangeCallbacks(
var.as_ptr(),
old_value.as_ptr(),
old_float,
))
};
}
#[doc(alias("GetCommands", "GetNext"))]
pub fn command_bases(self) -> CommandBases<'s> {
let mut listed = Vec::new();
let mut next = unsafe { vcall!(self.as_ptr() => ICvar_GetCommands()) };
while let Some(base) = NonNull::new(next)
&& listed.len() < MAX_LISTED
{
let is_command = unsafe { vcall!(base.as_ptr() => ConCommandBase_IsCommand()) };
next = unsafe { (&raw const (*base.as_ptr()).m_pNext).read() };
listed.push(CommandBase {
raw: base,
kind: if is_command {
CommandBaseKind::Command
} else {
CommandBaseKind::Variable
},
_scope: PhantomData,
_not_thread_safe: PhantomData,
});
}
CommandBases {
listed: listed.into_iter(),
}
}
#[doc(alias("ConsolePrintf"))]
pub(crate) fn console_printf(self, message: &CStr) {
unsafe { vcall!(self.as_ptr() => ICvar_ConsolePrintf(c"%s".as_ptr(), message.as_ptr())) };
}
#[doc(alias("FindCommandBase"))]
pub(crate) fn find_command_base(self, name: &CStr) -> Option<NonNull<sys::ConCommandBase>> {
NonNull::new(unsafe { vcall!(self.as_ptr() => ICvar_FindCommandBase(name.as_ptr())) })
}
#[doc(alias("FindVar"))]
pub fn find_var(self, name: &CStr) -> Option<ConVar<'s>> {
let var = NonNull::new(unsafe { vcall!(self.as_ptr() => ICvar_FindVar(name.as_ptr())) })?;
Some(unsafe { ConVar::from_raw(var) })
}
#[doc(alias("RegisterConCommand"))]
pub(crate) unsafe fn register_con_command(self, command: NonNull<sys::ConCommandBase>) {
unsafe { vcall!(self.as_ptr() => ICvar_RegisterConCommand(command.as_ptr())) };
}
#[doc(alias("UnregisterConCommand"))]
pub(crate) unsafe fn unregister_con_command(self, command: NonNull<sys::ConCommandBase>) {
unsafe { vcall!(self.as_ptr() => ICvar_UnregisterConCommand(command.as_ptr())) };
}
#[doc(alias("GetCommands"))]
pub fn vars(self) -> impl DoubleEndedIterator<Item = ConVar<'s>> + FusedIterator {
self.command_bases().filter_map(CommandBase::as_var)
}
#[doc(alias("InstallGlobalChangeCallback"))]
pub fn watch_changes(
self,
binding: ServerBinding,
callback: ConVarChangeFn,
) -> Result<ConVarWatch, ConVarWatchError> {
if WATCHING.get().is_some() {
return Err(ConVarWatchError::AlreadyWatching);
}
let installed: sys::FnChangeCallback_t = Some(changed);
WATCHING.set(Some(Watching {
binding,
callback,
cvar: self.raw,
}));
unsafe { vcall!(self.as_ptr() => ICvar_InstallGlobalChangeCallback(installed)) };
Ok(ConVarWatch {
cvar: self.raw,
installed,
_not_thread_safe: PhantomData,
})
}
}
#[derive(Clone, Copy)]
struct Watching {
binding: ServerBinding,
callback: ConVarChangeFn,
cvar: NonNull<sys::ICvar>,
}
unsafe extern "C" fn changed(var: *mut sys::IConVar, old_string: *const c_char, old_float: f32) {
let (Ok(Some(watching)), Some(var)) = (WATCHING.try_with(Cell::get), NonNull::new(var)) else {
return;
};
catch_unwind(AssertUnwindSafe(|| {
let scope = ();
let server = unsafe { watching.binding.server(&scope) };
let cvar = unsafe { Cvar::from_raw(watching.cvar) };
unsafe { notify(server, cvar, var, old_string, old_float, watching.callback) };
}))
.map_err(drop_payload)
.ok();
}
unsafe fn notify<'s>(
server: Server<'s>,
cvar: Cvar<'s>,
var: NonNull<sys::IConVar>,
old_string: *const c_char,
old_float: f32,
callback: ConVarChangeFn,
) {
let name =
unsafe { borrow_cstr(vcall!(var.as_ptr() => IConVar_GetName())) }.unwrap_or_default();
let listed = cvar
.find_var(name)
.filter(|listed| listed.interface() == var.as_ptr());
let unlisted;
let name = match listed {
Some(listed) => listed.name(),
None => {
unlisted = name.to_owned();
unlisted.as_c_str()
}
};
callback(
server,
ConVarChange {
name,
var: listed,
old_string: unsafe { borrow_cstr(old_string) }.unwrap_or_default(),
old_float,
_not_thread_safe: PhantomData,
},
);
}