use crate::NotThreadSafe;
use crate::datatables::{NetProp, NetPropError, ServerClass, ServerClasses, StandardSendProxies};
use crate::entities::Entity;
use sdk_raw::tier0::MAX_PATH;
use sdk_raw::util::cstr::{buffer_from_cstr, copy_cstr, cstring_from_buffer};
use sdk_raw::util::printf::capture_printf;
use sdk_raw::vcall;
use std::ffi::{CStr, CString, c_char, c_int};
use std::marker::PhantomData;
use std::ptr::{self, NonNull};
const MAX_USER_MESSAGES: usize = 1024;
const MAX_WORKSHOP_MAPS: u32 = 1 << 16;
const SAVE_COMMENT_CAPACITY: usize = 256;
const USER_MESSAGE_NAME_CAPACITY: usize = 256;
interface! {
#[doc(alias("IServerGameDLL", "CServerGameDLL"))]
pub struct ServerGameDll(sys::IServerGameDLL) = GameServer sdk_raw::interfaces::server_game_dll::VERSION;
}
#[doc(alias("ePrepareLevelResourcesResult"))]
#[derive(Debug, Clone, PartialEq)]
pub enum LevelPreparation {
#[doc(alias("ePrepareLevelResources_Prepared"))]
Prepared(LevelResources),
#[doc(alias("ePrepareLevelResources_InProgress"))]
InProgress {
resources: LevelResources,
progress: f32,
},
}
#[doc(alias("eCanProvideLevelResult"))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LevelProvision {
#[doc(alias("eCanProvideLevel_CannotProvide"))]
CannotProvide,
#[doc(alias("eCanProvideLevel_CanProvide"))]
CanProvide(CString),
#[doc(alias("eCanProvideLevel_Possibly"))]
Possibly,
}
#[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, 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>,
}
#[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,
}
#[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("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))
}
#[doc(alias("ShouldHibernate"))]
pub fn should_hibernate(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGCLobby_ShouldHibernate()) }
}
#[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()) };
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserMessage {
pub index: usize,
pub name: CString,
pub size: Option<usize>,
}
#[doc(alias("WorkshopMapDesc_t"))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WorkshopMap {
#[doc(alias("szMapName"))]
pub name: CString,
#[doc(alias("szOriginalMapName"))]
pub original_name: CString,
#[doc(alias("uTimestamp"))]
pub timestamp: u32,
#[doc(alias("bDownloaded"))]
pub downloaded: bool,
}
impl<'s> ServerGameDll<'s> {
#[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,
})
}
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("GetGameDescription"))]
pub fn game_description(self) -> CString {
unsafe { copy_cstr(vcall!(self.as_ptr() => IServerGameDLL_GetGameDescription())) }
.unwrap_or_default()
}
#[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("InvalidateMdlCache"))]
pub fn invalidate_mdl_cache(self) {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_InvalidateMdlCache()) };
}
#[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("IsRestoring"))]
pub fn is_restoring(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_IsRestoring()) }
}
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)
}
#[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("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("GetServerBrowserGameData"))]
pub fn server_browser_game_data(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_ptr() => IServerGameDLL_GetServerBrowserGameData())) }
}
#[doc(alias("GetServerBrowserMapOverride"))]
pub fn server_browser_map_override(self) -> Option<CString> {
unsafe { copy_cstr(vcall!(self.as_ptr() => IServerGameDLL_GetServerBrowserMapOverride())) }
}
#[doc(alias("GetAllServerClasses"))]
pub fn server_class(self, name: &CStr) -> Option<ServerClass<'s>> {
self.server_classes().find(|class| class.name() == name)
}
#[doc(alias("GetAllServerClasses"))]
pub fn server_classes(self) -> ServerClasses<'s> {
let head = unsafe { vcall!(self.as_ptr() => IServerGameDLL_GetAllServerClasses()) };
unsafe { ServerClasses::new(head) }
}
#[doc(alias("ShouldHideServer"))]
pub fn should_hide_server(self) -> bool {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_ShouldHideServer()) }
}
#[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) })
}
#[doc(alias("Status"))]
pub fn status(self) -> CString {
let ((), output) = capture_printf(|print| {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_Status(Some(print))) }
});
output
}
#[doc(alias("GetTickInterval"))]
pub fn tick_interval(self) -> f32 {
unsafe { vcall!(self.as_ptr() => IServerGameDLL_GetTickInterval()) }
}
#[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(),
})
}
#[doc(alias("GetUserMessageInfo"))]
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("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,
})
}
#[doc(alias("GetWorkshopMap"))]
pub fn workshop_maps(self) -> impl Iterator<Item = WorkshopMap> + use<'s> {
(0..MAX_WORKSHOP_MAPS).map_while(move |index| self.workshop_map(index))
}
}
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 = [b"maps/".as_slice(), map.to_bytes(), b".bsp"].concat();
let file = CString::new(file).map_err(|_| too_long())?;
Ok((
buffer_from_cstr(map).ok_or_else(too_long)?,
buffer_from_cstr(&file).ok_or_else(too_long)?,
))
}
fn steam_id_of(steam_id: u64) -> sys::CSteamID {
sys::CSteamID {
m_steamid: sys::CSteamID_SteamID_t {
m_unAll64Bits: steam_id,
},
}
}