xplane/
scenery.rs

1// SPDX-FileCopyrightText: 2024 Julia DeMille <me@jdemille.com>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use std::{
6    ffi::{c_char, c_void, CStr, CString, NulError},
7    marker::PhantomData,
8    path::{Path, PathBuf},
9};
10
11use snafu::prelude::*;
12use xplane_sys::{
13    XPLMCreateInstance, XPLMCreateProbe, XPLMDestroyProbe, XPLMLoadObject, XPLMLoadObjectAsync,
14    XPLMLookupObjects, XPLMObjectRef, XPLMProbeInfo_t, XPLMProbeRef, XPLMProbeResult,
15    XPLMProbeTerrainXYZ, XPLMProbeType, XPLMReloadScenery, XPLMUnloadObject,
16};
17
18#[cfg(feature = "XPLM300")]
19use xplane_sys::{XPLMDegMagneticToDegTrue, XPLMDegTrueToDegMagnetic, XPLMGetMagneticVariation};
20
21#[cfg(feature = "XPLM303")]
22use crate::obj_instance::Instance;
23
24use crate::NoSendSync;
25
26/// A probe for terrain. Keep it around in whatever will be probing.
27/// See [the X-Plane documentation](https://developer.x-plane.com/sdk/XPLMScenery/#Performance_Guidelines)
28/// for more information on proper use.
29pub struct TerrainProbe {
30    handle: XPLMProbeRef,
31    _typ: XPLMProbeType,
32    _phantom: NoSendSync,
33}
34
35impl TerrainProbe {
36    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
37    /// Probe the terrain at the given coordinates.
38    /// All coordinates are OpenGL local coordinates.
39    ///
40    /// # Errors
41    ///
42    /// If [`XPLMProbeTerrainXYZ`] says that an error has occured, an error will be returned.
43    /// I have no way of telling you what went wrong.
44    ///
45    /// # Panics
46    /// This function will panic if [`XPLMProbeTerrainXYZ`] returns an invalid result.
47    /// This shouldn't be possible.
48    pub fn probe_terrain(
49        &mut self,
50        x: f32,
51        y: f32,
52        z: f32,
53    ) -> Result<Option<XPLMProbeInfo_t>, ProbeError> {
54        let mut probe_info = XPLMProbeInfo_t {
55            structSize: std::mem::size_of::<XPLMProbeInfo_t>() as i32,
56            locationX: 0.0,
57            locationY: 0.0,
58            locationZ: 0.0,
59            normalX: 0.0,
60            normalY: 0.0,
61            normalZ: 0.0,
62            velocityX: 0.0,
63            velocityY: 0.0,
64            velocityZ: 0.0,
65            is_wet: 0,
66        };
67        match unsafe { XPLMProbeTerrainXYZ(self.handle, x, y, z, &mut probe_info) } {
68            XPLMProbeResult::Missed => Ok(None),
69            XPLMProbeResult::HitTerrain => Ok(Some(probe_info)),
70            XPLMProbeResult::Error => Err(ProbeError),
71            _ => panic!("XPLMProbeTerrainXYZ has returned an invalid result!"),
72        }
73    }
74}
75
76impl Drop for TerrainProbe {
77    fn drop(&mut self) {
78        unsafe {
79            XPLMDestroyProbe(self.handle);
80        }
81    }
82}
83
84#[derive(Debug, Snafu)]
85#[snafu(display("The terrain probe did not succeed."))]
86/// The terrain probe did not succeed. Either the probe struct size is bad (should not be possible),
87/// the probe is invalid, or the type is mismatched for the specific query call.
88pub struct ProbeError;
89
90/// An X-Plane OBJ file handle.
91pub struct XObject {
92    handle: XPLMObjectRef,
93    path: PathBuf,
94    _phantom: NoSendSync,
95}
96
97impl XObject {
98    #[must_use]
99    /// Get the path to this object, relative to the X-System root.
100    pub fn path(&self) -> &Path {
101        &self.path
102    }
103
104    #[must_use]
105    /// Try to clone this object.
106    /// If X-Plane decides for whatever reason it doesn't want to load another
107    /// copy of this object, this will return [`None`].
108    pub fn try_clone(&self) -> Option<Self> {
109        let path_c =
110            std::ffi::CString::new(self.path.as_os_str().to_string_lossy().into_owned()).ok()?;
111        let handle = unsafe { XPLMLoadObject(path_c.as_ptr()) };
112        if handle.is_null() {
113            None
114        } else {
115            Some(Self {
116                handle,
117                path: self.path.clone(),
118                _phantom: PhantomData,
119            })
120        }
121    }
122
123    #[cfg(feature = "XPLM303")]
124    /// Make a new instance of this object.
125    /// # Errors
126    /// Returns an error if any of the dataref names contain a NUL byte.
127    /// <div class="warning"> All datarefs used by this instance must be registered before the object is
128    /// even _loaded._ Failure to have them registered beforehand is undefined behavior.</div>
129    pub fn new_instance<const NUM_DATAREFS: usize, S: Into<Vec<u8>>>(
130        self,
131        datarefs: [S; NUM_DATAREFS],
132    ) -> Result<Instance<NUM_DATAREFS>, NulError> {
133        let datarefs = datarefs
134            .into_iter()
135            .map(|s| CString::new(s))
136            .collect::<Result<Vec<_>, _>>()?;
137        let mut dr_ptrs: Vec<_> = datarefs
138            .iter()
139            .map(|dr| dr.as_ptr())
140            .chain([std::ptr::null()])
141            .collect();
142        let handle = unsafe { XPLMCreateInstance(self.handle, dr_ptrs.as_mut_ptr()) };
143        Ok(Instance {
144            handle,
145            _phantom: PhantomData,
146        })
147    }
148}
149
150impl Drop for XObject {
151    fn drop(&mut self) {
152        unsafe {
153            XPLMUnloadObject(self.handle);
154        }
155    }
156}
157
158struct ObjectLoadContext<C>
159where
160    C: FnOnce(Option<XObject>),
161{
162    obj_path: PathBuf,
163    callback: C,
164}
165
166/// Access struct for X-Plane's scenery APIs.
167pub struct SceneryApi {
168    pub(crate) _phantom: NoSendSync,
169}
170
171impl SceneryApi {
172    /// Create a new terrain probe.
173    pub fn new_terrain_probe(&mut self, typ: XPLMProbeType) -> TerrainProbe {
174        let handle = unsafe { XPLMCreateProbe(typ) };
175        TerrainProbe {
176            handle,
177            _typ: typ,
178            _phantom: PhantomData,
179        }
180    }
181
182    #[cfg(feature = "XPLM300")]
183    /// Returns X-Plane’s simulated magnetic variation (declination) at the passed latitude and longitude.
184    pub fn get_magnetic_variation(&mut self, lat: f64, lon: f64) -> f32 {
185        unsafe { XPLMGetMagneticVariation(lat, lon) }
186    }
187
188    #[cfg(feature = "XPLM300")]
189    /// Converts a heading in degrees relative to true north at the user's current location
190    /// into a heading relative to magnetic north.
191    pub fn deg_true_to_mag(&mut self, deg: f32) -> f32 {
192        unsafe { XPLMDegTrueToDegMagnetic(deg) }
193    }
194
195    #[cfg(feature = "XPLM300")]
196    /// Converts a heading in degrees relative to magnetic north at the user's current location
197    /// into a heading relative to true north.
198    pub fn deg_mag_to_true(&mut self, deg: f32) -> f32 {
199        unsafe { XPLMDegMagneticToDegTrue(deg) }
200    }
201
202    /// Lookup a virtual path in X-Plane's library system, and return all matching objects.
203    /// `lat` and `lon` indicate the location the object will be used -- location-specific objects
204    /// will be returned. All paths will be relative to the X-System folder.
205    /// # Errors
206    /// An error will be returned if the virtual path contains a NUL byte.
207    /// # Panics
208    /// This function will panic if X-Plane provides invalid UTF-8. This should not be possible.
209    pub fn lookup_objects<P: AsRef<Path>>(
210        &mut self,
211        path: P,
212        lat: f32,
213        lon: f32,
214    ) -> Result<Vec<PathBuf>, NulError> {
215        let mut objects = Vec::new();
216        let objects_ptr: *mut _ = &mut objects;
217        let objects_ptr: *mut c_void = objects_ptr.cast();
218        let path_c =
219            std::ffi::CString::new(path.as_ref().as_os_str().to_string_lossy().into_owned())?;
220
221        unsafe {
222            XPLMLookupObjects(
223                path_c.as_ptr(),
224                lat,
225                lon,
226                Some(library_enumerator),
227                objects_ptr,
228            );
229        }
230
231        Ok(objects)
232    }
233
234    /// Load an object synchronously.
235    /// The path provided should be relative to the X-System root.
236    /// If the object could not be loaded (reason unknown), [`Ok(None)`] will be returned.
237    /// # Errors
238    /// An error will be returned if the path contains a NUL byte.
239    pub fn load_object(&mut self, path: PathBuf) -> Result<Option<XObject>, NulError> {
240        let path_c = std::ffi::CString::new(path.as_os_str().to_string_lossy().into_owned())?;
241        let handle = unsafe { XPLMLoadObject(path_c.as_ptr()) };
242        if handle.is_null() {
243            Ok(None)
244        } else {
245            Ok(Some(XObject {
246                handle,
247                path,
248                _phantom: PhantomData,
249            }))
250        }
251    }
252
253    /// Load an object asynchronously.
254    /// The path provided should be relative to the X-System root.
255    /// If the object could not be loaded (reason unknown), [`Ok(None)`] will be returned.
256    ///
257    /// I can't think of a better way to handle the callback, so I recommend making
258    /// an `Rc<Cell<Option<XObject>>>`, or something of the like, to get the [`XObject`]
259    /// out of the callback.
260    /// # Errors
261    /// An error will be returned if the path contains a NUL byte.
262    pub fn load_object_async<C>(&mut self, path: PathBuf, callback: C) -> Result<(), NulError>
263    where
264        C: FnOnce(Option<XObject>),
265    {
266        let path_c = std::ffi::CString::new(path.as_os_str().to_string_lossy().into_owned())?;
267
268        let ctx = Box::into_raw(Box::new(ObjectLoadContext {
269            obj_path: path,
270            callback,
271        }));
272
273        unsafe {
274            XPLMLoadObjectAsync(
275                path_c.as_ptr(),
276                Some(object_loaded_callback::<C>),
277                ctx.cast::<c_void>(),
278            );
279        }
280        Ok(())
281    }
282
283    /// Reload the current set of scenery.
284    ///
285    /// This will only cause X-Plane to re-read already loaded scenery, not load new scenery.
286    /// Equivalent to pressing "reload scenery" in the developer menu.
287    pub fn reload_scenery(&mut self) {
288        unsafe {
289            XPLMReloadScenery();
290        }
291    }
292}
293
294unsafe extern "C-unwind" fn library_enumerator(file_path: *const c_char, refcon: *mut c_void) {
295    let out = unsafe {
296        refcon.cast::<Vec<PathBuf>>().as_mut().unwrap() // UNWRAP: This pointer will never be null.
297    };
298    let file_path = unsafe { CStr::from_ptr(file_path) };
299    let file_path = file_path.to_owned();
300    let file_path = file_path.into_string().unwrap(); // UNWRAP: X-Plane promises to give good UTF-8.
301    let file_path = PathBuf::from(file_path);
302    out.push(file_path);
303}
304
305unsafe extern "C-unwind" fn object_loaded_callback<C>(obj: XPLMObjectRef, refcon: *mut c_void)
306where
307    C: FnOnce(Option<XObject>),
308{
309    let ctx = unsafe { Box::from_raw(refcon.cast::<ObjectLoadContext<C>>()) };
310    let obj = if obj.is_null() {
311        None
312    } else {
313        Some(XObject {
314            handle: obj,
315            path: ctx.obj_path,
316            _phantom: PhantomData,
317        })
318    };
319    (ctx.callback)(obj);
320}