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}