Skip to main content

rusteal_core/
console.rs

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