Skip to main content

rusteal_core/
console.rs

1// Console commands and variables: the C++ IConsoleManager, which is not in
2// reflection. What a library registers is removed when it unloads, so a hot
3// reload registers it again from its startup code.
4
5use rusteal_ffi::{RustealConsoleArgs, UObjectHandle};
6
7use crate::error::{check_ffi_ctx, RustealError, RustealResult};
8use crate::{delegate_registry, ffi_dispatch};
9
10/// Register the console command `name`: typing `name arg1 arg2` in the
11/// console, or `-ExecCmds` on the command line, calls `run` with the
12/// arguments and the world the command runs in (null when there is none).
13///
14/// Fails when the name is already a command or variable.
15pub fn register_command(
16    name: &str,
17    help: &str,
18    mut run: impl FnMut(&[&str], UObjectHandle) + Send + 'static,
19) -> RustealResult<()> {
20    let callback_id = delegate_registry::register_callback(move |params| {
21        // SAFETY: the plugin passes an FRustealConsoleArgs for a command's
22        // callback, valid for the call.
23        let args = unsafe { &*(params as *const RustealConsoleArgs) };
24        let text = if args.args.is_null() {
25            ""
26        } else {
27            let bytes = unsafe { std::slice::from_raw_parts(args.args, args.args_len as usize) };
28            std::str::from_utf8(bytes).unwrap_or("")
29        };
30        let words: Vec<&str> = if text.is_empty() { Vec::new() } else { text.split('\n').collect() };
31        run(&words, args.world);
32    });
33    let code = unsafe {
34        ffi_dispatch::console_register_command(
35            name.as_ptr(),
36            name.len() as u32,
37            help.as_ptr(),
38            help.len() as u32,
39            callback_id,
40        )
41    };
42    let result = check_ffi_ctx(code, name);
43    if result.is_err() {
44        delegate_registry::unregister_callback(callback_id);
45    }
46    result
47}
48
49/// A value a console variable holds.
50pub trait ConsoleValue: Sized {
51    /// The kind the plugin registers (0 bool, 1 int, 2 float, 3 string).
52    #[doc(hidden)]
53    const KIND: u32;
54    /// The value as the console writes it.
55    fn to_text(&self) -> String;
56    /// The value from the console's text.
57    fn from_text(text: &str) -> Option<Self>;
58}
59
60impl ConsoleValue for bool {
61    const KIND: u32 = 0;
62    fn to_text(&self) -> String {
63        if *self { "1" } else { "0" }.to_string()
64    }
65    fn from_text(text: &str) -> Option<Self> {
66        match text.trim().to_ascii_lowercase().as_str() {
67            "1" | "true" | "yes" | "on" => Some(true),
68            "0" | "false" | "no" | "off" | "" => Some(false),
69            other => other.parse::<f64>().ok().map(|n| n != 0.0),
70        }
71    }
72}
73
74impl ConsoleValue for i32 {
75    const KIND: u32 = 1;
76    fn to_text(&self) -> String {
77        self.to_string()
78    }
79    fn from_text(text: &str) -> Option<Self> {
80        let text = text.trim();
81        text.parse().ok().or_else(|| text.parse::<f64>().ok().map(|n| n as i32))
82    }
83}
84
85impl ConsoleValue for f32 {
86    const KIND: u32 = 2;
87    fn to_text(&self) -> String {
88        self.to_string()
89    }
90    fn from_text(text: &str) -> Option<Self> {
91        text.trim().parse().ok()
92    }
93}
94
95impl ConsoleValue for String {
96    const KIND: u32 = 3;
97    fn to_text(&self) -> String {
98        self.clone()
99    }
100    fn from_text(text: &str) -> Option<Self> {
101        Some(text.to_string())
102    }
103}
104
105/// A console variable, by name: one this library registered, or any other
106/// (the engine's included), read and written as `T`.
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub struct ConsoleVariable<T> {
109    name: &'static str,
110    _value: std::marker::PhantomData<fn() -> T>,
111}
112
113impl<T: ConsoleValue> ConsoleVariable<T> {
114    /// The variable `name`, registered or not.
115    pub const fn new(name: &'static str) -> Self {
116        Self { name, _value: std::marker::PhantomData }
117    }
118
119    /// Register it with its default value: `name value` in the console sets
120    /// it. Fails when the name is already a command or variable.
121    pub fn register(&self, help: &str, default: T) -> RustealResult<()> {
122        let default = default.to_text();
123        let code = unsafe {
124            ffi_dispatch::console_register_variable(
125                self.name.as_ptr(),
126                self.name.len() as u32,
127                help.as_ptr(),
128                help.len() as u32,
129                T::KIND,
130                default.as_ptr(),
131                default.len() as u32,
132            )
133        };
134        check_ffi_ctx(code, self.name)
135    }
136
137    /// Its name.
138    pub fn name(&self) -> &'static str {
139        self.name
140    }
141
142    /// Its value.
143    pub fn get(&self) -> RustealResult<T> {
144        let text = get_variable_text(self.name)?;
145        T::from_text(&text).ok_or_else(|| {
146            RustealError::Internal(format!("console variable {} holds '{text}'", self.name))
147        })
148    }
149
150    /// Set it, as code does.
151    pub fn set(&self, value: T) -> RustealResult<()> {
152        set_variable_text(self.name, &value.to_text())
153    }
154}
155
156/// Remove a command or variable this library registered.
157pub fn unregister(name: &str) -> RustealResult<()> {
158    check_ffi_ctx(
159        unsafe { ffi_dispatch::console_unregister(name.as_ptr(), name.len() as u32) },
160        name,
161    )
162}
163
164/// Any console variable's value as the console shows it.
165pub fn get_variable_text(name: &str) -> RustealResult<String> {
166    let mut buf = vec![0u8; 256];
167    loop {
168        let mut len: u32 = 0;
169        let code = unsafe {
170            ffi_dispatch::console_get_variable(
171                name.as_ptr(),
172                name.len() as u32,
173                buf.as_mut_ptr(),
174                buf.len() as u32,
175                &mut len,
176            )
177        };
178        check_ffi_ctx(code, name)?;
179        if (len as usize) <= buf.len() {
180            buf.truncate(len as usize);
181            return String::from_utf8(buf)
182                .map_err(|_| RustealError::Internal("console value is not UTF-8".into()));
183        }
184        buf.resize(len as usize, 0);
185    }
186}
187
188/// Set any console variable from text.
189pub fn set_variable_text(name: &str, value: &str) -> RustealResult<()> {
190    check_ffi_ctx(
191        unsafe {
192            ffi_dispatch::console_set_variable(
193                name.as_ptr(),
194                name.len() as u32,
195                value.as_ptr(),
196                value.len() as u32,
197            )
198        },
199        name,
200    )
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn values_read_the_console_text() {
209        assert_eq!(bool::from_text("1"), Some(true));
210        assert_eq!(bool::from_text("False"), Some(false));
211        assert_eq!(bool::from_text("2"), Some(true));
212        assert_eq!(i32::from_text("42"), Some(42));
213        assert_eq!(i32::from_text("3.0"), Some(3));
214        assert_eq!(f32::from_text("0.5"), Some(0.5));
215        assert_eq!(f32::from_text("x"), None);
216        assert_eq!(String::from_text("a b"), Some("a b".to_string()));
217        assert_eq!(true.to_text(), "1");
218        assert_eq!(7.to_text(), "7");
219    }
220}