1#![deny(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
27#![cfg_attr(
28 not(test),
29 deny(
30 clippy::expect_used,
31 clippy::panic,
32 clippy::todo,
33 clippy::unimplemented,
34 clippy::unreachable,
35 clippy::unwrap_used
36 )
37)]
38
39mod input;
40mod input_runtime;
41mod runtime;
42
43use std::borrow::Cow;
44use std::ffi::{CStr, CString};
45use std::fmt;
46
47use scs_sdk::{
48 AnyChannel, Attribute, Channel, ChannelFlags, ChannelValue, ConfigurationId, ConfigurationRef,
49 FrameStartRef, GameSchemaAvailability, GameSchemaVersion, GameplayEventId, GameplayEventRef,
50 LogLevel, SdkCall, SdkError, SdkIndex, SdkValue, StringValue, TelemetryApiVersion,
51 TrailerConfigurationId, TrailerIndex, ValueRef,
52};
53
54pub use input::{
55 InputAxisValue, InputAxisValueError, InputDeviceId, InputDeviceSpec, InputDeviceType,
56 InputEvent, InputEventFlags, InputEventRequest, InputGameCompatibility, InputGameInfo,
57 InputIndex, InputPlugin, InputPluginCompatibility, InputPluginContext, InputSpec, InputValue,
58 InputValueType,
59};
60pub use scs_sdk as sdk;
65pub use scs_sdk_plugin_macros::{export_input_plugin, export_plugin};
66
67pub use scs_sdk::Event as TelemetryEventKind;
74
75pub type PluginResult<T = ()> = Result<T, PluginError>;
77
78pub(crate) const fn telemetry_api_satisfies(
85 actual: TelemetryApiVersion,
86 minimum: TelemetryApiVersion,
87) -> bool {
88 actual.major() == minimum.major() && actual.raw() >= minimum.raw()
89}
90
91pub(crate) const fn game_schema_satisfies(
93 actual: GameSchemaVersion,
94 minimum: GameSchemaVersion,
95) -> bool {
96 actual.major() == minimum.major() && actual.raw() >= minimum.raw()
97}
98
99#[derive(Debug)]
106pub struct PluginError {
107 result: SdkError,
108 message: String,
109}
110
111impl PluginError {
112 #[must_use]
114 pub fn new(result: SdkError, message: impl Into<String>) -> Self {
115 Self {
116 result,
117 message: message.into(),
118 }
119 }
120
121 #[must_use]
123 pub const fn result(&self) -> SdkError {
124 self.result
125 }
126
127 #[must_use]
129 pub fn message(&self) -> &str {
130 &self.message
131 }
132}
133
134impl From<SdkError> for PluginError {
135 fn from(result: SdkError) -> Self {
136 Self::new(result, result.to_string())
137 }
138}
139
140impl fmt::Display for PluginError {
141 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142 formatter.write_str(&self.message)
143 }
144}
145
146impl std::error::Error for PluginError {}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub enum Game {
155 EuroTruckSimulator2,
156 AmericanTruckSimulator,
157 Other,
158}
159
160pub(crate) fn classify_game_id(id: &CStr) -> Game {
161 let id_bytes = id.to_bytes_with_nul();
165 if id_bytes == scs_sdk::sys::SCS_GAME_ID_EUT2 {
166 Game::EuroTruckSimulator2
167 } else if id_bytes == scs_sdk::sys::SCS_GAME_ID_ATS {
168 Game::AmericanTruckSimulator
169 } else {
170 Game::Other
171 }
172}
173
174#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct GameInfo {
181 name: String,
182 id: String,
183 kind: Game,
184 schema_version: GameSchemaVersion,
185}
186
187impl GameInfo {
188 pub(crate) fn new(name: &CStr, id: &CStr, schema_version: GameSchemaVersion) -> Self {
189 Self {
190 name: name.to_string_lossy().into_owned(),
191 id: id.to_string_lossy().into_owned(),
192 kind: classify_game_id(id),
193 schema_version,
194 }
195 }
196
197 #[must_use]
202 pub fn name(&self) -> &str {
203 &self.name
204 }
205
206 #[must_use]
208 pub fn id(&self) -> &str {
209 &self.id
210 }
211
212 #[must_use]
214 pub const fn kind(&self) -> Game {
215 self.kind
216 }
217
218 #[must_use]
222 pub const fn schema_version(&self) -> GameSchemaVersion {
223 self.schema_version
224 }
225
226 #[must_use]
233 pub const fn minimum_schema_for(
234 &self,
235 availability: GameSchemaAvailability,
236 ) -> Option<GameSchemaVersion> {
237 match self.kind {
238 Game::EuroTruckSimulator2 => availability.available_since_ets2(),
239 Game::AmericanTruckSimulator => availability.available_since_ats(),
240 Game::Other => None,
241 }
242 }
243
244 #[must_use]
252 pub const fn supports(&self, availability: GameSchemaAvailability) -> bool {
253 match self.minimum_schema_for(availability) {
254 Some(minimum) => game_schema_satisfies(self.schema_version, minimum),
255 None => false,
256 }
257 }
258}
259
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268pub struct GameCompatibility {
269 game: Game,
270 minimum_schema: GameSchemaVersion,
271}
272
273impl GameCompatibility {
274 #[must_use]
280 pub const fn new(game: Game, minimum_schema: GameSchemaVersion) -> Self {
281 Self {
282 game,
283 minimum_schema,
284 }
285 }
286
287 #[must_use]
289 pub const fn game(self) -> Game {
290 self.game
291 }
292
293 #[must_use]
295 pub const fn minimum_schema(self) -> GameSchemaVersion {
296 self.minimum_schema
297 }
298}
299
300#[derive(Clone, Copy, Debug, PartialEq, Eq)]
313pub struct PluginCompatibility {
314 minimum_telemetry_api: TelemetryApiVersion,
315 games: &'static [GameCompatibility],
316}
317
318impl PluginCompatibility {
319 #[must_use]
326 pub const fn new(
327 minimum_telemetry_api: TelemetryApiVersion,
328 games: &'static [GameCompatibility],
329 ) -> Self {
330 Self {
331 minimum_telemetry_api,
332 games,
333 }
334 }
335
336 #[must_use]
338 pub const fn minimum_telemetry_api(self) -> TelemetryApiVersion {
339 self.minimum_telemetry_api
340 }
341
342 #[must_use]
344 pub const fn games(self) -> &'static [GameCompatibility] {
345 self.games
346 }
347}
348
349#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355pub(crate) enum SubscriptionRequirement {
356 Required,
357 Optional,
358}
359
360impl SubscriptionRequirement {
361 pub(crate) const fn tolerates_channel_registration_error(self, error: SdkError) -> bool {
362 matches!(self, Self::Optional)
363 && matches!(error, SdkError::NotFound | SdkError::UnsupportedType)
364 }
365
366 pub(crate) const fn tolerates_event_registration_error(self, error: SdkError) -> bool {
367 matches!(self, Self::Optional)
368 && matches!(error, SdkError::Unsupported | SdkError::NotFound)
369 }
370}
371
372#[derive(Clone, Copy, Debug)]
373pub(crate) struct EventSubscriptionSpec {
374 pub(crate) event: TelemetryEventKind,
375 pub(crate) requirement: SubscriptionRequirement,
376}
377
378#[derive(Clone, Debug)]
379pub(crate) struct SubscriptionSpec {
380 pub(crate) channel: AnyChannel,
381 pub(crate) registered_name: CString,
382 pub(crate) sdk_index: Option<SdkIndex>,
383 pub(crate) trailer_index: Option<TrailerIndex>,
384 pub(crate) flags: ChannelFlags,
385 pub(crate) requirement: SubscriptionRequirement,
386}
387
388pub struct PluginContext<'scope> {
394 call: &'scope SdkCall<'scope>,
395 game: GameInfo,
396 events: Option<&'scope mut Vec<EventSubscriptionSpec>>,
397 subscriptions: Option<&'scope mut Vec<SubscriptionSpec>>,
398}
399
400impl<'scope> PluginContext<'scope> {
401 pub(crate) fn initializing(
402 call: &'scope SdkCall<'scope>,
403 game: GameInfo,
404 events: &'scope mut Vec<EventSubscriptionSpec>,
405 subscriptions: &'scope mut Vec<SubscriptionSpec>,
406 ) -> Self {
407 Self {
408 call,
409 game,
410 events: Some(events),
411 subscriptions: Some(subscriptions),
412 }
413 }
414
415 pub(crate) fn callback(call: &'scope SdkCall<'scope>, game: GameInfo) -> Self {
416 Self {
417 call,
418 game,
419 events: None,
420 subscriptions: None,
421 }
422 }
423
424 #[must_use]
426 pub const fn game(&self) -> &GameInfo {
427 &self.game
428 }
429
430 #[must_use]
438 pub const fn telemetry_api_version(&self) -> TelemetryApiVersion {
439 self.call.telemetry_api_version()
440 }
441
442 pub fn log(&self, level: LogLevel, arguments: fmt::Arguments<'_>) {
448 let rendered = format!("{arguments}").replace('\0', " ");
449 if let Ok(message) = CString::new(rendered) {
450 self.call.logger().log(level, &message);
451 }
452 }
453
454 pub fn message(&self, arguments: fmt::Arguments<'_>) {
456 self.log(LogLevel::Message, arguments);
457 }
458
459 pub fn warning(&self, arguments: fmt::Arguments<'_>) {
461 self.log(LogLevel::Warning, arguments);
462 }
463
464 pub fn error(&self, arguments: fmt::Arguments<'_>) {
466 self.log(LogLevel::Error, arguments);
467 }
468
469 pub fn subscribe_event(&mut self, event: TelemetryEventKind) -> PluginResult {
485 self.subscribe_event_with_requirement(event, SubscriptionRequirement::Required)
486 }
487
488 pub fn subscribe_event_optional(&mut self, event: TelemetryEventKind) -> PluginResult {
502 self.subscribe_event_with_requirement(event, SubscriptionRequirement::Optional)
503 }
504
505 fn subscribe_event_with_requirement(
506 &mut self,
507 event: TelemetryEventKind,
508 requirement: SubscriptionRequirement,
509 ) -> PluginResult {
510 let api_version = self.telemetry_api_version();
511 let Some(events) = self.events.as_deref_mut() else {
512 return Err(PluginError::new(
513 SdkError::NotNow,
514 "events may only be subscribed during plugin initialization",
515 ));
516 };
517 let minimum_api = event.minimum_api_version();
518 if !telemetry_api_satisfies(api_version, minimum_api)
519 && requirement == SubscriptionRequirement::Required
520 {
521 return Err(PluginError::new(
522 SdkError::Unsupported,
523 format!(
524 "event {event:?} requires telemetry API {minimum_api}, negotiated {api_version}"
525 ),
526 ));
527 }
528 let minimum_schema = self.game.minimum_schema_for(event.availability());
529 let schema_supported = self.game.supports(event.availability());
530 if !schema_supported && requirement == SubscriptionRequirement::Required {
531 let detail = minimum_schema.map_or_else(
532 || format!("is not available for {:?}", self.game.kind()),
533 |minimum| {
534 format!(
535 "requires {:?} telemetry schema {minimum}; detected {}",
536 self.game.kind(),
537 self.game.schema_version(),
538 )
539 },
540 );
541 return Err(PluginError::new(
542 SdkError::Unsupported,
543 format!("event {event:?} {detail}"),
544 ));
545 }
546 if events.iter().any(|candidate| candidate.event == event) {
547 return Err(PluginError::new(
548 SdkError::AlreadyRegistered,
549 format!("duplicate event subscription for {event:?}"),
550 ));
551 }
552 events.push(EventSubscriptionSpec { event, requirement });
553 Ok(())
554 }
555
556 pub fn subscribe<T: ChannelValue>(&mut self, channel: Channel<T>) -> PluginResult {
572 self.subscribe_with_flags(channel, ChannelFlags::NONE)
573 }
574
575 pub fn subscribe_with_flags<T: ChannelValue>(
581 &mut self,
582 channel: Channel<T>,
583 flags: ChannelFlags,
584 ) -> PluginResult {
585 self.subscribe_scalar_with_flags(channel, flags, SubscriptionRequirement::Required)
586 }
587
588 pub fn subscribe_optional<T: ChannelValue>(&mut self, channel: Channel<T>) -> PluginResult {
607 self.subscribe_optional_with_flags(channel, ChannelFlags::NONE)
608 }
609
610 pub fn subscribe_optional_with_flags<T: ChannelValue>(
617 &mut self,
618 channel: Channel<T>,
619 flags: ChannelFlags,
620 ) -> PluginResult {
621 self.subscribe_scalar_with_flags(channel, flags, SubscriptionRequirement::Optional)
622 }
623
624 fn subscribe_scalar_with_flags<T: ChannelValue>(
625 &mut self,
626 channel: Channel<T>,
627 flags: ChannelFlags,
628 requirement: SubscriptionRequirement,
629 ) -> PluginResult {
630 if channel.is_indexed() {
631 return Err(PluginError::new(
632 SdkError::InvalidParameter,
633 format!("indexed channel {:?} requires subscribe_at", channel.name()),
634 ));
635 }
636 self.push_subscription(
637 channel.erase(),
638 channel.name().to_owned(),
639 None,
640 None,
641 flags,
642 requirement,
643 )
644 }
645
646 pub fn subscribe_at<T: ChannelValue>(
656 &mut self,
657 channel: Channel<T>,
658 index: SdkIndex,
659 ) -> PluginResult {
660 self.subscribe_at_with_flags(channel, index, ChannelFlags::NONE)
661 }
662
663 pub fn subscribe_at_with_flags<T: ChannelValue>(
669 &mut self,
670 channel: Channel<T>,
671 index: SdkIndex,
672 flags: ChannelFlags,
673 ) -> PluginResult {
674 self.subscribe_at_with_flags_and_requirement(
675 channel,
676 index,
677 flags,
678 SubscriptionRequirement::Required,
679 )
680 }
681
682 pub fn subscribe_at_optional<T: ChannelValue>(
693 &mut self,
694 channel: Channel<T>,
695 index: SdkIndex,
696 ) -> PluginResult {
697 self.subscribe_at_optional_with_flags(channel, index, ChannelFlags::NONE)
698 }
699
700 pub fn subscribe_at_optional_with_flags<T: ChannelValue>(
707 &mut self,
708 channel: Channel<T>,
709 index: SdkIndex,
710 flags: ChannelFlags,
711 ) -> PluginResult {
712 self.subscribe_at_with_flags_and_requirement(
713 channel,
714 index,
715 flags,
716 SubscriptionRequirement::Optional,
717 )
718 }
719
720 fn subscribe_at_with_flags_and_requirement<T: ChannelValue>(
721 &mut self,
722 channel: Channel<T>,
723 index: SdkIndex,
724 flags: ChannelFlags,
725 requirement: SubscriptionRequirement,
726 ) -> PluginResult {
727 if !channel.is_indexed() {
728 return Err(PluginError::new(
729 SdkError::InvalidParameter,
730 format!(
731 "scalar channel {:?} does not accept an SDK index",
732 channel.name()
733 ),
734 ));
735 }
736 self.push_subscription(
737 channel.erase(),
738 channel.name().to_owned(),
739 Some(index),
740 None,
741 flags,
742 requirement,
743 )
744 }
745
746 pub fn subscribe_trailer<T: ChannelValue>(
761 &mut self,
762 channel: Channel<T>,
763 trailer_index: TrailerIndex,
764 ) -> PluginResult {
765 self.subscribe_trailer_with_flags(channel, trailer_index, ChannelFlags::NONE)
766 }
767
768 pub fn subscribe_trailer_with_flags<T: ChannelValue>(
774 &mut self,
775 channel: Channel<T>,
776 trailer_index: TrailerIndex,
777 flags: ChannelFlags,
778 ) -> PluginResult {
779 self.subscribe_trailer_with_flags_and_requirement(
780 channel,
781 trailer_index,
782 flags,
783 SubscriptionRequirement::Required,
784 )
785 }
786
787 pub fn subscribe_trailer_optional<T: ChannelValue>(
799 &mut self,
800 channel: Channel<T>,
801 trailer_index: TrailerIndex,
802 ) -> PluginResult {
803 self.subscribe_trailer_optional_with_flags(channel, trailer_index, ChannelFlags::NONE)
804 }
805
806 pub fn subscribe_trailer_optional_with_flags<T: ChannelValue>(
813 &mut self,
814 channel: Channel<T>,
815 trailer_index: TrailerIndex,
816 flags: ChannelFlags,
817 ) -> PluginResult {
818 self.subscribe_trailer_with_flags_and_requirement(
819 channel,
820 trailer_index,
821 flags,
822 SubscriptionRequirement::Optional,
823 )
824 }
825
826 fn subscribe_trailer_with_flags_and_requirement<T: ChannelValue>(
827 &mut self,
828 channel: Channel<T>,
829 trailer_index: TrailerIndex,
830 flags: ChannelFlags,
831 requirement: SubscriptionRequirement,
832 ) -> PluginResult {
833 if channel.is_indexed() {
834 return Err(PluginError::new(
835 SdkError::InvalidParameter,
836 "indexed trailer channels require subscribe_trailer_at",
837 ));
838 }
839 let name = trailer_channel_name(channel, trailer_index)?;
840 self.push_subscription(
841 channel.erase(),
842 name,
843 None,
844 Some(trailer_index),
845 flags,
846 requirement,
847 )
848 }
849
850 pub fn subscribe_trailer_at<T: ChannelValue>(
861 &mut self,
862 channel: Channel<T>,
863 trailer_index: TrailerIndex,
864 index: SdkIndex,
865 ) -> PluginResult {
866 self.subscribe_trailer_at_with_flags(channel, trailer_index, index, ChannelFlags::NONE)
867 }
868
869 pub fn subscribe_trailer_at_with_flags<T: ChannelValue>(
875 &mut self,
876 channel: Channel<T>,
877 trailer_index: TrailerIndex,
878 index: SdkIndex,
879 flags: ChannelFlags,
880 ) -> PluginResult {
881 self.subscribe_trailer_at_with_flags_and_requirement(
882 channel,
883 trailer_index,
884 index,
885 flags,
886 SubscriptionRequirement::Required,
887 )
888 }
889
890 pub fn subscribe_trailer_at_optional<T: ChannelValue>(
900 &mut self,
901 channel: Channel<T>,
902 trailer_index: TrailerIndex,
903 index: SdkIndex,
904 ) -> PluginResult {
905 self.subscribe_trailer_at_optional_with_flags(
906 channel,
907 trailer_index,
908 index,
909 ChannelFlags::NONE,
910 )
911 }
912
913 pub fn subscribe_trailer_at_optional_with_flags<T: ChannelValue>(
920 &mut self,
921 channel: Channel<T>,
922 trailer_index: TrailerIndex,
923 index: SdkIndex,
924 flags: ChannelFlags,
925 ) -> PluginResult {
926 self.subscribe_trailer_at_with_flags_and_requirement(
927 channel,
928 trailer_index,
929 index,
930 flags,
931 SubscriptionRequirement::Optional,
932 )
933 }
934
935 fn subscribe_trailer_at_with_flags_and_requirement<T: ChannelValue>(
936 &mut self,
937 channel: Channel<T>,
938 trailer_index: TrailerIndex,
939 index: SdkIndex,
940 flags: ChannelFlags,
941 requirement: SubscriptionRequirement,
942 ) -> PluginResult {
943 if !channel.is_indexed() {
944 return Err(PluginError::new(
945 SdkError::InvalidParameter,
946 "scalar trailer channels do not accept an SDK index",
947 ));
948 }
949 let name = trailer_channel_name(channel, trailer_index)?;
950 self.push_subscription(
951 channel.erase(),
952 name,
953 Some(index),
954 Some(trailer_index),
955 flags,
956 requirement,
957 )
958 }
959
960 fn push_subscription(
961 &mut self,
962 channel: AnyChannel,
963 registered_name: CString,
964 sdk_index: Option<SdkIndex>,
965 trailer_index: Option<TrailerIndex>,
966 flags: ChannelFlags,
967 requirement: SubscriptionRequirement,
968 ) -> PluginResult {
969 let api_version = self.telemetry_api_version();
970 let descriptor_minimum = self.game.minimum_schema_for(channel.availability());
971 let descriptor_supported = self.game.supports(channel.availability());
972 let trailer_minimum = trailer_index.and_then(|_| {
973 self.game
974 .minimum_schema_for(scs_sdk::game::capabilities::MULTI_TRAILER)
975 });
976 let trailer_supported = trailer_index.is_none()
977 || self
978 .game
979 .supports(scs_sdk::game::capabilities::MULTI_TRAILER);
980 let Some(subscriptions) = self.subscriptions.as_deref_mut() else {
981 return Err(PluginError::new(
982 SdkError::NotNow,
983 "channels may only be subscribed during plugin initialization",
984 ));
985 };
986
987 let value_type = channel.value_type();
988 let minimum_api = value_type.minimum_api_version();
989 if !telemetry_api_satisfies(api_version, minimum_api)
990 && requirement == SubscriptionRequirement::Required
991 {
992 return Err(PluginError::new(
993 SdkError::Unsupported,
994 format!(
995 "channel {registered_name:?} requests {value_type:?}, which requires telemetry API {minimum_api}; negotiated {api_version}",
996 ),
997 ));
998 }
999 if (!descriptor_supported || !trailer_supported)
1000 && requirement == SubscriptionRequirement::Required
1001 {
1002 let (capability, minimum) = if descriptor_supported {
1003 ("numbered multi-trailer namespace", trailer_minimum)
1004 } else {
1005 ("channel descriptor", descriptor_minimum)
1006 };
1007 let detail = minimum.map_or_else(
1008 || format!("is not available for {:?}", self.game.kind()),
1009 |minimum| {
1010 format!(
1011 "requires {:?} telemetry schema {minimum}; detected {}",
1012 self.game.kind(),
1013 self.game.schema_version(),
1014 )
1015 },
1016 );
1017 return Err(PluginError::new(
1018 SdkError::Unsupported,
1019 format!("{capability} {registered_name:?} {detail}"),
1020 ));
1021 }
1022
1023 let duplicate = subscriptions.iter().any(|candidate| {
1024 candidate.registered_name == registered_name
1025 && candidate.sdk_index == sdk_index
1026 && candidate.channel.value_type() == channel.value_type()
1027 });
1028 if duplicate {
1029 return Err(PluginError::new(
1030 SdkError::AlreadyRegistered,
1031 format!("duplicate subscription for {registered_name:?}, index {sdk_index:?}"),
1032 ));
1033 }
1034
1035 subscriptions.push(SubscriptionSpec {
1036 channel,
1037 registered_name,
1038 sdk_index,
1039 trailer_index,
1040 flags,
1041 requirement,
1042 });
1043 Ok(())
1044 }
1045}
1046
1047fn trailer_channel_name<T: ChannelValue>(
1048 channel: Channel<T>,
1049 trailer_index: TrailerIndex,
1050) -> PluginResult<CString> {
1051 let name = channel.name().to_str().map_err(|error| {
1052 PluginError::new(
1053 SdkError::InvalidParameter,
1054 format!("channel name is not UTF-8: {error}"),
1055 )
1056 })?;
1057 let Some(suffix) = name.strip_prefix("trailer.") else {
1058 return Err(PluginError::new(
1059 SdkError::InvalidParameter,
1060 format!("channel {name:?} is not a trailer channel"),
1061 ));
1062 };
1063 CString::new(format!("trailer.{trailer_index}.{suffix}")).map_err(|error| {
1064 PluginError::new(
1065 SdkError::InvalidParameter,
1066 format!("generated trailer channel contains a NUL byte: {error}"),
1067 )
1068 })
1069}
1070
1071#[derive(Clone, Copy)]
1077pub struct ChannelUpdate<'a> {
1078 channel: AnyChannel,
1079 registered_name: &'a CStr,
1080 index: Option<SdkIndex>,
1081 trailer_index: Option<TrailerIndex>,
1082 flags: ChannelFlags,
1083 value: Option<ValueRef<'a>>,
1084}
1085
1086impl<'a> ChannelUpdate<'a> {
1087 pub(crate) const fn new(
1088 channel: AnyChannel,
1089 registered_name: &'a CStr,
1090 index: Option<SdkIndex>,
1091 trailer_index: Option<TrailerIndex>,
1092 flags: ChannelFlags,
1093 value: Option<ValueRef<'a>>,
1094 ) -> Self {
1095 Self {
1096 channel,
1097 registered_name,
1098 index,
1099 trailer_index,
1100 flags,
1101 value,
1102 }
1103 }
1104
1105 #[must_use]
1107 pub const fn channel(self) -> AnyChannel {
1108 self.channel
1109 }
1110
1111 #[must_use]
1116 pub fn registered_name(self) -> Cow<'a, str> {
1117 self.registered_name.to_string_lossy()
1118 }
1119
1120 #[must_use]
1122 pub const fn index(self) -> Option<SdkIndex> {
1123 self.index
1124 }
1125
1126 #[must_use]
1128 pub const fn trailer_index(self) -> Option<TrailerIndex> {
1129 self.trailer_index
1130 }
1131
1132 #[must_use]
1134 pub const fn flags(self) -> ChannelFlags {
1135 self.flags
1136 }
1137
1138 #[must_use]
1140 pub const fn value_ref(self) -> Option<ValueRef<'a>> {
1141 self.value
1142 }
1143
1144 #[must_use]
1146 pub fn is<T: ChannelValue>(self, channel: Channel<T>) -> bool {
1147 self.channel == channel.erase()
1148 }
1149
1150 #[must_use]
1155 pub fn value<T: ChannelValue>(self, channel: Channel<T>) -> Option<T::Decoded<'a>> {
1156 if !self.is(channel) {
1157 return None;
1158 }
1159 channel.decode(self.value?)
1160 }
1161}
1162
1163#[derive(Clone, Copy)]
1169pub struct ConfigurationEvent<'a> {
1170 inner: ConfigurationRef<'a>,
1171}
1172
1173impl<'a> ConfigurationEvent<'a> {
1174 pub(crate) const fn new(inner: ConfigurationRef<'a>) -> Self {
1175 Self { inner }
1176 }
1177
1178 #[must_use]
1180 pub fn is(self, id: ConfigurationId) -> bool {
1181 self.inner.is(id)
1182 }
1183
1184 #[must_use]
1186 pub fn trailer(self) -> Option<TrailerConfigurationId> {
1187 self.inner.trailer()
1188 }
1189
1190 #[must_use]
1192 pub fn trailer_index(self) -> Option<TrailerIndex> {
1193 self.inner.trailer_index()
1194 }
1195
1196 #[must_use]
1198 pub fn is_legacy_trailer(self) -> bool {
1199 self.inner.is_legacy_trailer()
1200 }
1201
1202 #[must_use]
1204 pub fn id(self) -> Cow<'a, str> {
1205 self.inner.id().to_string_lossy()
1206 }
1207
1208 #[must_use]
1212 pub fn has_attributes(self) -> bool {
1213 self.inner.attributes().next().is_some()
1214 }
1215
1216 #[must_use]
1218 pub fn get<T: SdkValue>(self, attribute: Attribute<T>) -> Option<T::Decoded<'a>> {
1219 self.inner.attributes().get(attribute)
1220 }
1221
1222 #[must_use]
1224 pub fn get_at<T: SdkValue>(
1225 self,
1226 attribute: Attribute<T>,
1227 index: SdkIndex,
1228 ) -> Option<T::Decoded<'a>> {
1229 self.inner.attributes().get_at(attribute, index)
1230 }
1231
1232 #[must_use]
1234 pub fn string(self, attribute: Attribute<StringValue>) -> Option<Cow<'a, str>> {
1235 self.get(attribute).map(CStr::to_string_lossy)
1236 }
1237
1238 #[must_use]
1240 pub fn string_owned(self, attribute: Attribute<StringValue>) -> Option<String> {
1241 self.string(attribute).map(Cow::into_owned)
1242 }
1243
1244 #[must_use]
1251 pub fn shifter_type(self) -> Option<sdk::configuration::ShifterType> {
1252 let value = self.string(sdk::configuration::attributes::SHIFTER_TYPE)?;
1253 value.parse().ok()
1254 }
1255
1256 #[must_use]
1262 pub fn job_market(self) -> Option<sdk::configuration::JobMarket> {
1263 let value = self.string(sdk::configuration::attributes::JOB_MARKET)?;
1264 value.parse().ok()
1265 }
1266}
1267
1268#[derive(Clone, Copy)]
1273pub struct GameplayEvent<'a> {
1274 inner: GameplayEventRef<'a>,
1275}
1276
1277impl<'a> GameplayEvent<'a> {
1278 pub(crate) const fn new(inner: GameplayEventRef<'a>) -> Self {
1279 Self { inner }
1280 }
1281
1282 #[must_use]
1284 pub fn is(self, id: GameplayEventId) -> bool {
1285 self.inner.is(id)
1286 }
1287
1288 #[must_use]
1290 pub fn id(self) -> Cow<'a, str> {
1291 self.inner.id().to_string_lossy()
1292 }
1293
1294 #[must_use]
1296 pub fn get<T: SdkValue>(self, attribute: Attribute<T>) -> Option<T::Decoded<'a>> {
1297 self.inner.attributes().get(attribute)
1298 }
1299
1300 #[must_use]
1302 pub fn get_at<T: SdkValue>(
1303 self,
1304 attribute: Attribute<T>,
1305 index: SdkIndex,
1306 ) -> Option<T::Decoded<'a>> {
1307 self.inner.attributes().get_at(attribute, index)
1308 }
1309
1310 #[must_use]
1312 pub fn string(self, attribute: Attribute<StringValue>) -> Option<Cow<'a, str>> {
1313 self.get(attribute).map(CStr::to_string_lossy)
1314 }
1315
1316 #[must_use]
1318 pub fn string_owned(self, attribute: Attribute<StringValue>) -> Option<String> {
1319 self.string(attribute).map(Cow::into_owned)
1320 }
1321
1322 #[must_use]
1328 pub fn fine_offence(self) -> Option<sdk::gameplay::FineOffence> {
1329 let value = self.string(sdk::gameplay::attributes::FINE_OFFENCE)?;
1330 value.parse().ok()
1331 }
1332}
1333
1334#[derive(Clone, Copy)]
1339pub enum TelemetryEvent<'a> {
1340 FrameStart(FrameStartRef<'a>),
1341 FrameEnd,
1342 Paused,
1343 Started,
1344 Configuration(ConfigurationEvent<'a>),
1345 Gameplay(GameplayEvent<'a>),
1346}
1347
1348#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1355pub struct PluginMetadata {
1356 name: &'static str,
1357 version: &'static str,
1358}
1359
1360impl PluginMetadata {
1361 #[must_use]
1366 pub const fn new(name: &'static str, version: &'static str) -> Self {
1367 Self { name, version }
1368 }
1369
1370 #[must_use]
1372 pub const fn name(self) -> &'static str {
1373 self.name
1374 }
1375
1376 #[must_use]
1378 pub const fn version(self) -> &'static str {
1379 self.version
1380 }
1381}
1382
1383pub trait TelemetryPlugin: Send + 'static {
1390 fn metadata(&self) -> PluginMetadata;
1395
1396 fn compatibility(&self) -> PluginCompatibility;
1404
1405 fn initialize(&mut self, context: &mut PluginContext<'_>) -> PluginResult;
1416
1417 fn channel(&mut self, _context: &mut PluginContext<'_>, _update: ChannelUpdate<'_>) {}
1419
1420 fn event(&mut self, _context: &mut PluginContext<'_>, _event: TelemetryEvent<'_>) {}
1422
1423 fn shutdown(&mut self, _context: &mut PluginContext<'_>) {}
1425}
1426
1427#[doc(hidden)]
1432pub mod __private {
1433 pub use crate::input_runtime::InputRuntime;
1434 pub use crate::runtime::Runtime;
1435 pub use scs_sdk_sys::{ScsInputInitParams, ScsResult, ScsTelemetryInitParams, ScsU32};
1436}
1437
1438#[cfg(test)]
1439mod tests {
1440 use super::*;
1441 use scs_sdk::channels;
1442
1443 #[test]
1444 fn optional_subscriptions_tolerate_only_capability_absence() {
1445 assert!(
1446 SubscriptionRequirement::Optional
1447 .tolerates_channel_registration_error(SdkError::NotFound)
1448 );
1449 assert!(
1450 SubscriptionRequirement::Optional
1451 .tolerates_channel_registration_error(SdkError::UnsupportedType)
1452 );
1453 for error in [
1454 SdkError::Unsupported,
1455 SdkError::InvalidParameter,
1456 SdkError::AlreadyRegistered,
1457 SdkError::NotNow,
1458 SdkError::Generic,
1459 ] {
1460 assert!(
1461 !SubscriptionRequirement::Optional.tolerates_channel_registration_error(error),
1462 "optional registration unexpectedly tolerated {error}"
1463 );
1464 }
1465 for error in [
1466 SdkError::NotFound,
1467 SdkError::UnsupportedType,
1468 SdkError::Generic,
1469 ] {
1470 assert!(
1471 !SubscriptionRequirement::Required.tolerates_channel_registration_error(error),
1472 "required registration unexpectedly tolerated {error}"
1473 );
1474 }
1475
1476 for error in [SdkError::Unsupported, SdkError::NotFound] {
1477 assert!(
1478 SubscriptionRequirement::Optional.tolerates_event_registration_error(error),
1479 "optional event registration did not tolerate {error}"
1480 );
1481 }
1482 for error in [
1483 SdkError::InvalidParameter,
1484 SdkError::AlreadyRegistered,
1485 SdkError::UnsupportedType,
1486 SdkError::NotNow,
1487 SdkError::Generic,
1488 ] {
1489 assert!(
1490 !SubscriptionRequirement::Optional.tolerates_event_registration_error(error),
1491 "optional event registration unexpectedly tolerated {error}"
1492 );
1493 }
1494 }
1495
1496 #[test]
1497 fn game_info_owns_rust_text_and_classifies_known_ids() {
1498 let ets2 = GameInfo::new(
1499 c"Euro Truck Simulator 2",
1500 c"eut2",
1501 GameSchemaVersion::new(1, 56),
1502 );
1503 assert_eq!(ets2.kind(), Game::EuroTruckSimulator2);
1504 assert_eq!(ets2.name(), "Euro Truck Simulator 2");
1505 assert_eq!(ets2.id(), "eut2");
1506 assert_eq!(ets2.schema_version(), GameSchemaVersion::new(1, 56));
1507 assert!(ets2.supports(sdk::game::capabilities::MULTI_TRAILER));
1508 assert_eq!(
1509 ets2.minimum_schema_for(sdk::game::capabilities::MULTI_TRAILER),
1510 Some(sdk::game::ets2::V1_14)
1511 );
1512
1513 let ats = GameInfo::new(
1514 c"American Truck Simulator",
1515 c"ats",
1516 GameSchemaVersion::new(0, 0),
1517 );
1518 assert_eq!(ats.kind(), Game::AmericanTruckSimulator);
1519 assert!(!ats.supports(channels::truck::ADBLUE.availability()));
1520
1521 let future = GameInfo::new(c"Future Truck", c"future", GameSchemaVersion::new(0, 0));
1522 assert_eq!(future.kind(), Game::Other);
1523 assert_eq!(future.id(), "future");
1524 assert_eq!(
1525 future.minimum_schema_for(sdk::game::capabilities::MULTI_TRAILER),
1526 None
1527 );
1528 assert!(!future.supports(sdk::game::capabilities::MULTI_TRAILER));
1529 }
1530
1531 #[test]
1532 fn trailer_names_follow_the_official_zero_based_scheme() {
1533 let first = trailer_channel_name(channels::trailer::CONNECTED, TrailerIndex::ZERO)
1534 .expect("first trailer name should be valid");
1535 assert_eq!(first.as_c_str(), c"trailer.0.connected");
1536 let last_index = TrailerIndex::new(9).expect("index nine is in the SDK range");
1537 let last = trailer_channel_name(channels::trailer::WHEEL_ROTATION, last_index)
1538 .expect("last trailer name should be valid");
1539 assert_eq!(last.as_c_str(), c"trailer.9.wheel.rotation");
1540 assert_eq!(TrailerIndex::new(10), None);
1541 assert_eq!(
1542 trailer_channel_name(channels::truck::SPEED, TrailerIndex::ZERO)
1543 .expect_err("truck channels must not enter trailer naming")
1544 .result(),
1545 SdkError::InvalidParameter
1546 );
1547 }
1548
1549 #[test]
1550 fn channel_update_requires_the_original_typed_descriptor() {
1551 let raw = scs_sdk_sys::ScsValue {
1552 type_: scs_sdk_sys::SCS_VALUE_TYPE_FLOAT,
1553 padding: scs_sdk_sys::ScsPadding::uninit(),
1554 value: scs_sdk_sys::ScsValueData {
1555 value_float: scs_sdk_sys::ScsValueFloat { value: 42.5 },
1556 },
1557 };
1558 let raw_value = &raw const raw;
1559 let value = unsafe { ValueRef::from_ptr(raw_value) }.expect("test value should be present");
1562 let update = ChannelUpdate::new(
1563 channels::truck::SPEED.erase(),
1564 c"truck.speed",
1565 None,
1566 None,
1567 ChannelFlags::NONE,
1568 Some(value),
1569 );
1570
1571 assert_eq!(update.value(channels::truck::SPEED), Some(42.5));
1572 assert_eq!(update.value(channels::truck::ENGINE_RPM), None);
1573 assert_eq!(update.registered_name(), "truck.speed");
1574 }
1575
1576 #[test]
1577 fn channel_update_preserves_both_strong_index_domains() {
1578 let raw = scs_sdk_sys::ScsValue {
1579 type_: scs_sdk_sys::SCS_VALUE_TYPE_FLOAT,
1580 padding: scs_sdk_sys::ScsPadding::uninit(),
1581 value: scs_sdk_sys::ScsValueData {
1582 value_float: scs_sdk_sys::ScsValueFloat { value: 1.25 },
1583 },
1584 };
1585 let raw_value = &raw const raw;
1586 let value = unsafe { ValueRef::from_ptr(raw_value) }.expect("test value should be present");
1589 let sdk_index = SdkIndex::new(2).expect("ordinary SDK index");
1590 let trailer_index = TrailerIndex::new(1).expect("second trailer");
1591 let update = ChannelUpdate::new(
1592 channels::trailer::WHEEL_ROTATION.erase(),
1593 c"trailer.1.wheel.rotation",
1594 Some(sdk_index),
1595 Some(trailer_index),
1596 ChannelFlags::EACH_FRAME,
1597 Some(value),
1598 );
1599
1600 assert_eq!(update.index(), Some(sdk_index));
1601 assert_eq!(update.trailer_index(), Some(trailer_index));
1602 assert_eq!(update.registered_name(), "trailer.1.wheel.rotation");
1603 assert_eq!(update.value(channels::trailer::WHEEL_ROTATION), Some(1.25));
1604 }
1605
1606 #[test]
1607 fn high_level_configuration_values_parse_known_and_preserve_unknown_text() {
1608 for (raw_market, expected) in [
1609 (
1610 c"external_contracts",
1611 Some(sdk::configuration::JobMarket::ExternalContracts),
1612 ),
1613 (c"future_market", None),
1614 ] {
1615 let attributes = [
1616 scs_sdk_sys::ScsNamedValue {
1617 name: c"job.market".as_ptr(),
1618 index: scs_sdk_sys::SCS_U32_NIL,
1619 padding: scs_sdk_sys::ScsPadding::uninit(),
1620 value: scs_sdk_sys::ScsValue {
1621 type_: scs_sdk_sys::SCS_VALUE_TYPE_STRING,
1622 padding: scs_sdk_sys::ScsPadding::uninit(),
1623 value: scs_sdk_sys::ScsValueData {
1624 value_string: scs_sdk_sys::ScsValueString {
1625 value: raw_market.as_ptr(),
1626 },
1627 },
1628 },
1629 },
1630 scs_sdk_sys::ScsNamedValue {
1631 name: std::ptr::null(),
1632 index: 0,
1633 padding: scs_sdk_sys::ScsPadding::uninit(),
1634 value: scs_sdk_sys::ScsValue {
1635 type_: scs_sdk_sys::SCS_VALUE_TYPE_INVALID,
1636 padding: scs_sdk_sys::ScsPadding::uninit(),
1637 value: scs_sdk_sys::ScsValueData {
1638 value_u64: scs_sdk_sys::ScsValueU64 { value: 0 },
1639 },
1640 },
1641 },
1642 ];
1643 let raw = scs_sdk_sys::ScsTelemetryConfiguration {
1644 id: c"job".as_ptr(),
1645 attributes: attributes.as_ptr(),
1646 };
1647 let inner = unsafe {
1650 ConfigurationRef::from_event_info((&raw const raw).cast::<std::ffi::c_void>())
1651 }
1652 .expect("configuration fixture");
1653 let event = ConfigurationEvent::new(inner);
1654
1655 assert_eq!(event.job_market(), expected);
1656 assert_eq!(
1657 event.string(sdk::configuration::attributes::JOB_MARKET),
1658 Some(Cow::Borrowed(raw_market.to_str().expect("ASCII fixture")))
1659 );
1660 }
1661 }
1662
1663 #[test]
1664 fn high_level_shifter_values_parse_known_and_preserve_unknown_text() {
1665 for (raw_shifter, expected) in [
1666 (c"hshifter", Some(sdk::configuration::ShifterType::HShifter)),
1667 (c"future_shifter", None),
1668 ] {
1669 let attributes = [
1670 scs_sdk_sys::ScsNamedValue {
1671 name: c"shifter.type".as_ptr(),
1672 index: scs_sdk_sys::SCS_U32_NIL,
1673 padding: scs_sdk_sys::ScsPadding::uninit(),
1674 value: scs_sdk_sys::ScsValue {
1675 type_: scs_sdk_sys::SCS_VALUE_TYPE_STRING,
1676 padding: scs_sdk_sys::ScsPadding::uninit(),
1677 value: scs_sdk_sys::ScsValueData {
1678 value_string: scs_sdk_sys::ScsValueString {
1679 value: raw_shifter.as_ptr(),
1680 },
1681 },
1682 },
1683 },
1684 scs_sdk_sys::ScsNamedValue {
1685 name: std::ptr::null(),
1686 index: 0,
1687 padding: scs_sdk_sys::ScsPadding::uninit(),
1688 value: scs_sdk_sys::ScsValue {
1689 type_: scs_sdk_sys::SCS_VALUE_TYPE_INVALID,
1690 padding: scs_sdk_sys::ScsPadding::uninit(),
1691 value: scs_sdk_sys::ScsValueData {
1692 value_u64: scs_sdk_sys::ScsValueU64 { value: 0 },
1693 },
1694 },
1695 },
1696 ];
1697 let raw = scs_sdk_sys::ScsTelemetryConfiguration {
1698 id: c"controls".as_ptr(),
1699 attributes: attributes.as_ptr(),
1700 };
1701 let inner = unsafe {
1704 ConfigurationRef::from_event_info((&raw const raw).cast::<std::ffi::c_void>())
1705 }
1706 .expect("configuration fixture");
1707 let event = ConfigurationEvent::new(inner);
1708
1709 assert_eq!(event.shifter_type(), expected);
1710 assert_eq!(
1711 event.string(sdk::configuration::attributes::SHIFTER_TYPE),
1712 Some(Cow::Borrowed(raw_shifter.to_str().expect("ASCII fixture")))
1713 );
1714 }
1715 }
1716
1717 #[test]
1718 fn high_level_gameplay_values_parse_known_and_preserve_unknown_text() {
1719 for (raw_offence, expected) in [
1720 (
1721 c"damaged_vehicle_usage",
1722 Some(sdk::gameplay::FineOffence::DamagedVehicleUsage),
1723 ),
1724 (c"future_offence", None),
1725 ] {
1726 let attributes = [
1727 scs_sdk_sys::ScsNamedValue {
1728 name: c"fine.offence".as_ptr(),
1729 index: scs_sdk_sys::SCS_U32_NIL,
1730 padding: scs_sdk_sys::ScsPadding::uninit(),
1731 value: scs_sdk_sys::ScsValue {
1732 type_: scs_sdk_sys::SCS_VALUE_TYPE_STRING,
1733 padding: scs_sdk_sys::ScsPadding::uninit(),
1734 value: scs_sdk_sys::ScsValueData {
1735 value_string: scs_sdk_sys::ScsValueString {
1736 value: raw_offence.as_ptr(),
1737 },
1738 },
1739 },
1740 },
1741 scs_sdk_sys::ScsNamedValue {
1742 name: std::ptr::null(),
1743 index: 0,
1744 padding: scs_sdk_sys::ScsPadding::uninit(),
1745 value: scs_sdk_sys::ScsValue {
1746 type_: scs_sdk_sys::SCS_VALUE_TYPE_INVALID,
1747 padding: scs_sdk_sys::ScsPadding::uninit(),
1748 value: scs_sdk_sys::ScsValueData {
1749 value_u64: scs_sdk_sys::ScsValueU64 { value: 0 },
1750 },
1751 },
1752 },
1753 ];
1754 let raw = scs_sdk_sys::ScsTelemetryGameplayEvent {
1755 id: c"player.fined".as_ptr(),
1756 attributes: attributes.as_ptr(),
1757 };
1758 let inner = unsafe {
1761 GameplayEventRef::from_event_info((&raw const raw).cast::<std::ffi::c_void>())
1762 }
1763 .expect("gameplay fixture");
1764 let event = GameplayEvent::new(inner);
1765
1766 assert_eq!(event.fine_offence(), expected);
1767 assert_eq!(
1768 event.string(sdk::gameplay::attributes::FINE_OFFENCE),
1769 Some(Cow::Borrowed(raw_offence.to_str().expect("ASCII fixture")))
1770 );
1771 }
1772 }
1773}