use std::ffi::CString;
use std::fmt;
use scs_sdk::{InputApiVersion, InputGameVersion, LogLevel, ScopedLogger, SdkError};
use crate::{Game, PluginError, PluginMetadata, PluginResult, classify_game_id};
pub use scs_sdk::input::{
InputAxisValue, InputAxisValueError, InputDeviceType, InputEvent, InputEventFlags, InputIndex,
InputValue, InputValueType,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InputDeviceId(u32);
impl InputDeviceId {
pub(crate) const fn from_ordinal(ordinal: u32) -> Self {
Self(ordinal)
}
#[must_use]
pub const fn ordinal(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputSpec {
name: &'static str,
display_name: &'static str,
value_type: InputValueType,
}
impl InputSpec {
#[must_use]
pub const fn new(
name: &'static str,
display_name: &'static str,
value_type: InputValueType,
) -> Self {
Self {
name,
display_name,
value_type,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
#[must_use]
pub const fn display_name(self) -> &'static str {
self.display_name
}
#[must_use]
pub const fn value_type(self) -> InputValueType {
self.value_type
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputDeviceSpec {
name: &'static str,
display_name: &'static str,
device_type: InputDeviceType,
inputs: &'static [InputSpec],
activity_notifications: bool,
}
impl InputDeviceSpec {
#[must_use]
pub const fn new(
name: &'static str,
display_name: &'static str,
device_type: InputDeviceType,
inputs: &'static [InputSpec],
) -> Self {
Self {
name,
display_name,
device_type,
inputs,
activity_notifications: false,
}
}
#[must_use]
pub const fn with_activity_notifications(mut self) -> Self {
self.activity_notifications = true;
self
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
#[must_use]
pub const fn display_name(self) -> &'static str {
self.display_name
}
#[must_use]
pub const fn device_type(self) -> InputDeviceType {
self.device_type
}
#[must_use]
pub const fn inputs(self) -> &'static [InputSpec] {
self.inputs
}
#[must_use]
pub const fn activity_notifications(self) -> bool {
self.activity_notifications
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InputGameInfo {
name: String,
id: String,
kind: Game,
version: InputGameVersion,
}
impl InputGameInfo {
pub(crate) fn new(
name: &std::ffi::CStr,
id: &std::ffi::CStr,
version: InputGameVersion,
) -> Self {
Self {
name: name.to_string_lossy().into_owned(),
id: id.to_string_lossy().into_owned(),
kind: classify_game_id(id),
version,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub const fn kind(&self) -> Game {
self.kind
}
#[must_use]
pub const fn version(&self) -> InputGameVersion {
self.version
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputGameCompatibility {
game: Game,
minimum_version: InputGameVersion,
}
impl InputGameCompatibility {
#[must_use]
pub const fn new(game: Game, minimum_version: InputGameVersion) -> Self {
Self {
game,
minimum_version,
}
}
#[must_use]
pub const fn game(self) -> Game {
self.game
}
#[must_use]
pub const fn minimum_version(self) -> InputGameVersion {
self.minimum_version
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputPluginCompatibility {
minimum_input_api: InputApiVersion,
games: &'static [InputGameCompatibility],
}
impl InputPluginCompatibility {
#[must_use]
pub const fn new(
minimum_input_api: InputApiVersion,
games: &'static [InputGameCompatibility],
) -> Self {
Self {
minimum_input_api,
games,
}
}
#[must_use]
pub const fn minimum_input_api(self) -> InputApiVersion {
self.minimum_input_api
}
#[must_use]
pub const fn games(self) -> &'static [InputGameCompatibility] {
self.games
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputEventRequest {
device: InputDeviceId,
flags: InputEventFlags,
}
impl InputEventRequest {
pub(crate) const fn new(device: InputDeviceId, flags: InputEventFlags) -> Self {
Self { device, flags }
}
#[must_use]
pub const fn device(self) -> InputDeviceId {
self.device
}
#[must_use]
pub const fn flags(self) -> InputEventFlags {
self.flags
}
}
pub struct InputPluginContext<'scope> {
logger: ScopedLogger<'scope>,
api_version: InputApiVersion,
game: InputGameInfo,
devices: Option<&'scope mut Vec<InputDeviceSpec>>,
}
impl<'scope> InputPluginContext<'scope> {
pub(crate) fn initializing(
logger: ScopedLogger<'scope>,
api_version: InputApiVersion,
game: InputGameInfo,
devices: &'scope mut Vec<InputDeviceSpec>,
) -> Self {
Self {
logger,
api_version,
game,
devices: Some(devices),
}
}
pub(crate) fn callback(
logger: ScopedLogger<'scope>,
api_version: InputApiVersion,
game: InputGameInfo,
) -> Self {
Self {
logger,
api_version,
game,
devices: None,
}
}
#[must_use]
pub const fn game(&self) -> &InputGameInfo {
&self.game
}
#[must_use]
pub const fn input_api_version(&self) -> InputApiVersion {
self.api_version
}
pub fn register_device(&mut self, spec: InputDeviceSpec) -> PluginResult<InputDeviceId> {
let Some(devices) = self.devices.as_deref_mut() else {
return Err(PluginError::new(
SdkError::NotNow,
"input devices may only be registered during plugin initialization",
));
};
validate_device_spec(spec)?;
if devices.iter().any(|device| device.name() == spec.name()) {
return Err(PluginError::new(
SdkError::AlreadyRegistered,
format!("duplicate input device name {:?}", spec.name()),
));
}
let ordinal = u32::try_from(devices.len()).map_err(|_| {
PluginError::new(
SdkError::InvalidParameter,
"input device count exceeds the framework identity range",
)
})?;
let id = InputDeviceId(ordinal);
devices.push(spec);
Ok(id)
}
pub fn log(&self, level: LogLevel, arguments: fmt::Arguments<'_>) {
let rendered = format!("{arguments}").replace('\0', " ");
if let Ok(message) = CString::new(rendered) {
self.logger.log(level, &message);
}
}
pub fn message(&self, arguments: fmt::Arguments<'_>) {
self.log(LogLevel::Message, arguments);
}
pub fn warning(&self, arguments: fmt::Arguments<'_>) {
self.log(LogLevel::Warning, arguments);
}
pub fn error(&self, arguments: fmt::Arguments<'_>) {
self.log(LogLevel::Error, arguments);
}
}
fn validate_device_spec(spec: InputDeviceSpec) -> PluginResult {
if !valid_configuration_name(spec.name()) {
return Err(PluginError::new(
SdkError::InvalidParameter,
format!("invalid input device configuration name {:?}", spec.name()),
));
}
if !valid_display_name(spec.display_name()) {
return Err(PluginError::new(
SdkError::InvalidParameter,
format!(
"invalid input device display name {:?}",
spec.display_name()
),
));
}
if spec.inputs().is_empty() || spec.inputs().len() > InputIndex::MAX_COUNT as usize {
return Err(PluginError::new(
SdkError::InvalidParameter,
format!(
"input device {:?} must declare between 1 and {} inputs",
spec.name(),
InputIndex::MAX_COUNT,
),
));
}
for (position, input) in spec.inputs().iter().copied().enumerate() {
if !valid_configuration_name(input.name()) {
return Err(PluginError::new(
SdkError::InvalidParameter,
format!(
"invalid input name {:?} at position {position} for device {:?}",
input.name(),
spec.name(),
),
));
}
if !valid_display_name(input.display_name()) {
return Err(PluginError::new(
SdkError::InvalidParameter,
format!(
"invalid input display name {:?} at position {position} for device {:?}",
input.display_name(),
spec.name(),
),
));
}
if spec.inputs()[..position]
.iter()
.any(|previous| previous.name() == input.name())
{
return Err(PluginError::new(
SdkError::AlreadyRegistered,
format!(
"duplicate input name {:?} for device {:?}",
input.name(),
spec.name(),
),
));
}
}
Ok(())
}
fn valid_configuration_name(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
}
fn valid_display_name(value: &str) -> bool {
!value.is_empty()
&& value.bytes().all(|byte| {
byte.is_ascii_alphabetic()
|| byte.is_ascii_digit()
|| matches!(byte, b'_' | b' ' | b'.')
})
}
pub trait InputPlugin: Send + 'static {
fn metadata(&self) -> PluginMetadata;
fn compatibility(&self) -> InputPluginCompatibility;
fn initialize(&mut self, context: &mut InputPluginContext<'_>) -> PluginResult;
fn device_active(
&mut self,
_context: &mut InputPluginContext<'_>,
_device: InputDeviceId,
_active: bool,
) {
}
fn next_input_event(
&mut self,
_context: &mut InputPluginContext<'_>,
_request: InputEventRequest,
) -> Option<InputEvent> {
None
}
fn shutdown(&mut self, _context: &mut InputPluginContext<'_>) {}
}
#[cfg(test)]
mod tests {
use super::*;
const BOOL: InputSpec = InputSpec::new("button", "Button 1", InputValueType::Bool);
const DUPLICATES: [InputSpec; 2] = [BOOL, BOOL];
#[test]
fn validates_the_exact_header_name_character_sets() {
assert!(valid_configuration_name("device_01"));
assert!(!valid_configuration_name("Device"));
assert!(!valid_configuration_name("device-name"));
assert!(!valid_configuration_name(""));
assert!(valid_display_name("Example Device 1.0"));
assert!(!valid_display_name("Example/Device"));
assert!(!valid_display_name("设备"));
}
#[test]
fn validates_device_input_count_and_duplicate_names() {
let empty = InputDeviceSpec::new("empty", "Empty", InputDeviceType::Generic, &[]);
assert_eq!(
validate_device_spec(empty)
.err()
.map(|error| error.result()),
Some(SdkError::InvalidParameter)
);
let duplicate = InputDeviceSpec::new(
"duplicate",
"Duplicate",
InputDeviceType::Generic,
&DUPLICATES,
);
assert_eq!(
validate_device_spec(duplicate)
.err()
.map(|error| error.result()),
Some(SdkError::AlreadyRegistered)
);
}
}