#[cfg(test)]
#[path = "../tests/tf2/bots.rs"]
mod tests;
use crate::entities::Entity;
use crate::tf2::PlayerClass;
use crate::tf2::next_bot::{NextBotInterface, next_bot_pointer};
use crate::tf2::script_binding::{self as binding, BindingError, FLOAT, VOID, float, string};
use crate::tf2::script_instances::{ScriptInstance, ScriptInstanceError};
use crate::{Game, InterfaceError, Server};
use sdk_raw::tf2::bots::{
self as raw, attribute, behavior, difficulty, mission, weapon_restriction,
};
use sdk_raw::tf2::script_binding::{BOOL, HANDLE, INT, boolean, handle, int};
use std::ffi::{CStr, CString, c_int};
use std::num::NonZeroU8;
pub use raw::TF_BOT_TYPE;
pub const MAX_NAME_LEN: usize = 31;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BotError {
#[error("bots require Team Fortress 2")]
UnsupportedGame,
#[error("the entity is not one of TF2's bots")]
NotATfBot,
#[error("the entity is not a NextBot actor")]
NotANextBot,
#[error("the bot is marked for deletion")]
MarkedForDeletion,
#[error("the game does not expose the expected native bot method")]
UnsupportedMethod,
#[error("the native bot method rejected the call")]
Rejected,
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error(transparent)]
ScriptInstance(#[from] ScriptInstanceError),
}
impl From<BindingError> for BotError {
fn from(error: BindingError) -> Self {
match error {
BindingError::Unavailable | BindingError::SignatureMismatch => Self::UnsupportedMethod,
BindingError::Rejected => Self::Rejected,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum BotNameError {
#[error("the bot name is empty")]
Empty,
#[error("the bot name is {len} bytes long, but at most {MAX_NAME_LEN} fit")]
TooLong {
len: usize,
},
#[error("the bot name contains the byte {byte:#04x} at index {index}")]
InvalidByte {
index: usize,
byte: u8,
},
#[error("`tf_bot_add` would read the bot name as another argument")]
Reserved,
}
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BotAttributes: u32 {
const AGGRESSIVE = attribute::AGGRESSIVE as u32;
const AIR_CHARGE_ONLY = attribute::AIR_CHARGE_ONLY as u32;
const ALWAYS_CRIT = attribute::ALWAYS_CRIT as u32;
const ALWAYS_FIRE_WEAPON = attribute::ALWAYS_FIRE_WEAPON as u32;
const AUTO_JUMP = attribute::AUTO_JUMP as u32;
const BECOME_SPECTATOR_ON_DEATH = attribute::BECOME_SPECTATOR_ON_DEATH as u32;
const BLAST_IMMUNE = attribute::BLAST_IMMUNE as u32;
const BULLET_IMMUNE = attribute::BULLET_IMMUNE as u32;
const DISABLE_DODGE = attribute::DISABLE_DODGE as u32;
const FIRE_IMMUNE = attribute::FIRE_IMMUNE as u32;
const HOLD_FIRE_UNTIL_FULL_RELOAD = attribute::HOLD_FIRE_UNTIL_FULL_RELOAD as u32;
const IGNORE_ENEMIES = attribute::IGNORE_ENEMIES as u32;
const IGNORE_FLAG = attribute::IGNORE_FLAG as u32;
const IS_NPC = attribute::IS_NPC as u32;
const MINIBOSS = attribute::MINIBOSS as u32;
const PARACHUTE = attribute::PARACHUTE as u32;
const PREFER_VACCINATOR_BLAST = attribute::PREFER_VACCINATOR_BLAST as u32;
const PREFER_VACCINATOR_BULLETS = attribute::PREFER_VACCINATOR_BULLETS as u32;
const PREFER_VACCINATOR_FIRE = attribute::PREFER_VACCINATOR_FIRE as u32;
const PRIORITIZE_DEFENSE = attribute::PRIORITIZE_DEFENSE as u32;
const PROJECTILE_SHIELD = attribute::PROJECTILE_SHIELD as u32;
#[doc(alias("QUOTA_MANANGED"))]
const QUOTA_MANAGED = attribute::QUOTA_MANAGED as u32;
const REMOVE_ON_DEATH = attribute::REMOVE_ON_DEATH as u32;
const RETAIN_BUILDINGS = attribute::RETAIN_BUILDINGS as u32;
const SPAWN_WITH_FULL_CHARGE = attribute::SPAWN_WITH_FULL_CHARGE as u32;
const SUPPRESS_FIRE = attribute::SUPPRESS_FIRE as u32;
const TELEPORT_TO_HINT = attribute::TELEPORT_TO_HINT as u32;
const USE_BOSS_HEALTH_BAR = attribute::USE_BOSS_HEALTH_BAR as u32;
}
}
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BehaviorFlags: u32 {
#[doc(alias("TFBOT_IGNORE_ENEMY_DEMOMEN"))]
const IGNORE_ENEMY_DEMOMEN = behavior::IGNORE_ENEMY_DEMOMEN as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_ENGINEERS"))]
const IGNORE_ENEMY_ENGINEERS = behavior::IGNORE_ENEMY_ENGINEERS as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_HEAVIES"))]
const IGNORE_ENEMY_HEAVIES = behavior::IGNORE_ENEMY_HEAVIES as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_MEDICS"))]
const IGNORE_ENEMY_MEDICS = behavior::IGNORE_ENEMY_MEDICS as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_PYROS"))]
const IGNORE_ENEMY_PYROS = behavior::IGNORE_ENEMY_PYROS as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_SCOUTS"))]
const IGNORE_ENEMY_SCOUTS = behavior::IGNORE_ENEMY_SCOUTS as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_SENTRY_GUNS"))]
const IGNORE_ENEMY_SENTRY_GUNS = behavior::IGNORE_ENEMY_SENTRY_GUNS as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_SNIPERS"))]
const IGNORE_ENEMY_SNIPERS = behavior::IGNORE_ENEMY_SNIPERS as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_SOLDIERS"))]
const IGNORE_ENEMY_SOLDIERS = behavior::IGNORE_ENEMY_SOLDIERS as u32;
#[doc(alias("TFBOT_IGNORE_ENEMY_SPIES"))]
const IGNORE_ENEMY_SPIES = behavior::IGNORE_ENEMY_SPIES as u32;
#[doc(alias("TFBOT_IGNORE_SCENARIO_GOALS"))]
const IGNORE_SCENARIO_GOALS = behavior::IGNORE_SCENARIO_GOALS as u32;
}
}
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WeaponRestrictions: u32 {
const MELEE_ONLY = weapon_restriction::MELEE_ONLY as u32;
const PRIMARY_ONLY = weapon_restriction::PRIMARY_ONLY as u32;
const SECONDARY_ONLY = weapon_restriction::SECONDARY_ONLY as u32;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BotTeam {
Red,
Blue,
}
impl BotTeam {
pub const fn argument(self) -> &'static CStr {
match self {
Self::Red => c"red",
Self::Blue => c"blue",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Bots {
Count(NonZeroU8),
Named(CString),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Difficulty {
#[doc(alias("EASY"))]
Easy = difficulty::EASY as isize,
#[doc(alias("NORMAL"))]
Normal = difficulty::NORMAL as isize,
#[doc(alias("HARD"))]
Hard = difficulty::HARD as isize,
#[doc(alias("EXPERT"))]
Expert = difficulty::EXPERT as isize,
}
impl Difficulty {
pub const ALL: [Self; 4] = [Self::Easy, Self::Normal, Self::Hard, Self::Expert];
pub const fn from_raw(raw: c_int) -> Option<Self> {
match raw {
difficulty::EASY => Some(Self::Easy),
difficulty::NORMAL => Some(Self::Normal),
difficulty::HARD => Some(Self::Hard),
difficulty::EXPERT => Some(Self::Expert),
_ => None,
}
}
pub const fn argument(self) -> &'static CStr {
match self {
Self::Easy => c"easy",
Self::Normal => c"normal",
Self::Hard => c"hard",
Self::Expert => c"expert",
}
}
pub const fn to_raw(self) -> c_int {
self as c_int
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Mission {
#[doc(alias("NO_MISSION"))]
NoMission = mission::NO_MISSION as isize,
#[doc(alias("MISSION_SEEK_AND_DESTROY"))]
SeekAndDestroy = mission::SEEK_AND_DESTROY as isize,
#[doc(alias("MISSION_DESTROY_SENTRIES"))]
DestroySentries = mission::DESTROY_SENTRIES as isize,
#[doc(alias("MISSION_SNIPER"))]
Sniper = mission::SNIPER as isize,
#[doc(alias("MISSION_SPY"))]
Spy = mission::SPY as isize,
#[doc(alias("MISSION_ENGINEER"))]
Engineer = mission::ENGINEER as isize,
#[doc(alias("MISSION_REPROGRAMMED"))]
Reprogrammed = mission::REPROGRAMMED as isize,
}
impl Mission {
pub const fn from_raw(raw: c_int) -> Option<Self> {
match raw {
mission::NO_MISSION => Some(Self::NoMission),
mission::SEEK_AND_DESTROY => Some(Self::SeekAndDestroy),
mission::DESTROY_SENTRIES => Some(Self::DestroySentries),
mission::SNIPER => Some(Self::Sniper),
mission::SPY => Some(Self::Spy),
mission::ENGINEER => Some(Self::Engineer),
mission::REPROGRAMMED => Some(Self::Reprogrammed),
_ => None,
}
}
pub const fn to_raw(self) -> c_int {
self as c_int
}
}
#[derive(Debug, Clone, Copy)]
pub struct NextBot<'s> {
entity: Entity<'s>,
class: &'static CStr,
}
impl<'s> NextBot<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BotError> {
match TfBot::new(server, entity) {
Ok(bot) => return Ok(bot.next_bot()),
Err(BotError::NotATfBot) => {}
Err(error) => return Err(error),
}
if !entity
.data_maps()
.any(|map| map.class_name() == Some(raw::NEXT_BOT_COMBAT_CHARACTER_CLASS))
{
return Err(BotError::NotANextBot);
}
Ok(Self {
entity,
class: raw::NEXT_BOT_COMBAT_CHARACTER_CLASS,
})
}
unsafe fn bool(self, name: &CStr) -> Result<bool, BotError> {
let result = unsafe { self.call(name, &mut [], BOOL) }?;
Ok(unsafe { result.__bindgen_anon_1.m_bool })
}
#[doc(alias("GetBotId"))]
pub fn bot_id(self) -> Result<c_int, BotError> {
unsafe { self.int(c"GetBotId") }
}
unsafe fn call(
self,
name: &CStr,
arguments: &mut [sys::ScriptVariant_t],
result: sys::ScriptDataType_t,
) -> Result<sys::ScriptVariant_t, BotError> {
if self.entity.is_marked_for_deletion() {
return Err(BotError::MarkedForDeletion);
}
Ok(unsafe { binding::call(self.entity, self.class, name, arguments, result) }?)
}
#[doc(alias("ClearImmobileStatus"))]
pub fn clear_immobile_status(self) -> Result<(), BotError> {
unsafe { self.call(c"ClearImmobileStatus", &mut [], VOID) }.map(drop)
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
#[doc(alias("FlagForUpdate"))]
pub fn flag_for_update(self, flag: bool) -> Result<(), BotError> {
unsafe { self.call(c"FlagForUpdate", &mut [boolean(flag)], VOID) }.map(drop)
}
unsafe fn float(self, name: &CStr) -> Result<f32, BotError> {
let result = unsafe { self.call(name, &mut [], FLOAT) }?;
Ok(unsafe { result.__bindgen_anon_1.m_float })
}
#[doc(alias("GetImmobileDuration"))]
pub fn immobile_duration(self) -> Result<f32, BotError> {
unsafe { self.float(c"GetImmobileDuration") }
}
#[doc(alias("GetImmobileSpeedThreshold"))]
pub fn immobile_speed_threshold(self) -> Result<f32, BotError> {
unsafe { self.float(c"GetImmobileSpeedThreshold") }
}
#[doc(alias("MyNextBotPointer"))]
pub fn interface(self) -> Result<NextBotInterface<'s>, BotError> {
unsafe { next_bot_pointer(self.entity) }
.map(|bot| unsafe { NextBotInterface::from_raw(bot) })
.ok_or(BotError::NotANextBot)
}
unsafe fn int(self, name: &CStr) -> Result<c_int, BotError> {
let result = unsafe { self.call(name, &mut [], INT) }?;
Ok(unsafe { result.__bindgen_anon_1.m_int })
}
#[doc(alias("IsEnemy"))]
pub fn is_enemy(self, server: Server<'s>, other: Entity<'s>) -> Result<bool, BotError> {
let other = ScriptInstance::of(server, other)?;
let result = unsafe { self.call(c"IsEnemy", &mut [handle(other.as_raw())], BOOL) }?;
Ok(unsafe { result.__bindgen_anon_1.m_bool })
}
#[doc(alias("IsFlaggedForUpdate"))]
pub fn is_flagged_for_update(self) -> Result<bool, BotError> {
unsafe { self.bool(c"IsFlaggedForUpdate") }
}
#[doc(alias("IsFriend"))]
pub fn is_friend(self, server: Server<'s>, other: Entity<'s>) -> Result<bool, BotError> {
let other = ScriptInstance::of(server, other)?;
let result = unsafe { self.call(c"IsFriend", &mut [handle(other.as_raw())], BOOL) }?;
Ok(unsafe { result.__bindgen_anon_1.m_bool })
}
#[doc(alias("IsImmobile"))]
pub fn is_immobile(self) -> Result<bool, BotError> {
unsafe { self.bool(c"IsImmobile") }
}
#[doc(alias("GetTickLastUpdate"))]
pub fn tick_last_update(self) -> Result<c_int, BotError> {
unsafe { self.int(c"GetTickLastUpdate") }
}
}
#[derive(Debug, Clone, Copy)]
pub struct TfBot<'s> {
entity: Entity<'s>,
}
impl<'s> TfBot<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BotError> {
if server.game() != Game::TeamFortress2 {
return Err(BotError::UnsupportedGame);
}
if entity.is_marked_for_deletion() {
return Err(BotError::MarkedForDeletion);
}
let result =
unsafe { binding::call(entity, raw::TF_PLAYER_CLASS, c"GetBotType", &mut [], INT) };
let bot_type = match result {
Ok(result) => unsafe { result.__bindgen_anon_1.m_int },
Err(BindingError::Unavailable) => return Err(BotError::NotATfBot),
Err(error) => return Err(error.into()),
};
if bot_type != TF_BOT_TYPE {
return Err(BotError::NotATfBot);
}
Ok(Self { entity })
}
#[doc(alias("AddBotAttribute", "SetAttribute"))]
pub fn add_attributes(self, attributes: BotAttributes) -> Result<(), BotError> {
unsafe { self.void(c"AddBotAttribute", &mut [int(attributes.bits() as c_int)]) }
}
#[doc(alias("SetBehaviorFlag"))]
pub fn add_behavior_flags(self, flags: BehaviorFlags) -> Result<(), BotError> {
unsafe { self.void(c"SetBehaviorFlag", &mut [int(flags.bits() as c_int)]) }
}
#[doc(alias("AddBotTag", "AddTag"))]
pub fn add_tag(self, tag: &CStr) -> Result<(), BotError> {
unsafe { self.void(c"AddBotTag", &mut [string(tag)]) }
}
#[doc(alias("AddWeaponRestriction", "SetWeaponRestriction"))]
pub fn add_weapon_restrictions(self, restrictions: WeaponRestrictions) -> Result<(), BotError> {
unsafe {
self.void(
c"AddWeaponRestriction",
&mut [int(restrictions.bits() as c_int)],
)
}
}
pub fn attributes(self) -> Result<BotAttributes, BotError> {
let mut attributes = BotAttributes::empty();
for flag in BotAttributes::all().iter() {
if self.has_any_attribute(flag)? {
attributes |= flag;
}
}
Ok(attributes)
}
pub fn behavior_flags(self) -> Result<BehaviorFlags, BotError> {
let mut flags = BehaviorFlags::empty();
for flag in BehaviorFlags::all().iter() {
if self.has_any_behavior_flag(flag)? {
flags |= flag;
}
}
Ok(flags)
}
unsafe fn bool(
self,
name: &CStr,
arguments: &mut [sys::ScriptVariant_t],
) -> Result<bool, BotError> {
let result = unsafe { self.call(name, arguments, BOOL) }?;
Ok(unsafe { result.__bindgen_anon_1.m_bool })
}
unsafe fn call(
self,
name: &CStr,
arguments: &mut [sys::ScriptVariant_t],
result: sys::ScriptDataType_t,
) -> Result<sys::ScriptVariant_t, BotError> {
unsafe { self.next_bot().call(name, arguments, result) }
}
#[doc(alias("ClearAttentionFocus"))]
pub fn clear_attention_focus(self) -> Result<(), BotError> {
unsafe { self.void(c"ClearAttentionFocus", &mut []) }
}
#[doc(alias("ClearAllBotAttributes", "ClearAllAttributes"))]
pub fn clear_attributes(self) -> Result<(), BotError> {
unsafe { self.void(c"ClearAllBotAttributes", &mut []) }
}
#[doc(alias("ClearAllBotTags", "ClearTags"))]
pub fn clear_tags(self) -> Result<(), BotError> {
unsafe { self.void(c"ClearAllBotTags", &mut []) }
}
#[doc(alias("ClearAllWeaponRestrictions", "ClearWeaponRestrictions"))]
pub fn clear_weapon_restrictions(self) -> Result<(), BotError> {
unsafe { self.void(c"ClearAllWeaponRestrictions", &mut []) }
}
#[doc(alias("DelayedThreatNotice"))]
pub fn delayed_threat_notice(
self,
server: Server<'s>,
threat: Entity<'s>,
delay: f32,
) -> Result<(), BotError> {
assert!(delay.is_finite() && delay >= 0.0, "invalid notice delay");
let threat = ScriptInstance::of(server, threat)?;
unsafe {
self.void(
c"DelayedThreatNotice",
&mut [handle(threat.as_raw()), float(delay)],
)
}
}
#[doc(alias("GetDifficulty"))]
pub fn difficulty(self) -> Result<Option<Difficulty>, BotError> {
Ok(Difficulty::from_raw(unsafe {
self.int(c"GetDifficulty", &mut [])
}?))
}
#[doc(alias("DisbandCurrentSquad"))]
pub fn disband_squad(self) -> Result<(), BotError> {
unsafe { self.void(c"DisbandCurrentSquad", &mut []) }
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
unsafe fn float(
self,
name: &CStr,
arguments: &mut [sys::ScriptVariant_t],
) -> Result<f32, BotError> {
let result = unsafe { self.call(name, arguments, FLOAT) }?;
Ok(unsafe { result.__bindgen_anon_1.m_float })
}
#[doc(alias("GenerateAndWearItem"))]
pub fn generate_and_wear_item(self, name: &CStr) -> Result<(), BotError> {
unsafe { self.void(c"GenerateAndWearItem", &mut [string(name)]) }
}
#[doc(alias("HasBotAttribute", "HasAttribute"))]
pub fn has_any_attribute(self, attributes: BotAttributes) -> Result<bool, BotError> {
unsafe { self.bool(c"HasBotAttribute", &mut [int(attributes.bits() as c_int)]) }
}
#[doc(alias("IsBehaviorFlagSet"))]
pub fn has_any_behavior_flag(self, flags: BehaviorFlags) -> Result<bool, BotError> {
unsafe { self.bool(c"IsBehaviorFlagSet", &mut [int(flags.bits() as c_int)]) }
}
#[doc(alias("HasWeaponRestriction"))]
pub fn has_any_weapon_restriction(
self,
restrictions: WeaponRestrictions,
) -> Result<bool, BotError> {
unsafe {
self.bool(
c"HasWeaponRestriction",
&mut [int(restrictions.bits() as c_int)],
)
}
}
#[doc(alias("HasBotTag", "HasTag"))]
pub fn has_tag(self, tag: &CStr) -> Result<bool, BotError> {
unsafe { self.bool(c"HasBotTag", &mut [string(tag)]) }
}
unsafe fn int(
self,
name: &CStr,
arguments: &mut [sys::ScriptVariant_t],
) -> Result<c_int, BotError> {
let result = unsafe { self.call(name, arguments, INT) }?;
Ok(unsafe { result.__bindgen_anon_1.m_int })
}
#[doc(alias("IsAmmoFull"))]
pub fn is_ammo_full(self) -> Result<bool, BotError> {
unsafe { self.bool(c"IsAmmoFull", &mut []) }
}
#[doc(alias("IsAmmoLow"))]
pub fn is_ammo_low(self) -> Result<bool, BotError> {
unsafe { self.bool(c"IsAmmoLow", &mut []) }
}
#[doc(alias("IsAttentionFocused"))]
pub fn is_attention_focused(self) -> Result<bool, BotError> {
unsafe { self.bool(c"IsAttentionFocused", &mut []) }
}
#[doc(alias("IsAttentionFocusedOn"))]
pub fn is_attention_focused_on(
self,
server: Server<'s>,
target: Entity<'s>,
) -> Result<bool, BotError> {
let target = ScriptInstance::of(server, target)?;
unsafe { self.bool(c"IsAttentionFocusedOn", &mut [handle(target.as_raw())]) }
}
#[doc(alias("IsInASquad"))]
pub fn is_in_squad(self) -> Result<bool, BotError> {
unsafe { self.bool(c"IsInASquad", &mut []) }
}
#[doc(alias("IsOnAnyMission"))]
pub fn is_on_any_mission(self) -> Result<bool, BotError> {
unsafe { self.bool(c"IsOnAnyMission", &mut []) }
}
#[doc(alias("IsWeaponRestricted"))]
pub fn is_weapon_restricted(
self,
server: Server<'s>,
weapon: Entity<'s>,
) -> Result<bool, BotError> {
let weapon = ScriptInstance::of(server, weapon)?;
unsafe { self.bool(c"IsWeaponRestricted", &mut [handle(weapon.as_raw())]) }
}
pub fn kick(self, server: Server<'_>) -> Result<(), BotError> {
let engine = server.valve_engine()?;
let user_id = self
.entity
.edict()
.and_then(|edict| engine.user_id_of_edict(edict))
.ok_or(BotError::NotATfBot)?;
let command = format!("kickid {}\n", user_id.to_raw());
engine.server_command(&CString::new(command).expect("a number has no NUL"));
Ok(())
}
#[doc(alias("LeaveSquad"))]
pub fn leave_squad(self) -> Result<(), BotError> {
unsafe { self.void(c"LeaveSquad", &mut []) }
}
#[doc(alias("GetMaxVisionRangeOverride"))]
pub fn max_vision_range_override(self) -> Result<Option<f32>, BotError> {
let range = unsafe { self.float(c"GetMaxVisionRangeOverride", &mut []) }?;
Ok((range > 0.0).then_some(range))
}
#[doc(alias("GetMission"))]
pub fn mission(self) -> Result<Option<Mission>, BotError> {
Ok(Mission::from_raw(unsafe {
self.int(c"GetMission", &mut [])
}?))
}
#[doc(alias("GetMissionTarget"))]
pub fn mission_target(self, server: Server<'s>) -> Result<Option<Entity<'s>>, BotError> {
unsafe { self.returned_entity(server, c"GetMissionTarget") }
}
#[doc(alias("GetNearestKnownSappableTarget"))]
pub fn nearest_known_sappable_target(
self,
server: Server<'s>,
) -> Result<Option<Entity<'s>>, BotError> {
unsafe { self.returned_entity(server, c"GetNearestKnownSappableTarget") }
}
pub const fn next_bot(self) -> NextBot<'s> {
NextBot {
entity: self.entity,
class: raw::TF_BOT_CLASS,
}
}
fn press(self, name: &CStr, duration: Option<f32>) -> Result<(), BotError> {
let duration = match duration {
Some(duration) => {
assert!(
duration.is_finite() && duration >= 0.0,
"invalid button duration"
);
duration
}
None => -1.0,
};
unsafe { self.void(name, &mut [float(duration)]) }
}
#[doc(alias("PressAltFireButton"))]
pub fn press_alt_fire_button(self, duration: Option<f32>) -> Result<(), BotError> {
self.press(c"PressAltFireButton", duration)
}
#[doc(alias("PressFireButton"))]
pub fn press_fire_button(self, duration: Option<f32>) -> Result<(), BotError> {
self.press(c"PressFireButton", duration)
}
#[doc(alias("PressSpecialFireButton"))]
pub fn press_special_fire_button(self, duration: Option<f32>) -> Result<(), BotError> {
self.press(c"PressSpecialFireButton", duration)
}
#[doc(alias("GetPrevMission"))]
pub fn previous_mission(self) -> Result<Option<Mission>, BotError> {
Ok(Mission::from_raw(unsafe {
self.int(c"GetPrevMission", &mut [])
}?))
}
#[doc(alias("ShouldQuickBuild"))]
pub fn quick_build(self) -> Result<bool, BotError> {
unsafe { self.bool(c"ShouldQuickBuild", &mut []) }
}
#[doc(alias("RemoveBotAttribute", "ClearAttribute"))]
pub fn remove_attributes(self, attributes: BotAttributes) -> Result<(), BotError> {
unsafe {
self.void(
c"RemoveBotAttribute",
&mut [int(attributes.bits() as c_int)],
)
}
}
#[doc(alias("ClearBehaviorFlag"))]
pub fn remove_behavior_flags(self, flags: BehaviorFlags) -> Result<(), BotError> {
unsafe { self.void(c"ClearBehaviorFlag", &mut [int(flags.bits() as c_int)]) }
}
#[doc(alias("RemoveBotTag", "RemoveTag"))]
pub fn remove_tag(self, tag: &CStr) -> Result<(), BotError> {
unsafe { self.void(c"RemoveBotTag", &mut [string(tag)]) }
}
#[doc(alias("RemoveWeaponRestriction"))]
pub fn remove_weapon_restrictions(
self,
restrictions: WeaponRestrictions,
) -> Result<(), BotError> {
unsafe {
self.void(
c"RemoveWeaponRestriction",
&mut [int(restrictions.bits() as c_int)],
)
}
}
unsafe fn returned_entity(
self,
server: Server<'s>,
name: &CStr,
) -> Result<Option<Entity<'s>>, BotError> {
ScriptInstance::of(server, self.entity)?;
let result = unsafe { self.call(name, &mut [], HANDLE) }?;
let Some(instance) =
(unsafe { ScriptInstance::from_raw(result.__bindgen_anon_1.m_hScript) })
else {
return Ok(None);
};
Ok(instance.entity(server)?)
}
#[doc(alias("SetAttentionFocus"))]
pub fn set_attention_focus(
self,
server: Server<'s>,
target: Entity<'s>,
) -> Result<(), BotError> {
let target = ScriptInstance::of(server, target)?;
unsafe { self.void(c"SetAttentionFocus", &mut [handle(target.as_raw())]) }
}
#[doc(alias("SetAutoJump"))]
pub fn set_auto_jump(self, min: f32, max: f32) -> Result<(), BotError> {
assert!(
min.is_finite() && max.is_finite() && 0.0 <= min && min <= max,
"invalid auto jump interval"
);
unsafe { self.void(c"SetAutoJump", &mut [float(min), float(max)]) }
}
#[doc(alias("SetDifficulty"))]
pub fn set_difficulty(self, difficulty: Difficulty) -> Result<(), BotError> {
unsafe { self.void(c"SetDifficulty", &mut [int(difficulty.to_raw())]) }
}
#[doc(alias("SetMaxVisionRangeOverride"))]
pub fn set_max_vision_range_override(self, range: Option<f32>) -> Result<(), BotError> {
let range = match range {
Some(range) => {
assert!(range.is_finite() && range > 0.0, "invalid vision range");
range
}
None => -1.0,
};
unsafe { self.void(c"SetMaxVisionRangeOverride", &mut [float(range)]) }
}
#[doc(alias("SetMission"))]
pub fn set_mission(self, mission: Mission, reset_behavior: bool) -> Result<(), BotError> {
unsafe {
self.void(
c"SetMission",
&mut [int(mission.to_raw()), boolean(reset_behavior)],
)
}
}
#[doc(alias("SetMissionTarget"))]
pub fn set_mission_target(
self,
server: Server<'s>,
target: Option<Entity<'s>>,
) -> Result<(), BotError> {
let target = match target {
Some(target) => ScriptInstance::of(server, target)?.as_raw(),
None => std::ptr::null_mut(),
};
unsafe { self.void(c"SetMissionTarget", &mut [handle(target)]) }
}
#[doc(alias("SetPrevMission"))]
pub fn set_previous_mission(self, mission: Mission) -> Result<(), BotError> {
unsafe { self.void(c"SetPrevMission", &mut [int(mission.to_raw())]) }
}
#[doc(alias("SetShouldQuickBuild"))]
pub fn set_quick_build(self, quick_build: bool) -> Result<(), BotError> {
unsafe { self.void(c"SetShouldQuickBuild", &mut [boolean(quick_build)]) }
}
#[doc(alias("SetScaleOverride"))]
pub fn set_scale_override(self, scale: Option<f32>) -> Result<(), BotError> {
let scale = match scale {
Some(scale) => {
assert!(scale.is_finite() && scale > 0.0, "invalid model scale");
scale
}
None => -1.0,
};
unsafe { self.void(c"SetScaleOverride", &mut [float(scale)]) }
}
#[doc(alias("SetSquadFormationError"))]
pub fn set_squad_formation_error(self, error: f32) -> Result<(), BotError> {
assert!(error.is_finite(), "invalid squad formation error");
unsafe { self.void(c"SetSquadFormationError", &mut [float(error)]) }
}
#[doc(alias("GetSquadFormationError"))]
pub fn squad_formation_error(self) -> Result<f32, BotError> {
unsafe { self.float(c"GetSquadFormationError", &mut []) }
}
unsafe fn void(
self,
name: &CStr,
arguments: &mut [sys::ScriptVariant_t],
) -> Result<(), BotError> {
unsafe { self.call(name, arguments, VOID) }.map(drop)
}
pub fn weapon_restrictions(self) -> Result<WeaponRestrictions, BotError> {
let mut restrictions = WeaponRestrictions::empty();
for flag in WeaponRestrictions::all().iter() {
if self.has_any_weapon_restriction(flag)? {
restrictions |= flag;
}
}
Ok(restrictions)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TfBotRequest {
bots: Bots,
team: Option<BotTeam>,
class: Option<PlayerClass>,
difficulty: Option<Difficulty>,
quota_managed: bool,
}
impl TfBotRequest {
pub const fn new(count: NonZeroU8) -> Self {
Self {
bots: Bots::Count(count),
team: None,
class: None,
difficulty: None,
quota_managed: false,
}
}
pub fn named(name: &CStr) -> Result<Self, BotNameError> {
check_name(name)?;
Ok(Self {
bots: Bots::Named(name.to_owned()),
..Self::new(NonZeroU8::MIN)
})
}
pub const fn class(&self) -> Option<PlayerClass> {
self.class
}
pub fn command(&self) -> CString {
let mut command = b"tf_bot_add".to_vec();
match &self.bots {
Bots::Count(count) => command.extend_from_slice(format!(" {count}").as_bytes()),
Bots::Named(name) => {
command.extend_from_slice(b" \"");
command.extend_from_slice(name.to_bytes());
command.push(b'"');
}
}
let arguments = [
self.team.map(BotTeam::argument),
self.class.map(class_argument),
self.difficulty.map(Difficulty::argument),
(!self.quota_managed).then_some(c"noquota"),
];
for argument in arguments.into_iter().flatten() {
command.push(b' ');
command.extend_from_slice(argument.to_bytes());
}
command.push(b'\n');
CString::new(command).expect("a request holds no NUL")
}
pub const fn count(&self) -> NonZeroU8 {
match self.bots {
Bots::Count(count) => count,
Bots::Named(_) => NonZeroU8::MIN,
}
}
pub const fn difficulty(&self) -> Option<Difficulty> {
self.difficulty
}
pub fn name(&self) -> Option<&CStr> {
match &self.bots {
Bots::Count(_) => None,
Bots::Named(name) => Some(name),
}
}
pub const fn quota_managed(&self) -> bool {
self.quota_managed
}
pub const fn team(&self) -> Option<BotTeam> {
self.team
}
pub const fn with_class(mut self, class: Option<PlayerClass>) -> Self {
self.class = class;
self
}
pub const fn with_difficulty(mut self, difficulty: Option<Difficulty>) -> Self {
self.difficulty = difficulty;
self
}
pub const fn with_quota_managed(mut self, quota_managed: bool) -> Self {
self.quota_managed = quota_managed;
self
}
pub const fn with_team(mut self, team: Option<BotTeam>) -> Self {
self.team = team;
self
}
}
#[doc(alias("tf_bot_add"))]
pub fn add_tf_bots(server: Server<'_>, request: &TfBotRequest) -> Result<(), BotError> {
if server.game() != Game::TeamFortress2 {
return Err(BotError::UnsupportedGame);
}
server.valve_engine()?.server_command(&request.command());
Ok(())
}
fn check_name(name: &CStr) -> Result<(), BotNameError> {
let bytes = name.to_bytes();
if bytes.is_empty() {
return Err(BotNameError::Empty);
}
if bytes.len() > MAX_NAME_LEN {
return Err(BotNameError::TooLong { len: bytes.len() });
}
if let Some((index, &byte)) = bytes
.iter()
.enumerate()
.find(|&(_, &byte)| byte < 0x20 || byte == 0x7f || byte == b'"' || byte == b';')
{
return Err(BotNameError::InvalidByte { index, byte });
}
let reserved = PlayerClass::ALL
.into_iter()
.map(class_argument)
.chain([c"civilian", c"red", c"blue", c"noquota"])
.chain(Difficulty::ALL.map(Difficulty::argument))
.any(|argument| argument.to_bytes().eq_ignore_ascii_case(bytes));
if reserved || is_positive_for_atoi(bytes) {
return Err(BotNameError::Reserved);
}
Ok(())
}
const fn class_argument(class: PlayerClass) -> &'static CStr {
match class {
PlayerClass::Scout => c"scout",
PlayerClass::Sniper => c"sniper",
PlayerClass::Soldier => c"soldier",
PlayerClass::Demoman => c"demoman",
PlayerClass::Medic => c"medic",
PlayerClass::Heavy => c"heavyweapons",
PlayerClass::Pyro => c"pyro",
PlayerClass::Spy => c"spy",
PlayerClass::Engineer => c"engineer",
}
}
fn is_positive_for_atoi(bytes: &[u8]) -> bool {
let mut rest = bytes
.iter()
.skip_while(|byte| matches!(byte, b' ' | b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r'))
.peekable();
match rest.peek() {
Some(b'+') => {
rest.next();
}
Some(b'-') => return false,
_ => {}
}
rest.take_while(|byte| byte.is_ascii_digit())
.any(|&digit| digit != b'0')
}
#[doc(alias("tf_bot_kick"))]
pub fn kick_tf_bots(server: Server<'_>, team: Option<BotTeam>) -> Result<(), BotError> {
if server.game() != Game::TeamFortress2 {
return Err(BotError::UnsupportedGame);
}
let command = match team {
None => c"tf_bot_kick all\n",
Some(BotTeam::Red) => c"tf_bot_kick red\n",
Some(BotTeam::Blue) => c"tf_bot_kick blue\n",
};
server.valve_engine()?.server_command(command);
Ok(())
}