#[cfg(test)]
#[path = "tests/gc_hooks.rs"]
mod tests;
use crate::MetamodApi;
use crate::hook::{
Handler, HookAction, HookCall, HookError, HookId, HookTarget, HookTiming, VirtualFunction,
};
use source_sdk_2013::raw::steam::{ISteamGameCoordinator, SEND_MESSAGE_SLOT, SendMessageFn};
use source_sdk_2013::steam::{GameCoordinator, GcResult, MessageType, SteamClientError};
use std::cell::Cell;
use std::ffi::c_void;
use std::ptr::NonNull;
pub type GcSendFn = fn(&GcSend<'_>) -> GcSendAction;
const SEND_MESSAGE: VirtualFunction<SendMessageFn> = VirtualFunction::new(SEND_MESSAGE_SLOT);
static ROUTE: GcRoute = GcRoute::new();
#[derive(Debug, thiserror::Error)]
pub enum GcHookError {
#[error(transparent)]
SteamClient(#[from] SteamClientError),
#[error(transparent)]
Hook(#[from] HookError),
}
struct GcRoute {
state: Cell<Option<RoutedGc>>,
}
impl GcRoute {
const fn new() -> Self {
Self {
state: Cell::new(None),
}
}
}
impl Handler<SendMessageFn> for GcRoute {
fn call(&self, call: &HookCall<'_, SendMessageFn>) -> HookAction<i32> {
let Some(route) = self.state.get() else {
return HookAction::Ignore;
};
let (wire, data, len) = call.args();
let (message_type, protobuf) = MessageType::from_wire(wire);
let data = match data.is_null() || len == 0 {
true => &[][..],
false => unsafe { std::slice::from_raw_parts(data.cast::<u8>(), len as usize) },
};
let send = GcSend {
message_type,
protobuf,
data,
};
match (route.callback)(&send) {
GcSendAction::Pass => HookAction::Ignore,
GcSendAction::Block(result) => HookAction::Supersede(result.as_raw()),
}
}
}
unsafe impl Sync for GcRoute {}
#[derive(Debug, Clone, Copy)]
pub struct GcSend<'a> {
pub message_type: MessageType,
pub protobuf: bool,
pub data: &'a [u8],
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum GcSendAction {
#[default]
Pass,
Block(GcResult),
}
#[derive(Clone, Copy)]
struct RoutedGc {
callback: GcSendFn,
hook: HookId,
vtable: usize,
}
impl MetamodApi<'_> {
pub fn hook_gc_send(
self,
coordinator: GameCoordinator<'_>,
callback: GcSendFn,
) -> Result<HookId, GcHookError> {
let object = NonNull::new(coordinator.as_ptr()).ok_or(HookError::InvalidArgument)?;
if self.routed(unsafe { vtable_of(object) }).is_some() {
return Err(HookError::AlreadyInstalled.into());
}
coordinator.pin_steam_client()?;
Ok(unsafe { self.install_send(object, callback) }?)
}
unsafe fn install_send(
self,
object: NonNull<ISteamGameCoordinator>,
callback: GcSendFn,
) -> Result<HookId, HookError> {
let vtable = unsafe { vtable_of(object) };
match ROUTE.state.get() {
Some(state) if self.has_hook(state.hook) => {
return Err(match state.vtable == vtable {
true => HookError::AlreadyInstalled,
false => HookError::TooManyFunctions,
});
}
_ => {}
}
let hook = unsafe {
self.add_hook(
SEND_MESSAGE,
HookTarget::class_of(object),
HookTiming::Pre,
&ROUTE,
)
}?;
ROUTE.state.set(Some(RoutedGc {
callback,
hook,
vtable,
}));
Ok(hook)
}
pub fn is_gc_send_hooked(self, coordinator: GameCoordinator<'_>) -> bool {
let Some(object) = NonNull::new(coordinator.as_ptr()) else {
return false;
};
let vtable = unsafe { vtable_of(object) };
if self.routed(vtable).is_none() {
return false;
}
let Ok(original) =
(unsafe { self.original_function(SEND_MESSAGE, HookTarget::class_of(object)) })
else {
return false;
};
let current = unsafe {
(vtable as *const *const c_void)
.add(SEND_MESSAGE_SLOT)
.read()
};
current != original as *const c_void
}
fn routed(self, vtable: usize) -> Option<HookId> {
ROUTE
.state
.get()
.filter(|state| state.vtable == vtable && self.has_hook(state.hook))
.map(|state| state.hook)
}
}
unsafe fn vtable_of(object: NonNull<ISteamGameCoordinator>) -> usize {
unsafe { (&raw const (*object.as_ptr()).vtable_).read() }.addr()
}