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