1use rusteal_ffi::{RustealConsoleArgs, UObjectHandle};
6
7use crate::error::{check_ffi_ctx, RustealError, RustealResult};
8use crate::{delegate_registry, ffi_dispatch};
9
10pub 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 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
49pub trait ConsoleValue: Sized {
51 #[doc(hidden)]
53 const KIND: u32;
54 fn to_text(&self) -> String;
56 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#[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 pub const fn new(name: &'static str) -> Self {
116 Self { name, _value: std::marker::PhantomData }
117 }
118
119 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 pub fn name(&self) -> &'static str {
139 self.name
140 }
141
142 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 pub fn set(&self, value: T) -> RustealResult<()> {
152 set_variable_text(self.name, &value.to_text())
153 }
154}
155
156pub 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
164pub 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
188pub 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}