1use core::ffi::{CStr, c_void};
2use core::marker::PhantomData;
3
4use crate::{Event, GameSchemaVersion, SdkError, SdkResult, TelemetryApiVersion, sys};
5
6#[derive(Clone, Copy)]
12pub(crate) struct LoggerHandle(sys::ScsLog);
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15#[repr(i32)]
16pub enum LogLevel {
17 Message = sys::SCS_LOG_TYPE_MESSAGE,
18 Warning = sys::SCS_LOG_TYPE_WARNING,
19 Error = sys::SCS_LOG_TYPE_ERROR,
20}
21
22#[derive(Clone, Copy)]
23pub(crate) struct ApiTable {
24 pub(crate) version: TelemetryApiVersion,
25 pub(crate) logger: LoggerHandle,
26 pub(crate) register_for_event: sys::ScsTelemetryRegisterForEvent,
27 pub(crate) unregister_from_event: sys::ScsTelemetryUnregisterFromEvent,
28 pub(crate) register_for_channel: sys::ScsTelemetryRegisterForChannel,
29 pub(crate) unregister_from_channel: sys::ScsTelemetryUnregisterFromChannel,
30}
31
32impl ApiTable {
33 const fn from_raw(version: TelemetryApiVersion, raw: &sys::ScsTelemetryInitParamsV101) -> Self {
34 Self {
35 version,
36 logger: LoggerHandle(raw.common.log),
37 register_for_event: raw.register_for_event,
38 unregister_from_event: raw.unregister_from_event,
39 register_for_channel: raw.register_for_channel,
40 unregister_from_channel: raw.unregister_from_channel,
41 }
42 }
43}
44
45pub struct TelemetryApi<'a> {
47 raw: &'a sys::ScsTelemetryInitParamsV101,
48 table: ApiTable,
49 not_send_sync: PhantomData<*mut ()>,
51}
52
53#[derive(Clone, Copy)]
69pub struct TelemetrySession {
70 pub(crate) table: ApiTable,
71}
72
73pub struct SdkCall<'scope> {
90 pub(crate) table: ApiTable,
91 scope: PhantomData<&'scope mut ()>,
92 not_send_sync: PhantomData<*mut ()>,
93}
94
95#[derive(Clone, Copy)]
97pub struct ScopedLogger<'scope> {
98 raw: sys::ScsLog,
99 scope: PhantomData<&'scope SdkCall<'scope>>,
100}
101
102impl ScopedLogger<'_> {
103 pub(crate) const fn from_raw(raw: sys::ScsLog) -> Self {
104 Self {
105 raw,
106 scope: PhantomData,
107 }
108 }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119enum TelemetryInitLayout {
120 V100,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126struct TelemetryApiAdapter {
127 version: TelemetryApiVersion,
128 layout: TelemetryInitLayout,
129}
130
131const TELEMETRY_API_ADAPTERS: [TelemetryApiAdapter; 2] = [
137 TelemetryApiAdapter {
138 version: TelemetryApiVersion::V1_00,
139 layout: TelemetryInitLayout::V100,
140 },
141 TelemetryApiAdapter {
142 version: TelemetryApiVersion::V1_01,
143 layout: TelemetryInitLayout::V100,
144 },
145];
146
147const fn adapter_versions() -> [TelemetryApiVersion; TELEMETRY_API_ADAPTERS.len()] {
148 let mut versions = [TelemetryApiVersion::from_raw(0); TELEMETRY_API_ADAPTERS.len()];
149 let mut index = 0;
150 while index < TELEMETRY_API_ADAPTERS.len() {
151 versions[index] = TELEMETRY_API_ADAPTERS[index].version;
152 index += 1;
153 }
154 versions
155}
156
157const SUPPORTED_TELEMETRY_API_VERSIONS: [TelemetryApiVersion; TELEMETRY_API_ADAPTERS.len()] =
158 adapter_versions();
159
160impl<'a> TelemetryApi<'a> {
161 pub const SUPPORTED_VERSIONS: &'static [TelemetryApiVersion] =
168 &SUPPORTED_TELEMETRY_API_VERSIONS;
169
170 #[must_use]
178 pub const fn supports_version(version: TelemetryApiVersion) -> bool {
179 Self::init_layout(version).is_some()
180 }
181
182 const fn init_layout(version: TelemetryApiVersion) -> Option<TelemetryInitLayout> {
183 let mut index = 0;
184 while index < TELEMETRY_API_ADAPTERS.len() {
185 let adapter = TELEMETRY_API_ADAPTERS[index];
186 if adapter.version.raw() == version.raw() {
187 return Some(adapter.layout);
188 }
189 index += 1;
190 }
191 None
192 }
193
194 pub unsafe fn from_raw(
211 version: TelemetryApiVersion,
212 params: *const sys::ScsTelemetryInitParams,
213 ) -> SdkResult<Self> {
214 let Some(layout) = Self::init_layout(version) else {
219 return Err(SdkError::Unsupported);
220 };
221 let raw = match layout {
222 TelemetryInitLayout::V100 => {
223 unsafe { params.cast::<sys::ScsTelemetryInitParamsV100>().as_ref() }
227 .ok_or(SdkError::InvalidParameter)?
228 }
229 };
230 Ok(Self {
231 raw,
232 table: ApiTable::from_raw(version, raw),
233 not_send_sync: PhantomData,
234 })
235 }
236
237 #[must_use]
239 pub const fn version(&self) -> TelemetryApiVersion {
240 self.table.version
241 }
242
243 #[must_use]
244 pub const fn raw(&self) -> &sys::ScsTelemetryInitParamsV101 {
245 self.raw
246 }
247
248 #[must_use]
249 pub fn game_name(&self) -> &'a CStr {
250 unsafe { CStr::from_ptr(self.raw.common.game_name) }
253 }
254
255 #[must_use]
256 pub fn game_id(&self) -> &'a CStr {
257 unsafe { CStr::from_ptr(self.raw.common.game_id) }
260 }
261
262 #[must_use]
263 pub const fn game_schema_version(&self) -> GameSchemaVersion {
264 GameSchemaVersion::from_raw(self.raw.common.game_version)
265 }
266
267 #[must_use]
269 pub const fn session(&self) -> TelemetrySession {
270 TelemetrySession { table: self.table }
271 }
272
273 pub fn with_call<R>(&self, operation: impl for<'scope> FnOnce(&SdkCall<'scope>) -> R) -> R {
278 let call = SdkCall {
279 table: self.table,
280 scope: PhantomData,
281 not_send_sync: PhantomData,
282 };
283 operation(&call)
284 }
285}
286
287impl TelemetrySession {
288 pub unsafe fn with_call<R>(
298 self,
299 operation: impl for<'scope> FnOnce(&SdkCall<'scope>) -> R,
300 ) -> R {
301 let call = SdkCall {
302 table: self.table,
303 scope: PhantomData,
304 not_send_sync: PhantomData,
305 };
306 operation(&call)
307 }
308}
309
310impl SdkCall<'_> {
311 #[must_use]
317 pub const fn telemetry_api_version(&self) -> TelemetryApiVersion {
318 self.table.version
319 }
320
321 #[must_use]
322 pub const fn logger(&self) -> ScopedLogger<'_> {
323 ScopedLogger::from_raw(self.table.logger.0)
324 }
325
326 pub unsafe fn register_event(
341 &self,
342 event: Event,
343 callback: sys::ScsTelemetryEventCallback,
344 context: *mut c_void,
345 ) -> SdkResult {
346 let result = unsafe { (self.table.register_for_event)(event.raw(), callback, context) };
348 SdkError::from_code(result)
349 }
350
351 pub unsafe fn unregister_event(&self, event: Event) -> SdkResult {
363 let result = unsafe { (self.table.unregister_from_event)(event.raw()) };
365 SdkError::from_code(result)
366 }
367}
368
369impl ScopedLogger<'_> {
370 pub fn log(self, level: LogLevel, message: &CStr) {
371 unsafe { (self.raw)(level as sys::ScsLogType, message.as_ptr()) };
374 }
375
376 pub fn message(self, message: &CStr) {
377 self.log(LogLevel::Message, message);
378 }
379
380 pub fn warning(self, message: &CStr) {
381 self.log(LogLevel::Warning, message);
382 }
383
384 pub fn error(self, message: &CStr) {
385 self.log(LogLevel::Error, message);
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use core::ffi::c_void;
392 use core::sync::atomic::{AtomicUsize, Ordering};
393
394 use super::*;
395
396 static LOG_CALLS: AtomicUsize = AtomicUsize::new(0);
397 static EVENT_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0);
398
399 unsafe extern "system" fn fake_log(_level: sys::ScsLogType, _message: sys::ScsString) {
400 LOG_CALLS.fetch_add(1, Ordering::Relaxed);
401 }
402
403 unsafe extern "system" fn fake_event_callback(
404 _event: sys::ScsEvent,
405 _event_info: *const c_void,
406 _context: sys::ScsContext,
407 ) {
408 }
409
410 unsafe extern "system" fn fake_register_event(
411 _event: sys::ScsEvent,
412 _callback: sys::ScsTelemetryEventCallback,
413 _context: sys::ScsContext,
414 ) -> sys::ScsResult {
415 EVENT_REGISTRATIONS.fetch_add(1, Ordering::Relaxed);
416 sys::SCS_RESULT_OK
417 }
418
419 unsafe extern "system" fn fake_unregister_event(_event: sys::ScsEvent) -> sys::ScsResult {
420 sys::SCS_RESULT_OK
421 }
422
423 unsafe extern "system" fn fake_register_channel(
424 _name: sys::ScsString,
425 _index: sys::ScsU32,
426 _type: sys::ScsValueType,
427 _flags: sys::ScsU32,
428 _callback: sys::ScsTelemetryChannelCallback,
429 _context: sys::ScsContext,
430 ) -> sys::ScsResult {
431 sys::SCS_RESULT_OK
432 }
433
434 unsafe extern "system" fn fake_unregister_channel(
435 _name: sys::ScsString,
436 _index: sys::ScsU32,
437 _type: sys::ScsValueType,
438 ) -> sys::ScsResult {
439 sys::SCS_RESULT_OK
440 }
441
442 fn parameters() -> sys::ScsTelemetryInitParamsV101 {
443 sys::ScsTelemetryInitParamsV101 {
444 common: sys::ScsSdkInitParamsV100 {
445 game_name: c"Euro Truck Simulator 2".as_ptr(),
446 game_id: c"eut2".as_ptr(),
447 game_version: 0x0001_003c,
448 padding: sys::ScsPadding::uninit(),
449 log: fake_log,
450 },
451 register_for_event: fake_register_event,
452 unregister_from_event: fake_unregister_event,
453 register_for_channel: fake_register_channel,
454 unregister_from_channel: fake_unregister_channel,
455 }
456 }
457
458 #[test]
459 fn logging_and_registration_require_a_scoped_call() {
460 LOG_CALLS.store(0, Ordering::Relaxed);
461 EVENT_REGISTRATIONS.store(0, Ordering::Relaxed);
462 let parameters = parameters();
463 let pointer = (&raw const parameters).cast::<sys::ScsTelemetryInitParams>();
464 let api = unsafe { TelemetryApi::from_raw(TelemetryApiVersion::V1_01, pointer) }
465 .expect("valid function table");
466
467 assert_eq!(api.game_id(), c"eut2");
468 assert_eq!(api.game_schema_version(), GameSchemaVersion::new(1, 60));
469 api.with_call(|call| {
470 call.logger().message(c"initializing");
471 unsafe {
472 call.register_event(Event::Started, fake_event_callback, core::ptr::null_mut())
473 }
474 .expect("event registration");
475 });
476
477 let session = api.session();
478 unsafe {
479 session.with_call(|call| call.logger().message(c"callback"));
480 }
481
482 assert_eq!(LOG_CALLS.load(Ordering::Relaxed), 2);
483 assert_eq!(EVENT_REGISTRATIONS.load(Ordering::Relaxed), 1);
484 }
485
486 #[test]
487 fn unaudited_api_version_is_rejected_before_reading_parameters() {
488 assert_eq!(
489 TelemetryApi::SUPPORTED_VERSIONS,
490 &[TelemetryApiVersion::V1_00, TelemetryApiVersion::V1_01],
491 );
492 assert!(TelemetryApi::supports_version(TelemetryApiVersion::V1_00));
493 assert!(TelemetryApi::supports_version(TelemetryApiVersion::V1_01));
494 assert!(!TelemetryApi::supports_version(TelemetryApiVersion::new(
495 1, 2
496 )));
497
498 let result = unsafe {
499 TelemetryApi::from_raw(
500 TelemetryApiVersion::new(1, 2),
501 core::ptr::null::<sys::ScsTelemetryInitParams>(),
502 )
503 };
504
505 assert!(matches!(result, Err(SdkError::Unsupported)));
506 }
507}