1//! TF2's scoreboard, changed through the game's own scoring state.
2//!
3//! Everything here writes the state the game itself keeps and computes the
4//! scoreboard from. The game, clients, and every other reader then agree on
5//! the new values, and nothing has to be restored or kept applied: a change
6//! lasts until the game itself resets or replaces the value, as described
7//! [below](#resets).
8//!
9//! # Where the scoreboard comes from
10//!
11//! TF2 clients build the scoreboard from two kinds of networked entities:
12//!
13//! - `tf_player_manager` (`CTFPlayerResource`), which holds an array per
14//! column with an element per player slot. For each connected player, the
15//! game recomputes the elements from its own state at every think, ten
16//! times a second (`player_resource.cpp:94-101`); the damage, healing,
17//! support and credit columns at most once a second
18//! (`tf_player_resource.cpp:218-230`).
19//! - `tf_team` (`CTFTeam`), whose score and flag captures the game only
20//! assigns when they change.
21//!
22//! The game stores no player's score. It keeps statistics per player, which
23//! `sdk_raw`'s [`GameStats`] finds: for the session, which the scoreboard's
24//! Score is computed from, and for the current round, which the round's score
25//! and its MVPs are computed from. At each think, the player resource scores
26//! the session's statistics with `CTFGameRules::CalcPlayerScore`, and copies
27//! both scores and some of the statistics into the player's local scoring data
28//! (`tf_player_resource.cpp:200-258`). [`PlayerScore`] reads and changes those
29//! statistics, and scores them with the game's own function.
30//!
31//! # Points
32//!
33//! [`PlayerScore::add_points`] and [`PlayerScore::set_total`] change a
34//! player's Score by exactly the amount asked, through
35//! [`Stat::KillsRuneCarrier`]: the game scores it one point each, in every
36//! mode and whatever the player's attributes, and shows or uploads it nowhere
37//! else. The game also counts a kill of any player carrying a Mannpower rune
38//! there, so [`PlayerScore::points`] includes those. The game never shows a
39//! Score below 0: a total taken below it shows 0, and the deficit absorbs the
40//! points the player earns next. [`Applied`] tells what clients see change.
41//!
42//! The other [`Stat`]s change the Score by their own weights, some of them
43//! divided and floored (`tf_gamerules.cpp:17031-17093`), and feed the match
44//! summaries and medals the game sends at the end of a competitive match.
45//!
46//! # What the game reports
47//!
48//! At each think, the player resource first copies each connected player's
49//! session and round scores into the player's scoring data
50//! (`m_Shared.m_ScoreData.m_iPoints` and `m_RoundScoreData.m_iPoints`). When
51//! that raises the session's points, the game fires `player_score_changed`,
52//! which the war tracker and match experience listen to
53//! (`tf_player_shared.cpp:14574-14620`); a drop fires nothing. Then it
54//! computes the player's Score again and compares it with the one it last
55//! sent, the player's element of the resource's `m_iTotalScore`, and stores
56//! the new one. Only that comparison reports anything: the game reports the
57//! difference to the item servers as Strange "Points Scored" progress of the
58//! player's cosmetics, or in Mann vs. Machine to its statistics
59//! (`tf_player_resource.cpp:200-258`). A drop is reported as negative
60//! progress.
61//!
62//! Changing statistics with [`PlayerScore`] reports none of this when it is
63//! made: it also adds the change in each score to the values the game compares
64//! against, and marks them changed for clients, which receive the new values at
65//! once. Points the game awards are still reported as usual, including those it
66//! awards between a change and its next think. The plugin's change stays in
67//! those values, though, so when one of the game's [resets](#resets) later
68//! drops the player's Score, the game reports the whole drop, including the
69//! plugin's points, as negative progress. Vanilla would report only the points
70//! the game awarded.
71//!
72//! # Reporting nothing
73//!
74//! [`PlayerScore::rebase`] sets the values the game compares against to the
75//! scores themselves, discarding every difference the game has not sent yet.
76//! Called at each of these points, it leaves the game nothing to report:
77//!
78//! - From a [`GameEventId::PlayerScoreChanged`] listener, which covers every
79//! point the game awards. The game fires the event from the think, after
80//! writing the session's points and before computing and comparing the
81//! Score, with nothing changing statistics in between, and the engine calls
82//! each listener before the event returns.
83//! - From a [`GameEventId::ScorestatsAccumulatedReset`] listener. The game
84//! fires the event right after resetting every player's scores for
85//! `mp_restartgame`, a tournament restart, or the end of the wait for
86//! players, before any think (`teamplayroundbased_gamerules.cpp:3235-3282`).
87//! A hook after each player's `CTFPlayer::ResetScores`, at
88//! [`RESET_SCORES_SLOT`](sdk_raw::tf2::scoreboard::RESET_SCORES_SLOT), covers
89//! the same resets one player at a time, and also Mann vs. Machine's.
90//! - From a [`GameEventId::PlayerActivate`] listener, or a hook after the
91//! game's `IServerGameClients::ClientActive`, which spawns the player
92//! (`tf_client.cpp:101-108`). The engine creates, spawns, and activates a
93//! player in one call, before any think. The game
94//! never clears a slot's element of `m_iTotalScore` when its player leaves,
95//! so a newcomer's first think compares against whoever last had the slot.
96//! A newcomer wears no items yet then, so vanilla's report of that
97//! difference reaches no Strange counter in practice; the rebase covers a
98//! player who spawns with items before that think.
99//! - For every player when the plugin starts, unpauses, or a level starts,
100//! discarding what the game has not sent since its last think.
101//! - For every connected player from a hook before each of the player
102//! resource's thinks, through `CBaseEntity::Think` at
103//! [`THINK_SLOT`](sdk_raw::entities::THINK_SLOT), which covers all of the
104//! above: the think then finds nothing to report, whatever the game awarded
105//! or reset since its last one. The resource also updates as a Mann vs.
106//! Machine wave completes and as a matchmade game reports its result
107//! (`tf_player_resource.cpp:70-80`, `tf_gamerules.cpp:2575`), which the hook
108//! does not precede.
109//!
110//! The game reports as usual when nothing rebases, as while the plugin is
111//! paused, and in these cases:
112//!
113//! - After the plugin stops rebasing, a reset reports the whole drop of each
114//! player's Score, including the points the game awarded while it was
115//! rebased and never reported, as negative progress. Disconnecting and
116//! changing maps report nothing.
117//! - [`reset_scores`] fires no `scorestats_accumulated_reset`, so the next
118//! think reports the drop unless [`PlayerScore::rebase`] follows it.
119//! - The Score can change while no statistic does, and without an event: when
120//! the player's `scoreboard_minigame` attribute comes or goes, when the game
121//! switches to Mann vs. Machine's or Mannpower's scoring, when healing passes
122//! 10,000,000, beyond which the game stops scoring it, or when a sum
123//! overflows. The next think reports the difference.
124//! - A player resource created while a level runs starts every element of
125//! `m_iTotalScore` at 0, and so reports each player's whole Score at its
126//! first think.
127//! - Mann vs. Machine's population manager resets players' scores without an
128//! event, though through each player's `ResetScores`, which a hook sees.
129//! - Another plugin's `player_score_changed` listener that runs after the
130//! rebasing one and changes statistics leaves its change to be reported.
131//!
132//! # Resets
133//!
134//! The game resets a player's statistics, and so their Score, when they
135//! connect, when they disconnect, when `mp_restartgame`, a tournament restart,
136//! or the end of the wait for players resets every player's scores (after
137//! which it fires `scorestats_accumulated_reset`), when [`reset_scores`] resets
138//! them, which fires no event, and when Mann vs. Machine's population manager
139//! resets them, which fires none either. A map change reconnects everyone. Changing team
140//! resets nothing, and a team scramble only the teams' scores. The round's
141//! statistics reset with every round (`stats_resetround`) and Mann vs.
142//! Machine wave, leaving the session's alone. Frags and deaths reset with the
143//! scores.
144//!
145//! A plugin that keeps its own points across these resets applies them again
146//! after them: the session's after [`GameEventId::PlayerActivate`],
147//! [`GameEventId::ScorestatsAccumulatedReset`], and its own [`reset_scores`],
148//! but not after [`GameEventId::StatsResetround`], which leaves the session's
149//! statistics, and so points already applied, in place.
150//!
151//! # Holding a score
152//!
153//! To keep a player's Score at a value of the plugin's choosing while they
154//! play, listen to [`GameEventId::PlayerScoreChanged`] and call
155//! [`PlayerScore::set_total`] and then [`PlayerScore::rebase`] from the
156//! listener. The game fires the event from the player resource's think, after
157//! updating the player's scoring data but before computing the Score it
158//! sends, so the Score clients receive never changes, and the rebase leaves
159//! the game nothing to report. `set_total` alone keeps the difference the game
160//! has not sent, so the game would still report the points it awarded.
161//!
162//! The event only fires when the session's points rise, so the listener sees
163//! no decrease or reset: set the held value and rebase again after the resets
164//! above. Clients then receive the held Score at once.
165//! A script's `ResetScores` on the player clears their scoring data but not
166//! their statistics, so the next think fires the event with their whole
167//! Score as its increase.
168//!
169//! A hook before each of the player resource's thinks can hold every
170//! statistic the scoreboard shows, and the Score, without listening to any
171//! event: set the session's and the round's with [`PlayerScore::set_stats`],
172//! and the frags and deaths the think copies with [`PlayerScore::set_frags`]
173//! and [`PlayerScore::set_deaths`], then call [`PlayerScore::rebase`]. The
174//! think then shows exactly those, fires no `player_score_changed`, and
175//! reports nothing, whatever the game changed since its last think.
176//!
177//! # Limits
178//!
179//! The scoreboard's Score is networked as an unsigned 32-bit varint, the
180//! player's own points readout with 10 bits, so it wraps above 1023, and
181//! frags and deaths with 12 signed bits, -2048 to 2047 in the SDK.
182//!
183//! # Unverified
184//!
185//! The game's functions and statistics are found and checked in TF2's 64-bit
186//! Windows `server.dll` as [`GameStats`] describes. On Linux GNU servers they
187//! are found by symbols inferred from the SDK's source, which have not been
188//! checked against a retail build. The order of the player resource's think
189//! and of `player_score_changed`'s listeners was read from the 64-bit Windows
190//! `server.dll` and `engine.dll`; on Linux it is inferred from the SDK's
191//! source. No change described here has yet been observed on a live server:
192//! that clients receive it at once, that items' Strange counts stay as they
193//! were, with or without [`PlayerScore::rebase`], and the client-side effects
194//! of [`PlayerScore::set_killstreak`].
195//!
196//! [`GameEventId::PlayerActivate`]: crate::tf2::game_events::GameEventId::PlayerActivate
197//! [`GameEventId::PlayerScoreChanged`]: crate::tf2::game_events::GameEventId::PlayerScoreChanged
198//! [`GameEventId::ScorestatsAccumulatedReset`]: crate::tf2::game_events::GameEventId::ScorestatsAccumulatedReset
199//! [`GameEventId::StatsResetround`]: crate::tf2::game_events::GameEventId::StatsResetround
200201#[cfg(test)]
202#[path = "../../tests/tf2/scoreboard.rs"]
203mod tests;
204205use crate::NotThreadSafe;
206use crate::datatables::{NetPropError, PropFlags, PropKind, SendProp, ServerClass, Storage};
207use crate::edicts::Edict;
208use crate::entities::{Entity, EntityHandle};
209use crate::interfaces::{ServerGameDll, ServerTools, ValveEngine};
210use crate::{Game, InterfaceError, Server};
211use sdk_raw::edicts::MAX_CHANGE_OFFSETS;
212213use sdk_raw::tf2::scoreboard::{
214ELEMENT_SIZE, GameStats, GameStatsError, KILL_STREAK, KillStats, MAX_PLAYERS_ARRAY_SAFE,
215PlayerStats, RoundStats, TF_TEAM_BLUE, TF_TEAM_RED, stat,
216};
217218use sdk_raw::vcall;
219use std::ffi::{CStr, c_int};
220use std::mem::offset_of;
221use std::ops::RangeInclusive;
222use std::ptr::NonNull;
223224/// An exclusive bound on the datamap offsets trusted for a player's fields.
225const MAX_FIELD_OFFSET: usize = 1 << 16;
226227/// The class name of TF2's `CTFPlayerResource` (`tf_player_resource.cpp:52`).
228const RESOURCE_CLASS_NAME: &CStr = c"tf_player_manager";
229230/// The class name of TF2's `CTFTeam` (`tf_team.cpp:61`).
231const TEAM_CLASS_NAME: &CStr = c"tf_team";
232233/// Which of a player's statistics a change applies to, besides the session's.
234#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Adjust {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f, "Adjust",
"round", &&self.round)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for Adjust { }
#[automatically_derived]
impl ::core::clone::Clone for Adjust {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<bool>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for Adjust { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for Adjust { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Adjust {
#[inline]
fn eq(&self, other: &Self) -> bool { self.round == other.round }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for Adjust {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<bool>;
}
}Eq, #[automatically_derived]
impl ::core::hash::Hash for Adjust {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&self.round, state)
}
}Hash)]
235pub struct Adjust {
236/// Whether the current round's statistics change too, and with them the
237 /// round's score, which the game picks the round's MVPs by, and the
238 /// player's round summary. `true` by default.
239 ///
240 /// The game resets the round's statistics every round, but not the
241 /// session's.
242pub round: bool,
243}
244245impl Defaultfor Adjust {
246fn default() -> Self {
247Self { round: true }
248 }
249}
250251/// How much a change moved the scores clients are shown.
252#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Applied {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "Applied",
"round_shown", &self.round_shown, "shown", &&self.shown)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for Applied { }
#[automatically_derived]
impl ::core::clone::Clone for Applied {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<i32>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for Applied { }Copy, #[automatically_derived]
impl ::core::default::Default for Applied {
#[inline]
fn default() -> Self {
Applied {
round_shown: ::core::default::Default::default(),
shown: ::core::default::Default::default(),
}
}
}Default, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for Applied { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Applied {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.round_shown == other.round_shown && self.shown == other.shown
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for Applied {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<i32>;
}
}Eq, #[automatically_derived]
impl ::core::hash::Hash for Applied {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&self.round_shown, state);
::core::hash::Hash::hash(&self.shown, state)
}
}Hash)]
253pub struct Applied {
254/// The change in the round's score, 0 unless [`Adjust::round`].
255pub round_shown: i32,
256257/// The change in the session's score, the scoreboard's Score. It differs
258 /// from the change asked for where the game's clamping at 0 hides part of
259 /// it, or a statistic's weight is not 1.
260pub shown: i32,
261}
262263/// Where a player column's array lives in the player resource, as resolved
264/// from the resource's server class.
265#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ArrayLayout {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f, "ArrayLayout",
"first", &self.first, "len", &self.len, "range", &&self.range)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for ArrayLayout { }
#[automatically_derived]
impl ::core::clone::Clone for ArrayLayout {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<usize>;
let _: ::core::clone::AssertParamIsClone<NetRange>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for ArrayLayout { }Copy)]
266struct ArrayLayout {
267/// Bytes from the start of the entity to the first element.
268first: usize,
269270/// The number of elements, each [`ELEMENT_SIZE`] bytes after the last.
271len: usize,
272273/// The values an element can be networked with.
274range: NetRange,
275}
276277impl ArrayLayout {
278/// Resolves the array `name` in `class`, checking that it is a contiguous
279 /// array of `int`s that holds at least player slot 1.
280fn resolve(
281 dll: ServerGameDll<'_>,
282 class: ServerClass<'_>,
283 name: &'static CStr,
284 ) -> Result<Self, ScoreError> {
285let display_name = name.to_str().unwrap_or_default();
286let unexpected = || ScoreError::UnexpectedLayout {
287 name: display_name,
288 needed: 2,
289 };
290291let array = dll.net_prop(class, name)?;
292let len = array
293 .element_count()
294 .filter(|&len| len >= 2)
295 .ok_or_else(unexpected)?;
296let first = array.element(0)?;
297298for index in 0..len {
299let element = array.element(index)?;
300let expected = index
301 .checked_mul(ELEMENT_SIZE)
302 .and_then(|bytes| first.offset().checked_add(bytes));
303304if element.prop().kind() != PropKind::Int
305 || !element.storage().is_compatible(Storage::I32)
306 || Some(element.offset()) != expected
307 {
308return Err(unexpected());
309 }
310 }
311312Ok(Self {
313 first: first.offset(),
314len,
315 range: encodable_range(first.prop()),
316 })
317 }
318319/// Bytes from the start of the entity to the element for `slot`, or an
320 /// error naming the array `name` if it does not reach it.
321fn offset(self, name: &'static str, slot: usize) -> Result<usize, ScoreError> {
322if slot < self.len {
323// `resolve` checked that every element's offset is this.
324Ok(self.first + slot * ELEMENT_SIZE)
325 } else {
326Err(ScoreError::UnexpectedLayout {
327name,
328 needed: slot.saturating_add(1),
329 })
330 }
331 }
332}
333334/// The variables of one entity marked changed during one write, sent to the
335/// engine's change tracking together.
336#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Changes {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f, "Changes",
"offsets", &&self.offsets)
}
}Debug, #[automatically_derived]
impl ::core::default::Default for Changes {
#[inline]
fn default() -> Self {
Changes { offsets: ::core::default::Default::default() }
}
}Default)]
337struct Changes {
338/// The offsets of the variables to mark changed, without duplicates.
339offsets: Vec<usize>,
340}
341342impl Changes {
343/// Tells the engine about the changes to `edict`'s entity.
344 ///
345 /// More offsets than the engine records per frame, or one it cannot
346 /// record, mark the whole entity changed at once, as recording them one by
347 /// one would end up doing.
348fn flush(self, engine: ValveEngine<'_>, edict: Edict<'_>) {
349if self.offsets.is_empty() {
350return;
351 }
352353let offsets: Option<Vec<u16>> = self354 .offsets
355 .iter()
356 .map(|&offset| u16::try_from(offset).ok())
357 .collect();
358359match offsets {
360Some(offsets) if offsets.len() <= usize::from(MAX_CHANGE_OFFSETS) => {
361for offset in offsets {
362 edict.state_changed(engine, offset);
363 }
364 }
365366_ => edict.full_state_changed(engine),
367 }
368 }
369370/// Marks the variable at `offset` changed.
371fn mark(&mut self, offset: usize) {
372if !self.offsets.contains(&offset) {
373self.offsets.push(offset);
374 }
375 }
376}
377378/// The interfaces the scoreboard uses within one callback.
379#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for Context<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f, "Context",
"dll", &self.dll, "engine", &self.engine, "tools", &&self.tools)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for Context<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for Context<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<ServerGameDll<'s>>;
let _: ::core::clone::AssertParamIsClone<ValveEngine<'s>>;
let _: ::core::clone::AssertParamIsClone<ServerTools<'s>>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for Context<'s> { }Copy)]
380struct Context<'s> {
381 dll: ServerGameDll<'s>,
382 engine: ValveEngine<'s>,
383 tools: ServerTools<'s>,
384}
385386impl<'s> Context<'s> {
387fn new(server: Server<'s>) -> Result<Self, InterfaceError> {
388Ok(Self {
389 dll: server.server_game_dll()?,
390 engine: server.valve_engine()?,
391 tools: server.server_tools()?,
392 })
393 }
394}
395396/// A count `CBasePlayer` keeps twice: as a datamap field the game counts
397/// with, and in the player state the engine reads.
398#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for Count<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "Count",
"field", &self.field, "mirror", &&self.mirror)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for Count<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for Count<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<IntField<'s>>;
let _: ::core::clone::AssertParamIsClone<IntField<'s>>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for Count<'s> { }Copy)]
399struct Count<'s> {
400/// The datamap field, such as `m_iFrags`.
401field: IntField<'s>,
402403/// The player state's copy, such as `pl.frags`.
404mirror: IntField<'s>,
405}
406407impl Count<'_> {
408/// Writes `value` to the field, and to the player state's copy if that
409 /// still agreed with the field. Returns whether it did.
410fn set(self, value: i32) -> bool {
411let agreed = self.mirror.read() == self.field.read();
412413self.field.write(value);
414415if agreed {
416self.mirror.write(value);
417 }
418419agreed420 }
421}
422423/// An `int` member of a live entity, at an offset resolved for the entity's
424/// class.
425#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for IntField<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "IntField",
"entity", &self.entity, "offset", &&self.offset)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for IntField<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for IntField<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<Entity<'s>>;
let _: ::core::clone::AssertParamIsClone<usize>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for IntField<'s> { }Copy)]
426struct IntField<'s> {
427 entity: Entity<'s>,
428 offset: usize,
429}
430431impl<'s> IntField<'s> {
432/// # Safety
433 ///
434 /// `offset` must be where an `int` member lives in every entity of
435 /// `entity`'s class, as a send table or datamap of that class gives it.
436const unsafe fn new(entity: Entity<'s>, offset: usize) -> Self {
437Self { entity, offset }
438 }
439440fn read(self) -> i32 {
441// SAFETY: `new`'s contract places an `int` at the offset, within the
442 // entity, which stays allocated for `'s`. It is read without forming a
443 // reference, since the game writes it through its own pointers.
444unsafe {
445self.entity
446 .as_ptr()
447 .byte_add(self.offset)
448 .cast::<i32>()
449 .read_unaligned()
450 }
451 }
452453fn write(self, value: i32) {
454// SAFETY: As for `read`. The game writes its members the same way, on
455 // the main thread.
456unsafe {
457self.entity
458 .as_ptr()
459 .byte_add(self.offset)
460 .cast::<i32>()
461 .write_unaligned(value);
462 }
463 }
464}
465466/// The values a networked integer can hold, from [`encodable_range`].
467#[derive(#[automatically_derived]
impl ::core::fmt::Debug for NetRange {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "NetRange",
"min", &self.min, "max", &&self.max)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for NetRange { }
#[automatically_derived]
impl ::core::clone::Clone for NetRange {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<i32>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for NetRange { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for NetRange { }
#[automatically_derived]
impl ::core::cmp::PartialEq for NetRange {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.min == other.min && self.max == other.max
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for NetRange {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<i32>;
}
}Eq)]
468struct NetRange {
469 min: i32,
470 max: i32,
471}
472473impl NetRange {
474/// Checks that `value` can be networked as the variable `name`.
475fn check(self, name: &'static str, value: i32) -> Result<(), ScoreError> {
476if (self.min..=self.max).contains(&value) {
477Ok(())
478 } else {
479Err(ScoreError::OutOfRange {
480name,
481value,
482 min: self.min,
483 max: self.max,
484 })
485 }
486 }
487488const fn inclusive(self) -> RangeInclusive<i32> {
489self.min..=self.max
490 }
491}
492493/// Where a player class keeps its networked points, as resolved from its
494/// server class.
495#[derive(#[automatically_derived]
impl ::core::fmt::Debug for PlayerLayout {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f, "PlayerLayout",
"class", &self.class, "points", &self.points, "round_points",
&&self.round_points)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for PlayerLayout { }
#[automatically_derived]
impl ::core::clone::Clone for PlayerLayout {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<usize>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for PlayerLayout { }Copy)]
496struct PlayerLayout {
497/// The address of the player's server class, which this describes.
498class: usize,
499500/// `m_Shared.m_ScoreData.m_iPoints`: the session's score, as the player's
501 /// own client receives it.
502points: usize,
503504/// `m_Shared.m_RoundScoreData.m_iPoints`: the round's score.
505round_points: usize,
506}
507508impl PlayerLayout {
509fn resolve(dll: ServerGameDll<'_>, class: ServerClass<'_>) -> Result<Self, ScoreError> {
510Ok(Self {
511 class: class.as_ptr().addr(),
512 points: scoring_points(dll, class, c"m_ScoreData")?,
513 round_points: scoring_points(dll, class, c"m_RoundScoreData")?,
514 })
515 }
516}
517518/// One TF2 player's scoring state, for one callback.
519///
520/// It reads and changes the statistics the game keeps for the player, which
521/// the scoreboard's Score and the round's score are computed from, as the
522/// [module documentation](self) describes, and the player's frags, deaths, and
523/// kill streak. Every change is the game's own state from then on.
524///
525/// Like the [`Entity`] it is made for, it belongs to the callback its
526/// [`Server`] does, and is `Copy`, but neither `Send` nor `Sync`.
527#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for PlayerScore<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ =
&["edict", "game_stats", "index", "layout", "player", "resource",
"resource_edict", "server", "stats", "total_score"];
let values: &[&dyn ::core::fmt::Debug] =
&[&self.edict, &self.game_stats, &self.index, &self.layout,
&self.player, &self.resource, &self.resource_edict,
&self.server, &self.stats, &&self.total_score];
::core::fmt::Formatter::debug_struct_fields_finish(f, "PlayerScore",
names, values)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for PlayerScore<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for PlayerScore<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<Edict<'s>>;
let _: ::core::clone::AssertParamIsClone<GameStats>;
let _: ::core::clone::AssertParamIsClone<usize>;
let _: ::core::clone::AssertParamIsClone<PlayerLayout>;
let _: ::core::clone::AssertParamIsClone<Entity<'s>>;
let _: ::core::clone::AssertParamIsClone<Entity<'s>>;
let _: ::core::clone::AssertParamIsClone<Edict<'s>>;
let _: ::core::clone::AssertParamIsClone<Server<'s>>;
let _: ::core::clone::AssertParamIsClone<NonNull<PlayerStats>>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for PlayerScore<'s> { }Copy)]
528pub struct PlayerScore<'s> {
529 edict: Edict<'s>,
530 game_stats: GameStats,
531 index: usize,
532 layout: PlayerLayout,
533 player: Entity<'s>,
534 resource: Entity<'s>,
535 resource_edict: Edict<'s>,
536 server: Server<'s>,
537538/// The game's statistics block for the player's entity index.
539stats: NonNull<PlayerStats>,
540541/// The offset of the player's element of `m_iTotalScore` in the resource.
542total_score: usize,
543}
544545impl<'s> PlayerScore<'s> {
546/// The scoring state of `player`, a TF2 player, as
547 /// [`ScoreboardLayout::player`] finds it with a fresh layout.
548pub fn new(server: Server<'s>, player: Entity<'s>) -> Result<Self, ScoreError> {
549ScoreboardLayout::new().player(server, player)
550 }
551552/// Adds `delta` deaths, as [`Self::set_deaths`] sets them.
553 ///
554 /// Fails as [`Self::add_frags`] does.
555pub fn add_deaths(self, delta: i32) -> Result<bool, ScoreError> {
556let count = self.count(c"m_iDeaths", const { builtin # offset_of(sys::CPlayerState, deaths) }offset_of!(sys::CPlayerState, deaths))?;
557let deaths = count
558 .field
559 .read()
560 .checked_add(delta)
561 .ok_or(ScoreError::Overflow)?;
562563Ok(count.set(deaths))
564 }
565566/// Adds `delta` frags, as [`Self::set_frags`] sets them.
567 ///
568 /// Fails as [`Self::set_frags`] does, or with [`ScoreError::Overflow`],
569 /// writing nothing, if the sum does not fit an `i32`.
570pub fn add_frags(self, delta: i32) -> Result<bool, ScoreError> {
571let count = self.count(c"m_iFrags", const { builtin # offset_of(sys::CPlayerState, frags) }offset_of!(sys::CPlayerState, frags))?;
572let frags = count
573 .field
574 .read()
575 .checked_add(delta)
576 .ok_or(ScoreError::Overflow)?;
577578Ok(count.set(frags))
579 }
580581/// Changes the player's Score by `delta` points, through
582 /// [`Stat::KillsRuneCarrier`], as [`Self::add_stat`] does.
583 ///
584 /// Each point counts once, whatever the game mode and the player's
585 /// attributes, but the Score clients see never drops below 0:
586 /// [`Applied::shown`] is the change they see. Points taken below 0 are
587 /// owed, and absorb the points the player earns next.
588pub fn add_points(self, delta: i32, adjust: Adjust) -> Result<Applied, ScoreError> {
589self.add_stat(Stat::KillsRuneCarrier, delta, adjust)
590 }
591592/// Adds `delta` to one of the player's statistics for the session, and
593 /// for the round if [`Adjust::round`], and returns how much that moved the
594 /// scores clients see.
595 ///
596 /// The scores move by the statistic's weight in the game's
597 /// `CalcPlayerScore`, as the [module documentation](self#points) describes,
598 /// and clients receive them at once. The game does not report the change
599 /// to item servers, its Mann vs. Machine statistics, or
600 /// `player_score_changed` listeners when it is made, but a later reset of
601 /// the player's statistics reports the change as part of the Score it
602 /// drops, as the [module documentation](self#what-the-game-reports)
603 /// describes, unless [`Self::rebase`] follows the reset. Differences the
604 /// game has not sent yet are kept, and reported as usual; [`Self::rebase`]
605 /// discards them.
606 ///
607 /// Fails with [`ScoreError::Overflow`], writing nothing, if a statistic, or
608 /// a score the game compares against, would not fit an `i32`. The game sums
609 /// the statistics as `int`s too, so keep them far from that bound.
610pub fn add_stat(self, stat: Stat, delta: i32, adjust: Adjust) -> Result<Applied, ScoreError> {
611let index = stat.index();
612let session = self.read_block(StatScope::Session);
613let round = self.read_block(StatScope::Round);
614615let mut new_session = session;
616new_session.stat[index] = session.stat[index]
617 .checked_add(delta)
618 .ok_or(ScoreError::Overflow)?;
619620let new_round = if adjust.round {
621let mut new_round = round;
622623new_round.stat[index] = round.stat[index]
624 .checked_add(delta)
625 .ok_or(ScoreError::Overflow)?;
626Some(new_round)
627 } else {
628None629 };
630631// Both scores are clamped at 0, so their difference fits.
632let shown = self.score(&new_session) - self.score(&session);
633let round_shown =
634new_round.map_or(0, |new_round| self.score(&new_round) - self.score(&round));
635636// The values the game compares against keep any difference it has not
637 // sent yet, so only the change itself is added to them.
638let fields = self.reported_fields();
639let old = fields.read();
640let compensate =
641 |value: i32, shown: i32| value.checked_add(shown).ok_or(ScoreError::Overflow);
642let new = ReportedScores {
643 total: compensate(old.total, shown)?,
644 points: compensate(old.points, shown)?,
645 round_points: compensate(old.round_points, round_shown)?,
646 };
647let engine = self.server.valve_engine()?;
648649self.write_stat(StatScope::Session, index, new_session.stat[index]);
650651if let Some(new_round) = new_round {
652self.write_stat(StatScope::Round, index, new_round.stat[index]);
653 }
654655self.write_reported(fields, new, engine);
656Ok(Applied { round_shown, shown })
657 }
658659/// The player's statistics block for `scope`, which the game owns.
660fn block(self, scope: StatScope) -> *mut RoundStats {
661match scope {
662 StatScope::Session => PlayerStats::accumulated(self.stats.as_ptr()),
663 StatScope::Round => PlayerStats::current_round(self.stats.as_ptr()),
664 }
665 }
666667/// The values the player resource's column `name` can network.
668fn column_range(self, name: &'static CStr) -> Result<RangeInclusive<i32>, ScoreError> {
669let (class, _) = networking(self.resource)?;
670let layout = ArrayLayout::resolve(self.server.server_game_dll()?, class, name)?;
671672 layout.offset(name.to_str().unwrap_or_default(), self.index)?;
673674Ok(layout.range.inclusive())
675 }
676677/// The count `name` and its copy in the player state, from
678 /// `CBasePlayer`'s datamap.
679 ///
680 /// The copy's place is the datamap's embedded `pl` plus its offset in the
681 /// bindings' `CPlayerState`, `mirror`, which the game's `CPlayerState`
682 /// datamap leaves out. The datamap must agree with the bindings on the
683 /// fields it does declare.
684fn count(self, name: &'static CStr, mirror: usize) -> Result<Count<'s>, ScoreError> {
685let missing = |name: &'static CStr| ScoreError::MissingField {
686 class: "CBasePlayer",
687 name: name.to_str().unwrap_or_default(),
688 };
689690let map = self
691.player
692 .data_maps()
693 .find(|&map| map.class_name() == Some(c"CBasePlayer"))
694 .ok_or_else(|| missing(name))?;
695696let offset = map
697 .field_offset(name, sys::_fieldtypes_FIELD_INTEGER)
698 .filter(|&offset| offset < MAX_FIELD_OFFSET && offset.is_multiple_of(ELEMENT_SIZE))
699 .ok_or_else(|| missing(name))?;
700701let state = map
702 .fields()
703 .iter()
704 .filter(|field| field.fieldType == sys::_fieldtypes_FIELD_EMBEDDED)
705 .find(|field| field.name() == Some(c"pl"))
706 .filter(|field| {
707 field.embedded().next().is_some_and(|state| {
708 state.class_name() == Some(c"CPlayerState")
709 && state.field_offset(c"deadflag", sys::_fieldtypes_FIELD_BOOLEAN)
710 == Some(const { builtin # offset_of(sys::CPlayerState, deadflag) }offset_of!(sys::CPlayerState, deadflag))
711 && state.field_offset(c"v_angle", sys::_fieldtypes_FIELD_VECTOR)
712 == Some(const { builtin # offset_of(sys::CPlayerState, v_angle) }offset_of!(sys::CPlayerState, v_angle))
713 })
714 })
715 .and_then(|field| field.offset())
716 .filter(|&state| {
717 state.is_multiple_of(align_of::<sys::CPlayerState>())
718 && state
719 .checked_add(size_of::<sys::CPlayerState>())
720 .is_some_and(|end| end <= MAX_FIELD_OFFSET)
721 })
722 .ok_or_else(|| missing(c"pl"))?;
723724// SAFETY: `CBasePlayer`'s own datamap, which the player's chain
725 // includes, declares an `int` at `offset`, and embeds a `CPlayerState` at
726 // `state`, whose datamap agrees with the bindings' layout, which places
727 // an `int` at `mirror`. The game assigns both without notifying
728 // anything.
729Ok(unsafe {
730Count {
731 field: IntField::new(self.player, offset),
732 mirror: IntField::new(self.player, state + mirror),
733 }
734 })
735 }
736737/// The player's death count, `CBasePlayer::m_iDeaths`, which the
738 /// statistics panel shows.
739 ///
740 /// Fails as [`Self::set_deaths`] does.
741#[doc(alias("m_iDeaths", "DeathCount"))]
742pub fn deaths(self) -> Result<i32, ScoreError> {
743Ok(self
744 .count(c"m_iDeaths", const { builtin # offset_of(sys::CPlayerState, deaths) }offset_of!(sys::CPlayerState, deaths))?
745.field
746 .read())
747 }
748749/// The deaths clients can be shown, from the player resource's `m_iDeaths`
750 /// as the running game networks it: -2048 to 2047 in the SDK.
751pub fn deaths_range(self) -> Result<RangeInclusive<i32>, ScoreError> {
752self.column_range(c"m_iDeaths")
753 }
754755/// The player's frag count, `CBasePlayer::m_iFrags`, which the statistics
756 /// panel shows as kills.
757 ///
758 /// Fails as [`Self::set_frags`] does.
759#[doc(alias("m_iFrags", "FragCount"))]
760pub fn frags(self) -> Result<i32, ScoreError> {
761Ok(self
762 .count(c"m_iFrags", const { builtin # offset_of(sys::CPlayerState, frags) }offset_of!(sys::CPlayerState, frags))?
763.field
764 .read())
765 }
766767/// The frags clients can be shown, from the player resource's `m_iScore`
768 /// as the running game networks it: -2048 to 2047 in the SDK.
769pub fn frags_range(self) -> Result<RangeInclusive<i32>, ScoreError> {
770self.column_range(c"m_iScore")
771 }
772773/// The player's entity index, which is also their player slot.
774pub const fn index(self) -> usize {
775self.index
776 }
777778/// A copy of the game's record of the player's kills of each other player,
779 /// and theirs of the player, by entity index, since the player connected or
780 /// had their scores reset.
781 ///
782 /// The record's place in the player's statistics is inferred from the SDK's
783 /// header, as [`KillStats`] describes.
784#[doc(alias("statsKills", "KillStats_t"))]
785pub fn kill_stats(self) -> KillStats {
786// SAFETY: As for `read_block`: the record lies within the player's block,
787 // as `sdk_raw` asserts, which `GameStats::player_stats` found within the
788 // singleton, aligned for `int`s, of which the record is made.
789unsafe { PlayerStats::kills(self.stats.as_ptr()).read() }
790 }
791792/// The player's kill streak, `m_nStreaks[kTFStreak_Kills]`, which clients
793 /// show on the scoreboard and use for kill streak effects.
794 ///
795 /// Fails if the running game does not network it as an `int`.
796#[doc(alias("m_nStreaks", "kTFStreak_Kills", "GetStreak"))]
797pub fn killstreak(self) -> Result<i32, ScoreError> {
798Ok(self.streak()?.read())
799 }
800801/// The player.
802pub const fn player(self) -> Entity<'s> {
803self.player
804 }
805806/// The player's points: the session's [`Stat::KillsRuneCarrier`], which
807 /// [`Self::add_points`] changes, including the kills of rune carriers the
808 /// game counts there itself.
809pub fn points(self) -> i32 {
810self.stat(Stat::KillsRuneCarrier, StatScope::Session)
811 }
812813/// A copy of the player's statistics for `scope`.
814fn read_block(self, scope: StatScope) -> RoundStats {
815// SAFETY: `GameStats::player_stats` found the block in the game's
816 // singleton, aligned for `int`s, and its statistics blocks within it; the
817 // singleton lives as long as the game server module, which `Server::new`
818 // keeps loaded during the callback. It is copied without forming a
819 // reference, since the game writes it through its own pointers, on this
820 // thread.
821unsafe { self.block(scope).read() }
822 }
823824/// Sets the values the game compares the player's scores against, and
825 /// networks, to the scores themselves, and marks those that change for
826 /// clients, so that the game reports nothing for the scores as they are.
827 /// Returns what the game would otherwise have reported.
828 ///
829 /// It writes the player's element of the player resource's
830 /// `m_iTotalScore`, the Score the game last sent, and the session's and
831 /// round's points of the player's scoring data
832 /// (`m_Shared.m_ScoreData.m_iPoints` and `m_RoundScoreData.m_iPoints`), to
833 /// the session's and round's scores the game's `CalcPlayerScore` computes
834 /// for the player. The game's next think then finds no difference to
835 /// report as Strange "Points Scored" progress, or in Mann vs. Machine to its
836 /// statistics, and no rise of the session's points to fire
837 /// `player_score_changed` for. Clients receive the scores at once.
838 ///
839 /// Where and when to call it, and what it cannot cover, are in the
840 /// [module documentation](self#reporting-nothing). It only reads and writes
841 /// memory and runs `CalcPlayerScore`, which fires no event, so it can be
842 /// called from inside a `player_score_changed` listener, which the game
843 /// fires from the think it would report from.
844 ///
845 /// Fails, writing nothing, if the engine's interface is unavailable.
846#[doc(alias("m_iTotalScore", "kKillEaterEvent_PointsScored"))]
847pub fn rebase(self) -> Result<Rebased, ScoreError> {
848let engine = self.server.valve_engine()?;
849let total = self.total();
850let fields = self.reported_fields();
851let old = fields.read();
852853self.write_reported(
854fields,
855ReportedScores {
856total,
857 points: total,
858 round_points: self.round_total(),
859 },
860engine,
861 );
862863Ok(Rebased {
864 discarded: total.wrapping_sub(old.total),
865 points_discarded: total.wrapping_sub(old.points),
866 })
867 }
868869/// The values the game compares the player's scores against, and
870 /// networks.
871fn reported_fields(self) -> ReportedFields<'s> {
872// SAFETY: The offsets were resolved from the send tables of the classes
873 // the resource and the player had when this was made, during this
874 // callback, and are `int`s, as `ArrayLayout` and `scoring_points`
875 // checked.
876unsafe {
877ReportedFields {
878 total: IntField::new(self.resource, self.total_score),
879 points: IntField::new(self.player, self.layout.points),
880 round_points: IntField::new(self.player, self.layout.round_points),
881 }
882 }
883 }
884885/// Resets the player's unanswered deaths to every other player: the kills of
886 /// the player by each since the player last killed them, which the game
887 /// resets for one killer as the player kills them back.
888 ///
889 /// The game starts a domination as a kill makes the victim's count for the
890 /// killer, or the assister,
891 /// [`TF_KILLS_DOMINATION`](sdk_raw::tf2::dominations::TF_KILLS_DOMINATION),
892 /// before it counts the kill, so a reset before each of the player's deaths
893 /// is handled keeps every one of them from starting a domination, as
894 /// [`tf2::dominations`](crate::tf2::dominations) describes. Nothing else
895 /// reads the counts. The rest of [`Self::kill_stats`] is left alone.
896#[doc(alias("iNumKilledByUnanswered"))]
897pub fn reset_unanswered_kills(self) {
898let kills = PlayerStats::kills(self.stats.as_ptr());
899900// SAFETY: As for `kill_stats`. The counts are written without forming a
901 // reference, as the game writes them through its own pointers, on this
902 // thread.
903unsafe {
904 (&raw mut (*kills).killed_by_unanswered).write([0; MAX_PLAYERS_ARRAY_SAFE]);
905 }
906 }
907908/// The round's score, as the game computes it for the round's MVPs and
909 /// the player's round summary.
910pub fn round_total(self) -> i32 {
911self.score(&self.read_block(StatScope::Round))
912 }
913914/// Scores `stats` for the player with the game's `CalcPlayerScore`.
915fn score(self, stats: &RoundStats) -> i32 {
916// SAFETY: `Server::new` keeps the game server module, which `game_stats`
917 // was resolved in, loaded for the whole callback (condition 1), and this
918 // runs on the server's main thread within it (condition 3). `stats` is
919 // borrowed for the call, which only reads it. The player is a live
920 // `CTFPlayer` of the callback, as its datamaps showed, whose entity
921 // pointer is its `CTFPlayer` pointer, as `sdk_raw::tf2` asserts; the game
922 // reads its attributes through its items, which are entities of the
923 // callback too, and frees nothing.
924unsafe {
925self.game_stats
926 .calc_player_score(stats, self.player.as_ptr().cast::<sys::CTFPlayer>())
927 }
928 }
929930/// Sets the player's death count, as [`Self::set_frags`] sets frags.
931 ///
932 /// Fails as [`Self::set_frags`] does.
933#[doc(alias("m_iDeaths"))]
934pub fn set_deaths(self, deaths: i32) -> Result<bool, ScoreError> {
935Ok(self
936 .count(c"m_iDeaths", const { builtin # offset_of(sys::CPlayerState, deaths) }offset_of!(sys::CPlayerState, deaths))?
937.set(deaths))
938 }
939940/// Sets the player's frag count, `CBasePlayer::m_iFrags`, which the game
941 /// keeps counting from, and the copy in the player's state (`pl.frags`),
942 /// which the engine, rather than the game, reports, as in the player list
943 /// it gives Steam's server browser.
944 ///
945 /// The player resource copies the frags into the statistics panel's kills
946 /// at its next update. Clients see them wrapped into [`Self::frags_range`].
947 /// The Score is computed from [`Stat::Kills`] instead, and does not change.
948 ///
949 /// The engine's copy is only written if it agreed with the frag count
950 /// before, as the game keeps it; returns whether it was. Code that sets the
951 /// frag count alone leaves the copy behind until the game next counts a
952 /// frag.
953 ///
954 /// Fails if `CBasePlayer`'s datamap does not declare the count as an `int`
955 /// at a plausible offset, or does not embed a player state laid out as the
956 /// bindings' `CPlayerState`.
957#[doc(alias("m_iFrags"))]
958pub fn set_frags(self, frags: i32) -> Result<bool, ScoreError> {
959Ok(self
960 .count(c"m_iFrags", const { builtin # offset_of(sys::CPlayerState, frags) }offset_of!(sys::CPlayerState, frags))?
961.set(frags))
962 }
963964/// Sets the player's kill streak, which the player resource copies into
965 /// the scoreboard at its next update, and marks it changed for clients.
966 ///
967 /// The game counts on from it at the player's next kill, announcing kill
968 /// streaks by it, and resets it when the player dies or respawns.
969 ///
970 /// Fails if `killstreak` is negative or cannot be networked, or as
971 /// [`Self::killstreak`] does.
972#[doc(alias("m_nStreaks", "kTFStreak_Kills"))]
973pub fn set_killstreak(self, killstreak: i32) -> Result<(), ScoreError> {
974let (field, range) = self.streak_with_range()?;
975let engine = self.server.valve_engine()?;
976977 range.check("m_nStreaks", killstreak)?;
978field.write(killstreak);
979980let mut changes = Changes::default();
981982changes.mark(field.offset);
983changes.flush(engine, self.edict);
984Ok(())
985 }
986987/// Sets one of the player's statistics for the session to `value`, and
988 /// changes the round's by as much if [`Adjust::round`], as
989 /// [`Self::add_stat`] does.
990pub fn set_stat(self, stat: Stat, value: i32, adjust: Adjust) -> Result<Applied, ScoreError> {
991let delta = value
992 .checked_sub(self.stat(stat, StatScope::Session))
993 .ok_or(ScoreError::Overflow)?;
994995self.add_stat(stat, delta, adjust)
996 }
997998/// Sets the listed statistics of the player's block for `scope` to their
999 /// values, and the block's points, [`Stat::KillsRuneCarrier`], so that it
1000 /// scores `total`, as [`Self::set_total`] does, and returns how much that
1001 /// moved the score clients see for `scope`.
1002 ///
1003 /// As [`Self::add_stat`] does, it adds the change in the score to the values
1004 /// the game compares against, so the game reports nothing for it, and
1005 /// clients receive it at once. Setting both blocks this way, then
1006 /// [rebasing](Self::rebase), before each of the player resource's thinks
1007 /// shows exactly the statistics set, as the
1008 /// [module documentation](self#holding-a-score) describes.
1009 ///
1010 /// Fails with [`ScoreError::Overflow`], writing nothing, if `total` exceeds
1011 /// `i32::MAX`, or the points, or a score the game compares against, would
1012 /// not fit an `i32`.
1013pub fn set_stats(
1014self,
1015 scope: StatScope,
1016 stats: &[(Stat, i32)],
1017 total: u32,
1018 ) -> Result<Applied, ScoreError> {
1019let total = i32::try_from(total).map_err(|_| ScoreError::Overflow)?;
1020let old = self.read_block(scope);
1021let mut new = old;
10221023for &(stat, value) in stats {
1024 new.stat[stat.index()] = value;
1025 }
10261027let unclamped = self.unclamped(new)?;
10281029// A block scoring below 0 already shows 0.
1030if total != 0 || unclamped > 0 {
1031let points = new.stat[stat::KILLS_RUNECARRIER];
10321033new.stat[stat::KILLS_RUNECARRIER] = total
1034 .checked_sub(unclamped)
1035 .and_then(|delta| points.checked_add(delta))
1036 .ok_or(ScoreError::Overflow)?;
1037 }
10381039if new.stat == old.stat {
1040return Ok(Applied::default());
1041 }
10421043// Both scores are clamped at 0, so their difference fits.
1044let change = self.score(&new) - self.score(&old);
1045let fields = self.reported_fields();
1046let mut reported = fields.read();
1047let compensate = |value: i32| value.checked_add(change).ok_or(ScoreError::Overflow);
10481049let applied = match scope {
1050 StatScope::Session => {
1051reported.total = compensate(reported.total)?;
1052reported.points = compensate(reported.points)?;
10531054Applied {
1055 round_shown: 0,
1056 shown: change,
1057 }
1058 }
10591060 StatScope::Round => {
1061reported.round_points = compensate(reported.round_points)?;
10621063Applied {
1064 round_shown: change,
1065 shown: 0,
1066 }
1067 }
1068 };
10691070let engine = self.server.valve_engine()?;
10711072// Only the listed statistics and the points can differ.
1073for (index, (&old, &new)) in old.stat.iter().zip(&new.stat).enumerate() {
1074if new != old {
1075self.write_stat(scope, index, new);
1076 }
1077 }
10781079self.write_reported(fields, reported, engine);
1080Ok(applied)
1081 }
10821083/// Sets the player's Score to `target` exactly, by changing their points,
1084 /// as [`Self::add_points`] does, and the round's points by as much if
1085 /// [`Adjust::round`].
1086 ///
1087 /// While the player's statistics score below 0, which the game shows as 0,
1088 /// the change needed is found by scoring copies of them with more points,
1089 /// the game's scoring being linear in points.
1090 ///
1091 /// Fails as [`Self::add_points`] does, or with [`ScoreError::Overflow`],
1092 /// writing nothing, if `target` exceeds `i32::MAX`.
1093pub fn set_total(self, target: u32, adjust: Adjust) -> Result<Applied, ScoreError> {
1094let target = i32::try_from(target).map_err(|_| ScoreError::Overflow)?;
1095let unclamped = self.unclamped(self.read_block(StatScope::Session))?;
10961097if target == 0 && unclamped <= 0 {
1098return Ok(Applied::default());
1099 }
11001101let delta = target.checked_sub(unclamped).ok_or(ScoreError::Overflow)?;
11021103self.add_points(delta, adjust)
1104 }
11051106/// One of the player's statistics, for `scope`.
1107pub fn stat(self, stat: Stat, scope: StatScope) -> i32 {
1108self.read_block(scope).stat[stat.index()]
1109 }
11101111/// The player's kill streak variable.
1112fn streak(self) -> Result<IntField<'s>, ScoreError> {
1113self.streak_with_range().map(|(field, _)| field)
1114 }
11151116/// The player's kill streak variable, and the values it can hold.
1117fn streak_with_range(self) -> Result<(IntField<'s>, NetRange), ScoreError> {
1118let element = self
1119.server
1120 .server_game_dll()?
1121.entity_net_prop(self.player, c"m_nStreaks")?
1122.element(KILL_STREAK)?;
1123let range = int_range(element.prop(), element.storage(), "m_nStreaks")?;
11241125// SAFETY: The offset was resolved from the send table of the player's
1126 // class, and is an `int`, as `int_range` checked.
1127let field = unsafe { IntField::new(self.player, element.offset()) };
11281129Ok((
1130field,
1131NetRange {
1132 min: range.min.max(0),
1133 max: range.max,
1134 },
1135 ))
1136 }
11371138/// The player's Score, as the game computes and shows it: the session's
1139 /// statistics scored with the game's `CalcPlayerScore`, including the terms
1140 /// of the player's `scoreboard_minigame` attribute, if any.
1141#[doc(alias("m_iTotalScore", "CalcPlayerScore"))]
1142pub fn total(self) -> i32 {
1143self.score(&self.read_block(StatScope::Session))
1144 }
11451146/// The unclamped score of `stats`, which the game clamps at 0.
1147 ///
1148 /// Below 0, it adds points to a copy, doubling them until the copy scores
1149 /// above 0, then takes them back off: the score is linear in points.
1150fn unclamped(self, stats: RoundStats) -> Result<i32, ScoreError> {
1151let score = self.score(&stats);
11521153if score > 0 {
1154return Ok(score);
1155 }
11561157let points = stats.stat[stat::KILLS_RUNECARRIER];
1158let mut lift: i32 = 1;
11591160loop {
1161let mut probe = stats;
11621163probe.stat[stat::KILLS_RUNECARRIER] =
1164 points.checked_add(lift).ok_or(ScoreError::Overflow)?;
11651166let score = self.score(&probe);
11671168if score > 0 {
1169return Ok(score - lift);
1170 }
11711172lift = lift.checked_mul(2).ok_or(ScoreError::Overflow)?;
1173 }
1174 }
11751176/// Writes each of `new`'s values that differs from `fields`', and marks it
1177 /// changed for clients.
1178 ///
1179 /// The game compares what it computes with these, and only reports, and
1180 /// marks changed, a difference, so a value written without a mark would
1181 /// not reach clients until the game itself changes it.
1182fn write_reported(
1183self,
1184 fields: ReportedFields<'s>,
1185 new: ReportedScores,
1186 engine: ValveEngine<'_>,
1187 ) {
1188let old = fields.read();
1189let mut resource_changes = Changes::default();
1190let mut player_changes = Changes::default();
11911192if new.total != old.total {
1193fields.total.write(new.total);
1194resource_changes.mark(fields.total.offset);
1195 }
11961197if new.points != old.points {
1198fields.points.write(new.points);
1199player_changes.mark(fields.points.offset);
1200 }
12011202if new.round_points != old.round_points {
1203fields.round_points.write(new.round_points);
1204player_changes.mark(fields.round_points.offset);
1205 }
12061207resource_changes.flush(engine, self.resource_edict);
1208player_changes.flush(engine, self.edict);
1209 }
12101211/// Writes one statistic of the player's block for `scope`.
1212fn write_stat(self, scope: StatScope, index: usize, value: i32) {
1213// SAFETY: As for `read_block`, and `index` is one of the block's
1214 // `TFSTAT_TOTAL` statistics, since a `Stat` gave it. The game writes its
1215 // statistics the same way, on this thread.
1216unsafe {
1217 (&raw mut (*self.block(scope)).stat)
1218 .cast::<c_int>()
1219 .add(index)
1220 .write(value);
1221 }
1222 }
1223}
12241225/// What [`PlayerScore::rebase`] discarded: the differences the game had not
1226/// sent yet. Each wraps as the game's own `int` subtraction does.
1227#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Rebased {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "Rebased",
"discarded", &self.discarded, "points_discarded",
&&self.points_discarded)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for Rebased { }
#[automatically_derived]
impl ::core::clone::Clone for Rebased {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<i32>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for Rebased { }Copy, #[automatically_derived]
impl ::core::default::Default for Rebased {
#[inline]
fn default() -> Self {
Rebased {
discarded: ::core::default::Default::default(),
points_discarded: ::core::default::Default::default(),
}
}
}Default, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for Rebased { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Rebased {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.discarded == other.discarded &&
self.points_discarded == other.points_discarded
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for Rebased {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<i32>;
}
}Eq, #[automatically_derived]
impl ::core::hash::Hash for Rebased {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&self.discarded, state);
::core::hash::Hash::hash(&self.points_discarded, state)
}
}Hash)]
1228pub struct Rebased {
1229/// The Score minus the one the game last sent: the change the player
1230 /// resource would have reported at its next think, as Strange "Points
1231 /// Scored" progress or to Mann vs. Machine's statistics.
1232pub discarded: i32,
12331234/// The Score minus the session's points of the player's scoring data. Above
1235 /// 0, the game would have fired `player_score_changed` with this increase
1236 /// at its next think.
1237pub points_discarded: i32,
1238}
12391240/// The values of one player that the player resource compares the scores it
1241/// computes against at each think, and networks.
1242#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for ReportedFields<'s> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f,
"ReportedFields", "total", &self.total, "points", &self.points,
"round_points", &&self.round_points)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for ReportedFields<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for ReportedFields<'s> {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<IntField<'s>>;
let _: ::core::clone::AssertParamIsClone<IntField<'s>>;
let _: ::core::clone::AssertParamIsClone<IntField<'s>>;
*self
}
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for ReportedFields<'s> { }Copy)]
1243struct ReportedFields<'s> {
1244/// The player's element of the resource's `m_iTotalScore`: the Score the
1245 /// game last sent, which it reports changes from.
1246total: IntField<'s>,
12471248/// `m_Shared.m_ScoreData.m_iPoints`: the session's score the game last
1249 /// copied into the player's scoring data, whose rises fire
1250 /// `player_score_changed`.
1251points: IntField<'s>,
12521253/// `m_Shared.m_RoundScoreData.m_iPoints`: the round's score the game last
1254 /// copied into the player's scoring data.
1255round_points: IntField<'s>,
1256}
12571258impl ReportedFields<'_> {
1259fn read(self) -> ReportedScores {
1260ReportedScores {
1261 total: self.total.read(),
1262 points: self.points.read(),
1263 round_points: self.round_points.read(),
1264 }
1265 }
1266}
12671268/// The values of [`ReportedFields`].
1269#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ReportedScores {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f,
"ReportedScores", "total", &self.total, "points", &self.points,
"round_points", &&self.round_points)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for ReportedScores { }
#[automatically_derived]
impl ::core::clone::Clone for ReportedScores {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<i32>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for ReportedScores { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ReportedScores { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ReportedScores {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.total == other.total && self.points == other.points &&
self.round_points == other.round_points
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ReportedScores {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<i32>;
}
}Eq)]
1270struct ReportedScores {
1271 total: i32,
1272 points: i32,
1273 round_points: i32,
1274}
12751276/// What [`ScoreboardLayout`] keeps of the player resource it last found.
1277#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ResourceCache {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f, "ResourceCache",
"handle", &self.handle, "class", &self.class, "total_score",
&&self.total_score)
}
}Debug)]
1278struct ResourceCache {
1279 handle: EntityHandle,
12801281/// The address of the resource's server class, which `total_score`
1282 /// describes.
1283class: usize,
12841285/// The array of `m_iTotalScore`, the scoreboard's Score.
1286total_score: Result<ArrayLayout, ScoreError>,
1287}
12881289impl ResourceCache {
1290fn resolve(dll: ServerGameDll<'_>, entity: Entity<'_>, class: ServerClass<'_>) -> Self {
1291Self {
1292 handle: entity.handle(),
1293 class: class.as_ptr().addr(),
1294 total_score: ArrayLayout::resolve(dll, class, c"m_iTotalScore"),
1295 }
1296 }
1297}
12981299/// Where a team's variable lives, as resolved from the team's server class.
1300#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Scalar {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "Scalar",
"offset", &self.offset, "range", &&self.range)
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for Scalar { }
#[automatically_derived]
impl ::core::clone::Clone for Scalar {
#[inline]
fn clone(&self) -> Self {
let _: ::core::clone::AssertParamIsClone<usize>;
let _: ::core::clone::AssertParamIsClone<NetRange>;
*self
}
}Clone, #[automatically_derived]
impl ::core::marker::Copy for Scalar { }Copy)]
1301struct Scalar {
1302 offset: usize,
1303 range: NetRange,
1304}
13051306impl Scalar {
1307fn resolve(
1308 dll: ServerGameDll<'_>,
1309 class: ServerClass<'_>,
1310 stat: TeamStat,
1311 ) -> Result<Self, ScoreError> {
1312let variable = dll.net_prop(class, stat.name())?;
13131314Ok(Self {
1315 offset: variable.offset(),
1316 range: int_range(variable.prop(), variable.storage(), stat.display_name())?,
1317 })
1318 }
1319}
13201321/// Why a scoreboard operation failed.
1322#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ScoreError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
Self::GameStats(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"GameStats", &__self_0),
Self::Interface(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"Interface", &__self_0),
Self::MissingField { class: __self_0, name: __self_1 } =>
::core::fmt::Formatter::debug_struct_field2_finish(f,
"MissingField", "class", __self_0, "name", &__self_1),
Self::NetProp(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"NetProp", &__self_0),
Self::NoPlayerResource =>
::core::fmt::Formatter::write_str(f, "NoPlayerResource"),
Self::NoPlayerStats =>
::core::fmt::Formatter::write_str(f, "NoPlayerStats"),
Self::NoTeam(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "NoTeam",
&__self_0),
Self::NotAnInteger { name: __self_0 } =>
::core::fmt::Formatter::debug_struct_field1_finish(f,
"NotAnInteger", "name", &__self_0),
Self::NotTf2 => ::core::fmt::Formatter::write_str(f, "NotTf2"),
Self::NotTfPlayer =>
::core::fmt::Formatter::write_str(f, "NotTfPlayer"),
Self::OutOfRange {
name: __self_0, value: __self_1, min: __self_2, max: __self_3
} =>
::core::fmt::Formatter::debug_struct_field4_finish(f,
"OutOfRange", "name", __self_0, "value", __self_1, "min",
__self_2, "max", &__self_3),
Self::Overflow =>
::core::fmt::Formatter::write_str(f, "Overflow"),
Self::UnexpectedLayout { name: __self_0, needed: __self_1 } =>
::core::fmt::Formatter::debug_struct_field2_finish(f,
"UnexpectedLayout", "name", __self_0, "needed", &__self_1),
}
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for ScoreError {
#[inline]
fn clone(&self) -> Self {
match self {
Self::GameStats(__self_0) =>
ScoreError::GameStats(::core::clone::Clone::clone(__self_0)),
Self::Interface(__self_0) =>
ScoreError::Interface(::core::clone::Clone::clone(__self_0)),
Self::MissingField { class: __self_0, name: __self_1 } =>
ScoreError::MissingField {
class: ::core::clone::Clone::clone(__self_0),
name: ::core::clone::Clone::clone(__self_1),
},
Self::NetProp(__self_0) =>
ScoreError::NetProp(::core::clone::Clone::clone(__self_0)),
Self::NoPlayerResource => ScoreError::NoPlayerResource,
Self::NoPlayerStats => ScoreError::NoPlayerStats,
Self::NoTeam(__self_0) =>
ScoreError::NoTeam(::core::clone::Clone::clone(__self_0)),
Self::NotAnInteger { name: __self_0 } =>
ScoreError::NotAnInteger {
name: ::core::clone::Clone::clone(__self_0),
},
Self::NotTf2 => ScoreError::NotTf2,
Self::NotTfPlayer => ScoreError::NotTfPlayer,
Self::OutOfRange {
name: __self_0, value: __self_1, min: __self_2, max: __self_3
} =>
ScoreError::OutOfRange {
name: ::core::clone::Clone::clone(__self_0),
value: ::core::clone::Clone::clone(__self_1),
min: ::core::clone::Clone::clone(__self_2),
max: ::core::clone::Clone::clone(__self_3),
},
Self::Overflow => ScoreError::Overflow,
Self::UnexpectedLayout { name: __self_0, needed: __self_1 } =>
ScoreError::UnexpectedLayout {
name: ::core::clone::Clone::clone(__self_0),
needed: ::core::clone::Clone::clone(__self_1),
},
}
}
}Clone, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ScoreError { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ScoreError {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other) &&
match (self, other) {
(Self::GameStats(__self_0), Self::GameStats(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::Interface(__self_0), Self::Interface(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::MissingField { class: __self_0, name: __self_1 },
Self::MissingField { class: __arg1_0, name: __arg1_1 }) =>
__self_0 == __arg1_0 && __self_1 == __arg1_1,
(Self::NetProp(__self_0), Self::NetProp(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::NoTeam(__self_0), Self::NoTeam(__arg1_0)) =>
__self_0 == __arg1_0,
(Self::NotAnInteger { name: __self_0 }, Self::NotAnInteger {
name: __arg1_0 }) => __self_0 == __arg1_0,
(Self::OutOfRange {
name: __self_0,
value: __self_1,
min: __self_2,
max: __self_3 }, Self::OutOfRange {
name: __arg1_0,
value: __arg1_1,
min: __arg1_2,
max: __arg1_3 }) =>
__self_1 == __arg1_1 && __self_2 == __arg1_2 &&
__self_3 == __arg1_3 && __self_0 == __arg1_0,
(Self::UnexpectedLayout { name: __self_0, needed: __self_1 },
Self::UnexpectedLayout { name: __arg1_0, needed: __arg1_1 })
=> __self_0 == __arg1_0 && __self_1 == __arg1_1,
_ => true,
}
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ScoreError {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<GameStatsError>;
let _: ::core::cmp::AssertParamIsEq<InterfaceError>;
let _: ::core::cmp::AssertParamIsEq<&'static str>;
let _: ::core::cmp::AssertParamIsEq<&'static str>;
let _: ::core::cmp::AssertParamIsEq<NetPropError>;
let _: ::core::cmp::AssertParamIsEq<ScoringTeam>;
let _: ::core::cmp::AssertParamIsEq<&'static str>;
let _: ::core::cmp::AssertParamIsEq<&'static str>;
let _: ::core::cmp::AssertParamIsEq<i32>;
let _: ::core::cmp::AssertParamIsEq<&'static str>;
let _: ::core::cmp::AssertParamIsEq<usize>;
}
}Eq, #[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private21::Error for ScoreError {
fn source(&self)
->
::core::option::Option<&(dyn ::thiserror::__private21::Error +
'static)> {
use ::thiserror::__private21::AsDynError as _;
#[allow(deprecated)]
match self {
ScoreError::GameStats { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
ScoreError::Interface { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
ScoreError::MissingField { .. } => ::core::option::Option::None,
ScoreError::NetProp { 0: transparent } =>
::thiserror::__private21::Error::source(transparent.as_dyn_error()),
ScoreError::NoPlayerResource { .. } =>
::core::option::Option::None,
ScoreError::NoPlayerStats { .. } => ::core::option::Option::None,
ScoreError::NoTeam { .. } => ::core::option::Option::None,
ScoreError::NotAnInteger { .. } => ::core::option::Option::None,
ScoreError::NotTf2 { .. } => ::core::option::Option::None,
ScoreError::NotTfPlayer { .. } => ::core::option::Option::None,
ScoreError::OutOfRange { .. } => ::core::option::Option::None,
ScoreError::Overflow { .. } => ::core::option::Option::None,
ScoreError::UnexpectedLayout { .. } =>
::core::option::Option::None,
}
}
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for ScoreError {
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 {
ScoreError::GameStats(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
ScoreError::Interface(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
ScoreError::MissingField { class, name } =>
match (class.as_display(), name.as_display()) {
(__display_class, __display_name) =>
__formatter.write_fmt(format_args!("the `{0}` datamap has no usable `{1}` field",
__display_class, __display_name)),
},
ScoreError::NetProp(_0) =>
::core::fmt::Display::fmt(_0, __formatter),
ScoreError::NoPlayerResource {} =>
__formatter.write_str("no `tf_player_manager` entity exists; no level is running"),
ScoreError::NoPlayerStats {} =>
__formatter.write_str("the game keeps no statistics for the player's entity index"),
ScoreError::NoTeam(_0) =>
__formatter.write_fmt(format_args!("no `tf_team` entity has team number {0}",
_0.to_raw())),
ScoreError::NotAnInteger { name } =>
match (name.as_display(),) {
(__display_name,) =>
__formatter.write_fmt(format_args!("`{0}` is not a networked 32-bit integer",
__display_name)),
},
ScoreError::NotTf2 {} =>
__formatter.write_str("the scoreboard requires a TF2 server"),
ScoreError::NotTfPlayer {} =>
__formatter.write_str("the entity is not a TF2 player"),
ScoreError::OutOfRange { name, value, min, max } =>
match (value.as_display(), name.as_display(),
min.as_display(), max.as_display()) {
(__display_value, __display_name, __display_min,
__display_max) =>
__formatter.write_fmt(format_args!("{0} cannot be networked as `{1}`, which holds {2}..={3}",
__display_value, __display_name, __display_min,
__display_max)),
},
ScoreError::Overflow {} =>
__formatter.write_str("the change would take a value outside 32-bit integers"),
ScoreError::UnexpectedLayout { name, needed } =>
match (name.as_display(), needed.as_display()) {
(__display_name, __display_needed) =>
__formatter.write_fmt(format_args!("`{0}` is not a contiguous array of 32-bit integers with at least {1} elements",
__display_name, __display_needed)),
},
}
}
}
ScoreError
#[allow(clippy :: redundant_field_names)]
fn from(source: GameStatsError) -> Self {
ScoreError::GameStats { 0: source }
}
ScoreError
#[allow(clippy :: redundant_field_names)]
fn from(source: InterfaceError) -> Self {
ScoreError::Interface { 0: source }
}
ScoreError
#[allow(clippy :: redundant_field_names)]
fn from(source: NetPropError) -> Self { ScoreError::NetProp { 0: source } }thiserror::Error)]
1323pub enum ScoreError {
1324/// The game's statistics or score calculation could not be found or
1325 /// trusted.
1326#[error(transparent)]
1327GameStats(#[from] GameStatsError),
13281329/// A required engine or game interface is unavailable.
1330#[error(transparent)]
1331Interface(#[from] InterfaceError),
13321333/// The player's datamaps lack a field, or give it an implausible offset
1334 /// or layout.
1335#[error("the `{class}` datamap has no usable `{name}` field")]
1336MissingField {
1337/// The class whose datamap declares the field.
1338class: &'static str,
13391340/// The field's name.
1341name: &'static str,
1342 },
13431344/// A networked variable could not be found or accessed.
1345#[error(transparent)]
1346NetProp(#[from] NetPropError),
13471348/// No player resource exists, as while no level runs.
1349#[error("no `tf_player_manager` entity exists; no level is running")]
1350NoPlayerResource,
13511352/// The game keeps no statistics block for the player's entity index, as
1353 /// for an entity index above `MAX_PLAYERS`, or a player without an edict.
1354#[error("the game keeps no statistics for the player's entity index")]
1355NoPlayerStats,
13561357/// No team entity has the team's number, as while no level runs.
1358#[error("no `tf_team` entity has team number {}", .0.to_raw())]
1359NoTeam(ScoringTeam),
13601361/// A variable is not a networked `int`.
1362#[error("`{name}` is not a networked 32-bit integer")]
1363NotAnInteger {
1364/// The variable's name.
1365name: &'static str,
1366 },
13671368/// The server does not run TF2.
1369#[error("the scoreboard requires a TF2 server")]
1370NotTf2,
13711372/// The entity is not a TF2 player.
1373#[error("the entity is not a TF2 player")]
1374NotTfPlayer,
13751376/// The value cannot be networked as the variable.
1377#[error("{value} cannot be networked as `{name}`, which holds {min}..={max}")]
1378OutOfRange {
1379/// The variable's name.
1380name: &'static str,
13811382/// The value given.
1383value: i32,
13841385/// The lowest value the variable can network.
1386min: i32,
13871388/// The highest value the variable can network.
1389max: i32,
1390 },
13911392/// A statistic, score, or count would not fit a 32-bit integer.
1393#[error("the change would take a value outside 32-bit integers")]
1394Overflow,
13951396/// A player column is not laid out as an array of `int`s with an element
1397 /// for the player's slot.
1398#[error(
1399"`{name}` is not a contiguous array of 32-bit integers with at least {needed} elements"
1400)]
1401UnexpectedLayout {
1402/// The array's name.
1403name: &'static str,
14041405/// The number of elements needed.
1406needed: usize,
1407 },
1408}
14091410/// What the scoreboard resolves in the running game, kept by a plugin
1411/// between callbacks so that each [`PlayerScore`] and team change does not
1412/// resolve it again.
1413///
1414/// It holds no engine pointer: each call finds the entities again by handle,
1415/// and checks them against the server class their layout was resolved from,
1416/// so it stays sound across levels. The free functions and
1417/// [`PlayerScore::new`] use a fresh one each call.
1418#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ScoreboardLayout {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f,
"ScoreboardLayout", "game_stats", &self.game_stats, "player",
&self.player, "resource", &self.resource, "teams", &self.teams,
"_not_thread_safe", &&self._not_thread_safe)
}
}Debug, #[automatically_derived]
impl ::core::default::Default for ScoreboardLayout {
#[inline]
fn default() -> Self {
ScoreboardLayout {
game_stats: ::core::default::Default::default(),
player: ::core::default::Default::default(),
resource: ::core::default::Default::default(),
teams: ::core::default::Default::default(),
_not_thread_safe: ::core::default::Default::default(),
}
}
}Default)]
1419pub struct ScoreboardLayout {
1420/// The game's statistics, with the address of the `CreateInterface` of
1421 /// the game server module they were found in.
1422game_stats: Option<(usize, GameStats)>,
1423 player: Option<PlayerLayout>,
1424 resource: Option<ResourceCache>,
1425 teams: [Option<TeamCache>; ScoringTeam::ALL.len()],
1426 _not_thread_safe: NotThreadSafe,
1427}
14281429impl ScoreboardLayout {
1430/// An empty layout, which resolves everything at its first use.
1431pub fn new() -> Self {
1432Self::default()
1433 }
14341435/// Adds `delta` to one of a team's numbers, as [`Self::set_team_stat`]
1436 /// sets it, and returns the new value.
1437 ///
1438 /// Fails as [`Self::set_team_stat`] does, or with
1439 /// [`ScoreError::Overflow`], writing nothing.
1440pub fn add_team_stat(
1441&mut self,
1442 server: Server<'_>,
1443 team: ScoringTeam,
1444 stat: TeamStat,
1445 delta: i32,
1446 ) -> Result<i32, ScoreError> {
1447let (variable, range, edict, engine) = self.team_variable(server, team, stat)?;
1448let value = variable
1449 .read()
1450 .checked_add(delta)
1451 .ok_or(ScoreError::Overflow)?;
14521453 range.check(stat.display_name(), value)?;
1454write_team_variable(variable, value, edict, engine);
1455Ok(value)
1456 }
14571458/// The scoring state of `player`, a TF2 player.
1459 ///
1460 /// The first call in the game server module finds and tests the game's
1461 /// statistics and score calculation, as `sdk_raw`'s [`GameStats::cached`]
1462 /// describes, running the game's `CalcPlayerScore` once; later calls reuse
1463 /// them, and the layout keeps them, so that its own later calls need not
1464 /// look the module up. This relies on Source never unloading that module
1465 /// while plugins are loaded.
1466 ///
1467 /// Fails if the server does not run TF2, `player` is not a TF2 player with
1468 /// an entity index from 1 to `MAX_PLAYERS`, the game's statistics cannot
1469 /// be found or trusted, or no player resource exists or the running game
1470 /// lays out the scores differently.
1471pub fn player<'s>(
1472&mut self,
1473 server: Server<'s>,
1474 player: Entity<'s>,
1475 ) -> Result<PlayerScore<'s>, ScoreError> {
1476self.player_with(server, player, || {
1477// SAFETY: `Server::new` guarantees that the game server module, whose
1478 // factory this is, stays loaded through the callback, on the server's
1479 // main thread, where the resolution's test calls `CalcPlayerScore`. A
1480 // cached resolution for the same factory and module base was made in
1481 // this same image: Source never unloads the game server module while
1482 // plugins are loaded, since Metamod:Source and the engine unload
1483 // plugins first, and the cache, a static of this plugin, is unloaded
1484 // with it.
1485unsafe { GameStats::cached(server.game_server_factory().as_raw()) }
1486 })
1487 }
14881489/// The layout of `class`, a player's server class.
1490fn player_layout(
1491&mut self,
1492 dll: ServerGameDll<'_>,
1493 class: ServerClass<'_>,
1494 ) -> Result<PlayerLayout, ScoreError> {
1495if let Some(layout) = self.player
1496 && layout.class == class.as_ptr().addr()
1497 {
1498return Ok(layout);
1499 }
15001501let layout = PlayerLayout::resolve(dll, class)?;
15021503self.player = Some(layout);
1504Ok(layout)
1505 }
15061507/// [`Self::player`], with the game's statistics from `game_stats`, which
1508 /// is only called once the player is checked, and unless the layout kept
1509 /// them for the same game server module.
1510fn player_with<'s>(
1511&mut self,
1512 server: Server<'s>,
1513 player: Entity<'s>,
1514 game_stats: impl FnOnce() -> Result<GameStats, GameStatsError>,
1515 ) -> Result<PlayerScore<'s>, ScoreError> {
1516 check_game(server)?;
15171518if !player.has_data_map_class(c"CTFPlayer") {
1519return Err(ScoreError::NotTfPlayer);
1520 }
15211522let index = player
1523 .index()
1524 .filter(|index| (1..MAX_PLAYERS_ARRAY_SAFE).contains(index))
1525 .ok_or(ScoreError::NoPlayerStats)?;
1526let (class, edict) = networking(player)?;
1527let context = Context::new(server)?;
1528let factory = server.game_server_factory().as_raw() as usize;
15291530let game_stats = match self.game_stats {
1531Some((module, game_stats)) if module == factory => game_stats,
15321533_ => {
1534let game_stats = game_stats()?;
15351536self.game_stats = Some((factory, game_stats));
1537game_stats1538 }
1539 };
15401541let base = NonNull::new(player.as_ptr().cast::<sys::CBasePlayer>())
1542 .ok_or(ScoreError::NotTfPlayer)?;
15431544// SAFETY: As for `Self::player`'s resolution, the module stays loaded
1545 // and this runs on the main thread. Statistics the layout kept were
1546 // resolved in the module of the same factory, which is this one, since
1547 // Source never unloads the game server module while plugins are loaded.
1548 // The player is a live `CTFPlayer`, as its datamaps show, whose entity
1549 // pointer is its `CBasePlayer` pointer, as `sdk_raw::tf2` asserts, with
1550 // its live edict.
1551let stats =
1552unsafe { game_stats.player_stats(base, index) }.ok_or(ScoreError::NoPlayerStats)?;
15531554let layout = self.player_layout(context.dll, class)?;
1555let (resource, resource_edict, cache) = resource_entity(&mut self.resource, context)?;
1556let total_score = cache.total_score.clone()?.offset("m_iTotalScore", index)?;
15571558Ok(PlayerScore {
1559edict,
1560game_stats,
1561index,
1562layout,
1563player,
1564resource,
1565resource_edict,
1566server,
1567stats,
1568total_score,
1569 })
1570 }
15711572/// Sets one of a team's numbers in the game's own state, with the
1573 /// consequences [`set_team_score`] and [`set_team_flag_captures`]
1574 /// document, and marks it changed for clients.
1575 ///
1576 /// Fails if the server does not run TF2, an interface is unavailable, the
1577 /// team has no entity, the running game does not network the number as an
1578 /// `int`, or `value` is outside [`Self::team_stat_range`].
1579#[doc(alias("SetScore", "SetFlagCaptures"))]
1580pub fn set_team_stat(
1581&mut self,
1582 server: Server<'_>,
1583 team: ScoringTeam,
1584 stat: TeamStat,
1585 value: i32,
1586 ) -> Result<(), ScoreError> {
1587let (variable, range, edict, engine) = self.team_variable(server, team, stat)?;
15881589 range.check(stat.display_name(), value)?;
1590write_team_variable(variable, value, edict, engine);
1591Ok(())
1592 }
15931594/// One of a team's numbers, as the game keeps it.
1595 ///
1596 /// Fails as [`Self::set_team_stat`] does.
1597pub fn team_stat(
1598&mut self,
1599 server: Server<'_>,
1600 team: ScoringTeam,
1601 stat: TeamStat,
1602 ) -> Result<i32, ScoreError> {
1603let (variable, ..) = self.team_variable(server, team, stat)?;
16041605Ok(variable.read())
1606 }
16071608/// The values the running game can network for one of a team's numbers,
1609 /// from its networked variable's bit count and flags.
1610pub fn team_stat_range(
1611&mut self,
1612 server: Server<'_>,
1613 team: ScoringTeam,
1614 stat: TeamStat,
1615 ) -> Result<RangeInclusive<i32>, ScoreError> {
1616let (_, range, ..) = self.team_variable(server, team, stat)?;
16171618Ok(range.inclusive())
1619 }
16201621/// One of a team's numbers in its live entity, with what it can network,
1622 /// the entity's edict, and the engine.
1623fn team_variable<'s>(
1624&mut self,
1625 server: Server<'s>,
1626 team: ScoringTeam,
1627 stat: TeamStat,
1628 ) -> Result<(IntField<'s>, NetRange, Edict<'s>, ValveEngine<'s>), ScoreError> {
1629 check_game(server)?;
16301631let context = Context::new(server)?;
1632let (entity, edict, cache) = team_entity(&mut self.teams[team.index()], context, team)?;
1633let scalar = cache.fields[stat.index()].clone()?;
16341635// SAFETY: The offset was resolved from the send table of the class
1636 // `team_entity` checked the entity still has, and is an `int`, as
1637 // `int_range` checked.
1638let variable = unsafe { IntField::new(entity, scalar.offset) };
16391640Ok((variable, scalar.range, edict, context.engine))
1641 }
1642}
16431644/// A team with a score: `TF_TEAM_RED` or `TF_TEAM_BLUE`, numbered as TF2 does
1645/// (`tf_shareddefs.h:31-35`).
1646#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ScoringTeam {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
ScoringTeam::Red => "Red",
ScoringTeam::Blue => "Blue",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for ScoringTeam { }
#[automatically_derived]
impl ::core::clone::Clone for ScoringTeam {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for ScoringTeam { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ScoringTeam { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ScoringTeam {
#[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 ScoringTeam { }Eq, #[automatically_derived]
impl ::core::hash::Hash for ScoringTeam {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
state)
}
}Hash)]
1647#[repr(i32)]
1648pub enum ScoringTeam {
1649/// RED.
1650#[doc(alias("TF_TEAM_RED"))]
1651Red = TF_TEAM_RED,
16521653/// BLU.
1654#[doc(alias("TF_TEAM_BLUE"))]
1655Blue = TF_TEAM_BLUE,
1656}
16571658impl ScoringTeam {
1659/// Both teams.
1660pub const ALL: [Self; 2] = [Self::Red, Self::Blue];
16611662/// The team with this team number, or `None` for any other number.
1663pub const fn from_raw(raw: i32) -> Option<Self> {
1664match raw {
1665TF_TEAM_RED => Some(Self::Red),
1666TF_TEAM_BLUE => Some(Self::Blue),
1667_ => None,
1668 }
1669 }
16701671/// The team's position in [`Self::ALL`].
1672const fn index(self) -> usize {
1673match self {
1674Self::Red => 0,
1675Self::Blue => 1,
1676 }
1677 }
16781679/// The team's number, as `m_iTeamNum` holds it.
1680pub const fn to_raw(self) -> i32 {
1681selfas i321682 }
1683}
16841685/// One of the statistics TF2 keeps for each player, `TFStatType_t`, numbered
1686/// as `sdk_raw`'s [`stat`] indices.
1687///
1688/// The game's `CalcPlayerScore` scores some of them, with the weights of
1689/// `sdk_raw`'s `TF_SCORE_` constants, as the [module documentation](self#points)
1690/// describes. The others only appear in summaries the game sends.
1691#[doc(alias("TFStatType_t"))]
1692#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Stat {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
Stat::ShotsHit => "ShotsHit",
Stat::ShotsFired => "ShotsFired",
Stat::Kills => "Kills",
Stat::Deaths => "Deaths",
Stat::Damage => "Damage",
Stat::Captures => "Captures",
Stat::Defenses => "Defenses",
Stat::Dominations => "Dominations",
Stat::Revenge => "Revenge",
Stat::PointsScored => "PointsScored",
Stat::BuildingsDestroyed => "BuildingsDestroyed",
Stat::Headshots => "Headshots",
Stat::PlayTime => "PlayTime",
Stat::Healing => "Healing",
Stat::Invulns => "Invulns",
Stat::KillAssists => "KillAssists",
Stat::Backstabs => "Backstabs",
Stat::HealthLeached => "HealthLeached",
Stat::BuildingsBuilt => "BuildingsBuilt",
Stat::MaxSentryKills => "MaxSentryKills",
Stat::Teleports => "Teleports",
Stat::FireDamage => "FireDamage",
Stat::BonusPoints => "BonusPoints",
Stat::BlastDamage => "BlastDamage",
Stat::DamageTaken => "DamageTaken",
Stat::HealthKits => "HealthKits",
Stat::AmmoKits => "AmmoKits",
Stat::ClassChanges => "ClassChanges",
Stat::Crits => "Crits",
Stat::Suicides => "Suicides",
Stat::CurrencyCollected => "CurrencyCollected",
Stat::DamageAssist => "DamageAssist",
Stat::HealingAssist => "HealingAssist",
Stat::DamageBoss => "DamageBoss",
Stat::DamageBlocked => "DamageBlocked",
Stat::DamageRanged => "DamageRanged",
Stat::DamageRangedCritRandom => "DamageRangedCritRandom",
Stat::DamageRangedCritBoosted => "DamageRangedCritBoosted",
Stat::Revived => "Revived",
Stat::ThrowableHit => "ThrowableHit",
Stat::ThrowableKill => "ThrowableKill",
Stat::KillstreakMax => "KillstreakMax",
Stat::KillsRuneCarrier => "KillsRuneCarrier",
Stat::FlagReturns => "FlagReturns",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for Stat { }
#[automatically_derived]
impl ::core::clone::Clone for Stat {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for Stat { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for Stat { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Stat {
#[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 Stat { }Eq, #[automatically_derived]
impl ::core::hash::Hash for Stat {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
state)
}
}Hash)]
1693#[repr(usize)]
1694pub enum Stat {
1695/// Shots that hit.
1696#[doc(alias("TFSTAT_SHOTS_HIT"))]
1697ShotsHit = stat::SHOTS_HIT,
16981699/// Shots fired.
1700#[doc(alias("TFSTAT_SHOTS_FIRED"))]
1701ShotsFired = stat::SHOTS_FIRED,
17021703/// Kills, one point each, and one more with the `scoreboard_minigame`
1704 /// attribute.
1705#[doc(alias("TFSTAT_KILLS"))]
1706Kills = stat::KILLS,
17071708/// Deaths, which only score with the `scoreboard_minigame` attribute,
1709 /// minus three points each.
1710#[doc(alias("TFSTAT_DEATHS"))]
1711Deaths = stat::DEATHS,
17121713/// Damage dealt, a point per 600, or per 250 in Mann vs. Machine, together
1714 /// with the other damage and healing statistics the game scores.
1715#[doc(alias("TFSTAT_DAMAGE"))]
1716Damage = stat::DAMAGE,
17171718/// Captures of control points and flags, two points each, five in
1719 /// Mannpower, and one more with the `scoreboard_minigame` attribute.
1720#[doc(alias("TFSTAT_CAPTURES"))]
1721Captures = stat::CAPTURES,
17221723/// Defenses of control points and flags, one point each, and one more with
1724 /// the `scoreboard_minigame` attribute.
1725#[doc(alias("TFSTAT_DEFENSES"))]
1726Defenses = stat::DEFENSES,
17271728/// Dominations.
1729#[doc(alias("TFSTAT_DOMINATIONS"))]
1730Dominations = stat::DOMINATIONS,
17311732/// Revenges, one point each.
1733#[doc(alias("TFSTAT_REVENGE"))]
1734Revenge = stat::REVENGE,
17351736/// Points scored, which the game counts per life only.
1737#[doc(alias("TFSTAT_POINTSSCORED"))]
1738PointsScored = stat::POINTSSCORED,
17391740/// Enemy buildings destroyed, one point each, and one more with the
1741 /// `scoreboard_minigame` attribute.
1742#[doc(alias("TFSTAT_BUILDINGSDESTROYED"))]
1743BuildingsDestroyed = stat::BUILDINGSDESTROYED,
17441745/// Headshots, a point per two.
1746#[doc(alias("TFSTAT_HEADSHOTS"))]
1747Headshots = stat::HEADSHOTS,
17481749/// Seconds played.
1750#[doc(alias("TFSTAT_PLAYTIME"))]
1751PlayTime = stat::PLAYTIME,
17521753/// Healing given, scored as [`Self::Damage`] is, while it lies within 0 to
1754 /// 10,000,000.
1755#[doc(alias("TFSTAT_HEALING"))]
1756Healing = stat::HEALING,
17571758/// Invulnerabilities given, one point each.
1759#[doc(alias("TFSTAT_INVULNS"))]
1760Invulns = stat::INVULNS,
17611762/// Kill assists, a point per two.
1763#[doc(alias("TFSTAT_KILLASSISTS"))]
1764KillAssists = stat::KILLASSISTS,
17651766/// Backstabs, one point each.
1767#[doc(alias("TFSTAT_BACKSTABS"))]
1768Backstabs = stat::BACKSTABS,
17691770/// Health leached.
1771#[doc(alias("TFSTAT_HEALTHLEACHED"))]
1772HealthLeached = stat::HEALTHLEACHED,
17731774/// Buildings built.
1775#[doc(alias("TFSTAT_BUILDINGSBUILT"))]
1776BuildingsBuilt = stat::BUILDINGSBUILT,
17771778/// The most kills of one sentry gun.
1779#[doc(alias("TFSTAT_MAXSENTRYKILLS"))]
1780MaxSentryKills = stat::MAXSENTRYKILLS,
17811782/// Teleports given by the player's teleporters, a point per two.
1783#[doc(alias("TFSTAT_TELEPORTS"))]
1784Teleports = stat::TELEPORTS,
17851786/// Damage dealt by fire.
1787#[doc(alias("TFSTAT_FIREDAMAGE"))]
1788FireDamage = stat::FIREDAMAGE,
17891790/// Bonus points, a point per ten. The player resource's bonus column shows
1791 /// the round's.
1792#[doc(alias("TFSTAT_BONUS_POINTS"))]
1793BonusPoints = stat::BONUS_POINTS,
17941795/// Damage dealt by explosions.
1796#[doc(alias("TFSTAT_BLASTDAMAGE"))]
1797BlastDamage = stat::BLASTDAMAGE,
17981799/// Damage taken.
1800#[doc(alias("TFSTAT_DAMAGETAKEN"))]
1801DamageTaken = stat::DAMAGETAKEN,
18021803/// Health kits picked up.
1804#[doc(alias("TFSTAT_HEALTHKITS"))]
1805HealthKits = stat::HEALTHKITS,
18061807/// Ammunition kits picked up.
1808#[doc(alias("TFSTAT_AMMOKITS"))]
1809AmmoKits = stat::AMMOKITS,
18101811/// Changes of class.
1812#[doc(alias("TFSTAT_CLASSCHANGES"))]
1813ClassChanges = stat::CLASSCHANGES,
18141815/// Critical hits.
1816#[doc(alias("TFSTAT_CRITS"))]
1817Crits = stat::CRITS,
18181819/// Suicides.
1820#[doc(alias("TFSTAT_SUICIDES"))]
1821Suicides = stat::SUICIDES,
18221823/// Credits collected in Mann vs. Machine, a point per twenty.
1824#[doc(alias("TFSTAT_CURRENCY_COLLECTED"))]
1825CurrencyCollected = stat::CURRENCY_COLLECTED,
18261827/// Damage dealt by others with the player's help, scored as
1828 /// [`Self::Damage`] is.
1829#[doc(alias("TFSTAT_DAMAGE_ASSIST"))]
1830DamageAssist = stat::DAMAGE_ASSIST,
18311832/// Healing given by others with the player's help, scored as
1833 /// [`Self::Damage`] is.
1834#[doc(alias("TFSTAT_HEALING_ASSIST"))]
1835HealingAssist = stat::HEALING_ASSIST,
18361837/// Damage dealt to bosses, scored as [`Self::Damage`] is.
1838#[doc(alias("TFSTAT_DAMAGE_BOSS"))]
1839DamageBoss = stat::DAMAGE_BOSS,
18401841/// Damage blocked, scored as [`Self::Damage`] is.
1842#[doc(alias("TFSTAT_DAMAGE_BLOCKED"))]
1843DamageBlocked = stat::DAMAGE_BLOCKED,
18441845/// Damage dealt at range.
1846#[doc(alias("TFSTAT_DAMAGE_RANGED"))]
1847DamageRanged = stat::DAMAGE_RANGED,
18481849/// Damage dealt at range by random critical hits.
1850#[doc(alias("TFSTAT_DAMAGE_RANGED_CRIT_RANDOM"))]
1851DamageRangedCritRandom = stat::DAMAGE_RANGED_CRIT_RANDOM,
18521853/// Damage dealt at range by crit-boosted hits.
1854#[doc(alias("TFSTAT_DAMAGE_RANGED_CRIT_BOOSTED"))]
1855DamageRangedCritBoosted = stat::DAMAGE_RANGED_CRIT_BOOSTED,
18561857/// Revivals.
1858#[doc(alias("TFSTAT_REVIVED"))]
1859Revived = stat::REVIVED,
18601861/// Hits with throwables.
1862#[doc(alias("TFSTAT_THROWABLEHIT"))]
1863ThrowableHit = stat::THROWABLEHIT,
18641865/// Kills with throwables.
1866#[doc(alias("TFSTAT_THROWABLEKILL"))]
1867ThrowableKill = stat::THROWABLEKILL,
18681869/// The longest kill streak.
1870#[doc(alias("TFSTAT_KILLSTREAK_MAX"))]
1871KillstreakMax = stat::KILLSTREAK_MAX,
18721873/// Kills of players carrying a Mannpower rune, one point each, whatever
1874 /// the mode and the player's attributes. The game counts them for any rune
1875 /// carrier, and the scoreboard's points are kept here, as the
1876 /// [module documentation](self#points) describes.
1877#[doc(alias("TFSTAT_KILLS_RUNECARRIER"))]
1878KillsRuneCarrier = stat::KILLS_RUNECARRIER,
18791880/// Flags returned, four points each.
1881#[doc(alias("TFSTAT_FLAGRETURNS"))]
1882FlagReturns = stat::FLAGRETURNS,
1883}
18841885impl Stat {
1886/// The statistic's index in a `RoundStats_t`, its `TFStatType_t` value.
1887pub const fn index(self) -> usize {
1888selfas usize1889 }
1890}
18911892/// Which of a player's statistics to read: the session's or the current
1893/// round's.
1894#[derive(#[automatically_derived]
impl ::core::fmt::Debug for StatScope {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
StatScope::Session => "Session",
StatScope::Round => "Round",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for StatScope { }
#[automatically_derived]
impl ::core::clone::Clone for StatScope {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for StatScope { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for StatScope { }
#[automatically_derived]
impl ::core::cmp::PartialEq for StatScope {
#[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 StatScope { }Eq, #[automatically_derived]
impl ::core::hash::Hash for StatScope {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
state)
}
}Hash)]
1895pub enum StatScope {
1896/// Since the player connected or their scores were last reset,
1897 /// `statsAccumulated`, which the scoreboard's Score is computed from.
1898#[doc(alias("statsAccumulated"))]
1899Session,
19001901/// Since the current round started, `statsCurrentRound`, which the
1902 /// round's score is computed from.
1903#[doc(alias("statsCurrentRound"))]
1904Round,
1905}
19061907/// What [`ScoreboardLayout`] keeps of a team entity it last found.
1908#[derive(#[automatically_derived]
impl ::core::fmt::Debug for TeamCache {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f, "TeamCache",
"handle", &self.handle, "class", &self.class, "fields",
&&self.fields)
}
}Debug)]
1909struct TeamCache {
1910 handle: EntityHandle,
19111912/// The address of the team's server class, which `fields` describes.
1913class: usize,
19141915/// Each number's variable, in [`TeamStat::ALL`]'s order.
1916fields: [Result<Scalar, ScoreError>; TeamStat::ALL.len()],
1917}
19181919impl TeamCache {
1920fn resolve(dll: ServerGameDll<'_>, entity: Entity<'_>, class: ServerClass<'_>) -> Self {
1921Self {
1922 handle: entity.handle(),
1923 class: class.as_ptr().addr(),
1924 fields: TeamStat::ALL.map(|stat| Scalar::resolve(dll, class, stat)),
1925 }
1926 }
1927}
19281929/// A team's number on the scoreboard and HUD.
1930#[derive(#[automatically_derived]
impl ::core::fmt::Debug for TeamStat {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
TeamStat::Score => "Score",
TeamStat::FlagCaptures => "FlagCaptures",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for TeamStat { }
#[automatically_derived]
impl ::core::clone::Clone for TeamStat {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for TeamStat { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for TeamStat { }
#[automatically_derived]
impl ::core::cmp::PartialEq for TeamStat {
#[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 TeamStat { }Eq, #[automatically_derived]
impl ::core::hash::Hash for TeamStat {
#[inline]
fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
state)
}
}Hash)]
1931pub enum TeamStat {
1932/// The team's score, `CTeam::m_iScore`: rounds won in most game modes.
1933#[doc(alias("m_iScore"))]
1934Score,
19351936/// The flags the team captured this round, `CTFTeam::m_nFlagCaptures`,
1937 /// which the HUD shows instead of the score while
1938 /// `tf_flag_caps_per_round` is above 0 (`tf_hud_flagstatus.cpp:643-677`).
1939 /// PASS Time keeps its goals here, and its HUD shows them as the team's
1940 /// score (`tf_hud_passtime.cpp:200-213`).
1941#[doc(alias("m_nFlagCaptures"))]
1942FlagCaptures,
1943}
19441945impl TeamStat {
1946/// Both numbers.
1947pub const ALL: [Self; 2] = [Self::Score, Self::FlagCaptures];
19481949/// The networked variable's name, as an `&str` for errors.
1950fn display_name(self) -> &'static str {
1951self.name().to_str().unwrap_or_default()
1952 }
19531954/// The number's position in [`Self::ALL`].
1955const fn index(self) -> usize {
1956match self {
1957Self::Score => 0,
1958Self::FlagCaptures => 1,
1959 }
1960 }
19611962/// The networked variable's name in `DT_TFTeam`, such as
1963 /// `m_nFlagCaptures`.
1964pub const fn name(self) -> &'static CStr {
1965match self {
1966Self::Score => c"m_iScore",
1967Self::FlagCaptures => c"m_nFlagCaptures",
1968 }
1969 }
1970}
19711972/// Adds `delta` to a team's score, as [`set_team_score`] sets it, and returns
1973/// the new score.
1974///
1975/// Fails as [`set_team_score`] does, or with [`ScoreError::Overflow`].
1976#[doc(alias("AddScore", "m_iScore"))]
1977pub fn add_team_score(
1978 server: Server<'_>,
1979 team: ScoringTeam,
1980 delta: i32,
1981) -> Result<i32, ScoreError> {
1982ScoreboardLayout::new().add_team_stat(server, team, TeamStat::Score, delta)
1983}
19841985/// Fails with [`ScoreError::NotTf2`] unless the server runs TF2.
1986fn check_game(server: Server<'_>) -> Result<(), ScoreError> {
1987if server.game() == Game::TeamFortress2 {
1988Ok(())
1989 } else {
1990Err(ScoreError::NotTf2)
1991 }
1992}
19931994/// The values a networked integer can hold, from its property's live bit
1995/// count and flags.
1996///
1997/// `SendPropInt` replaces a bit count of 0 or less with the variable's width
1998/// (`dt_send.cpp:519-524`), which is 32 for these variables, and so does this.
1999/// Integers reuse `SPROP_NORMAL` as `SPROP_VARINT`, which encodes all 32
2000/// bits. Unsigned ranges stop at `i32::MAX`, since the variables are `int`s.
2001fn encodable_range(prop: SendProp<'_>) -> NetRange {
2002let flags = prop.flags();
2003let bits = match prop.bits() {
2004_ if flags.contains(PropFlags::NORMAL) => 32,
2005 bits @ 1..=32 => bits,
2006_ => 32,
2007 };
20082009let (min, max) = if flags.contains(PropFlags::UNSIGNED) {
2010 (0, (1_i64 << bits) - 1)
2011 } else {
2012 (-(1_i64 << (bits - 1)), (1_i64 << (bits - 1)) - 1)
2013 };
20142015NetRange {
2016 min: saturate(min),
2017 max: saturate(max),
2018 }
2019}
20202021/// Finds the `tf_team` entity of `team`.
2022fn find_team<'s>(context: Context<'s>, team: ScoringTeam) -> Result<Entity<'s>, ScoreError> {
2023let mut after = None;
20242025for _ in 0..EntityHandle::SLOTS {
2026let Some(entity) = context.tools.find_by_class_name(after, TEAM_CLASS_NAME) else {
2027break;
2028 };
20292030let number = context
2031 .dll
2032 .entity_net_prop(entity, c"m_iTeamNum")?
2033.get::<i32>(entity)?;
20342035if number == team.to_raw() {
2036return Ok(entity);
2037 }
20382039 after = Some(entity);
2040 }
20412042Err(ScoreError::NoTeam(team))
2043}
20442045/// The range of an `int` variable named `name`, or an error if it is not one.
2046fn int_range(
2047 prop: SendProp<'_>,
2048 storage: Storage,
2049 name: &'static str,
2050) -> Result<NetRange, ScoreError> {
2051if prop.kind() == PropKind::Int && storage.is_compatible(Storage::I32) {
2052Ok(encodable_range(prop))
2053 } else {
2054Err(ScoreError::NotAnInteger { name })
2055 }
2056}
20572058/// The entity a cache describes, if `handle` still finds it and it still has
2059/// the server class at `class`, with its edict.
2060fn live<'s>(
2061 tools: ServerTools<'s>,
2062 handle: EntityHandle,
2063 class: usize,
2064) -> Option<(Entity<'s>, Edict<'s>)> {
2065let entity = tools.entity_by_handle(handle)?;
2066let (found, edict) = networking(entity).ok()?;
20672068 (found.as_ptr().addr() == class).then_some((entity, edict))
2069}
20702071/// An entity's server class and edict, which every networked entity has.
2072fn networking(entity: Entity<'_>) -> Result<(ServerClass<'_>, Edict<'_>), ScoreError> {
2073match (entity.server_class(), entity.edict()) {
2074 (Some(class), Some(edict)) => Ok((class, edict)),
20752076_ => Err(NetPropError::NotNetworked {
2077 class_name: entity.class_name().to_string_lossy().into_owned(),
2078 }
2079 .into()),
2080 }
2081}
20822083/// Resets `player`'s scoring through `CTFPlayer::ResetScores`, as
2084/// `mp_restartgame`, a tournament restart, and the end of the wait for players
2085/// do for every player.
2086///
2087/// This resets the statistics the game keeps for the player, which the
2088/// "Score" column is computed from, their frags and deaths, the statistics
2089/// panel's counts, and every domination and revenge relationship they are part
2090/// of, and drops their Mann vs. Machine events (`tf_player.cpp:3286-3294`).
2091/// Unlike the game's resets of every player, it fires no
2092/// `scorestats_accumulated_reset`. The player resource then reports the drop
2093/// in their Score at its next think, as a change of Strange "Points Scored"
2094/// progress, or of Mann vs. Machine statistics, unless
2095/// [`PlayerScore::rebase`] follows this, as the
2096/// [module documentation](self#reporting-nothing) describes.
2097///
2098/// # Re-entrancy
2099///
2100/// Other code runs before this returns. Removing the relationships fires the
2101/// `remove_nemesis_relationships` game event (`tf_player.cpp:4005-4010`), so
2102/// every listener runs during the call: the game's, other plugins', map
2103/// scripts', and this plugin's own. For a human player, dropping the Mann vs.
2104/// Machine events sends the `MVMResetPlayerStats` user message
2105/// (`tf_mann_vs_machine_stats.cpp:361-373`), which plugins hooking user
2106/// messages see. Release every borrow of plugin state those may need, such as
2107/// a `RefCell` or thread-local, before calling this.
2108///
2109/// Fails if the server does not run TF2, or `player`'s datamaps do not include
2110/// `CTFPlayer`'s.
2111#[doc(alias("ResetScores"))]
2112pub fn reset_scores(server: Server<'_>, player: Entity<'_>) -> Result<(), ScoreError> {
2113 check_game(server)?;
21142115if !player.has_data_map_class(c"CTFPlayer") {
2116return Err(ScoreError::NotTfPlayer);
2117 }
21182119let player = player.as_ptr().cast::<sys::CTFPlayer>();
21202121// SAFETY: The datamaps show a `CTFPlayer`, whose entity base, and so its
2122 // primary vtable, `sdk_raw::tf2` asserts is at offset zero, and the
2123 // generated field gives `ResetScores`' slot in it under both ABIs. The
2124 // game's own code resets statistics and relationships, and frees no entity.
2125 // It also fires a game event to every listener and, for a human player,
2126 // sends a user message, both before returning; `Server::new`'s contract
2127 // requires the code those reach to free entities only through deferred
2128 // deletion, so every entity of the scope stays allocated.
2129unsafe {
2130{
let this = player;
let vtable =
::source_sdk_2013_raw::util::vtable::vtable_pointer::<sys::CTFPlayer__bindgen_vtable>(this);
((*vtable).CTFPlayer_ResetScores)(this.cast())
};vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_ResetScores());
2131 }
21322133Ok(())
2134}
21352136/// Finds the player resource again through `cache`, or anew if the cached
2137/// entity is gone or has another class.
2138fn resource_entity<'c, 's>(
2139 cache: &'c mut Option<ResourceCache>,
2140 context: Context<'s>,
2141) -> Result<(Entity<'s>, Edict<'s>, &'c ResourceCache), ScoreError> {
2142let found = cache2143 .as_ref()
2144 .and_then(|cached| live(context.tools, cached.handle, cached.class));
21452146let (entity, edict) = match found {
2147Some(found) => found,
21482149None => {
2150*cache = None;
21512152let entity = context
2153 .tools
2154 .find_by_class_name(None, RESOURCE_CLASS_NAME)
2155 .ok_or(ScoreError::NoPlayerResource)?;
2156let (class, edict) = networking(entity)?;
21572158*cache = Some(ResourceCache::resolve(context.dll, entity, class));
2159 (entity, edict)
2160 }
2161 };
21622163let cached = cache.as_ref().ok_or(ScoreError::NoPlayerResource)?;
21642165Ok((entity, edict, cached))
2166}
21672168/// Converts to `i32`, saturating.
2169fn saturate(value: i64) -> i32 {
2170i32::try_from(value).unwrap_or(if value < 0 { i32::MIN } else { i32::MAX })
2171}
21722173/// The offset of `m_iPoints` in the player's scoring data `table`, such as
2174/// `m_ScoreData`, from the player's class.
2175///
2176/// A name search finds only the first `m_iPoints`, so the variable is looked
2177/// up within the scoring data's own nested table.
2178fn scoring_points(
2179 dll: ServerGameDll<'_>,
2180 class: ServerClass<'_>,
2181 table: &'static CStr,
2182) -> Result<usize, ScoreError> {
2183let data = dll.net_prop(class, table)?;
21842185if data.prop().kind() == PropKind::DataTable {
2186for index in 0..data.element_count().unwrap_or(0) {
2187let element = data.element(index)?;
21882189if element.prop().name() == c"m_iPoints" {
2190 int_range(element.prop(), element.storage(), "m_iPoints")?;
21912192return Ok(element.offset());
2193 }
2194 }
2195 }
21962197Err(NetPropError::NotFound {
2198 table: table.to_string_lossy().into_owned(),
2199 name: "m_iPoints".to_owned(),
2200 }
2201 .into())
2202}
22032204/// Sets how many flags a team captured this round, as
2205/// `CTFTeam::SetFlagCaptures` does, and marks it changed for clients.
2206///
2207/// # Game consequences
2208///
2209/// This is the game's state, not only what clients see:
2210///
2211/// - In capture the flag, the game wins the round for a team whose captures
2212/// reach `tf_flag_caps_per_round` at its next check
2213/// (`tf_gamerules.cpp:9383-9419`).
2214/// - In PASS Time, the number is the team's score. The game wins the round for
2215/// a team whose score reaches `tf_passtime_scores_per_round` at its next
2216/// check (`tf_gamerules.cpp:9350-9378`) or when the ball next respawns
2217/// (`tf_passtime_logic.cpp:937-941`), and works out goals' points and the
2218/// winner when time runs out from it (`tf_passtime_logic.cpp:1345`,
2219/// `1706-1707`, `1785-1812`).
2220///
2221/// The game resets it when each round starts (`tf_gamerules.cpp:15089-15098`).
2222///
2223/// Fails as [`ScoreboardLayout::set_team_stat`] does; the SDK networks -128 to
2224/// 127.
2225#[doc(alias("SetFlagCaptures", "m_nFlagCaptures"))]
2226pub fn set_team_flag_captures(
2227 server: Server<'_>,
2228 team: ScoringTeam,
2229 captures: i32,
2230) -> Result<(), ScoreError> {
2231ScoreboardLayout::new().set_team_stat(server, team, TeamStat::FlagCaptures, captures)
2232}
22332234/// Sets a team's score, as `CTeam::SetScore` does, and marks it changed for
2235/// clients.
2236///
2237/// # Game consequences
2238///
2239/// This is the game's state, not only what clients see. A score reaching
2240/// `mp_winlimit`, or leading by `mp_windifference`, ends the map at the game's
2241/// next check (`tf_gamerules.cpp:9424-9470`), and arena and team balancing use
2242/// it too. Restarts and some modes reset it.
2243///
2244/// Fails as [`ScoreboardLayout::set_team_stat`] does.
2245#[doc(alias("SetScore", "m_iScore"))]
2246pub fn set_team_score(server: Server<'_>, team: ScoringTeam, score: i32) -> Result<(), ScoreError> {
2247ScoreboardLayout::new().set_team_stat(server, team, TeamStat::Score, score)
2248}
22492250/// Finds a team's entity again through `cache`, or anew if the cached entity
2251/// is gone or has another class.
2252fn team_entity<'c, 's>(
2253 cache: &'c mut Option<TeamCache>,
2254 context: Context<'s>,
2255 team: ScoringTeam,
2256) -> Result<(Entity<'s>, Edict<'s>, &'c TeamCache), ScoreError> {
2257let found = cache2258 .as_ref()
2259 .and_then(|cached| live(context.tools, cached.handle, cached.class));
22602261let (entity, edict) = match found {
2262Some(found) => found,
22632264None => {
2265*cache = None;
22662267let entity = find_team(context, team)?;
2268let (class, edict) = networking(entity)?;
22692270*cache = Some(TeamCache::resolve(context.dll, entity, class));
2271 (entity, edict)
2272 }
2273 };
22742275let cached = cache.as_ref().ok_or(ScoreError::NoTeam(team))?;
22762277Ok((entity, edict, cached))
2278}
22792280/// Writes a team's variable and marks it changed, as the game's setters do.
2281fn write_team_variable(
2282 variable: IntField<'_>,
2283 value: i32,
2284 edict: Edict<'_>,
2285 engine: ValveEngine<'_>,
2286) {
2287let mut changes = Changes::default();
22882289variable.write(value);
2290changes.mark(variable.offset);
2291changes.flush(engine, edict);
2292}