1use core::ffi::{CStr, c_void};
2use core::marker::PhantomData;
3
4use crate::{
5 Attribute, ConfigurationId, GameSchemaAvailability, GameplayEventId, SdkIndex, SdkValue,
6 TelemetryApiVersion, TrailerConfigurationId, TrailerIndex, ValueRef, game, sys,
7};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16#[repr(u32)]
17pub enum Event {
18 FrameStart = sys::SCS_TELEMETRY_EVENT_FRAME_START,
19 FrameEnd = sys::SCS_TELEMETRY_EVENT_FRAME_END,
20 Paused = sys::SCS_TELEMETRY_EVENT_PAUSED,
21 Started = sys::SCS_TELEMETRY_EVENT_STARTED,
22 Configuration = sys::SCS_TELEMETRY_EVENT_CONFIGURATION,
23 Gameplay = sys::SCS_TELEMETRY_EVENT_GAMEPLAY,
24}
25
26impl Event {
27 #[must_use]
30 pub const fn raw(self) -> sys::ScsEvent {
31 self as sys::ScsEvent
32 }
33
34 #[must_use]
42 pub const fn minimum_api_version(self) -> TelemetryApiVersion {
43 match self {
44 Self::Gameplay => TelemetryApiVersion::V1_01,
45 Self::FrameStart
46 | Self::FrameEnd
47 | Self::Paused
48 | Self::Started
49 | Self::Configuration => TelemetryApiVersion::V1_00,
50 }
51 }
52
53 #[must_use]
60 pub const fn availability(self) -> GameSchemaAvailability {
61 match self {
62 Self::Gameplay => game::capabilities::GAMEPLAY_EVENTS,
63 Self::FrameStart
64 | Self::FrameEnd
65 | Self::Paused
66 | Self::Started
67 | Self::Configuration => {
68 GameSchemaAvailability::new(Some(game::ets2::V1_00), Some(game::ats::V1_00))
69 }
70 }
71 }
72}
73
74#[derive(Clone, Copy)]
75pub struct FrameStartRef<'a> {
76 raw: &'a sys::ScsTelemetryFrameStart,
77}
78
79impl FrameStartRef<'_> {
80 #[must_use]
88 pub unsafe fn from_event_info(event_info: *const c_void) -> Option<Self> {
89 unsafe { event_info.cast::<sys::ScsTelemetryFrameStart>().as_ref() }.map(|raw| Self { raw })
93 }
94
95 #[must_use]
96 pub const fn flags(self) -> sys::ScsU32 {
97 self.raw.flags
98 }
99
100 #[must_use]
101 pub const fn render_time(self) -> sys::ScsTimestamp {
102 self.raw.render_time
103 }
104
105 #[must_use]
106 pub const fn simulation_time(self) -> sys::ScsTimestamp {
107 self.raw.simulation_time
108 }
109
110 #[must_use]
111 pub const fn paused_simulation_time(self) -> sys::ScsTimestamp {
112 self.raw.paused_simulation_time
113 }
114
115 #[must_use]
116 pub const fn timer_restarted(self) -> bool {
117 self.raw.flags & sys::SCS_TELEMETRY_FRAME_START_FLAG_TIMER_RESTART != 0
118 }
119}
120
121#[derive(Clone, Copy)]
122pub struct NamedValueRef<'a> {
123 raw: &'a sys::ScsNamedValue,
124}
125
126impl<'a> NamedValueRef<'a> {
127 #[must_use]
128 pub fn name(self) -> &'a CStr {
129 unsafe { CStr::from_ptr(self.raw.name) }
131 }
132
133 #[must_use]
134 pub fn index(self) -> Option<SdkIndex> {
135 if self.raw.index == sys::SCS_U32_NIL {
136 None
137 } else {
138 SdkIndex::new(self.raw.index)
139 }
140 }
141
142 #[must_use]
143 pub fn value(self) -> ValueRef<'a> {
144 ValueRef::from_ref(&self.raw.value)
145 }
146}
147
148#[derive(Clone)]
149pub struct NamedValues<'a> {
150 current: *const sys::ScsNamedValue,
151 marker: PhantomData<&'a sys::ScsNamedValue>,
152}
153
154impl<'a> NamedValues<'a> {
155 #[must_use]
165 pub unsafe fn from_ptr(attributes: *const sys::ScsNamedValue) -> Self {
166 Self {
167 current: attributes,
168 marker: PhantomData,
169 }
170 }
171
172 #[must_use]
173 pub fn find(self, name: &CStr) -> Option<NamedValueRef<'a>> {
174 self.filter(|attribute| attribute.index().is_none())
175 .find(|attribute| attribute.name() == name)
176 }
177
178 #[must_use]
180 pub fn find_at(self, name: &CStr, index: SdkIndex) -> Option<NamedValueRef<'a>> {
181 self.filter(|attribute| attribute.index() == Some(index))
182 .find(|attribute| attribute.name() == name)
183 }
184
185 #[must_use]
187 pub fn get<T: SdkValue>(self, attribute: Attribute<T>) -> Option<T::Decoded<'a>> {
188 if attribute.is_indexed() {
189 return None;
190 }
191 attribute.decode(self.find(attribute.name())?.value())
192 }
193
194 #[must_use]
196 pub fn get_at<T: SdkValue>(
197 self,
198 attribute: Attribute<T>,
199 index: SdkIndex,
200 ) -> Option<T::Decoded<'a>> {
201 if !attribute.is_indexed() {
202 return None;
203 }
204 attribute.decode(self.find_at(attribute.name(), index)?.value())
205 }
206}
207
208impl<'a> Iterator for NamedValues<'a> {
209 type Item = NamedValueRef<'a>;
210
211 fn next(&mut self) -> Option<Self::Item> {
212 let raw = unsafe { self.current.as_ref() }?;
216 if raw.name.is_null() {
217 return None;
218 }
219
220 self.current = unsafe { self.current.add(1) };
224 Some(NamedValueRef { raw })
225 }
226}
227
228#[derive(Clone, Copy)]
229pub struct ConfigurationRef<'a> {
230 raw: &'a sys::ScsTelemetryConfiguration,
231}
232
233impl<'a> ConfigurationRef<'a> {
234 #[must_use]
243 pub unsafe fn from_event_info(event_info: *const c_void) -> Option<Self> {
244 unsafe { event_info.cast::<sys::ScsTelemetryConfiguration>().as_ref() }
248 .map(|raw| Self { raw })
249 }
250
251 #[must_use]
252 pub fn id(self) -> &'a CStr {
253 unsafe { CStr::from_ptr(self.raw.id) }
255 }
256
257 #[must_use]
259 pub fn is(self, id: ConfigurationId) -> bool {
260 self.id() == id.name()
261 }
262
263 #[must_use]
270 pub fn trailer(self) -> Option<TrailerConfigurationId> {
271 let id = self.id();
272 if id == crate::configuration::ids::TRAILER.name() {
273 return Some(TrailerConfigurationId::Legacy);
274 }
275 let digits = id.to_bytes().strip_prefix(b"trailer.")?;
276 if digits.is_empty() || (digits.len() > 1 && digits[0] == b'0') {
277 return None;
278 }
279
280 let mut raw = 0_u32;
281 for digit in digits {
282 if !digit.is_ascii_digit() {
283 return None;
284 }
285 raw = raw.checked_mul(10)?.checked_add(u32::from(*digit - b'0'))?;
286 }
287 TrailerIndex::new(raw).map(TrailerConfigurationId::Numbered)
288 }
289
290 #[must_use]
292 pub fn trailer_index(self) -> Option<TrailerIndex> {
293 self.trailer()?.index()
294 }
295
296 #[must_use]
298 pub fn is_legacy_trailer(self) -> bool {
299 self.trailer()
300 .is_some_and(TrailerConfigurationId::is_legacy)
301 }
302
303 #[must_use]
304 pub fn attributes(self) -> NamedValues<'a> {
305 unsafe { NamedValues::from_ptr(self.raw.attributes) }
307 }
308}
309
310#[derive(Clone, Copy)]
311pub struct GameplayEventRef<'a> {
312 raw: &'a sys::ScsTelemetryGameplayEvent,
313}
314
315impl<'a> GameplayEventRef<'a> {
316 #[must_use]
325 pub unsafe fn from_event_info(event_info: *const c_void) -> Option<Self> {
326 unsafe { event_info.cast::<sys::ScsTelemetryGameplayEvent>().as_ref() }
330 .map(|raw| Self { raw })
331 }
332
333 #[must_use]
334 pub fn id(self) -> &'a CStr {
335 unsafe { CStr::from_ptr(self.raw.id) }
337 }
338
339 #[must_use]
341 pub fn is(self, id: GameplayEventId) -> bool {
342 self.id() == id.name()
343 }
344
345 #[must_use]
346 pub fn attributes(self) -> NamedValues<'a> {
347 unsafe { NamedValues::from_ptr(self.raw.attributes) }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn event_capabilities_follow_the_official_api_history() {
358 assert_eq!(
359 Event::FrameStart.raw(),
360 sys::SCS_TELEMETRY_EVENT_FRAME_START
361 );
362 assert_eq!(Event::FrameEnd.raw(), sys::SCS_TELEMETRY_EVENT_FRAME_END);
363 assert_eq!(Event::Paused.raw(), sys::SCS_TELEMETRY_EVENT_PAUSED);
364 assert_eq!(Event::Started.raw(), sys::SCS_TELEMETRY_EVENT_STARTED);
365 assert_eq!(
366 Event::Configuration.raw(),
367 sys::SCS_TELEMETRY_EVENT_CONFIGURATION
368 );
369 assert_eq!(Event::Gameplay.raw(), sys::SCS_TELEMETRY_EVENT_GAMEPLAY);
370
371 assert_eq!(
372 Event::FrameStart.minimum_api_version(),
373 TelemetryApiVersion::V1_00
374 );
375 assert_eq!(
376 Event::FrameEnd.minimum_api_version(),
377 TelemetryApiVersion::V1_00
378 );
379 assert_eq!(
380 Event::Paused.minimum_api_version(),
381 TelemetryApiVersion::V1_00
382 );
383 assert_eq!(
384 Event::Started.minimum_api_version(),
385 TelemetryApiVersion::V1_00
386 );
387 assert_eq!(
388 Event::Configuration.minimum_api_version(),
389 TelemetryApiVersion::V1_00
390 );
391 assert_eq!(
392 Event::Gameplay.minimum_api_version(),
393 TelemetryApiVersion::V1_01
394 );
395
396 for event in [
397 Event::FrameStart,
398 Event::FrameEnd,
399 Event::Paused,
400 Event::Started,
401 Event::Configuration,
402 ] {
403 assert_eq!(
404 event.availability().available_since_ets2(),
405 Some(game::ets2::V1_00)
406 );
407 assert_eq!(
408 event.availability().available_since_ats(),
409 Some(game::ats::V1_00)
410 );
411 }
412 assert_eq!(
413 Event::Gameplay.availability().available_since_ets2(),
414 Some(game::ets2::V1_14)
415 );
416 assert_eq!(
417 Event::Gameplay.availability().available_since_ats(),
418 Some(game::ats::V1_01)
419 );
420 }
421
422 #[test]
423 fn iterates_and_finds_unindexed_attributes() {
424 let cargo_name = c"cargo";
425 let cargo_value = c"盆栽花朵";
426 let attributes = [
427 sys::ScsNamedValue {
428 name: cargo_name.as_ptr(),
429 index: sys::SCS_U32_NIL,
430 padding: sys::ScsPadding::uninit(),
431 value: sys::ScsValue {
432 type_: sys::SCS_VALUE_TYPE_STRING,
433 padding: sys::ScsPadding::uninit(),
434 value: sys::ScsValueData {
435 value_string: sys::ScsValueString {
436 value: cargo_value.as_ptr(),
437 },
438 },
439 },
440 },
441 sys::ScsNamedValue {
442 name: core::ptr::null(),
443 index: 0,
444 padding: sys::ScsPadding::uninit(),
445 value: sys::ScsValue {
446 type_: sys::SCS_VALUE_TYPE_INVALID,
447 padding: sys::ScsPadding::uninit(),
448 value: sys::ScsValueData {
449 value_u64: sys::ScsValueU64 { value: 0 },
450 },
451 },
452 },
453 ];
454
455 let values = unsafe { NamedValues::from_ptr(attributes.as_ptr()) };
458 let cargo = values
459 .find(cargo_name)
460 .expect("cargo attribute should exist");
461
462 assert_eq!(cargo.value().as_c_str(), Some(cargo_value));
463 assert_eq!(cargo.index(), None);
464 }
465
466 #[test]
467 fn indexed_attribute_lookup_uses_the_strong_sdk_index_domain() {
468 let slot = c"slot.gear";
469 let attributes = [
470 sys::ScsNamedValue {
471 name: slot.as_ptr(),
472 index: 2,
473 padding: sys::ScsPadding::uninit(),
474 value: sys::ScsValue {
475 type_: sys::SCS_VALUE_TYPE_S32,
476 padding: sys::ScsPadding::uninit(),
477 value: sys::ScsValueData {
478 value_s32: sys::ScsValueS32 { value: 7 },
479 },
480 },
481 },
482 sys::ScsNamedValue {
483 name: core::ptr::null(),
484 index: 0,
485 padding: sys::ScsPadding::uninit(),
486 value: sys::ScsValue {
487 type_: sys::SCS_VALUE_TYPE_INVALID,
488 padding: sys::ScsPadding::uninit(),
489 value: sys::ScsValueData {
490 value_u64: sys::ScsValueU64 { value: 0 },
491 },
492 },
493 },
494 ];
495
496 let values = unsafe { NamedValues::from_ptr(attributes.as_ptr()) };
499 let index = SdkIndex::new(2).expect("ordinary array index");
500 assert_eq!(
501 values
502 .clone()
503 .get_at(crate::configuration::attributes::SLOT_GEAR, index),
504 Some(7)
505 );
506 assert!(values.find(slot).is_none());
507 assert_eq!(SdkIndex::new(sys::SCS_U32_NIL), None);
508 }
509
510 #[test]
511 fn trailer_configuration_ids_require_the_canonical_numbered_form() {
512 let terminator = [sys::ScsNamedValue {
513 name: core::ptr::null(),
514 index: 0,
515 padding: sys::ScsPadding::uninit(),
516 value: sys::ScsValue {
517 type_: sys::SCS_VALUE_TYPE_INVALID,
518 padding: sys::ScsPadding::uninit(),
519 value: sys::ScsValueData {
520 value_u64: sys::ScsValueU64 { value: 0 },
521 },
522 },
523 }];
524
525 for (id, expected) in [
526 (c"trailer", Some(TrailerConfigurationId::Legacy)),
527 (
528 c"trailer.0",
529 Some(TrailerConfigurationId::Numbered(TrailerIndex::ZERO)),
530 ),
531 (
532 c"trailer.9",
533 Some(TrailerConfigurationId::Numbered(
534 TrailerIndex::new(9).expect("last trailer index"),
535 )),
536 ),
537 (c"trailer.00", None),
538 (c"trailer.01", None),
539 (c"trailer.10", None),
540 (c"trailer.-1", None),
541 (c"trailer.foo", None),
542 (c"trailer.", None),
543 (c"truck", None),
544 ] {
545 let raw = sys::ScsTelemetryConfiguration {
546 id: id.as_ptr(),
547 attributes: terminator.as_ptr(),
548 };
549 let event_info = (&raw const raw).cast::<c_void>();
550 let event = unsafe { ConfigurationRef::from_event_info(event_info) }
553 .expect("configuration fixture");
554 assert_eq!(event.trailer(), expected, "configuration id {id:?}");
555 assert_eq!(
556 event.trailer_index(),
557 expected.and_then(TrailerConfigurationId::index)
558 );
559 assert_eq!(
560 event.is_legacy_trailer(),
561 expected.is_some_and(TrailerConfigurationId::is_legacy)
562 );
563 }
564 }
565
566 #[test]
567 fn frame_start_ignores_uninitialized_alignment_storage() {
568 let frame = sys::ScsTelemetryFrameStart {
569 flags: sys::SCS_TELEMETRY_FRAME_START_FLAG_TIMER_RESTART,
570 padding: sys::ScsPadding::uninit(),
571 render_time: 11,
572 simulation_time: 12,
573 paused_simulation_time: 13,
574 };
575 let pointer = (&raw const frame).cast::<c_void>();
576 let frame = unsafe { FrameStartRef::from_event_info(pointer) }.expect("frame start");
579
580 assert!(frame.timer_restarted());
581 assert_eq!(frame.render_time(), 11);
582 assert_eq!(frame.simulation_time(), 12);
583 assert_eq!(frame.paused_simulation_time(), 13);
584 }
585}