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(®ister_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(®ister_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(®ister_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(®ister_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 {}