1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
//! TF2's taunts: starting, stopping and reading a player's taunt, through
//! the game's native script methods and the player's networked variables.
//!
//! [`TfPlayer::set_forced_taunt_cam`](crate::tf2::player::TfPlayer::set_forced_taunt_cam)
//! forces a player's view into third person, as taunts do.
#[cfg(test)]
#[path = "../tests/tf2/taunts.rs"]
mod tests;
use crate::Server;
use crate::datatables::NetProp;
use crate::entities::Entity;
use crate::tf2::conditions::Condition;
use crate::tf2::effects::EffectError;
use crate::tf2::player_methods;
use crate::tf2::weapons::ItemDefinitionIndex;
use sdk_raw::tf2::conditions as raw_conditions;
use sdk_raw::tf2::script_binding::{boolean, float, int};
use std::ffi::{CStr, c_int};
use std::ptr::NonNull;
/// The most taunt slots a loadout has, which
/// `CTFPlayer::HandleTauntCommand` numbers from 1.
pub const TAUNT_SLOTS: u8 = 8;
/// One player's taunts within the current engine callback.
#[derive(Debug, Clone, Copy)]
pub struct PlayerTaunts<'s> {
server: Server<'s>,
player: Entity<'s>,
}
impl<'s> PlayerTaunts<'s> {
/// Wraps `player` for taunt calls. Fails with [`EffectError::NotTfPlayer`]
/// unless the server runs TF2 and `player`'s class name is `player`.
pub fn new(server: Server<'s>, player: Entity<'s>) -> Result<Self, EffectError> {
if !player_methods::is_tf_player(server, player) {
return Err(EffectError::NotTfPlayer);
}
Ok(Self { server, player })
}
/// Calls one of the player's native methods that returns nothing.
///
/// # Safety
///
/// The method accepts these arguments, and runs only the game's own taunt
/// code on the player, which frees entities only through deferred
/// deletion.
unsafe fn call(
self,
method: &CStr,
arguments: &mut [sys::ScriptVariant_t],
) -> Result<(), EffectError> {
// SAFETY: A TF2 `player` is a CTFPlayer, live on the main thread for
// this callback, whose game module stays loaded, and the caller vouches
// for the method.
Ok(unsafe { player_methods::call_void(self.player, method, arguments) }?)
}
/// Stops the player's taunt, as [`Self::stop`] does with its prop, and
/// calls off a Eureka Effect teleport the taunt began.
#[doc(alias("CancelTaunt"))]
pub fn cancel(self) -> Result<(), EffectError> {
// SAFETY: As for `call`. The method stops the taunt.
unsafe { self.call(c"CancelTaunt", &mut []) }
}
/// Ends the player's press-and-hold taunt as letting go of the taunt key
/// does, after its least time and through its outro, if it has them.
/// Returns whether the player was in such a taunt; the game would read
/// the taunt's item without checking it, so no other taunt is ended.
#[doc(alias("EndLongTaunt"))]
pub fn end_long_taunt(self) -> Result<bool, EffectError> {
match self.state()? {
Some(state) if state.is_long() && state.item.is_some() => {}
_ => return Ok(false),
}
// SAFETY: As for `call`. The player is in a press-and-hold taunt from an
// item, whose definition the method reads, which the game clears only
// along with the taunt's item index.
unsafe { self.call(c"EndLongTaunt", &mut []) }?;
Ok(true)
}
/// Whether the game may end the player's taunt once its time is up, which
/// a press-and-hold taunt holds off while the key is held.
#[doc(alias("IsAllowedToRemoveTaunt"))]
pub fn is_allowed_to_remove_taunt(self) -> Result<bool, EffectError> {
self.predicate(c"IsAllowedToRemoveTaunt")
}
/// Whether the player could start a taunt now: alive, on the ground, not
/// already taunting, charging, carrying a building, or holding a weapon
/// that forbids it, among the game's other checks.
#[doc(alias("IsAllowedToTaunt"))]
pub fn is_allowed_to_taunt(self) -> Result<bool, EffectError> {
self.predicate(c"IsAllowedToTaunt")
}
/// Whether the player is taunting ([`Condition::TAUNTING`]).
#[doc(alias("IsTaunting"))]
pub fn is_taunting(self) -> Result<bool, EffectError> {
self.predicate(c"IsTaunting")
}
/// How fast the player's taunt moves them.
#[doc(alias("GetCurrentTauntMoveSpeed"))]
pub fn move_speed(self) -> Result<f32, EffectError> {
// SAFETY: As for `call`. The method reads a member.
Ok(unsafe {
player_methods::call_float(self.player, c"GetCurrentTauntMoveSpeed", &mut [])
}?)
}
/// Resolves one of the player's networked variables.
fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, EffectError> {
Ok(self
.server
.server_game_dll()?
.entity_net_prop(self.player, name)?)
}
/// The player whose taunts these are.
pub const fn player(self) -> Entity<'s> {
self.player
}
/// Calls one of the player's native predicates, which take nothing.
fn predicate(self, method: &CStr) -> Result<bool, EffectError> {
// SAFETY: As for `call`. The predicates only read the player's state.
Ok(unsafe { player_methods::call_bool(self.player, method, &mut []) }?)
}
/// The player's pointer, for the raw condition methods.
fn raw_player(self) -> NonNull<sys::CBaseEntity> {
// SAFETY: An entity's pointer is never null.
unsafe { NonNull::new_unchecked(self.player.as_ptr()) }
}
/// The game time at which the player's taunt may end, or 0 once it has
/// been stopped.
#[doc(alias("GetTauntRemoveTime"))]
pub fn remove_time(self) -> Result<f32, EffectError> {
// SAFETY: As for `call`. The method reads a member.
Ok(unsafe { player_methods::call_float(self.player, c"GetTauntRemoveTime", &mut []) }?)
}
/// Sets how fast the player's taunt moves them, for a taunt that lets them
/// move. Fails with [`EffectError::OutOfRange`] for a negative or infinite
/// speed.
#[doc(alias("SetCurrentTauntMoveSpeed"))]
pub fn set_move_speed(self, speed: f32) -> Result<(), EffectError> {
if !(speed.is_finite() && speed >= 0.0) {
return Err(EffectError::OutOfRange("speed"));
}
// SAFETY: As for `call`. The method writes a member.
unsafe { self.call(c"SetCurrentTauntMoveSpeed", &mut [float(speed)]) }
}
/// The taunt the player is in, or `None` if they are not taunting.
#[doc(alias("m_iTauntIndex", "m_iTauntConcept", "m_iTauntItemDefIndex"))]
pub fn state(self) -> Result<Option<TauntState>, EffectError> {
// SAFETY: As for `call`. The method reads the player's condition bits.
let taunting =
unsafe { raw_conditions::in_cond(self.raw_player(), Condition::TAUNTING.to_raw()) }?;
if !taunting {
return Ok(None);
}
let item = self
.net_prop(c"m_iTauntItemDefIndex")?
.get::<c_int>(self.player)?;
Ok(Some(TauntState {
can_move: self
.net_prop(c"m_bAllowMoveDuringTaunt")?
.get::<bool>(self.player)?,
concept: self
.net_prop(c"m_iTauntConcept")?
.get::<c_int>(self.player)?,
item: u16::try_from(item).ok().and_then(ItemDefinitionIndex::new),
kind: self.net_prop(c"m_iTauntIndex")?.get::<c_int>(self.player)?,
move_speed: self
.net_prop(c"m_flCurrentTauntMoveSpeed")?
.get::<f32>(self.player)?,
}))
}
/// Stops the player's taunt at once, with its scene, sound and partner
/// taunt, and removes its prop, if any, unless `remove_prop` is false and
/// the prop removes itself after its outro.
#[doc(alias("StopTaunt"))]
pub fn stop(self, remove_prop: bool) -> Result<(), EffectError> {
// SAFETY: As for `call`. The method stops the taunt, and removes its
// prop through the engine's deferred deletion.
unsafe { self.call(c"StopTaunt", &mut [boolean(remove_prop)]) }
}
/// Starts a taunt of `kind`, as the game's code does, and returns whether
/// the player is taunting afterwards. The game refuses a player who is
/// not [allowed to taunt](Self::is_allowed_to_taunt), or whose class and
/// weapon have no such taunt.
#[doc(alias("Taunt"))]
pub fn taunt(self, kind: TauntKind) -> Result<bool, EffectError> {
// SAFETY: As for `call`. Both kinds pick their own response concept, so
// the concept passed is unused.
unsafe { self.call(c"Taunt", &mut [int(kind.to_raw()), int(0)]) }?;
self.is_taunting()
}
/// Starts a taunt as the taunt key does: the taunt equipped in loadout
/// taunt slot `slot`, from 1 to [`TAUNT_SLOTS`], or for 0, joining a
/// nearby partner's taunt or else the active weapon's taunt. Returns
/// whether the player is taunting afterwards.
///
/// Fails with [`EffectError::OutOfRange`] for a slot past
/// [`TAUNT_SLOTS`].
#[doc(alias("HandleTauntCommand"))]
pub fn taunt_slot(self, slot: u8) -> Result<bool, EffectError> {
if slot > TAUNT_SLOTS {
return Err(EffectError::OutOfRange("slot"));
}
// SAFETY: As for `call`. The method checks the slot, and plays the
// equipped taunt item, if any.
unsafe { self.call(c"HandleTauntCommand", &mut [int(c_int::from(slot))]) }?;
self.is_taunting()
}
}
/// A taunt the game starts from code rather than from an item, which
/// [`PlayerTaunts::taunt`] starts (`taunts_t`).
///
/// The game's other kinds play an item's taunt, which
/// [`PlayerTaunts::taunt_slot`] starts by its loadout slot, or are reserved
/// for its own code.
#[doc(alias("taunts_t"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TauntKind {
/// `TAUNT_BASE_WEAPON`: the taunt of the player's active weapon, as the
/// taunt key plays without a taunt item.
#[doc(alias("TAUNT_BASE_WEAPON"))]
BaseWeapon,
/// `TAUNT_SHOW_ITEM`: showing off the active weapon to those nearby.
#[doc(alias("TAUNT_SHOW_ITEM"))]
ShowItem,
}
impl TauntKind {
/// The game's number for the kind.
pub const fn to_raw(self) -> c_int {
match self {
Self::BaseWeapon => sys::taunts_t_TAUNT_BASE_WEAPON as c_int,
Self::ShowItem => sys::taunts_t_TAUNT_SHOW_ITEM as c_int,
}
}
}
/// A taunt a player is in, as the game networks it.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TauntState {
/// Whether the taunt lets the player move, as some press-and-hold taunts
/// do (`m_bAllowMoveDuringTaunt`).
pub can_move: bool,
/// The response concept the taunt was played for (`m_iTauntConcept`).
pub concept: c_int,
/// The taunt item playing, if any (`m_iTauntItemDefIndex`).
pub item: Option<ItemDefinitionIndex>,
/// The kind of taunt, a `taunts_t` (`m_iTauntIndex`).
pub kind: c_int,
/// How fast the taunt moves the player (`m_flCurrentTauntMoveSpeed`).
pub move_speed: f32,
}
impl TauntState {
/// Whether the taunt is a press-and-hold taunt from an item
/// (`TAUNT_LONG`), which [`PlayerTaunts::end_long_taunt`] ends.
pub const fn is_long(&self) -> bool {
self.kind == sys::taunts_t_TAUNT_LONG as c_int
}
}