Skip to main content

rusteal_core/
console.rs

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