Skip to main content

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}