1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
// SPDX-FileCopyrightText: 2024 Julia DeMille <me@jdemille.com>
//
// SPDX-License-Identifier: MPL-2.0
//! Bindings to the X-Plane plugin SDK.
//! These should be mostly safe, although care must be taken in some aspects.
//! Any functions or modules that could behave in unexpected ways will try document that.
//!
//! Panics should reliably unwind out into the simulator and produce a backtrace. The core
//! will dump in a non-graceful manner, however, since X-Plane does not have an exception
//! handler with the right personality for libunwind to grab at the bottom of the stack.
#[cfg(feature = "XPLM400")]
use crate::avionics::AvionicsApi;
use crate::camera::CameraApi;
use crate::command::CommandApi;
use crate::data::DataApi;
use crate::feature::FeatureApi;
use crate::flight_loop::{FlightLoop, FlightLoopCallback, FlightLoopPhase};
use crate::menu::MenuApi;
use crate::navigation::{Fms, NavApi};
use crate::paths::PathApi;
use crate::player::PlayerApi;
use crate::plugin::management::PluginApi;
use crate::scenery::SceneryApi;
#[cfg(all(feature = "XPLM400", feature = "fmod"))]
use crate::sound::SoundApi;
#[cfg(feature = "XPLM400")]
use crate::weather::WeatherApi;
use std::ffi::c_void;
use std::{
ffi::{CStr, CString, NulError},
marker::PhantomData,
ptr,
};
use tailcall::tailcall;
use xplane_sys::{
XPLMDebugString, XPLMGetLanguage, XPLMGetVersions, XPLMGetVirtualKeyDescription,
XPLMHostApplicationID, XPLMLanguageCode, XPLMSpeakString,
};
/// FFI utilities
mod ffi;
/// Plugin macro
mod plugin_macro;
/// Utilities that the `xplane_plugin` macro-generated code uses
mod internal;
/// Avionics
#[cfg(feature = "XPLM400")]
pub mod avionics;
/// Camera access.
pub mod camera;
/// Commands
pub mod command;
/// Datarefs
pub mod data;
/// Error detection
pub mod error;
/// SDK feature management
pub mod feature;
/// Flight loop callbacks
pub mod flight_loop;
pub mod geometry;
/// User interface menus
pub mod menu;
/// Plugin messages
pub mod message;
/// Navigation APIs
pub mod navigation;
#[cfg(feature = "XPLM303")]
/// [`XPLMInstance`] API wrappers.
/// Locked behind XPLM303 due to bugs in earlier versions of X-Plane.
pub mod obj_instance;
/// Path conversion
pub mod paths;
/// Utility functions relating to the player.
pub mod player;
/// Plugin creation and management
pub mod plugin;
/// APIs to interact with X-Plane's scenery system.
pub mod scenery;
/// APIs to interact with Fmod in X-Plane.
#[cfg(all(feature = "XPLM400", feature = "fmod"))]
pub mod sound;
/// Weather system
#[cfg(feature = "XPLM400")]
pub mod weather;
/// Relatively low-level windows
pub mod window;
type NoSendSync = PhantomData<*mut ()>;
#[tailcall]
fn xp_major_ver(input: i32, full_version: i32) -> (i32, i32) {
if !(-99..=99).contains(&input) {
xp_major_ver(input, full_version)
}
(input, full_version)
}
/// Access struct for all APIs in this crate. Intentionally neither [`Send`] nor [`Sync`]. Almost nothing in this crate is.
#[allow(missing_docs)]
pub struct XPAPI {
// Name not decided on.
#[cfg(feature = "XPLM400")]
pub avionics: AvionicsApi,
pub camera: CameraApi,
pub command: CommandApi,
pub data: DataApi,
pub features: FeatureApi,
pub menu: MenuApi,
pub nav: NavApi,
pub paths: PathApi,
pub player: PlayerApi,
pub plugins: PluginApi,
pub scenery: SceneryApi,
#[cfg(all(feature = "XPLM400", feature = "fmod"))]
pub sound: SoundApi,
#[cfg(feature = "XPLM400")]
pub weather: WeatherApi,
_phantom: NoSendSync, // Make this !Send + !Sync.
}
impl XPAPI {
/// Write a string to the X-Plane log. You probably want [`debug!`] or [`debugln!`] instead.
/// Keep output to the X-Plane log to a minimum. This file can get rather cluttered.
/// # Errors
/// This function will error if the passed [`String`] has a NUL ('\0') character in it.
pub fn debug_string<S: Into<Vec<u8>>>(&mut self, s: S) -> Result<(), NulError> {
let s = CString::new(s)?;
unsafe {
XPLMDebugString(s.as_ptr());
}
Ok(())
}
/// Display a string on the screen, and speak it with TTS, if enabled.
/// # Errors
/// Returns a [`NulError`] if the passed string contains a NUL byte.
pub fn speak_string<S: Into<Vec<u8>>>(&mut self, s: S) -> Result<(), NulError> {
let s = CString::new(s)?;
unsafe {
XPLMSpeakString(s.as_ptr());
}
Ok(())
}
/// Creates a new flight loop. The provided callback will not be
/// called until the loop is scheduled.
pub fn new_flight_loop<T: 'static>(
&mut self,
phase: FlightLoopPhase,
callback: impl FlightLoopCallback<T>,
base_state: T,
) -> FlightLoop<T> {
FlightLoop::new(phase, callback, base_state)
}
/// Attempts to locate a symbol. If it exists, returns a pointer to it.
/// Otherwise, a null pointer is returned.
pub fn find_symbol<S: Into<String>>(&mut self, name: S) -> *mut c_void {
match std::ffi::CString::new(name.into()) {
Ok(name_c) => unsafe { xplane_sys::XPLMFindSymbol(name_c.as_ptr()) },
Err(_) => ptr::null_mut(),
}
}
/// Get the versions of X-Plane and XPLM, respectively.
///
/// There are no guarantees about the form of the version numbers, except
/// that subsequent versions will have greater numbers.
///
/// The first entry of the tuple is a tuple containing:
/// - The major version of X-Plane (the two most significant digits of the X-Plane version)
/// - All remaining digits of the X-Plane version
/// The second entry of the tuple is the XPLM version.
pub fn get_versions(&mut self) -> ((i32, i32), i32) {
let mut xp = 0i32;
let mut xplm = 0i32;
let mut host_id = XPLMHostApplicationID::XPlane;
unsafe {
XPLMGetVersions(&mut xp, &mut xplm, &mut host_id);
}
(xp_major_ver(xp, xp), xplm)
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
/// Get the description of a virtual key.
/// # Panics
/// Panics if X-Plane gives invalid UTF-8. If this happens, panicking here
/// is the least of your problems.
pub fn get_vkey_desc(&mut self, key: window::Key) -> &str {
let desc = unsafe { XPLMGetVirtualKeyDescription(u32::from(key) as i8) };
unsafe {
CStr::from_ptr(desc).to_str().unwrap() // UNWRAP: X-Plane promises to give good UTF-8.
}
}
/// Get the language X-Plane is running in.
/// If recognized, returns [`Some`], with the ISO 639-1 code.
/// Returns [`None`] if it is not recognized.
pub fn get_language(&mut self) -> Option<&'static str> {
let lang = unsafe { XPLMGetLanguage() };
match lang {
XPLMLanguageCode::English => Some("en"),
XPLMLanguageCode::French => Some("fr"),
XPLMLanguageCode::German => Some("de"),
XPLMLanguageCode::Italian => Some("it"),
XPLMLanguageCode::Spanish => Some("es"),
XPLMLanguageCode::Korean => Some("ko"),
XPLMLanguageCode::Russian => Some("ru"),
XPLMLanguageCode::Greek => Some("el"),
XPLMLanguageCode::Japanese => Some("ja"),
#[cfg(feature = "XPLM300")]
XPLMLanguageCode::Chinese => Some("zh"),
_ => None,
}
}
}
#[inline]
fn make_x() -> XPAPI {
XPAPI {
#[cfg(feature = "XPLM400")]
avionics: AvionicsApi {
_phantom: PhantomData,
},
camera: CameraApi {
_phantom: PhantomData,
},
command: CommandApi {
_phantom: PhantomData,
},
data: DataApi {
_phantom: PhantomData,
},
features: FeatureApi {
_phantom: PhantomData,
},
menu: MenuApi {
_phantom: PhantomData,
},
nav: NavApi {
fms: Fms {
_phantom: PhantomData,
},
_phantom: PhantomData,
},
paths: PathApi {
_phantom: PhantomData,
},
player: PlayerApi {
_phantom: PhantomData,
},
plugins: PluginApi {
_phantom: PhantomData,
},
scenery: SceneryApi {
_phantom: PhantomData,
},
#[cfg(all(feature = "XPLM400", feature = "fmod"))]
sound: SoundApi {
_phantom: PhantomData,
},
#[cfg(feature = "XPLM400")]
weather: WeatherApi {
_phantom: PhantomData,
},
_phantom: PhantomData,
}
}
/// Writes a message to the developer console and Log.txt file.
/// Keep output to the X-Plane log to a minimum. This file can get rather cluttered.
/// # Errors
/// This macro will return a `Result<(), NulError>`. An [`Err`] may be returned if
/// the formatting you specify produces a NUL byte within the string.
#[macro_export]
macro_rules! debug {
($x:ident, $($arg:tt)*) => ({
let formatted_string: String = std::fmt::format(std::format_args!($($arg)*));
$x.debug_string(formatted_string)
});
}
/// Writes a message to the developer console and Log.txt file, with a newline.
/// Keep output to the X-Plane log to a minimum. This file can get rather cluttered.
/// # Errors
/// This macro will return a `Result<(), NulError>`. An [`Err`] may be returned if
/// the formatting you specify produces a NUL byte within the string.
#[macro_export]
#[allow(unused_unsafe)]
macro_rules! debugln {
($x:ident) => ($crate::debug!($x, "\n"));
($x:ident, $($arg:tt)*) => ({
let mut formatted_string: String = std::fmt::format(std::format_args!($($arg)*));
formatted_string.push_str("\n");
$x.debug_string(formatted_string)
});
}