#[cfg(test)]
#[path = "../tests/tf2/mvm.rs"]
mod tests;
use crate::datatables::{NetPropError, NetValue};
use crate::entities::Entity;
use crate::entities::spawn::{EntitySpawn, SpawnError};
use crate::inputs::{InputError, InputValue};
use crate::tf2::script_binding::{self as binding, BindingError, VOID};
use crate::{Game, InterfaceError, Server};
use sdk_raw::tf2::bots::TF_PLAYER_CLASS;
use sdk_raw::tf2::script_binding::{BOOL, INT, boolean, int};
use std::ffi::{CStr, CString, c_int};
pub const MAX_ADDED_CURRENCY: c_int = 30_000;
const OBJECTIVE_RESOURCE_CLASS: &CStr = c"CTFObjectiveResource";
const OBJECTIVE_RESOURCE: &CStr = c"tf_objective_resource";
const POPULATOR: &CStr = c"point_populator_interface";
const POPULATOR_CLASS: &CStr = c"CPointPopulatorInterface";
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum MvmError {
#[error("Mann vs. Machine requires Team Fortress 2")]
UnsupportedGame,
#[error("the entity is not a {class:?}")]
WrongClass {
class: &'static CStr,
},
#[error("the entity is marked for deletion")]
MarkedForDeletion,
#[error("{amount} would overflow the player's money of {currency}")]
CurrencyOverflow {
currency: c_int,
amount: c_int,
},
#[error("the mission name cannot be passed to tf_mvm_popfile")]
InvalidPopfileName,
#[error("the networked variable {name:?} is not of the expected kind")]
UnsupportedLayout {
name: &'static CStr,
},
#[error("the game does not expose the expected native method")]
UnsupportedMethod,
#[error("the native method rejected the call")]
Rejected,
#[error(transparent)]
Input(#[from] InputError),
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error(transparent)]
Spawn(#[from] SpawnError),
}
impl From<BindingError> for MvmError {
fn from(error: BindingError) -> Self {
match error {
BindingError::Unavailable | BindingError::SignatureMismatch => Self::UnsupportedMethod,
BindingError::Rejected => Self::Rejected,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct MvmPlayer<'s> {
entity: Entity<'s>,
}
impl<'s> MvmPlayer<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, MvmError> {
check_class(server, entity, TF_PLAYER_CLASS)?;
Ok(Self { entity })
}
#[doc(alias("AddCurrency"))]
pub fn add_currency(self, amount: c_int) -> Result<(), MvmError> {
let currency = self.currency()?;
if currency.checked_add(amount).is_none() {
return Err(MvmError::CurrencyOverflow { currency, amount });
}
unsafe { self.call(c"AddCurrency", &mut [int(amount)], VOID) }?;
Ok(())
}
unsafe fn call(
self,
name: &CStr,
arguments: &mut [sys::ScriptVariant_t],
result: sys::ScriptDataType_t,
) -> Result<sys::ScriptVariant_t, MvmError> {
if self.entity.is_marked_for_deletion() {
return Err(MvmError::MarkedForDeletion);
}
Ok(unsafe { binding::call(self.entity, TF_PLAYER_CLASS, name, arguments, result) }?)
}
#[doc(alias("GetCurrency", "m_nCurrency"))]
pub fn currency(self) -> Result<c_int, MvmError> {
let result = unsafe { self.call(c"GetCurrency", &mut [], INT) }?;
Ok(unsafe { result.__bindgen_anon_1.m_int })
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
#[doc(alias("IsMiniBoss", "m_bIsMiniBoss"))]
pub fn is_mini_boss(self) -> Result<bool, MvmError> {
let result = unsafe { self.call(c"IsMiniBoss", &mut [], BOOL) }?;
Ok(unsafe { result.__bindgen_anon_1.m_bool })
}
#[doc(alias("RemoveCurrency"))]
pub fn remove_currency(self, amount: c_int) -> Result<(), MvmError> {
let currency = self.currency()?;
if currency.checked_sub(amount).is_none() {
return Err(MvmError::CurrencyOverflow { currency, amount });
}
unsafe { self.call(c"RemoveCurrency", &mut [int(amount)], VOID) }?;
Ok(())
}
#[doc(alias("GrantOrRemoveAllUpgrades"))]
pub fn remove_upgrades(self, refund: bool) -> Result<(), MvmError> {
unsafe {
self.call(
c"GrantOrRemoveAllUpgrades",
&mut [boolean(true), boolean(refund)],
VOID,
)
}?;
Ok(())
}
#[doc(alias("SetCurrency", "m_nCurrency"))]
pub fn set_currency(self, currency: c_int) -> Result<(), MvmError> {
unsafe { self.call(c"SetCurrency", &mut [int(currency)], VOID) }?;
Ok(())
}
#[doc(alias("SetIsMiniBoss", "m_bIsMiniBoss"))]
pub fn set_mini_boss(self, mini_boss: bool) -> Result<(), MvmError> {
unsafe { self.call(c"SetIsMiniBoss", &mut [boolean(mini_boss)], VOID) }?;
Ok(())
}
#[doc(alias("SetUseBossHealthBar", "m_bUseBossHealthBar"))]
pub fn set_use_boss_health_bar(self, use_bar: bool) -> Result<(), MvmError> {
unsafe { self.call(c"SetUseBossHealthBar", &mut [boolean(use_bar)], VOID) }?;
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct MvmWave<'s> {
server: Server<'s>,
entity: Entity<'s>,
}
impl<'s> MvmWave<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, MvmError> {
check_class(server, entity, OBJECTIVE_RESOURCE_CLASS)?;
Ok(Self { server, entity })
}
pub fn find(server: Server<'s>) -> Result<Option<Self>, MvmError> {
find(server, OBJECTIVE_RESOURCE, Self::new)
}
#[doc(alias("m_nFlagCarrierUpgradeLevel"))]
pub fn bomb_carrier_level(self) -> Result<c_int, MvmError> {
self.get(c"m_nFlagCarrierUpgradeLevel")
}
#[doc(alias("m_nMannVsMachineMaxWaveCount"))]
pub fn count(self) -> Result<c_int, MvmError> {
self.get(c"m_nMannVsMachineMaxWaveCount")
}
#[doc(alias("m_nMannVsMachineWaveEnemyCount"))]
pub fn enemy_count(self) -> Result<c_int, MvmError> {
self.get(c"m_nMannVsMachineWaveEnemyCount")
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
fn get<T: crate::datatables::NetVar>(self, name: &CStr) -> Result<T, MvmError> {
if self.entity.is_marked_for_deletion() {
return Err(MvmError::MarkedForDeletion);
}
let prop = self
.server
.server_game_dll()?
.entity_net_prop(self.entity, name)?;
Ok(prop.get(self.entity)?)
}
#[doc(alias("m_nMannVsMachineWaveHasTanks"))]
pub fn has_tanks(self) -> Result<bool, MvmError> {
self.get(c"m_nMannVsMachineWaveHasTanks")
}
#[doc(alias("m_bMannVsMachineBetweenWaves"))]
pub fn is_between_waves(self) -> Result<bool, MvmError> {
self.get(c"m_bMannVsMachineBetweenWaves")
}
#[doc(alias("m_flMannVsMachineNextWaveTime"))]
pub fn next_wave_time(self) -> Result<Option<f32>, MvmError> {
let time = self.get::<f32>(c"m_flMannVsMachineNextWaveTime")?;
Ok((time > 0.0).then_some(time))
}
#[doc(alias("m_nMannVsMachineWaveCount"))]
pub fn number(self) -> Result<c_int, MvmError> {
self.get(c"m_nMannVsMachineWaveCount")
}
#[doc(alias("m_iszMvMPopfileName"))]
pub fn popfile(self) -> Result<CString, MvmError> {
const NAME: &CStr = c"m_iszMvMPopfileName";
if self.entity.is_marked_for_deletion() {
return Err(MvmError::MarkedForDeletion);
}
let prop = self
.server
.server_game_dll()?
.entity_net_prop(self.entity, NAME)?;
match prop.value(self.entity)? {
NetValue::String(name) => Ok(name),
_ => Err(MvmError::UnsupportedLayout { name: NAME }),
}
}
#[doc(alias("m_nMvMWorldMoney"))]
pub fn world_money(self) -> Result<c_int, MvmError> {
self.get(c"m_nMvMWorldMoney")
}
}
#[doc(alias("point_populator_interface", "CPointPopulatorInterface"))]
#[derive(Debug, Clone, Copy)]
pub struct Populator<'s> {
server: Server<'s>,
entity: Entity<'s>,
}
impl<'s> Populator<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, MvmError> {
check_class(server, entity, POPULATOR_CLASS)?;
Ok(Self { server, entity })
}
pub fn find_or_spawn(server: Server<'s>) -> Result<Self, MvmError> {
if let Some(populator) = find(server, POPULATOR, Self::new)? {
return Ok(populator);
}
let entity = unsafe { EntitySpawn::new(POPULATOR).spawn(server.server_tools()?) }?;
Self::new(server, entity)
}
#[doc(alias("ChangeBotAttributes"))]
pub fn change_bot_attributes(self, event: &CStr) -> Result<(), MvmError> {
self.input(c"ChangeBotAttributes", InputValue::String(event))
}
#[doc(alias("ChangeDefaultEventAttributes"))]
pub fn change_default_event_attributes(self, event: &CStr) -> Result<(), MvmError> {
self.input(c"ChangeDefaultEventAttributes", InputValue::String(event))
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
fn input(self, name: &CStr, value: InputValue<'_>) -> Result<(), MvmError> {
let tools = self.server.server_tools()?;
Ok(tools.accept_input(self.entity, name, value, self.entity, self.entity)?)
}
#[doc(alias("PauseBotSpawning"))]
pub fn pause_spawning(self) -> Result<(), MvmError> {
self.input(c"PauseBotSpawning", InputValue::Void)
}
#[doc(alias("UnpauseBotSpawning"))]
pub fn unpause_spawning(self) -> Result<(), MvmError> {
self.input(c"UnpauseBotSpawning", InputValue::Void)
}
}
#[doc(alias("tf_mvm_popfile"))]
pub fn load_popfile(server: Server<'_>, name: &CStr) -> Result<(), MvmError> {
if server.game() != Game::TeamFortress2 {
return Err(MvmError::UnsupportedGame);
}
let bytes = name.to_bytes();
if bytes.is_empty()
|| bytes
.iter()
.any(|&byte| byte < 0x20 || byte == 0x7f || byte == b'"')
{
return Err(MvmError::InvalidPopfileName);
}
let mut command = b"tf_mvm_popfile \"".to_vec();
command.extend_from_slice(bytes);
command.extend_from_slice(b"\"\n");
let command = CString::new(command).expect("the name holds no NUL");
server.valve_engine()?.server_command(&command);
Ok(())
}
fn check_class(
server: Server<'_>,
entity: Entity<'_>,
class: &'static CStr,
) -> Result<(), MvmError> {
if server.game() != Game::TeamFortress2 {
return Err(MvmError::UnsupportedGame);
}
if entity.has_data_map_class(class) {
Ok(())
} else {
Err(MvmError::WrongClass { class })
}
}
fn find<'s, T>(
server: Server<'s>,
class_name: &CStr,
wrap: impl Fn(Server<'s>, Entity<'s>) -> Result<T, MvmError>,
) -> Result<Option<T>, MvmError> {
if server.game() != Game::TeamFortress2 {
return Err(MvmError::UnsupportedGame);
}
let tools = server.server_tools()?;
let mut found = tools.find_by_class_name(None, class_name);
while let Some(entity) = found {
if !entity.is_marked_for_deletion()
&& let Ok(wrapped) = wrap(server, entity)
{
return Ok(Some(wrapped));
}
found = tools.find_by_class_name(Some(entity), class_name);
}
Ok(None)
}