scs_sdk/descriptor.rs
1use core::ffi::CStr;
2use core::marker::PhantomData;
3
4use crate::{GameSchemaVersion, SdkValue, ValueRef, ValueType, sys};
5
6/// Per-game schema versions in which an official telemetry descriptor exists.
7///
8/// SCS versions three different contracts independently: the downloadable SDK
9/// archive, the Telemetry API negotiated by `scs_telemetry_init`, and the game
10/// telemetry schema supplied in the initialization parameters. Channel,
11/// configuration, and gameplay descriptor additions belong to the last of
12/// those domains. Consequently this value stores one minimum schema for ETS2
13/// and another for ATS instead of pretending that an archive number or a
14/// Telemetry API version answers the same question.
15///
16/// A `None` entry is meaningful: the current official game header explicitly
17/// documents that descriptor as unavailable for that game. It is not an
18/// unknown-version wildcard. The SDK 1.0 through 1.14 header history is the
19/// source for the metadata attached to the built-in catalogs.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct GameSchemaAvailability {
22 ets2: Option<GameSchemaVersion>,
23 ats: Option<GameSchemaVersion>,
24}
25
26impl GameSchemaAvailability {
27 /// Describes the first schema which exposes a descriptor in each game.
28 ///
29 /// Use `None` only when the official headers explicitly exclude the
30 /// descriptor for that game. Custom descriptors should still state their
31 /// evidence explicitly instead of inheriting an implicit "all versions"
32 /// policy.
33 #[must_use]
34 pub const fn new(ets2: Option<GameSchemaVersion>, ats: Option<GameSchemaVersion>) -> Self {
35 Self { ets2, ats }
36 }
37
38 /// Oldest ETS2 telemetry schema which exposes the descriptor.
39 #[must_use]
40 pub const fn available_since_ets2(self) -> Option<GameSchemaVersion> {
41 self.ets2
42 }
43
44 /// Oldest ATS telemetry schema which exposes the descriptor.
45 #[must_use]
46 pub const fn available_since_ats(self) -> Option<GameSchemaVersion> {
47 self.ats
48 }
49}
50
51/// Identifies one configuration group delivered by an SCS configuration event.
52///
53/// Examples include truck, trailer, active job, controls, and H-shifter
54/// configuration. The identifier is a stable ASCII subset of UTF-8.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct ConfigurationId {
57 name: &'static CStr,
58 availability: GameSchemaAvailability,
59}
60
61impl ConfigurationId {
62 #[must_use]
63 pub const fn new(name: &'static CStr, availability: GameSchemaAvailability) -> Self {
64 Self { name, availability }
65 }
66
67 #[must_use]
68 pub const fn name(self) -> &'static CStr {
69 self.name
70 }
71
72 /// Official per-game schema history for this configuration identifier.
73 #[must_use]
74 pub const fn availability(self) -> GameSchemaAvailability {
75 self.availability
76 }
77}
78
79/// Identifies one gameplay event delivered by the Telemetry API.
80///
81/// The descriptor only identifies the event. Its typed attributes are declared
82/// separately because several events share names such as payment amount,
83/// source ID, and target ID.
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct GameplayEventId {
86 name: &'static CStr,
87 availability: GameSchemaAvailability,
88}
89
90impl GameplayEventId {
91 #[must_use]
92 pub const fn new(name: &'static CStr, availability: GameSchemaAvailability) -> Self {
93 Self { name, availability }
94 }
95
96 #[must_use]
97 pub const fn name(self) -> &'static CStr {
98 self.name
99 }
100
101 /// Official per-game schema history for this gameplay event identifier.
102 #[must_use]
103 pub const fn availability(self) -> GameSchemaAvailability {
104 self.availability
105 }
106}
107
108/// A typed configuration or gameplay-event attribute descriptor.
109///
110/// `T` selects the expected SCS tagged-union member and the decoded Rust value.
111/// Indexed descriptors represent arrays such as wheel positions or forward
112/// gear ratios; callers must select an explicit zero-based index for them.
113#[derive(Debug, PartialEq, Eq)]
114pub struct Attribute<T: SdkValue> {
115 name: &'static CStr,
116 indexed: bool,
117 availability: GameSchemaAvailability,
118 marker: PhantomData<fn() -> T>,
119}
120
121/// A configuration or gameplay attribute after erasing its Rust marker type.
122///
123/// Catalogs need one homogeneous representation even though attributes decode
124/// to different Rust types. Erasure deliberately retains all metadata needed
125/// to audit the descriptor against the SDK header: the canonical name, tagged
126/// union discriminator, and whether lookup requires a zero-based index.
127///
128/// Normal callback code should keep using [`Attribute<T>`](Attribute), because
129/// that preserves typed decoding. `AnyAttribute` is intended for enumeration,
130/// diagnostics, schema generation, and coverage tests rather than replacing
131/// the typed API.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub struct AnyAttribute {
134 name: &'static CStr,
135 value_type: ValueType,
136 indexed: bool,
137 availability: GameSchemaAvailability,
138}
139
140/// One configuration-group to attribute relationship from the SDK schema.
141///
142/// Attribute descriptors answer which name and value representation SCS uses;
143/// they do not answer which configuration group carries that value. This
144/// distinction matters for shared names such as `id`, `brand`, and
145/// `wheel.position`, and for attributes added to an existing group later than
146/// the attribute name itself first appeared elsewhere.
147///
148/// The relationship therefore owns separate per-game availability metadata.
149/// Schema generators and diagnostics can enumerate these values without
150/// weakening the typed [`Attribute<T>`] API used to decode callback data.
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct ConfigurationAttributeAssociation {
153 configuration: ConfigurationId,
154 attribute: AnyAttribute,
155 availability: GameSchemaAvailability,
156}
157
158impl ConfigurationAttributeAssociation {
159 /// Declares that one typed attribute belongs to a configuration group.
160 ///
161 /// `availability` must describe the relationship, not merely repeat the
162 /// earliest schema of either descriptor. For example, `brand` existed on
163 /// the truck configuration before it was added to trailer configuration.
164 #[must_use]
165 pub const fn new<T: SdkValue>(
166 configuration: ConfigurationId,
167 attribute: Attribute<T>,
168 availability: GameSchemaAvailability,
169 ) -> Self {
170 Self {
171 configuration,
172 attribute: attribute.erase(),
173 availability,
174 }
175 }
176
177 /// Configuration group which carries the attribute.
178 #[must_use]
179 pub const fn configuration(self) -> ConfigurationId {
180 self.configuration
181 }
182
183 /// Type-erased attribute metadata for enumeration and diagnostics.
184 #[must_use]
185 pub const fn attribute(self) -> AnyAttribute {
186 self.attribute
187 }
188
189 /// First ETS2 and ATS schemas which expose this relationship.
190 #[must_use]
191 pub const fn availability(self) -> GameSchemaAvailability {
192 self.availability
193 }
194}
195
196/// One gameplay-event to attribute relationship from the SDK schema.
197///
198/// Several gameplay events reuse attribute descriptors such as `pay.amount`,
199/// `source.id`, and `target.id`. Keeping event membership outside
200/// [`AnyAttribute`] preserves that many-to-many shape and gives schema tooling
201/// an enumerable association catalog without duplicating typed descriptors.
202#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203pub struct GameplayAttributeAssociation {
204 event: GameplayEventId,
205 attribute: AnyAttribute,
206 availability: GameSchemaAvailability,
207}
208
209impl GameplayAttributeAssociation {
210 /// Declares that one typed attribute is carried by a gameplay event.
211 #[must_use]
212 pub const fn new<T: SdkValue>(
213 event: GameplayEventId,
214 attribute: Attribute<T>,
215 availability: GameSchemaAvailability,
216 ) -> Self {
217 Self {
218 event,
219 attribute: attribute.erase(),
220 availability,
221 }
222 }
223
224 /// Gameplay event which carries the attribute.
225 #[must_use]
226 pub const fn event(self) -> GameplayEventId {
227 self.event
228 }
229
230 /// Type-erased attribute metadata for enumeration and diagnostics.
231 #[must_use]
232 pub const fn attribute(self) -> AnyAttribute {
233 self.attribute
234 }
235
236 /// First ETS2 and ATS schemas which expose this relationship.
237 #[must_use]
238 pub const fn availability(self) -> GameSchemaAvailability {
239 self.availability
240 }
241}
242
243impl AnyAttribute {
244 /// Returns the canonical, NUL-terminated attribute name from the SDK
245 /// header.
246 #[must_use]
247 pub const fn name(self) -> &'static CStr {
248 self.name
249 }
250
251 /// Returns the SCS tagged-union member expected for this attribute.
252 #[must_use]
253 pub const fn value_type(self) -> ValueType {
254 self.value_type
255 }
256
257 /// Returns whether lookup requires an explicit zero-based SDK index.
258 ///
259 /// Indexed attributes include wheel properties, transmission ratios, and
260 /// H-shifter slots. Scalar attributes must instead be looked up using the
261 /// SDK sentinel index `SCS_U32_NIL`.
262 #[must_use]
263 pub const fn is_indexed(self) -> bool {
264 self.indexed
265 }
266
267 /// Official per-game schema history retained during type erasure.
268 #[must_use]
269 pub const fn availability(self) -> GameSchemaAvailability {
270 self.availability
271 }
272}
273
274impl<T: SdkValue> Clone for Attribute<T> {
275 fn clone(&self) -> Self {
276 *self
277 }
278}
279
280impl<T: SdkValue> Copy for Attribute<T> {}
281
282impl<T: SdkValue> Attribute<T> {
283 /// Creates a scalar attribute descriptor whose SDK index is `SCS_U32_NIL`.
284 #[must_use]
285 pub const fn new(name: &'static CStr, availability: GameSchemaAvailability) -> Self {
286 Self {
287 name,
288 indexed: false,
289 availability,
290 marker: PhantomData,
291 }
292 }
293
294 /// Creates an attribute descriptor selected by a zero-based SDK index.
295 #[must_use]
296 pub const fn indexed(name: &'static CStr, availability: GameSchemaAvailability) -> Self {
297 Self {
298 name,
299 indexed: true,
300 availability,
301 marker: PhantomData,
302 }
303 }
304
305 #[must_use]
306 pub const fn name(self) -> &'static CStr {
307 self.name
308 }
309
310 #[must_use]
311 pub const fn is_indexed(self) -> bool {
312 self.indexed
313 }
314
315 /// Official per-game schema history for this attribute.
316 #[must_use]
317 pub const fn availability(self) -> GameSchemaAvailability {
318 self.availability
319 }
320
321 #[must_use]
322 pub const fn value_type(self) -> sys::ScsValueType {
323 T::TYPE
324 }
325
326 /// Erases the Rust marker type while preserving the complete descriptor
327 /// metadata required for catalog enumeration.
328 ///
329 /// This does not decode values and does not weaken [`Attribute::decode`].
330 /// The returned descriptor remains explicit about both the SCS value type
331 /// and indexed/scalar lookup mode.
332 #[must_use]
333 pub const fn erase(self) -> AnyAttribute {
334 AnyAttribute {
335 name: self.name,
336 value_type: T::VALUE_TYPE,
337 indexed: self.indexed,
338 availability: self.availability,
339 }
340 }
341
342 /// Decodes one attribute value after verifying its SCS type tag.
343 #[must_use]
344 pub fn decode(self, value: ValueRef<'_>) -> Option<T::Decoded<'_>> {
345 T::decode(value)
346 }
347}