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