scs_sdk/index.rs
1//! Strong index domains used by telemetry descriptors and callbacks.
2//!
3//! SCS uses the same raw `scs_u32_t` representation for two unrelated ideas:
4//! an SDK array index supplied beside a channel or named value, and the trailer
5//! number embedded in a `trailer.[index].*` name. Keeping both as `u32` lets
6//! otherwise safe application code silently swap them. These newtypes preserve
7//! the distinct domains while remaining zero-cost at the FFI boundary.
8
9use core::fmt;
10
11use crate::sys;
12
13/// Zero-based index passed through the SDK's explicit `index` field.
14///
15/// This selects one member of an indexed channel or configuration attribute,
16/// such as a wheel, H-shifter slot, or substance. The raw value
17/// [`sys::SCS_U32_NIL`] is reserved by SCS to represent a scalar descriptor and
18/// therefore has no `SdkIndex` representation.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(transparent)]
21pub struct SdkIndex(u32);
22
23impl SdkIndex {
24 /// First zero-based SDK array element.
25 pub const ZERO: Self = Self(0);
26
27 /// Creates an SDK index unless `raw` is the scalar sentinel.
28 #[must_use]
29 pub const fn new(raw: u32) -> Option<Self> {
30 if raw == sys::SCS_U32_NIL {
31 None
32 } else {
33 Some(Self(raw))
34 }
35 }
36
37 /// Returns the raw non-sentinel value used by the SCS ABI.
38 #[must_use]
39 pub const fn raw(self) -> u32 {
40 self.0
41 }
42}
43
44impl fmt::Display for SdkIndex {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 self.0.fmt(formatter)
47 }
48}
49
50/// Zero-based trailer number embedded in a multi-trailer name.
51///
52/// SCS SDK 1.14 defines ten numbered trailer configurations and channel
53/// namespaces, `trailer.0` through `trailer.9`. Construction validates that
54/// fixed header boundary once, so framework APIs accepting this type do not
55/// repeat range checks or expose an invalid trailer identity.
56#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
57#[repr(transparent)]
58pub struct TrailerIndex(u32);
59
60impl TrailerIndex {
61 /// Number of trailer slots declared by the official SDK header.
62 pub const COUNT: usize = sys::configuration::MAX_TRAILERS;
63
64 /// First trailer in a chain.
65 pub const ZERO: Self = Self(0);
66
67 /// Every valid trailer index in canonical numeric order.
68 pub const ALL: [Self; Self::COUNT] = [
69 Self(0),
70 Self(1),
71 Self(2),
72 Self(3),
73 Self(4),
74 Self(5),
75 Self(6),
76 Self(7),
77 Self(8),
78 Self(9),
79 ];
80
81 /// Creates a trailer index when `raw` is within the SDK 1.14 range.
82 #[must_use]
83 pub fn new(raw: u32) -> Option<Self> {
84 // The official count is small and fixed by the vendored header. Using
85 // the raw ABI constant as the comparison source prevents the wrapper
86 // and sys inventories from acquiring separate trailer limits.
87 match usize::try_from(raw) {
88 Ok(index) if index < Self::COUNT => Some(Self(raw)),
89 Ok(_) | Err(_) => None,
90 }
91 }
92
93 /// Returns the number embedded after the `trailer.` prefix.
94 #[must_use]
95 pub const fn raw(self) -> u32 {
96 self.0
97 }
98}
99
100impl fmt::Display for TrailerIndex {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 self.0.fmt(formatter)
103 }
104}
105
106/// Identity of a trailer configuration callback.
107///
108/// SCS emits both the legacy unnumbered `trailer` configuration and the newer
109/// numbered `trailer.0` through `trailer.9` configurations. They describe
110/// related payloads but are not interchangeable: the numbered namespace has a
111/// later game-schema requirement and `trailer` remains a compatibility alias
112/// for the first trailer.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
114pub enum TrailerConfigurationId {
115 /// Backward-compatible unnumbered `trailer` configuration.
116 Legacy,
117 /// One canonical numbered `trailer.[index]` configuration.
118 Numbered(TrailerIndex),
119}
120
121impl TrailerConfigurationId {
122 /// Returns the numbered trailer index, or `None` for the legacy alias.
123 #[must_use]
124 pub const fn index(self) -> Option<TrailerIndex> {
125 match self {
126 Self::Legacy => None,
127 Self::Numbered(index) => Some(index),
128 }
129 }
130
131 /// Whether this identity is the legacy unnumbered compatibility alias.
132 #[must_use]
133 pub const fn is_legacy(self) -> bool {
134 matches!(self, Self::Legacy)
135 }
136}
137
138const _: [(); TrailerIndex::COUNT] = [(); sys::configuration::MAX_TRAILERS];
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn sdk_index_excludes_only_the_scalar_sentinel() {
146 assert_eq!(SdkIndex::new(0), Some(SdkIndex::ZERO));
147 assert_eq!(SdkIndex::new(42).map(SdkIndex::raw), Some(42));
148 assert_eq!(SdkIndex::new(sys::SCS_U32_NIL), None);
149 }
150
151 #[test]
152 fn trailer_indices_match_the_official_header_range() {
153 assert_eq!(TrailerIndex::COUNT, 10);
154 assert_eq!(TrailerIndex::COUNT, sys::configuration::MAX_TRAILERS);
155 for (expected, index) in (0_u32..10).zip(TrailerIndex::ALL) {
156 assert_eq!(index.raw(), expected);
157 assert_eq!(TrailerIndex::new(expected), Some(index));
158 }
159 assert_eq!(TrailerIndex::new(10), None);
160 assert_eq!(TrailerIndex::new(u32::MAX), None);
161 }
162
163 #[test]
164 fn trailer_configuration_identity_keeps_legacy_separate() {
165 assert!(TrailerConfigurationId::Legacy.is_legacy());
166 assert_eq!(TrailerConfigurationId::Legacy.index(), None);
167 assert_eq!(
168 TrailerConfigurationId::Numbered(TrailerIndex::ZERO).index(),
169 Some(TrailerIndex::ZERO)
170 );
171 assert!(!TrailerConfigurationId::Numbered(TrailerIndex::ZERO).is_legacy());
172 }
173}