1use std::ffi::CString;
10use std::fmt;
11
12use scs_sdk::{InputApiVersion, InputGameVersion, LogLevel, ScopedLogger, SdkError};
13
14use crate::{Game, PluginError, PluginMetadata, PluginResult, classify_game_id};
15
16pub use scs_sdk::input::{
17 InputAxisValue, InputAxisValueError, InputDeviceType, InputEvent, InputEventFlags, InputIndex,
18 InputValue, InputValueType,
19};
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct InputDeviceId(u32);
28
29impl InputDeviceId {
30 pub(crate) const fn from_ordinal(ordinal: u32) -> Self {
31 Self(ordinal)
32 }
33
34 #[must_use]
36 pub const fn ordinal(self) -> u32 {
37 self.0
38 }
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct InputSpec {
44 name: &'static str,
45 display_name: &'static str,
46 value_type: InputValueType,
47}
48
49impl InputSpec {
50 #[must_use]
57 pub const fn new(
58 name: &'static str,
59 display_name: &'static str,
60 value_type: InputValueType,
61 ) -> Self {
62 Self {
63 name,
64 display_name,
65 value_type,
66 }
67 }
68
69 #[must_use]
71 pub const fn name(self) -> &'static str {
72 self.name
73 }
74
75 #[must_use]
77 pub const fn display_name(self) -> &'static str {
78 self.display_name
79 }
80
81 #[must_use]
83 pub const fn value_type(self) -> InputValueType {
84 self.value_type
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub struct InputDeviceSpec {
91 name: &'static str,
92 display_name: &'static str,
93 device_type: InputDeviceType,
94 inputs: &'static [InputSpec],
95 activity_notifications: bool,
96}
97
98impl InputDeviceSpec {
99 #[must_use]
104 pub const fn new(
105 name: &'static str,
106 display_name: &'static str,
107 device_type: InputDeviceType,
108 inputs: &'static [InputSpec],
109 ) -> Self {
110 Self {
111 name,
112 display_name,
113 device_type,
114 inputs,
115 activity_notifications: false,
116 }
117 }
118
119 #[must_use]
121 pub const fn with_activity_notifications(mut self) -> Self {
122 self.activity_notifications = true;
123 self
124 }
125
126 #[must_use]
128 pub const fn name(self) -> &'static str {
129 self.name
130 }
131
132 #[must_use]
134 pub const fn display_name(self) -> &'static str {
135 self.display_name
136 }
137
138 #[must_use]
140 pub const fn device_type(self) -> InputDeviceType {
141 self.device_type
142 }
143
144 #[must_use]
146 pub const fn inputs(self) -> &'static [InputSpec] {
147 self.inputs
148 }
149
150 #[must_use]
152 pub const fn activity_notifications(self) -> bool {
153 self.activity_notifications
154 }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct InputGameInfo {
160 name: String,
161 id: String,
162 kind: Game,
163 version: InputGameVersion,
164}
165
166impl InputGameInfo {
167 pub(crate) fn new(
168 name: &std::ffi::CStr,
169 id: &std::ffi::CStr,
170 version: InputGameVersion,
171 ) -> Self {
172 Self {
173 name: name.to_string_lossy().into_owned(),
174 id: id.to_string_lossy().into_owned(),
175 kind: classify_game_id(id),
176 version,
177 }
178 }
179
180 #[must_use]
182 pub fn name(&self) -> &str {
183 &self.name
184 }
185
186 #[must_use]
188 pub fn id(&self) -> &str {
189 &self.id
190 }
191
192 #[must_use]
194 pub const fn kind(&self) -> Game {
195 self.kind
196 }
197
198 #[must_use]
200 pub const fn version(&self) -> InputGameVersion {
201 self.version
202 }
203}
204
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub struct InputGameCompatibility {
208 game: Game,
209 minimum_version: InputGameVersion,
210}
211
212impl InputGameCompatibility {
213 #[must_use]
215 pub const fn new(game: Game, minimum_version: InputGameVersion) -> Self {
216 Self {
217 game,
218 minimum_version,
219 }
220 }
221
222 #[must_use]
224 pub const fn game(self) -> Game {
225 self.game
226 }
227
228 #[must_use]
230 pub const fn minimum_version(self) -> InputGameVersion {
231 self.minimum_version
232 }
233}
234
235#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub struct InputPluginCompatibility {
238 minimum_input_api: InputApiVersion,
239 games: &'static [InputGameCompatibility],
240}
241
242impl InputPluginCompatibility {
243 #[must_use]
246 pub const fn new(
247 minimum_input_api: InputApiVersion,
248 games: &'static [InputGameCompatibility],
249 ) -> Self {
250 Self {
251 minimum_input_api,
252 games,
253 }
254 }
255
256 #[must_use]
258 pub const fn minimum_input_api(self) -> InputApiVersion {
259 self.minimum_input_api
260 }
261
262 #[must_use]
264 pub const fn games(self) -> &'static [InputGameCompatibility] {
265 self.games
266 }
267}
268
269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
271pub struct InputEventRequest {
272 device: InputDeviceId,
273 flags: InputEventFlags,
274}
275
276impl InputEventRequest {
277 pub(crate) const fn new(device: InputDeviceId, flags: InputEventFlags) -> Self {
278 Self { device, flags }
279 }
280
281 #[must_use]
283 pub const fn device(self) -> InputDeviceId {
284 self.device
285 }
286
287 #[must_use]
289 pub const fn flags(self) -> InputEventFlags {
290 self.flags
291 }
292}
293
294pub struct InputPluginContext<'scope> {
296 logger: ScopedLogger<'scope>,
297 api_version: InputApiVersion,
298 game: InputGameInfo,
299 devices: Option<&'scope mut Vec<InputDeviceSpec>>,
300}
301
302impl<'scope> InputPluginContext<'scope> {
303 pub(crate) fn initializing(
304 logger: ScopedLogger<'scope>,
305 api_version: InputApiVersion,
306 game: InputGameInfo,
307 devices: &'scope mut Vec<InputDeviceSpec>,
308 ) -> Self {
309 Self {
310 logger,
311 api_version,
312 game,
313 devices: Some(devices),
314 }
315 }
316
317 pub(crate) fn callback(
318 logger: ScopedLogger<'scope>,
319 api_version: InputApiVersion,
320 game: InputGameInfo,
321 ) -> Self {
322 Self {
323 logger,
324 api_version,
325 game,
326 devices: None,
327 }
328 }
329
330 #[must_use]
332 pub const fn game(&self) -> &InputGameInfo {
333 &self.game
334 }
335
336 #[must_use]
338 pub const fn input_api_version(&self) -> InputApiVersion {
339 self.api_version
340 }
341
342 pub fn register_device(&mut self, spec: InputDeviceSpec) -> PluginResult<InputDeviceId> {
354 let Some(devices) = self.devices.as_deref_mut() else {
355 return Err(PluginError::new(
356 SdkError::NotNow,
357 "input devices may only be registered during plugin initialization",
358 ));
359 };
360 validate_device_spec(spec)?;
361 if devices.iter().any(|device| device.name() == spec.name()) {
362 return Err(PluginError::new(
363 SdkError::AlreadyRegistered,
364 format!("duplicate input device name {:?}", spec.name()),
365 ));
366 }
367 let ordinal = u32::try_from(devices.len()).map_err(|_| {
368 PluginError::new(
369 SdkError::InvalidParameter,
370 "input device count exceeds the framework identity range",
371 )
372 })?;
373 let id = InputDeviceId(ordinal);
374 devices.push(spec);
375 Ok(id)
376 }
377
378 pub fn log(&self, level: LogLevel, arguments: fmt::Arguments<'_>) {
380 let rendered = format!("{arguments}").replace('\0', " ");
381 if let Ok(message) = CString::new(rendered) {
382 self.logger.log(level, &message);
383 }
384 }
385
386 pub fn message(&self, arguments: fmt::Arguments<'_>) {
388 self.log(LogLevel::Message, arguments);
389 }
390
391 pub fn warning(&self, arguments: fmt::Arguments<'_>) {
393 self.log(LogLevel::Warning, arguments);
394 }
395
396 pub fn error(&self, arguments: fmt::Arguments<'_>) {
398 self.log(LogLevel::Error, arguments);
399 }
400}
401
402fn validate_device_spec(spec: InputDeviceSpec) -> PluginResult {
403 if !valid_configuration_name(spec.name()) {
404 return Err(PluginError::new(
405 SdkError::InvalidParameter,
406 format!("invalid input device configuration name {:?}", spec.name()),
407 ));
408 }
409 if !valid_display_name(spec.display_name()) {
410 return Err(PluginError::new(
411 SdkError::InvalidParameter,
412 format!(
413 "invalid input device display name {:?}",
414 spec.display_name()
415 ),
416 ));
417 }
418 if spec.inputs().is_empty() || spec.inputs().len() > InputIndex::MAX_COUNT as usize {
419 return Err(PluginError::new(
420 SdkError::InvalidParameter,
421 format!(
422 "input device {:?} must declare between 1 and {} inputs",
423 spec.name(),
424 InputIndex::MAX_COUNT,
425 ),
426 ));
427 }
428 for (position, input) in spec.inputs().iter().copied().enumerate() {
429 if !valid_configuration_name(input.name()) {
430 return Err(PluginError::new(
431 SdkError::InvalidParameter,
432 format!(
433 "invalid input name {:?} at position {position} for device {:?}",
434 input.name(),
435 spec.name(),
436 ),
437 ));
438 }
439 if !valid_display_name(input.display_name()) {
440 return Err(PluginError::new(
441 SdkError::InvalidParameter,
442 format!(
443 "invalid input display name {:?} at position {position} for device {:?}",
444 input.display_name(),
445 spec.name(),
446 ),
447 ));
448 }
449 if spec.inputs()[..position]
450 .iter()
451 .any(|previous| previous.name() == input.name())
452 {
453 return Err(PluginError::new(
454 SdkError::AlreadyRegistered,
455 format!(
456 "duplicate input name {:?} for device {:?}",
457 input.name(),
458 spec.name(),
459 ),
460 ));
461 }
462 }
463 Ok(())
464}
465
466fn valid_configuration_name(value: &str) -> bool {
467 !value.is_empty()
468 && value
469 .bytes()
470 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
471}
472
473fn valid_display_name(value: &str) -> bool {
474 !value.is_empty()
475 && value.bytes().all(|byte| {
476 byte.is_ascii_alphabetic()
477 || byte.is_ascii_digit()
478 || matches!(byte, b'_' | b' ' | b'.')
479 })
480}
481
482pub trait InputPlugin: Send + 'static {
484 fn metadata(&self) -> PluginMetadata;
486
487 fn compatibility(&self) -> InputPluginCompatibility;
489
490 fn initialize(&mut self, context: &mut InputPluginContext<'_>) -> PluginResult;
497
498 fn device_active(
500 &mut self,
501 _context: &mut InputPluginContext<'_>,
502 _device: InputDeviceId,
503 _active: bool,
504 ) {
505 }
506
507 fn next_input_event(
510 &mut self,
511 _context: &mut InputPluginContext<'_>,
512 _request: InputEventRequest,
513 ) -> Option<InputEvent> {
514 None
515 }
516
517 fn shutdown(&mut self, _context: &mut InputPluginContext<'_>) {}
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 const BOOL: InputSpec = InputSpec::new("button", "Button 1", InputValueType::Bool);
526 const DUPLICATES: [InputSpec; 2] = [BOOL, BOOL];
527
528 #[test]
529 fn validates_the_exact_header_name_character_sets() {
530 assert!(valid_configuration_name("device_01"));
531 assert!(!valid_configuration_name("Device"));
532 assert!(!valid_configuration_name("device-name"));
533 assert!(!valid_configuration_name(""));
534
535 assert!(valid_display_name("Example Device 1.0"));
536 assert!(!valid_display_name("Example/Device"));
537 assert!(!valid_display_name("设备"));
538 }
539
540 #[test]
541 fn validates_device_input_count_and_duplicate_names() {
542 let empty = InputDeviceSpec::new("empty", "Empty", InputDeviceType::Generic, &[]);
543 assert_eq!(
544 validate_device_spec(empty)
545 .err()
546 .map(|error| error.result()),
547 Some(SdkError::InvalidParameter)
548 );
549
550 let duplicate = InputDeviceSpec::new(
551 "duplicate",
552 "Duplicate",
553 InputDeviceType::Generic,
554 &DUPLICATES,
555 );
556 assert_eq!(
557 validate_device_spec(duplicate)
558 .err()
559 .map(|error| error.result()),
560 Some(SdkError::AlreadyRegistered)
561 );
562 }
563}