use std::ffi::{c_void, CStr, CString};
use std::marker::PhantomData;
use std::os::raw::c_int;
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(any(feature = "csl-offline", feature = "csl-on-demand"))]
use xpmp2_sys::XPMPLoadCSLPackage;
use xpmp2_sys::{
xpmp2_shim_aircraft_camera_bearing, xpmp2_shim_aircraft_camera_dist,
xpmp2_shim_aircraft_change_model, xpmp2_shim_aircraft_contrail_remove,
xpmp2_shim_aircraft_contrail_request, xpmp2_shim_aircraft_contrail_trigger,
xpmp2_shim_aircraft_create, xpmp2_shim_aircraft_dataref_count, xpmp2_shim_aircraft_destroy,
xpmp2_shim_aircraft_get_dataref, xpmp2_shim_aircraft_get_flight_id,
xpmp2_shim_aircraft_get_location, xpmp2_shim_aircraft_get_model_name,
xpmp2_shim_aircraft_get_vert_ofs, xpmp2_shim_aircraft_ground_speed_kn,
xpmp2_shim_aircraft_is_glider, xpmp2_shim_aircraft_is_ground_vehicle,
xpmp2_shim_aircraft_is_related_to, xpmp2_shim_aircraft_is_rendered,
xpmp2_shim_aircraft_is_shown_as_ai, xpmp2_shim_aircraft_is_shown_as_tcas_target,
xpmp2_shim_aircraft_is_sound_muted, xpmp2_shim_aircraft_is_valid,
xpmp2_shim_aircraft_is_visible, xpmp2_shim_aircraft_match_quality,
xpmp2_shim_aircraft_mode_s_id, xpmp2_shim_aircraft_rematch_model,
xpmp2_shim_aircraft_set_ai_priority, xpmp2_shim_aircraft_set_clamp_to_ground,
xpmp2_shim_aircraft_set_dataref, xpmp2_shim_aircraft_set_gear_deflect_ratio,
xpmp2_shim_aircraft_set_heading, xpmp2_shim_aircraft_set_info_texts,
xpmp2_shim_aircraft_set_label, xpmp2_shim_aircraft_set_label_color,
xpmp2_shim_aircraft_set_label_drawn, xpmp2_shim_aircraft_set_local_loc,
xpmp2_shim_aircraft_set_location, xpmp2_shim_aircraft_set_mass,
xpmp2_shim_aircraft_set_on_ground, xpmp2_shim_aircraft_set_pitch,
xpmp2_shim_aircraft_set_radar, xpmp2_shim_aircraft_set_render, xpmp2_shim_aircraft_set_roll,
xpmp2_shim_aircraft_set_sound_min_dist, xpmp2_shim_aircraft_set_sound_muted,
xpmp2_shim_aircraft_set_velocity, xpmp2_shim_aircraft_set_vert_ofs_ratio,
xpmp2_shim_aircraft_set_visible, xpmp2_shim_aircraft_set_wing_area,
xpmp2_shim_aircraft_set_wing_span, xpmp2_shim_aircraft_show_as_ai_plane,
xpmp2_shim_aircraft_tcas_target_idx, xpmp2_shim_aircraft_wake_apply_defaults,
XPMP2ShimAircraft, XPMPMultiplayerCleanup, XPMPMultiplayerInit,
};
#[cfg(all(feature = "csl-offline", feature = "csl-on-demand"))]
compile_error!(
"xpmp2's `csl-offline` and `csl-on-demand` features are mutually exclusive — \
enable only the one matching your plugin's loading strategy. This is most \
often a Cargo feature-unification accident in a workspace: if one crate \
depends on xpmp2 with `csl-offline` and another with `csl-on-demand`, \
building both together unifies onto both being enabled. See CLAUDE.md's \
\"CSL package loading\" note for why these are two independent loading \
strategies, not a dependency chain."
);
#[cfg(feature = "csl-on-demand")]
pub mod csl_on_demand;
static INITIALIZED: AtomicBool = AtomicBool::new(false);
pub struct Multiplayer {
_not_sync: PhantomData<*const ()>,
}
unsafe impl Send for Multiplayer {}
impl Multiplayer {
pub fn init(
plugin_name: &str,
resource_dir: &str,
default_icao: Option<&str>,
log_acronym: Option<&str>,
) -> Result<Self, String> {
if INITIALIZED.swap(true, Ordering::SeqCst) {
panic!("xpmp2::Multiplayer is already initialized — only one instance may be live at a time");
}
let plugin_name = CString::new(plugin_name).expect("plugin_name contains a NUL byte");
let resource_dir = CString::new(resource_dir).expect("resource_dir contains a NUL byte");
let default_icao =
default_icao.map(|s| CString::new(s).expect("default_icao contains a NUL byte"));
let log_acronym =
log_acronym.map(|s| CString::new(s).expect("log_acronym contains a NUL byte"));
let result = unsafe {
XPMPMultiplayerInit(
plugin_name.as_ptr(),
resource_dir.as_ptr(),
None,
default_icao
.as_ref()
.map_or(std::ptr::null(), |s| s.as_ptr()),
log_acronym
.as_ref()
.map_or(std::ptr::null(), |s| s.as_ptr()),
)
};
let message = unsafe { CStr::from_ptr(result) }
.to_string_lossy()
.into_owned();
if message.is_empty() {
Ok(Self {
_not_sync: PhantomData,
})
} else {
INITIALIZED.store(false, Ordering::SeqCst);
Err(message)
}
}
#[cfg(feature = "csl-offline")]
pub fn load_csl_package(&self, csl_folder: &str) -> Result<(), String> {
load_csl_package_raw(csl_folder)
}
}
#[cfg(any(feature = "csl-offline", feature = "csl-on-demand"))]
pub(crate) fn load_csl_package_raw(csl_folder: &str) -> Result<(), String> {
let csl_folder = CString::new(csl_folder).expect("csl_folder contains a NUL byte");
let result = unsafe { XPMPLoadCSLPackage(csl_folder.as_ptr()) };
let message = unsafe { CStr::from_ptr(result) }
.to_string_lossy()
.into_owned();
if message.is_empty() {
Ok(())
} else {
Err(message)
}
}
impl Drop for Multiplayer {
fn drop(&mut self) {
unsafe {
XPMPMultiplayerCleanup();
}
INITIALIZED.store(false, Ordering::SeqCst);
}
}
pub trait Aircraft: std::any::Any {
fn update_position(
&mut self,
plane: &PlaneHandle,
elapsed_since_last_call: f32,
fl_counter: i32,
);
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransponderMode {
Off = 0,
Standby,
ModeA,
ModeC,
Test,
ModeSGround,
ModeSTaOnly,
ModeSTaRa,
}
#[repr(usize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataRefIndex {
ControlsGearRatio = 0,
ControlsNwsRatio,
ControlsFlapRatio,
ControlsSpoilerRatio,
ControlsSpeedBrakeRatio,
ControlsSlatRatio,
ControlsWingSweepRatio,
ControlsThrustRatio,
ControlsYokePitchRatio,
ControlsYokeHeadingRatio,
ControlsYokeRollRatio,
ControlsThrustRevers,
ControlsTaxiLitesOn,
ControlsLandingLitesOn,
ControlsBeaconLitesOn,
ControlsStrobeLitesOn,
ControlsNavLitesOn,
GearNoseGearDeflectionMtr,
GearTireVerticalDeflectionMtr,
GearTireRotationAngleDeg,
GearTireRotationSpeedRpm,
GearTireRotationSpeedRadSec,
EnginesEngineRotationAngleDeg,
EnginesEngineRotationSpeedRpm,
EnginesEngineRotationSpeedRadSec,
EnginesPropRotationAngleDeg,
EnginesPropRotationSpeedRpm,
EnginesPropRotationSpeedRadSec,
EnginesThrustReverserDeployRatio,
EnginesEngineRotationAngleDeg1,
EnginesEngineRotationAngleDeg2,
EnginesEngineRotationAngleDeg3,
EnginesEngineRotationAngleDeg4,
EnginesEngineRotationSpeedRpm1,
EnginesEngineRotationSpeedRpm2,
EnginesEngineRotationSpeedRpm3,
EnginesEngineRotationSpeedRpm4,
EnginesEngineRotationSpeedRadSec1,
EnginesEngineRotationSpeedRadSec2,
EnginesEngineRotationSpeedRadSec3,
EnginesEngineRotationSpeedRadSec4,
MiscTouchDown,
}
pub struct PlaneHandle<'a> {
raw: *mut XPMP2ShimAircraft,
_marker: PhantomData<&'a ()>,
}
impl PlaneHandle<'_> {
pub fn mode_s_id(&self) -> u32 {
unsafe { xpmp2_shim_aircraft_mode_s_id(self.raw) }
}
pub fn is_valid(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_valid(self.raw) != 0 }
}
pub fn set_visible(&self, visible: bool) {
unsafe { xpmp2_shim_aircraft_set_visible(self.raw, visible as c_int) }
}
pub fn is_visible(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_visible(self.raw) != 0 }
}
pub fn set_location(&self, lat: f64, lon: f64, alt_ft: f64) {
unsafe { xpmp2_shim_aircraft_set_location(self.raw, lat, lon, alt_ft) }
}
pub fn location(&self) -> (f64, f64, f64) {
let (mut lat, mut lon, mut alt_ft) = (0.0, 0.0, 0.0);
unsafe { xpmp2_shim_aircraft_get_location(self.raw, &mut lat, &mut lon, &mut alt_ft) }
(lat, lon, alt_ft)
}
pub fn set_local_location(&self, x: f32, y: f32, z: f32) {
unsafe { xpmp2_shim_aircraft_set_local_loc(self.raw, x, y, z) }
}
pub fn set_pitch(&self, degrees: f32) {
unsafe { xpmp2_shim_aircraft_set_pitch(self.raw, degrees) }
}
pub fn set_heading(&self, degrees: f32) {
unsafe { xpmp2_shim_aircraft_set_heading(self.raw, degrees) }
}
pub fn set_roll(&self, degrees: f32) {
unsafe { xpmp2_shim_aircraft_set_roll(self.raw, degrees) }
}
pub fn set_on_ground(&self, on_ground: bool) {
unsafe { xpmp2_shim_aircraft_set_on_ground(self.raw, on_ground as c_int) }
}
pub fn set_velocity(&self, vx: f32, vy: f32, vz: f32) {
unsafe { xpmp2_shim_aircraft_set_velocity(self.raw, vx, vy, vz) }
}
pub fn set_label(&self, label: &str) {
let label = CString::new(label).unwrap_or_default();
unsafe { xpmp2_shim_aircraft_set_label(self.raw, label.as_ptr()) }
}
pub fn set_label_drawn(&self, drawn: bool) {
unsafe { xpmp2_shim_aircraft_set_label_drawn(self.raw, drawn as c_int) }
}
pub fn dataref_count(&self) -> usize {
unsafe { xpmp2_shim_aircraft_dataref_count(self.raw) }
}
pub fn dataref(&self, index: usize) -> f32 {
unsafe { xpmp2_shim_aircraft_get_dataref(self.raw, index) }
}
pub fn set_dataref(&self, index: usize, value: f32) {
unsafe { xpmp2_shim_aircraft_set_dataref(self.raw, index, value) }
}
pub fn set_radar(&self, code: u16, mode: TransponderMode) {
unsafe {
xpmp2_shim_aircraft_set_radar(self.raw, code as std::os::raw::c_long, mode as c_int)
}
}
pub fn set_label_color(&self, r: f32, g: f32, b: f32, a: f32) {
unsafe { xpmp2_shim_aircraft_set_label_color(self.raw, r, g, b, a) }
}
pub fn set_vert_ofs_ratio(&self, ratio: f32) {
unsafe { xpmp2_shim_aircraft_set_vert_ofs_ratio(self.raw, ratio) }
}
pub fn vert_ofs(&self) -> f32 {
unsafe { xpmp2_shim_aircraft_get_vert_ofs(self.raw) }
}
pub fn set_gear_deflect_ratio(&self, ratio: f32) {
unsafe { xpmp2_shim_aircraft_set_gear_deflect_ratio(self.raw, ratio) }
}
pub fn set_clamp_to_ground(&self, clamp: bool) {
unsafe { xpmp2_shim_aircraft_set_clamp_to_ground(self.raw, clamp as c_int) }
}
pub fn set_ai_priority(&self, priority: i32) {
unsafe { xpmp2_shim_aircraft_set_ai_priority(self.raw, priority) }
}
pub fn set_render(&self, render: bool) {
unsafe { xpmp2_shim_aircraft_set_render(self.raw, render as c_int) }
}
pub fn is_rendered(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_rendered(self.raw) != 0 }
}
pub fn tcas_target_idx(&self) -> i32 {
unsafe { xpmp2_shim_aircraft_tcas_target_idx(self.raw) }
}
pub fn is_shown_as_tcas_target(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_shown_as_tcas_target(self.raw) != 0 }
}
pub fn is_shown_as_ai(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_shown_as_ai(self.raw) != 0 }
}
pub fn show_as_ai_plane(&self) -> bool {
unsafe { xpmp2_shim_aircraft_show_as_ai_plane(self.raw) != 0 }
}
pub fn camera_dist(&self) -> f32 {
unsafe { xpmp2_shim_aircraft_camera_dist(self.raw) }
}
pub fn camera_bearing(&self) -> f32 {
unsafe { xpmp2_shim_aircraft_camera_bearing(self.raw) }
}
pub fn ground_speed_kn(&self) -> f32 {
unsafe { xpmp2_shim_aircraft_ground_speed_kn(self.raw) }
}
pub fn is_related_to(&self, icao_type: &str) -> bool {
let icao_type = CString::new(icao_type).unwrap_or_default();
unsafe { xpmp2_shim_aircraft_is_related_to(self.raw, icao_type.as_ptr()) != 0 }
}
pub fn is_ground_vehicle(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_ground_vehicle(self.raw) != 0 }
}
pub fn is_glider(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_glider(self.raw) != 0 }
}
pub fn change_model(&self, icao_type: &str, icao_airline: &str, livery: &str) -> i32 {
let icao_type = CString::new(icao_type).unwrap_or_default();
let icao_airline = CString::new(icao_airline).unwrap_or_default();
let livery = CString::new(livery).unwrap_or_default();
unsafe {
xpmp2_shim_aircraft_change_model(
self.raw,
icao_type.as_ptr(),
icao_airline.as_ptr(),
livery.as_ptr(),
)
}
}
pub fn rematch_model(&self) -> i32 {
unsafe { xpmp2_shim_aircraft_rematch_model(self.raw) }
}
pub fn match_quality(&self) -> i32 {
unsafe { xpmp2_shim_aircraft_match_quality(self.raw) }
}
pub fn model_name(&self) -> String {
unsafe { read_shim_string(self.raw, xpmp2_shim_aircraft_get_model_name) }
}
pub fn flight_id(&self) -> String {
unsafe { read_shim_string(self.raw, xpmp2_shim_aircraft_get_flight_id) }
}
#[allow(clippy::too_many_arguments)]
pub fn set_info_texts(
&self,
tail_num: &str,
icao_ac_type: &str,
manufacturer: &str,
model: &str,
icao_airline: &str,
airline: &str,
flight_num: &str,
apt_from: &str,
apt_to: &str,
) {
let tail_num = CString::new(tail_num).unwrap_or_default();
let icao_ac_type = CString::new(icao_ac_type).unwrap_or_default();
let manufacturer = CString::new(manufacturer).unwrap_or_default();
let model = CString::new(model).unwrap_or_default();
let icao_airline = CString::new(icao_airline).unwrap_or_default();
let airline = CString::new(airline).unwrap_or_default();
let flight_num = CString::new(flight_num).unwrap_or_default();
let apt_from = CString::new(apt_from).unwrap_or_default();
let apt_to = CString::new(apt_to).unwrap_or_default();
unsafe {
xpmp2_shim_aircraft_set_info_texts(
self.raw,
tail_num.as_ptr(),
icao_ac_type.as_ptr(),
manufacturer.as_ptr(),
model.as_ptr(),
icao_airline.as_ptr(),
airline.as_ptr(),
flight_num.as_ptr(),
apt_from.as_ptr(),
apt_to.as_ptr(),
)
}
}
pub fn set_wing_span(&self, meters: f32) {
unsafe { xpmp2_shim_aircraft_set_wing_span(self.raw, meters) }
}
pub fn set_wing_area(&self, square_meters: f32) {
unsafe { xpmp2_shim_aircraft_set_wing_area(self.raw, square_meters) }
}
pub fn set_mass(&self, kg: f32) {
unsafe { xpmp2_shim_aircraft_set_mass(self.raw, kg) }
}
pub fn wake_apply_defaults(&self, overwrite_all: bool) {
unsafe { xpmp2_shim_aircraft_wake_apply_defaults(self.raw, overwrite_all as c_int) }
}
pub fn contrail_request(&self, num: u32, dist_m: u32, life_time_s: u32) {
unsafe { xpmp2_shim_aircraft_contrail_request(self.raw, num, dist_m, life_time_s) }
}
pub fn contrail_remove(&self) {
unsafe { xpmp2_shim_aircraft_contrail_remove(self.raw) }
}
pub fn contrail_trigger(&self) -> u32 {
unsafe { xpmp2_shim_aircraft_contrail_trigger(self.raw) }
}
pub fn set_sound_min_dist(&self, meters: i32) {
unsafe { xpmp2_shim_aircraft_set_sound_min_dist(self.raw, meters) }
}
pub fn set_sound_muted(&self, mute: bool) {
unsafe { xpmp2_shim_aircraft_set_sound_muted(self.raw, mute as c_int) }
}
pub fn is_sound_muted(&self) -> bool {
unsafe { xpmp2_shim_aircraft_is_sound_muted(self.raw) != 0 }
}
}
unsafe fn read_shim_string(
raw: *mut XPMP2ShimAircraft,
f: unsafe extern "C" fn(*const XPMP2ShimAircraft, *mut std::os::raw::c_char, usize) -> usize,
) -> String {
let mut buf = [0u8; 128];
let len = f(raw, buf.as_mut_ptr() as *mut _, buf.len());
if len < buf.len() {
return String::from_utf8_lossy(&buf[..len]).into_owned();
}
let mut buf = vec![0u8; len + 1];
let len2 = f(raw, buf.as_mut_ptr() as *mut _, buf.len());
String::from_utf8_lossy(&buf[..len2]).into_owned()
}
struct Refcon {
aircraft: Box<dyn Aircraft>,
raw: *mut XPMP2ShimAircraft,
}
unsafe extern "C" fn update_position_trampoline(
refcon: *mut c_void,
elapsed_since_last_call: f32,
fl_counter: c_int,
) {
xplm::guard(|| {
let refcon = &mut *(refcon as *mut Refcon);
let plane = PlaneHandle {
raw: refcon.raw,
_marker: PhantomData,
};
refcon
.aircraft
.update_position(&plane, elapsed_since_last_call, fl_counter);
});
}
pub struct Plane {
raw: *mut XPMP2ShimAircraft,
_refcon: Box<Refcon>,
}
unsafe impl Send for Plane {}
impl Plane {
pub fn new(
_multiplayer: &Multiplayer,
icao_type: &str,
icao_airline: &str,
livery: &str,
mode_s_id: u32,
csl_id: &str,
aircraft: impl Aircraft + 'static,
) -> Option<Self> {
#[cfg(feature = "csl-on-demand")]
assert!(
!csl_id.is_empty(),
"xpmp2::Plane::new: csl_id must not be empty under the csl-on-demand feature \
— pass csl_on_demand::FetchedPackage::csl_id, not \"\""
);
let icao_type = CString::new(icao_type).unwrap_or_default();
let icao_airline = CString::new(icao_airline).unwrap_or_default();
let livery = CString::new(livery).unwrap_or_default();
let csl_id = CString::new(csl_id).unwrap_or_default();
let mut refcon = Box::new(Refcon {
aircraft: Box::new(aircraft),
raw: std::ptr::null_mut(),
});
let refcon_ptr: *mut Refcon = &mut *refcon;
let raw = unsafe {
xpmp2_shim_aircraft_create(
icao_type.as_ptr(),
icao_airline.as_ptr(),
livery.as_ptr(),
mode_s_id,
csl_id.as_ptr(),
Some(update_position_trampoline),
refcon_ptr as *mut c_void,
)
};
if raw.is_null() {
return None;
}
refcon.raw = raw;
Some(Self {
raw,
_refcon: refcon,
})
}
pub fn handle(&self) -> PlaneHandle<'_> {
PlaneHandle {
raw: self.raw,
_marker: PhantomData,
}
}
pub fn aircraft(&self) -> &dyn Aircraft {
&*self._refcon.aircraft
}
pub fn aircraft_mut(&mut self) -> &mut dyn Aircraft {
&mut *self._refcon.aircraft
}
}
impl Drop for Plane {
fn drop(&mut self) {
unsafe {
xpmp2_shim_aircraft_destroy(self.raw);
}
}
}