#[cfg(test)]
#[path = "../tests/tf2/game_rules.rs"]
mod tests;
use crate::NotThreadSafe;
use crate::datatables::{
NetProp, NetPropError, NetVar, PropKind, SendProp, SendTable, ServerClass, StandardSendProxies,
};
use crate::edicts::Edict;
use crate::entities::{Entity, EntityHandle};
use crate::inputs::{InputError, InputValue};
use crate::interfaces::ServerTools;
use crate::interfaces::ValveEngine;
use crate::tf2::ammo::AmmoType;
use crate::tf2::player::TfPlayer;
use crate::tf2::scoreboard::ScoringTeam;
use crate::{Game, InterfaceError, Server};
use sdk_raw::datatables::call_table_proxy;
use sdk_raw::tf2::game_rules::{
GR_STATE_BETWEEN_RNDS, GR_STATE_BONUS, GR_STATE_GAME_OVER, GR_STATE_INIT, GR_STATE_PREGAME,
GR_STATE_PREROUND, GR_STATE_RESTART, GR_STATE_RND_RUNNING, GR_STATE_STALEMATE,
GR_STATE_STARTGAME, GR_STATE_TEAM_WIN, can_have_ammo, find_game_rules_vtable,
};
use sdk_raw::tf2::game_rules::{TEAM_ROLE_ATTACKERS, TEAM_ROLE_DEFENDERS, TEAM_ROLE_NONE};
use sdk_raw::util;
use std::ffi::{CStr, c_int, c_void};
use std::marker::PhantomData;
use std::ptr::NonNull;
const PROXY_CLASS: &CStr = c"CTFGameRulesProxy";
const PROXY_ENTITY: &CStr = c"tf_gamerules";
const PROXY_TABLE: &CStr = c"DT_TFGameRulesProxy";
const ROUND_RULES_DATA: (&CStr, &CStr) = (
c"teamplayroundbased_gamerules_data",
c"DT_TeamplayRoundBasedRules",
);
const RULES_DATA: (&CStr, &CStr) = (c"tf_gamerules_data", c"DT_TFGameRules");
#[doc(alias("CTFGameRules", "TFGameRules"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GameRules<'s> {
rules: NonNull<c_void>,
round_rules: NonNull<c_void>,
proxy: Entity<'s>,
proxy_edict: Edict<'s>,
rules_table: SendTable<'s>,
round_rules_table: SendTable<'s>,
proxies: StandardSendProxies<'s>,
_not_thread_safe: NotThreadSafe,
}
impl GameRules<'_> {
#[doc(alias("SetRedKothClockActive", "SetBlueKothClockActive"))]
pub fn activate_koth_clock(
self,
tools: ServerTools<'_>,
team: ScoringTeam,
) -> Result<(), InputError> {
let input = match team {
ScoringTeam::Red => c"SetRedKothClockActive",
ScoringTeam::Blue => c"SetBlueKothClockActive",
};
self.input(tools, input, InputValue::Void)
}
#[doc(alias("AddRedTeamRespawnWaveTime", "AddBlueTeamRespawnWaveTime"))]
pub fn add_respawn_wave_time(
self,
tools: ServerTools<'_>,
team: ScoringTeam,
seconds: f32,
) -> Result<(), InputError> {
let input = match team {
ScoringTeam::Red => c"AddRedTeamRespawnWaveTime",
ScoringTeam::Blue => c"AddBlueTeamRespawnWaveTime",
};
self.input(tools, input, InputValue::Float(seconds))
}
#[doc(alias("AddRedTeamScore", "AddBlueTeamScore"))]
pub fn add_team_score(
self,
tools: ServerTools<'_>,
team: ScoringTeam,
points: c_int,
) -> Result<(), InputError> {
let input = match team {
ScoringTeam::Red => c"AddRedTeamScore",
ScoringTeam::Blue => c"AddBlueTeamScore",
};
self.input(tools, input, InputValue::Int(points))
}
fn input(
self,
tools: ServerTools<'_>,
input: &CStr,
value: InputValue<'_>,
) -> Result<(), InputError> {
tools.accept_input(self.proxy, input, value, self.proxy, self.proxy)
}
#[doc(alias("SetRedTeamRespawnWaveTime", "SetBlueTeamRespawnWaveTime"))]
pub fn set_respawn_wave_time(
self,
tools: ServerTools<'_>,
team: ScoringTeam,
seconds: f32,
) -> Result<(), InputError> {
let input = match team {
ScoringTeam::Red => c"SetRedTeamRespawnWaveTime",
ScoringTeam::Blue => c"SetBlueTeamRespawnWaveTime",
};
self.input(tools, input, InputValue::Float(seconds))
}
#[doc(alias("SetRedTeamGoalString", "SetBlueTeamGoalString"))]
pub fn set_team_goal(
self,
tools: ServerTools<'_>,
team: ScoringTeam,
goal: &CStr,
) -> Result<(), InputError> {
let input = match team {
ScoringTeam::Red => c"SetRedTeamGoalString",
ScoringTeam::Blue => c"SetBlueTeamGoalString",
};
self.input(tools, input, InputValue::String(goal))
}
#[doc(alias("SetRedTeamRole", "SetBlueTeamRole"))]
pub fn set_team_role(
self,
tools: ServerTools<'_>,
team: ScoringTeam,
role: TeamRole,
) -> Result<(), InputError> {
let input = match team {
ScoringTeam::Red => c"SetRedTeamRole",
ScoringTeam::Blue => c"SetBlueTeamRole",
};
self.input(tools, input, InputValue::Int(role.to_raw()))
}
}
impl<'s> GameRules<'s> {
#[doc(alias("m_bMultipleTrains", "HasMultipleTrains"))]
pub fn has_multiple_trains(self) -> Result<bool, GameRulesError> {
self.read(c"m_bMultipleTrains")
}
#[doc(alias("m_bInOvertime", "InOvertime"))]
pub fn is_in_overtime(self) -> Result<bool, GameRulesError> {
self.read(c"m_bInOvertime")
}
#[doc(alias("m_bStopWatch", "IsInStopWatch"))]
pub fn is_stopwatch(self) -> Result<bool, GameRulesError> {
self.read(c"m_bStopWatch")
}
#[doc(alias("m_nRoundsPlayed", "GetRoundsPlayed"))]
pub fn rounds_played(self) -> Result<c_int, GameRulesError> {
self.read(c"m_nRoundsPlayed")
}
#[doc(alias("m_bSwitchedTeamsThisRound", "SwitchedTeamsThisRound"))]
pub fn switched_teams_this_round(self) -> Result<bool, GameRulesError> {
self.read(c"m_bSwitchedTeamsThisRound")
}
#[doc(alias("m_iWinningTeam", "GetWinningTeam"))]
pub fn winning_team(self) -> Result<Option<ScoringTeam>, GameRulesError> {
Ok(ScoringTeam::from_raw(self.read(c"m_iWinningTeam")?))
}
}
impl<'s> GameRules<'s> {
pub fn get(server: Server<'s>) -> Result<Self, GameRulesError> {
if server.game() != Game::TeamFortress2 {
return Err(GameRulesError::WrongGame);
}
let dll = server.server_game_dll()?;
let proxies = dll
.standard_send_proxies()
.ok_or(NetPropError::NoStandardProxies)?;
let class = dll
.server_class(PROXY_CLASS)
.ok_or(GameRulesError::UnexpectedTables)?;
let table = class
.table()
.filter(|table| table.name() == PROXY_TABLE)
.ok_or(GameRulesError::UnexpectedTables)?;
let base = nested(table, c"baseclass")
.filter(|&base| base.offset() == 0 && proxies.is_direct(base))
.and_then(SendProp::data_table)
.ok_or(GameRulesError::UnexpectedTables)?;
let (rules_prop, rules_table) = relocating(table, RULES_DATA, proxies)?;
let (round_rules_prop, round_rules_table) = relocating(base, ROUND_RULES_DATA, proxies)?;
let entity = proxy_entity(server, class)?;
let edict = entity.edict().ok_or(GameRulesError::NoProxyEntity)?;
let object = entity.as_ptr().cast::<c_void>().cast_const();
let call = |prop: SendProp<'_>| {
let rules = unsafe { call_table_proxy(prop.as_ptr(), object, object, edict.index()) };
rules
.and_then(NonNull::new)
.ok_or(GameRulesError::NoGameRules)
};
Ok(Self {
rules: call(rules_prop)?,
round_rules: call(round_rules_prop)?,
proxy: entity,
proxy_edict: edict,
rules_table,
round_rules_table,
proxies,
_not_thread_safe: PhantomData,
})
}
pub const fn as_ptr(self) -> *mut c_void {
self.rules.as_ptr()
}
pub(crate) const fn as_non_null(self) -> NonNull<c_void> {
self.rules
}
#[doc(alias("m_bInSetup"))]
pub fn is_in_setup(self) -> Result<bool, GameRulesError> {
self.read(c"m_bInSetup")
}
#[doc(alias("m_bInWaitingForPlayers"))]
pub fn is_waiting_for_players(self) -> Result<bool, GameRulesError> {
self.read(c"m_bInWaitingForPlayers")
}
#[doc(alias("NetworkStateChanged", "NotifyNetworkStateChanged"))]
pub fn network_state_changed(self, engine: ValveEngine<'_>) {
self.proxy_edict.full_state_changed(engine);
}
#[doc(alias("CTFGameRulesProxy", "tf_gamerules"))]
pub const fn proxy(self) -> Entity<'s> {
self.proxy
}
pub fn read<T: NetVar>(self, name: &CStr) -> Result<T, GameRulesError> {
let (prop, object) = self.variable(name)?;
Ok(unsafe { prop.get_at(object) }?)
}
#[doc(alias("SendPropEHandle", "CHandle", "EHANDLE"))]
pub fn read_handle(self, name: &CStr) -> Result<Option<EntityHandle>, GameRulesError> {
let (prop, object) = self.variable(name)?;
let handle = unsafe { prop.get_handle_at(object) }?;
Ok(handle.is_valid().then_some(handle))
}
#[doc(alias("m_iRoundState", "State_Get"))]
pub fn round_state(self) -> Result<RoundState, GameRulesError> {
let raw = self.read::<c_int>(c"m_iRoundState")?;
RoundState::from_raw(raw).ok_or(GameRulesError::UnknownRoundState(raw))
}
pub unsafe fn set<T: NetVar>(
self,
engine: ValveEngine<'_>,
name: &CStr,
value: T,
) -> Result<(), GameRulesError> {
unsafe { self.write(name, value) }?;
self.network_state_changed(engine);
Ok(())
}
fn variable(self, name: &CStr) -> Result<(NetProp<'s>, NonNull<c_void>), GameRulesError> {
match NetProp::resolve(self.round_rules_table, name, self.proxies) {
Err(NetPropError::NotFound { .. }) => Ok((
NetProp::resolve(self.rules_table, name, self.proxies)?,
self.rules,
)),
found => Ok((found?, self.round_rules)),
}
}
pub unsafe fn write<T: NetVar>(self, name: &CStr, value: T) -> Result<(), GameRulesError> {
let (prop, object) = self.variable(name)?;
Ok(unsafe { prop.set_at(object, value) }?)
}
}
impl GameRules<'_> {
#[doc(alias("CanHaveAmmo", "GetMaxAmmo"))]
pub fn can_have_ammo(self, player: TfPlayer<'_>, ammo_type: AmmoType) -> bool {
let player = unsafe { NonNull::new_unchecked(player.player().as_ptr()) };
unsafe { can_have_ammo(self.rules, player.cast(), ammo_type.to_raw()) }
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum GameRulesError {
#[error("game rules access requires Team Fortress 2")]
WrongGame,
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the game rules' send tables are not nested as TF2 nests them")]
UnexpectedTables,
#[error("no `tf_gamerules` entity exists")]
NoProxyEntity,
#[error("the game has no game rules object")]
NoGameRules,
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error("the game rules hold an unknown round state, {0}")]
UnknownRoundState(c_int),
#[error("the game rules' `{}` holds an unknown value, {value}", variable.to_string_lossy())]
UnknownValue {
variable: &'static CStr,
value: c_int,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GameRulesVtable<'s> {
vtable: NonNull<*mut c_void>,
_scope: PhantomData<&'s Server<'s>>,
}
impl GameRulesVtable<'_> {
pub const fn as_ptr(self) -> NonNull<*mut c_void> {
self.vtable
}
}
#[derive(Debug, thiserror::Error)]
pub enum GameRulesVtableError {
#[error("the game rules vtable is only known for Team Fortress 2")]
WrongGame,
#[error("the game module could not be inspected")]
Image(#[from] std::io::Error),
#[error("the game module has an unsupported executable image")]
InvalidImage,
#[error("the game module has no unique vtable of `CTFGameRules`")]
NotFound,
}
impl From<util::Error> for GameRulesVtableError {
fn from(error: util::Error) -> Self {
match error {
util::Error::InvalidImage => Self::InvalidImage,
util::Error::Io(error) => Self::Image(error),
}
}
}
#[doc(alias("gamerules_roundstate_t"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum RoundState {
#[doc(alias("GR_STATE_INIT"))]
Init = GR_STATE_INIT,
#[doc(alias("GR_STATE_PREGAME"))]
Pregame = GR_STATE_PREGAME,
#[doc(alias("GR_STATE_STARTGAME"))]
StartGame = GR_STATE_STARTGAME,
#[doc(alias("GR_STATE_PREROUND"))]
Preround = GR_STATE_PREROUND,
#[doc(alias("GR_STATE_RND_RUNNING"))]
Running = GR_STATE_RND_RUNNING,
#[doc(alias("GR_STATE_TEAM_WIN"))]
TeamWin = GR_STATE_TEAM_WIN,
#[doc(alias("GR_STATE_RESTART"))]
Restart = GR_STATE_RESTART,
#[doc(alias("GR_STATE_STALEMATE"))]
Stalemate = GR_STATE_STALEMATE,
#[doc(alias("GR_STATE_GAME_OVER"))]
GameOver = GR_STATE_GAME_OVER,
#[doc(alias("GR_STATE_BONUS"))]
Bonus = GR_STATE_BONUS,
#[doc(alias("GR_STATE_BETWEEN_RNDS"))]
BetweenRounds = GR_STATE_BETWEEN_RNDS,
}
impl RoundState {
pub const ALL: [Self; 11] = [
Self::Init,
Self::Pregame,
Self::StartGame,
Self::Preround,
Self::Running,
Self::TeamWin,
Self::Restart,
Self::Stalemate,
Self::GameOver,
Self::Bonus,
Self::BetweenRounds,
];
pub const fn from_raw(raw: c_int) -> Option<Self> {
match raw {
GR_STATE_INIT => Some(Self::Init),
GR_STATE_PREGAME => Some(Self::Pregame),
GR_STATE_STARTGAME => Some(Self::StartGame),
GR_STATE_PREROUND => Some(Self::Preround),
GR_STATE_RND_RUNNING => Some(Self::Running),
GR_STATE_TEAM_WIN => Some(Self::TeamWin),
GR_STATE_RESTART => Some(Self::Restart),
GR_STATE_STALEMATE => Some(Self::Stalemate),
GR_STATE_GAME_OVER => Some(Self::GameOver),
GR_STATE_BONUS => Some(Self::Bonus),
GR_STATE_BETWEEN_RNDS => Some(Self::BetweenRounds),
_ => None,
}
}
pub const fn to_raw(self) -> c_int {
self as c_int
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum TeamRole {
#[doc(alias("TEAM_ROLE_NONE"))]
None = TEAM_ROLE_NONE,
#[doc(alias("TEAM_ROLE_DEFENDERS"))]
Defenders = TEAM_ROLE_DEFENDERS,
#[doc(alias("TEAM_ROLE_ATTACKERS"))]
Attackers = TEAM_ROLE_ATTACKERS,
}
impl TeamRole {
pub const fn to_raw(self) -> c_int {
self as c_int
}
}
pub fn game_rules_vtable(server: Server<'_>) -> Result<GameRulesVtable<'_>, GameRulesVtableError> {
if server.game() != Game::TeamFortress2 {
return Err(GameRulesVtableError::WrongGame);
}
let vtable = unsafe { find_game_rules_vtable(server.game_server_factory().as_raw()) }?
.ok_or(GameRulesVtableError::NotFound)?;
Ok(GameRulesVtable {
vtable,
_scope: PhantomData,
})
}
fn nested<'s>(table: SendTable<'s>, name: &CStr) -> Option<SendProp<'s>> {
table
.props()
.find(|prop| prop.kind() == PropKind::DataTable && prop.name() == name)
}
fn proxy_entity<'s>(
server: Server<'s>,
class: ServerClass<'s>,
) -> Result<Entity<'s>, GameRulesError> {
let tools = server.server_tools()?;
std::iter::successors(tools.find_by_class_name(None, PROXY_ENTITY), |&entity| {
tools.find_by_class_name(Some(entity), PROXY_ENTITY)
})
.find(|entity| entity.server_class() == Some(class) && !entity.is_marked_for_deletion())
.ok_or(GameRulesError::NoProxyEntity)
}
fn relocating<'s>(
table: SendTable<'s>,
(prop, nested_table): (&CStr, &CStr),
proxies: StandardSendProxies<'s>,
) -> Result<(SendProp<'s>, SendTable<'s>), GameRulesError> {
nested(table, prop)
.filter(|&prop| prop.offset() == 0 && !proxies.is_direct(prop))
.and_then(|prop| Some((prop, prop.data_table()?)))
.filter(|(_, table)| table.name() == nested_table)
.ok_or(GameRulesError::UnexpectedTables)
}