use crate::datatables::{NetProp, NetPropError, ServerClass, ServerClasses, StandardSendProxies};
use crate::entities::Entity;
use crate::ffi::{NotThreadSafe, buffer_from_cstr, copy_cstr, cstring_from_buffer, vcall};
use std::cell::RefCell;
use std::ffi::{CStr, CString, VaList, c_char, c_int};
use std::marker::PhantomData;
use std::ptr::{self, NonNull};
const MAX_PATH: usize = 260;
const USER_MESSAGE_NAME_CAPACITY: usize = 256;
const SAVE_COMMENT_CAPACITY: usize = 256;
const STATUS_LINE_CAPACITY: usize = 1024;
const MAX_USER_MESSAGES: usize = 1024;
const MAX_WORKSHOP_MAPS: u32 = 1 << 16;
interface! {
#[doc(alias = "IServerGameDLL")]
#[doc(alias = "CServerGameDLL")]
pub struct ServerGameDll(sys::IServerGameDLL) = GameServer c"ServerGameDLL012";
}
impl<'s> ServerGameDll<'s> {
#[doc(alias = "GetTickInterval")]
pub fn tick_interval(self) -> f32 {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_GetTickInterval()) }
}
#[doc(alias = "GetAllServerClasses")]
pub fn server_classes(self) -> ServerClasses<'s> {
let head = unsafe { vcall!(self.as_ptr() => IServerGameDLL_GetAllServerClasses()) };
unsafe { ServerClasses::new(head) }
}
pub fn server_class(self, name: &CStr) -> Option<ServerClass<'s>> {
self.server_classes().find(|class| class.name() == name)
}
#[doc(alias = "GetGameDescription")]
pub fn game_description(self) -> CString {
unsafe { copy_cstr(vcall!(self.as_ptr() => IServerGameDLL_GetGameDescription())) }
.unwrap_or_default()
}
#[doc(alias = "GetSaveComment")]
pub fn save_comment(self, minutes: f32, seconds: f32, include_time: bool) -> CString {
let mut comment = [0 as c_char; SAVE_COMMENT_CAPACITY];
unsafe {
vcall!(self.as_ptr() => IServerGameDLL_GetSaveComment(comment.as_mut_ptr(), SAVE_COMMENT_CAPACITY as c_int, minutes, seconds, !include_time))
};
cstring_from_buffer(&comment)
}
#[doc(alias = "IsRestoring")]
pub fn is_restoring(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_IsRestoring()) }
}
#[doc(alias = "GetUserMessageInfo")]
pub fn user_message(self, index: usize) -> Option<UserMessage> {
let message_type = c_int::try_from(index).ok()?;
let mut name = [0 as c_char; USER_MESSAGE_NAME_CAPACITY];
let mut size = 0;
let found = unsafe {
vcall!(self.as_ptr() => IServerGameDLL_GetUserMessageInfo(message_type, name.as_mut_ptr(), USER_MESSAGE_NAME_CAPACITY as c_int, &mut size))
};
found.then(|| UserMessage {
index,
name: cstring_from_buffer(&name),
size: usize::try_from(size).ok(),
})
}
pub fn user_messages(self) -> impl Iterator<Item = UserMessage> + use<'s> {
(0..MAX_USER_MESSAGES).map_while(move |index| self.user_message(index))
}
#[doc(alias = "GetStandardSendProxies")]
pub fn standard_send_proxies(self) -> Option<StandardSendProxies<'s>> {
let proxies = NonNull::new(unsafe {
vcall!(self.as_ptr() => IServerGameDLL_GetStandardSendProxies())
})?;
Some(unsafe { StandardSendProxies::from_raw(proxies) })
}
pub fn net_prop(
self,
class: ServerClass<'s>,
name: &CStr,
) -> Result<NetProp<'s>, NetPropError> {
let table = class.table().ok_or_else(|| NetPropError::NoTable {
class: class.name().to_string_lossy().into_owned(),
})?;
let proxies = self
.standard_send_proxies()
.ok_or(NetPropError::NoStandardProxies)?;
NetProp::resolve(table, name, proxies)
}
pub fn entity_net_prop(
self,
entity: Entity<'s>,
name: &CStr,
) -> Result<NetProp<'s>, NetPropError> {
let class = entity
.server_class()
.ok_or_else(|| NetPropError::NotNetworked {
class_name: entity.class_name().to_string_lossy().into_owned(),
})?;
self.net_prop(class, name)
}
#[doc(alias = "ShouldHideServer")]
pub fn should_hide_server(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_ShouldHideServer()) }
}
#[doc(alias = "InvalidateMdlCache")]
pub fn invalidate_mdl_cache(self) {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_InvalidateMdlCache()) };
}
#[doc(alias = "GetServerGCLobby")]
pub fn gc_lobby(self) -> Option<ServerGcLobby<'s>> {
let lobby =
NonNull::new(unsafe { vcall!(self.as_ptr() => IServerGameDLL_GetServerGCLobby()) })?;
Some(ServerGcLobby {
raw: lobby,
_scope: PhantomData,
_not_thread_safe: PhantomData,
})
}
#[doc(alias = "GetServerBrowserMapOverride")]
pub fn server_browser_map_override(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_ptr() => IServerGameDLL_GetServerBrowserMapOverride())) }
}
#[doc(alias = "GetServerBrowserGameData")]
pub fn server_browser_game_data(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_ptr() => IServerGameDLL_GetServerBrowserGameData())) }
}
#[doc(alias = "Status")]
pub fn status(self) -> CString {
let previous = STATUS_OUTPUT.replace(Some(Vec::new()));
unsafe { vcall!(self.as_ptr() => IServerGameDLL_Status(Some(append_status))) };
let output = STATUS_OUTPUT.replace(previous).unwrap_or_default();
unsafe { CString::from_vec_unchecked(output) }
}
#[doc(alias = "PrepareLevelResources")]
pub fn prepare_level_resources(self, map: &CStr) -> Result<LevelResources, MapNameTooLong> {
let (mut map_name, mut map_file) = level_buffers(map)?;
unsafe {
vcall!(self.as_ptr() => IServerGameDLL_PrepareLevelResources(map_name.as_mut_ptr(), MAX_PATH, map_file.as_mut_ptr(), MAX_PATH))
};
Ok(LevelResources::from_buffers(&map_name, &map_file))
}
#[doc(alias = "AsyncPrepareLevelResources")]
pub fn async_prepare_level_resources(
self,
map: &CStr,
) -> Result<LevelPreparation, MapNameTooLong> {
let (mut map_name, mut map_file) = level_buffers(map)?;
let mut progress = 0.0;
let result = unsafe {
vcall!(self.as_ptr() => IServerGameDLL_AsyncPrepareLevelResources(map_name.as_mut_ptr(), MAX_PATH, map_file.as_mut_ptr(), MAX_PATH, &mut progress))
};
let resources = LevelResources::from_buffers(&map_name, &map_file);
Ok(match result {
sys::IServerGameDLL_ePrepareLevelResourcesResult_ePrepareLevelResources_InProgress => {
LevelPreparation::InProgress {
resources,
progress,
}
}
_ => LevelPreparation::Prepared(resources),
})
}
#[doc(alias = "CanProvideLevel")]
pub fn can_provide_level(self, map: &CStr) -> Result<LevelProvision, MapNameTooLong> {
let (mut map_name, _) = level_buffers(map)?;
let result = unsafe {
vcall!(self.as_ptr() => IServerGameDLL_CanProvideLevel(map_name.as_mut_ptr(), MAX_PATH as c_int))
};
Ok(match result {
sys::IServerGameDLL_eCanProvideLevelResult_eCanProvideLevel_CanProvide => {
LevelProvision::CanProvide(cstring_from_buffer(&map_name))
}
sys::IServerGameDLL_eCanProvideLevelResult_eCanProvideLevel_Possibly => {
LevelProvision::Possibly
}
_ => LevelProvision::CannotProvide,
})
}
#[doc(alias = "IsManualMapChangeOkay")]
pub fn is_manual_map_change_okay(self) -> Result<(), MapChangeRefused> {
let mut reason = ptr::null();
let okay =
unsafe { vcall!(self.as_ptr() => IServerGameDLL_IsManualMapChangeOkay(&mut reason)) };
if okay {
return Ok(());
}
Err(MapChangeRefused {
reason: unsafe { copy_cstr(reason) }
.map(|reason| reason.to_string_lossy().into_owned()),
})
}
#[doc(alias = "GetWorkshopMap")]
pub fn workshop_map(self, index: u32) -> Option<WorkshopMap> {
let mut description: sys::WorkshopMapDesc_t = unsafe { std::mem::zeroed() };
let found = unsafe {
vcall!(self.as_ptr() => IServerGameDLL_GetWorkshopMap(index, &mut description))
};
found.then(|| WorkshopMap {
name: cstring_from_buffer(&description.szMapName),
original_name: cstring_from_buffer(&description.szOriginalMapName),
timestamp: description.uTimestamp,
downloaded: description.bDownloaded,
})
}
pub fn workshop_maps(self) -> impl Iterator<Item = WorkshopMap> + use<'s> {
(0..MAX_WORKSHOP_MAPS).map_while(move |index| self.workshop_map(index))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserMessage {
pub index: usize,
pub name: CString,
pub size: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LevelResources {
pub map_name: CString,
pub map_file: CString,
}
impl LevelResources {
fn from_buffers(map_name: &[c_char], map_file: &[c_char]) -> Self {
Self {
map_name: cstring_from_buffer(map_name),
map_file: cstring_from_buffer(map_file),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LevelPreparation {
Prepared(LevelResources),
InProgress {
resources: LevelResources,
progress: f32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LevelProvision {
CannotProvide,
CanProvide(CString),
Possibly,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WorkshopMap {
pub name: CString,
pub original_name: CString,
pub timestamp: u32,
pub downloaded: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("map name `{name}` does not fit in the game's {MAX_PATH}-byte buffer")]
pub struct MapNameTooLong {
name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("the game refuses map changes right now{}", .reason.as_deref().map(|reason| format!(": {reason}")).unwrap_or_default())]
pub struct MapChangeRefused {
pub reason: Option<String>,
}
#[doc(alias = "IServerGCLobby")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ServerGcLobby<'s> {
raw: NonNull<sys::IServerGCLobby>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> ServerGcLobby<'s> {
pub const fn as_ptr(self) -> *mut sys::IServerGCLobby {
self.raw.as_ptr()
}
#[doc(alias = "HasLobby")]
pub fn has_lobby(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGCLobby_HasLobby()) }
}
#[doc(alias = "SteamIDAllowedToConnect")]
pub fn steam_id_allowed_to_connect(self, steam_id: u64) -> bool {
let steam_id = steam_id_of(steam_id);
unsafe { vcall!(self.as_ptr() => IServerGCLobby_SteamIDAllowedToConnect(&steam_id)) }
}
#[doc(alias = "UpdateServerDetails")]
pub fn update_server_details(self) {
unsafe { vcall!(self.as_ptr() => IServerGCLobby_UpdateServerDetails()) };
}
#[doc(alias = "ShouldHibernate")]
pub fn should_hibernate(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGCLobby_ShouldHibernate()) }
}
#[doc(alias = "MatchAllowsNameChanges")]
pub fn match_allows_name_changes(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGCLobby_MatchAllowsNameChanges()) }
}
#[doc(alias = "GetPlayerGCMatchName")]
pub fn player_gc_match_name(self, steam_id: u64) -> Option<CString> {
let steam_id = steam_id_of(steam_id);
let mut name = [0 as c_char; MAX_PATH];
let found = unsafe {
vcall!(self.as_ptr() => IServerGCLobby_GetPlayerGCMatchName(&steam_id, name.as_mut_ptr(), MAX_PATH))
};
found.then(|| cstring_from_buffer(&name))
}
}
fn steam_id_of(steam_id: u64) -> sys::CSteamID {
sys::CSteamID {
m_steamid: sys::CSteamID_SteamID_t {
m_unAll64Bits: steam_id,
},
}
}
fn level_buffers(map: &CStr) -> Result<([c_char; MAX_PATH], [c_char; MAX_PATH]), MapNameTooLong> {
let too_long = || MapNameTooLong {
name: map.to_string_lossy().into_owned(),
};
let file =
CString::new(format!("maps/{}.bsp", map.to_string_lossy())).map_err(|_| too_long())?;
Ok((
buffer_from_cstr(map).ok_or_else(too_long)?,
buffer_from_cstr(&file).ok_or_else(too_long)?,
))
}
thread_local! {
static STATUS_OUTPUT: RefCell<Option<Vec<u8>>> = const { RefCell::new(None) };
}
unsafe extern "C" {
#[cfg(target_os = "windows")]
fn __stdio_common_vsprintf(
options: u64,
buffer: *mut c_char,
count: usize,
format: *const c_char,
locale: *mut std::ffi::c_void,
arguments: VaList<'_>,
) -> c_int;
#[cfg(not(target_os = "windows"))]
fn vsnprintf(
buffer: *mut c_char,
count: usize,
format: *const c_char,
arguments: VaList<'_>,
) -> c_int;
}
unsafe fn format_into(
buffer: &mut [c_char],
format: *const c_char,
arguments: VaList<'_>,
) -> c_int {
cfg_select! {
target_os = "windows" => {
const STANDARD_SNPRINTF_BEHAVIOR: u64 = 1 << 1;
unsafe {
__stdio_common_vsprintf(
STANDARD_SNPRINTF_BEHAVIOR,
buffer.as_mut_ptr(),
buffer.len(),
format,
ptr::null_mut(),
arguments,
)
}
}
_ => {
unsafe { vsnprintf(buffer.as_mut_ptr(), buffer.len(), format, arguments) }
}
}
}
unsafe extern "C" fn append_status(format: *const c_char, arguments: ...) {
if format.is_null() {
return;
}
let mut line = [0 as c_char; STATUS_LINE_CAPACITY];
if unsafe { format_into(&mut line, format, arguments) } < 0 {
return;
}
let line = cstring_from_buffer(&line);
STATUS_OUTPUT.with_borrow_mut(|output| {
if let Some(output) = output {
output.extend_from_slice(line.as_bytes());
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::test_support::{mock_vtable, unexpected_call};
unsafe extern "C" fn tick_interval(_: *const sys::IServerGameDLL) -> f32 {
0.015
}
unsafe extern "C" fn status(
_: *mut sys::IServerGameDLL,
print: Option<unsafe extern "C" fn(*const c_char, ...)>,
) {
let print = print.unwrap();
unsafe {
print(c"Blue Team Wins: %d\n".as_ptr(), 3 as c_int);
print(c"%-8s %6.1f\n".as_ptr(), c"Scout".as_ptr(), 2.5f64);
}
}
unsafe extern "C" fn user_message_info(
_: *mut sys::IServerGameDLL,
index: c_int,
name: *mut c_char,
capacity: c_int,
size: *mut c_int,
) -> bool {
let (message, message_size): (&CStr, c_int) = match index {
0 => (c"Geiger", 1),
1 => (c"SayText2", -1),
_ => return false,
};
assert!(capacity as usize > message.count_bytes());
unsafe {
ptr::copy_nonoverlapping(message.as_ptr(), name, message.count_bytes() + 1);
size.write(message_size);
}
true
}
unsafe extern "C" fn can_provide_level(
_: *mut sys::IServerGameDLL,
name: *mut c_char,
capacity: c_int,
) -> sys::IServerGameDLL_eCanProvideLevelResult {
assert_eq!(capacity as usize, MAX_PATH);
if unsafe { CStr::from_ptr(name) } != c"workshop/123" {
return sys::IServerGameDLL_eCanProvideLevelResult_eCanProvideLevel_CannotProvide;
}
unsafe { ptr::copy_nonoverlapping(c"workshop/cp_example.ugc123".as_ptr(), name, 27) };
sys::IServerGameDLL_eCanProvideLevelResult_eCanProvideLevel_CanProvide
}
unsafe extern "C" fn manual_map_change(
_: *mut sys::IServerGameDLL,
reason: *mut *const c_char,
) -> bool {
unsafe { reason.write(c"Tournament in progress".as_ptr()) };
false
}
#[test]
fn methods_convert_the_games_answers() {
let vtable = unsafe {
mock_vtable::<sys::IServerGameDLL__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IServerGameDLL_GetTickInterval).write(tick_interval);
(&raw mut (*vtable).IServerGameDLL_Status).write(status);
(&raw mut (*vtable).IServerGameDLL_GetUserMessageInfo).write(user_message_info);
(&raw mut (*vtable).IServerGameDLL_CanProvideLevel).write(can_provide_level);
(&raw mut (*vtable).IServerGameDLL_IsManualMapChangeOkay)
.write(manual_map_change);
},
)
};
let mut interface = sys::IServerGameDLL {
vtable_: &raw const *vtable,
};
let game = unsafe { ServerGameDll::from_raw(NonNull::from(&mut interface)) };
assert_eq!(game.tick_interval(), 0.015);
assert_eq!(
game.status().as_c_str(),
c"Blue Team Wins: 3\nScout 2.5\n"
);
let messages = game.user_messages().collect::<Vec<_>>();
assert_eq!(messages.len(), 2);
assert_eq!(
(messages[0].name.as_c_str(), messages[0].size),
(c"Geiger", Some(1))
);
assert_eq!((messages[1].index, messages[1].size), (1, None));
assert_eq!(
game.can_provide_level(c"workshop/123"),
Ok(LevelProvision::CanProvide(
c"workshop/cp_example.ugc123".to_owned()
))
);
assert_eq!(
game.can_provide_level(c"ctf_2fort"),
Ok(LevelProvision::CannotProvide)
);
assert!(
game.can_provide_level(&CString::new("x".repeat(MAX_PATH)).unwrap())
.is_err()
);
assert_eq!(
game.is_manual_map_change_okay().unwrap_err().to_string(),
"the game refuses map changes right now: Tournament in progress"
);
}
}