1//! TF2's bosses: the Halloween bosses, skeletons and their spawners,
2//! `base_boss`, and the boss health bar.
3//!
4//! # Spawning
5//!
6//! [`spawn_halloween_boss`] and [`spawn_skeleton`] spawn a boss or skeleton
7//! as the game's own `CHalloweenBaseBoss::SpawnBossAtPos` and
8//! `CZombie::SpawnAtPos` do (`game/server/tf/halloween`): they create the
9//! entity, put it on its team, including the Halloween team the game's own
10//! bosses fight on, and spawn it. Each boss computes its health in its
11//! `Spawn`, from the players on RED and BLU and its convars, so a health
12//! given to [`spawn_halloween_boss`] is set afterwards.
13//!
14//! [`SkeletonSpawner`] places a `tf_zombie_spawner`, the map entity that
15//! spawns skeletons of any type, with a lifetime, while it is enabled.
16//! [`BaseBoss`] spawns and controls a `base_boss`, the NextBot actor that
17//! Mann vs. Machine's tank derives from. The spawned entities
18//! are NextBot actors, which [`NextBot`](crate::tf2::bots::NextBot) wraps.
19//!
20//! The bosses and skeletons find their way through the level's navigation
21//! mesh, and stand still on a level without one.
22//!
23//! # Boss health bar
24//!
25//! [`BossBar`] reads and sets the health bar the HUD shows at the top of the
26//! screen, which Monoculus and Merasmus fill while they live. It belongs to
27//! no boss: the game shows whatever was last set, and hides it at the start
28//! of each round.
29//!
30//! # Unverified
31//!
32//! The wrappers follow Valve's Source SDK 2013 and have not been tested on a
33//! live server.
3435#[cfg(test)]
36#[path = "../tests/tf2/bosses.rs"]
37mod tests;
3839use crate::datatables::NetPropError;
40use crate::entities::Entity;
41use crate::entities::health::HealthError;
42use crate::entities::spawn::{EntitySpawn, SpawnError};
43use crate::inputs::{InputError, InputValue};
44use crate::interfaces::ServerTools;
45use crate::math::Vector;
46use crate::tf2::script_binding::{selfas binding, BindingError, VOID};
47use crate::tf2::teams::Team;
48use crate::{Game, InterfaceError, Server};
49use sdk_raw::players::TEAM_UNASSIGNED;
50use sdk_raw::tf2::bosses::{selfas raw, skeleton};
51use sdk_raw::tf2::scoreboard::{TF_TEAM_BLUE, TF_TEAM_RED};
52use sdk_raw::tf2::script_binding::boolean;
53use sdk_raw::vcall;
54use std::ffi::{CStr, CString, c_int};
55use std::fmt::Display;
5657/// The most a boss bar shows: the full bar, as a byte of the monster
58/// resource.
59const FULL_BAR: c_int = 255;
6061/// Why a boss could not be spawned, wrapped or controlled.
62#[derive(#[automatically_derived]
impl ::core::fmt::Debug for BossError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
Self::UnsupportedGame =>
::core::fmt::Formatter::write_str(f, "UnsupportedGame"),
Self::WrongClass { class: __self_0 } =>
::core::fmt::Formatter::debug_struct_field1_finish(f,
"WrongClass", "class", &__self_0),
Self::MarkedForDeletion =>
::core::fmt::Formatter::write_str(f, "MarkedForDeletion"),
Self::NonFinite =>
::core::fmt::Formatter::write_str(f, "NonFinite"),
Self::NonPositiveHealth(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"NonPositiveHealth", &__self_0),
Self::ModelNotPrecached =>
::core::fmt::Formatter::write_str(f, "ModelNotPrecached"),
Self::UnsupportedMethod =>
::core::fmt::Formatter::write_str(f, "UnsupportedMethod"),
Self::Rejected =>
::core::fmt::Formatter::write_str(f, "Rejected"),
Self::Health(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Health",
&__self_0),
Self::Input(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Input",
&__self_0),
Self::Interface(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"Interface", &__self_0),
Self::NetProp(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"NetProp", &__self_0),
Self::Spawn(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Spawn",
&__self_0),
}
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for BossError {
#[inline]
fn clone(&self) -> Self {
match self {
Self::UnsupportedGame => BossError::UnsupportedGame,
Self::WrongClass { class: __self_0 } =>
BossError::WrongClass {
class: ::core::clone::Clone::clone(__self_0),
},
Self::MarkedForDeletion => BossError::MarkedForDeletion,
Self::NonFinite => BossError::NonFinite,
Self::NonPositiveHealth(__self_0) =>
BossError::NonPositiveHealth(::core::clone::Clone::clone(__self_0)),
Self::ModelNotPrecached => BossError::ModelNotPrecached,
Self::UnsupportedMethod => BossError::UnsupportedMethod,
Self::Rejected => BossError::Rejected,
Self::Health(__self_0) =>
BossError::Health(::core::clone::Clone::clone(__self_0)),
Self::Input(__self_0) =>
BossError::Input(::core::clone::Clone::clone(__self_0)),
Self::Interface(__self_0) =>
BossError::Interface(::core::clone::Clone::clone(__self_0)),
Self::NetProp(__self_0) =>
BossError::NetProp(::core::clone::Clone::clone(__self_0)),
Self::Spawn(__self_0) =>
BossError::Spawn(::core::clone::Clone::clone(__self_0)),
}
}
}Clone, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for BossError { }
#[automatically_derived]
impl ::core::cmp::PartialEq for BossError {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other) &&
match (self, other) {
(Self::WrongClass { class: __self_0 }, Self::WrongClass {
class: __arg1_0 }) => __self_0 == __arg1_0,
(Self::NonPositiveHealth(__self_0),
Self::NonPositiveHealth(__arg1_0)) => __self_0 == __arg1_0,
(Self::Health(__self_0), Self::Health(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::Input(__self_0), Self::Input(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::Interface(__self_0), Self::Interface(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::NetProp(__self_0), Self::NetProp(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::Spawn(__self_0), Self::Spawn(__arg1_0)) =>
__self_0 == __arg1_0,
_ => true,
}
}
}PartialEq, #[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private21::Error for BossError {
fn source(&self)
->
::core::option::Option<&(dyn ::thiserror::__private21::Error +
'static)> {
use ::thiserror::__private21::AsDynError as _;
#[allow(deprecated)]
match self {
BossError::UnsupportedGame { .. } => ::core::option::Option::None,
BossError::WrongClass { .. } => ::core::option::Option::None,
BossError::MarkedForDeletion { .. } =>
::core::option::Option::None,
BossError::NonFinite { .. } => ::core::option::Option::None,
BossError::NonPositiveHealth { .. } =>
::core::option::Option::None,
BossError::ModelNotPrecached { .. } =>
::core::option::Option::None,
BossError::UnsupportedMethod { .. } =>
::core::option::Option::None,
BossError::Rejected { .. } => ::core::option::Option::None,
BossError::Health { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
BossError::Input { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
BossError::Interface { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
BossError::NetProp { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
BossError::Spawn { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
}
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for BossError {
fn fmt(&self, __formatter: &mut ::core::fmt::Formatter)
-> ::core::fmt::Result {
use ::thiserror::__private21::AsDisplay as _;
#[allow(unused_variables, deprecated, clippy ::
used_underscore_binding)]
match self {
BossError::UnsupportedGame {} =>
__formatter.write_str("bosses require Team Fortress 2"),
BossError::WrongClass { class } =>
match (class,) {
(__field_class,) =>
__formatter.write_fmt(format_args!("the entity is not a {0:?}",
__field_class)),
},
BossError::MarkedForDeletion {} =>
__formatter.write_str("the entity is marked for deletion"),
BossError::NonFinite {} =>
__formatter.write_str("a value given is NaN or infinite"),
BossError::NonPositiveHealth(_0) =>
match (_0.as_display(),) {
(__display0,) =>
__formatter.write_fmt(format_args!("health {0} is not positive",
__display0)),
},
BossError::ModelNotPrecached {} =>
__formatter.write_str("the model is not precached"),
BossError::UnsupportedMethod {} =>
__formatter.write_str("the game does not expose the expected native method"),
BossError::Rejected {} =>
__formatter.write_str("the native method rejected the call"),
BossError::Health(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
BossError::Input(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
BossError::Interface(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
BossError::NetProp(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
BossError::Spawn(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
}
}
}
BossError
#[allow(clippy :: redundant_field_names)]
fn from(source: HealthError) -> Self { BossError::Health { 0: source } }
BossError
#[allow(clippy :: redundant_field_names)]
fn from(source: InputError) -> Self { BossError::Input { 0: source } }
BossError
#[allow(clippy :: redundant_field_names)]
fn from(source: InterfaceError) -> Self { BossError::Interface { 0: source } }
BossError
#[allow(clippy :: redundant_field_names)]
fn from(source: NetPropError) -> Self { BossError::NetProp { 0: source } }
BossError
#[allow(clippy :: redundant_field_names)]
fn from(source: SpawnError) -> Self { BossError::Spawn { 0: source } }thiserror::Error)]
63pub enum BossError {
64/// The server is not running Team Fortress 2.
65#[error("bosses require Team Fortress 2")]
66UnsupportedGame,
6768/// The entity is not of the class the wrapper takes.
69#[error("the entity is not a {class:?}")]
70WrongClass {
71/// The data map class the wrapper takes.
72class: &'static CStr,
73 },
7475/// The entity is marked for deletion.
76#[error("the entity is marked for deletion")]
77MarkedForDeletion,
7879/// A position, speed, height, lifetime or fraction given is NaN or
80 /// infinite.
81#[error("a value given is NaN or infinite")]
82NonFinite,
8384/// A health given is 0 or less, which game code divides by.
85#[error("health {0} is not positive")]
86NonPositiveHealth(c_int),
8788/// A `base_boss` model given is not precached. Precaching it while the
89 /// level runs could overflow the engine's model table, so it must be
90 /// precached already, as the level's own models are, or with
91 /// [`Server::precache_model`] while the level loads.
92#[error("the model is not precached")]
93ModelNotPrecached,
9495/// The script class descriptors lack a native method, or its signature
96 /// differs from the SDK's.
97#[error("the game does not expose the expected native method")]
98UnsupportedMethod,
99100/// A native method's binding adapter reported failure.
101#[error("the native method rejected the call")]
102Rejected,
103104/// The new boss's health could not be set.
105#[error(transparent)]
106Health(#[from] HealthError),
107108/// An input was not sent, or the entity rejected it.
109#[error(transparent)]
110Input(#[from] InputError),
111112/// A required engine interface is unavailable.
113#[error(transparent)]
114Interface(#[from] InterfaceError),
115116/// A networked variable could not be read or written.
117#[error(transparent)]
118NetProp(#[from] NetPropError),
119120/// The game created no entity of the class, the entity refused a key
121 /// value and was removed, or it removed itself as it spawned.
122#[error(transparent)]
123Spawn(#[from] SpawnError),
124}
125126impl From<BindingError> for BossError {
127fn from(error: BindingError) -> Self {
128match error {
129 BindingError::Unavailable | BindingError::SignatureMismatch => Self::UnsupportedMethod,
130 BindingError::Rejected => Self::Rejected,
131 }
132 }
133}
134135/// A `base_boss` (`CTFBaseBoss`) within the current engine callback, or one
136/// of the classes deriving from it, such as Mann vs. Machine's tank
137/// (`tank_boss`).
138///
139/// A `base_boss` is a NextBot actor with a model, health and speed, and no
140/// behaviour of its own: it moves only as its locomotion is told to, which
141/// the tank's own behaviour does along its path. While enabled, it pushes
142/// living players out of its way, unless told not to. Killed, it fires
143/// `OnKilled` and drops money packs. Send it the health inputs through
144/// [`ServerTools::send_health_input`](crate::interfaces::ServerTools::send_health_input).
145#[doc(alias("base_boss", "CTFBaseBoss"))]
146#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for BaseBoss<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "BaseBoss",
"server", &self.server, "entity", &&self.entity)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for BaseBoss<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for BaseBoss<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<Server<'s>>;
let _: ::core::clone::AssertParamIsClone<Entity<'s>>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for BaseBoss<'s> { }Copy)]
147pub struct BaseBoss<'s> {
148 server: Server<'s>,
149 entity: Entity<'s>,
150}
151152impl<'s> BaseBoss<'s> {
153/// Wraps `entity`. Fails with [`BossError::UnsupportedGame`] outside TF2,
154 /// and with [`BossError::WrongClass`] unless its data maps include
155 /// `CTFBaseBoss`'s.
156pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BossError> {
157 check_class(server, entity, raw::BASE_BOSS_CLASS)?;
158159Ok(Self { server, entity })
160 }
161162/// Spawns a `base_boss` as `spawn` describes.
163 ///
164 /// Fails, before creating it, with [`BossError::NonFinite`] for a position
165 /// or speed that is not finite, [`BossError::NonPositiveHealth`] for a
166 /// health of 0 or less, and [`BossError::ModelNotPrecached`] for a model
167 /// that is not precached.
168pub fn spawn(server: Server<'s>, spawn: &BaseBossSpawn<'_>) -> Result<Self, BossError> {
169 check_game(server)?;
170171if !spawn.origin.is_finite() || !spawn.speed.is_none_or(f32::is_finite) {
172return Err(BossError::NonFinite);
173 }
174175if spawn.health <= 0 {
176return Err(BossError::NonPositiveHealth(spawn.health));
177 }
178179if server.model_info()?.model_index(spawn.model).is_none() {
180return Err(BossError::ModelNotPrecached);
181 }
182183let mut boss = EntitySpawn::new(raw::BASE_BOSS)
184 .origin(spawn.origin)
185 .model(spawn.model)
186 .key(c"health", &number_value(spawn.health))
187 .key(c"teamnumber", &number_value(spawn.team.to_raw()))
188 .key(c"start_disabled", flag_value(spawn.start_disabled));
189190if let Some(speed) = spawn.speed {
191boss = boss.key(c"speed", &number_value(speed));
192 }
193194// SAFETY: `CTFBaseBoss`'s constructor only sets its members and makes
195 // its NextBot components (`game/server/tf/player_vs_environment`). Its
196 // `Spawn` precaches its model, which the check above found precached,
197 // sets its model and health, adds it to the game rules' bosses, and
198 // starts it thinking. Its `Activate` is `CBaseAnimating`'s, which only
199 // finds entities by name.
200let entity = unsafe { boss.spawn(server.server_tools()?) }?;
201202Ok(Self { server, entity })
203 }
204205/// Stops the boss: it neither updates nor pushes players (`Disable`).
206#[doc(alias("Disable"))]
207pub fn disable(self) -> Result<(), BossError> {
208self.input(c"Disable", InputValue::Void)
209 }
210211/// Lets the boss update and push players again (`Enable`).
212#[doc(alias("Enable"))]
213pub fn enable(self) -> Result<(), BossError> {
214self.input(c"Enable", InputValue::Void)
215 }
216217/// The boss's entity.
218pub const fn entity(self) -> Entity<'s> {
219self.entity
220 }
221222/// Sends the boss an input, with itself as the activator and caller.
223fn input(self, name: &CStr, value: InputValue<'_>) -> Result<(), BossError> {
224let tools = self.server.server_tools()?;
225226Ok(tools.accept_input(self.entity, name, value, self.entity, self.entity)?)
227 }
228229/// Sets the highest ledge the boss climbs, in units (`SetMaxJumpHeight`).
230#[doc(alias("SetMaxJumpHeight"))]
231pub fn set_max_jump_height(self, height: f32) -> Result<(), BossError> {
232self.input(c"SetMaxJumpHeight", InputValue::Float(height))
233 }
234235/// Sets whether the boss pushes players out of its way as it moves,
236 /// which it does unless told otherwise (`SetResolvePlayerCollisions`).
237#[doc(alias("SetResolvePlayerCollisions"))]
238pub fn set_resolve_player_collisions(self, resolve: bool) -> Result<(), BossError> {
239 check_live(self.entity)?;
240241// SAFETY: The method only stores the flag
242 // (`game/server/tf/player_vs_environment/tf_base_boss.h`).
243unsafe {
244 binding::call(
245self.entity,
246 raw::BASE_BOSS_CLASS,
247c"SetResolvePlayerCollisions",
248&mut [boolean(resolve)],
249 VOID,
250 )
251 }?;
252253Ok(())
254 }
255256/// Sets the speed the boss's locomotion runs at, in units per second
257 /// (`SetSpeed`).
258#[doc(alias("SetSpeed"))]
259pub fn set_speed(self, speed: f32) -> Result<(), BossError> {
260self.input(c"SetSpeed", InputValue::Float(speed))
261 }
262263/// Sets the highest step the boss walks up, in units (`SetStepHeight`).
264#[doc(alias("SetStepHeight"))]
265pub fn set_step_height(self, height: f32) -> Result<(), BossError> {
266self.input(c"SetStepHeight", InputValue::Float(height))
267 }
268}
269270/// What [`BaseBoss::spawn`] spawns: a `base_boss`'s key values.
271#[derive(#[automatically_derived]
impl<'a> ::core::fmt::Debug for BaseBossSpawn<'a> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ =
&["origin", "model", "health", "speed", "team", "start_disabled"];
let values: &[&dyn ::core::fmt::Debug] =
&[&self.origin, &self.model, &self.health, &self.speed,
&self.team, &&self.start_disabled];
::core::fmt::Formatter::debug_struct_fields_finish(f, "BaseBossSpawn",
names, values)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'a> ::core::clone::TrivialClone for BaseBossSpawn<'a> { }
#[automatically_derived]
impl<'a> ::core::clone::Clone for BaseBossSpawn<'a> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<Vector>;
let _: ::core::clone::AssertParamIsClone<&'a CStr>;
let _: ::core::clone::AssertParamIsClone<c_int>;
let _: ::core::clone::AssertParamIsClone<Option<f32>>;
let _: ::core::clone::AssertParamIsClone<Team>;
let _: ::core::clone::AssertParamIsClone<bool>;
*self
}
}Clone, #[automatically_derived]
impl<'a> ::core::marker::Copy for BaseBossSpawn<'a> { }Copy, #[automatically_derived]
impl<'a> ::core::marker::StructuralPartialEq for BaseBossSpawn<'a> { }
#[automatically_derived]
impl<'a> ::core::cmp::PartialEq for BaseBossSpawn<'a> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.start_disabled == other.start_disabled &&
self.origin == other.origin && self.model == other.model &&
self.health == other.health && self.speed == other.speed &&
self.team == other.team
}
}PartialEq)]
272pub struct BaseBossSpawn<'a> {
273/// Where it spawns.
274pub origin: Vector,
275276/// Its model, such as `models/bots/boss_bot/boss_tank.mdl`, which must
277 /// be precached already (`model`).
278pub model: &'a CStr,
279280/// Its health and maximum health (`health`).
281pub health: c_int,
282283/// The speed its locomotion runs at, in units per second, or `None` for
284 /// `tf_base_boss_speed` (`speed`).
285pub speed: Option<f32>,
286287/// Its team (`teamnumber`).
288pub team: Team,
289290/// Whether it starts disabled, until [`BaseBoss::enable`]
291 /// (`start_disabled`).
292pub start_disabled: bool,
293}
294295impl<'a> BaseBossSpawn<'a> {
296/// A `base_boss` with `model` and `health` at `origin`, on no team, at the
297 /// game's default speed, and enabled.
298pub const fn new(origin: Vector, model: &'a CStr, health: c_int) -> Self {
299Self {
300origin,
301model,
302health,
303 speed: None,
304 team: Team::Unassigned,
305 start_disabled: false,
306 }
307 }
308}
309310/// The boss health bar (`monster_resource`, `CMonsterResource`): the bar
311/// the HUD shows at the top of the screen while it is more than empty, with
312/// a stun meter below it.
313///
314/// It holds its fractions as bytes, so the fractions read back are
315/// multiples of 1/255, rounded down. Monoculus and Merasmus set it each
316/// update while they live, and the game rules hide it at the start of each
317/// round.
318#[doc(alias("monster_resource", "CMonsterResource"))]
319#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for BossBar<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "BossBar",
"server", &self.server, "entity", &&self.entity)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for BossBar<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for BossBar<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<Server<'s>>;
let _: ::core::clone::AssertParamIsClone<Entity<'s>>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for BossBar<'s> { }Copy)]
320pub struct BossBar<'s> {
321 server: Server<'s>,
322 entity: Entity<'s>,
323}
324325impl<'s> BossBar<'s> {
326/// Wraps `entity`. Fails with [`BossError::UnsupportedGame`] outside TF2,
327 /// and with [`BossError::WrongClass`] unless its data maps include
328 /// `CMonsterResource`'s.
329pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BossError> {
330 check_class(server, entity, raw::MONSTER_RESOURCE_CLASS)?;
331332Ok(Self { server, entity })
333 }
334335/// Finds the monster resource, which TF2's game rules create with each
336 /// level, or `None` if there is none.
337pub fn find(server: Server<'s>) -> Result<Option<Self>, BossError> {
338 check_game(server)?;
339340let tools = server.server_tools()?;
341let mut found = tools.find_by_class_name(None, raw::MONSTER_RESOURCE);
342343while let Some(entity) = found {
344if let Ok(bar) = Self::new(server, entity) {
345return Ok(Some(bar));
346 }
347348 found = tools.find_by_class_name(Some(entity), raw::MONSTER_RESOURCE);
349 }
350351Ok(None)
352 }
353354/// The monster resource's entity.
355pub const fn entity(self) -> Entity<'s> {
356self.entity
357 }
358359/// How full the bar is, from 0, which hides it, to 1
360 /// (`m_iBossHealthPercentageByte`).
361#[doc(alias("m_iBossHealthPercentageByte", "GetBossHealthPercentage"))]
362pub fn health(self) -> Result<f32, BossError> {
363self.fraction(c"m_iBossHealthPercentageByte")
364 }
365366/// Empties the bar and the stun meter, which hides them, as the game's
367 /// `HideBossHealthMeter` does.
368#[doc(alias("HideBossHealthMeter"))]
369pub fn hide(self) -> Result<(), BossError> {
370self.set_fraction(c"m_iBossHealthPercentageByte", 0.0)?;
371self.set_fraction(c"m_iBossStunPercentageByte", 0.0)
372 }
373374/// Whether the bar shows in the HUD's inactive colour, as while Merasmus
375 /// hides among props, rather than its boss colour (`m_iBossState`).
376#[doc(alias("m_iBossState"))]
377pub fn is_inactive(self) -> Result<bool, BossError> {
378Ok(self.prop(c"m_iBossState")? != 0)
379 }
380381/// Whether the HUD shows the bar: whether it is more than empty.
382pub fn is_shown(self) -> Result<bool, BossError> {
383Ok(self.prop(c"m_iBossHealthPercentageByte")? > 0)
384 }
385386/// Sets the bar to `fraction` of its length, which shows it unless it
387 /// rounds down to 0, as the game's `SetBossHealthPercentage` does.
388 /// Fractions beyond 0 to 1 are clamped.
389 ///
390 /// Fails with [`BossError::NonFinite`] for NaN or an infinite fraction.
391#[doc(alias("SetBossHealthPercentage", "m_iBossHealthPercentageByte"))]
392pub fn set_health(self, fraction: f32) -> Result<(), BossError> {
393self.set_fraction(c"m_iBossHealthPercentageByte", fraction)
394 }
395396/// Sets whether the bar shows in the HUD's inactive colour, as
397 /// [`is_inactive`](Self::is_inactive) describes.
398#[doc(alias("SetBossState", "m_iBossState"))]
399pub fn set_inactive(self, inactive: bool) -> Result<(), BossError> {
400self.set_prop(c"m_iBossState", c_int::from(inactive))
401 }
402403/// Sets the stun meter to `fraction` of its length, as the game's
404 /// `SetBossStunPercentage` does: clients show it while the bar shows,
405 /// unless they set `cl_boss_show_stun` to 0. Fractions beyond 0 to 1 are
406 /// clamped.
407 ///
408 /// Fails with [`BossError::NonFinite`] for NaN or an infinite fraction.
409#[doc(alias("SetBossStunPercentage", "m_iBossStunPercentageByte"))]
410pub fn set_stun(self, fraction: f32) -> Result<(), BossError> {
411self.set_fraction(c"m_iBossStunPercentageByte", fraction)
412 }
413414/// How full the stun meter is, from 0, which hides it, to 1
415 /// (`m_iBossStunPercentageByte`).
416#[doc(alias("m_iBossStunPercentageByte", "GetBossStunPercentage"))]
417pub fn stun(self) -> Result<f32, BossError> {
418self.fraction(c"m_iBossStunPercentageByte")
419 }
420421/// Reads a byte variable as a fraction of [`FULL_BAR`].
422fn fraction(self, name: &CStr) -> Result<f32, BossError> {
423Ok(self.prop(name)?.clamp(0, FULL_BAR) as f32 / FULL_BARas f32)
424 }
425426/// Reads the networked `int` `name`.
427fn prop(self, name: &CStr) -> Result<c_int, BossError> {
428 check_live(self.entity)?;
429430let prop = self
431.server
432 .server_game_dll()?
433.entity_net_prop(self.entity, name)?;
434435Ok(prop.get::<c_int>(self.entity)?)
436 }
437438/// Writes `fraction` of [`FULL_BAR`] to a byte variable, rounding down as
439 /// the game does.
440fn set_fraction(self, name: &CStr, fraction: f32) -> Result<(), BossError> {
441if !fraction.is_finite() {
442return Err(BossError::NonFinite);
443 }
444445self.set_prop(name, (fraction.clamp(0.0, 1.0) * FULL_BARas f32) as c_int)
446 }
447448/// Writes the networked `int` `name`.
449fn set_prop(self, name: &CStr, value: c_int) -> Result<(), BossError> {
450 check_live(self.entity)?;
451452let prop = self
453.server
454 .server_game_dll()?
455.entity_net_prop(self.entity, name)?;
456457// SAFETY: The callers write a byte of 0 to 255, or a state of 0 or 1,
458 // as the game's own setters do; clients only draw them.
459Ok(unsafe { prop.set(self.server.valve_engine()?, self.entity, value) }?)
460 }
461}
462463/// The team [`spawn_halloween_boss`] and [`spawn_skeleton`] put a boss or
464/// skeleton on.
465#[derive(#[automatically_derived]
impl ::core::fmt::Debug for BossTeam {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
BossTeam::Red => "Red",
BossTeam::Blue => "Blue",
BossTeam::Halloween => "Halloween",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for BossTeam { }
#[automatically_derived]
impl ::core::clone::Clone for BossTeam {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for BossTeam { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for BossTeam { }
#[automatically_derived]
impl ::core::cmp::PartialEq for BossTeam {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for BossTeam { }Eq, #[automatically_derived]
impl ::core::hash::Hash for BossTeam {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
state)
}
}Hash)]
466pub enum BossTeam {
467/// RED, as the Halloween spells summon them for RED's casters.
468#[doc(alias("TF_TEAM_RED"))]
469Red,
470471/// BLU, as for RED.
472#[doc(alias("TF_TEAM_BLUE"))]
473Blue,
474475/// The Halloween team, neither RED nor BLU, which the game spawns its own
476 /// Halloween bosses and skeletons on (`TF_TEAM_HALLOWEEN`).
477#[doc(alias("TF_TEAM_HALLOWEEN"))]
478Halloween,
479}
480481impl BossTeam {
482/// The team numbered `raw`, or `None` for any other number.
483pub const fn from_raw(raw: c_int) -> Option<Self> {
484match raw {
485TF_TEAM_RED => Some(Self::Red),
486TF_TEAM_BLUE => Some(Self::Blue),
487 raw::TF_TEAM_HALLOWEEN => Some(Self::Halloween),
488_ => None,
489 }
490 }
491492/// The team's number, as `m_iTeamNum` holds it.
493pub const fn to_raw(self) -> c_int {
494match self {
495Self::Red => TF_TEAM_RED,
496Self::Blue => TF_TEAM_BLUE,
497Self::Halloween => raw::TF_TEAM_HALLOWEEN,
498 }
499 }
500}
501502/// One of TF2's Halloween bosses, which [`spawn_halloween_boss`] spawns.
503#[derive(#[automatically_derived]
impl ::core::fmt::Debug for HalloweenBoss {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
HalloweenBoss::Horsemann => "Horsemann",
HalloweenBoss::Monoculus => "Monoculus",
HalloweenBoss::Merasmus => "Merasmus",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for HalloweenBoss { }
#[automatically_derived]
impl ::core::clone::Clone for HalloweenBoss {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for HalloweenBoss { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for HalloweenBoss { }
#[automatically_derived]
impl ::core::cmp::PartialEq for HalloweenBoss {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for HalloweenBoss { }Eq, #[automatically_derived]
impl ::core::hash::Hash for HalloweenBoss {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
state)
}
}Hash)]
504pub enum HalloweenBoss {
505/// The Horseless Headless Horsemann (`headless_hatman`), who hunts one
506 /// player at a time with his axe.
507#[doc(alias("headless_hatman", "CHeadlessHatman", "HHH"))]
508Horsemann,
509510/// Monoculus (`eyeball_boss`), who fires rockets from the air and
511 /// teleports between the `info_target`s named `spawn_boss_alt`, or stays
512 /// where it is on a level without them.
513#[doc(alias("eyeball_boss", "CEyeballBoss"))]
514Monoculus,
515516/// Merasmus (`merasmus`), who throws bombs and hides among props on the
517 /// level's navigation mesh, and leaves after `tf_merasmus_lifetime`
518 /// seconds.
519#[doc(alias("merasmus", "CMerasmus"))]
520Merasmus,
521}
522523impl HalloweenBoss {
524/// Every Halloween boss.
525pub const ALL: [Self; 3] = [Self::Horsemann, Self::Monoculus, Self::Merasmus];
526527/// The boss's class name.
528pub const fn class_name(self) -> &'static CStr {
529match self {
530Self::Horsemann => raw::HEADLESS_HATMAN,
531Self::Monoculus => raw::EYEBALL_BOSS,
532Self::Merasmus => raw::MERASMUS,
533 }
534 }
535}
536537/// A `tf_zombie_spawner` (`CZombieSpawner`) within the current engine
538/// callback: the map entity that spawns skeletons at its position while it
539/// is enabled, one every 1.5 to 3 seconds.
540///
541/// With infinite skeletons, it keeps up to its maximum alive at a time;
542/// otherwise it spawns up to its maximum in all, and spawns them again once
543/// disabled and enabled. Its skeletons take its team if it is RED or BLU,
544/// and the Halloween team otherwise.
545#[doc(alias("tf_zombie_spawner", "CZombieSpawner"))]
546#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for SkeletonSpawner<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f,
"SkeletonSpawner", "server", &self.server, "entity",
&&self.entity)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for SkeletonSpawner<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for SkeletonSpawner<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<Server<'s>>;
let _: ::core::clone::AssertParamIsClone<Entity<'s>>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for SkeletonSpawner<'s> { }Copy)]
547pub struct SkeletonSpawner<'s> {
548 server: Server<'s>,
549 entity: Entity<'s>,
550}
551552impl<'s> SkeletonSpawner<'s> {
553/// Wraps `entity`. Fails with [`BossError::UnsupportedGame`] outside TF2,
554 /// and with [`BossError::WrongClass`] unless its data maps include
555 /// `CZombieSpawner`'s.
556pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BossError> {
557 check_class(server, entity, raw::TF_ZOMBIE_SPAWNER_CLASS)?;
558559Ok(Self { server, entity })
560 }
561562/// Spawns a disabled spawner at `origin`, as `spawn` describes. Enable
563 /// it to spawn skeletons.
564 ///
565 /// Fails, before creating it, with [`BossError::NonFinite`] for a position
566 /// or lifetime that is not finite.
567pub fn spawn(
568 server: Server<'s>,
569 origin: Vector,
570 spawn: &SkeletonSpawn,
571 ) -> Result<Self, BossError> {
572 check_game(server)?;
573574if !origin.is_finite() || !spawn.lifetime.is_none_or(f32::is_finite) {
575return Err(BossError::NonFinite);
576 }
577578// The spawner gives its skeletons the Halloween team for any team
579 // other than RED and BLU.
580let team = match spawn.team {
581 BossTeam::Red => TF_TEAM_RED,
582 BossTeam::Blue => TF_TEAM_BLUE,
583 BossTeam::Halloween => TEAM_UNASSIGNED,
584 };
585586let spawner = EntitySpawn::new(raw::TF_ZOMBIE_SPAWNER)
587 .origin(origin)
588 .key(c"teamnumber", &number_value(team))
589 .key(c"zombie_type", &number_value(spawn.kind.to_raw()))
590 .key(c"max_zombies", &number_value(spawn.count))
591 .key(c"infinite_zombies", flag_value(spawn.infinite))
592 .key(
593c"zombie_lifetime",
594&number_value(spawn.lifetime.unwrap_or(0.0)),
595 );
596597// SAFETY: `CZombieSpawner`'s constructor only sets its members
598 // (`game/server/tf/halloween/zombie/zombie_spawner.cpp`), its `Spawn`
599 // only starts it thinking, and its `Activate` is `CBaseEntity`'s. It
600 // spawns skeletons from its thinks, while enabled.
601let entity = unsafe { spawner.spawn(server.server_tools()?) }?;
602603Ok(Self { server, entity })
604 }
605606/// Stops the spawner, and forgets the skeletons it spawned, which live on
607 /// (`Disable`).
608#[doc(alias("Disable"))]
609pub fn disable(self) -> Result<(), BossError> {
610self.input(c"Disable", InputValue::Void)
611 }
612613/// Starts the spawner (`Enable`).
614#[doc(alias("Enable"))]
615pub fn enable(self) -> Result<(), BossError> {
616self.input(c"Enable", InputValue::Void)
617 }
618619/// The spawner's entity.
620pub const fn entity(self) -> Entity<'s> {
621self.entity
622 }
623624/// Sends the spawner an input, with itself as the activator and caller.
625fn input(self, name: &CStr, value: InputValue<'_>) -> Result<(), BossError> {
626let tools = self.server.server_tools()?;
627628Ok(tools.accept_input(self.entity, name, value, self.entity, self.entity)?)
629 }
630631/// Sets how many skeletons the spawner keeps alive, or spawns in all
632 /// (`SetMaxActiveZombies`).
633#[doc(alias("SetMaxActiveZombies", "max_zombies"))]
634pub fn set_count(self, count: c_int) -> Result<(), BossError> {
635self.input(c"SetMaxActiveZombies", InputValue::Int(count))
636 }
637}
638639/// What a [`SkeletonSpawner`] spawns.
640#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SkeletonSpawn {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f, "SkeletonSpawn",
"kind", &self.kind, "team", &self.team, "count", &self.count,
"infinite", &self.infinite, "lifetime", &&self.lifetime)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for SkeletonSpawn { }
#[automatically_derived]
impl ::core::clone::Clone for SkeletonSpawn {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<SkeletonType>;
let _: ::core::clone::AssertParamIsClone<BossTeam>;
let _: ::core::clone::AssertParamIsClone<c_int>;
let _: ::core::clone::AssertParamIsClone<bool>;
let _: ::core::clone::AssertParamIsClone<Option<f32>>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for SkeletonSpawn { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for SkeletonSpawn { }
#[automatically_derived]
impl ::core::cmp::PartialEq for SkeletonSpawn {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.infinite == other.infinite && self.kind == other.kind &&
self.team == other.team && self.count == other.count &&
self.lifetime == other.lifetime
}
}PartialEq)]
641pub struct SkeletonSpawn {
642/// The skeletons' type (`zombie_type`).
643pub kind: SkeletonType,
644645/// The skeletons' team.
646pub team: BossTeam,
647648/// How many skeletons it keeps alive, or spawns in all, as
649 /// [`infinite`](Self::infinite) decides (`max_zombies`).
650pub count: c_int,
651652/// Whether it keeps spawning skeletons as they die, rather than stopping
653 /// after [`count`](Self::count) (`infinite_zombies`).
654pub infinite: bool,
655656/// How long each skeleton lives, in seconds, or `None` for no limit
657 /// (`zombie_lifetime`).
658pub lifetime: Option<f32>,
659}
660661impl SkeletonSpawn {
662/// One skeleton of `kind` on the Halloween team, with no lifetime.
663pub const fn new(kind: SkeletonType) -> Self {
664Self {
665kind,
666 team: BossTeam::Halloween,
667 count: 1,
668 infinite: false,
669 lifetime: None,
670 }
671 }
672}
673674/// A skeleton's type (`CZombie::SkeletonType_t`).
675#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SkeletonType {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
SkeletonType::Normal => "Normal",
SkeletonType::King => "King",
SkeletonType::Mini => "Mini",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for SkeletonType { }
#[automatically_derived]
impl ::core::clone::Clone for SkeletonType {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for SkeletonType { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for SkeletonType { }
#[automatically_derived]
impl ::core::cmp::PartialEq for SkeletonType {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SkeletonType { }Eq, #[automatically_derived]
impl ::core::hash::Hash for SkeletonType {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
state)
}
}Hash)]
676pub enum SkeletonType {
677/// An ordinary skeleton, with 50 health (`SKELETON_NORMAL`).
678#[doc(alias("SKELETON_NORMAL"))]
679Normal,
680681/// A skeleton king, twice the size, with 1,000 health and a crown, which
682 /// the game never kills to keep skeletons under `tf_max_active_zombie`
683 /// (`SKELETON_KING`).
684#[doc(alias("SKELETON_KING"))]
685King,
686687/// A small skeleton (`SKELETON_MINI`).
688#[doc(alias("SKELETON_MINI"))]
689Mini,
690}
691692impl SkeletonType {
693/// The type numbered `raw`, or `None` for any other number.
694pub const fn from_raw(raw: c_int) -> Option<Self> {
695match raw {
696 skeleton::SKELETON_NORMAL => Some(Self::Normal),
697 skeleton::SKELETON_KING => Some(Self::King),
698 skeleton::SKELETON_MINI => Some(Self::Mini),
699_ => None,
700 }
701 }
702703/// The type's number.
704pub const fn to_raw(self) -> c_int {
705match self {
706Self::Normal => skeleton::SKELETON_NORMAL,
707Self::King => skeleton::SKELETON_KING,
708Self::Mini => skeleton::SKELETON_MINI,
709 }
710 }
711}
712713/// Spawns a Halloween boss at `origin` on `team`, as the game's
714/// `CHalloweenBaseBoss::SpawnBossAtPos` does, which spawns them 10 units
715/// above the ground it picks.
716///
717/// The boss computes its health as it spawns, from the players on RED and
718/// BLU and its `tf_halloween_bot_health_*`, `tf_eyeball_boss_health_*` or
719/// `tf_merasmus_health_*` convars. `Some(health)` then sets its health and
720/// maximum health to that.
721///
722/// A boss on RED or BLU counts as one a spell summoned: Monoculus then shows
723/// no health bar. On a level the game did not precache the boss for, as it
724/// does on the Halloween levels each boss belongs to, the boss precaches its
725/// models and sounds as it spawns, which needs room in the engine's model
726/// table, as
727/// [`MODEL_PRECACHE`](crate::interfaces::network_string_tables::MODEL_PRECACHE)
728/// describes.
729///
730/// Fails, before creating it, with [`BossError::NonFinite`] for a position
731/// that is not finite, and [`BossError::NonPositiveHealth`] for a health of
732/// 0 or less.
733pub fn spawn_halloween_boss<'s>(
734 server: Server<'s>,
735 boss: HalloweenBoss,
736 origin: Vector,
737 team: BossTeam,
738 health: Option<c_int>,
739) -> Result<Entity<'s>, BossError> {
740 check_game(server)?;
741742if !origin.is_finite() {
743return Err(BossError::NonFinite);
744 }
745746if let Some(health) = health747 && health <= 0
748{
749return Err(BossError::NonPositiveHealth(health));
750 }
751752// SAFETY: The bosses' constructors only set their members, make their
753 // NextBot components, and, for Merasmus, listen for game events
754 // (`game/server/tf/halloween`). The game puts its own bosses on any of
755 // these teams before they spawn, through the same method. Their `Spawn`s
756 // precache what they use, set their models and health, create the
757 // Horsemann's axe as a prop, find the entities they work with by name, add
758 // them to the game rules' bosses, and fire `recalculate_truce` or
759 // `merasmus_summoned`, whose listeners `Server::new` condition 4 covers.
760let entity = unsafe { spawn_on_team(server.server_tools()?, boss.class_name(), origin, team) }?;
761762if let Some(health) = health {
763 entity.set_max_health(server, health)?;
764 entity.set_health(server, health)?;
765 }
766767Ok(entity)
768}
769770/// Spawns an ordinary skeleton at `origin` on `team`, as the game's
771/// `CZombie::SpawnAtPos` does, with no lifetime. A [`SkeletonSpawner`]
772/// spawns the other types, and skeletons with a lifetime.
773///
774/// Once more skeletons live than `tf_max_active_zombie` allows, the oldest,
775/// other than skeleton kings, die on their next update. On a level without
776/// skeletons of its own, the skeleton precaches its models as it spawns, as
777/// [`spawn_halloween_boss`] describes for the bosses.
778///
779/// Fails, before creating it, with [`BossError::NonFinite`] for a position
780/// that is not finite.
781#[doc(alias("tf_zombie", "CZombie"))]
782pub fn spawn_skeleton<'s>(
783 server: Server<'s>,
784 origin: Vector,
785 team: BossTeam,
786) -> Result<Entity<'s>, BossError> {
787 check_game(server)?;
788789if !origin.is_finite() {
790return Err(BossError::NonFinite);
791 }
792793// SAFETY: `CZombie`'s constructor only sets its members and makes its
794 // NextBot components (`game/server/tf/halloween/zombie/zombie.cpp`). As
795 // for the bosses, the game puts skeletons on any of these teams before
796 // they spawn. Its `Spawn` precaches what it uses, sets its model, health
797 // and skin, and marks the oldest skeletons beyond `tf_max_active_zombie`
798 // to kill themselves on their next update.
799unsafe { spawn_on_team(server.server_tools()?, raw::TF_ZOMBIE, origin, team) }
800}
801802/// Fails with [`BossError::UnsupportedGame`] outside TF2, and with
803/// [`BossError::WrongClass`] unless `entity`'s data maps include `class`'s.
804fn check_class(
805 server: Server<'_>,
806 entity: Entity<'_>,
807 class: &'static CStr,
808) -> Result<(), BossError> {
809 check_game(server)?;
810811if entity.has_data_map_class(class) {
812Ok(())
813 } else {
814Err(BossError::WrongClass { class })
815 }
816}
817818/// Fails with [`BossError::UnsupportedGame`] outside TF2.
819fn check_game(server: Server<'_>) -> Result<(), BossError> {
820if server.game() == Game::TeamFortress2 {
821Ok(())
822 } else {
823Err(BossError::UnsupportedGame)
824 }
825}
826827/// Fails with [`BossError::MarkedForDeletion`] for an entity marked for
828/// deletion.
829fn check_live(entity: Entity<'_>) -> Result<(), BossError> {
830if entity.is_marked_for_deletion() {
831Err(BossError::MarkedForDeletion)
832 } else {
833Ok(())
834 }
835}
836837/// Puts an entity that has not spawned on `team`, through its
838/// `CBaseEntity::ChangeTeam`.
839///
840/// # Safety
841///
842/// The entity's class must accept the team, as the game's own spawning of it
843/// shows.
844unsafe fn change_team(entity: Entity<'_>, team: BossTeam) {
845let raw = entity.as_ptr();
846847// SAFETY: The entity is live during the callback, on the main thread, and
848 // its class belongs to TF2's game DLL, whose vtables have `ChangeTeam`
849 // where the generated one does (`sdk_raw::tf2::teams`). The classes here
850 // only store the team, and leave a nav area they are not in yet.
851unsafe {
852{
let this = raw;
let vtable =
::source_sdk_2013_raw::util::vtable::vtable_pointer::<sys::CBaseEntity__bindgen_vtable>(this);
((*vtable).CBaseEntity_ChangeTeam)(this.cast(), team.to_raw())
}vcall!(raw as sys::CBaseEntity__bindgen_vtable => CBaseEntity_ChangeTeam(team.to_raw()))853 };
854}
855856/// Creates an entity of `class` at `origin` and puts it on `team` before
857/// spawning it, as the game spawns its own bosses and skeletons, which
858/// [`EntitySpawn`] cannot. It is not activated, as the game's are not.
859///
860/// # Safety
861///
862/// The class's constructor and `Spawn` must free entities only through
863/// Source's deferred deletion, as [`ServerTools::create_entity_by_name`] and
864/// [`ServerTools::dispatch_spawn`] require, and it must accept the team, as
865/// [`change_team`] requires.
866unsafe fn spawn_on_team<'s>(
867 tools: ServerTools<'s>,
868 class: &CStr,
869 origin: Vector,
870 team: BossTeam,
871) -> Result<Entity<'s>, BossError> {
872// SAFETY: As the caller promises.
873let entity =
874unsafe { tools.create_entity_by_name(class) }.ok_or_else(|| SpawnError::UnknownClass {
875 class: class.to_owned(),
876 })?;
877878if !tools.set_key_value(entity, c"origin", &vector_value(origin)) {
879// No protected entity is new and unspawned.
880let _ = tools.remove(entity);
881882return Err(SpawnError::KeyRejected {
883 key: c"origin".to_owned(),
884 }
885 .into());
886 }
887888// SAFETY: The entity has not spawned, and the caller vouches for its
889 // class.
890unsafe { change_team(entity, team) };
891892// SAFETY: The entity is new, and the caller vouches for its `Spawn`.
893unsafe { tools.dispatch_spawn(entity) };
894895if entity.is_marked_for_deletion() {
896return Err(SpawnError::RemovedItself.into());
897 }
898899Ok(entity)
900}
901902/// `1` or `0`, as boolean key values are given.
903fn flag_value(flag: bool) -> &'static CStr {
904if flag { c"1" } else { c"0" }
905}
906907/// A number as a key value, in decimal.
908fn number_value(number: impl Display) -> CString {
909CString::new(number.to_string()).expect("numbers contain no NUL")
910}
911912/// A vector as a key value: its three components, in decimal.
913fn vector_value(vector: Vector) -> CString {
914let value = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} {1} {2}", vector.x, vector.y,
vector.z))
})format!("{} {} {}", vector.x, vector.y, vector.z);
915916CString::new(value).expect("numbers contain no NUL")
917}