xplane/
flight_loop.rs

1// SPDX-FileCopyrightText: 2024 Julia DeMille <me@jdemille.com>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! # Flight loop callbacks
6//!
7//! X-Plane can call plugin code at timed intervals or when it runs its flight model.
8//!
9//! A [`FlightLoop`] object must persist for callbacks to occur. When the [`FlightLoop`] is dropped,
10//! its callbacks will stop.
11//!
12//! # Examples
13//!
14//! Closure handler:
15//!
16//! ```no_run
17//! use xplane::{XPAPI, flight_loop::{FlightLoop, FlightLoopPhase, LoopState, LoopResult}};
18//!
19//! struct MyLoopState;
20//! fn a_callback(xpapi: &mut XPAPI) {
21//!     let handler = |_xpapi: &mut XPAPI, loop_state: &mut LoopState<()>| -> LoopResult {
22//!         println!("Flight loop callback running");
23//!         LoopResult::NextLoop
24//!     };
25//!
26//!     let mut flight_loop = xpapi.new_flight_loop(FlightLoopPhase::BeforeFlightModel, handler, ());
27//!     flight_loop.schedule_immediate();
28//! }
29//! ```
30//!
31//! Struct handler:
32//!
33//! ```no_run
34//! use xplane::{XPAPI, flight_loop::{FlightLoop, FlightLoopCallback, FlightLoopPhase, LoopState, LoopResult}};
35//!
36//! struct MyLoopHandler;
37//!
38//! impl FlightLoopCallback<()> for MyLoopHandler {
39//!     fn flight_loop(&mut self, _xpapi: &mut XPAPI, state: &mut LoopState<()>) -> LoopResult {
40//!         println!("Flight loop callback running");
41//!         // You can keep state data in your own struct.
42//!         LoopResult::NextLoop
43//!     }
44//! }
45//! fn a_callback(xpapi: &mut XPAPI) {
46//!     let mut flight_loop = xpapi.new_flight_loop(FlightLoopPhase::BeforeFlightModel, MyLoopHandler, ());
47//!     flight_loop.schedule_immediate();
48//! }
49//! ```
50//!
51
52use std::{f32, fmt, marker::PhantomData, mem, time::Duration};
53
54use std::ffi::{c_float, c_int, c_void};
55
56pub use xplane_sys::XPLMFlightLoopPhaseType as FlightLoopPhase;
57
58use crate::{make_x, NoSendSync, XPAPI};
59
60/// Tracks a flight loop callback, which can be called by X-Plane periodically for calculations
61///
62#[derive(Debug)]
63pub struct FlightLoop<T: 'static> {
64    /// The loop data, allocated by a [`Box`]
65    data: *mut LoopData<T>,
66}
67
68impl<T: 'static> FlightLoop<T> {
69    pub(crate) fn new(
70        phase: FlightLoopPhase,
71        callback: impl FlightLoopCallback<T>,
72        base_state: T,
73    ) -> Self {
74        let data = Box::new(LoopData::new(callback, base_state));
75        let data_ptr: *mut LoopData<T> = Box::into_raw(data);
76        // Create a flight loop
77        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
78        let mut config = xplane_sys::XPLMCreateFlightLoop_t {
79            structSize: mem::size_of::<xplane_sys::XPLMCreateFlightLoop_t>() as c_int,
80            phase,
81            callbackFunc: Some(flight_loop_callback::<T>),
82            refcon: data_ptr.cast::<c_void>(),
83        };
84        unsafe {
85            (*data_ptr).loop_id = Some(xplane_sys::XPLMCreateFlightLoop(&mut config));
86        }
87        FlightLoop { data: data_ptr }
88    }
89
90    /// Schedules the flight loop callback to be executed in the next flight loop
91    ///
92    /// After the flight loop callback is first called, it will continue to be called
93    /// every flight loop unless it cancels itself or changes its schedule.
94    pub fn schedule_immediate(&mut self) {
95        unsafe {
96            (*self.data).set_interval(LoopResult::Loops(1));
97        }
98    }
99
100    /// Schedules the flight loop callback to be executed after a specified number of flight loops
101    ///
102    /// After the callback is first called, it will continue to be called with the provided loop
103    /// interval.
104    pub fn schedule_after_loops(&mut self, loops: u32) {
105        unsafe {
106            (*self.data).set_interval(LoopResult::Loops(loops));
107        }
108    }
109
110    /// Schedules the flight loop callback to be executed after the specified delay
111    ///
112    /// After the callback is first called, it will continue to be called with that interval.
113    #[allow(clippy::cast_precision_loss)]
114    pub fn schedule_after(&mut self, time: Duration) {
115        let seconds_f = time.as_secs_f32();
116        unsafe {
117            (*self.data).set_interval(LoopResult::Seconds(seconds_f));
118        }
119    }
120
121    /// Deactivates the flight loop
122    pub fn deactivate(&mut self) {
123        unsafe {
124            (*self.data).set_interval(LoopResult::Deactivate);
125        }
126    }
127}
128
129impl<T> Drop for FlightLoop<T> {
130    fn drop(&mut self) {
131        unsafe {
132            let _ = Box::from_raw(self.data);
133        }
134    }
135}
136
137/// Data stored as part of a [`FlightLoop`] and used as a refcon
138struct LoopData<T> {
139    /// The loop result, or [`None`] if the loop has not been scheduled
140    loop_result: Option<LoopResult>,
141    /// The loop ID
142    loop_id: Option<xplane_sys::XPLMFlightLoopID>,
143    /// The callback (stored here but not used)
144    callback: *mut dyn FlightLoopCallback<T>,
145    /// The flight loop's stored state
146    loop_state: *mut T,
147    _phantom: NoSendSync,
148}
149
150#[allow(clippy::missing_fields_in_debug)] // Clippy thinks _phantom is missing. There is no reason to include it, and a lack of inclusion does not make it non-exhaustive.
151impl<T> fmt::Debug for LoopData<T> {
152    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153        f.debug_struct("LoopData")
154            .field("loop_result", &self.loop_result)
155            .field("loop_id", &self.loop_id)
156            .field("callback", &"[callback]")
157            .field("loop_state", &"[callback state]")
158            .finish()
159    }
160}
161
162impl<T> LoopData<T> {
163    /// Creates a new [`LoopData`] with a callback
164    pub(crate) fn new(callback: impl FlightLoopCallback<T>, base_state: T) -> Self {
165        LoopData {
166            loop_result: None,
167            loop_id: None,
168            callback: Box::into_raw(Box::new(callback)),
169            loop_state: Box::into_raw(Box::new(base_state)),
170            _phantom: PhantomData,
171        }
172    }
173
174    fn set_interval(&mut self, loop_result: LoopResult) {
175        let loop_id = self.loop_id.expect("Loop ID not set");
176        unsafe {
177            xplane_sys::XPLMScheduleFlightLoop(loop_id, loop_result.into(), 1);
178        }
179        self.loop_result = Some(loop_result);
180    }
181}
182
183impl<T> Drop for LoopData<T> {
184    fn drop(&mut self) {
185        if let Some(loop_id) = self.loop_id {
186            unsafe { xplane_sys::XPLMDestroyFlightLoop(loop_id) }
187        }
188        let _ = unsafe { Box::from_raw(self.callback) };
189        let _ = unsafe { Box::from_raw(self.loop_state) };
190    }
191}
192
193/// Trait for objects that can receive flight loop callbacks
194pub trait FlightLoopCallback<T>: 'static {
195    /// Called periodically by X-Plane according to the provided scheduling
196    ///
197    /// In this callback, processing can be done. Drawing cannot be done.
198    ///
199    /// The provided [`LoopState`] can be used to get information and change the scheduling of
200    /// callbacks. If the scheduling is not changed, this callback will continue to be called
201    /// with the same schedule.
202    fn flight_loop(&mut self, x: &mut XPAPI, state: &mut LoopState<T>) -> LoopResult;
203}
204
205/// Closures can be used as [`FlightLoopCallback`]s
206impl<F, T> FlightLoopCallback<T> for F
207where
208    F: 'static + FnMut(&mut XPAPI, &mut LoopState<T>) -> LoopResult,
209{
210    fn flight_loop(&mut self, x: &mut XPAPI, state: &mut LoopState<T>) -> LoopResult {
211        self(x, state)
212    }
213}
214
215/// Information available during a flight loop callback
216///
217/// By default, a flight loop callback will continue to be called on its initial schedule.
218/// The scheduling functions only need to be called if the callback scheduling should change.
219#[derive(Debug)]
220pub struct LoopState<T> {
221    /// Time since last callback call
222    since_call: Duration,
223    /// Time since last flight loop
224    since_loop: Duration,
225    /// Callback counter
226    counter: i32,
227    state_data: *mut T,
228}
229
230impl<T> LoopState<T> {
231    /// Returns the duration since the last time this callback was called
232    #[must_use]
233    pub fn since_last_call(&self) -> Duration {
234        self.since_call
235    }
236    /// Returns the duration since the last flight loop
237    ///
238    /// If this callback is not called every flight loop, this may be different from the
239    /// value returned from `time_since_last_call`.
240    #[must_use]
241    pub fn since_last_loop(&self) -> Duration {
242        self.since_loop
243    }
244    /// Returns the value of a counter that increments every time the callback is called
245    #[must_use]
246    pub fn counter(&self) -> i32 {
247        self.counter
248    }
249    /// Get an immutable reference to the state data.
250    /// # Panics
251    /// Panics if the pointer to the state data is null. This should not be possible.
252    #[must_use]
253    pub fn state(&mut self) -> &T {
254        unsafe {
255            self.state_data.as_ref().unwrap() // This will not be a null pointer.
256        }
257    }
258    /// Get a mutable reference to the state data.
259    /// # Panics
260    /// Panics if the pointer to the state data is null. This should not be possible.
261    #[must_use]
262    pub fn state_mut(&mut self) -> &mut T {
263        unsafe {
264            self.state_data.as_mut().unwrap() // This will not be a null pointer.
265        }
266    }
267}
268
269/// Loop results, which determine when the callback will be called next
270#[derive(Debug, Clone, Copy)]
271pub enum LoopResult {
272    /// Callback will be called after the provided number of seconds
273    Seconds(f32),
274    /// Callback will be called after the provided number of loops
275    Loops(u32),
276    /// Callback will be called after the next loop.
277    /// Equivalent to Loops(1).
278    NextLoop,
279    /// Callback will not be called again until it is rescheduled
280    Deactivate,
281}
282
283/// Converts a [`LoopResult`] into an [`f32`] suitable for returning from a flight loop callback
284impl From<LoopResult> for f32 {
285    #[allow(clippy::cast_precision_loss)]
286    fn from(lr: LoopResult) -> Self {
287        match lr {
288            LoopResult::Deactivate => 0f32,
289            LoopResult::Seconds(secs) => secs,
290            LoopResult::NextLoop => -1.0f32,
291            LoopResult::Loops(loops) => -1.0f32 * (loops as f32),
292        }
293    }
294}
295
296impl From<Duration> for LoopResult {
297    fn from(value: Duration) -> Self {
298        LoopResult::Seconds(value.as_secs_f32())
299    }
300}
301
302/// The flight loop callback that X-Plane calls
303///
304/// This expands to a separate callback for every type C.
305unsafe extern "C-unwind" fn flight_loop_callback<T: 'static>(
306    since_last_call: c_float,
307    since_loop: c_float,
308    counter: c_int,
309    refcon: *mut c_void,
310) -> c_float {
311    // Get the loop data
312    let loop_data = refcon.cast::<LoopData<T>>();
313    // Create a state
314    let mut state = LoopState {
315        since_call: Duration::from_secs_f32(since_last_call),
316        since_loop: Duration::from_secs_f32(since_loop),
317        counter,
318        state_data: unsafe { (*loop_data).loop_state },
319    };
320    let mut x = make_x();
321    let res = unsafe { (*(*loop_data).callback).flight_loop(&mut x, &mut state) };
322
323    unsafe {
324        (*loop_data).loop_result = Some(res);
325    }
326
327    // Return the next loop time
328    f32::from(res)
329}
330
331#[cfg(test)]
332mod tests {
333    use std::{cell::RefCell, ptr::NonNull, rc::Rc};
334
335    use super::*;
336    #[test]
337    #[allow(clippy::too_many_lines, clippy::float_cmp)]
338    fn test_flight_loops() {
339        struct TestLoopHandler {
340            internal_state: bool,
341        }
342        impl FlightLoopCallback<TestLoopState> for TestLoopHandler {
343            fn flight_loop(
344                &mut self,
345                _x: &mut XPAPI,
346                state: &mut LoopState<TestLoopState>,
347            ) -> LoopResult {
348                let test_state = state.state_mut();
349                test_state.test_thing += 1;
350                self.internal_state = !self.internal_state;
351                println!("Test thing: {}", test_state.test_thing);
352                println!("Internal state: {}", self.internal_state);
353                match test_state.test_thing {
354                    1 => {
355                        assert_eq!(state.since_last_call(), Duration::from_secs_f32(2.0));
356                        assert_eq!(state.since_last_loop(), Duration::from_secs_f32(2.0));
357                        LoopResult::NextLoop
358                    }
359                    2 => LoopResult::Loops(2),
360                    3 => LoopResult::Seconds(1.5f32),
361                    4 => LoopResult::Deactivate,
362                    _ => unreachable!(),
363                }
364            }
365        }
366        struct TestLoopState {
367            test_thing: i32,
368        }
369        let expected_ptr = NonNull::<c_void>::dangling().as_ptr();
370        let refcon_cell = Rc::new(RefCell::new(NonNull::<c_void>::dangling().as_ptr()));
371        let create_flight_loop_ctx = xplane_sys::XPLMCreateFlightLoop_context();
372        let refcon_cell_1 = refcon_cell.clone();
373        create_flight_loop_ctx
374            .expect()
375            .once()
376            .return_once_st(move |s| {
377                let s = unsafe { *s };
378                *refcon_cell_1.borrow_mut() = s.refcon;
379                expected_ptr
380            });
381        let schedule_flight_loop_ctx = xplane_sys::XPLMScheduleFlightLoop_context();
382        schedule_flight_loop_ctx.expect().once().return_once_st(
383            move |loop_ref, when, relative_to_now| {
384                assert!(loop_ref == expected_ptr);
385                assert_eq!(when, -1.0f32);
386                assert_eq!(relative_to_now, 1);
387            },
388        );
389        let destroy_flight_loop_ctx = xplane_sys::XPLMDestroyFlightLoop_context();
390        destroy_flight_loop_ctx
391            .expect()
392            .once()
393            .return_once_st(move |loop_ref| {
394                assert!(loop_ref == expected_ptr);
395            });
396        let mut x = make_x();
397        let mut fl = x.new_flight_loop(
398            FlightLoopPhase::BeforeFlightModel,
399            TestLoopHandler {
400                internal_state: false,
401            },
402            TestLoopState { test_thing: 0 },
403        );
404        fl.schedule_immediate();
405        create_flight_loop_ctx.checkpoint();
406        schedule_flight_loop_ctx.checkpoint();
407        let refcon = *refcon_cell.borrow();
408        unsafe {
409            let res = flight_loop_callback::<TestLoopState>(2.0f32, 2.0f32, 1, refcon);
410            assert_eq!(res, -1.0f32);
411            let res = flight_loop_callback::<TestLoopState>(2.0f32, 2.0f32, 2, refcon);
412            assert_eq!(res, -2.0f32);
413            let res = flight_loop_callback::<TestLoopState>(2.0f32, 2.0f32, 3, refcon);
414            assert_eq!(res, 1.5f32);
415            let res = flight_loop_callback::<TestLoopState>(2.0f32, 2.0f32, 4, refcon);
416            assert_eq!(res, 0.0f32);
417        }
418    }
419
420    #[test]
421    #[allow(clippy::float_cmp)]
422    fn test_duration() {
423        let dur = Duration::from_secs_f32(2.5f32);
424        let loop_res: LoopResult = dur.into();
425        let LoopResult::Seconds(secs) = loop_res else {
426            panic!("Conversion failure somehow!");
427        };
428        assert_eq!(secs, 2.5f32);
429    }
430}