#[cfg(test)]
#[path = "../tests/tf2/projectiles.rs"]
mod tests;
use crate::datatables::{NetProp, NetPropError, NetVar};
use crate::entities::Entity;
use crate::{Game, InterfaceError, Server};
use sdk_raw::tf2::projectiles as raw;
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
const CLASSES: &CStr = c"tf_projectile_*";
#[doc(alias("m_iProjectileType", "ProjectileType_t"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArrowKind {
#[doc(alias("TF_PROJECTILE_ARROW"))]
Arrow,
#[doc(alias("TF_PROJECTILE_BUILDING_REPAIR_BOLT"))]
BuildingRepairBolt,
#[doc(alias("TF_PROJECTILE_FESTIVE_ARROW"))]
FestiveArrow,
#[doc(alias("TF_PROJECTILE_FESTIVE_HEALING_BOLT"))]
FestiveHealingBolt,
#[doc(alias("TF_PROJECTILE_GRAPPLINGHOOK"))]
GrapplingHook,
#[doc(alias("TF_PROJECTILE_HEALING_BOLT"))]
HealingBolt,
}
impl ArrowKind {
pub const fn from_raw(raw: c_int) -> Option<Self> {
Some(match raw {
raw::TF_PROJECTILE_ARROW => Self::Arrow,
raw::TF_PROJECTILE_BUILDING_REPAIR_BOLT => Self::BuildingRepairBolt,
raw::TF_PROJECTILE_FESTIVE_ARROW => Self::FestiveArrow,
raw::TF_PROJECTILE_FESTIVE_HEALING_BOLT => Self::FestiveHealingBolt,
raw::TF_PROJECTILE_GRAPPLINGHOOK => Self::GrapplingHook,
raw::TF_PROJECTILE_HEALING_BOLT => Self::HealingBolt,
_ => return None,
})
}
pub const fn to_raw(self) -> c_int {
match self {
Self::Arrow => raw::TF_PROJECTILE_ARROW,
Self::BuildingRepairBolt => raw::TF_PROJECTILE_BUILDING_REPAIR_BOLT,
Self::FestiveArrow => raw::TF_PROJECTILE_FESTIVE_ARROW,
Self::FestiveHealingBolt => raw::TF_PROJECTILE_FESTIVE_HEALING_BOLT,
Self::GrapplingHook => raw::TF_PROJECTILE_GRAPPLINGHOOK,
Self::HealingBolt => raw::TF_PROJECTILE_HEALING_BOLT,
}
}
}
#[doc(alias("CTFFlameManager", "tf_flame_manager"))]
#[derive(Debug, Clone, Copy)]
pub struct FlameManager<'s> {
server: Server<'s>,
entity: Entity<'s>,
}
impl<'s> FlameManager<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, ProjectileError> {
if server.game() != Game::TeamFortress2 {
return Err(ProjectileError::WrongGame);
}
if !entity.has_data_map_class(c"CTFFlameManager") {
return Err(ProjectileError::NotFlameManager);
}
Ok(Self { server, entity })
}
#[doc(alias("m_hAttacker", "GetAttacker"))]
pub fn attacker(self) -> Result<Option<Entity<'s>>, ProjectileError> {
handle(self.server, self.entity, c"m_hAttacker")
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
#[doc(alias("m_bIsFiring"))]
pub fn is_firing(self) -> Result<bool, ProjectileError> {
get(self.server, self.entity, c"m_bIsFiring")
}
#[doc(alias("m_hWeapon"))]
pub fn weapon(self) -> Result<Option<Entity<'s>>, ProjectileError> {
handle(self.server, self.entity, c"m_hWeapon")
}
}
#[doc(alias("m_iType", "TF_GL_MODE"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PipebombKind {
#[doc(alias("TF_GL_MODE_CANNONBALL"))]
Cannonball,
#[doc(alias("TF_GL_MODE_REGULAR"))]
Grenade,
#[doc(alias("TF_GL_MODE_REMOTE_DETONATE_PRACTICE"))]
PracticeStickybomb,
#[doc(alias("TF_GL_MODE_REMOTE_DETONATE"))]
Stickybomb,
}
impl PipebombKind {
pub const fn from_raw(raw: c_int) -> Option<Self> {
Some(match raw {
raw::TF_GL_MODE_CANNONBALL => Self::Cannonball,
raw::TF_GL_MODE_REGULAR => Self::Grenade,
raw::TF_GL_MODE_REMOTE_DETONATE_PRACTICE => Self::PracticeStickybomb,
raw::TF_GL_MODE_REMOTE_DETONATE => Self::Stickybomb,
_ => return None,
})
}
pub const fn to_raw(self) -> c_int {
match self {
Self::Cannonball => raw::TF_GL_MODE_CANNONBALL,
Self::Grenade => raw::TF_GL_MODE_REGULAR,
Self::PracticeStickybomb => raw::TF_GL_MODE_REMOTE_DETONATE_PRACTICE,
Self::Stickybomb => raw::TF_GL_MODE_REMOTE_DETONATE,
}
}
}
#[doc(alias("CBaseProjectile"))]
#[derive(Debug, Clone, Copy)]
pub struct Projectile<'s> {
server: Server<'s>,
entity: Entity<'s>,
family: ProjectileFamily,
}
impl<'s> Projectile<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, ProjectileError> {
if server.game() != Game::TeamFortress2 {
return Err(ProjectileError::WrongGame);
}
let family = ProjectileFamily::ALL
.into_iter()
.find(|family| entity.has_data_map_class(family.data_map_class()))
.ok_or(ProjectileError::NotProjectile)?;
Ok(Self {
server,
entity,
family,
})
}
pub fn all(server: Server<'s>) -> Result<impl Iterator<Item = Self>, ProjectileError> {
if server.game() != Game::TeamFortress2 {
return Err(ProjectileError::WrongGame);
}
let tools = server.server_tools()?;
Ok(
std::iter::successors(tools.find_by_class_name(None, CLASSES), move |&entity| {
tools.find_by_class_name(Some(entity), CLASSES)
})
.filter(|entity| !entity.is_marked_for_deletion())
.filter_map(move |entity| Self::new(server, entity).ok()),
)
}
#[doc(alias("m_iProjectileType", "GetProjectileType"))]
pub fn arrow_kind(self) -> Result<Option<ArrowKind>, ProjectileError> {
Ok(ArrowKind::from_raw(self.get(c"m_iProjectileType")?))
}
#[doc(alias("GetDamage", "m_flDamage"))]
pub fn damage(self) -> f32 {
let projectile = self.entity.as_ptr();
unsafe { vcall!(projectile as sys::CBaseEntity__bindgen_vtable => CBaseEntity_GetDamage()) }
}
#[doc(alias("m_hDeflectOwner", "GetDeflectOwner"))]
pub fn deflected_by(self) -> Result<Option<Entity<'s>>, ProjectileError> {
self.handle(c"m_hDeflectOwner")
}
#[doc(alias("m_iDeflected", "GetDeflected"))]
pub fn deflections(self) -> Result<c_int, ProjectileError> {
self.get(c"m_iDeflected")
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
pub const fn family(self) -> ProjectileFamily {
self.family
}
fn get<T: NetVar>(self, name: &CStr) -> Result<T, ProjectileError> {
get(self.server, self.entity, name)
}
fn handle(self, name: &CStr) -> Result<Option<Entity<'s>>, ProjectileError> {
handle(self.server, self.entity, name)
}
#[doc(alias("m_bTouched"))]
pub fn has_touched(self) -> Result<bool, ProjectileError> {
self.get(c"m_bTouched")
}
#[doc(alias("m_bArrowAlight"))]
pub fn is_alight(self) -> Result<bool, ProjectileError> {
self.get(c"m_bArrowAlight")
}
#[doc(alias("m_bCritical", "IsCritical"))]
pub fn is_critical(self) -> Result<bool, ProjectileError> {
self.get(c"m_bCritical")
}
pub fn kind(self) -> Option<ProjectileKind> {
ProjectileKind::from_class_name(self.entity.class_name())
}
#[doc(alias("m_hLauncher", "GetLauncher"))]
pub fn launcher(self) -> Result<Option<Entity<'s>>, ProjectileError> {
self.handle(c"m_hLauncher")
}
fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, ProjectileError> {
net_prop(self.server, self.entity, name)
}
#[doc(alias("m_hOriginalLauncher", "GetOriginalLauncher"))]
pub fn original_launcher(self) -> Result<Option<Entity<'s>>, ProjectileError> {
self.handle(c"m_hOriginalLauncher")
}
#[doc(alias("m_hOwnerEntity", "GetOwnerEntity"))]
pub fn owner(self) -> Result<Option<Entity<'s>>, ProjectileError> {
self.handle(c"m_hOwnerEntity")
}
#[doc(alias("m_iType"))]
pub fn pipebomb_kind(self) -> Result<Option<PipebombKind>, ProjectileError> {
Ok(PipebombKind::from_raw(self.get(c"m_iType")?))
}
#[doc(alias("SetCritical"))]
pub fn set_critical(self, critical: bool) -> Result<(), ProjectileError> {
let prop = self.net_prop(c"m_bCritical")?;
let engine = self.server.valve_engine()?;
unsafe { prop.set(engine, self.entity, critical) }?;
Ok(())
}
#[doc(alias("SetDamage", "m_flDamage"))]
pub fn set_damage(self, damage: f32) -> Result<(), ProjectileError> {
if !damage.is_finite() || damage < 0.0 {
return Err(ProjectileError::InvalidDamage(damage));
}
let projectile = self.entity.as_ptr();
unsafe {
vcall!(projectile as sys::CBaseEntity__bindgen_vtable => CBaseEntity_SetDamage(damage))
};
Ok(())
}
#[doc(alias("m_hThrower", "GetThrower"))]
pub fn thrower(self) -> Result<Option<Entity<'s>>, ProjectileError> {
self.handle(c"m_hThrower")
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProjectileError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("{0} is not a damage a projectile can do")]
InvalidDamage(f32),
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error("the entity is not a TF2 flame manager")]
NotFlameManager,
#[error("the entity is not a TF2 projectile")]
NotProjectile,
#[error("projectile operations require Team Fortress 2")]
WrongGame,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProjectileFamily {
#[doc(alias("CTFWeaponBaseGrenadeProj", "CBaseGrenade"))]
Grenade,
#[doc(alias("CTFBaseProjectile"))]
Nail,
#[doc(alias("CTFBaseRocket"))]
Rocket,
}
impl ProjectileFamily {
pub const ALL: [Self; 3] = [Self::Grenade, Self::Nail, Self::Rocket];
pub const fn data_map_class(self) -> &'static CStr {
match self {
Self::Grenade => c"CTFWeaponBaseGrenadeProj",
Self::Nail => c"CTFBaseProjectile",
Self::Rocket => c"CTFBaseRocket",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ProjectileKind {
#[doc(alias("tf_projectile_arrow", "CTFProjectile_Arrow"))]
Arrow,
#[doc(alias("tf_projectile_balloffire", "CTFProjectile_BallOfFire"))]
BallOfFire,
#[doc(alias("tf_projectile_cleaver", "CTFProjectile_Cleaver"))]
Cleaver,
#[doc(alias("tf_projectile_energy_ball", "CTFProjectile_EnergyBall"))]
EnergyBall,
#[doc(alias("tf_projectile_energy_ring", "CTFProjectile_EnergyRing"))]
EnergyRing,
#[doc(alias("tf_projectile_flare", "CTFProjectile_Flare"))]
Flare,
#[doc(alias("tf_projectile_grapplinghook", "CTFProjectile_GrapplingHook"))]
GrapplingHook,
#[doc(alias("tf_projectile_healing_bolt", "CTFProjectile_HealingBolt"))]
HealingBolt,
#[doc(alias("tf_projectile_jar", "CTFProjectile_Jar"))]
Jar,
#[doc(alias("tf_projectile_jar_gas", "CTFProjectile_JarGas"))]
JarGas,
#[doc(alias("tf_projectile_jar_milk", "CTFProjectile_JarMilk"))]
JarMilk,
#[doc(alias("tf_projectile_mechanicalarmorb", "CTFProjectile_MechanicalArmOrb"))]
MechanicalArmOrb,
#[doc(alias("tf_projectile_ball_ornament", "CTFBall_Ornament"))]
Ornament,
#[doc(alias("tf_projectile_pipe", "CTFGrenadePipebombProjectile"))]
Pipe,
#[doc(alias("tf_projectile_pipe_remote", "CTFGrenadePipebombProjectile"))]
PipeRemote,
#[doc(alias("tf_projectile_rocket", "CTFProjectile_Rocket"))]
Rocket,
#[doc(alias("tf_projectile_sentryrocket", "CTFProjectile_SentryRocket"))]
SentryRocket,
#[doc(alias("tf_projectile_spellbats", "CTFProjectile_SpellBats"))]
SpellBats,
#[doc(alias("tf_projectile_spellfireball", "CTFProjectile_SpellFireball"))]
SpellFireball,
#[doc(alias("tf_projectile_spellkartbats", "CTFProjectile_SpellKartBats"))]
SpellKartBats,
#[doc(alias("tf_projectile_spellkartorb", "CTFProjectile_SpellKartOrb"))]
SpellKartOrb,
#[doc(alias("tf_projectile_lightningorb", "CTFProjectile_SpellLightningOrb"))]
SpellLightningOrb,
#[doc(alias("tf_projectile_spellmeteorshower", "CTFProjectile_SpellMeteorShower"))]
SpellMeteorShower,
#[doc(alias("tf_projectile_spellmirv", "CTFProjectile_SpellMirv"))]
SpellMirv,
#[doc(alias("tf_projectile_spellpumpkin", "CTFProjectile_SpellPumpkin"))]
SpellPumpkin,
#[doc(alias("tf_projectile_spellspawnboss", "CTFProjectile_SpellSpawnBoss"))]
SpellSpawnBoss,
#[doc(alias("tf_projectile_spellspawnhorde", "CTFProjectile_SpellSpawnHorde"))]
SpellSpawnHorde,
#[doc(alias("tf_projectile_spellspawnzombie", "CTFProjectile_SpellSpawnZombie"))]
SpellSpawnZombie,
#[doc(alias(
"tf_projectile_spelltransposeteleport",
"CTFProjectile_SpellTransposeTeleport"
))]
SpellTransposeTeleport,
#[doc(alias("tf_projectile_stun_ball", "CTFStunBall"))]
StunBall,
#[doc(alias("tf_projectile_syringe", "CTFProjectile_Syringe"))]
Syringe,
#[doc(alias("tf_projectile_throwable", "CTFProjectile_Throwable"))]
Throwable,
#[doc(alias(
"tf_projectile_throwable_breadmonster",
"CTFProjectile_ThrowableBreadMonster"
))]
ThrowableBreadMonster,
#[doc(alias("tf_projectile_throwable_brick", "CTFProjectile_ThrowableBrick"))]
ThrowableBrick,
#[doc(alias("tf_projectile_throwable_repel", "CTFProjectile_ThrowableRepel"))]
ThrowableRepel,
}
impl ProjectileKind {
pub const ALL: [Self; 35] = [
Self::Arrow,
Self::Ornament,
Self::BallOfFire,
Self::Cleaver,
Self::EnergyBall,
Self::EnergyRing,
Self::Flare,
Self::GrapplingHook,
Self::HealingBolt,
Self::Jar,
Self::JarGas,
Self::JarMilk,
Self::SpellLightningOrb,
Self::MechanicalArmOrb,
Self::Pipe,
Self::PipeRemote,
Self::Rocket,
Self::SentryRocket,
Self::SpellBats,
Self::SpellFireball,
Self::SpellKartBats,
Self::SpellKartOrb,
Self::SpellMeteorShower,
Self::SpellMirv,
Self::SpellPumpkin,
Self::SpellSpawnBoss,
Self::SpellSpawnHorde,
Self::SpellSpawnZombie,
Self::SpellTransposeTeleport,
Self::StunBall,
Self::Syringe,
Self::Throwable,
Self::ThrowableBreadMonster,
Self::ThrowableBrick,
Self::ThrowableRepel,
];
pub fn from_class_name(class_name: &CStr) -> Option<Self> {
Self::ALL
.into_iter()
.find(|kind| kind.class_name() == class_name)
}
pub const fn class_name(self) -> &'static CStr {
match self {
Self::Arrow => c"tf_projectile_arrow",
Self::BallOfFire => c"tf_projectile_balloffire",
Self::Cleaver => c"tf_projectile_cleaver",
Self::EnergyBall => c"tf_projectile_energy_ball",
Self::EnergyRing => c"tf_projectile_energy_ring",
Self::Flare => c"tf_projectile_flare",
Self::GrapplingHook => c"tf_projectile_grapplinghook",
Self::HealingBolt => c"tf_projectile_healing_bolt",
Self::Jar => c"tf_projectile_jar",
Self::JarGas => c"tf_projectile_jar_gas",
Self::JarMilk => c"tf_projectile_jar_milk",
Self::MechanicalArmOrb => c"tf_projectile_mechanicalarmorb",
Self::Ornament => c"tf_projectile_ball_ornament",
Self::Pipe => c"tf_projectile_pipe",
Self::PipeRemote => c"tf_projectile_pipe_remote",
Self::Rocket => c"tf_projectile_rocket",
Self::SentryRocket => c"tf_projectile_sentryrocket",
Self::SpellBats => c"tf_projectile_spellbats",
Self::SpellFireball => c"tf_projectile_spellfireball",
Self::SpellKartBats => c"tf_projectile_spellkartbats",
Self::SpellKartOrb => c"tf_projectile_spellkartorb",
Self::SpellLightningOrb => c"tf_projectile_lightningorb",
Self::SpellMeteorShower => c"tf_projectile_spellmeteorshower",
Self::SpellMirv => c"tf_projectile_spellmirv",
Self::SpellPumpkin => c"tf_projectile_spellpumpkin",
Self::SpellSpawnBoss => c"tf_projectile_spellspawnboss",
Self::SpellSpawnHorde => c"tf_projectile_spellspawnhorde",
Self::SpellSpawnZombie => c"tf_projectile_spellspawnzombie",
Self::SpellTransposeTeleport => c"tf_projectile_spelltransposeteleport",
Self::StunBall => c"tf_projectile_stun_ball",
Self::Syringe => c"tf_projectile_syringe",
Self::Throwable => c"tf_projectile_throwable",
Self::ThrowableBreadMonster => c"tf_projectile_throwable_breadmonster",
Self::ThrowableBrick => c"tf_projectile_throwable_brick",
Self::ThrowableRepel => c"tf_projectile_throwable_repel",
}
}
pub const fn family(self) -> ProjectileFamily {
match self {
Self::Arrow
| Self::BallOfFire
| Self::EnergyBall
| Self::Flare
| Self::GrapplingHook
| Self::HealingBolt
| Self::MechanicalArmOrb
| Self::Rocket
| Self::SentryRocket
| Self::SpellFireball
| Self::SpellKartOrb
| Self::SpellLightningOrb => ProjectileFamily::Rocket,
Self::EnergyRing | Self::Syringe => ProjectileFamily::Nail,
Self::Cleaver
| Self::Jar
| Self::JarGas
| Self::JarMilk
| Self::Ornament
| Self::Pipe
| Self::PipeRemote
| Self::SpellBats
| Self::SpellKartBats
| Self::SpellMeteorShower
| Self::SpellMirv
| Self::SpellPumpkin
| Self::SpellSpawnBoss
| Self::SpellSpawnHorde
| Self::SpellSpawnZombie
| Self::SpellTransposeTeleport
| Self::StunBall
| Self::Throwable
| Self::ThrowableBreadMonster
| Self::ThrowableBrick
| Self::ThrowableRepel => ProjectileFamily::Grenade,
}
}
}
fn get<T: NetVar>(
server: Server<'_>,
entity: Entity<'_>,
name: &CStr,
) -> Result<T, ProjectileError> {
Ok(net_prop(server, entity, name)?.get(entity)?)
}
fn handle<'s>(
server: Server<'s>,
entity: Entity<'s>,
name: &CStr,
) -> Result<Option<Entity<'s>>, ProjectileError> {
let handle = net_prop(server, entity, name)?.get_handle(entity)?;
Ok(server.server_tools()?.entity_by_handle(handle))
}
fn net_prop<'s>(
server: Server<'s>,
entity: Entity<'s>,
name: &CStr,
) -> Result<NetProp<'s>, ProjectileError> {
Ok(server.server_game_dll()?.entity_net_prop(entity, name)?)
}