use core::ffi::{CStr, c_void};
use core::marker::PhantomData;
use crate::{Event, GameSchemaVersion, SdkError, SdkResult, TelemetryApiVersion, sys};
#[derive(Clone, Copy)]
pub(crate) struct LoggerHandle(sys::ScsLog);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum LogLevel {
Message = sys::SCS_LOG_TYPE_MESSAGE,
Warning = sys::SCS_LOG_TYPE_WARNING,
Error = sys::SCS_LOG_TYPE_ERROR,
}
#[derive(Clone, Copy)]
pub(crate) struct ApiTable {
pub(crate) version: TelemetryApiVersion,
pub(crate) logger: LoggerHandle,
pub(crate) register_for_event: sys::ScsTelemetryRegisterForEvent,
pub(crate) unregister_from_event: sys::ScsTelemetryUnregisterFromEvent,
pub(crate) register_for_channel: sys::ScsTelemetryRegisterForChannel,
pub(crate) unregister_from_channel: sys::ScsTelemetryUnregisterFromChannel,
}
impl ApiTable {
const fn from_raw(version: TelemetryApiVersion, raw: &sys::ScsTelemetryInitParamsV101) -> Self {
Self {
version,
logger: LoggerHandle(raw.common.log),
register_for_event: raw.register_for_event,
unregister_from_event: raw.unregister_from_event,
register_for_channel: raw.register_for_channel,
unregister_from_channel: raw.unregister_from_channel,
}
}
}
pub struct TelemetryApi<'a> {
raw: &'a sys::ScsTelemetryInitParamsV101,
table: ApiTable,
not_send_sync: PhantomData<*mut ()>,
}
#[derive(Clone, Copy)]
pub struct TelemetrySession {
pub(crate) table: ApiTable,
}
pub struct SdkCall<'scope> {
pub(crate) table: ApiTable,
scope: PhantomData<&'scope mut ()>,
not_send_sync: PhantomData<*mut ()>,
}
#[derive(Clone, Copy)]
pub struct ScopedLogger<'scope> {
raw: sys::ScsLog,
scope: PhantomData<&'scope SdkCall<'scope>>,
}
impl ScopedLogger<'_> {
pub(crate) const fn from_raw(raw: sys::ScsLog) -> Self {
Self {
raw,
scope: PhantomData,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TelemetryInitLayout {
V100,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TelemetryApiAdapter {
version: TelemetryApiVersion,
layout: TelemetryInitLayout,
}
const TELEMETRY_API_ADAPTERS: [TelemetryApiAdapter; 2] = [
TelemetryApiAdapter {
version: TelemetryApiVersion::V1_00,
layout: TelemetryInitLayout::V100,
},
TelemetryApiAdapter {
version: TelemetryApiVersion::V1_01,
layout: TelemetryInitLayout::V100,
},
];
const fn adapter_versions() -> [TelemetryApiVersion; TELEMETRY_API_ADAPTERS.len()] {
let mut versions = [TelemetryApiVersion::from_raw(0); TELEMETRY_API_ADAPTERS.len()];
let mut index = 0;
while index < TELEMETRY_API_ADAPTERS.len() {
versions[index] = TELEMETRY_API_ADAPTERS[index].version;
index += 1;
}
versions
}
const SUPPORTED_TELEMETRY_API_VERSIONS: [TelemetryApiVersion; TELEMETRY_API_ADAPTERS.len()] =
adapter_versions();
impl<'a> TelemetryApi<'a> {
pub const SUPPORTED_VERSIONS: &'static [TelemetryApiVersion] =
&SUPPORTED_TELEMETRY_API_VERSIONS;
#[must_use]
pub const fn supports_version(version: TelemetryApiVersion) -> bool {
Self::init_layout(version).is_some()
}
const fn init_layout(version: TelemetryApiVersion) -> Option<TelemetryInitLayout> {
let mut index = 0;
while index < TELEMETRY_API_ADAPTERS.len() {
let adapter = TELEMETRY_API_ADAPTERS[index];
if adapter.version.raw() == version.raw() {
return Some(adapter.layout);
}
index += 1;
}
None
}
pub unsafe fn from_raw(
version: TelemetryApiVersion,
params: *const sys::ScsTelemetryInitParams,
) -> SdkResult<Self> {
let Some(layout) = Self::init_layout(version) else {
return Err(SdkError::Unsupported);
};
let raw = match layout {
TelemetryInitLayout::V100 => {
unsafe { params.cast::<sys::ScsTelemetryInitParamsV100>().as_ref() }
.ok_or(SdkError::InvalidParameter)?
}
};
Ok(Self {
raw,
table: ApiTable::from_raw(version, raw),
not_send_sync: PhantomData,
})
}
#[must_use]
pub const fn version(&self) -> TelemetryApiVersion {
self.table.version
}
#[must_use]
pub const fn raw(&self) -> &sys::ScsTelemetryInitParamsV101 {
self.raw
}
#[must_use]
pub fn game_name(&self) -> &'a CStr {
unsafe { CStr::from_ptr(self.raw.common.game_name) }
}
#[must_use]
pub fn game_id(&self) -> &'a CStr {
unsafe { CStr::from_ptr(self.raw.common.game_id) }
}
#[must_use]
pub const fn game_schema_version(&self) -> GameSchemaVersion {
GameSchemaVersion::from_raw(self.raw.common.game_version)
}
#[must_use]
pub const fn session(&self) -> TelemetrySession {
TelemetrySession { table: self.table }
}
pub fn with_call<R>(&self, operation: impl for<'scope> FnOnce(&SdkCall<'scope>) -> R) -> R {
let call = SdkCall {
table: self.table,
scope: PhantomData,
not_send_sync: PhantomData,
};
operation(&call)
}
}
impl TelemetrySession {
pub unsafe fn with_call<R>(
self,
operation: impl for<'scope> FnOnce(&SdkCall<'scope>) -> R,
) -> R {
let call = SdkCall {
table: self.table,
scope: PhantomData,
not_send_sync: PhantomData,
};
operation(&call)
}
}
impl SdkCall<'_> {
#[must_use]
pub const fn telemetry_api_version(&self) -> TelemetryApiVersion {
self.table.version
}
#[must_use]
pub const fn logger(&self) -> ScopedLogger<'_> {
ScopedLogger::from_raw(self.table.logger.0)
}
pub unsafe fn register_event(
&self,
event: Event,
callback: sys::ScsTelemetryEventCallback,
context: *mut c_void,
) -> SdkResult {
let result = unsafe { (self.table.register_for_event)(event.raw(), callback, context) };
SdkError::from_code(result)
}
pub unsafe fn unregister_event(&self, event: Event) -> SdkResult {
let result = unsafe { (self.table.unregister_from_event)(event.raw()) };
SdkError::from_code(result)
}
}
impl ScopedLogger<'_> {
pub fn log(self, level: LogLevel, message: &CStr) {
unsafe { (self.raw)(level as sys::ScsLogType, message.as_ptr()) };
}
pub fn message(self, message: &CStr) {
self.log(LogLevel::Message, message);
}
pub fn warning(self, message: &CStr) {
self.log(LogLevel::Warning, message);
}
pub fn error(self, message: &CStr) {
self.log(LogLevel::Error, message);
}
}
#[cfg(test)]
mod tests {
use core::ffi::c_void;
use core::sync::atomic::{AtomicUsize, Ordering};
use super::*;
static LOG_CALLS: AtomicUsize = AtomicUsize::new(0);
static EVENT_REGISTRATIONS: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn fake_log(_level: sys::ScsLogType, _message: sys::ScsString) {
LOG_CALLS.fetch_add(1, Ordering::Relaxed);
}
unsafe extern "system" fn fake_event_callback(
_event: sys::ScsEvent,
_event_info: *const c_void,
_context: sys::ScsContext,
) {
}
unsafe extern "system" fn fake_register_event(
_event: sys::ScsEvent,
_callback: sys::ScsTelemetryEventCallback,
_context: sys::ScsContext,
) -> sys::ScsResult {
EVENT_REGISTRATIONS.fetch_add(1, Ordering::Relaxed);
sys::SCS_RESULT_OK
}
unsafe extern "system" fn fake_unregister_event(_event: sys::ScsEvent) -> sys::ScsResult {
sys::SCS_RESULT_OK
}
unsafe extern "system" fn fake_register_channel(
_name: sys::ScsString,
_index: sys::ScsU32,
_type: sys::ScsValueType,
_flags: sys::ScsU32,
_callback: sys::ScsTelemetryChannelCallback,
_context: sys::ScsContext,
) -> sys::ScsResult {
sys::SCS_RESULT_OK
}
unsafe extern "system" fn fake_unregister_channel(
_name: sys::ScsString,
_index: sys::ScsU32,
_type: sys::ScsValueType,
) -> sys::ScsResult {
sys::SCS_RESULT_OK
}
fn parameters() -> sys::ScsTelemetryInitParamsV101 {
sys::ScsTelemetryInitParamsV101 {
common: sys::ScsSdkInitParamsV100 {
game_name: c"Euro Truck Simulator 2".as_ptr(),
game_id: c"eut2".as_ptr(),
game_version: 0x0001_003c,
padding: sys::ScsPadding::uninit(),
log: fake_log,
},
register_for_event: fake_register_event,
unregister_from_event: fake_unregister_event,
register_for_channel: fake_register_channel,
unregister_from_channel: fake_unregister_channel,
}
}
#[test]
fn logging_and_registration_require_a_scoped_call() {
LOG_CALLS.store(0, Ordering::Relaxed);
EVENT_REGISTRATIONS.store(0, Ordering::Relaxed);
let parameters = parameters();
let pointer = (&raw const parameters).cast::<sys::ScsTelemetryInitParams>();
let api = unsafe { TelemetryApi::from_raw(TelemetryApiVersion::V1_01, pointer) }
.expect("valid function table");
assert_eq!(api.game_id(), c"eut2");
assert_eq!(api.game_schema_version(), GameSchemaVersion::new(1, 60));
api.with_call(|call| {
call.logger().message(c"initializing");
unsafe {
call.register_event(Event::Started, fake_event_callback, core::ptr::null_mut())
}
.expect("event registration");
});
let session = api.session();
unsafe {
session.with_call(|call| call.logger().message(c"callback"));
}
assert_eq!(LOG_CALLS.load(Ordering::Relaxed), 2);
assert_eq!(EVENT_REGISTRATIONS.load(Ordering::Relaxed), 1);
}
#[test]
fn unaudited_api_version_is_rejected_before_reading_parameters() {
assert_eq!(
TelemetryApi::SUPPORTED_VERSIONS,
&[TelemetryApiVersion::V1_00, TelemetryApiVersion::V1_01],
);
assert!(TelemetryApi::supports_version(TelemetryApiVersion::V1_00));
assert!(TelemetryApi::supports_version(TelemetryApiVersion::V1_01));
assert!(!TelemetryApi::supports_version(TelemetryApiVersion::new(
1, 2
)));
let result = unsafe {
TelemetryApi::from_raw(
TelemetryApiVersion::new(1, 2),
core::ptr::null::<sys::ScsTelemetryInitParams>(),
)
};
assert!(matches!(result, Err(SdkError::Unsupported)));
}
}