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 })
90 }
91
92 #[must_use]
93 pub const fn flags(self) -> sys::ScsU32 {
94 self.raw.flags
95 }
96
97 #[must_use]
98 pub const fn render_time(self) -> sys::ScsTimestamp {
99 self.raw.render_time
100 }
101
102 #[must_use]
103 pub const fn simulation_time(self) -> sys::ScsTimestamp {
104 self.raw.simulation_time
105 }
106
107 #[must_use]
108 pub const fn paused_simulation_time(self) -> sys::ScsTimestamp {
109 self.raw.paused_simulation_time
110 }
111
112 #[must_use]
113 pub const fn timer_restarted(self) -> bool {
114 self.raw.flags & sys::SCS_TELEMETRY_FRAME_START_FLAG_TIMER_RESTART != 0
115 }
116}
117
118#[derive(Clone, Copy)]
119pub struct NamedValueRef<'a> {
120 raw: &'a sys::ScsNamedValue,
121}
122
123impl<'a> NamedValueRef<'a> {
124 #[must_use]
125 pub fn name(self) -> &'a CStr {
126 unsafe { CStr::from_ptr(self.raw.name) }
128 }
129
130 #[must_use]
131 pub fn index(self) -> Option<SdkIndex> {
132 if self.raw.index == sys::SCS_U32_NIL {
133 None
134 } else {
135 SdkIndex::new(self.raw.index)
136 }
137 }
138
139 #[must_use]
140 pub fn value(self) -> ValueRef<'a> {
141 ValueRef::from_ref(&self.raw.value)
142 }
143}
144
145#[derive(Clone)]
146pub struct NamedValues<'a> {
147 current: *const sys::ScsNamedValue,
148 marker: PhantomData<&'a sys::ScsNamedValue>,
149}
150
151impl<'a> NamedValues<'a> {
152 #[must_use]
162 pub unsafe fn from_ptr(attributes: *const sys::ScsNamedValue) -> Self {
163 Self {
164 current: attributes,
165 marker: PhantomData,
166 }
167 }
168
169 #[must_use]
170 pub fn find(self, name: &CStr) -> Option<NamedValueRef<'a>> {
171 self.filter(|attribute| attribute.index().is_none())
172 .find(|attribute| attribute.name() == name)
173 }
174
175 #[must_use]
177 pub fn find_at(self, name: &CStr, index: SdkIndex) -> Option<NamedValueRef<'a>> {
178 self.filter(|attribute| attribute.index() == Some(index))
179 .find(|attribute| attribute.name() == name)
180 }
181
182 #[must_use]
184 pub fn get<T: SdkValue>(self, attribute: Attribute<T>) -> Option<T::Decoded<'a>> {
185 if attribute.is_indexed() {
186 return None;
187 }
188 attribute.decode(self.find(attribute.name())?.value())
189 }
190
191 #[must_use]
193 pub fn get_at<T: SdkValue>(
194 self,
195 attribute: Attribute<T>,
196 index: SdkIndex,
197 ) -> Option<T::Decoded<'a>> {
198 if !attribute.is_indexed() {
199 return None;
200 }
201 attribute.decode(self.find_at(attribute.name(), index)?.value())
202 }
203}
204
205impl<'a> Iterator for NamedValues<'a> {
206 type Item = NamedValueRef<'a>;
207
208 fn next(&mut self) -> Option<Self::Item> {
209 let raw = unsafe { self.current.as_ref() }?;
210 if raw.name.is_null() {
211 return None;
212 }
213
214 self.current = unsafe { self.current.add(1) };
215 Some(NamedValueRef { raw })
216 }
217}
218
219#[derive(Clone, Copy)]
220pub struct ConfigurationRef<'a> {
221 raw: &'a sys::ScsTelemetryConfiguration,
222}
223
224impl<'a> ConfigurationRef<'a> {
225 #[must_use]
234 pub unsafe fn from_event_info(event_info: *const c_void) -> Option<Self> {
235 unsafe { event_info.cast::<sys::ScsTelemetryConfiguration>().as_ref() }
236 .map(|raw| Self { raw })
237 }
238
239 #[must_use]
240 pub fn id(self) -> &'a CStr {
241 unsafe { CStr::from_ptr(self.raw.id) }
243 }
244
245 #[must_use]
247 pub fn is(self, id: ConfigurationId) -> bool {
248 self.id() == id.name()
249 }
250
251 #[must_use]
258 pub fn trailer(self) -> Option<TrailerConfigurationId> {
259 let id = self.id();
260 if id == crate::configuration::ids::TRAILER.name() {
261 return Some(TrailerConfigurationId::Legacy);
262 }
263 let digits = id.to_bytes().strip_prefix(b"trailer.")?;
264 if digits.is_empty() || (digits.len() > 1 && digits[0] == b'0') {
265 return None;
266 }
267
268 let mut raw = 0_u32;
269 for digit in digits {
270 if !digit.is_ascii_digit() {
271 return None;
272 }
273 raw = raw.checked_mul(10)?.checked_add(u32::from(*digit - b'0'))?;
274 }
275 TrailerIndex::new(raw).map(TrailerConfigurationId::Numbered)
276 }
277
278 #[must_use]
280 pub fn trailer_index(self) -> Option<TrailerIndex> {
281 self.trailer()?.index()
282 }
283
284 #[must_use]
286 pub fn is_legacy_trailer(self) -> bool {
287 self.trailer()
288 .is_some_and(TrailerConfigurationId::is_legacy)
289 }
290
291 #[must_use]
292 pub fn attributes(self) -> NamedValues<'a> {
293 unsafe { NamedValues::from_ptr(self.raw.attributes) }
295 }
296}
297
298#[derive(Clone, Copy)]
299pub struct GameplayEventRef<'a> {
300 raw: &'a sys::ScsTelemetryGameplayEvent,
301}
302
303impl<'a> GameplayEventRef<'a> {
304 #[must_use]
313 pub unsafe fn from_event_info(event_info: *const c_void) -> Option<Self> {
314 unsafe { event_info.cast::<sys::ScsTelemetryGameplayEvent>().as_ref() }
315 .map(|raw| Self { raw })
316 }
317
318 #[must_use]
319 pub fn id(self) -> &'a CStr {
320 unsafe { CStr::from_ptr(self.raw.id) }
322 }
323
324 #[must_use]
326 pub fn is(self, id: GameplayEventId) -> bool {
327 self.id() == id.name()
328 }
329
330 #[must_use]
331 pub fn attributes(self) -> NamedValues<'a> {
332 unsafe { NamedValues::from_ptr(self.raw.attributes) }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn event_capabilities_follow_the_official_api_history() {
343 assert_eq!(
344 Event::FrameStart.raw(),
345 sys::SCS_TELEMETRY_EVENT_FRAME_START
346 );
347 assert_eq!(Event::FrameEnd.raw(), sys::SCS_TELEMETRY_EVENT_FRAME_END);
348 assert_eq!(Event::Paused.raw(), sys::SCS_TELEMETRY_EVENT_PAUSED);
349 assert_eq!(Event::Started.raw(), sys::SCS_TELEMETRY_EVENT_STARTED);
350 assert_eq!(
351 Event::Configuration.raw(),
352 sys::SCS_TELEMETRY_EVENT_CONFIGURATION
353 );
354 assert_eq!(Event::Gameplay.raw(), sys::SCS_TELEMETRY_EVENT_GAMEPLAY);
355
356 assert_eq!(
357 Event::FrameStart.minimum_api_version(),
358 TelemetryApiVersion::V1_00
359 );
360 assert_eq!(
361 Event::FrameEnd.minimum_api_version(),
362 TelemetryApiVersion::V1_00
363 );
364 assert_eq!(
365 Event::Paused.minimum_api_version(),
366 TelemetryApiVersion::V1_00
367 );
368 assert_eq!(
369 Event::Started.minimum_api_version(),
370 TelemetryApiVersion::V1_00
371 );
372 assert_eq!(
373 Event::Configuration.minimum_api_version(),
374 TelemetryApiVersion::V1_00
375 );
376 assert_eq!(
377 Event::Gameplay.minimum_api_version(),
378 TelemetryApiVersion::V1_01
379 );
380
381 for event in [
382 Event::FrameStart,
383 Event::FrameEnd,
384 Event::Paused,
385 Event::Started,
386 Event::Configuration,
387 ] {
388 assert_eq!(
389 event.availability().available_since_ets2(),
390 Some(game::ets2::V1_00)
391 );
392 assert_eq!(
393 event.availability().available_since_ats(),
394 Some(game::ats::V1_00)
395 );
396 }
397 assert_eq!(
398 Event::Gameplay.availability().available_since_ets2(),
399 Some(game::ets2::V1_14)
400 );
401 assert_eq!(
402 Event::Gameplay.availability().available_since_ats(),
403 Some(game::ats::V1_01)
404 );
405 }
406
407 #[test]
408 fn iterates_and_finds_unindexed_attributes() {
409 let cargo_name = c"cargo";
410 let cargo_value = c"盆栽花朵";
411 let attributes = [
412 sys::ScsNamedValue {
413 name: cargo_name.as_ptr(),
414 index: sys::SCS_U32_NIL,
415 padding: sys::ScsPadding::uninit(),
416 value: sys::ScsValue {
417 type_: sys::SCS_VALUE_TYPE_STRING,
418 padding: sys::ScsPadding::uninit(),
419 value: sys::ScsValueData {
420 value_string: sys::ScsValueString {
421 value: cargo_value.as_ptr(),
422 },
423 },
424 },
425 },
426 sys::ScsNamedValue {
427 name: core::ptr::null(),
428 index: 0,
429 padding: sys::ScsPadding::uninit(),
430 value: sys::ScsValue {
431 type_: sys::SCS_VALUE_TYPE_INVALID,
432 padding: sys::ScsPadding::uninit(),
433 value: sys::ScsValueData {
434 value_u64: sys::ScsValueU64 { value: 0 },
435 },
436 },
437 },
438 ];
439
440 let values = unsafe { NamedValues::from_ptr(attributes.as_ptr()) };
441 let cargo = values
442 .find(cargo_name)
443 .expect("cargo attribute should exist");
444
445 assert_eq!(cargo.value().as_c_str(), Some(cargo_value));
446 assert_eq!(cargo.index(), None);
447 }
448
449 #[test]
450 fn indexed_attribute_lookup_uses_the_strong_sdk_index_domain() {
451 let slot = c"slot.gear";
452 let attributes = [
453 sys::ScsNamedValue {
454 name: slot.as_ptr(),
455 index: 2,
456 padding: sys::ScsPadding::uninit(),
457 value: sys::ScsValue {
458 type_: sys::SCS_VALUE_TYPE_S32,
459 padding: sys::ScsPadding::uninit(),
460 value: sys::ScsValueData {
461 value_s32: sys::ScsValueS32 { value: 7 },
462 },
463 },
464 },
465 sys::ScsNamedValue {
466 name: core::ptr::null(),
467 index: 0,
468 padding: sys::ScsPadding::uninit(),
469 value: sys::ScsValue {
470 type_: sys::SCS_VALUE_TYPE_INVALID,
471 padding: sys::ScsPadding::uninit(),
472 value: sys::ScsValueData {
473 value_u64: sys::ScsValueU64 { value: 0 },
474 },
475 },
476 },
477 ];
478
479 let values = unsafe { NamedValues::from_ptr(attributes.as_ptr()) };
480 let index = SdkIndex::new(2).expect("ordinary array index");
481 assert_eq!(
482 values
483 .clone()
484 .get_at(crate::configuration::attributes::SLOT_GEAR, index),
485 Some(7)
486 );
487 assert!(values.find(slot).is_none());
488 assert_eq!(SdkIndex::new(sys::SCS_U32_NIL), None);
489 }
490
491 #[test]
492 fn trailer_configuration_ids_require_the_canonical_numbered_form() {
493 let terminator = [sys::ScsNamedValue {
494 name: core::ptr::null(),
495 index: 0,
496 padding: sys::ScsPadding::uninit(),
497 value: sys::ScsValue {
498 type_: sys::SCS_VALUE_TYPE_INVALID,
499 padding: sys::ScsPadding::uninit(),
500 value: sys::ScsValueData {
501 value_u64: sys::ScsValueU64 { value: 0 },
502 },
503 },
504 }];
505
506 for (id, expected) in [
507 (c"trailer", Some(TrailerConfigurationId::Legacy)),
508 (
509 c"trailer.0",
510 Some(TrailerConfigurationId::Numbered(TrailerIndex::ZERO)),
511 ),
512 (
513 c"trailer.9",
514 Some(TrailerConfigurationId::Numbered(
515 TrailerIndex::new(9).expect("last trailer index"),
516 )),
517 ),
518 (c"trailer.00", None),
519 (c"trailer.01", None),
520 (c"trailer.10", None),
521 (c"trailer.-1", None),
522 (c"trailer.foo", None),
523 (c"trailer.", None),
524 (c"truck", None),
525 ] {
526 let raw = sys::ScsTelemetryConfiguration {
527 id: id.as_ptr(),
528 attributes: terminator.as_ptr(),
529 };
530 let event =
531 unsafe { ConfigurationRef::from_event_info((&raw const raw).cast::<c_void>()) }
532 .expect("configuration fixture");
533 assert_eq!(event.trailer(), expected, "configuration id {id:?}");
534 assert_eq!(
535 event.trailer_index(),
536 expected.and_then(TrailerConfigurationId::index)
537 );
538 assert_eq!(
539 event.is_legacy_trailer(),
540 expected.is_some_and(TrailerConfigurationId::is_legacy)
541 );
542 }
543 }
544
545 #[test]
546 fn frame_start_ignores_uninitialized_alignment_storage() {
547 let frame = sys::ScsTelemetryFrameStart {
548 flags: sys::SCS_TELEMETRY_FRAME_START_FLAG_TIMER_RESTART,
549 padding: sys::ScsPadding::uninit(),
550 render_time: 11,
551 simulation_time: 12,
552 paused_simulation_time: 13,
553 };
554 let pointer = (&raw const frame).cast::<c_void>();
555 let frame = unsafe { FrameStartRef::from_event_info(pointer) }.expect("frame start");
556
557 assert!(frame.timer_restarted());
558 assert_eq!(frame.render_time(), 11);
559 assert_eq!(frame.simulation_time(), 12);
560 assert_eq!(frame.paused_simulation_time(), 13);
561 }
562}