rrplug/high/engine/
convars.rs

1//! **convars** are simple. You register them then you can read or edit its data; either in the plugin or from scripts.
2//!
3//! they can either be a
4//! 1. `string` (`String`)
5//! 2. `int` (`i32`)
6//! 3. `float` (`f32`)
7//!
8//! ## Safety
9//!
10//! convars are also being accessed by the sqvm or the engine so it is unsafe to read or edit them from threads.
11//!
12//! so they must read/writen only from **convar callbacks**, **concommands** or **native sqfunction**
13//!
14//! if you like **taking risks** you can ignore this or **if** the sqvm/engine never read or edit the convar.
15//!
16//!
17//! ## Working with Convars
18//!
19//! convars can be created at any time after engine load but its better to create them when the engine loads
20//!
21//! ```no_run
22//! use rrplug::prelude::*;
23//! use rrplug::exports::OnceCell; // just as a example
24//!
25//! // inside Plugin impl
26//! fn on_engine_load(engine_data: Option<&EngineData>, _dll_ptr: &DLLPointer, engine_token: EngineToken) {
27//!     let Some(_) = engine_data else {
28//!         return;
29//!     };
30//!
31//!     let register_info = ConVarRegister { // struct containing info the convar ( there is a lot of stuff )
32//!         ..ConVarRegister::mandatory(
33//!         "cool_convar",
34//!         "cool_default",
35//!         0,
36//!         "this is a cool convar",
37//!     )
38//!     };
39//!    
40//!     let convar = ConVarStruct::try_new(&register_info, engine_token).unwrap(); // create and register the convar
41//!     _ = COOLCONVAR.set(convar);
42//! }
43//!
44//! // to access your convar, you will have to save them into a static or in the plugin struct
45//!
46//! static COOLCONVAR: OnceCell<ConVarStruct> = OnceCell::new();
47//!
48//! // reading it from a convar change callback
49//! #[rrplug::convar]
50//! fn cool_convar_change_callback(old_value: String, float_old_value: f32) {
51//!     let convar = COOLCONVAR.wait();
52//!
53//!     log::info!("convar name: {}", convar.get_name());
54//!     log::info!("new value: {}", convar.get_value().value);
55//!     log::info!("old value: {}", old_value)
56//! }
57//! ```
58
59use std::{
60    alloc::{GlobalAlloc, Layout},
61    borrow::Cow,
62    ffi::{c_char, c_void, CStr},
63    mem,
64    ptr::addr_of_mut,
65};
66
67use super::EngineData;
68#[cfg(doc)]
69use crate::bindings::cvar::RawCVar;
70use crate::{
71    bindings::cvar::convar::{ConVar, FnChangeCallback_t, FCVAR_NEVER_AS_STRING},
72    errors::{CStringPtrError, CVarQueryError, RegisterError},
73    mid::{
74        engine::{get_engine_data, ENGINE_DATA},
75        source_alloc::SOURCE_ALLOC,
76        utils::{to_cstring, try_cstring},
77    },
78    prelude::EngineToken,
79};
80
81/// the state of the convar in all of its possible types
82///
83/// values are always valid
84#[derive(Debug, PartialEq)]
85pub struct ConVarValues {
86    /// string value
87    ///
88    /// the strings should always be valid utf8
89    pub value: String,
90    /// float value will be 0 if it's a string
91    pub value_float: f32,
92    /// int representation of the convar will be 0 if it's a string
93    pub value_int: i32,
94}
95
96/// [`ConVarRegister`] is builder sturct for convars
97///
98/// consumed by [`ConVarStruct`]`::register`
99pub struct ConVarRegister {
100    /// literally the name of the convar
101    ///
102    /// This is **required**
103    pub name: String,
104    /// the default value
105    ///
106    /// This is **required**
107    pub default_value: String,
108    /// any flags like [`crate::bindings::cvar::convar::FCVAR_GAMEDLL`]
109    ///
110    /// This is **required**
111    pub flags: i32,
112    /// the help string
113    pub help_string: &'static str,
114    /// should use min or not
115    pub bmin: bool,
116    /// min value for floats and integers
117    pub fmin: f32,
118    /// should use max or not
119    pub bmax: bool,
120    /// max value for floats and integers
121    pub fmax: f32,
122    /// callbak when the convar is changed should be created with [`crate::convar`]
123    pub callback: FnChangeCallback_t,
124}
125
126impl ConVarRegister {
127    /// creates a new [`ConVarRegister`]
128    pub fn new(
129        name: impl Into<String>,
130        default_value: impl Into<String>,
131        flags: i32,
132        help_string: &'static str,
133    ) -> Self {
134        Self::mandatory(name, default_value, flags, help_string)
135    }
136
137    /// all the required fields to register a convar
138    ///
139    /// can be used in this way to inforce it
140    /// ```
141    /// # use rrplug::prelude::*;
142    /// _ = ConVarRegister {
143    ///     ..ConVarRegister::mandatory(
144    ///     "a_convar",
145    ///     "default_value",
146    ///     0,
147    ///     "this is a convar",
148    /// )
149    /// };
150    /// ```
151    pub fn mandatory(
152        name: impl Into<String>,
153        default_value: impl Into<String>,
154        flags: i32,
155        help_string: &'static str,
156    ) -> Self {
157        Self {
158            name: name.into(),
159            default_value: default_value.into(),
160            flags,
161            help_string,
162            bmin: bool::default(),
163            fmin: f32::default(),
164            bmax: bool::default(),
165            fmax: f32::default(),
166            callback: None,
167        }
168    }
169}
170
171// so the convars have to init at mod scope with a macro
172// sync and send must be removed from ConVarStruct to forbid unsafe access with race condition
173// altought a unsafe way of initing the ConVarStruct should be given
174// of course this adds extra overhead with forced atomics for convars but it's required to make everything harder to break by accident
175// also concommand and convar macros should give an option of including the plugin struct like a self input :P
176// note: not all dlls are loaded on the engine thread
177
178/// [`ConVarStruct`] wraps unsafe code in a safe api for convar access
179///
180/// ### Thread Safety
181/// even thought [`Sync`] and [`Send`] are implemented for this struct
182///
183/// it is not safe to call any of its functions outside of titanfall's engine callbacks to plugins
184/// and may result in race condition (really bad thing)
185pub struct ConVarStruct {
186    inner: &'static mut ConVar,
187}
188
189impl ConVarStruct {
190    /// creates and registers a convar from [`ConVarRegister`]
191    ///
192    /// this functions leaks the strings since convars live for the lifetime of the game :)
193    ///
194    /// # Example
195    /// ```no_run
196    /// # use rrplug::prelude::*;
197    /// # let engine_token = unsafe { EngineToken::new_unchecked() };
198    /// let register_info = ConVarRegister { // struct containing info the convar ( there is a lot of stuff )
199    ///     ..ConVarRegister::mandatory(
200    ///     "a_convar",
201    ///     "default_value",
202    ///     0,
203    ///     "this is a convar",
204    /// )
205    /// };
206    ///
207    /// let convar = ConVarStruct::try_new(&register_info, engine_token).unwrap(); // create and register the convar
208    /// ```
209    pub fn try_new(register_info: &ConVarRegister, _: EngineToken) -> Result<Self, RegisterError> {
210        get_engine_data()
211            .map(move |engine| unsafe { Self::internal_try_new(engine, register_info) })
212            .unwrap_or_else(|| Err(RegisterError::NoneFunction))
213    }
214
215    #[inline]
216    unsafe fn internal_try_new(
217        engine_data: &EngineData,
218        register_info: &ConVarRegister,
219    ) -> Result<Self, RegisterError> {
220        let convar_classes = engine_data.convar;
221        let convar = unsafe {
222            let convar = SOURCE_ALLOC.alloc_zeroed(Layout::new::<ConVar>()) as *mut ConVar;
223
224            addr_of_mut!((*convar).m_ConCommandBase.m_pConCommandBaseVTable)
225                .write(convar_classes.convar_vtable);
226
227            addr_of_mut!((*convar).m_ConCommandBase.s_pConCommandBases)
228                .write(convar_classes.iconvar_vtable);
229
230            #[allow(clippy::crosspointer_transmute)] // its what c++ this->convar_malloc is
231            (convar_classes.convar_malloc)(addr_of_mut!((*convar).m_pMalloc).cast(), 0, 0); // Allocate new memory for ConVar.
232
233            convar
234        };
235
236        debug_assert!(!register_info.name.is_empty());
237
238        // TODO: could be optimized to not allocated a cstring
239
240        let name = try_cstring(&register_info.name)?.into_bytes_with_nul();
241        let name_ptr =
242            unsafe {
243                SOURCE_ALLOC.alloc(Layout::array::<c_char>(name.len()).expect(
244                    "the Layout for a char array became too large : string allocation failed",
245                ))
246            };
247        unsafe { name_ptr.copy_from_nonoverlapping(name.as_ptr(), name.len()) };
248
249        let default_value = try_cstring(&register_info.default_value)?.into_bytes_with_nul();
250        let default_value_ptr =
251            unsafe {
252                SOURCE_ALLOC.alloc(Layout::array::<c_char>(default_value.len()).expect(
253                    "the Layout for a char array became too large : string allocation failed",
254                ))
255            };
256        unsafe {
257            default_value_ptr.copy_from_nonoverlapping(default_value.as_ptr(), default_value.len())
258        };
259
260        let help_string = try_cstring(register_info.help_string)?.into_bytes_with_nul();
261        let help_string_ptr =
262            unsafe {
263                SOURCE_ALLOC.alloc(Layout::array::<c_char>(help_string.len()).expect(
264                    "the Layout for a char array became too large : string allocation failed",
265                ))
266            };
267        unsafe {
268            help_string_ptr.copy_from_nonoverlapping(help_string.as_ptr(), help_string.len())
269        };
270
271        unsafe {
272            (convar_classes.convar_register)(
273                convar,
274                name_ptr as *const i8,
275                default_value_ptr as *const i8,
276                register_info.flags,
277                help_string_ptr as *const i8,
278                register_info.bmin,
279                register_info.fmin,
280                register_info.bmax,
281                register_info.fmax,
282                register_info.callback,
283            )
284        }
285
286        log::info!("Registering ConVar {}", register_info.name);
287
288        Ok(Self {
289            inner: unsafe { &mut *convar }, // no way this is invalid
290        })
291    }
292
293    /// creates a [`ConVarStruct`] from the convar name by searching for it with the [`RawCVar`]
294    ///
295    /// # Errors
296    ///
297    /// This function will return an error if the cvar doesn't exist
298    pub fn find_convar_by_name(name: &str, _: EngineToken) -> Result<Self, CVarQueryError> {
299        let name = try_cstring(name)?;
300
301        Ok(Self {
302            inner: unsafe {
303                ENGINE_DATA
304                    .get()
305                    .ok_or(CVarQueryError::NoCVarInterface)?
306                    .cvar
307                    .find_convar(name.as_ptr())
308                    .as_mut()
309                    .ok_or(CVarQueryError::NotFound)?
310            },
311        })
312    }
313
314    /// get the name of the convar
315    pub fn get_name(&self) -> String {
316        unsafe {
317            CStr::from_ptr(self.inner.m_ConCommandBase.m_pszName)
318                .to_string_lossy()
319                .to_string()
320        }
321    }
322
323    /// get the value inside the convar
324    pub fn get_value(&self) -> ConVarValues {
325        unsafe {
326            let value = &self.inner.m_Value;
327
328            let string = if !value.m_pszString.is_null()
329                && !self.has_flags(
330                    FCVAR_NEVER_AS_STRING
331                        .try_into()
332                        .expect("supposed to always work"),
333                ) {
334                CStr::from_ptr(value.m_pszString)
335                    .to_string_lossy()
336                    .to_string()
337            } else {
338                "".to_string()
339            };
340
341            ConVarValues {
342                value: string,
343                value_float: value.m_fValue,
344                value_int: value.m_nValue,
345            }
346        }
347    }
348
349    fn get_value_c_str(&self) -> Option<&CStr> {
350        unsafe {
351            let value = &self.inner.m_Value;
352
353            if value.m_pszString.is_null()
354                || self.has_flags(
355                    FCVAR_NEVER_AS_STRING
356                        .try_into()
357                        .expect("supposed to always work"),
358                )
359            {
360                return None;
361            }
362
363            Some(CStr::from_ptr(value.m_pszString))
364        }
365    }
366
367    /// get the value as a cow string which will be a owned value if the string in the convar is not a utf-8 string
368    /// or if the string was invalid
369    pub fn get_value_cow(&self) -> Cow<str> {
370        self.get_value_c_str()
371            .map(|cstr| cstr.to_string_lossy())
372            .unwrap_or_default()
373    }
374
375    /// get the value as a string
376    pub fn get_value_string(&self) -> String {
377        self.get_value_cow().to_string()
378    }
379
380    /// Returns the a [`str`] reprensentation of the [`ConVarStruct`] value.
381    ///
382    /// # Errors
383    ///
384    /// This function will return an error if the string is invalid, if it's not utf-8 valid or if the convar can't be a string.
385    pub fn get_value_str(&self) -> Result<&str, CStringPtrError> {
386        self.get_value_c_str()
387            .ok_or(CStringPtrError::None)?
388            .to_str()
389            .map_err(|err| err.into())
390    }
391
392    /// get the value as a i32
393    pub fn get_value_i32(&self) -> i32 {
394        self.inner.m_Value.m_nValue
395    }
396
397    /// get the value as a bool
398    pub fn get_value_bool(&self) -> bool {
399        self.inner.m_Value.m_nValue != 0
400    }
401
402    /// get the value as a f32
403    pub fn get_value_f32(&self) -> f32 {
404        self.inner.m_Value.m_fValue
405    }
406
407    // TODO: add exclusive access set_value s aka &mut self
408    // this might not be needed acutally since this acts like a Cell where you can't get a reference to internal parts
409    // well the string can be referenced but eh ub is fine ig XD
410    // this really should not be a &self
411
412    /// set the int value of the convar
413    /// also sets float and string
414    ///
415    /// only safe on the titanfall thread
416    pub fn set_value_i32(&self, new_value: i32, _: EngineToken) {
417        unsafe {
418            let value = &self.inner.m_Value;
419
420            if value.m_nValue == new_value {
421                return;
422            }
423
424            let vtable_adr = self.inner.m_ConCommandBase.m_pConCommandBaseVTable as usize;
425            let vtable_array = *(vtable_adr as *const [*const std::ffi::c_void; 21]);
426            let set_value_int = vtable_array[14];
427            // the index for SetValue for ints; weird stuff
428
429            let func = mem::transmute::<*const c_void, fn(*const ConVar, i32)>(set_value_int);
430
431            func(self.inner, new_value)
432        }
433    }
434
435    /// set the float value of the convar
436    /// also sets int and string
437    ///
438    /// only safe on the titanfall thread
439    pub fn set_value_f32(&self, new_value: f32, _: EngineToken) {
440        unsafe {
441            let value = &self.inner.m_Value;
442
443            if value.m_fValue == new_value {
444                return;
445            }
446
447            let vtable_adr = self.inner.m_ConCommandBase.m_pConCommandBaseVTable as usize;
448            let vtable_array = *(vtable_adr as *const [*const std::ffi::c_void; 21]);
449            let set_value_float = vtable_array[13];
450            // the index for SetValue for floats; weird stuff
451
452            let func = mem::transmute::<*const c_void, fn(*const ConVar, f32)>(set_value_float);
453
454            func(self.inner, new_value)
455        }
456    }
457
458    /// set the string value of the convar
459    ///
460    /// only safe on the titanfall thread
461    pub fn set_value_string(&self, new_value: impl AsRef<str>, _: EngineToken) {
462        unsafe {
463            if self.has_flags(FCVAR_NEVER_AS_STRING.try_into().unwrap()) {
464                return;
465            }
466
467            let vtable_adr = self.inner.m_ConCommandBase.m_pConCommandBaseVTable as usize;
468            let vtable_array = *(vtable_adr as *const [*const std::ffi::c_void; 21]);
469            let set_value_string = vtable_array[12];
470            // the index for SetValue for strings; weird stuff
471
472            let func =
473                mem::transmute::<*const c_void, fn(*const ConVar, *const c_char)>(set_value_string);
474
475            let string_value = to_cstring(new_value.as_ref());
476            func(self.inner, string_value.as_ptr())
477        }
478    }
479
480    /// fr why would you need this?
481    pub fn get_help_text(&self) -> String {
482        let help = self.inner.m_ConCommandBase.m_pszHelpString;
483        unsafe { CStr::from_ptr(help).to_string_lossy().to_string() }
484    }
485
486    /// returns [`true`] if the convar is registered
487    pub fn is_registered(&self) -> bool {
488        self.inner.m_ConCommandBase.m_bRegistered
489    }
490
491    /// returns [`true`] if the given flags are set for this convar
492    pub fn has_flags(&self, flags: i32) -> bool {
493        self.inner.m_ConCommandBase.m_nFlags & flags != 0
494    }
495
496    /// adds flags to the convar
497    pub fn add_flags(&mut self, flags: i32, _: EngineToken) {
498        self.inner.m_ConCommandBase.m_nFlags |= flags
499    }
500
501    /// removes flags from the convar
502    ///
503    /// only safe on the titanfall thread
504    pub fn remove_flags(&mut self, flags: i32, _: EngineToken) {
505        self.inner.m_ConCommandBase.m_nFlags &= !flags // TODO: figure out if this still needs fixing
506    }
507
508    /// exposes the raw pointer to the [`ConVar`] class
509    ///
510    /// # Safety
511    /// accessing the underlying pointer can produce ub
512    pub unsafe fn get_raw_convar_ptr(&mut self) -> *mut ConVar {
513        self.inner
514    }
515}
516
517unsafe impl Sync for ConVarStruct {}
518unsafe impl Sync for ConVar {}
519unsafe impl Send for ConVarStruct {}
520unsafe impl Send for ConVar {}