use core::ffi::{CStr, c_void};
use core::fmt;
use core::marker::PhantomData;
use core::mem::MaybeUninit;
use crate::{InputApiVersion, InputGameVersion, ScopedLogger, SdkError, SdkResult, sys};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum InputDeviceType {
Generic = sys::SCS_INPUT_DEVICE_TYPE_GENERIC,
Semantical = sys::SCS_INPUT_DEVICE_TYPE_SEMANTICAL,
}
impl InputDeviceType {
#[must_use]
pub const fn raw(self) -> sys::ScsInputDeviceType {
self as sys::ScsInputDeviceType
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputValueType {
Bool,
Float,
}
impl InputValueType {
#[must_use]
pub const fn raw(self) -> sys::ScsValueType {
match self {
Self::Bool => sys::SCS_VALUE_TYPE_BOOL,
Self::Float => sys::SCS_VALUE_TYPE_FLOAT,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InputAxisValue(f32);
impl InputAxisValue {
pub const MIN: Self = Self(-1.0);
pub const CENTER: Self = Self(0.0);
pub const MAX: Self = Self(1.0);
pub fn new(value: f32) -> Result<Self, InputAxisValueError> {
if !value.is_finite() {
return Err(InputAxisValueError::NotFinite);
}
if !(-1.0..=1.0).contains(&value) {
return Err(InputAxisValueError::OutOfRange);
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> f32 {
self.0
}
}
impl TryFrom<f32> for InputAxisValue {
type Error = InputAxisValueError;
fn try_from(value: f32) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<InputAxisValue> for f32 {
fn from(value: InputAxisValue) -> Self {
value.get()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputAxisValueError {
NotFinite,
OutOfRange,
}
impl fmt::Display for InputAxisValueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFinite => formatter.write_str("input axis value is not finite"),
Self::OutOfRange => {
formatter.write_str("input axis value is outside the inclusive -1.0 to 1.0 range")
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InputIndex(u32);
impl InputIndex {
pub const MAX_COUNT: u32 = sys::SCS_INPUT_MAX_INPUT_COUNT;
#[must_use]
pub const fn new(raw: u32) -> Option<Self> {
if raw < Self::MAX_COUNT {
Some(Self(raw))
} else {
None
}
}
#[must_use]
pub const fn raw(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputEventFlags(u32);
impl InputEventFlags {
#[must_use]
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> u32 {
self.0
}
#[must_use]
pub const fn first_in_frame(self) -> bool {
self.0 & sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME != 0
}
#[must_use]
pub const fn first_after_activation(self) -> bool {
self.0 & sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION != 0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum InputValue {
Bool(bool),
Float(InputAxisValue),
}
impl InputValue {
#[must_use]
pub const fn value_type(self) -> InputValueType {
match self {
Self::Bool(_) => InputValueType::Bool,
Self::Float(_) => InputValueType::Float,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InputEvent {
index: InputIndex,
value: InputValue,
}
impl InputEvent {
#[must_use]
pub const fn new(index: InputIndex, value: InputValue) -> Self {
Self { index, value }
}
#[must_use]
pub const fn index(self) -> InputIndex {
self.index
}
#[must_use]
pub const fn value(self) -> InputValue {
self.value
}
pub unsafe fn write_to(
self,
output: *mut sys::ScsInputEvent,
expected_type: InputValueType,
) -> SdkResult {
if output.is_null() || self.value.value_type() != expected_type {
return Err(SdkError::InvalidParameter);
}
let input_index = unsafe { core::ptr::addr_of_mut!((*output).input_index) };
unsafe { input_index.write(self.index.raw()) };
let output_value = unsafe { core::ptr::addr_of_mut!((*output).value) };
match self.value {
InputValue::Bool(value) => {
let output_bool = output_value.cast::<sys::ScsValueBool>();
unsafe {
output_bool.write(sys::ScsValueBool {
value: u8::from(value),
});
};
}
InputValue::Float(value) => {
let output_float = output_value.cast::<sys::ScsValueFloat>();
unsafe { output_float.write(sys::ScsValueFloat { value: value.get() }) };
}
}
Ok(())
}
}
#[repr(transparent)]
pub struct InputDeviceInput<'a> {
raw: sys::ScsInputDeviceInput,
lifetime: PhantomData<(&'a CStr, &'a CStr)>,
}
impl<'a> InputDeviceInput<'a> {
#[must_use]
pub const fn new(name: &'a CStr, display_name: &'a CStr, value_type: InputValueType) -> Self {
Self {
raw: sys::ScsInputDeviceInput {
name: name.as_ptr(),
display_name: display_name.as_ptr(),
value_type: value_type.raw(),
padding: MaybeUninit::uninit(),
},
lifetime: PhantomData,
}
}
#[must_use]
pub const fn value_type(&self) -> Option<InputValueType> {
match self.raw.value_type {
sys::SCS_VALUE_TYPE_BOOL => Some(InputValueType::Bool),
sys::SCS_VALUE_TYPE_FLOAT => Some(InputValueType::Float),
_ => None,
}
}
}
pub struct InputDeviceRegistration<'a> {
raw: sys::ScsInputDevice,
lifetime: PhantomData<&'a [InputDeviceInput<'a>]>,
}
impl<'a> InputDeviceRegistration<'a> {
pub unsafe fn new(
name: &'a CStr,
display_name: &'a CStr,
device_type: InputDeviceType,
inputs: &'a [InputDeviceInput<'a>],
callback_context: *mut c_void,
active_callback: Option<sys::ScsInputActiveCallback>,
event_callback: sys::ScsInputEventCallback,
) -> SdkResult<Self> {
if inputs.is_empty() || inputs.len() > sys::SCS_INPUT_MAX_INPUT_COUNT as usize {
return Err(SdkError::InvalidParameter);
}
let input_count = u32::try_from(inputs.len()).map_err(|_| SdkError::InvalidParameter)?;
Ok(Self {
raw: sys::ScsInputDevice {
name: name.as_ptr(),
display_name: display_name.as_ptr(),
type_: device_type.raw(),
input_count,
inputs: inputs.as_ptr().cast::<sys::ScsInputDeviceInput>(),
callback_context,
input_active_callback: active_callback,
input_event_callback: event_callback,
},
lifetime: PhantomData,
})
}
}
#[derive(Clone, Copy)]
struct InputSessionTable {
version: InputApiVersion,
logger: sys::ScsLog,
}
pub struct InputApi<'a> {
raw: &'a sys::ScsInputInitParamsV100,
version: InputApiVersion,
not_send_sync: PhantomData<*mut ()>,
}
#[derive(Clone, Copy)]
pub struct InputSession {
table: InputSessionTable,
}
pub struct InputCall<'scope> {
table: InputSessionTable,
scope: PhantomData<&'scope mut ()>,
not_send_sync: PhantomData<*mut ()>,
}
pub struct InputInitCall<'scope> {
call: InputCall<'scope>,
register_device: sys::ScsInputRegisterDevice,
}
impl<'a> InputApi<'a> {
pub const SUPPORTED_VERSIONS: &'static [InputApiVersion] = &[InputApiVersion::V1_00];
#[must_use]
pub const fn supports_version(version: InputApiVersion) -> bool {
version.raw() == InputApiVersion::V1_00.raw()
}
pub unsafe fn from_raw(
version: InputApiVersion,
params: *const sys::ScsInputInitParams,
) -> SdkResult<Self> {
if !Self::supports_version(version) {
return Err(SdkError::Unsupported);
}
let raw = unsafe { params.cast::<sys::ScsInputInitParamsV100>().as_ref() }
.ok_or(SdkError::InvalidParameter)?;
Ok(Self {
raw,
version,
not_send_sync: PhantomData,
})
}
#[must_use]
pub const fn version(&self) -> InputApiVersion {
self.version
}
#[must_use]
pub fn game_name(&self) -> &'a CStr {
unsafe { CStr::from_ptr(self.raw.common.game_name) }
}
#[must_use]
pub fn game_id(&self) -> &'a CStr {
unsafe { CStr::from_ptr(self.raw.common.game_id) }
}
#[must_use]
pub const fn game_version(&self) -> InputGameVersion {
InputGameVersion::from_raw(self.raw.common.game_version)
}
#[must_use]
pub const fn session(&self) -> InputSession {
InputSession {
table: InputSessionTable {
version: self.version,
logger: self.raw.common.log,
},
}
}
pub fn with_init_call<R>(
&self,
operation: impl for<'scope> FnOnce(&InputInitCall<'scope>) -> R,
) -> R {
let call = InputInitCall {
call: InputCall {
table: self.session().table,
scope: PhantomData,
not_send_sync: PhantomData,
},
register_device: self.raw.register_device,
};
operation(&call)
}
}
impl InputSession {
pub unsafe fn with_call<R>(
self,
operation: impl for<'scope> FnOnce(&InputCall<'scope>) -> R,
) -> R {
let call = InputCall {
table: self.table,
scope: PhantomData,
not_send_sync: PhantomData,
};
operation(&call)
}
}
impl InputCall<'_> {
#[must_use]
pub const fn input_api_version(&self) -> InputApiVersion {
self.table.version
}
#[must_use]
pub const fn logger(&self) -> ScopedLogger<'_> {
ScopedLogger::from_raw(self.table.logger)
}
}
impl InputInitCall<'_> {
#[must_use]
pub const fn input_api_version(&self) -> InputApiVersion {
self.call.input_api_version()
}
#[must_use]
pub const fn logger(&self) -> ScopedLogger<'_> {
self.call.logger()
}
pub fn register_device(&self, device: &InputDeviceRegistration<'_>) -> SdkResult {
let result = unsafe { (self.register_device)(&raw const device.raw) };
SdkError::from_code(result)
}
}
pub mod game {
use crate::{InputGameVersion, sys};
pub mod ets2 {
use super::{InputGameVersion, sys};
pub const V1_00: InputGameVersion =
InputGameVersion::from_raw(sys::SCS_INPUT_EUT2_GAME_VERSION_1_00);
pub const CURRENT: InputGameVersion = V1_00;
}
pub mod ats {
use super::{InputGameVersion, sys};
pub const V1_00: InputGameVersion =
InputGameVersion::from_raw(sys::SCS_INPUT_ATS_GAME_VERSION_1_00);
pub const CURRENT: InputGameVersion = V1_00;
}
}
#[cfg(test)]
mod tests {
extern crate std;
use core::ffi::c_void;
use core::sync::atomic::{AtomicUsize, Ordering};
use std::vec::Vec;
use super::*;
static REGISTRATIONS: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn fake_log(_level: sys::ScsLogType, _message: sys::ScsString) {}
unsafe extern "system" fn fake_event(
_event: *mut sys::ScsInputEvent,
_flags: u32,
_context: *mut c_void,
) -> sys::ScsResult {
sys::SCS_RESULT_NOT_FOUND
}
unsafe extern "system" fn fake_register(_device: *const sys::ScsInputDevice) -> sys::ScsResult {
REGISTRATIONS.fetch_add(1, Ordering::Relaxed);
sys::SCS_RESULT_OK
}
fn raw_api() -> sys::ScsInputInitParamsV100 {
sys::ScsInputInitParamsV100 {
common: sys::ScsSdkInitParamsV100 {
game_name: c"Game".as_ptr(),
game_id: c"eut2".as_ptr(),
game_version: sys::SCS_INPUT_EUT2_GAME_VERSION_1_00,
padding: MaybeUninit::uninit(),
log: fake_log,
},
register_device: fake_register,
}
}
#[test]
fn input_api_accepts_only_the_audited_v100_layout() {
let raw = raw_api();
let unsupported =
unsafe { InputApi::from_raw(InputApiVersion::new(1, 1), (&raw const raw).cast()) };
assert_eq!(unsupported.err(), Some(SdkError::Unsupported));
let api = unsafe { InputApi::from_raw(InputApiVersion::V1_00, (&raw const raw).cast()) }
.expect("v1.00 should be supported");
assert_eq!(api.game_version(), game::ets2::V1_00);
}
#[test]
fn registration_and_event_writing_preserve_types() {
REGISTRATIONS.store(0, Ordering::Relaxed);
let raw = raw_api();
let api = unsafe { InputApi::from_raw(InputApiVersion::V1_00, (&raw const raw).cast()) }
.expect("v1.00 should be supported");
let inputs = [InputDeviceInput::new(
c"button",
c"Button",
InputValueType::Bool,
)];
let device = unsafe {
InputDeviceRegistration::new(
c"device",
c"Device",
InputDeviceType::Generic,
&inputs,
core::ptr::null_mut(),
None,
fake_event,
)
}
.expect("device should be valid");
api.with_init_call(|call| call.register_device(&device))
.expect("registration should succeed");
assert_eq!(REGISTRATIONS.load(Ordering::Relaxed), 1);
let mut output = MaybeUninit::<sys::ScsInputEvent>::zeroed();
let event = InputEvent::new(
InputIndex::new(0).expect("zero is valid"),
InputValue::Bool(true),
);
unsafe { event.write_to(output.as_mut_ptr(), InputValueType::Bool) }
.expect("matching value type should write");
let output = unsafe { output.assume_init() };
assert_eq!(output.input_index, 0);
let value = unsafe { output.value.value_bool.value };
assert_eq!(value, 1);
}
#[test]
fn event_writing_rejects_null_and_value_type_mismatch() {
let index = InputIndex::new(0).expect("zero is valid");
let bool_event = InputEvent::new(index, InputValue::Bool(true));
let mut output = MaybeUninit::<sys::ScsInputEvent>::uninit();
let null_result =
unsafe { bool_event.write_to(core::ptr::null_mut(), InputValueType::Bool) };
assert_eq!(null_result, Err(SdkError::InvalidParameter));
let mismatch = unsafe { bool_event.write_to(output.as_mut_ptr(), InputValueType::Float) };
assert_eq!(mismatch, Err(SdkError::InvalidParameter));
}
#[test]
fn event_writing_initializes_only_the_active_union_storage() {
const SENTINEL: u8 = 0xA5;
let mut float_output = MaybeUninit::<sys::ScsInputEvent>::uninit();
unsafe {
core::ptr::write_bytes(
float_output.as_mut_ptr().cast::<u8>(),
SENTINEL,
core::mem::size_of::<sys::ScsInputEvent>(),
);
}
let event = InputEvent::new(
InputIndex::new(3).expect("three is valid"),
InputValue::Float(InputAxisValue::new(-0.625).expect("value is normalized")),
);
unsafe { event.write_to(float_output.as_mut_ptr(), InputValueType::Float) }
.expect("matching float value should write");
let float_bytes = unsafe {
core::slice::from_raw_parts(
float_output.as_ptr().cast::<u8>(),
core::mem::size_of::<sys::ScsInputEvent>(),
)
};
assert_eq!(&float_bytes[..4], &3_u32.to_ne_bytes());
assert_eq!(&float_bytes[4..8], &(-0.625_f32).to_ne_bytes());
assert!(float_bytes[8..].iter().all(|byte| *byte == SENTINEL));
let mut bool_output = MaybeUninit::<sys::ScsInputEvent>::uninit();
unsafe {
core::ptr::write_bytes(
bool_output.as_mut_ptr().cast::<u8>(),
SENTINEL,
core::mem::size_of::<sys::ScsInputEvent>(),
);
}
let event = InputEvent::new(
InputIndex::new(7).expect("seven is valid"),
InputValue::Bool(true),
);
unsafe { event.write_to(bool_output.as_mut_ptr(), InputValueType::Bool) }
.expect("matching bool value should write");
let bool_bytes = unsafe {
core::slice::from_raw_parts(
bool_output.as_ptr().cast::<u8>(),
core::mem::size_of::<sys::ScsInputEvent>(),
)
};
assert_eq!(&bool_bytes[..4], &7_u32.to_ne_bytes());
assert_eq!(bool_bytes[4], 1);
assert!(bool_bytes[5..].iter().all(|byte| *byte == SENTINEL));
}
#[test]
fn input_axis_value_accepts_only_the_finite_normalized_domain() {
for value in [-1.0, -0.625, -0.0, 0.0, 0.625, 1.0] {
let normalized = InputAxisValue::new(value).expect("value should be normalized");
assert_eq!(normalized.get().to_bits(), value.to_bits());
assert_eq!(f32::from(normalized).to_bits(), value.to_bits());
assert_eq!(InputAxisValue::try_from(value), Ok(normalized));
}
for value in [-1.000_000_1, -2.0, 1.000_000_1, 2.0] {
assert_eq!(
InputAxisValue::new(value),
Err(InputAxisValueError::OutOfRange)
);
}
for value in [f32::NAN, f32::NEG_INFINITY, f32::INFINITY] {
assert_eq!(
InputAxisValue::new(value),
Err(InputAxisValueError::NotFinite)
);
}
assert_eq!(InputAxisValue::MIN.get().to_bits(), (-1.0_f32).to_bits());
assert_eq!(InputAxisValue::CENTER.get().to_bits(), 0.0_f32.to_bits());
assert_eq!(InputAxisValue::MAX.get().to_bits(), 1.0_f32.to_bits());
}
#[test]
fn device_registration_rejects_empty_and_too_many_inputs() {
let empty = unsafe {
InputDeviceRegistration::new(
c"device",
c"Device",
InputDeviceType::Generic,
&[],
core::ptr::null_mut(),
None,
fake_event,
)
};
assert_eq!(empty.err(), Some(SdkError::InvalidParameter));
let inputs: Vec<_> = (0..=InputIndex::MAX_COUNT)
.map(|_| InputDeviceInput::new(c"button", c"Button", InputValueType::Bool))
.collect();
let too_many = unsafe {
InputDeviceRegistration::new(
c"device",
c"Device",
InputDeviceType::Generic,
&inputs,
core::ptr::null_mut(),
None,
fake_event,
)
};
assert_eq!(too_many.err(), Some(SdkError::InvalidParameter));
}
#[test]
fn input_event_flags_decode_known_bits_and_preserve_unknown_bits() {
let flags = InputEventFlags::from_raw(
sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME
| sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION
| 0x8000_0000,
);
assert!(flags.first_in_frame());
assert!(flags.first_after_activation());
assert_eq!(
flags.raw(),
sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME
| sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION
| 0x8000_0000
);
}
#[test]
fn input_device_value_type_does_not_treat_unknown_as_float() {
let mut input = InputDeviceInput::new(c"axis", c"Axis", InputValueType::Float);
assert_eq!(input.value_type(), Some(InputValueType::Float));
input.raw.value_type = sys::SCS_VALUE_TYPE_INVALID;
assert_eq!(input.value_type(), None);
}
}