Skip to main content

source_sdk_2013/tf2/
bosses.rs

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.
34
35#[cfg(test)]
36#[path = "../tests/tf2/bosses.rs"]
37mod tests;
38
39use 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::{self as binding, BindingError, VOID};
47use crate::tf2::teams::Team;
48use crate::{Game, InterfaceError, Server};
49use sdk_raw::players::TEAM_UNASSIGNED;
50use sdk_raw::tf2::bosses::{self as 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;
56
57/// The most a boss bar shows: the full bar, as a byte of the monster
58/// resource.
59const FULL_BAR: c_int = 255;
60
61/// 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")]
66	UnsupportedGame,
67
68	/// The entity is not of the class the wrapper takes.
69	#[error("the entity is not a {class:?}")]
70	WrongClass {
71		/// The data map class the wrapper takes.
72		class: &'static CStr,
73	},
74
75	/// The entity is marked for deletion.
76	#[error("the entity is marked for deletion")]
77	MarkedForDeletion,
78
79	/// A position, speed, height, lifetime or fraction given is NaN or
80	/// infinite.
81	#[error("a value given is NaN or infinite")]
82	NonFinite,
83
84	/// A health given is 0 or less, which game code divides by.
85	#[error("health {0} is not positive")]
86	NonPositiveHealth(c_int),
87
88	/// 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")]
93	ModelNotPrecached,
94
95	/// 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")]
98	UnsupportedMethod,
99
100	/// A native method's binding adapter reported failure.
101	#[error("the native method rejected the call")]
102	Rejected,
103
104	/// The new boss's health could not be set.
105	#[error(transparent)]
106	Health(#[from] HealthError),
107
108	/// An input was not sent, or the entity rejected it.
109	#[error(transparent)]
110	Input(#[from] InputError),
111
112	/// A required engine interface is unavailable.
113	#[error(transparent)]
114	Interface(#[from] InterfaceError),
115
116	/// A networked variable could not be read or written.
117	#[error(transparent)]
118	NetProp(#[from] NetPropError),
119
120	/// 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)]
123	Spawn(#[from] SpawnError),
124}
125
126impl From<BindingError> for BossError {
127	fn from(error: BindingError) -> Self {
128		match error {
129			BindingError::Unavailable | BindingError::SignatureMismatch => Self::UnsupportedMethod,
130			BindingError::Rejected => Self::Rejected,
131		}
132	}
133}
134
135/// 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}
151
152impl<'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.
156	pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BossError> {
157		check_class(server, entity, raw::BASE_BOSS_CLASS)?;
158
159		Ok(Self { server, entity })
160	}
161
162	/// 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.
168	pub fn spawn(server: Server<'s>, spawn: &BaseBossSpawn<'_>) -> Result<Self, BossError> {
169		check_game(server)?;
170
171		if !spawn.origin.is_finite() || !spawn.speed.is_none_or(f32::is_finite) {
172			return Err(BossError::NonFinite);
173		}
174
175		if spawn.health <= 0 {
176			return Err(BossError::NonPositiveHealth(spawn.health));
177		}
178
179		if server.model_info()?.model_index(spawn.model).is_none() {
180			return Err(BossError::ModelNotPrecached);
181		}
182
183		let 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));
189
190		if let Some(speed) = spawn.speed {
191			boss = boss.key(c"speed", &number_value(speed));
192		}
193
194		// 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.
200		let entity = unsafe { boss.spawn(server.server_tools()?) }?;
201
202		Ok(Self { server, entity })
203	}
204
205	/// Stops the boss: it neither updates nor pushes players (`Disable`).
206	#[doc(alias("Disable"))]
207	pub fn disable(self) -> Result<(), BossError> {
208		self.input(c"Disable", InputValue::Void)
209	}
210
211	/// Lets the boss update and push players again (`Enable`).
212	#[doc(alias("Enable"))]
213	pub fn enable(self) -> Result<(), BossError> {
214		self.input(c"Enable", InputValue::Void)
215	}
216
217	/// The boss's entity.
218	pub const fn entity(self) -> Entity<'s> {
219		self.entity
220	}
221
222	/// Sends the boss an input, with itself as the activator and caller.
223	fn input(self, name: &CStr, value: InputValue<'_>) -> Result<(), BossError> {
224		let tools = self.server.server_tools()?;
225
226		Ok(tools.accept_input(self.entity, name, value, self.entity, self.entity)?)
227	}
228
229	/// Sets the highest ledge the boss climbs, in units (`SetMaxJumpHeight`).
230	#[doc(alias("SetMaxJumpHeight"))]
231	pub fn set_max_jump_height(self, height: f32) -> Result<(), BossError> {
232		self.input(c"SetMaxJumpHeight", InputValue::Float(height))
233	}
234
235	/// 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"))]
238	pub fn set_resolve_player_collisions(self, resolve: bool) -> Result<(), BossError> {
239		check_live(self.entity)?;
240
241		// SAFETY: The method only stores the flag
242		// (`game/server/tf/player_vs_environment/tf_base_boss.h`).
243		unsafe {
244			binding::call(
245				self.entity,
246				raw::BASE_BOSS_CLASS,
247				c"SetResolvePlayerCollisions",
248				&mut [boolean(resolve)],
249				VOID,
250			)
251		}?;
252
253		Ok(())
254	}
255
256	/// Sets the speed the boss's locomotion runs at, in units per second
257	/// (`SetSpeed`).
258	#[doc(alias("SetSpeed"))]
259	pub fn set_speed(self, speed: f32) -> Result<(), BossError> {
260		self.input(c"SetSpeed", InputValue::Float(speed))
261	}
262
263	/// Sets the highest step the boss walks up, in units (`SetStepHeight`).
264	#[doc(alias("SetStepHeight"))]
265	pub fn set_step_height(self, height: f32) -> Result<(), BossError> {
266		self.input(c"SetStepHeight", InputValue::Float(height))
267	}
268}
269
270/// 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.
274	pub origin: Vector,
275
276	/// Its model, such as `models/bots/boss_bot/boss_tank.mdl`, which must
277	/// be precached already (`model`).
278	pub model: &'a CStr,
279
280	/// Its health and maximum health (`health`).
281	pub health: c_int,
282
283	/// The speed its locomotion runs at, in units per second, or `None` for
284	/// `tf_base_boss_speed` (`speed`).
285	pub speed: Option<f32>,
286
287	/// Its team (`teamnumber`).
288	pub team: Team,
289
290	/// Whether it starts disabled, until [`BaseBoss::enable`]
291	/// (`start_disabled`).
292	pub start_disabled: bool,
293}
294
295impl<'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.
298	pub const fn new(origin: Vector, model: &'a CStr, health: c_int) -> Self {
299		Self {
300			origin,
301			model,
302			health,
303			speed: None,
304			team: Team::Unassigned,
305			start_disabled: false,
306		}
307	}
308}
309
310/// 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}
324
325impl<'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.
329	pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BossError> {
330		check_class(server, entity, raw::MONSTER_RESOURCE_CLASS)?;
331
332		Ok(Self { server, entity })
333	}
334
335	/// Finds the monster resource, which TF2's game rules create with each
336	/// level, or `None` if there is none.
337	pub fn find(server: Server<'s>) -> Result<Option<Self>, BossError> {
338		check_game(server)?;
339
340		let tools = server.server_tools()?;
341		let mut found = tools.find_by_class_name(None, raw::MONSTER_RESOURCE);
342
343		while let Some(entity) = found {
344			if let Ok(bar) = Self::new(server, entity) {
345				return Ok(Some(bar));
346			}
347
348			found = tools.find_by_class_name(Some(entity), raw::MONSTER_RESOURCE);
349		}
350
351		Ok(None)
352	}
353
354	/// The monster resource's entity.
355	pub const fn entity(self) -> Entity<'s> {
356		self.entity
357	}
358
359	/// How full the bar is, from 0, which hides it, to 1
360	/// (`m_iBossHealthPercentageByte`).
361	#[doc(alias("m_iBossHealthPercentageByte", "GetBossHealthPercentage"))]
362	pub fn health(self) -> Result<f32, BossError> {
363		self.fraction(c"m_iBossHealthPercentageByte")
364	}
365
366	/// Empties the bar and the stun meter, which hides them, as the game's
367	/// `HideBossHealthMeter` does.
368	#[doc(alias("HideBossHealthMeter"))]
369	pub fn hide(self) -> Result<(), BossError> {
370		self.set_fraction(c"m_iBossHealthPercentageByte", 0.0)?;
371		self.set_fraction(c"m_iBossStunPercentageByte", 0.0)
372	}
373
374	/// 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"))]
377	pub fn is_inactive(self) -> Result<bool, BossError> {
378		Ok(self.prop(c"m_iBossState")? != 0)
379	}
380
381	/// Whether the HUD shows the bar: whether it is more than empty.
382	pub fn is_shown(self) -> Result<bool, BossError> {
383		Ok(self.prop(c"m_iBossHealthPercentageByte")? > 0)
384	}
385
386	/// 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"))]
392	pub fn set_health(self, fraction: f32) -> Result<(), BossError> {
393		self.set_fraction(c"m_iBossHealthPercentageByte", fraction)
394	}
395
396	/// 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"))]
399	pub fn set_inactive(self, inactive: bool) -> Result<(), BossError> {
400		self.set_prop(c"m_iBossState", c_int::from(inactive))
401	}
402
403	/// 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"))]
410	pub fn set_stun(self, fraction: f32) -> Result<(), BossError> {
411		self.set_fraction(c"m_iBossStunPercentageByte", fraction)
412	}
413
414	/// How full the stun meter is, from 0, which hides it, to 1
415	/// (`m_iBossStunPercentageByte`).
416	#[doc(alias("m_iBossStunPercentageByte", "GetBossStunPercentage"))]
417	pub fn stun(self) -> Result<f32, BossError> {
418		self.fraction(c"m_iBossStunPercentageByte")
419	}
420
421	/// Reads a byte variable as a fraction of [`FULL_BAR`].
422	fn fraction(self, name: &CStr) -> Result<f32, BossError> {
423		Ok(self.prop(name)?.clamp(0, FULL_BAR) as f32 / FULL_BAR as f32)
424	}
425
426	/// Reads the networked `int` `name`.
427	fn prop(self, name: &CStr) -> Result<c_int, BossError> {
428		check_live(self.entity)?;
429
430		let prop = self
431			.server
432			.server_game_dll()?
433			.entity_net_prop(self.entity, name)?;
434
435		Ok(prop.get::<c_int>(self.entity)?)
436	}
437
438	/// Writes `fraction` of [`FULL_BAR`] to a byte variable, rounding down as
439	/// the game does.
440	fn set_fraction(self, name: &CStr, fraction: f32) -> Result<(), BossError> {
441		if !fraction.is_finite() {
442			return Err(BossError::NonFinite);
443		}
444
445		self.set_prop(name, (fraction.clamp(0.0, 1.0) * FULL_BAR as f32) as c_int)
446	}
447
448	/// Writes the networked `int` `name`.
449	fn set_prop(self, name: &CStr, value: c_int) -> Result<(), BossError> {
450		check_live(self.entity)?;
451
452		let prop = self
453			.server
454			.server_game_dll()?
455			.entity_net_prop(self.entity, name)?;
456
457		// 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.
459		Ok(unsafe { prop.set(self.server.valve_engine()?, self.entity, value) }?)
460	}
461}
462
463/// 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"))]
469	Red,
470
471	/// BLU, as for RED.
472	#[doc(alias("TF_TEAM_BLUE"))]
473	Blue,
474
475	/// 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"))]
478	Halloween,
479}
480
481impl BossTeam {
482	/// The team numbered `raw`, or `None` for any other number.
483	pub const fn from_raw(raw: c_int) -> Option<Self> {
484		match raw {
485			TF_TEAM_RED => Some(Self::Red),
486			TF_TEAM_BLUE => Some(Self::Blue),
487			raw::TF_TEAM_HALLOWEEN => Some(Self::Halloween),
488			_ => None,
489		}
490	}
491
492	/// The team's number, as `m_iTeamNum` holds it.
493	pub const fn to_raw(self) -> c_int {
494		match self {
495			Self::Red => TF_TEAM_RED,
496			Self::Blue => TF_TEAM_BLUE,
497			Self::Halloween => raw::TF_TEAM_HALLOWEEN,
498		}
499	}
500}
501
502/// 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"))]
508	Horsemann,
509
510	/// 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"))]
514	Monoculus,
515
516	/// 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"))]
520	Merasmus,
521}
522
523impl HalloweenBoss {
524	/// Every Halloween boss.
525	pub const ALL: [Self; 3] = [Self::Horsemann, Self::Monoculus, Self::Merasmus];
526
527	/// The boss's class name.
528	pub const fn class_name(self) -> &'static CStr {
529		match self {
530			Self::Horsemann => raw::HEADLESS_HATMAN,
531			Self::Monoculus => raw::EYEBALL_BOSS,
532			Self::Merasmus => raw::MERASMUS,
533		}
534	}
535}
536
537/// 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}
551
552impl<'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.
556	pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, BossError> {
557		check_class(server, entity, raw::TF_ZOMBIE_SPAWNER_CLASS)?;
558
559		Ok(Self { server, entity })
560	}
561
562	/// 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.
567	pub fn spawn(
568		server: Server<'s>,
569		origin: Vector,
570		spawn: &SkeletonSpawn,
571	) -> Result<Self, BossError> {
572		check_game(server)?;
573
574		if !origin.is_finite() || !spawn.lifetime.is_none_or(f32::is_finite) {
575			return Err(BossError::NonFinite);
576		}
577
578		// The spawner gives its skeletons the Halloween team for any team
579		// other than RED and BLU.
580		let team = match spawn.team {
581			BossTeam::Red => TF_TEAM_RED,
582			BossTeam::Blue => TF_TEAM_BLUE,
583			BossTeam::Halloween => TEAM_UNASSIGNED,
584		};
585
586		let 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(
593				c"zombie_lifetime",
594				&number_value(spawn.lifetime.unwrap_or(0.0)),
595			);
596
597		// 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.
601		let entity = unsafe { spawner.spawn(server.server_tools()?) }?;
602
603		Ok(Self { server, entity })
604	}
605
606	/// Stops the spawner, and forgets the skeletons it spawned, which live on
607	/// (`Disable`).
608	#[doc(alias("Disable"))]
609	pub fn disable(self) -> Result<(), BossError> {
610		self.input(c"Disable", InputValue::Void)
611	}
612
613	/// Starts the spawner (`Enable`).
614	#[doc(alias("Enable"))]
615	pub fn enable(self) -> Result<(), BossError> {
616		self.input(c"Enable", InputValue::Void)
617	}
618
619	/// The spawner's entity.
620	pub const fn entity(self) -> Entity<'s> {
621		self.entity
622	}
623
624	/// Sends the spawner an input, with itself as the activator and caller.
625	fn input(self, name: &CStr, value: InputValue<'_>) -> Result<(), BossError> {
626		let tools = self.server.server_tools()?;
627
628		Ok(tools.accept_input(self.entity, name, value, self.entity, self.entity)?)
629	}
630
631	/// Sets how many skeletons the spawner keeps alive, or spawns in all
632	/// (`SetMaxActiveZombies`).
633	#[doc(alias("SetMaxActiveZombies", "max_zombies"))]
634	pub fn set_count(self, count: c_int) -> Result<(), BossError> {
635		self.input(c"SetMaxActiveZombies", InputValue::Int(count))
636	}
637}
638
639/// 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`).
643	pub kind: SkeletonType,
644
645	/// The skeletons' team.
646	pub team: BossTeam,
647
648	/// How many skeletons it keeps alive, or spawns in all, as
649	/// [`infinite`](Self::infinite) decides (`max_zombies`).
650	pub count: c_int,
651
652	/// Whether it keeps spawning skeletons as they die, rather than stopping
653	/// after [`count`](Self::count) (`infinite_zombies`).
654	pub infinite: bool,
655
656	/// How long each skeleton lives, in seconds, or `None` for no limit
657	/// (`zombie_lifetime`).
658	pub lifetime: Option<f32>,
659}
660
661impl SkeletonSpawn {
662	/// One skeleton of `kind` on the Halloween team, with no lifetime.
663	pub const fn new(kind: SkeletonType) -> Self {
664		Self {
665			kind,
666			team: BossTeam::Halloween,
667			count: 1,
668			infinite: false,
669			lifetime: None,
670		}
671	}
672}
673
674/// 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"))]
679	Normal,
680
681	/// 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"))]
685	King,
686
687	/// A small skeleton (`SKELETON_MINI`).
688	#[doc(alias("SKELETON_MINI"))]
689	Mini,
690}
691
692impl SkeletonType {
693	/// The type numbered `raw`, or `None` for any other number.
694	pub const fn from_raw(raw: c_int) -> Option<Self> {
695		match 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	}
702
703	/// The type's number.
704	pub const fn to_raw(self) -> c_int {
705		match self {
706			Self::Normal => skeleton::SKELETON_NORMAL,
707			Self::King => skeleton::SKELETON_KING,
708			Self::Mini => skeleton::SKELETON_MINI,
709		}
710	}
711}
712
713/// 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)?;
741
742	if !origin.is_finite() {
743		return Err(BossError::NonFinite);
744	}
745
746	if let Some(health) = health
747		&& health <= 0
748	{
749		return Err(BossError::NonPositiveHealth(health));
750	}
751
752	// 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.
760	let entity = unsafe { spawn_on_team(server.server_tools()?, boss.class_name(), origin, team) }?;
761
762	if let Some(health) = health {
763		entity.set_max_health(server, health)?;
764		entity.set_health(server, health)?;
765	}
766
767	Ok(entity)
768}
769
770/// 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)?;
788
789	if !origin.is_finite() {
790		return Err(BossError::NonFinite);
791	}
792
793	// 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.
799	unsafe { spawn_on_team(server.server_tools()?, raw::TF_ZOMBIE, origin, team) }
800}
801
802/// 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)?;
810
811	if entity.has_data_map_class(class) {
812		Ok(())
813	} else {
814		Err(BossError::WrongClass { class })
815	}
816}
817
818/// Fails with [`BossError::UnsupportedGame`] outside TF2.
819fn check_game(server: Server<'_>) -> Result<(), BossError> {
820	if server.game() == Game::TeamFortress2 {
821		Ok(())
822	} else {
823		Err(BossError::UnsupportedGame)
824	}
825}
826
827/// Fails with [`BossError::MarkedForDeletion`] for an entity marked for
828/// deletion.
829fn check_live(entity: Entity<'_>) -> Result<(), BossError> {
830	if entity.is_marked_for_deletion() {
831		Err(BossError::MarkedForDeletion)
832	} else {
833		Ok(())
834	}
835}
836
837/// 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) {
845	let raw = entity.as_ptr();
846
847	// 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.
851	unsafe {
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}
855
856/// 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.
873	let entity =
874		unsafe { tools.create_entity_by_name(class) }.ok_or_else(|| SpawnError::UnknownClass {
875			class: class.to_owned(),
876		})?;
877
878	if !tools.set_key_value(entity, c"origin", &vector_value(origin)) {
879		// No protected entity is new and unspawned.
880		let _ = tools.remove(entity);
881
882		return Err(SpawnError::KeyRejected {
883			key: c"origin".to_owned(),
884		}
885		.into());
886	}
887
888	// SAFETY: The entity has not spawned, and the caller vouches for its
889	// class.
890	unsafe { change_team(entity, team) };
891
892	// SAFETY: The entity is new, and the caller vouches for its `Spawn`.
893	unsafe { tools.dispatch_spawn(entity) };
894
895	if entity.is_marked_for_deletion() {
896		return Err(SpawnError::RemovedItself.into());
897	}
898
899	Ok(entity)
900}
901
902/// `1` or `0`, as boolean key values are given.
903fn flag_value(flag: bool) -> &'static CStr {
904	if flag { c"1" } else { c"0" }
905}
906
907/// A number as a key value, in decimal.
908fn number_value(number: impl Display) -> CString {
909	CString::new(number.to_string()).expect("numbers contain no NUL")
910}
911
912/// A vector as a key value: its three components, in decimal.
913fn vector_value(vector: Vector) -> CString {
914	let 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);
915
916	CString::new(value).expect("numbers contain no NUL")
917}