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}