arma_rs/call_context/
call.rs

1use std::path::Path;
2
3use super::stack::ArmaContextStackTrace;
4
5#[repr(C)]
6struct RawArmaCallContext {
7    pub steam_id: u64,
8    pub source: *const libc::c_char,
9    pub mission: *const libc::c_char,
10    pub server: *const libc::c_char,
11    pub remote_exec_owner: i16,
12    pub call_stack: Option<*const super::stack::RawContextStackTrace>,
13}
14
15impl RawArmaCallContext {
16    fn from_arma(args: *mut *mut i8, count: libc::c_int) -> Self {
17        let steam_id = unsafe { *args.offset(0) as u64 };
18        let source = unsafe { *args.offset(1) as *const libc::c_char };
19        let mission = unsafe { *args.offset(2) as *const libc::c_char };
20        let server = unsafe { *args.offset(3) as *const libc::c_char };
21        let remote_exec_owner = unsafe { *args.offset(4) as i16 };
22
23        let call_stack = if count > 5 {
24            let stack = unsafe { *args.offset(5) as *const super::stack::RawContextStackTrace };
25            Some(stack)
26        } else {
27            None
28        };
29
30        Self {
31            steam_id,
32            source,
33            mission,
34            server,
35            remote_exec_owner,
36            call_stack,
37        }
38    }
39}
40
41pub trait StackRequest {}
42
43#[derive(Default)]
44pub struct WithStackTrace;
45impl StackRequest for WithStackTrace {}
46
47pub struct WithoutStackTrace;
48impl StackRequest for WithoutStackTrace {}
49
50/// Context of the callExtension, provided by Arma.
51pub type CallContext = ArmaCallContext<WithoutStackTrace>;
52/// Context of the callExtension, provided by Arma, with a stack trace.
53pub type CallContextStackTrace = ArmaCallContext<WithStackTrace>;
54
55#[derive(Clone, Default)]
56/// Context of the Arma call.
57pub struct ArmaCallContext<T: StackRequest> {
58    pub(super) caller: Caller,
59    pub(super) source: Source,
60    pub(super) mission: Mission,
61    pub(super) server: Server,
62    pub(super) remote_exec_owner: i16,
63
64    _stack_marker: std::marker::PhantomData<T>,
65    stack: Option<ArmaContextStackTrace>,
66}
67
68impl<T: StackRequest> ArmaCallContext<T> {
69    pub(crate) const fn new(
70        caller: Caller,
71        source: Source,
72        mission: Mission,
73        server: Server,
74        remote_exec_owner: i16,
75    ) -> Self {
76        Self {
77            caller,
78            source,
79            mission,
80            server,
81            remote_exec_owner,
82
83            _stack_marker: std::marker::PhantomData,
84            stack: None,
85        }
86    }
87
88    /// Create a new ArmaCallContext from pointers provided by Arma.
89    pub fn from_arma(args: *mut *mut i8, count: libc::c_int) -> Self {
90        let raw = RawArmaCallContext::from_arma(args, count);
91        Self {
92            caller: Caller::Steam(raw.steam_id),
93            source: Source::from(unsafe { std::ffi::CStr::from_ptr(raw.source).to_str().unwrap() }),
94            mission: Mission::from(unsafe {
95                std::ffi::CStr::from_ptr(raw.mission).to_str().unwrap()
96            }),
97            server: Server::from(unsafe { std::ffi::CStr::from_ptr(raw.server).to_str().unwrap() }),
98            remote_exec_owner: raw.remote_exec_owner,
99
100            _stack_marker: std::marker::PhantomData,
101            stack: raw.call_stack.map(ArmaContextStackTrace::from),
102        }
103    }
104
105    #[must_use]
106    /// Player that called the extension. Can be [`Caller::Unknown`] when the player's steamID64 is unavailable
107    /// # Note
108    /// Unlike <https://community.bistudio.com/wiki/getPlayerUID> [`Caller::Steam`] isn't limited to multiplayer.
109    pub const fn caller(&self) -> &Caller {
110        &self.caller
111    }
112
113    #[must_use]
114    /// Source from where the extension was called.
115    pub const fn source(&self) -> &Source {
116        &self.source
117    }
118
119    #[must_use]
120    /// Current mission's name.
121    /// # Note
122    /// Can result in [`Mission::None`] in missions made prior to Arma v2.02.
123    pub const fn mission(&self) -> &Mission {
124        &self.mission
125    }
126
127    #[must_use]
128    /// Current server's name
129    pub const fn server(&self) -> &Server {
130        &self.server
131    }
132
133    #[must_use]
134    /// Remote execution owner.
135    pub const fn remote_exec_owner(&self) -> i16 {
136        self.remote_exec_owner
137    }
138}
139
140impl ArmaCallContext<WithStackTrace> {
141    #[must_use]
142    /// Call stack of the extension call.
143    pub const fn stack_trace(&self) -> &ArmaContextStackTrace {
144        // By the time this gets to consumer code, to_without_stack would've been called if the stack was not requested
145        self.stack.as_ref().expect("Stack is missing")
146    }
147
148    /// Convert the context to one without a stack trace.
149    pub(crate) fn into_without_stack(self) -> ArmaCallContext<WithoutStackTrace> {
150        ArmaCallContext::new(
151            self.caller,
152            self.source,
153            self.mission,
154            self.server,
155            self.remote_exec_owner,
156        )
157    }
158}
159
160/// Identification of the player calling your extension.
161#[derive(Debug, Clone, Default, PartialEq, Eq)]
162pub enum Caller {
163    /// The player's steamID64.
164    Steam(u64),
165    #[default]
166    /// Unable to determine.
167    Unknown,
168}
169
170impl Caller {
171    #[must_use]
172    /// Convert the caller to a string.
173    pub fn as_str(&self) -> String {
174        match self {
175            Self::Steam(id) => id.to_string(),
176            Self::Unknown => "0".to_string(),
177        }
178    }
179
180    #[must_use]
181    /// Convert the caller to a u64.
182    pub const fn as_u64(&self) -> u64 {
183        match self {
184            Self::Steam(id) => *id,
185            Self::Unknown => 0,
186        }
187    }
188}
189
190impl From<&str> for Caller {
191    fn from(s: &str) -> Self {
192        if s.is_empty() || s == "0" {
193            Self::Unknown
194        } else {
195            s.parse::<u64>().map_or(Self::Unknown, Self::Steam)
196        }
197    }
198}
199
200impl From<u64> for Caller {
201    fn from(id: u64) -> Self {
202        if id == 0 {
203            Self::Unknown
204        } else {
205            Self::Steam(id)
206        }
207    }
208}
209
210/// Source of the extension call.
211#[derive(Debug, Clone, Default, PartialEq, Eq)]
212pub enum Source {
213    /// Absolute path of the file on the players system.
214    /// For example on windows: `C:\Users\user\Documents\Arma 3\missions\test.VR\fn_armaContext.sqf`.
215    File(String),
216    /// Path inside of a pbo.
217    /// For example: `z\test\addons\main\fn_armaContext.sqf`.
218    Pbo(String),
219    #[default]
220    /// Debug console or an other form of on the fly execution, such as mission triggers.
221    Console,
222}
223
224impl Source {
225    #[must_use]
226    /// Convert the source to a string.
227    pub fn as_str(&self) -> &str {
228        match self {
229            Self::File(s) | Self::Pbo(s) => s,
230            Self::Console => "",
231        }
232    }
233}
234
235impl From<&str> for Source {
236    fn from(s: &str) -> Self {
237        if s.is_empty() {
238            Self::Console
239        } else if Path::new(s).is_absolute() {
240            Self::File(s.to_string())
241        } else {
242            Self::Pbo(s.to_string())
243        }
244    }
245}
246
247impl From<*const libc::c_char> for Source {
248    #[allow(clippy::not_unsafe_ptr_arg_deref)]
249    fn from(s: *const libc::c_char) -> Self {
250        Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
251    }
252}
253
254/// Current mission.
255#[derive(Debug, Clone, Default, PartialEq, Eq)]
256pub enum Mission {
257    /// Mission name.
258    Mission(String),
259    #[default]
260    /// Not in a mission.
261    None,
262}
263
264impl Mission {
265    /// Convert the mission to a string.
266    pub fn as_str(&self) -> &str {
267        match self {
268            Self::Mission(s) => s,
269            Self::None => "",
270        }
271    }
272}
273
274impl From<&str> for Mission {
275    fn from(s: &str) -> Self {
276        if s.is_empty() {
277            Self::None
278        } else {
279            Self::Mission(s.to_string())
280        }
281    }
282}
283
284impl From<*const libc::c_char> for Mission {
285    #[allow(clippy::not_unsafe_ptr_arg_deref)]
286    fn from(s: *const libc::c_char) -> Self {
287        Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
288    }
289}
290
291/// Current server.
292#[derive(Debug, Clone, Default, PartialEq, Eq)]
293pub enum Server {
294    /// Server name
295    Multiplayer(String),
296    #[default]
297    /// Singleplayer or no mission
298    Singleplayer,
299}
300
301impl Server {
302    /// Convert the server to a string.
303    pub fn as_str(&self) -> &str {
304        match self {
305            Self::Multiplayer(s) => s,
306            Self::Singleplayer => "",
307        }
308    }
309}
310
311impl From<&str> for Server {
312    fn from(s: &str) -> Self {
313        if s.is_empty() {
314            Self::Singleplayer
315        } else {
316            Self::Multiplayer(s.to_string())
317        }
318    }
319}
320
321impl From<*const libc::c_char> for Server {
322    #[allow(clippy::not_unsafe_ptr_arg_deref)]
323    fn from(s: *const libc::c_char) -> Self {
324        Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn caller_empty() {
334        assert_eq!(Caller::from(""), Caller::Unknown);
335    }
336
337    #[test]
338    fn caller_zero() {
339        assert_eq!(Caller::from("0"), Caller::Unknown);
340    }
341
342    #[test]
343    fn source_empty() {
344        assert_eq!(Source::from(""), Source::Console);
345    }
346
347    #[test]
348    fn source_pbo() {
349        let path = "x\\ctx\\addons\\main\\fn_armaContext.sqf";
350        assert_eq!(Source::from(path), Source::Pbo(path.to_string()));
351    }
352
353    #[test]
354    fn source_file() {
355        let path = env!("CARGO_MANIFEST_DIR");
356        assert_eq!(Source::from(path), Source::File(path.to_string()));
357    }
358
359    #[test]
360    fn mission_empty() {
361        assert_eq!(Mission::from(""), Mission::None);
362    }
363
364    #[test]
365    fn server_empty() {
366        assert_eq!(Server::from(""), Server::Singleplayer);
367    }
368}