Skip to main content

scs_sdk/
configuration.rs

1//! Typed configuration catalog for the SCS Telemetry SDK.
2//!
3//! This module covers every public configuration ID, attribute, and
4//! documented H-shifter value in the SDK 1.14 header bundle.
5
6use core::ffi::CStr;
7use core::str::FromStr;
8
9use crate::{GameSchemaAvailability, UnknownStringValue, game};
10
11// Configuration descriptors use the game telemetry schema, not the SDK
12// archive number or the negotiated Telemetry API. ATS schema 1.00 corresponds
13// to ETS2 schema 1.12, which is why the two minima often differ.
14const INITIAL: GameSchemaAvailability =
15    GameSchemaAvailability::new(Some(game::ets2::V1_00), Some(game::ats::V1_00));
16const ETS2_1_01_ATS_1_00: GameSchemaAvailability =
17    GameSchemaAvailability::new(Some(game::ets2::V1_01), Some(game::ats::V1_00));
18const ETS2_1_09_ATS_1_00: GameSchemaAvailability =
19    GameSchemaAvailability::new(Some(game::ets2::V1_09), Some(game::ats::V1_00));
20const ETS2_1_10_ATS_1_00: GameSchemaAvailability =
21    GameSchemaAvailability::new(Some(game::ets2::V1_10), Some(game::ats::V1_00));
22const ETS2_1_12_ATS_1_00: GameSchemaAvailability =
23    GameSchemaAvailability::new(Some(game::ets2::V1_12), Some(game::ats::V1_00));
24const ETS2_1_14_ATS_1_01: GameSchemaAvailability =
25    GameSchemaAvailability::new(Some(game::ets2::V1_14), Some(game::ats::V1_01));
26const ETS2_1_15_ATS_1_02: GameSchemaAvailability =
27    GameSchemaAvailability::new(Some(game::ets2::V1_15), Some(game::ats::V1_02));
28
29/// Maximum number of trailers represented by the SDK.
30pub const MAX_TRAILERS: usize = crate::TrailerIndex::COUNT;
31
32/// Configuration group identifiers.
33pub mod ids {
34    use super::{ETS2_1_09_ATS_1_00, INITIAL};
35    use crate::ConfigurationId;
36
37    /// Number of official configuration identifiers.
38    pub const COUNT: usize = 6;
39
40    /// Configuration of the substances.
41    ///
42    /// Attribute index is index of the substance.
43    ///
44    /// Supported attributes:
45    /// - `id`
46    ///
47    /// The upstream SDK header leaves the remaining substance attributes as a
48    /// TODO, so version 1.14 does not define any additional public names here.
49    pub const SUBSTANCES: ConfigurationId = ConfigurationId::new(c"substances", INITIAL);
50
51    /// Static configuration of the controls.
52    ///
53    /// - `shifter_type`
54    pub const CONTROLS: ConfigurationId = ConfigurationId::new(c"controls", INITIAL);
55
56    /// Configuration of the h-shifter.
57    ///
58    /// When evaluating the selected gear, find slot which matches
59    /// the handle position and bitmask of on/off state of selectors.
60    /// If one is found, it contains the resulting gear. Otherwise
61    /// a neutral is assumed.
62    ///
63    /// Supported attributes:
64    /// - `selector_count`
65    /// - resulting gear index for each slot
66    /// - handle position index for each slot
67    /// - bitmask of selectors for each slot
68    pub const HSHIFTER: ConfigurationId = ConfigurationId::new(c"hshifter", INITIAL);
69
70    /// Static configuration of the truck.
71    ///
72    /// If empty set of attributes is returned, there is no configured truck.
73    ///
74    /// Supported attributes:
75    /// - `brand_id`
76    /// - brand
77    /// - id
78    /// - name
79    /// - `fuel_capacity`
80    /// - `fuel_warning_factor`
81    /// - `adblue_capacity`
82    /// - `ablue_warning_factor`
83    /// - `air_pressure_warning`
84    /// - `air_pressure_emergency`
85    /// - `oil_pressure_warning`
86    /// - `water_temperature_warning`
87    /// - `battery_voltage_warning`
88    /// - `rpm_limit`
89    /// - `foward_gear_count`
90    /// - `reverse_gear_count`
91    /// - `retarder_step_count`
92    /// - `cabin_position`
93    /// - `head_position`
94    /// - `hook_position`
95    /// - `license_plate`
96    /// - `license_plate_country`
97    /// - `license_plate_country_id`
98    /// - `wheel_count`
99    /// - wheel positions for `wheel_count` wheels
100    pub const TRUCK: ConfigurationId = ConfigurationId::new(c"truck", INITIAL);
101
102    /// Backward compatibility static configuration of the first trailer (attributes are equal to trailer.0).
103    ///
104    /// The trailers configurations are returned using `trailer.[index]`
105    /// (e.g. trailer.0, trailer.1, ... trailer.9 ...)
106    ///
107    /// SDK currently can return up to `SCS_TELEMETRY_trailers_count` trailers.
108    ///
109    /// If there are less trailers in game than `SCS_TELEMETRY_trailers_count`
110    /// telemetry will return all configurations however starting from the trailer after last
111    /// existing one its attributes will be empty.
112    ///
113    /// Supported attributes:
114    /// - id
115    /// - `cargo_accessory_id`
116    /// - `hook_position`
117    /// - `brand_id`
118    /// - brand
119    /// - name
120    /// - `chain_type` (reported only for first trailer)
121    /// - `body_type` (reported only for first trailer)
122    /// - `license_plate`
123    /// - `license_plate_country`
124    /// - `license_plate_country_id`
125    /// - `wheel_count`
126    /// - wheel offsets for `wheel_count` wheels
127    pub const TRAILER: ConfigurationId = ConfigurationId::new(c"trailer", INITIAL);
128
129    /// Static configuration of the job.
130    ///
131    /// If empty set of attributes is returned, there is no job.
132    ///
133    /// Supported attributes:
134    /// - `cargo_id`
135    /// - cargo
136    /// - `cargo_mass`
137    /// - `destination_city_id`
138    /// - `destination_city`
139    /// - `source_city_id`
140    /// - `source_city`
141    /// - `destination_company_id` (only available for non special transport jobs)
142    /// - `destination_company` (only available for non special transport jobs)
143    /// - `source_company_id` (only available for non special transport jobs)
144    /// - `source_company` (only available for non special transport jobs)
145    /// - income - represents expected income for the job without any penalties
146    /// - `delivery_time`
147    /// - `is_cargo_loaded`
148    /// - `job_market`
149    /// - `special_job`
150    /// - `planned_distance_km`
151    pub const JOB: ConfigurationId = ConfigurationId::new(c"job", ETS2_1_09_ATS_1_00);
152
153    /// Every public configuration group in SDK 1.14 header order.
154    ///
155    /// The trailer entry is the backward-compatible `trailer` identifier. The
156    /// game additionally emits `trailer.0` through `trailer.9`; consumers must
157    /// still select those numbered groups explicitly when they need them.
158    pub const ALL: [ConfigurationId; COUNT] = [SUBSTANCES, CONTROLS, HSHIFTER, TRUCK, TRAILER, JOB];
159}
160
161/// Typed attributes carried by configuration events.
162pub mod attributes {
163    use super::{
164        ETS2_1_01_ATS_1_00, ETS2_1_09_ATS_1_00, ETS2_1_10_ATS_1_00, ETS2_1_12_ATS_1_00,
165        ETS2_1_14_ATS_1_01, ETS2_1_15_ATS_1_02, INITIAL,
166    };
167    use crate::{AnyAttribute, Attribute};
168
169    /// Number of official typed configuration attributes.
170    pub const COUNT: usize = 60;
171
172    /// Brand id for configuration purposes.
173    ///
174    /// Limited to C-identifier characters.
175    ///
176    /// Type: string
177    pub const BRAND_ID: Attribute<crate::StringValue> = Attribute::new(c"brand_id", INITIAL);
178
179    /// Brand for display purposes.
180    ///
181    /// Localized using the current in-game language.
182    ///
183    /// Type: string
184    pub const BRAND: Attribute<crate::StringValue> = Attribute::new(c"brand", INITIAL);
185
186    /// Name for internal use by code.
187    ///
188    /// Limited to C-identifier characters and dots.
189    ///
190    /// Type: string
191    pub const ID: Attribute<crate::StringValue> = Attribute::new(c"id", INITIAL);
192
193    /// Name of cargo accessory for internal use by code.
194    ///
195    /// Limited to C-identifier characters and dots.
196    ///
197    /// Type: string
198    pub const CARGO_ACCESSORY_ID: Attribute<crate::StringValue> =
199        Attribute::new(c"cargo.accessory.id", INITIAL);
200
201    /// Name of trailer chain type.
202    ///
203    /// Limited to C-identifier characters and dots.
204    ///
205    /// Type: string
206    pub const CHAIN_TYPE: Attribute<crate::StringValue> =
207        Attribute::new(c"chain.type", ETS2_1_14_ATS_1_01);
208
209    /// Name of trailer body type.
210    ///
211    /// Limited to C-identifier characters and dots.
212    ///
213    /// Type: string
214    pub const BODY_TYPE: Attribute<crate::StringValue> =
215        Attribute::new(c"body.type", ETS2_1_14_ATS_1_01);
216
217    /// Vehicle license plate.
218    ///
219    /// Type: string
220    pub const LICENSE_PLATE: Attribute<crate::StringValue> =
221        Attribute::new(c"license.plate", ETS2_1_14_ATS_1_01);
222
223    /// The id representing license plate country.
224    ///
225    /// Type: string
226    pub const LICENSE_PLATE_COUNTRY_ID: Attribute<crate::StringValue> =
227        Attribute::new(c"license.plate.country.id", ETS2_1_14_ATS_1_01);
228
229    /// The name of the license plate country.
230    ///
231    /// Type: string
232    pub const LICENSE_PLATE_COUNTRY: Attribute<crate::StringValue> =
233        Attribute::new(c"license.plate.country", ETS2_1_14_ATS_1_01);
234
235    /// Name for display purposes.
236    ///
237    /// Localized using the current in-game language.
238    ///
239    /// Type: string
240    pub const NAME: Attribute<crate::StringValue> = Attribute::new(c"name", INITIAL);
241
242    /// Fuel tank capacity in litres.
243    ///
244    /// Type: float
245    pub const FUEL_CAPACITY: Attribute<f32> = Attribute::new(c"fuel.capacity", INITIAL);
246
247    /// Fraction of the fuel capacity below which
248    /// is activated the fuel warning.
249    ///
250    /// Type: float
251    pub const FUEL_WARNING_FACTOR: Attribute<f32> = Attribute::new(c"fuel.warning.factor", INITIAL);
252
253    /// `AdBlue` tank capacity in litres.
254    ///
255    /// Type: float
256    pub const ADBLUE_CAPACITY: Attribute<f32> =
257        Attribute::new(c"adblue.capacity", ETS2_1_12_ATS_1_00);
258
259    /// Fraction of the adblue capacity below which
260    /// is activated the adblue warning.
261    ///
262    /// Type: float
263    pub const ADBLUE_WARNING_FACTOR: Attribute<f32> =
264        Attribute::new(c"adblue.warning.factor", ETS2_1_12_ATS_1_00);
265
266    /// Pressure of the air in the tank below which
267    /// the warning activates.
268    ///
269    /// Type: float
270    pub const AIR_PRESSURE_WARNING: Attribute<f32> =
271        Attribute::new(c"brake.air.pressure.warning", INITIAL);
272
273    /// Pressure of the air in the tank below which
274    /// the emergency brakes activate.
275    ///
276    /// Type: float
277    pub const AIR_PRESSURE_EMERGENCY: Attribute<f32> =
278        Attribute::new(c"brake.air.pressure.emergency", ETS2_1_01_ATS_1_00);
279
280    /// Pressure of the oil below which the warning activates.
281    ///
282    /// Type: float
283    pub const OIL_PRESSURE_WARNING: Attribute<f32> =
284        Attribute::new(c"oil.pressure.warning", INITIAL);
285
286    /// Temperature of the water above which the warning activates.
287    ///
288    /// Type: float
289    pub const WATER_TEMPERATURE_WARNING: Attribute<f32> =
290        Attribute::new(c"water.temperature.warning", INITIAL);
291
292    /// Voltage of the battery below which the warning activates.
293    ///
294    /// Type: float
295    pub const BATTERY_VOLTAGE_WARNING: Attribute<f32> =
296        Attribute::new(c"battery.voltage.warning", INITIAL);
297
298    /// Maximum rpm value.
299    ///
300    /// Type: float
301    pub const RPM_LIMIT: Attribute<f32> = Attribute::new(c"rpm.limit", INITIAL);
302
303    /// Number of forward gears on undamaged truck.
304    ///
305    /// Type: u32
306    pub const FORWARD_GEAR_COUNT: Attribute<u32> = Attribute::new(c"gears.forward", INITIAL);
307
308    /// Number of reversee gears on undamaged truck.
309    ///
310    /// Type: u32
311    pub const REVERSE_GEAR_COUNT: Attribute<u32> = Attribute::new(c"gears.reverse", INITIAL);
312
313    /// Differential ratio of the truck.
314    ///
315    /// Type: float
316    pub const DIFFERENTIAL_RATIO: Attribute<f32> =
317        Attribute::new(c"differential.ratio", ETS2_1_12_ATS_1_00);
318
319    /// Number of steps in the retarder.
320    ///
321    /// Set to zero if retarder is not mounted to the truck.
322    ///
323    /// Type: u32
324    pub const RETARDER_STEP_COUNT: Attribute<u32> = Attribute::new(c"retarder.steps", INITIAL);
325
326    /// Forward transmission ratios.
327    ///
328    /// Type: indexed float
329    pub const FORWARD_RATIO: Attribute<f32> =
330        Attribute::indexed(c"forward.ratio", ETS2_1_12_ATS_1_00);
331
332    /// Reverse transmission ratios.
333    ///
334    /// Type: indexed float
335    pub const REVERSE_RATIO: Attribute<f32> =
336        Attribute::indexed(c"reverse.ratio", ETS2_1_12_ATS_1_00);
337
338    /// Position of the cabin in the vehicle space.
339    ///
340    /// This is position of the joint around which the cabin rotates.
341    /// This attribute might be not present if the vehicle does not
342    /// have a separate cabin.
343    ///
344    /// Type: fvector
345    pub const CABIN_POSITION: Attribute<crate::FVector> =
346        Attribute::new(c"cabin.position", INITIAL);
347
348    /// Default position of the head in the cabin space.
349    ///
350    /// Type: fvector
351    pub const HEAD_POSITION: Attribute<crate::FVector> = Attribute::new(c"head.position", INITIAL);
352
353    /// Position of the trailer connection hook in vehicle
354    /// space.
355    ///
356    /// Type: fvector
357    pub const HOOK_POSITION: Attribute<crate::FVector> = Attribute::new(c"hook.position", INITIAL);
358
359    /// Number of wheels
360    ///
361    /// Type: u32
362    pub const WHEEL_COUNT: Attribute<u32> = Attribute::new(c"wheels.count", INITIAL);
363
364    /// Position of respective wheels in the vehicle space.
365    ///
366    /// Type: indexed fvector
367    pub const WHEEL_POSITION: Attribute<crate::FVector> =
368        Attribute::indexed(c"wheel.position", INITIAL);
369
370    /// Is the wheel steerable?
371    ///
372    /// Type: indexed bool
373    pub const WHEEL_STEERABLE: Attribute<bool> = Attribute::indexed(c"wheel.steerable", INITIAL);
374
375    /// Is the wheel physicaly simulated?
376    ///
377    /// Type: indexed bool
378    pub const WHEEL_SIMULATED: Attribute<bool> = Attribute::indexed(c"wheel.simulated", INITIAL);
379
380    /// Radius of the wheel
381    ///
382    /// Type: indexed float
383    pub const WHEEL_RADIUS: Attribute<f32> = Attribute::indexed(c"wheel.radius", INITIAL);
384
385    /// Is the wheel powered?
386    ///
387    /// Type: indexed bool
388    pub const WHEEL_POWERED: Attribute<bool> =
389        Attribute::indexed(c"wheel.powered", ETS2_1_10_ATS_1_00);
390
391    /// Is the wheel liftable?
392    ///
393    /// Type: indexed bool
394    pub const WHEEL_LIFTABLE: Attribute<bool> =
395        Attribute::indexed(c"wheel.liftable", ETS2_1_10_ATS_1_00);
396
397    /// Number of selectors (e.g. range/splitter toggles).
398    ///
399    /// Type: u32
400    pub const SELECTOR_COUNT: Attribute<u32> = Attribute::new(c"selector.count", INITIAL);
401
402    /// Gear selected when requirements for this h-shifter slot are meet.
403    ///
404    /// Type: indexed s32
405    pub const SLOT_GEAR: Attribute<i32> = Attribute::indexed(c"slot.gear", INITIAL);
406
407    /// Position of h-shifter handle.
408    ///
409    /// Zero corresponds to neutral position. Mapping to physical position of
410    /// the handle depends on input setup.
411    ///
412    /// Type: indexed u32
413    pub const SLOT_HANDLE_POSITION: Attribute<u32> =
414        Attribute::indexed(c"slot.handle.position", INITIAL);
415
416    /// Bitmask of required on/off state of selectors.
417    ///
418    /// Only first `selector_count` bits are relevant.
419    ///
420    /// Type: indexed u32
421    pub const SLOT_SELECTORS: Attribute<u32> = Attribute::indexed(c"slot.selectors", INITIAL);
422
423    /// Type of the shifter.
424    ///
425    /// One from `SCS_SHIFTER_TYPE`_* values.
426    ///
427    /// Type: string
428    pub const SHIFTER_TYPE: Attribute<crate::StringValue> =
429        Attribute::new(c"shifter.type", INITIAL);
430
431    /// Id of the cargo for internal use by code.
432    ///
433    /// Limited to C-identifier characters and dots.
434    ///
435    /// Type: string
436    pub const CARGO_ID: Attribute<crate::StringValue> =
437        Attribute::new(c"cargo.id", ETS2_1_09_ATS_1_00);
438
439    /// Name of the cargo for display purposes.
440    ///
441    /// Localized using the current in-game language.
442    ///
443    /// Type: string
444    pub const CARGO: Attribute<crate::StringValue> = Attribute::new(c"cargo", ETS2_1_09_ATS_1_00);
445
446    /// Mass of the cargo in kilograms.
447    ///
448    /// Type: float
449    pub const CARGO_MASS: Attribute<f32> = Attribute::new(c"cargo.mass", ETS2_1_09_ATS_1_00);
450
451    /// Mass of the single unit of the cargo in kilograms.
452    ///
453    /// Type: float
454    pub const CARGO_UNIT_MASS: Attribute<f32> =
455        Attribute::new(c"cargo.unit.mass", ETS2_1_14_ATS_1_01);
456
457    /// How many units of the cargo the job consist of.
458    ///
459    /// Type: u32
460    pub const CARGO_UNIT_COUNT: Attribute<u32> =
461        Attribute::new(c"cargo.unit.count", ETS2_1_14_ATS_1_01);
462
463    /// Id of the destination city for internal use by code.
464    ///
465    /// Limited to C-identifier characters and dots.
466    ///
467    /// Type: string
468    pub const DESTINATION_CITY_ID: Attribute<crate::StringValue> =
469        Attribute::new(c"destination.city.id", ETS2_1_09_ATS_1_00);
470
471    /// Name of the destination city for display purposes.
472    ///
473    /// Localized using the current in-game language.
474    ///
475    /// Type: string
476    pub const DESTINATION_CITY: Attribute<crate::StringValue> =
477        Attribute::new(c"destination.city", ETS2_1_09_ATS_1_00);
478
479    /// Id of the destination company for internal use by code.
480    ///
481    /// Limited to C-identifier characters and dots.
482    ///
483    /// Type: string
484    pub const DESTINATION_COMPANY_ID: Attribute<crate::StringValue> =
485        Attribute::new(c"destination.company.id", ETS2_1_09_ATS_1_00);
486
487    /// Name of the destination company for display purposes.
488    ///
489    /// Localized using the current in-game language.
490    ///
491    /// Type: string
492    pub const DESTINATION_COMPANY: Attribute<crate::StringValue> =
493        Attribute::new(c"destination.company", ETS2_1_09_ATS_1_00);
494
495    /// Id of the source city for internal use by code.
496    ///
497    /// Limited to C-identifier characters and dots.
498    ///
499    /// Type: string
500    pub const SOURCE_CITY_ID: Attribute<crate::StringValue> =
501        Attribute::new(c"source.city.id", ETS2_1_09_ATS_1_00);
502
503    /// Name of the source city for display purposes.
504    ///
505    /// Localized using the current in-game language.
506    ///
507    /// Type: string
508    pub const SOURCE_CITY: Attribute<crate::StringValue> =
509        Attribute::new(c"source.city", ETS2_1_09_ATS_1_00);
510
511    /// Id of the source company for internal use by code.
512    ///
513    /// Limited to C-identifier characters and dots.
514    ///
515    /// Type: string
516    pub const SOURCE_COMPANY_ID: Attribute<crate::StringValue> =
517        Attribute::new(c"source.company.id", ETS2_1_09_ATS_1_00);
518
519    /// Name of the source company for display purposes.
520    ///
521    /// Localized using the current in-game language.
522    ///
523    /// Type: string
524    pub const SOURCE_COMPANY: Attribute<crate::StringValue> =
525        Attribute::new(c"source.company", ETS2_1_09_ATS_1_00);
526
527    /// Reward in internal game-specific currency.
528    ///
529    /// For detailed information about the currency see "Game specific units"
530    /// documentation in `scssdk_telemetry`_<`game_id>.h`
531    ///
532    /// Type: u64
533    pub const INCOME: Attribute<u64> = Attribute::new(c"income", ETS2_1_09_ATS_1_00);
534
535    /// Absolute in-game time of end of job delivery window.
536    ///
537    /// Delivering the job after this time will cause it be late.
538    ///
539    /// See `SCS_TELEMETRY_CHANNEL_game_time` for more info about absolute time.
540    /// Time remaining for delivery can be obtained like (`delivery_time` - `game_time`).
541    ///
542    /// Type: u32
543    pub const DELIVERY_TIME: Attribute<u32> = Attribute::new(c"delivery.time", ETS2_1_09_ATS_1_00);
544
545    /// Planned job distance in simulated kilometers.
546    ///
547    /// Does not include distance driven using ferry.
548    ///
549    /// Type: u32
550    pub const PLANNED_DISTANCE_KM: Attribute<u32> =
551        Attribute::new(c"planned_distance.km", ETS2_1_15_ATS_1_02);
552
553    /// Is cargo loaded on the trailer?
554    ///
555    /// For non cargo market jobs this is always true
556    ///
557    /// Type: bool
558    pub const IS_CARGO_LOADED: Attribute<bool> =
559        Attribute::new(c"cargo.loaded", ETS2_1_14_ATS_1_01);
560
561    /// The job market this job is from.
562    ///
563    /// The value is a string representing the type of the job market.
564    /// Possible values:
565    /// - `cargo_market`
566    /// - `quick_job`
567    /// - `freight_market`
568    /// - `external_contracts`
569    /// - `external_market`
570    ///
571    /// Type: string
572    pub const JOB_MARKET: Attribute<crate::StringValue> =
573        Attribute::new(c"job.market", ETS2_1_14_ATS_1_01);
574
575    /// Flag indicating that the job is special transport job.
576    ///
577    /// Type: bool
578    pub const SPECIAL_JOB: Attribute<bool> = Attribute::new(c"is.special.job", ETS2_1_14_ATS_1_01);
579
580    /// Every public configuration attribute from the SDK 1.14 headers.
581    ///
582    /// Type erasure exists only to make the heterogeneous catalog enumerable.
583    /// Each entry retains its exact SCS value type and indexed/scalar lookup
584    /// mode; callback code should continue using the individual typed constants
585    /// for decoding.
586    pub const ALL: [AnyAttribute; COUNT] = [
587        BRAND_ID.erase(),
588        BRAND.erase(),
589        ID.erase(),
590        CARGO_ACCESSORY_ID.erase(),
591        CHAIN_TYPE.erase(),
592        BODY_TYPE.erase(),
593        LICENSE_PLATE.erase(),
594        LICENSE_PLATE_COUNTRY_ID.erase(),
595        LICENSE_PLATE_COUNTRY.erase(),
596        NAME.erase(),
597        FUEL_CAPACITY.erase(),
598        FUEL_WARNING_FACTOR.erase(),
599        ADBLUE_CAPACITY.erase(),
600        ADBLUE_WARNING_FACTOR.erase(),
601        AIR_PRESSURE_WARNING.erase(),
602        AIR_PRESSURE_EMERGENCY.erase(),
603        OIL_PRESSURE_WARNING.erase(),
604        WATER_TEMPERATURE_WARNING.erase(),
605        BATTERY_VOLTAGE_WARNING.erase(),
606        RPM_LIMIT.erase(),
607        FORWARD_GEAR_COUNT.erase(),
608        REVERSE_GEAR_COUNT.erase(),
609        DIFFERENTIAL_RATIO.erase(),
610        RETARDER_STEP_COUNT.erase(),
611        FORWARD_RATIO.erase(),
612        REVERSE_RATIO.erase(),
613        CABIN_POSITION.erase(),
614        HEAD_POSITION.erase(),
615        HOOK_POSITION.erase(),
616        WHEEL_COUNT.erase(),
617        WHEEL_POSITION.erase(),
618        WHEEL_STEERABLE.erase(),
619        WHEEL_SIMULATED.erase(),
620        WHEEL_RADIUS.erase(),
621        WHEEL_POWERED.erase(),
622        WHEEL_LIFTABLE.erase(),
623        SELECTOR_COUNT.erase(),
624        SLOT_GEAR.erase(),
625        SLOT_HANDLE_POSITION.erase(),
626        SLOT_SELECTORS.erase(),
627        SHIFTER_TYPE.erase(),
628        CARGO_ID.erase(),
629        CARGO.erase(),
630        CARGO_MASS.erase(),
631        CARGO_UNIT_MASS.erase(),
632        CARGO_UNIT_COUNT.erase(),
633        DESTINATION_CITY_ID.erase(),
634        DESTINATION_CITY.erase(),
635        DESTINATION_COMPANY_ID.erase(),
636        DESTINATION_COMPANY.erase(),
637        SOURCE_CITY_ID.erase(),
638        SOURCE_CITY.erase(),
639        SOURCE_COMPANY_ID.erase(),
640        SOURCE_COMPANY.erase(),
641        INCOME.erase(),
642        DELIVERY_TIME.erase(),
643        PLANNED_DISTANCE_KM.erase(),
644        IS_CARGO_LOADED.erase(),
645        JOB_MARKET.erase(),
646        SPECIAL_JOB.erase(),
647    ];
648}
649
650/// Configuration-group membership for every typed attribute.
651///
652/// The common header declares attribute names in one flat namespace, while
653/// configuration callbacks deliver subsets of those names under a specific
654/// group ID. This catalog records that missing relational layer. It is grouped
655/// in configuration-header order; attributes within each group follow the
656/// documented list and then the attribute declaration order for official
657/// values which the prose list omits (notably transmission and wheel data).
658pub mod associations {
659    use super::{
660        ETS2_1_01_ATS_1_00, ETS2_1_09_ATS_1_00, ETS2_1_10_ATS_1_00, ETS2_1_12_ATS_1_00,
661        ETS2_1_14_ATS_1_01, ETS2_1_15_ATS_1_02, INITIAL, attributes, ids,
662    };
663    use crate::{
664        Attribute, ConfigurationAttributeAssociation, ConfigurationId, GameSchemaAvailability,
665        SdkValue,
666    };
667
668    /// Total number of configuration-to-attribute relationships.
669    ///
670    /// This is larger than the 60-name attribute catalog because identifiers
671    /// such as `id`, `brand`, license-plate data, hook position, and wheel
672    /// geometry occur in more than one configuration group.
673    pub const COUNT: usize = 71;
674
675    const fn association<T: SdkValue>(
676        configuration: ConfigurationId,
677        attribute: Attribute<T>,
678        availability: GameSchemaAvailability,
679    ) -> ConfigurationAttributeAssociation {
680        ConfigurationAttributeAssociation::new(configuration, attribute, availability)
681    }
682
683    /// Attributes carried by the `substances` configuration.
684    pub const SUBSTANCES: [ConfigurationAttributeAssociation; 1] =
685        [association(ids::SUBSTANCES, attributes::ID, INITIAL)];
686
687    /// Attributes carried by the static `controls` configuration.
688    pub const CONTROLS: [ConfigurationAttributeAssociation; 1] = [association(
689        ids::CONTROLS,
690        attributes::SHIFTER_TYPE,
691        INITIAL,
692    )];
693
694    /// Attributes carried by the H-shifter configuration.
695    pub const HSHIFTER: [ConfigurationAttributeAssociation; 4] = [
696        association(ids::HSHIFTER, attributes::SELECTOR_COUNT, INITIAL),
697        association(ids::HSHIFTER, attributes::SLOT_GEAR, INITIAL),
698        association(ids::HSHIFTER, attributes::SLOT_HANDLE_POSITION, INITIAL),
699        association(ids::HSHIFTER, attributes::SLOT_SELECTORS, INITIAL),
700    ];
701
702    /// Attributes carried by the static truck configuration.
703    ///
704    /// The header's short "Supported attributes" prose predates several flat
705    /// attribute macros and was not kept exhaustive. The game-specific schema
706    /// changelog explicitly associates transmission information with the truck,
707    /// while the wheel descriptors use the truck wheel index domain. Those
708    /// official relationships are retained here instead of repeating the
709    /// incomplete prose list verbatim.
710    pub const TRUCK: [ConfigurationAttributeAssociation; 33] = [
711        association(ids::TRUCK, attributes::BRAND_ID, INITIAL),
712        association(ids::TRUCK, attributes::BRAND, INITIAL),
713        association(ids::TRUCK, attributes::ID, INITIAL),
714        association(ids::TRUCK, attributes::NAME, INITIAL),
715        association(ids::TRUCK, attributes::FUEL_CAPACITY, INITIAL),
716        association(ids::TRUCK, attributes::FUEL_WARNING_FACTOR, INITIAL),
717        association(ids::TRUCK, attributes::ADBLUE_CAPACITY, ETS2_1_12_ATS_1_00),
718        association(
719            ids::TRUCK,
720            attributes::ADBLUE_WARNING_FACTOR,
721            ETS2_1_12_ATS_1_00,
722        ),
723        association(ids::TRUCK, attributes::AIR_PRESSURE_WARNING, INITIAL),
724        association(
725            ids::TRUCK,
726            attributes::AIR_PRESSURE_EMERGENCY,
727            ETS2_1_01_ATS_1_00,
728        ),
729        association(ids::TRUCK, attributes::OIL_PRESSURE_WARNING, INITIAL),
730        association(ids::TRUCK, attributes::WATER_TEMPERATURE_WARNING, INITIAL),
731        association(ids::TRUCK, attributes::BATTERY_VOLTAGE_WARNING, INITIAL),
732        association(ids::TRUCK, attributes::RPM_LIMIT, INITIAL),
733        association(ids::TRUCK, attributes::FORWARD_GEAR_COUNT, INITIAL),
734        association(ids::TRUCK, attributes::REVERSE_GEAR_COUNT, INITIAL),
735        association(
736            ids::TRUCK,
737            attributes::DIFFERENTIAL_RATIO,
738            ETS2_1_12_ATS_1_00,
739        ),
740        association(ids::TRUCK, attributes::RETARDER_STEP_COUNT, INITIAL),
741        association(ids::TRUCK, attributes::FORWARD_RATIO, ETS2_1_12_ATS_1_00),
742        association(ids::TRUCK, attributes::REVERSE_RATIO, ETS2_1_12_ATS_1_00),
743        association(ids::TRUCK, attributes::CABIN_POSITION, INITIAL),
744        association(ids::TRUCK, attributes::HEAD_POSITION, INITIAL),
745        association(ids::TRUCK, attributes::HOOK_POSITION, INITIAL),
746        association(ids::TRUCK, attributes::LICENSE_PLATE, ETS2_1_14_ATS_1_01),
747        association(
748            ids::TRUCK,
749            attributes::LICENSE_PLATE_COUNTRY,
750            ETS2_1_14_ATS_1_01,
751        ),
752        association(
753            ids::TRUCK,
754            attributes::LICENSE_PLATE_COUNTRY_ID,
755            ETS2_1_14_ATS_1_01,
756        ),
757        association(ids::TRUCK, attributes::WHEEL_COUNT, INITIAL),
758        association(ids::TRUCK, attributes::WHEEL_POSITION, INITIAL),
759        association(ids::TRUCK, attributes::WHEEL_STEERABLE, INITIAL),
760        association(ids::TRUCK, attributes::WHEEL_SIMULATED, INITIAL),
761        association(ids::TRUCK, attributes::WHEEL_RADIUS, INITIAL),
762        association(ids::TRUCK, attributes::WHEEL_POWERED, ETS2_1_10_ATS_1_00),
763        association(ids::TRUCK, attributes::WHEEL_LIFTABLE, ETS2_1_10_ATS_1_00),
764    ];
765
766    /// Attributes carried by the legacy `trailer` configuration and by each
767    /// numbered trailer configuration once that namespace is available.
768    ///
769    /// For `trailer.[index]`, consumers must additionally apply
770    /// [`crate::game::capabilities::MULTI_TRAILER`]. The relationship minima
771    /// below still describe when each attribute joined the trailer schema, so
772    /// the legacy unnumbered alias is represented accurately as well.
773    pub const TRAILER: [ConfigurationAttributeAssociation; 13] = [
774        association(ids::TRAILER, attributes::ID, INITIAL),
775        association(ids::TRAILER, attributes::CARGO_ACCESSORY_ID, INITIAL),
776        association(ids::TRAILER, attributes::HOOK_POSITION, INITIAL),
777        association(ids::TRAILER, attributes::BRAND_ID, ETS2_1_14_ATS_1_01),
778        association(ids::TRAILER, attributes::BRAND, ETS2_1_14_ATS_1_01),
779        association(ids::TRAILER, attributes::NAME, ETS2_1_14_ATS_1_01),
780        association(ids::TRAILER, attributes::CHAIN_TYPE, ETS2_1_14_ATS_1_01),
781        association(ids::TRAILER, attributes::BODY_TYPE, ETS2_1_14_ATS_1_01),
782        association(ids::TRAILER, attributes::LICENSE_PLATE, ETS2_1_14_ATS_1_01),
783        association(
784            ids::TRAILER,
785            attributes::LICENSE_PLATE_COUNTRY,
786            ETS2_1_14_ATS_1_01,
787        ),
788        association(
789            ids::TRAILER,
790            attributes::LICENSE_PLATE_COUNTRY_ID,
791            ETS2_1_14_ATS_1_01,
792        ),
793        association(ids::TRAILER, attributes::WHEEL_COUNT, INITIAL),
794        association(ids::TRAILER, attributes::WHEEL_POSITION, INITIAL),
795    ];
796
797    /// Attributes carried by the active-job configuration.
798    pub const JOB: [ConfigurationAttributeAssociation; 19] = [
799        association(ids::JOB, attributes::CARGO_ID, ETS2_1_09_ATS_1_00),
800        association(ids::JOB, attributes::CARGO, ETS2_1_09_ATS_1_00),
801        association(ids::JOB, attributes::CARGO_MASS, ETS2_1_09_ATS_1_00),
802        association(ids::JOB, attributes::CARGO_UNIT_MASS, ETS2_1_14_ATS_1_01),
803        association(ids::JOB, attributes::CARGO_UNIT_COUNT, ETS2_1_14_ATS_1_01),
804        association(
805            ids::JOB,
806            attributes::DESTINATION_CITY_ID,
807            ETS2_1_09_ATS_1_00,
808        ),
809        association(ids::JOB, attributes::DESTINATION_CITY, ETS2_1_09_ATS_1_00),
810        association(ids::JOB, attributes::SOURCE_CITY_ID, ETS2_1_09_ATS_1_00),
811        association(ids::JOB, attributes::SOURCE_CITY, ETS2_1_09_ATS_1_00),
812        association(
813            ids::JOB,
814            attributes::DESTINATION_COMPANY_ID,
815            ETS2_1_09_ATS_1_00,
816        ),
817        association(
818            ids::JOB,
819            attributes::DESTINATION_COMPANY,
820            ETS2_1_09_ATS_1_00,
821        ),
822        association(ids::JOB, attributes::SOURCE_COMPANY_ID, ETS2_1_09_ATS_1_00),
823        association(ids::JOB, attributes::SOURCE_COMPANY, ETS2_1_09_ATS_1_00),
824        association(ids::JOB, attributes::INCOME, ETS2_1_09_ATS_1_00),
825        association(ids::JOB, attributes::DELIVERY_TIME, ETS2_1_09_ATS_1_00),
826        association(ids::JOB, attributes::IS_CARGO_LOADED, ETS2_1_14_ATS_1_01),
827        association(ids::JOB, attributes::JOB_MARKET, ETS2_1_14_ATS_1_01),
828        association(ids::JOB, attributes::SPECIAL_JOB, ETS2_1_14_ATS_1_01),
829        association(
830            ids::JOB,
831            attributes::PLANNED_DISTANCE_KM,
832            ETS2_1_15_ATS_1_02,
833        ),
834    ];
835
836    /// Every official configuration-to-attribute relationship in SDK 1.14.
837    pub const ALL: [ConfigurationAttributeAssociation; COUNT] = [
838        SUBSTANCES[0],
839        CONTROLS[0],
840        HSHIFTER[0],
841        HSHIFTER[1],
842        HSHIFTER[2],
843        HSHIFTER[3],
844        TRUCK[0],
845        TRUCK[1],
846        TRUCK[2],
847        TRUCK[3],
848        TRUCK[4],
849        TRUCK[5],
850        TRUCK[6],
851        TRUCK[7],
852        TRUCK[8],
853        TRUCK[9],
854        TRUCK[10],
855        TRUCK[11],
856        TRUCK[12],
857        TRUCK[13],
858        TRUCK[14],
859        TRUCK[15],
860        TRUCK[16],
861        TRUCK[17],
862        TRUCK[18],
863        TRUCK[19],
864        TRUCK[20],
865        TRUCK[21],
866        TRUCK[22],
867        TRUCK[23],
868        TRUCK[24],
869        TRUCK[25],
870        TRUCK[26],
871        TRUCK[27],
872        TRUCK[28],
873        TRUCK[29],
874        TRUCK[30],
875        TRUCK[31],
876        TRUCK[32],
877        TRAILER[0],
878        TRAILER[1],
879        TRAILER[2],
880        TRAILER[3],
881        TRAILER[4],
882        TRAILER[5],
883        TRAILER[6],
884        TRAILER[7],
885        TRAILER[8],
886        TRAILER[9],
887        TRAILER[10],
888        TRAILER[11],
889        TRAILER[12],
890        JOB[0],
891        JOB[1],
892        JOB[2],
893        JOB[3],
894        JOB[4],
895        JOB[5],
896        JOB[6],
897        JOB[7],
898        JOB[8],
899        JOB[9],
900        JOB[10],
901        JOB[11],
902        JOB[12],
903        JOB[13],
904        JOB[14],
905        JOB[15],
906        JOB[16],
907        JOB[17],
908        JOB[18],
909    ];
910}
911
912/// Documented values of the shifter.type configuration attribute.
913#[derive(Clone, Copy, Debug, PartialEq, Eq)]
914pub enum ShifterType {
915    /// Simplified arcade transmission.
916    Arcade,
917    /// Automatic transmission.
918    Automatic,
919    /// Manual transmission controlled without an H-shifter.
920    Manual,
921    /// Physical H-pattern shifter.
922    HShifter,
923}
924
925impl ShifterType {
926    /// Number of shifter values declared by the official header.
927    pub const COUNT: usize = 4;
928
929    /// Every documented shifter type in header order.
930    pub const ALL: [Self; Self::COUNT] =
931        [Self::Arcade, Self::Automatic, Self::Manual, Self::HShifter];
932
933    /// First game schemas which provide the shifter-type configuration value.
934    #[must_use]
935    pub const fn availability(self) -> GameSchemaAvailability {
936        let _ = self;
937        INITIAL
938    }
939
940    /// Canonical Rust string used by the SDK.
941    #[must_use]
942    pub const fn as_str(self) -> &'static str {
943        match self {
944            Self::Arcade => "arcade",
945            Self::Automatic => "automatic",
946            Self::Manual => "manual",
947            Self::HShifter => "hshifter",
948        }
949    }
950
951    #[must_use]
952    pub const fn as_c_str(self) -> &'static CStr {
953        match self {
954            Self::Arcade => c"arcade",
955            Self::Automatic => c"automatic",
956            Self::Manual => c"manual",
957            Self::HShifter => c"hshifter",
958        }
959    }
960
961    #[must_use]
962    pub fn from_c_str(value: &CStr) -> Option<Self> {
963        value.to_str().ok()?.parse().ok()
964    }
965}
966
967impl FromStr for ShifterType {
968    type Err = UnknownStringValue;
969
970    fn from_str(value: &str) -> Result<Self, Self::Err> {
971        match value {
972            "arcade" => Ok(Self::Arcade),
973            "automatic" => Ok(Self::Automatic),
974            "manual" => Ok(Self::Manual),
975            "hshifter" => Ok(Self::HShifter),
976            _ => Err(UnknownStringValue),
977        }
978    }
979}
980
981/// Documented values of the `job.market` configuration attribute.
982///
983/// Unknown future strings remain available through the generic typed string
984/// attribute API. This enum intentionally recognizes only the values listed by
985/// the vendored SDK 1.14 header, allowing callers to distinguish a known market
986/// from a future additive value without losing the original text.
987#[derive(Clone, Copy, Debug, PartialEq, Eq)]
988pub enum JobMarket {
989    CargoMarket,
990    QuickJob,
991    FreightMarket,
992    ExternalContracts,
993    ExternalMarket,
994}
995
996impl JobMarket {
997    /// Number of job-market strings documented by SDK 1.14.
998    pub const COUNT: usize = 5;
999
1000    /// Every documented job-market value in header order.
1001    pub const ALL: [Self; Self::COUNT] = [
1002        Self::CargoMarket,
1003        Self::QuickJob,
1004        Self::FreightMarket,
1005        Self::ExternalContracts,
1006        Self::ExternalMarket,
1007    ];
1008
1009    /// First game schemas which provide the `job.market` value.
1010    #[must_use]
1011    pub const fn availability(self) -> GameSchemaAvailability {
1012        let _ = self;
1013        ETS2_1_14_ATS_1_01
1014    }
1015
1016    /// Canonical Rust string used by the SDK.
1017    #[must_use]
1018    pub const fn as_str(self) -> &'static str {
1019        match self {
1020            Self::CargoMarket => "cargo_market",
1021            Self::QuickJob => "quick_job",
1022            Self::FreightMarket => "freight_market",
1023            Self::ExternalContracts => "external_contracts",
1024            Self::ExternalMarket => "external_market",
1025        }
1026    }
1027
1028    /// Canonical NUL-terminated string used by the C SDK.
1029    #[must_use]
1030    pub const fn as_c_str(self) -> &'static CStr {
1031        match self {
1032            Self::CargoMarket => c"cargo_market",
1033            Self::QuickJob => c"quick_job",
1034            Self::FreightMarket => c"freight_market",
1035            Self::ExternalContracts => c"external_contracts",
1036            Self::ExternalMarket => c"external_market",
1037        }
1038    }
1039
1040    /// Parses one canonical job-market value from an SDK C string.
1041    #[must_use]
1042    pub fn from_c_str(value: &CStr) -> Option<Self> {
1043        value.to_str().ok()?.parse().ok()
1044    }
1045}
1046
1047impl FromStr for JobMarket {
1048    type Err = UnknownStringValue;
1049
1050    fn from_str(value: &str) -> Result<Self, Self::Err> {
1051        match value {
1052            "cargo_market" => Ok(Self::CargoMarket),
1053            "quick_job" => Ok(Self::QuickJob),
1054            "freight_market" => Ok(Self::FreightMarket),
1055            "external_contracts" => Ok(Self::ExternalContracts),
1056            "external_market" => Ok(Self::ExternalMarket),
1057            _ => Err(UnknownStringValue),
1058        }
1059    }
1060}
1061
1062const _: [(); 6] = [(); ids::COUNT];
1063const _: [(); 60] = [(); attributes::COUNT];
1064const _: [(); 71] = [(); associations::COUNT];
1065const _: [(); 4] = [(); ShifterType::COUNT];
1066const _: [(); 5] = [(); JobMarket::COUNT];
1067
1068#[cfg(test)]
1069mod tests {
1070    use super::*;
1071    use crate::GameSchemaVersion;
1072
1073    fn assert_since<T: crate::SdkValue>(
1074        attribute: crate::Attribute<T>,
1075        ets2: GameSchemaVersion,
1076        ats: GameSchemaVersion,
1077    ) {
1078        assert_eq!(attribute.availability().available_since_ets2(), Some(ets2));
1079        assert_eq!(attribute.availability().available_since_ats(), Some(ats));
1080    }
1081
1082    #[test]
1083    fn typed_catalog_counts_match_the_raw_sdk_catalog() {
1084        assert_eq!(ids::COUNT, crate::sys::configuration::ids::COUNT);
1085        assert_eq!(
1086            attributes::COUNT,
1087            crate::sys::configuration::attributes::COUNT
1088        );
1089        assert_eq!(ids::COUNT, 6);
1090        assert_eq!(attributes::COUNT, 60);
1091        assert_eq!(MAX_TRAILERS, crate::sys::configuration::MAX_TRAILERS);
1092        assert_eq!(associations::COUNT, 71);
1093        assert_eq!(associations::SUBSTANCES.len(), 1);
1094        assert_eq!(associations::CONTROLS.len(), 1);
1095        assert_eq!(associations::HSHIFTER.len(), 4);
1096        assert_eq!(associations::TRUCK.len(), 33);
1097        assert_eq!(associations::TRAILER.len(), 13);
1098        assert_eq!(associations::JOB.len(), 19);
1099    }
1100
1101    #[test]
1102    fn association_catalog_has_complete_membership_without_duplicate_pairs() {
1103        for association in associations::ALL {
1104            assert!(
1105                ids::ALL.contains(&association.configuration()),
1106                "association uses unknown configuration: {:?}",
1107                association.configuration().name()
1108            );
1109            assert!(
1110                attributes::ALL.contains(&association.attribute()),
1111                "association uses unknown attribute: {:?}",
1112                association.attribute().name()
1113            );
1114        }
1115
1116        for attribute in attributes::ALL {
1117            assert!(
1118                associations::ALL
1119                    .iter()
1120                    .any(|association| association.attribute() == attribute),
1121                "attribute has no configuration membership: {:?}",
1122                attribute.name()
1123            );
1124        }
1125
1126        for (position, association) in associations::ALL.iter().enumerate() {
1127            assert!(
1128                associations::ALL[..position].iter().all(|earlier| {
1129                    earlier.configuration() != association.configuration()
1130                        || earlier.attribute() != association.attribute()
1131                }),
1132                "duplicate configuration association: {:?} -> {:?}",
1133                association.configuration().name(),
1134                association.attribute().name()
1135            );
1136        }
1137    }
1138
1139    #[test]
1140    fn association_catalog_preserves_configuration_group_order() {
1141        let groups = [
1142            (ids::SUBSTANCES, 0..1),
1143            (ids::CONTROLS, 1..2),
1144            (ids::HSHIFTER, 2..6),
1145            (ids::TRUCK, 6..39),
1146            (ids::TRAILER, 39..52),
1147            (ids::JOB, 52..71),
1148        ];
1149
1150        for (configuration, range) in groups {
1151            assert!(
1152                associations::ALL[range]
1153                    .iter()
1154                    .all(|association| association.configuration() == configuration)
1155            );
1156        }
1157
1158        assert_eq!(
1159            associations::SUBSTANCES[0].attribute(),
1160            attributes::ID.erase()
1161        );
1162        assert_eq!(
1163            associations::CONTROLS[0].attribute(),
1164            attributes::SHIFTER_TYPE.erase()
1165        );
1166        assert_eq!(
1167            associations::TRUCK[16].attribute(),
1168            attributes::DIFFERENTIAL_RATIO.erase()
1169        );
1170        assert_eq!(
1171            associations::TRAILER[3].attribute(),
1172            attributes::BRAND_ID.erase()
1173        );
1174        assert_eq!(
1175            associations::JOB[18].attribute(),
1176            attributes::PLANNED_DISTANCE_KM.erase()
1177        );
1178    }
1179
1180    #[test]
1181    fn association_availability_is_never_earlier_than_its_descriptors() {
1182        for association in associations::ALL {
1183            let availability = association.availability();
1184            let configuration = association.configuration().availability();
1185            let attribute = association.attribute().availability();
1186
1187            assert!(matches!(
1188                (
1189                    availability.available_since_ets2(),
1190                    configuration.available_since_ets2(),
1191                    attribute.available_since_ets2()
1192                ),
1193                (Some(relation), Some(group), Some(value)) if relation >= group && relation >= value
1194            ));
1195            assert!(matches!(
1196                (
1197                    availability.available_since_ats(),
1198                    configuration.available_since_ats(),
1199                    attribute.available_since_ats()
1200                ),
1201                (Some(relation), Some(group), Some(value)) if relation >= group && relation >= value
1202            ));
1203        }
1204    }
1205
1206    #[test]
1207    fn shared_attribute_relationships_keep_independent_history() {
1208        assert_eq!(
1209            associations::TRUCK[0].availability(),
1210            INITIAL,
1211            "truck brand_id belongs to the initial truck schema"
1212        );
1213        assert_eq!(
1214            associations::TRAILER[3].availability(),
1215            game::capabilities::MULTI_TRAILER,
1216            "trailer brand_id arrived with the multi-trailer schema"
1217        );
1218        assert_eq!(
1219            associations::TRAILER[0].availability(),
1220            INITIAL,
1221            "the legacy trailer id relationship predates numbered trailers"
1222        );
1223        assert_eq!(
1224            associations::JOB[3].availability(),
1225            game::capabilities::MULTI_TRAILER,
1226            "cargo unit mass first appears in the same historical archive"
1227        );
1228        assert_eq!(
1229            game::capabilities::MULTI_TRAILER.available_since_ets2(),
1230            Some(game::ets2::V1_14)
1231        );
1232        assert_eq!(
1233            game::capabilities::MULTI_TRAILER.available_since_ats(),
1234            Some(game::ats::V1_01)
1235        );
1236    }
1237
1238    #[test]
1239    fn every_typed_descriptor_matches_the_raw_header_catalog() {
1240        for (descriptor, raw_name) in ids::ALL.iter().zip(crate::sys::configuration::ids::ALL) {
1241            assert_eq!(descriptor.name().to_bytes_with_nul(), raw_name);
1242        }
1243        for (descriptor, raw_name) in attributes::ALL
1244            .iter()
1245            .zip(crate::sys::configuration::attributes::ALL)
1246        {
1247            assert_eq!(descriptor.name().to_bytes_with_nul(), raw_name);
1248        }
1249
1250        for (position, descriptor) in attributes::ALL.iter().enumerate() {
1251            assert!(
1252                attributes::ALL[..position]
1253                    .iter()
1254                    .all(|earlier| earlier.name() != descriptor.name()),
1255                "duplicate configuration attribute: {:?}",
1256                descriptor.name()
1257            );
1258        }
1259        assert_eq!(
1260            attributes::ALL
1261                .iter()
1262                .filter(|attribute| attribute.is_indexed())
1263                .count(),
1264            11
1265        );
1266    }
1267
1268    #[test]
1269    fn representative_descriptors_preserve_header_metadata() {
1270        assert_eq!(ids::JOB.name(), c"job");
1271        assert_eq!(attributes::CARGO_MASS.name(), c"cargo.mass");
1272        assert_eq!(
1273            attributes::CARGO_MASS.value_type(),
1274            crate::sys::SCS_VALUE_TYPE_FLOAT
1275        );
1276        assert!(!attributes::CARGO_MASS.is_indexed());
1277        assert_eq!(attributes::WHEEL_POSITION.name(), c"wheel.position");
1278        assert!(attributes::WHEEL_POSITION.is_indexed());
1279    }
1280
1281    #[test]
1282    fn configuration_availability_follows_official_game_schema_history() {
1283        assert_eq!(
1284            ids::JOB.availability().available_since_ets2(),
1285            Some(game::ets2::V1_09)
1286        );
1287        assert_eq!(
1288            ids::JOB.availability().available_since_ats(),
1289            Some(game::ats::V1_00)
1290        );
1291        assert_since(attributes::CARGO_ID, game::ets2::V1_09, game::ats::V1_00);
1292        assert_since(attributes::INCOME, game::ets2::V1_09, game::ats::V1_00);
1293
1294        assert_since(
1295            attributes::AIR_PRESSURE_EMERGENCY,
1296            game::ets2::V1_01,
1297            game::ats::V1_00,
1298        );
1299        for attribute in [attributes::WHEEL_POWERED, attributes::WHEEL_LIFTABLE] {
1300            assert_since(attribute, game::ets2::V1_10, game::ats::V1_00);
1301        }
1302        for attribute in [
1303            attributes::ADBLUE_CAPACITY,
1304            attributes::ADBLUE_WARNING_FACTOR,
1305            attributes::DIFFERENTIAL_RATIO,
1306            attributes::FORWARD_RATIO,
1307            attributes::REVERSE_RATIO,
1308        ] {
1309            assert_since(attribute, game::ets2::V1_12, game::ats::V1_00);
1310        }
1311        for attribute in [
1312            attributes::CHAIN_TYPE,
1313            attributes::BODY_TYPE,
1314            attributes::LICENSE_PLATE,
1315            attributes::LICENSE_PLATE_COUNTRY_ID,
1316            attributes::LICENSE_PLATE_COUNTRY,
1317        ] {
1318            assert_since(attribute, game::ets2::V1_14, game::ats::V1_01);
1319        }
1320        assert_since(
1321            attributes::CARGO_UNIT_MASS,
1322            game::ets2::V1_14,
1323            game::ats::V1_01,
1324        );
1325        assert_since(
1326            attributes::CARGO_UNIT_COUNT,
1327            game::ets2::V1_14,
1328            game::ats::V1_01,
1329        );
1330        assert_since(
1331            attributes::IS_CARGO_LOADED,
1332            game::ets2::V1_14,
1333            game::ats::V1_01,
1334        );
1335        assert_since(attributes::JOB_MARKET, game::ets2::V1_14, game::ats::V1_01);
1336        assert_since(attributes::SPECIAL_JOB, game::ets2::V1_14, game::ats::V1_01);
1337        assert_since(
1338            attributes::PLANNED_DISTANCE_KM,
1339            game::ets2::V1_15,
1340            game::ats::V1_02,
1341        );
1342
1343        assert!(ids::ALL.iter().all(|descriptor| {
1344            descriptor.availability().available_since_ets2().is_some()
1345                && descriptor.availability().available_since_ats().is_some()
1346        }));
1347        assert!(attributes::ALL.iter().all(|descriptor| {
1348            descriptor.availability().available_since_ets2().is_some()
1349                && descriptor.availability().available_since_ats().is_some()
1350        }));
1351    }
1352
1353    #[test]
1354    fn every_documented_shifter_type_round_trips() {
1355        assert_eq!(ShifterType::COUNT, 4);
1356        for value in ShifterType::ALL {
1357            assert_eq!(ShifterType::from_c_str(value.as_c_str()), Some(value));
1358            assert_eq!(value.as_str().parse(), Ok(value));
1359            assert_eq!(value.availability(), INITIAL);
1360        }
1361        assert_eq!(ShifterType::from_c_str(c"future-shifter"), None);
1362        assert_eq!(
1363            "future-shifter".parse::<ShifterType>(),
1364            Err(UnknownStringValue)
1365        );
1366    }
1367
1368    #[test]
1369    fn every_documented_job_market_round_trips() {
1370        assert_eq!(JobMarket::COUNT, 5);
1371        for value in JobMarket::ALL {
1372            assert_eq!(JobMarket::from_c_str(value.as_c_str()), Some(value));
1373            assert_eq!(value.as_str().parse(), Ok(value));
1374            assert_eq!(value.availability(), ETS2_1_14_ATS_1_01);
1375        }
1376        assert_eq!(JobMarket::from_c_str(c"future_market"), None);
1377        assert_eq!(
1378            "future_market".parse::<JobMarket>(),
1379            Err(UnknownStringValue)
1380        );
1381    }
1382}