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 unsafe {
271 core::ptr::addr_of_mut!((*output).input_index).write(self.index.raw());
272 match self.value {
273 InputValue::Bool(value) => {
274 core::ptr::addr_of_mut!((*output).value.value_bool).write(sys::ScsValueBool {
275 value: u8::from(value),
276 });
277 }
278 InputValue::Float(value) => {
279 core::ptr::addr_of_mut!((*output).value.value_float)
280 .write(sys::ScsValueFloat { value: value.get() });
281 }
282 }
283 }
284 Ok(())
285 }
286}
287
288#[repr(transparent)]
290pub struct InputDeviceInput<'a> {
291 raw: sys::ScsInputDeviceInput,
292 lifetime: PhantomData<(&'a CStr, &'a CStr)>,
293}
294
295impl<'a> InputDeviceInput<'a> {
296 #[must_use]
297 pub const fn new(name: &'a CStr, display_name: &'a CStr, value_type: InputValueType) -> Self {
298 Self {
299 raw: sys::ScsInputDeviceInput {
300 name: name.as_ptr(),
301 display_name: display_name.as_ptr(),
302 value_type: value_type.raw(),
303 padding: MaybeUninit::uninit(),
304 },
305 lifetime: PhantomData,
306 }
307 }
308
309 #[must_use]
310 pub const fn value_type(&self) -> Option<InputValueType> {
311 match self.raw.value_type {
312 sys::SCS_VALUE_TYPE_BOOL => Some(InputValueType::Bool),
313 sys::SCS_VALUE_TYPE_FLOAT => Some(InputValueType::Float),
314 _ => None,
315 }
316 }
317}
318
319pub struct InputDeviceRegistration<'a> {
322 raw: sys::ScsInputDevice,
323 lifetime: PhantomData<&'a [InputDeviceInput<'a>]>,
324}
325
326impl<'a> InputDeviceRegistration<'a> {
327 pub unsafe fn new(
342 name: &'a CStr,
343 display_name: &'a CStr,
344 device_type: InputDeviceType,
345 inputs: &'a [InputDeviceInput<'a>],
346 callback_context: *mut c_void,
347 active_callback: Option<sys::ScsInputActiveCallback>,
348 event_callback: sys::ScsInputEventCallback,
349 ) -> SdkResult<Self> {
350 if inputs.is_empty() || inputs.len() > sys::SCS_INPUT_MAX_INPUT_COUNT as usize {
351 return Err(SdkError::InvalidParameter);
352 }
353 let input_count = u32::try_from(inputs.len()).map_err(|_| SdkError::InvalidParameter)?;
354 Ok(Self {
355 raw: sys::ScsInputDevice {
356 name: name.as_ptr(),
357 display_name: display_name.as_ptr(),
358 type_: device_type.raw(),
359 input_count,
360 inputs: inputs.as_ptr().cast::<sys::ScsInputDeviceInput>(),
361 callback_context,
362 input_active_callback: active_callback,
363 input_event_callback: event_callback,
364 },
365 lifetime: PhantomData,
366 })
367 }
368}
369
370#[derive(Clone, Copy)]
371struct InputSessionTable {
372 version: InputApiVersion,
373 logger: sys::ScsLog,
374}
375
376pub struct InputApi<'a> {
378 raw: &'a sys::ScsInputInitParamsV100,
379 version: InputApiVersion,
380 not_send_sync: PhantomData<*mut ()>,
381}
382
383#[derive(Clone, Copy)]
385pub struct InputSession {
386 table: InputSessionTable,
387}
388
389pub struct InputCall<'scope> {
404 table: InputSessionTable,
405 scope: PhantomData<&'scope mut ()>,
406 not_send_sync: PhantomData<*mut ()>,
407}
408
409pub struct InputInitCall<'scope> {
411 call: InputCall<'scope>,
412 register_device: sys::ScsInputRegisterDevice,
413}
414
415impl<'a> InputApi<'a> {
416 pub const SUPPORTED_VERSIONS: &'static [InputApiVersion] = &[InputApiVersion::V1_00];
417
418 #[must_use]
419 pub const fn supports_version(version: InputApiVersion) -> bool {
420 version.raw() == InputApiVersion::V1_00.raw()
421 }
422
423 pub unsafe fn from_raw(
436 version: InputApiVersion,
437 params: *const sys::ScsInputInitParams,
438 ) -> SdkResult<Self> {
439 if !Self::supports_version(version) {
440 return Err(SdkError::Unsupported);
441 }
442 let raw = unsafe { params.cast::<sys::ScsInputInitParamsV100>().as_ref() }
445 .ok_or(SdkError::InvalidParameter)?;
446 Ok(Self {
447 raw,
448 version,
449 not_send_sync: PhantomData,
450 })
451 }
452
453 #[must_use]
454 pub const fn version(&self) -> InputApiVersion {
455 self.version
456 }
457
458 #[must_use]
459 pub fn game_name(&self) -> &'a CStr {
460 unsafe { CStr::from_ptr(self.raw.common.game_name) }
463 }
464
465 #[must_use]
466 pub fn game_id(&self) -> &'a CStr {
467 unsafe { CStr::from_ptr(self.raw.common.game_id) }
470 }
471
472 #[must_use]
473 pub const fn game_version(&self) -> InputGameVersion {
474 InputGameVersion::from_raw(self.raw.common.game_version)
475 }
476
477 #[must_use]
478 pub const fn session(&self) -> InputSession {
479 InputSession {
480 table: InputSessionTable {
481 version: self.version,
482 logger: self.raw.common.log,
483 },
484 }
485 }
486
487 pub fn with_init_call<R>(
488 &self,
489 operation: impl for<'scope> FnOnce(&InputInitCall<'scope>) -> R,
490 ) -> R {
491 let call = InputInitCall {
492 call: InputCall {
493 table: self.session().table,
494 scope: PhantomData,
495 not_send_sync: PhantomData,
496 },
497 register_device: self.raw.register_device,
498 };
499 operation(&call)
500 }
501}
502
503impl InputSession {
504 pub unsafe fn with_call<R>(
522 self,
523 operation: impl for<'scope> FnOnce(&InputCall<'scope>) -> R,
524 ) -> R {
525 let call = InputCall {
526 table: self.table,
527 scope: PhantomData,
528 not_send_sync: PhantomData,
529 };
530 operation(&call)
531 }
532}
533
534impl InputCall<'_> {
535 #[must_use]
536 pub const fn input_api_version(&self) -> InputApiVersion {
537 self.table.version
538 }
539
540 #[must_use]
541 pub const fn logger(&self) -> ScopedLogger<'_> {
542 ScopedLogger::from_raw(self.table.logger)
543 }
544}
545
546impl InputInitCall<'_> {
547 #[must_use]
548 pub const fn input_api_version(&self) -> InputApiVersion {
549 self.call.input_api_version()
550 }
551
552 #[must_use]
553 pub const fn logger(&self) -> ScopedLogger<'_> {
554 self.call.logger()
555 }
556
557 pub fn register_device(&self, device: &InputDeviceRegistration<'_>) -> SdkResult {
564 let result = unsafe { (self.register_device)(&raw const device.raw) };
569 SdkError::from_code(result)
570 }
571}
572
573pub mod game {
575 use crate::{InputGameVersion, sys};
576
577 pub mod ets2 {
578 use super::{InputGameVersion, sys};
579
580 pub const V1_00: InputGameVersion =
581 InputGameVersion::from_raw(sys::SCS_INPUT_EUT2_GAME_VERSION_1_00);
582 pub const CURRENT: InputGameVersion = V1_00;
583 }
584
585 pub mod ats {
586 use super::{InputGameVersion, sys};
587
588 pub const V1_00: InputGameVersion =
589 InputGameVersion::from_raw(sys::SCS_INPUT_ATS_GAME_VERSION_1_00);
590 pub const CURRENT: InputGameVersion = V1_00;
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 extern crate std;
597
598 use core::ffi::c_void;
599 use core::sync::atomic::{AtomicUsize, Ordering};
600 use std::vec::Vec;
601
602 use super::*;
603
604 static REGISTRATIONS: AtomicUsize = AtomicUsize::new(0);
605
606 unsafe extern "system" fn fake_log(_level: sys::ScsLogType, _message: sys::ScsString) {}
607
608 unsafe extern "system" fn fake_event(
609 _event: *mut sys::ScsInputEvent,
610 _flags: u32,
611 _context: *mut c_void,
612 ) -> sys::ScsResult {
613 sys::SCS_RESULT_NOT_FOUND
614 }
615
616 unsafe extern "system" fn fake_register(_device: *const sys::ScsInputDevice) -> sys::ScsResult {
617 REGISTRATIONS.fetch_add(1, Ordering::Relaxed);
618 sys::SCS_RESULT_OK
619 }
620
621 fn raw_api() -> sys::ScsInputInitParamsV100 {
622 sys::ScsInputInitParamsV100 {
623 common: sys::ScsSdkInitParamsV100 {
624 game_name: c"Game".as_ptr(),
625 game_id: c"eut2".as_ptr(),
626 game_version: sys::SCS_INPUT_EUT2_GAME_VERSION_1_00,
627 padding: MaybeUninit::uninit(),
628 log: fake_log,
629 },
630 register_device: fake_register,
631 }
632 }
633
634 #[test]
635 fn input_api_accepts_only_the_audited_v100_layout() {
636 let raw = raw_api();
637 let unsupported =
638 unsafe { InputApi::from_raw(InputApiVersion::new(1, 1), (&raw const raw).cast()) };
639 assert_eq!(unsupported.err(), Some(SdkError::Unsupported));
640
641 let api = unsafe { InputApi::from_raw(InputApiVersion::V1_00, (&raw const raw).cast()) }
642 .expect("v1.00 should be supported");
643 assert_eq!(api.game_version(), game::ets2::V1_00);
644 }
645
646 #[test]
647 fn registration_and_event_writing_preserve_types() {
648 REGISTRATIONS.store(0, Ordering::Relaxed);
649 let raw = raw_api();
650 let api = unsafe { InputApi::from_raw(InputApiVersion::V1_00, (&raw const raw).cast()) }
651 .expect("v1.00 should be supported");
652 let inputs = [InputDeviceInput::new(
653 c"button",
654 c"Button",
655 InputValueType::Bool,
656 )];
657 let device = unsafe {
658 InputDeviceRegistration::new(
659 c"device",
660 c"Device",
661 InputDeviceType::Generic,
662 &inputs,
663 core::ptr::null_mut(),
664 None,
665 fake_event,
666 )
667 }
668 .expect("device should be valid");
669 api.with_init_call(|call| call.register_device(&device))
670 .expect("registration should succeed");
671 assert_eq!(REGISTRATIONS.load(Ordering::Relaxed), 1);
672
673 let mut output = MaybeUninit::<sys::ScsInputEvent>::zeroed();
674 let event = InputEvent::new(
675 InputIndex::new(0).expect("zero is valid"),
676 InputValue::Bool(true),
677 );
678 unsafe { event.write_to(output.as_mut_ptr(), InputValueType::Bool) }
679 .expect("matching value type should write");
680 let output = unsafe { output.assume_init() };
683 assert_eq!(output.input_index, 0);
684 let value = unsafe { output.value.value_bool.value };
686 assert_eq!(value, 1);
687 }
688
689 #[test]
690 fn event_writing_rejects_null_and_value_type_mismatch() {
691 let index = InputIndex::new(0).expect("zero is valid");
692 let bool_event = InputEvent::new(index, InputValue::Bool(true));
693 let mut output = MaybeUninit::<sys::ScsInputEvent>::uninit();
694
695 let null_result =
696 unsafe { bool_event.write_to(core::ptr::null_mut(), InputValueType::Bool) };
697 assert_eq!(null_result, Err(SdkError::InvalidParameter));
698
699 let mismatch = unsafe { bool_event.write_to(output.as_mut_ptr(), InputValueType::Float) };
700 assert_eq!(mismatch, Err(SdkError::InvalidParameter));
701 }
702
703 #[test]
704 fn event_writing_initializes_only_the_active_union_storage() {
705 const SENTINEL: u8 = 0xA5;
706
707 let mut float_output = MaybeUninit::<sys::ScsInputEvent>::uninit();
708 unsafe {
714 core::ptr::write_bytes(
715 float_output.as_mut_ptr().cast::<u8>(),
716 SENTINEL,
717 core::mem::size_of::<sys::ScsInputEvent>(),
718 );
719 }
720 let event = InputEvent::new(
721 InputIndex::new(3).expect("three is valid"),
722 InputValue::Float(InputAxisValue::new(-0.625).expect("value is normalized")),
723 );
724
725 unsafe { event.write_to(float_output.as_mut_ptr(), InputValueType::Float) }
726 .expect("matching float value should write");
727
728 let float_bytes = unsafe {
732 core::slice::from_raw_parts(
733 float_output.as_ptr().cast::<u8>(),
734 core::mem::size_of::<sys::ScsInputEvent>(),
735 )
736 };
737 assert_eq!(&float_bytes[..4], &3_u32.to_ne_bytes());
738 assert_eq!(&float_bytes[4..8], &(-0.625_f32).to_ne_bytes());
739 assert!(float_bytes[8..].iter().all(|byte| *byte == SENTINEL));
740
741 let mut bool_output = MaybeUninit::<sys::ScsInputEvent>::uninit();
742 unsafe {
745 core::ptr::write_bytes(
746 bool_output.as_mut_ptr().cast::<u8>(),
747 SENTINEL,
748 core::mem::size_of::<sys::ScsInputEvent>(),
749 );
750 }
751 let event = InputEvent::new(
752 InputIndex::new(7).expect("seven is valid"),
753 InputValue::Bool(true),
754 );
755
756 unsafe { event.write_to(bool_output.as_mut_ptr(), InputValueType::Bool) }
757 .expect("matching bool value should write");
758
759 let bool_bytes = unsafe {
762 core::slice::from_raw_parts(
763 bool_output.as_ptr().cast::<u8>(),
764 core::mem::size_of::<sys::ScsInputEvent>(),
765 )
766 };
767 assert_eq!(&bool_bytes[..4], &7_u32.to_ne_bytes());
768 assert_eq!(bool_bytes[4], 1);
769 assert!(bool_bytes[5..].iter().all(|byte| *byte == SENTINEL));
770 }
771
772 #[test]
773 fn input_axis_value_accepts_only_the_finite_normalized_domain() {
774 for value in [-1.0, -0.625, -0.0, 0.0, 0.625, 1.0] {
775 let normalized = InputAxisValue::new(value).expect("value should be normalized");
776 assert_eq!(normalized.get().to_bits(), value.to_bits());
777 assert_eq!(f32::from(normalized).to_bits(), value.to_bits());
778 assert_eq!(InputAxisValue::try_from(value), Ok(normalized));
779 }
780
781 for value in [-1.000_000_1, -2.0, 1.000_000_1, 2.0] {
782 assert_eq!(
783 InputAxisValue::new(value),
784 Err(InputAxisValueError::OutOfRange)
785 );
786 }
787
788 for value in [f32::NAN, f32::NEG_INFINITY, f32::INFINITY] {
789 assert_eq!(
790 InputAxisValue::new(value),
791 Err(InputAxisValueError::NotFinite)
792 );
793 }
794
795 assert_eq!(InputAxisValue::MIN.get().to_bits(), (-1.0_f32).to_bits());
796 assert_eq!(InputAxisValue::CENTER.get().to_bits(), 0.0_f32.to_bits());
797 assert_eq!(InputAxisValue::MAX.get().to_bits(), 1.0_f32.to_bits());
798 }
799
800 #[test]
801 fn device_registration_rejects_empty_and_too_many_inputs() {
802 let empty = unsafe {
803 InputDeviceRegistration::new(
804 c"device",
805 c"Device",
806 InputDeviceType::Generic,
807 &[],
808 core::ptr::null_mut(),
809 None,
810 fake_event,
811 )
812 };
813 assert_eq!(empty.err(), Some(SdkError::InvalidParameter));
814
815 let inputs: Vec<_> = (0..=InputIndex::MAX_COUNT)
816 .map(|_| InputDeviceInput::new(c"button", c"Button", InputValueType::Bool))
817 .collect();
818 let too_many = unsafe {
819 InputDeviceRegistration::new(
820 c"device",
821 c"Device",
822 InputDeviceType::Generic,
823 &inputs,
824 core::ptr::null_mut(),
825 None,
826 fake_event,
827 )
828 };
829 assert_eq!(too_many.err(), Some(SdkError::InvalidParameter));
830 }
831
832 #[test]
833 fn input_event_flags_decode_known_bits_and_preserve_unknown_bits() {
834 let flags = InputEventFlags::from_raw(
835 sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME
836 | sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION
837 | 0x8000_0000,
838 );
839
840 assert!(flags.first_in_frame());
841 assert!(flags.first_after_activation());
842 assert_eq!(
843 flags.raw(),
844 sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_IN_FRAME
845 | sys::SCS_INPUT_EVENT_CALLBACK_FLAG_FIRST_AFTER_ACTIVATION
846 | 0x8000_0000
847 );
848 }
849
850 #[test]
851 fn input_device_value_type_does_not_treat_unknown_as_float() {
852 let mut input = InputDeviceInput::new(c"axis", c"Axis", InputValueType::Float);
853 assert_eq!(input.value_type(), Some(InputValueType::Float));
854
855 input.raw.value_type = sys::SCS_VALUE_TYPE_INVALID;
856 assert_eq!(input.value_type(), None);
857 }
858}