1use core::ffi::{CStr, c_void};
10use core::fmt;
11use core::marker::PhantomData;
12use core::mem::MaybeUninit;
13
14use crate::{InputApiVersion, InputGameVersion, ScopedLogger, SdkError, SdkResult, sys};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18#[repr(u32)]
19pub enum InputDeviceType {
20 Generic = sys::SCS_INPUT_DEVICE_TYPE_GENERIC,
22 Semantical = sys::SCS_INPUT_DEVICE_TYPE_SEMANTICAL,
24}
25
26impl InputDeviceType {
27 #[must_use]
28 pub const fn raw(self) -> sys::ScsInputDeviceType {
29 self as sys::ScsInputDeviceType
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum InputValueType {
36 Bool,
37 Float,
39}
40
41impl InputValueType {
42 #[must_use]
43 pub const fn raw(self) -> sys::ScsValueType {
44 match self {
45 Self::Bool => sys::SCS_VALUE_TYPE_BOOL,
46 Self::Float => sys::SCS_VALUE_TYPE_FLOAT,
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
65pub struct InputAxisValue(f32);
66
67impl InputAxisValue {
68 pub const MIN: Self = Self(-1.0);
70 pub const CENTER: Self = Self(0.0);
72 pub const MAX: Self = Self(1.0);
74
75 pub fn new(value: f32) -> Result<Self, InputAxisValueError> {
85 if !value.is_finite() {
86 return Err(InputAxisValueError::NotFinite);
87 }
88 if !(-1.0..=1.0).contains(&value) {
89 return Err(InputAxisValueError::OutOfRange);
90 }
91 Ok(Self(value))
92 }
93
94 #[must_use]
96 pub const fn get(self) -> f32 {
97 self.0
98 }
99}
100
101impl TryFrom<f32> for InputAxisValue {
102 type Error = InputAxisValueError;
103
104 fn try_from(value: f32) -> Result<Self, Self::Error> {
105 Self::new(value)
106 }
107}
108
109impl From<InputAxisValue> for f32 {
110 fn from(value: InputAxisValue) -> Self {
111 value.get()
112 }
113}
114
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub enum InputAxisValueError {
118 NotFinite,
120 OutOfRange,
122}
123
124impl fmt::Display for InputAxisValueError {
125 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self {
127 Self::NotFinite => formatter.write_str("input axis value is not finite"),
128 Self::OutOfRange => {
129 formatter.write_str("input axis value is outside the inclusive -1.0 to 1.0 range")
130 }
131 }
132 }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
137pub struct InputIndex(u32);
138
139impl InputIndex {
140 pub const MAX_COUNT: u32 = sys::SCS_INPUT_MAX_INPUT_COUNT;
141
142 #[must_use]
143 pub const fn new(raw: u32) -> Option<Self> {
144 if raw < Self::MAX_COUNT {
145 Some(Self(raw))
146 } else {
147 None
148 }
149 }
150
151 #[must_use]
152 pub const fn raw(self) -> u32 {
153 self.0
154 }
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub struct InputEventFlags(u32);
160
161impl InputEventFlags {
162 #[must_use]
163 pub const fn from_raw(raw: u32) -> Self {
164 Self(raw)
165 }
166
167 #[must_use]
168 pub const fn raw(self) -> u32 {
169 self.0
170 }
171
172 #[must_use]
173 pub const fn first_in_frame(self) -> bool {
174 self.0 & sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME != 0
175 }
176
177 #[must_use]
178 pub const fn first_after_activation(self) -> bool {
179 self.0 & sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION != 0
180 }
181}
182
183#[derive(Clone, Copy, Debug, PartialEq)]
194pub enum InputValue {
195 Bool(bool),
196 Float(InputAxisValue),
199}
200
201impl InputValue {
202 #[must_use]
203 pub const fn value_type(self) -> InputValueType {
204 match self {
205 Self::Bool(_) => InputValueType::Bool,
206 Self::Float(_) => InputValueType::Float,
207 }
208 }
209}
210
211#[derive(Clone, Copy, Debug, PartialEq)]
213pub struct InputEvent {
214 index: InputIndex,
215 value: InputValue,
216}
217
218impl InputEvent {
219 #[must_use]
220 pub const fn new(index: InputIndex, value: InputValue) -> Self {
221 Self { index, value }
222 }
223
224 #[must_use]
225 pub const fn index(self) -> InputIndex {
226 self.index
227 }
228
229 #[must_use]
230 pub const fn value(self) -> InputValue {
231 self.value
232 }
233
234 pub unsafe fn write_to(
255 self,
256 output: *mut sys::ScsInputEvent,
257 expected_type: InputValueType,
258 ) -> SdkResult {
259 if output.is_null() || self.value.value_type() != expected_type {
260 return Err(SdkError::InvalidParameter);
261 }
262
263 let input_index = unsafe { core::ptr::addr_of_mut!((*output).input_index) };
267 unsafe { input_index.write(self.index.raw()) };
270
271 let output_value = unsafe { core::ptr::addr_of_mut!((*output).value) };
274 match self.value {
275 InputValue::Bool(value) => {
276 let output_bool = output_value.cast::<sys::ScsValueBool>();
277 unsafe {
280 output_bool.write(sys::ScsValueBool {
281 value: u8::from(value),
282 });
283 };
284 }
285 InputValue::Float(value) => {
286 let output_float = output_value.cast::<sys::ScsValueFloat>();
287 unsafe { output_float.write(sys::ScsValueFloat { value: value.get() }) };
290 }
291 }
292 Ok(())
293 }
294}
295
296#[repr(transparent)]
298pub struct InputDeviceInput<'a> {
299 raw: sys::ScsInputDeviceInput,
300 lifetime: PhantomData<(&'a CStr, &'a CStr)>,
301}
302
303impl<'a> InputDeviceInput<'a> {
304 #[must_use]
305 pub const fn new(name: &'a CStr, display_name: &'a CStr, value_type: InputValueType) -> Self {
306 Self {
307 raw: sys::ScsInputDeviceInput {
308 name: name.as_ptr(),
309 display_name: display_name.as_ptr(),
310 value_type: value_type.raw(),
311 padding: MaybeUninit::uninit(),
312 },
313 lifetime: PhantomData,
314 }
315 }
316
317 #[must_use]
318 pub const fn value_type(&self) -> Option<InputValueType> {
319 match self.raw.value_type {
320 sys::SCS_VALUE_TYPE_BOOL => Some(InputValueType::Bool),
321 sys::SCS_VALUE_TYPE_FLOAT => Some(InputValueType::Float),
322 _ => None,
323 }
324 }
325}
326
327pub struct InputDeviceRegistration<'a> {
330 raw: sys::ScsInputDevice,
331 lifetime: PhantomData<&'a [InputDeviceInput<'a>]>,
332}
333
334impl<'a> InputDeviceRegistration<'a> {
335 pub unsafe fn new(
350 name: &'a CStr,
351 display_name: &'a CStr,
352 device_type: InputDeviceType,
353 inputs: &'a [InputDeviceInput<'a>],
354 callback_context: *mut c_void,
355 active_callback: Option<sys::ScsInputActiveCallback>,
356 event_callback: sys::ScsInputEventCallback,
357 ) -> SdkResult<Self> {
358 if inputs.is_empty() || inputs.len() > sys::SCS_INPUT_MAX_INPUT_COUNT as usize {
359 return Err(SdkError::InvalidParameter);
360 }
361 let input_count = u32::try_from(inputs.len()).map_err(|_| SdkError::InvalidParameter)?;
362 Ok(Self {
363 raw: sys::ScsInputDevice {
364 name: name.as_ptr(),
365 display_name: display_name.as_ptr(),
366 type_: device_type.raw(),
367 input_count,
368 inputs: inputs.as_ptr().cast::<sys::ScsInputDeviceInput>(),
369 callback_context,
370 input_active_callback: active_callback,
371 input_event_callback: event_callback,
372 },
373 lifetime: PhantomData,
374 })
375 }
376}
377
378#[derive(Clone, Copy)]
379struct InputSessionTable {
380 version: InputApiVersion,
381 logger: sys::ScsLog,
382}
383
384pub struct InputApi<'a> {
386 raw: &'a sys::ScsInputInitParamsV100,
387 version: InputApiVersion,
388 not_send_sync: PhantomData<*mut ()>,
389}
390
391#[derive(Clone, Copy)]
393pub struct InputSession {
394 table: InputSessionTable,
395}
396
397pub struct InputCall<'scope> {
412 table: InputSessionTable,
413 scope: PhantomData<&'scope mut ()>,
414 not_send_sync: PhantomData<*mut ()>,
415}
416
417pub struct InputInitCall<'scope> {
419 call: InputCall<'scope>,
420 register_device: sys::ScsInputRegisterDevice,
421}
422
423impl<'a> InputApi<'a> {
424 pub const SUPPORTED_VERSIONS: &'static [InputApiVersion] = &[InputApiVersion::V1_00];
425
426 #[must_use]
427 pub const fn supports_version(version: InputApiVersion) -> bool {
428 version.raw() == InputApiVersion::V1_00.raw()
429 }
430
431 pub unsafe fn from_raw(
444 version: InputApiVersion,
445 params: *const sys::ScsInputInitParams,
446 ) -> SdkResult<Self> {
447 if !Self::supports_version(version) {
448 return Err(SdkError::Unsupported);
449 }
450 let raw = unsafe { params.cast::<sys::ScsInputInitParamsV100>().as_ref() }
453 .ok_or(SdkError::InvalidParameter)?;
454 Ok(Self {
455 raw,
456 version,
457 not_send_sync: PhantomData,
458 })
459 }
460
461 #[must_use]
462 pub const fn version(&self) -> InputApiVersion {
463 self.version
464 }
465
466 #[must_use]
467 pub fn game_name(&self) -> &'a CStr {
468 unsafe { CStr::from_ptr(self.raw.common.game_name) }
471 }
472
473 #[must_use]
474 pub fn game_id(&self) -> &'a CStr {
475 unsafe { CStr::from_ptr(self.raw.common.game_id) }
478 }
479
480 #[must_use]
481 pub const fn game_version(&self) -> InputGameVersion {
482 InputGameVersion::from_raw(self.raw.common.game_version)
483 }
484
485 #[must_use]
486 pub const fn session(&self) -> InputSession {
487 InputSession {
488 table: InputSessionTable {
489 version: self.version,
490 logger: self.raw.common.log,
491 },
492 }
493 }
494
495 pub fn with_init_call<R>(
496 &self,
497 operation: impl for<'scope> FnOnce(&InputInitCall<'scope>) -> R,
498 ) -> R {
499 let call = InputInitCall {
500 call: InputCall {
501 table: self.session().table,
502 scope: PhantomData,
503 not_send_sync: PhantomData,
504 },
505 register_device: self.raw.register_device,
506 };
507 operation(&call)
508 }
509}
510
511impl InputSession {
512 pub unsafe fn with_call<R>(
530 self,
531 operation: impl for<'scope> FnOnce(&InputCall<'scope>) -> R,
532 ) -> R {
533 let call = InputCall {
534 table: self.table,
535 scope: PhantomData,
536 not_send_sync: PhantomData,
537 };
538 operation(&call)
539 }
540}
541
542impl InputCall<'_> {
543 #[must_use]
544 pub const fn input_api_version(&self) -> InputApiVersion {
545 self.table.version
546 }
547
548 #[must_use]
549 pub const fn logger(&self) -> ScopedLogger<'_> {
550 ScopedLogger::from_raw(self.table.logger)
551 }
552}
553
554impl InputInitCall<'_> {
555 #[must_use]
556 pub const fn input_api_version(&self) -> InputApiVersion {
557 self.call.input_api_version()
558 }
559
560 #[must_use]
561 pub const fn logger(&self) -> ScopedLogger<'_> {
562 self.call.logger()
563 }
564
565 pub fn register_device(&self, device: &InputDeviceRegistration<'_>) -> SdkResult {
572 let result = unsafe { (self.register_device)(&raw const device.raw) };
577 SdkError::from_code(result)
578 }
579}
580
581pub mod game {
583 use crate::{InputGameVersion, sys};
584
585 pub mod ets2 {
586 use super::{InputGameVersion, sys};
587
588 pub const V1_00: InputGameVersion =
589 InputGameVersion::from_raw(sys::SCS_INPUT_EUT2_GAME_VERSION_1_00);
590 pub const CURRENT: InputGameVersion = V1_00;
591 }
592
593 pub mod ats {
594 use super::{InputGameVersion, sys};
595
596 pub const V1_00: InputGameVersion =
597 InputGameVersion::from_raw(sys::SCS_INPUT_ATS_GAME_VERSION_1_00);
598 pub const CURRENT: InputGameVersion = V1_00;
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 extern crate std;
605
606 use core::ffi::c_void;
607 use core::sync::atomic::{AtomicUsize, Ordering};
608 use std::vec::Vec;
609
610 use super::*;
611
612 static REGISTRATIONS: AtomicUsize = AtomicUsize::new(0);
613
614 unsafe extern "system" fn fake_log(_level: sys::ScsLogType, _message: sys::ScsString) {}
615
616 unsafe extern "system" fn fake_event(
617 _event: *mut sys::ScsInputEvent,
618 _flags: u32,
619 _context: *mut c_void,
620 ) -> sys::ScsResult {
621 sys::SCS_RESULT_NOT_FOUND
622 }
623
624 unsafe extern "system" fn fake_register(_device: *const sys::ScsInputDevice) -> sys::ScsResult {
625 REGISTRATIONS.fetch_add(1, Ordering::Relaxed);
626 sys::SCS_RESULT_OK
627 }
628
629 fn raw_api() -> sys::ScsInputInitParamsV100 {
630 sys::ScsInputInitParamsV100 {
631 common: sys::ScsSdkInitParamsV100 {
632 game_name: c"Game".as_ptr(),
633 game_id: c"eut2".as_ptr(),
634 game_version: sys::SCS_INPUT_EUT2_GAME_VERSION_1_00,
635 padding: MaybeUninit::uninit(),
636 log: fake_log,
637 },
638 register_device: fake_register,
639 }
640 }
641
642 #[test]
643 fn input_api_accepts_only_the_audited_v100_layout() {
644 let raw = raw_api();
645 let unsupported =
649 unsafe { InputApi::from_raw(InputApiVersion::new(1, 1), (&raw const raw).cast()) };
650 assert_eq!(unsupported.err(), Some(SdkError::Unsupported));
651
652 let api = unsafe { InputApi::from_raw(InputApiVersion::V1_00, (&raw const raw).cast()) }
655 .expect("v1.00 should be supported");
656 assert_eq!(api.game_version(), game::ets2::V1_00);
657 }
658
659 #[test]
660 fn registration_and_event_writing_preserve_types() {
661 REGISTRATIONS.store(0, Ordering::Relaxed);
662 let raw = raw_api();
663 let api = unsafe { InputApi::from_raw(InputApiVersion::V1_00, (&raw const raw).cast()) }
666 .expect("v1.00 should be supported");
667 let inputs = [InputDeviceInput::new(
668 c"button",
669 c"Button",
670 InputValueType::Bool,
671 )];
672 let device = unsafe {
676 InputDeviceRegistration::new(
677 c"device",
678 c"Device",
679 InputDeviceType::Generic,
680 &inputs,
681 core::ptr::null_mut(),
682 None,
683 fake_event,
684 )
685 }
686 .expect("device should be valid");
687 api.with_init_call(|call| call.register_device(&device))
688 .expect("registration should succeed");
689 assert_eq!(REGISTRATIONS.load(Ordering::Relaxed), 1);
690
691 let mut output = MaybeUninit::<sys::ScsInputEvent>::zeroed();
692 let event = InputEvent::new(
693 InputIndex::new(0).expect("zero is valid"),
694 InputValue::Bool(true),
695 );
696 unsafe { event.write_to(output.as_mut_ptr(), InputValueType::Bool) }
699 .expect("matching value type should write");
700 let output = unsafe { output.assume_init() };
703 assert_eq!(output.input_index, 0);
704 let value = unsafe { output.value.value_bool.value };
706 assert_eq!(value, 1);
707 }
708
709 #[test]
710 fn event_writing_rejects_null_and_value_type_mismatch() {
711 let index = InputIndex::new(0).expect("zero is valid");
712 let bool_event = InputEvent::new(index, InputValue::Bool(true));
713 let mut output = MaybeUninit::<sys::ScsInputEvent>::uninit();
714
715 let null_result =
718 unsafe { bool_event.write_to(core::ptr::null_mut(), InputValueType::Bool) };
719 assert_eq!(null_result, Err(SdkError::InvalidParameter));
720
721 let mismatch = unsafe { bool_event.write_to(output.as_mut_ptr(), InputValueType::Float) };
724 assert_eq!(mismatch, Err(SdkError::InvalidParameter));
725 }
726
727 #[test]
728 fn event_writing_initializes_only_the_active_union_storage() {
729 const SENTINEL: u8 = 0xA5;
730
731 let mut float_output = MaybeUninit::<sys::ScsInputEvent>::uninit();
732 unsafe {
738 core::ptr::write_bytes(
739 float_output.as_mut_ptr().cast::<u8>(),
740 SENTINEL,
741 core::mem::size_of::<sys::ScsInputEvent>(),
742 );
743 }
744 let event = InputEvent::new(
745 InputIndex::new(3).expect("three is valid"),
746 InputValue::Float(InputAxisValue::new(-0.625).expect("value is normalized")),
747 );
748
749 unsafe { event.write_to(float_output.as_mut_ptr(), InputValueType::Float) }
753 .expect("matching float value should write");
754
755 let float_bytes = unsafe {
759 core::slice::from_raw_parts(
760 float_output.as_ptr().cast::<u8>(),
761 core::mem::size_of::<sys::ScsInputEvent>(),
762 )
763 };
764 assert_eq!(&float_bytes[..4], &3_u32.to_ne_bytes());
765 assert_eq!(&float_bytes[4..8], &(-0.625_f32).to_ne_bytes());
766 assert!(float_bytes[8..].iter().all(|byte| *byte == SENTINEL));
767
768 let mut bool_output = MaybeUninit::<sys::ScsInputEvent>::uninit();
769 unsafe {
772 core::ptr::write_bytes(
773 bool_output.as_mut_ptr().cast::<u8>(),
774 SENTINEL,
775 core::mem::size_of::<sys::ScsInputEvent>(),
776 );
777 }
778 let event = InputEvent::new(
779 InputIndex::new(7).expect("seven is valid"),
780 InputValue::Bool(true),
781 );
782
783 unsafe { event.write_to(bool_output.as_mut_ptr(), InputValueType::Bool) }
787 .expect("matching bool value should write");
788
789 let bool_bytes = unsafe {
792 core::slice::from_raw_parts(
793 bool_output.as_ptr().cast::<u8>(),
794 core::mem::size_of::<sys::ScsInputEvent>(),
795 )
796 };
797 assert_eq!(&bool_bytes[..4], &7_u32.to_ne_bytes());
798 assert_eq!(bool_bytes[4], 1);
799 assert!(bool_bytes[5..].iter().all(|byte| *byte == SENTINEL));
800 }
801
802 #[test]
803 fn input_axis_value_accepts_only_the_finite_normalized_domain() {
804 for value in [-1.0, -0.625, -0.0, 0.0, 0.625, 1.0] {
805 let normalized = InputAxisValue::new(value).expect("value should be normalized");
806 assert_eq!(normalized.get().to_bits(), value.to_bits());
807 assert_eq!(f32::from(normalized).to_bits(), value.to_bits());
808 assert_eq!(InputAxisValue::try_from(value), Ok(normalized));
809 }
810
811 for value in [-1.000_000_1, -2.0, 1.000_000_1, 2.0] {
812 assert_eq!(
813 InputAxisValue::new(value),
814 Err(InputAxisValueError::OutOfRange)
815 );
816 }
817
818 for value in [f32::NAN, f32::NEG_INFINITY, f32::INFINITY] {
819 assert_eq!(
820 InputAxisValue::new(value),
821 Err(InputAxisValueError::NotFinite)
822 );
823 }
824
825 assert_eq!(InputAxisValue::MIN.get().to_bits(), (-1.0_f32).to_bits());
826 assert_eq!(InputAxisValue::CENTER.get().to_bits(), 0.0_f32.to_bits());
827 assert_eq!(InputAxisValue::MAX.get().to_bits(), 1.0_f32.to_bits());
828 }
829
830 #[test]
831 fn device_registration_rejects_empty_and_too_many_inputs() {
832 let empty = unsafe {
836 InputDeviceRegistration::new(
837 c"device",
838 c"Device",
839 InputDeviceType::Generic,
840 &[],
841 core::ptr::null_mut(),
842 None,
843 fake_event,
844 )
845 };
846 assert_eq!(empty.err(), Some(SdkError::InvalidParameter));
847
848 let inputs: Vec<_> = (0..=InputIndex::MAX_COUNT)
849 .map(|_| InputDeviceInput::new(c"button", c"Button", InputValueType::Bool))
850 .collect();
851 let too_many = unsafe {
855 InputDeviceRegistration::new(
856 c"device",
857 c"Device",
858 InputDeviceType::Generic,
859 &inputs,
860 core::ptr::null_mut(),
861 None,
862 fake_event,
863 )
864 };
865 assert_eq!(too_many.err(), Some(SdkError::InvalidParameter));
866 }
867
868 #[test]
869 fn input_event_flags_decode_known_bits_and_preserve_unknown_bits() {
870 let flags = InputEventFlags::from_raw(
871 sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME
872 | sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION
873 | 0x8000_0000,
874 );
875
876 assert!(flags.first_in_frame());
877 assert!(flags.first_after_activation());
878 assert_eq!(
879 flags.raw(),
880 sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME
881 | sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION
882 | 0x8000_0000
883 );
884 }
885
886 #[test]
887 fn input_device_value_type_does_not_treat_unknown_as_float() {
888 let mut input = InputDeviceInput::new(c"axis", c"Axis", InputValueType::Float);
889 assert_eq!(input.value_type(), Some(InputValueType::Float));
890
891 input.raw.value_type = sys::SCS_VALUE_TYPE_INVALID;
892 assert_eq!(input.value_type(), None);
893 }
894}