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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
//! TF2 sounds played to chosen clients without a position, as TF2's announcer
//! plays its lines, sound script entries played from an entity, and the
//! precaching of sound script entries.
//!
//! [`broadcast`] sends each recipient TF2's `teamplay_broadcast_audio` game
//! event, which the client answers by playing the sound from its own view.
//! The client resolves the name, so it can be a sound script entry, such as
//! `Announcer.RoundBegins5Seconds`, or a sample path. [`emit_script_sound`]
//! plays a sound script entry from an entity, as the game plays its own. For
//! a sample played from an entity or a point, to chosen recipients, use
//! [`EngineSound::emit_sound`](crate::interfaces::EngineSound::emit_sound),
//! which takes precached samples only. To make a player speak a voice line,
//! use [`voice`](crate::tf2::voice).
//!
//! # Precaching
//!
//! The server's precache table lists the samples clients load ahead of play.
//! It is emptied at every level change, so a plugin precaches its sounds
//! again for each map: samples through
//! [`EngineSound::precache_sound`](crate::interfaces::EngineSound::precache_sound),
//! and every sample of a sound script entry through [`precache_script_sound`].
//! TF2 precaches the sounds it plays itself, such as every voice line its
//! response rules can choose, when the level starts.
//!
//! # Bots
//!
//! Bots and other fake clients have no client, so they hear nothing.
//! [`broadcast`] skips them.
//!
//! # Unverified
//!
//! On TF2's 64-bit Windows server, a client has been observed to play a
//! broadcast sound script entry, `Game.YourTeamWon`, as its handler in the
//! SDK does (`game/client/clientmode_shared.cpp`): from its own view, not
//! positioned in the world. Whether a client loads on demand a sample the
//! server never precached has not been tested, nor have Linux servers.
use crateEntity;
use crateSoundFlags;
use crateCreateEventError;
use crateGameEvent;
use crate;
use crateGameEventId;
use crate;
use crateRecipients;
use crate::;
use ;
/// The `team` of a `teamplay_broadcast_audio` event that clients on every team
/// play.
const ANY_TEAM: c_int = 255;
/// The game event TF2's `BroadcastSound` fires.
const BROADCAST_EVENT: &CStr = TeamplayBroadcastAudio.name_cstr;
/// The longest sound name, in bytes, that [`broadcast`] sends.
///
/// An `svc_GameEvent` carries at most 2047 bits of event. TF2 describes
/// `teamplay_broadcast_audio` (`resource/modevents.res`) as a byte `team`,
/// the string `sound`, and the shorts `additional_flags` and `player`, which
/// with the event's 9-bit ID take 49 bits besides the name, leaving 249
/// bytes for the name and its terminator.
pub const MAX_SOUND_LEN: usize = 248;
/// The `player` of a `teamplay_broadcast_audio` event that names no player.
const NO_PLAYER: c_int = -1;
/// Why [`broadcast`] could not send its event.
/// Why [`emit_script_sound`] could not play a sound.
/// Why [`precache_script_sound`] could not precache a sound.
/// Plays a sound to every client `recipients` names, from the client's own
/// view, as TF2's announcer lines play. Returns how many clients' channels
/// queued it.
///
/// This builds the `teamplay_broadcast_audio` event that TF2's
/// `CTFGameRules::BroadcastSound` fires (`game/shared/tf/tf_gamerules.cpp`),
/// but instead of firing it to every client, sends it to each recipient as an
/// `svc_GameEvent`, in the stream the event's description asks for. The
/// client plays `sound`, a sound script entry or a sample path of at most
/// [`MAX_SOUND_LEN`] bytes, with `flags` added, through
/// `C_BaseEntity::EmitSound` from `SOUND_FROM_LOCAL_PLAYER`, so it does not
/// fade with distance. The server neither resolves nor checks the name.
///
/// # Teams
///
/// The client plays the event only for its `team`
/// (`ClientModeShared::FireGameEvent` in `game/client/clientmode_shared.cpp`):
/// a client plays a team number if it is on that team, or spectates a player
/// on it in first-person, chase or point-of-interest mode. Team 0 plays only
/// for a client that finds no team of its own (`GetLocalTeam` is null) and
/// spectates a player on team 0, which almost never happens. Team 255 plays
/// for every client whatever its team, and is what this sends, so
/// `recipients` alone choose who hears the sound.
///
/// # Voice
///
/// `voice_of` fills the event's `player` field. If it is a TF2 player, and
/// the sound plays on `CHAN_VOICE`, each client pitches the sound as it
/// pitches that player's voice (`C_TFPlayer::ClientAdjustStartSoundParams`),
/// such as for Pyrovision or a voice-changing spell. `None` sends -1, which no
/// client adjusts the sound for. Clients adjust the sound for TF2 players
/// only, so any other networked entity changes nothing, as `None` does.
///
/// # Recipients
///
/// Only recipients with a player entity and a net channel get the event.
/// Bots, SourceTV and empty slots have no channel, and a client still
/// connecting has no player until the game creates one
/// (`ClientPutInServer`), so all of them are skipped. If none is left, this
/// returns `Ok(0)` before creating the event, which the game event manager
/// refuses to create while no client listens for it, as on a server with
/// only bots. A recipient whose stream has no room left for the event is not
/// counted.
///
/// [`Recipients::reliable`] has no effect here: the event goes in the stream
/// its description asks for, as when the game fires it.
///
/// The event is sent, never fired, so no listener on the server sees it.
/// SourceTV relays the fired event to its spectators and demos
/// (`CTFHLTVDirector::GetModEvents` in `game/server/tf/tf_hltvdirector.cpp`),
/// but SourceTV demos and Replay recordings never contain a broadcast.
///
/// # Errors
///
/// The sound's length and `voice_of` are checked first, so an empty or
/// bot-only `recipients` fails as any other would: with
/// [`BroadcastError::Encode`] for a sound longer than [`MAX_SOUND_LEN`] bytes,
/// and with [`BroadcastError::NotNetworked`] for a `voice_of` without an
/// edict.
/// Plays a sound script entry, such as `TFPlayer.Decapitated`, from `entity`,
/// as the game's own `CBaseEntity::EmitSound` does: positioned at the entity,
/// on the entry's channel, at its volume, pitch and sound level, to the
/// clients that can hear the entity's origin (`CPASAttenuationFilter`).
///
/// This calls the `EmitSound` member that `CBaseEntity` exposes to VScript
/// (`ScriptEmitSound`, `game/shared/SoundEmitterSystem.cpp`) on the entity,
/// through its native binding, without a script VM. The member returns
/// nothing, so an unknown entry is not reported as an error; the game plays
/// nothing. The entry's samples must be precached, as TF2 precaches the
/// sounds it plays itself, or as [`precache_script_sound`] does.
///
/// The game's filter does not use prediction rules, so unlike the game's
/// predicted sounds, it reaches a player whose command the game is running.
/// The engine's `EmitSound` runs before this returns, with any plugin hook
/// on it.
/// Precaches every sample of a sound script entry, such as `Scout.Thanks01`,
/// for the current level, as the game's `CBaseEntity::PrecacheScriptSound`
/// does.
///
/// A name that is no entry is precached as a sample path if it contains
/// `.wav` in any case, or `.mp3` in lowercase only
/// (`CSoundEmitterSystem::PrecacheScriptSound` in
/// `game/shared/SoundEmitterSystem.cpp`), so give sample paths a lowercase
/// extension.
///
/// This calls the `PrecacheScriptSound` member that `CBaseEntity` exposes to
/// VScript (`game/server/baseentity.cpp`) on the world entity, through its
/// native binding, without a script VM. The member returns nothing, so an
/// unknown entry is not reported as an error; the game prints
/// `PrecacheScriptSound '...' failed, no such sound script entry` to the
/// developer console instead, the first time each name fails.
///
/// # Timing
///
/// The world exists once the map's entities have spawned; before that, this
/// fails with [`PrecacheError::NoWorld`]. The game allows precaching until
/// the end of its own `ServerActivate`, which closes the window
/// (`CBaseEntity::SetAllowPrecache(false)` in `game/server/gameinterface.cpp`),
/// so precache in a hook that runs after the game's `LevelInit`, or before
/// the game's `ServerActivate`. A callback that runs after the game's
/// `ServerActivate`, such as a post-hook of it, is already late. Unlike
/// VScript's global `PrecacheScriptSound`, the member does not reopen the
/// window, so a late call still precaches each sample, but the game warns
/// `Late precache of ...` for each one the engine reports as not precached
/// yet (`CBaseEntity::PrecacheSound`).