use super::ffi::{self, Id, Pool, sel};
use super::{
AXES, BUTTONS, GameControllerError, Key, Path, Poller, Reading, Seen, Source, kind_of,
};
use crate::{GamepadEvent, PadKind};
unsafe fn element(object: Id, path: Path) -> Id {
let mut current = object;
for getter in path {
let selector = sel(getter);
if current.is_null() || !unsafe { ffi::responds_to(current, selector) } {
return core::ptr::null_mut();
}
current = unsafe { ffi::msg(current, selector) };
}
current
}
unsafe fn read_profile(profile: Id) -> Reading {
let mut reading = Reading::default();
for (pressed, &(path, _)) in reading.pressed.iter_mut().zip(&BUTTONS) {
let button = unsafe { element(profile, path) };
*pressed = !button.is_null() && unsafe { ffi::msg_bool(button, sel(c"isPressed")) };
}
for (value, &(path, _)) in reading.values.iter_mut().zip(&AXES) {
let input = unsafe { element(profile, path) };
if !input.is_null() {
*value = unsafe { ffi::msg_f32(input, sel(c"value")) };
}
}
reading
}
unsafe fn kind_of_controller(controller: Id) -> PadKind {
let category = unsafe { ffi::string(element(controller, &[c"productCategory"])) };
let vendor = unsafe { ffi::string(element(controller, &[c"vendorName"])) };
kind_of(category.as_deref(), vendor.as_deref())
}
#[derive(Debug)]
struct Framework {
class: Id,
retained: Vec<(Id, PadKind)>,
}
impl Source for Framework {
fn read(&mut self, into: &mut Vec<Seen>) {
into.clear();
let _pool = Pool::push();
let array = unsafe { ffi::msg(self.class, sel(c"controllers")) };
let count = if array.is_null() {
0
} else {
unsafe { ffi::msg_usize(array, sel(c"count")) }
};
for index in 0..count {
let controller = unsafe { ffi::object_at(array, index) };
let profile = unsafe { ffi::msg(controller, sel(c"extendedGamepad")) };
if profile.is_null() {
continue;
}
let kind = match self.retained.iter().find(|&&(held, _)| held == controller) {
Some(&(_, kind)) => kind,
None => {
unsafe { ffi::msg(controller, sel(c"retain")) };
let kind = unsafe { kind_of_controller(controller) };
self.retained.push((controller, kind));
kind
}
};
into.push(Seen {
key: Key(controller as usize),
kind,
reading: unsafe { read_profile(profile) },
});
}
self.retained.retain(|&(held, _)| {
let listed = into.iter().any(|seen| seen.key == Key(held as usize));
if !listed {
unsafe { ffi::msg_void(held, sel(c"release")) };
}
listed
});
}
}
impl Drop for Framework {
fn drop(&mut self) {
for &(held, _) in &self.retained {
unsafe { ffi::msg_void(held, sel(c"release")) };
}
}
}
#[derive(Debug)]
pub struct GameController {
framework: Framework,
poller: Poller,
}
impl GameController {
pub fn new() -> Result<Self, GameControllerError> {
let class = ffi::class(c"GCController").ok_or(GameControllerError::Unavailable)?;
Ok(Self {
framework: Framework {
class,
retained: Vec::new(),
},
poller: Poller::default(),
})
}
pub fn poll(&mut self, mut emit: impl FnMut(GamepadEvent)) {
self.poller.poll(&mut self.framework, &mut emit);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::product_name::kind_of_name;
#[test]
fn the_controller_list_is_an_array() {
let pads = GameController::new().expect("GameController is linked");
let pool = Pool::push();
let array = unsafe { ffi::msg(pads.framework.class, sel(c"controllers")) };
assert!(!array.is_null(), "+controllers returned nil");
let ns_array = ffi::class(c"NSArray").expect("Foundation is loaded");
assert!(
unsafe { ffi::is_kind_of(array, ns_array) },
"not an NSArray"
);
drop(pool);
let mut pads = pads;
let mut connected = Vec::new();
pads.poll(|event| match event {
GamepadEvent::Connected { id, .. } => connected.push(id),
GamepadEvent::State { id, snapshot } => {
assert!(connected.contains(&id), "a state before its connection");
assert!(snapshot.is_finite());
}
GamepadEvent::Disconnected { .. } => panic!("nothing was connected yet"),
});
}
#[test]
fn every_path_names_real_getters() {
let class = |name| ffi::class(name).unwrap_or_else(|| panic!("no class {name:?}"));
let profile = class(c"GCExtendedGamepad");
let pad = class(c"GCControllerDirectionPad");
let button = class(c"GCControllerButtonInput");
let axis = class(c"GCControllerAxisInput");
let has = |class, getter: &core::ffi::CStr| unsafe {
ffi::instances_respond_to(class, sel(getter))
};
let paths = BUTTONS
.iter()
.map(|&(path, _)| (path, button, c"isPressed"))
.chain(AXES.iter().map(|&(path, _)| {
let last = if path.len() == 2 { axis } else { button };
(path, last, c"value")
}));
for (path, last, reader) in paths {
assert!(has(profile, path[0]), "GCExtendedGamepad.{:?}", path[0]);
if let Some(&inner) = path.get(1) {
assert!(has(pad, inner), "GCControllerDirectionPad.{inner:?}");
}
assert!(has(last, reader), "{path:?} then {reader:?}");
}
}
#[test]
fn the_framework_categories_name_their_families() {
let _pool = Pool::push();
let categories = unsafe {
[
(ffi::GCProductCategoryDualShock4, PadKind::PlayStation),
(ffi::GCProductCategoryDualSense, PadKind::PlayStation),
(ffi::GCProductCategoryXboxOne, PadKind::Xbox),
(ffi::GCProductCategoryMFi, PadKind::Generic),
]
};
for (constant, kind) in categories {
let name = unsafe { ffi::string(constant) }.expect("a category name");
assert_eq!(kind_of_name(&name), kind, "{name:?}");
assert_eq!(kind_of(Some(&name), None), kind, "{name:?}");
}
}
}