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
// SPDX-FileCopyrightText: 2024 Julia DeMille <me@jdemille.com>
//
// SPDX-License-Identifier: MPL-2.0
use std::{
ffi::{CStr, CString, NulError},
fmt,
marker::PhantomData,
};
use std::ffi::{c_char, c_void};
use xplane_sys;
use crate::NoSendSync;
/// A feature provided by the SDK that this plugin is running in
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Feature {
/// The name of this feature
/// Invariant: this can be successfully converted into a CString
name: String,
_phantom: NoSendSync,
}
/// Access struct for the Feature API.
pub struct FeatureApi {
pub(crate) _phantom: NoSendSync, // Make this !Send + !Sync
}
impl Feature {
/// Paradoxically, when this is enabled, X-Plane will use Unix-style paths.
/// On Windows, the drive letter will be retained, but backslashes will be converted to slashes.
///
/// # Note
/// This feature should be enabled automatically by this library.
pub const USE_NATIVE_PATHS: &'static str = "XPLM_USE_NATIVE_PATHS";
/// When this is enabled, the X-Plane widgets library will use new, modern, X-Plane backed `XPLMDisplay`
/// windows to anchor all widget trees. Without it, widgets will always use legacy windows.
///
/// You probably want this enabled. Make sure your widget code can handle the UI coordinate system
/// not being the same as the OpenGL window coordinate system.
pub const USE_NATIVE_WIDGET_WINDOWS: &'static str = "XPLM_USE_NATIVE_WIDGET_WINDOWS";
/// When enabled, X-Plane will send a message any time new datarefs are added.
///
/// XPLM will combine consecutive dataref registrations to minimize the number of messages sent.
pub const WANTS_DATAREF_NOTIFICATIONS: &'static str = "XPLM_WANTS_DATAREF_NOTIFICATIONS";
/// Returns the name of this feature
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
/// Returns true if this feature is currently enabled
/// # Panics
/// This function can in theory panic if the `Feature` name is not a valid `CString`. This should not be possible.
#[must_use]
pub fn enabled(&self) -> bool {
let name_c = CString::new(&*self.name).unwrap();
let enabled = unsafe { xplane_sys::XPLMIsFeatureEnabled(name_c.as_ptr()) };
enabled == 1
}
/// Enables or disables this feature
/// # Panics
/// This function can in theory panic if the `Feature` name is not a valid `CString`. This should not be possible.
pub fn set_enabled(&self, enable: bool) {
// Because this name was either copied from C with XPLMEnumerateFeatures or
// checked with XPLMHasFeature, it must be valid as a C string.
let name_c = CString::new(&*self.name).unwrap();
unsafe { xplane_sys::XPLMEnableFeature(name_c.as_ptr(), i32::from(enable)) }
}
}
impl fmt::Display for Feature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl FeatureApi {
/// Looks for a feature with the provided name and returns it if it exists
/// # Errors
/// This function will return an error if `name` contains a NUL byte.
/// # Panics
/// This function can panic if `name`, which came in as a String-like, then was turned into a `CString`,
/// cannot be turned back into a `String`. This should not be possible.
pub fn find<S: Into<String>>(&mut self, name: S) -> Result<Option<Feature>, NulError> {
let name = CString::new(name.into())?;
let has_feature = unsafe { xplane_sys::XPLMHasFeature(name.as_ptr()) };
if has_feature == 1 {
// Convert name back into a String
// Because the string was not modified, conversion should always work.
Ok(Some(Feature {
name: name.into_string().unwrap(),
_phantom: PhantomData,
}))
} else {
Ok(None)
}
}
/// Returns all features supported by the X-Plane plugin SDK
pub fn all(&mut self) -> Vec<Feature> {
let mut features = Vec::new();
let features_ptr: *mut _ = &mut features;
unsafe {
xplane_sys::XPLMEnumerateFeatures(
Some(feature_callback),
features_ptr.cast::<c_void>(),
);
}
features
}
}
/// Interprets refcon as a pointer to a Vec<Feature>.
/// Allocates a new Feature and adds it to the vector
unsafe extern "C-unwind" fn feature_callback(feature: *const c_char, refcon: *mut c_void) {
let features = refcon.cast::<Vec<Feature>>();
let name = unsafe { CStr::from_ptr(feature) };
if let Ok(name) = name.to_str() {
let new_feature = Feature {
name: name.to_owned(),
_phantom: PhantomData,
};
unsafe {
(*features).push(new_feature);
}
}
}
#[cfg(test)]
mod tests {
use crate::make_x;
use super::*;
#[test]
#[allow(clippy::too_many_lines)]
fn test_features() {
// Part miri food, part unit test.
let enumerate_features_ctx = xplane_sys::XPLMEnumerateFeatures_context();
enumerate_features_ctx
.expect()
.once()
.return_once_st(|cb, refcon| {
let cb = cb.unwrap(); // This should be Some.
let paths_feature = CString::new("XPLM_USE_NATIVE_PATHS").unwrap(); // We know that this is a valid C-string.
let other_feature = CString::new("XPLM_SOME_OTHER_FEATURE").unwrap(); // We know that this is a valid C-string.
unsafe {
cb(paths_feature.as_ptr(), refcon);
cb(other_feature.as_ptr(), refcon);
}
});
let has_feature_ctx = xplane_sys::XPLMHasFeature_context();
has_feature_ctx.expect().once().return_once_st(|feat| {
let feat = unsafe { CStr::from_ptr(feat) };
let feat = feat.to_str().unwrap(); // This should be valid UTF-8.
assert_eq!(feat, "XPLM_ANOTHER_FEATURE");
1
});
let feature_enabled_ctx = xplane_sys::XPLMIsFeatureEnabled_context();
feature_enabled_ctx.expect().once().return_once_st(|feat| {
let feat = unsafe { CStr::from_ptr(feat) };
let feat = feat.to_str().unwrap(); // This should be valid UTF-8.
assert_eq!(feat, "XPLM_ANOTHER_FEATURE");
0
});
let enable_feature_ctx = xplane_sys::XPLMEnableFeature_context();
enable_feature_ctx
.expect()
.once()
.return_once_st(|feat, enable| {
let feat = unsafe { CStr::from_ptr(feat) };
let feat = feat.to_str().unwrap(); // This should be valid UTF-8.
assert_eq!(feat, "XPLM_ANOTHER_FEATURE");
assert_eq!(enable, 1);
});
let mut x = make_x();
let feats: Vec<String> = x
.features
.all()
.iter()
.map(|feat| feat.name.clone())
.collect();
assert_eq!(
feats,
vec!["XPLM_USE_NATIVE_PATHS", "XPLM_SOME_OTHER_FEATURE"]
);
let feat = x.features.find("XPLM_ANOTHER_FEATURE").unwrap().unwrap();
assert!(!feat.enabled());
feat.set_enabled(true);
}
}