#[cfg(test)]
#[path = "../tests/net/incoming.rs"]
mod tests;
use crate::NotThreadSafe;
use crate::bitbuf::BitWriter;
use crate::interfaces::game_server::GameClient;
use crate::server::{InterfaceError, Server, ServerBinding};
use sdk_raw::net::incoming::{
self as raw, ClientLayoutError, ClientMessage, MessageClass, StringCmdError,
};
use sdk_raw::util;
use sdk_raw::util::cstr::{copy_cstr, cstring_from_buffer};
use sdk_raw::vcall;
use std::ffi::{CStr, CString, c_int};
use std::marker::PhantomData;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::NonNull;
#[derive(Debug, Clone, Copy)]
pub struct HookTarget {
pub handler: NonNull<sys::IClientMessageHandler>,
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,
}
impl From<ClientLayoutError> for HookTargetError {
fn from(_: ClientLayoutError) -> Self {
Self::UnexpectedLayout
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Incoming {
#[doc(alias("NET_Tick"))]
Tick {
tick: c_int,
host_frame_time: f32,
host_frame_time_std_deviation: f32,
},
#[doc(alias("NET_StringCmd"))]
StringCmd {
command: CString,
},
#[doc(alias("NET_SetConVar"))]
SetConVar {
convars: Vec<(CString, CString)>,
},
#[doc(alias("NET_SignonState"))]
SignonState {
state: c_int,
spawn_count: c_int,
},
#[doc(alias("CLC_ClientInfo"))]
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],
},
#[doc(alias("CLC_Move"))]
Move {
backup_commands: c_int,
new_commands: c_int,
data: BitWriter,
},
#[doc(alias("CLC_VoiceData"))]
VoiceData {
data: BitWriter,
},
#[doc(alias("CLC_BaselineAck"))]
BaselineAck {
tick: c_int,
baseline: c_int,
},
#[doc(alias("CLC_ListenEvents"))]
ListenEvents {
events: [u32; 16],
},
#[doc(alias("CLC_RespondCvarValue"))]
RespondCvarValue {
cookie: c_int,
status: c_int,
name: CString,
value: CString,
},
#[doc(alias("CLC_FileCRCCheck"))]
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,
},
#[doc(alias("CLC_FileMD5Check"))]
FileMd5Check {
path_id: CString,
file_name: CString,
md5: [u8; 16],
},
#[doc(alias("CLC_SaveReplay"))]
SaveReplay {
start_send_byte: c_int,
file_name: CString,
post_death_record_time: f32,
},
#[doc(alias("CLC_CmdKeyValues"))]
CmdKeyValues,
}
impl Incoming {
fn from_raw(message: ClientMessage) -> Self {
match message {
ClientMessage::Tick(fields) => Self::Tick {
tick: fields.tick,
host_frame_time: fields.host_frame_time,
host_frame_time_std_deviation: fields.host_frame_time_std_deviation,
},
ClientMessage::StringCmd(fields) => Self::StringCmd {
command: cstring_from_buffer(&fields.command_buffer),
},
ClientMessage::SetConVar(convars) => Self::SetConVar {
convars: convars
.iter()
.map(|convar| {
(
cstring_from_buffer(&convar.name),
cstring_from_buffer(&convar.value),
)
})
.collect(),
},
ClientMessage::SignonState(fields) => Self::SignonState {
state: fields.signon_state,
spawn_count: fields.spawn_count,
},
ClientMessage::ClientInfo(fields) => Self::ClientInfo {
send_table_crc: fields.send_table_crc,
server_count: fields.server_count,
is_hltv: fields.is_hltv != 0,
is_replay: fields.is_replay != 0,
friends_id: fields.friends_id,
friends_name: cstring_from_buffer(&fields.friends_name),
custom_files: fields.custom_files,
},
ClientMessage::Move { fields, data } => Self::Move {
backup_commands: fields.backup_commands,
new_commands: fields.new_commands,
data: data.into(),
},
ClientMessage::VoiceData { data, .. } => Self::VoiceData { data: data.into() },
ClientMessage::BaselineAck(fields) => Self::BaselineAck {
tick: fields.baseline_tick,
baseline: fields.baseline_number,
},
ClientMessage::ListenEvents(fields) => Self::ListenEvents {
events: fields.events,
},
ClientMessage::RespondCvarValue(fields) => Self::RespondCvarValue {
cookie: fields.cookie,
status: fields.status,
name: cstring_from_buffer(&fields.name_buffer),
value: cstring_from_buffer(&fields.value_buffer),
},
ClientMessage::FileCrcCheck(fields) => Self::FileCrcCheck {
path_id: cstring_from_buffer(&fields.path_id),
file_name: cstring_from_buffer(&fields.file_name),
md5: fields.md5,
crc: fields.crc,
hash_type: fields.hash_type,
length: fields.length,
pack_file_number: fields.pack_file_number,
pack_file_id: fields.pack_file_id,
fraction: fields.fraction,
},
ClientMessage::FileMd5Check(fields) => Self::FileMd5Check {
path_id: cstring_from_buffer(&fields.path_id),
file_name: cstring_from_buffer(&fields.file_name),
md5: fields.md5,
},
ClientMessage::SaveReplay(fields) => Self::SaveReplay {
start_send_byte: fields.start_send_byte,
file_name: cstring_from_buffer(&fields.file_name),
post_death_record_time: fields.post_death_record_time,
},
ClientMessage::CmdKeyValues(_) => Self::CmdKeyValues,
}
}
}
pub trait IncomingHandler: 'static {
fn incoming(&self, server: Server<'_>, message: IncomingMessage<'_>) -> Verdict;
}
#[doc(alias("IClientMessageHandler"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IncomingKind {
#[doc(alias("ProcessTick"))]
Tick,
#[doc(alias("ProcessStringCmd"))]
StringCmd,
#[doc(alias("ProcessSetConVar"))]
SetConVar,
#[doc(alias("ProcessSignonState"))]
SignonState,
#[doc(alias("ProcessClientInfo"))]
ClientInfo,
#[doc(alias("ProcessMove"))]
Move,
#[doc(alias("ProcessVoiceData"))]
VoiceData,
#[doc(alias("ProcessBaselineAck"))]
BaselineAck,
#[doc(alias("ProcessListenEvents"))]
ListenEvents,
#[doc(alias("ProcessRespondCvarValue"))]
RespondCvarValue,
#[doc(alias("ProcessFileCRCCheck"))]
FileCrcCheck,
#[doc(alias("ProcessFileMD5Check"))]
FileMd5Check,
#[doc(alias("ProcessSaveReplay"))]
SaveReplay,
#[doc(alias("ProcessCmdKeyValues"))]
CmdKeyValues,
}
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] = {
let mut slots = [0; 14];
let mut index = 0;
while index < slots.len() {
let slot = Self::ALL[index].raw().process_slot();
assert!(slot <= c_int::MAX as usize);
slots[index] = slot as c_int;
index += 1;
}
slots
};
fn from_index(index: c_int) -> Option<Self> {
Self::ALL.get(usize::try_from(index).ok()?).copied()
}
const fn raw(self) -> MessageClass {
match self {
Self::Tick => MessageClass::Tick,
Self::StringCmd => MessageClass::StringCmd,
Self::SetConVar => MessageClass::SetConVar,
Self::SignonState => MessageClass::SignonState,
Self::ClientInfo => MessageClass::ClientInfo,
Self::Move => MessageClass::Move,
Self::VoiceData => MessageClass::VoiceData,
Self::BaselineAck => MessageClass::BaselineAck,
Self::ListenEvents => MessageClass::ListenEvents,
Self::RespondCvarValue => MessageClass::RespondCvarValue,
Self::FileCrcCheck => MessageClass::FileCrcCheck,
Self::FileMd5Check => MessageClass::FileMd5Check,
Self::SaveReplay => MessageClass::SaveReplay,
Self::CmdKeyValues => MessageClass::CmdKeyValues,
}
}
}
#[doc(alias("INetMessage"))]
#[derive(Debug, Clone, Copy)]
pub struct IncomingMessage<'s> {
kind: IncomingKind,
raw: NonNull<sys::INetMessage>,
client: GameClient<'s>,
handler: NonNull<sys::IClientMessageHandler>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> IncomingMessage<'s> {
const fn as_const(self) -> *const sys::INetMessage {
self.raw.as_ptr().cast_const()
}
pub const fn as_ptr(self) -> *mut sys::INetMessage {
self.raw.as_ptr()
}
pub const fn client(self) -> GameClient<'s> {
self.client
}
pub fn decode(self) -> Option<Incoming> {
let message = unsafe { raw::read_message(self.kind.raw(), self.handler, self.raw) }?;
Some(Incoming::from_raw(message))
}
#[doc(alias("ToString"))]
pub fn describe(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_const() => INetMessage_ToString())) }
}
#[doc(alias("GetGroup"))]
pub fn group(self) -> c_int {
unsafe { vcall!(self.as_const() => INetMessage_GetGroup()) }
}
#[doc(alias("GetType"))]
pub fn id(self) -> c_int {
unsafe { vcall!(self.as_const() => INetMessage_GetType()) }
}
#[doc(alias("IsReliable"))]
pub fn is_reliable(self) -> bool {
unsafe { vcall!(self.as_const() => INetMessage_IsReliable()) }
}
pub const fn kind(self) -> IncomingKind {
self.kind
}
#[doc(alias("GetName"))]
pub fn name(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_const() => INetMessage_GetName())) }
}
}
impl<'s> GameClient<'s> {
#[doc(alias("ProcessStringCmd", "NET_StringCmd"))]
pub unsafe fn process_string_command(self, command: &CStr) -> Result<bool, StringCommandError> {
if command.count_bytes() > raw::MAX_STRING_CMD_LEN {
return Err(StringCommandError::TooLong);
}
if self.is_fake() {
return Err(StringCommandError::FakeClient);
}
let channel = self
.net_channel()
.and_then(|channel| NonNull::new(channel.as_ptr()))
.ok_or(StringCommandError::NoChannel)?;
let client = NonNull::new(self.as_ptr()).ok_or(StringCommandError::UnexpectedLayout)?;
let handler = unsafe { raw::handler_of_client(client) }?;
let engine = unsafe { raw::engine_factory_of_client(client) }?;
let vtable = unsafe { raw::cached_string_cmd_vtable(engine) }?
.ok_or(StringCommandError::NoVtable)?;
let mut message = unsafe { raw::StringCmdMessage::new(vtable, handler, channel, command) }?;
Ok(unsafe { message.process() })
}
}
#[derive(Debug, thiserror::Error)]
pub enum StringCommandError {
#[error("the client is a fake client, such as a bot")]
FakeClient,
#[error("the client has no channel")]
NoChannel,
#[error("the command is longer than {} bytes", raw::MAX_STRING_CMD_LEN)]
TooLong,
#[error("the engine has no unique vtable of NET_StringCmd")]
NoVtable,
#[error("the engine's clients or string commands are not laid out as expected")]
UnexpectedLayout,
#[error("the engine module could not be inspected: {0}")]
Image(#[from] util::Error),
}
impl From<ClientLayoutError> for StringCommandError {
fn from(_: ClientLayoutError) -> Self {
Self::UnexpectedLayout
}
}
impl From<StringCmdError> for StringCommandError {
fn from(error: StringCmdError) -> Self {
match error {
StringCmdError::TooLong => Self::TooLong,
StringCmdError::UnexpectedLayout => Self::UnexpectedLayout,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Verdict {
#[default]
Continue,
Block,
}
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 = NonNull::new(client.as_ptr()).ok_or(HookTargetError::NotReady)?;
let handler = unsafe { raw::handler_of_client(client) }?;
Ok(HookTarget {
handler,
slots: IncomingKind::HANDLER_SLOTS,
})
}
#[cfg(test)]
pub(crate) const unsafe fn mock_incoming_message<'s>(
kind: IncomingKind,
raw: NonNull<sys::INetMessage>,
client: GameClient<'s>,
) -> IncomingMessage<'s> {
IncomingMessage {
kind,
raw,
client,
handler: NonNull::dangling(),
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
pub unsafe fn route_incoming(
binding: &ServerBinding,
handler: &dyn IncomingHandler,
kind: c_int,
this: NonNull<sys::IClientMessageHandler>,
message: NonNull<sys::INetMessage>,
) -> Verdict {
let Some(kind) = IncomingKind::from_index(kind) else {
return Verdict::Continue;
};
let Some(client) = (unsafe { raw::client_of_handler(this) }) else {
return Verdict::Continue;
};
let scope = ();
let server = unsafe { binding.server(&scope) };
let client = unsafe { GameClient::from_raw(client) };
let message = IncomingMessage {
kind,
raw: message,
client,
handler: this,
_scope: PhantomData,
_not_thread_safe: PhantomData,
};
catch_unwind(AssertUnwindSafe(|| handler.incoming(server, message)))
.unwrap_or(Verdict::Continue)
}