1use 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#[derive(Debug)]
63pub struct FlightLoop<T: 'static> {
64 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 #[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 pub fn schedule_immediate(&mut self) {
95 unsafe {
96 (*self.data).set_interval(LoopResult::Loops(1));
97 }
98 }
99
100 pub fn schedule_after_loops(&mut self, loops: u32) {
105 unsafe {
106 (*self.data).set_interval(LoopResult::Loops(loops));
107 }
108 }
109
110 #[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 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
137struct LoopData<T> {
139 loop_result: Option<LoopResult>,
141 loop_id: Option<xplane_sys::XPLMFlightLoopID>,
143 callback: *mut dyn FlightLoopCallback<T>,
145 loop_state: *mut T,
147 _phantom: NoSendSync,
148}
149
150#[allow(clippy::missing_fields_in_debug)] impl<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 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
193pub trait FlightLoopCallback<T>: 'static {
195 fn flight_loop(&mut self, x: &mut XPAPI, state: &mut LoopState<T>) -> LoopResult;
203}
204
205impl<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#[derive(Debug)]
220pub struct LoopState<T> {
221 since_call: Duration,
223 since_loop: Duration,
225 counter: i32,
227 state_data: *mut T,
228}
229
230impl<T> LoopState<T> {
231 #[must_use]
233 pub fn since_last_call(&self) -> Duration {
234 self.since_call
235 }
236 #[must_use]
241 pub fn since_last_loop(&self) -> Duration {
242 self.since_loop
243 }
244 #[must_use]
246 pub fn counter(&self) -> i32 {
247 self.counter
248 }
249 #[must_use]
253 pub fn state(&mut self) -> &T {
254 unsafe {
255 self.state_data.as_ref().unwrap() }
257 }
258 #[must_use]
262 pub fn state_mut(&mut self) -> &mut T {
263 unsafe {
264 self.state_data.as_mut().unwrap() }
266 }
267}
268
269#[derive(Debug, Clone, Copy)]
271pub enum LoopResult {
272 Seconds(f32),
274 Loops(u32),
276 NextLoop,
279 Deactivate,
281}
282
283impl 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
302unsafe 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 let loop_data = refcon.cast::<LoopData<T>>();
313 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 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}