Skip to main content

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}