use crate::bitbuf::BitWriter;
use crate::ffi::{NotThreadSafe, copy_cstr, cstring_from_buffer, vcall};
use crate::interfaces::game_server::GameClient;
use crate::rtti;
use crate::server::{InterfaceError, Server, ServerBinding};
use std::ffi::{CString, c_char, c_int, c_void};
use std::marker::PhantomData;
use std::mem::offset_of;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::NonNull;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IncomingKind {
Tick,
StringCmd,
SetConVar,
SignonState,
ClientInfo,
Move,
VoiceData,
BaselineAck,
ListenEvents,
RespondCvarValue,
FileCrcCheck,
FileMd5Check,
SaveReplay,
CmdKeyValues,
}
const SLOT: usize = size_of::<*const ()>();
macro_rules! handler_slot {
($method:ident) => {
(offset_of!(sys::IClientMessageHandler__bindgen_vtable, $method) / SLOT) as c_int
};
}
impl IncomingKind {
pub const ALL: [Self; 14] = [
Self::Tick,
Self::StringCmd,
Self::SetConVar,
Self::SignonState,
Self::ClientInfo,
Self::Move,
Self::VoiceData,
Self::BaselineAck,
Self::ListenEvents,
Self::RespondCvarValue,
Self::FileCrcCheck,
Self::FileMd5Check,
Self::SaveReplay,
Self::CmdKeyValues,
];
pub const HANDLER_SLOTS: [c_int; 14] = [
handler_slot!(IClientMessageHandler_ProcessTick),
handler_slot!(IClientMessageHandler_ProcessStringCmd),
handler_slot!(IClientMessageHandler_ProcessSetConVar),
handler_slot!(IClientMessageHandler_ProcessSignonState),
handler_slot!(IClientMessageHandler_ProcessClientInfo),
handler_slot!(IClientMessageHandler_ProcessMove),
handler_slot!(IClientMessageHandler_ProcessVoiceData),
handler_slot!(IClientMessageHandler_ProcessBaselineAck),
handler_slot!(IClientMessageHandler_ProcessListenEvents),
handler_slot!(IClientMessageHandler_ProcessRespondCvarValue),
handler_slot!(IClientMessageHandler_ProcessFileCRCCheck),
handler_slot!(IClientMessageHandler_ProcessFileMD5Check),
handler_slot!(IClientMessageHandler_ProcessSaveReplay),
handler_slot!(IClientMessageHandler_ProcessCmdKeyValues),
];
fn from_index(index: c_int) -> Option<Self> {
Self::ALL.get(usize::try_from(index).ok()?).copied()
}
const fn own_size(self) -> usize {
match self {
Self::Tick => 24,
Self::StringCmd => 1040,
Self::SetConVar => 40,
Self::SignonState | Self::BaselineAck | Self::CmdKeyValues => 16,
Self::ClientInfo | Self::ListenEvents => 72,
Self::Move | Self::VoiceData => 88,
Self::RespondCvarValue => 552,
Self::FileCrcCheck => 568,
Self::FileMd5Check => 544,
Self::SaveReplay => 280,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Verdict {
#[default]
Continue,
Block,
}
pub trait IncomingHandler: 'static {
fn incoming(&self, server: Server<'_>, message: IncomingMessage<'_>) -> Verdict;
}
#[derive(Debug, Clone, Copy)]
pub struct IncomingMessage<'s> {
kind: IncomingKind,
raw: NonNull<sys::INetMessage>,
client: GameClient<'s>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> IncomingMessage<'s> {
pub const fn kind(self) -> IncomingKind {
self.kind
}
pub const fn client(self) -> GameClient<'s> {
self.client
}
pub const fn as_ptr(self) -> *mut sys::INetMessage {
self.raw.as_ptr()
}
const fn as_const(self) -> *const sys::INetMessage {
self.raw.as_ptr().cast_const()
}
#[doc(alias = "GetType")]
pub fn id(self) -> c_int {
unsafe { vcall!(self.as_const() => INetMessage_GetType()) }
}
#[doc(alias = "GetName")]
pub fn name(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_const() => INetMessage_GetName())) }
}
#[doc(alias = "GetGroup")]
pub fn group(self) -> c_int {
unsafe { vcall!(self.as_const() => INetMessage_GetGroup()) }
}
#[doc(alias = "IsReliable")]
pub fn is_reliable(self) -> bool {
unsafe { vcall!(self.as_const() => INetMessage_IsReliable()) }
}
#[doc(alias = "ToString")]
pub fn describe(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_const() => INetMessage_ToString())) }
}
fn object_size(self) -> usize {
unsafe { vcall!(self.as_const() => INetMessage_GetSize()) }
}
pub fn decode(self) -> Option<Incoming> {
let base = message_base(self)?;
unsafe { decode(self.kind, self.raw.cast::<u8>().as_ptr().add(base)) }
}
}
static MESSAGE_BASE: AtomicUsize = AtomicUsize::new(0);
const SMALLEST_MESSAGE_BASE: usize = 24;
fn message_base(message: IncomingMessage<'_>) -> Option<usize> {
let size = message.object_size();
let base = size.checked_sub(message.kind.own_size())?;
if !(SMALLEST_MESSAGE_BASE..=256).contains(&base) || !base.is_multiple_of(SLOT) {
return None;
}
match MESSAGE_BASE.load(Ordering::Relaxed) {
0 if confirms_base(message, base) => {
MESSAGE_BASE.store(base, Ordering::Relaxed);
Some(base)
}
0 => None,
known => (known == base).then_some(base),
}
}
fn confirms_base(message: IncomingMessage<'_>, base: usize) -> bool {
let this = message.raw.cast::<u8>().as_ptr();
unsafe {
let points_at = |pointer: usize, buffer: usize| {
this.add(base + pointer)
.cast::<*const u8>()
.read_unaligned()
== this.add(base + buffer).cast_const()
};
match message.kind {
IncomingKind::StringCmd => points_at(8, 16),
IncomingKind::RespondCvarValue => points_at(16, 36) && points_at(24, 292),
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Incoming {
Tick {
tick: c_int,
host_frame_time: f32,
host_frame_time_std_deviation: f32,
},
StringCmd {
command: CString,
},
SetConVar {
convars: Vec<(CString, CString)>,
},
SignonState {
state: c_int,
spawn_count: c_int,
},
ClientInfo {
send_table_crc: u32,
server_count: c_int,
is_hltv: bool,
is_replay: bool,
friends_id: u32,
friends_name: CString,
custom_files: [u32; 4],
},
Move {
backup_commands: c_int,
new_commands: c_int,
data: BitWriter,
},
VoiceData {
data: BitWriter,
},
BaselineAck {
tick: c_int,
baseline: c_int,
},
ListenEvents {
events: [u32; 16],
},
RespondCvarValue {
cookie: c_int,
status: c_int,
name: CString,
value: CString,
},
FileCrcCheck {
path_id: CString,
file_name: CString,
md5: [u8; 16],
crc: u32,
hash_type: c_int,
length: c_int,
pack_file_number: c_int,
pack_file_id: c_int,
fraction: c_int,
},
FileMd5Check {
path_id: CString,
file_name: CString,
md5: [u8; 16],
},
SaveReplay {
start_send_byte: c_int,
file_name: CString,
post_death_record_time: f32,
},
CmdKeyValues,
}
unsafe fn read<T: Copy>(base: *const u8, offset: usize) -> T {
unsafe { base.add(offset).cast::<T>().read_unaligned() }
}
unsafe fn string<const N: usize>(base: *const u8, offset: usize) -> CString {
cstring_from_buffer(&unsafe { read::<[c_char; N]>(base, offset) })
}
unsafe fn payload(reader: *const u8, bits: c_int) -> Option<BitWriter> {
let (data, bytes, limit, position) = unsafe {
(
read::<*const u8>(reader, 0),
read::<c_int>(reader, 8),
read::<c_int>(reader, 12),
read::<c_int>(reader, 16),
)
};
let bits = usize::try_from(bits).ok()?;
let position = usize::try_from(position).ok()?;
let end = position.checked_add(bits)?;
if data.is_null()
|| end > usize::try_from(limit).ok()?
|| end > usize::try_from(bytes).ok()? * 8
{
return None;
}
let mut out = BitWriter::with_capacity(bits);
for bit in position..end {
let byte = unsafe { data.add(bit / 8).read() };
out.write_bit(byte >> (bit % 8) & 1 != 0);
}
Some(out)
}
unsafe fn decode(kind: IncomingKind, base: *const u8) -> Option<Incoming> {
unsafe {
Some(match kind {
IncomingKind::Tick => Incoming::Tick {
tick: read(base, 8),
host_frame_time: read(base, 12),
host_frame_time_std_deviation: read(base, 16),
},
IncomingKind::StringCmd => Incoming::StringCmd {
command: string::<1024>(base, 16),
},
IncomingKind::SetConVar => Incoming::SetConVar {
convars: convars(base)?,
},
IncomingKind::SignonState => Incoming::SignonState {
state: read(base, 8),
spawn_count: read(base, 12),
},
IncomingKind::ClientInfo => Incoming::ClientInfo {
send_table_crc: read(base, 8),
server_count: read(base, 12),
is_hltv: read::<u8>(base, 16) != 0,
is_replay: read::<u8>(base, 17) != 0,
friends_id: read(base, 20),
friends_name: string::<32>(base, 24),
custom_files: read(base, 56),
},
IncomingKind::Move => Incoming::Move {
backup_commands: read(base, 8),
new_commands: read(base, 12),
data: payload(base.add(24), read(base, 16))?,
},
IncomingKind::VoiceData => Incoming::VoiceData {
data: payload(base.add(16), read(base, 8))?,
},
IncomingKind::BaselineAck => Incoming::BaselineAck {
tick: read(base, 8),
baseline: read(base, 12),
},
IncomingKind::ListenEvents => Incoming::ListenEvents {
events: read(base, 8),
},
IncomingKind::RespondCvarValue => Incoming::RespondCvarValue {
cookie: read(base, 8),
status: read(base, 32),
name: string::<256>(base, 36),
value: string::<256>(base, 292),
},
IncomingKind::FileCrcCheck => Incoming::FileCrcCheck {
path_id: string::<260>(base, 8),
file_name: string::<260>(base, 268),
md5: read(base, 528),
crc: read(base, 544),
hash_type: read(base, 548),
length: read(base, 552),
pack_file_number: read(base, 556),
pack_file_id: read(base, 560),
fraction: read(base, 564),
},
IncomingKind::FileMd5Check => Incoming::FileMd5Check {
path_id: string::<260>(base, 8),
file_name: string::<260>(base, 268),
md5: read(base, 528),
},
IncomingKind::SaveReplay => Incoming::SaveReplay {
start_send_byte: read(base, 8),
file_name: string::<260>(base, 12),
post_death_record_time: read(base, 272),
},
IncomingKind::CmdKeyValues => Incoming::CmdKeyValues,
})
}
}
const MAX_CONVARS: usize = 255;
const CONVAR_TEXT: usize = 260;
unsafe fn convars(base: *const u8) -> Option<Vec<(CString, CString)>> {
let (memory, capacity, count, elements) = unsafe {
(
read::<*const u8>(base, 8),
read::<c_int>(base, 16),
read::<c_int>(base, 24),
read::<*const u8>(base, 32),
)
};
let count = usize::try_from(count).ok()?;
if count > MAX_CONVARS
|| count > usize::try_from(capacity).ok()?
|| (count > 0 && (memory.is_null() || memory != elements))
{
return None;
}
Some(
(0..count)
.map(|index| {
let convar = index * 2 * CONVAR_TEXT;
unsafe {
(
string::<CONVAR_TEXT>(memory, convar),
string::<CONVAR_TEXT>(memory, convar + CONVAR_TEXT),
)
}
})
.collect(),
)
}
static HANDLER_OFFSET: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy)]
pub struct HookTarget {
pub handler: NonNull<c_void>,
pub slots: [c_int; 14],
}
#[derive(Debug, thiserror::Error)]
pub enum HookTargetError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the engine has no game server")]
NoServer,
#[error("the server has no client objects yet")]
NotReady,
#[error("the engine's clients are not laid out as expected")]
UnexpectedLayout,
}
pub fn hook_target(server: Server<'_>) -> Result<HookTarget, HookTargetError> {
let client = server
.valve_engine()?
.game_server()
.ok_or(HookTargetError::NoServer)?
.client(0)
.ok_or(HookTargetError::NotReady)?;
let client = client.as_ptr().cast::<c_void>().cast_const();
let client_offset = unsafe { rtti::subobject_offset(client, "CGameClient") };
if client_offset != Some(SLOT as isize) {
return Err(HookTargetError::UnexpectedLayout);
}
let handler = unsafe { client.byte_add(SLOT) };
let handler_offset = unsafe { rtti::subobject_offset(handler, "CGameClient") };
if handler_offset != Some(2 * SLOT as isize) {
return Err(HookTargetError::UnexpectedLayout);
}
HANDLER_OFFSET.store(SLOT, Ordering::Relaxed);
Ok(HookTarget {
handler: NonNull::new(handler.cast_mut()).ok_or(HookTargetError::UnexpectedLayout)?,
slots: IncomingKind::HANDLER_SLOTS,
})
}
pub unsafe fn route_incoming(
binding: &ServerBinding,
handler: &dyn IncomingHandler,
kind: c_int,
this: NonNull<c_void>,
message: NonNull<c_void>,
) -> Verdict {
let (Some(kind), offset @ 1..) = (
IncomingKind::from_index(kind),
HANDLER_OFFSET.load(Ordering::Relaxed),
) else {
return Verdict::Continue;
};
let scope = ();
let server = unsafe { binding.server(&scope) };
let client = unsafe { GameClient::from_raw(this.byte_sub(offset).cast()) };
let message = IncomingMessage {
kind,
raw: message.cast(),
client,
_scope: PhantomData,
_not_thread_safe: PhantomData,
};
catch_unwind(AssertUnwindSafe(|| handler.incoming(server, message)))
.unwrap_or(Verdict::Continue)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn handler_slots_follow_the_abi() {
let destructors = if cfg!(target_os = "windows") { 1 } else { 2 };
assert_eq!(IncomingKind::HANDLER_SLOTS[0], destructors);
assert_eq!(IncomingKind::HANDLER_SLOTS[6], destructors + 6);
assert_eq!(IncomingKind::HANDLER_SLOTS[13], destructors + 13);
}
#[test]
fn kinds_map_to_indices() {
assert_eq!(IncomingKind::from_index(1), Some(IncomingKind::StringCmd));
assert_eq!(IncomingKind::from_index(14), None);
assert_eq!(IncomingKind::from_index(-1), None);
}
}