Skip to main content

source_sdk_2013/tf2/scoreboard/
mod.rs

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
200
201#[cfg(test)]
202#[path = "../../tests/tf2/scoreboard.rs"]
203mod tests;
204
205use 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;
212
213use sdk_raw::tf2::scoreboard::{
214	ELEMENT_SIZE, GameStats, GameStatsError, KILL_STREAK, KillStats, MAX_PLAYERS_ARRAY_SAFE,
215	PlayerStats, RoundStats, TF_TEAM_BLUE, TF_TEAM_RED, stat,
216};
217
218use sdk_raw::vcall;
219use std::ffi::{CStr, c_int};
220use std::mem::offset_of;
221use std::ops::RangeInclusive;
222use std::ptr::NonNull;
223
224/// An exclusive bound on the datamap offsets trusted for a player's fields.
225const MAX_FIELD_OFFSET: usize = 1 << 16;
226
227/// The class name of TF2's `CTFPlayerResource` (`tf_player_resource.cpp:52`).
228const RESOURCE_CLASS_NAME: &CStr = c"tf_player_manager";
229
230/// The class name of TF2's `CTFTeam` (`tf_team.cpp:61`).
231const TEAM_CLASS_NAME: &CStr = c"tf_team";
232
233/// 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.
242	pub round: bool,
243}
244
245impl Default for Adjust {
246	fn default() -> Self {
247		Self { round: true }
248	}
249}
250
251/// 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`].
255	pub round_shown: i32,
256
257	/// 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.
260	pub shown: i32,
261}
262
263/// 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.
268	first: usize,
269
270	/// The number of elements, each [`ELEMENT_SIZE`] bytes after the last.
271	len: usize,
272
273	/// The values an element can be networked with.
274	range: NetRange,
275}
276
277impl 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.
280	fn resolve(
281		dll: ServerGameDll<'_>,
282		class: ServerClass<'_>,
283		name: &'static CStr,
284	) -> Result<Self, ScoreError> {
285		let display_name = name.to_str().unwrap_or_default();
286		let unexpected = || ScoreError::UnexpectedLayout {
287			name: display_name,
288			needed: 2,
289		};
290
291		let array = dll.net_prop(class, name)?;
292		let len = array
293			.element_count()
294			.filter(|&len| len >= 2)
295			.ok_or_else(unexpected)?;
296		let first = array.element(0)?;
297
298		for index in 0..len {
299			let element = array.element(index)?;
300			let expected = index
301				.checked_mul(ELEMENT_SIZE)
302				.and_then(|bytes| first.offset().checked_add(bytes));
303
304			if element.prop().kind() != PropKind::Int
305				|| !element.storage().is_compatible(Storage::I32)
306				|| Some(element.offset()) != expected
307			{
308				return Err(unexpected());
309			}
310		}
311
312		Ok(Self {
313			first: first.offset(),
314			len,
315			range: encodable_range(first.prop()),
316		})
317	}
318
319	/// 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.
321	fn offset(self, name: &'static str, slot: usize) -> Result<usize, ScoreError> {
322		if slot < self.len {
323			// `resolve` checked that every element's offset is this.
324			Ok(self.first + slot * ELEMENT_SIZE)
325		} else {
326			Err(ScoreError::UnexpectedLayout {
327				name,
328				needed: slot.saturating_add(1),
329			})
330		}
331	}
332}
333
334/// 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.
339	offsets: Vec<usize>,
340}
341
342impl 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.
348	fn flush(self, engine: ValveEngine<'_>, edict: Edict<'_>) {
349		if self.offsets.is_empty() {
350			return;
351		}
352
353		let offsets: Option<Vec<u16>> = self
354			.offsets
355			.iter()
356			.map(|&offset| u16::try_from(offset).ok())
357			.collect();
358
359		match offsets {
360			Some(offsets) if offsets.len() <= usize::from(MAX_CHANGE_OFFSETS) => {
361				for offset in offsets {
362					edict.state_changed(engine, offset);
363				}
364			}
365
366			_ => edict.full_state_changed(engine),
367		}
368	}
369
370	/// Marks the variable at `offset` changed.
371	fn mark(&mut self, offset: usize) {
372		if !self.offsets.contains(&offset) {
373			self.offsets.push(offset);
374		}
375	}
376}
377
378/// 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}
385
386impl<'s> Context<'s> {
387	fn new(server: Server<'s>) -> Result<Self, InterfaceError> {
388		Ok(Self {
389			dll: server.server_game_dll()?,
390			engine: server.valve_engine()?,
391			tools: server.server_tools()?,
392		})
393	}
394}
395
396/// 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`.
401	field: IntField<'s>,
402
403	/// The player state's copy, such as `pl.frags`.
404	mirror: IntField<'s>,
405}
406
407impl 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.
410	fn set(self, value: i32) -> bool {
411		let agreed = self.mirror.read() == self.field.read();
412
413		self.field.write(value);
414
415		if agreed {
416			self.mirror.write(value);
417		}
418
419		agreed
420	}
421}
422
423/// 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}
430
431impl<'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.
436	const unsafe fn new(entity: Entity<'s>, offset: usize) -> Self {
437		Self { entity, offset }
438	}
439
440	fn 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.
444		unsafe {
445			self.entity
446				.as_ptr()
447				.byte_add(self.offset)
448				.cast::<i32>()
449				.read_unaligned()
450		}
451	}
452
453	fn write(self, value: i32) {
454		// SAFETY: As for `read`. The game writes its members the same way, on
455		// the main thread.
456		unsafe {
457			self.entity
458				.as_ptr()
459				.byte_add(self.offset)
460				.cast::<i32>()
461				.write_unaligned(value);
462		}
463	}
464}
465
466/// 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}
472
473impl NetRange {
474	/// Checks that `value` can be networked as the variable `name`.
475	fn check(self, name: &'static str, value: i32) -> Result<(), ScoreError> {
476		if (self.min..=self.max).contains(&value) {
477			Ok(())
478		} else {
479			Err(ScoreError::OutOfRange {
480				name,
481				value,
482				min: self.min,
483				max: self.max,
484			})
485		}
486	}
487
488	const fn inclusive(self) -> RangeInclusive<i32> {
489		self.min..=self.max
490	}
491}
492
493/// 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.
498	class: usize,
499
500	/// `m_Shared.m_ScoreData.m_iPoints`: the session's score, as the player's
501	/// own client receives it.
502	points: usize,
503
504	/// `m_Shared.m_RoundScoreData.m_iPoints`: the round's score.
505	round_points: usize,
506}
507
508impl PlayerLayout {
509	fn resolve(dll: ServerGameDll<'_>, class: ServerClass<'_>) -> Result<Self, ScoreError> {
510		Ok(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}
517
518/// 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>,
537
538	/// The game's statistics block for the player's entity index.
539	stats: NonNull<PlayerStats>,
540
541	/// The offset of the player's element of `m_iTotalScore` in the resource.
542	total_score: usize,
543}
544
545impl<'s> PlayerScore<'s> {
546	/// The scoring state of `player`, a TF2 player, as
547	/// [`ScoreboardLayout::player`] finds it with a fresh layout.
548	pub fn new(server: Server<'s>, player: Entity<'s>) -> Result<Self, ScoreError> {
549		ScoreboardLayout::new().player(server, player)
550	}
551
552	/// Adds `delta` deaths, as [`Self::set_deaths`] sets them.
553	///
554	/// Fails as [`Self::add_frags`] does.
555	pub fn add_deaths(self, delta: i32) -> Result<bool, ScoreError> {
556		let count = self.count(c"m_iDeaths", const { builtin # offset_of(sys::CPlayerState, deaths) }offset_of!(sys::CPlayerState, deaths))?;
557		let deaths = count
558			.field
559			.read()
560			.checked_add(delta)
561			.ok_or(ScoreError::Overflow)?;
562
563		Ok(count.set(deaths))
564	}
565
566	/// 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`.
570	pub fn add_frags(self, delta: i32) -> Result<bool, ScoreError> {
571		let count = self.count(c"m_iFrags", const { builtin # offset_of(sys::CPlayerState, frags) }offset_of!(sys::CPlayerState, frags))?;
572		let frags = count
573			.field
574			.read()
575			.checked_add(delta)
576			.ok_or(ScoreError::Overflow)?;
577
578		Ok(count.set(frags))
579	}
580
581	/// 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.
588	pub fn add_points(self, delta: i32, adjust: Adjust) -> Result<Applied, ScoreError> {
589		self.add_stat(Stat::KillsRuneCarrier, delta, adjust)
590	}
591
592	/// 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.
610	pub fn add_stat(self, stat: Stat, delta: i32, adjust: Adjust) -> Result<Applied, ScoreError> {
611		let index = stat.index();
612		let session = self.read_block(StatScope::Session);
613		let round = self.read_block(StatScope::Round);
614
615		let mut new_session = session;
616		new_session.stat[index] = session.stat[index]
617			.checked_add(delta)
618			.ok_or(ScoreError::Overflow)?;
619
620		let new_round = if adjust.round {
621			let mut new_round = round;
622
623			new_round.stat[index] = round.stat[index]
624				.checked_add(delta)
625				.ok_or(ScoreError::Overflow)?;
626			Some(new_round)
627		} else {
628			None
629		};
630
631		// Both scores are clamped at 0, so their difference fits.
632		let shown = self.score(&new_session) - self.score(&session);
633		let round_shown =
634			new_round.map_or(0, |new_round| self.score(&new_round) - self.score(&round));
635
636		// The values the game compares against keep any difference it has not
637		// sent yet, so only the change itself is added to them.
638		let fields = self.reported_fields();
639		let old = fields.read();
640		let compensate =
641			|value: i32, shown: i32| value.checked_add(shown).ok_or(ScoreError::Overflow);
642		let new = ReportedScores {
643			total: compensate(old.total, shown)?,
644			points: compensate(old.points, shown)?,
645			round_points: compensate(old.round_points, round_shown)?,
646		};
647		let engine = self.server.valve_engine()?;
648
649		self.write_stat(StatScope::Session, index, new_session.stat[index]);
650
651		if let Some(new_round) = new_round {
652			self.write_stat(StatScope::Round, index, new_round.stat[index]);
653		}
654
655		self.write_reported(fields, new, engine);
656		Ok(Applied { round_shown, shown })
657	}
658
659	/// The player's statistics block for `scope`, which the game owns.
660	fn block(self, scope: StatScope) -> *mut RoundStats {
661		match scope {
662			StatScope::Session => PlayerStats::accumulated(self.stats.as_ptr()),
663			StatScope::Round => PlayerStats::current_round(self.stats.as_ptr()),
664		}
665	}
666
667	/// The values the player resource's column `name` can network.
668	fn column_range(self, name: &'static CStr) -> Result<RangeInclusive<i32>, ScoreError> {
669		let (class, _) = networking(self.resource)?;
670		let layout = ArrayLayout::resolve(self.server.server_game_dll()?, class, name)?;
671
672		layout.offset(name.to_str().unwrap_or_default(), self.index)?;
673
674		Ok(layout.range.inclusive())
675	}
676
677	/// 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.
684	fn count(self, name: &'static CStr, mirror: usize) -> Result<Count<'s>, ScoreError> {
685		let missing = |name: &'static CStr| ScoreError::MissingField {
686			class: "CBasePlayer",
687			name: name.to_str().unwrap_or_default(),
688		};
689
690		let map = self
691			.player
692			.data_maps()
693			.find(|&map| map.class_name() == Some(c"CBasePlayer"))
694			.ok_or_else(|| missing(name))?;
695
696		let 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))?;
700
701		let 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"))?;
723
724		// 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.
729		Ok(unsafe {
730			Count {
731				field: IntField::new(self.player, offset),
732				mirror: IntField::new(self.player, state + mirror),
733			}
734		})
735	}
736
737	/// 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"))]
742	pub fn deaths(self) -> Result<i32, ScoreError> {
743		Ok(self
744			.count(c"m_iDeaths", const { builtin # offset_of(sys::CPlayerState, deaths) }offset_of!(sys::CPlayerState, deaths))?
745			.field
746			.read())
747	}
748
749	/// 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.
751	pub fn deaths_range(self) -> Result<RangeInclusive<i32>, ScoreError> {
752		self.column_range(c"m_iDeaths")
753	}
754
755	/// 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"))]
760	pub fn frags(self) -> Result<i32, ScoreError> {
761		Ok(self
762			.count(c"m_iFrags", const { builtin # offset_of(sys::CPlayerState, frags) }offset_of!(sys::CPlayerState, frags))?
763			.field
764			.read())
765	}
766
767	/// 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.
769	pub fn frags_range(self) -> Result<RangeInclusive<i32>, ScoreError> {
770		self.column_range(c"m_iScore")
771	}
772
773	/// The player's entity index, which is also their player slot.
774	pub const fn index(self) -> usize {
775		self.index
776	}
777
778	/// 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"))]
785	pub 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.
789		unsafe { PlayerStats::kills(self.stats.as_ptr()).read() }
790	}
791
792	/// 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"))]
797	pub fn killstreak(self) -> Result<i32, ScoreError> {
798		Ok(self.streak()?.read())
799	}
800
801	/// The player.
802	pub const fn player(self) -> Entity<'s> {
803		self.player
804	}
805
806	/// 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.
809	pub fn points(self) -> i32 {
810		self.stat(Stat::KillsRuneCarrier, StatScope::Session)
811	}
812
813	/// A copy of the player's statistics for `scope`.
814	fn 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.
821		unsafe { self.block(scope).read() }
822	}
823
824	/// 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"))]
847	pub fn rebase(self) -> Result<Rebased, ScoreError> {
848		let engine = self.server.valve_engine()?;
849		let total = self.total();
850		let fields = self.reported_fields();
851		let old = fields.read();
852
853		self.write_reported(
854			fields,
855			ReportedScores {
856				total,
857				points: total,
858				round_points: self.round_total(),
859			},
860			engine,
861		);
862
863		Ok(Rebased {
864			discarded: total.wrapping_sub(old.total),
865			points_discarded: total.wrapping_sub(old.points),
866		})
867	}
868
869	/// The values the game compares the player's scores against, and
870	/// networks.
871	fn 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.
876		unsafe {
877			ReportedFields {
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	}
884
885	/// 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"))]
897	pub fn reset_unanswered_kills(self) {
898		let kills = PlayerStats::kills(self.stats.as_ptr());
899
900		// 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.
903		unsafe {
904			(&raw mut (*kills).killed_by_unanswered).write([0; MAX_PLAYERS_ARRAY_SAFE]);
905		}
906	}
907
908	/// The round's score, as the game computes it for the round's MVPs and
909	/// the player's round summary.
910	pub fn round_total(self) -> i32 {
911		self.score(&self.read_block(StatScope::Round))
912	}
913
914	/// Scores `stats` for the player with the game's `CalcPlayerScore`.
915	fn 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.
924		unsafe {
925			self.game_stats
926				.calc_player_score(stats, self.player.as_ptr().cast::<sys::CTFPlayer>())
927		}
928	}
929
930	/// 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"))]
934	pub fn set_deaths(self, deaths: i32) -> Result<bool, ScoreError> {
935		Ok(self
936			.count(c"m_iDeaths", const { builtin # offset_of(sys::CPlayerState, deaths) }offset_of!(sys::CPlayerState, deaths))?
937			.set(deaths))
938	}
939
940	/// 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"))]
958	pub fn set_frags(self, frags: i32) -> Result<bool, ScoreError> {
959		Ok(self
960			.count(c"m_iFrags", const { builtin # offset_of(sys::CPlayerState, frags) }offset_of!(sys::CPlayerState, frags))?
961			.set(frags))
962	}
963
964	/// 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"))]
973	pub fn set_killstreak(self, killstreak: i32) -> Result<(), ScoreError> {
974		let (field, range) = self.streak_with_range()?;
975		let engine = self.server.valve_engine()?;
976
977		range.check("m_nStreaks", killstreak)?;
978		field.write(killstreak);
979
980		let mut changes = Changes::default();
981
982		changes.mark(field.offset);
983		changes.flush(engine, self.edict);
984		Ok(())
985	}
986
987	/// 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.
990	pub fn set_stat(self, stat: Stat, value: i32, adjust: Adjust) -> Result<Applied, ScoreError> {
991		let delta = value
992			.checked_sub(self.stat(stat, StatScope::Session))
993			.ok_or(ScoreError::Overflow)?;
994
995		self.add_stat(stat, delta, adjust)
996	}
997
998	/// 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`.
1013	pub fn set_stats(
1014		self,
1015		scope: StatScope,
1016		stats: &[(Stat, i32)],
1017		total: u32,
1018	) -> Result<Applied, ScoreError> {
1019		let total = i32::try_from(total).map_err(|_| ScoreError::Overflow)?;
1020		let old = self.read_block(scope);
1021		let mut new = old;
1022
1023		for &(stat, value) in stats {
1024			new.stat[stat.index()] = value;
1025		}
1026
1027		let unclamped = self.unclamped(new)?;
1028
1029		// A block scoring below 0 already shows 0.
1030		if total != 0 || unclamped > 0 {
1031			let points = new.stat[stat::KILLS_RUNECARRIER];
1032
1033			new.stat[stat::KILLS_RUNECARRIER] = total
1034				.checked_sub(unclamped)
1035				.and_then(|delta| points.checked_add(delta))
1036				.ok_or(ScoreError::Overflow)?;
1037		}
1038
1039		if new.stat == old.stat {
1040			return Ok(Applied::default());
1041		}
1042
1043		// Both scores are clamped at 0, so their difference fits.
1044		let change = self.score(&new) - self.score(&old);
1045		let fields = self.reported_fields();
1046		let mut reported = fields.read();
1047		let compensate = |value: i32| value.checked_add(change).ok_or(ScoreError::Overflow);
1048
1049		let applied = match scope {
1050			StatScope::Session => {
1051				reported.total = compensate(reported.total)?;
1052				reported.points = compensate(reported.points)?;
1053
1054				Applied {
1055					round_shown: 0,
1056					shown: change,
1057				}
1058			}
1059
1060			StatScope::Round => {
1061				reported.round_points = compensate(reported.round_points)?;
1062
1063				Applied {
1064					round_shown: change,
1065					shown: 0,
1066				}
1067			}
1068		};
1069
1070		let engine = self.server.valve_engine()?;
1071
1072		// Only the listed statistics and the points can differ.
1073		for (index, (&old, &new)) in old.stat.iter().zip(&new.stat).enumerate() {
1074			if new != old {
1075				self.write_stat(scope, index, new);
1076			}
1077		}
1078
1079		self.write_reported(fields, reported, engine);
1080		Ok(applied)
1081	}
1082
1083	/// 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`.
1093	pub fn set_total(self, target: u32, adjust: Adjust) -> Result<Applied, ScoreError> {
1094		let target = i32::try_from(target).map_err(|_| ScoreError::Overflow)?;
1095		let unclamped = self.unclamped(self.read_block(StatScope::Session))?;
1096
1097		if target == 0 && unclamped <= 0 {
1098			return Ok(Applied::default());
1099		}
1100
1101		let delta = target.checked_sub(unclamped).ok_or(ScoreError::Overflow)?;
1102
1103		self.add_points(delta, adjust)
1104	}
1105
1106	/// One of the player's statistics, for `scope`.
1107	pub fn stat(self, stat: Stat, scope: StatScope) -> i32 {
1108		self.read_block(scope).stat[stat.index()]
1109	}
1110
1111	/// The player's kill streak variable.
1112	fn streak(self) -> Result<IntField<'s>, ScoreError> {
1113		self.streak_with_range().map(|(field, _)| field)
1114	}
1115
1116	/// The player's kill streak variable, and the values it can hold.
1117	fn streak_with_range(self) -> Result<(IntField<'s>, NetRange), ScoreError> {
1118		let element = self
1119			.server
1120			.server_game_dll()?
1121			.entity_net_prop(self.player, c"m_nStreaks")?
1122			.element(KILL_STREAK)?;
1123		let range = int_range(element.prop(), element.storage(), "m_nStreaks")?;
1124
1125		// SAFETY: The offset was resolved from the send table of the player's
1126		// class, and is an `int`, as `int_range` checked.
1127		let field = unsafe { IntField::new(self.player, element.offset()) };
1128
1129		Ok((
1130			field,
1131			NetRange {
1132				min: range.min.max(0),
1133				max: range.max,
1134			},
1135		))
1136	}
1137
1138	/// 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"))]
1142	pub fn total(self) -> i32 {
1143		self.score(&self.read_block(StatScope::Session))
1144	}
1145
1146	/// 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.
1150	fn unclamped(self, stats: RoundStats) -> Result<i32, ScoreError> {
1151		let score = self.score(&stats);
1152
1153		if score > 0 {
1154			return Ok(score);
1155		}
1156
1157		let points = stats.stat[stat::KILLS_RUNECARRIER];
1158		let mut lift: i32 = 1;
1159
1160		loop {
1161			let mut probe = stats;
1162
1163			probe.stat[stat::KILLS_RUNECARRIER] =
1164				points.checked_add(lift).ok_or(ScoreError::Overflow)?;
1165
1166			let score = self.score(&probe);
1167
1168			if score > 0 {
1169				return Ok(score - lift);
1170			}
1171
1172			lift = lift.checked_mul(2).ok_or(ScoreError::Overflow)?;
1173		}
1174	}
1175
1176	/// 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.
1182	fn write_reported(
1183		self,
1184		fields: ReportedFields<'s>,
1185		new: ReportedScores,
1186		engine: ValveEngine<'_>,
1187	) {
1188		let old = fields.read();
1189		let mut resource_changes = Changes::default();
1190		let mut player_changes = Changes::default();
1191
1192		if new.total != old.total {
1193			fields.total.write(new.total);
1194			resource_changes.mark(fields.total.offset);
1195		}
1196
1197		if new.points != old.points {
1198			fields.points.write(new.points);
1199			player_changes.mark(fields.points.offset);
1200		}
1201
1202		if new.round_points != old.round_points {
1203			fields.round_points.write(new.round_points);
1204			player_changes.mark(fields.round_points.offset);
1205		}
1206
1207		resource_changes.flush(engine, self.resource_edict);
1208		player_changes.flush(engine, self.edict);
1209	}
1210
1211	/// Writes one statistic of the player's block for `scope`.
1212	fn 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.
1216		unsafe {
1217			(&raw mut (*self.block(scope)).stat)
1218				.cast::<c_int>()
1219				.add(index)
1220				.write(value);
1221		}
1222	}
1223}
1224
1225/// 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.
1232	pub discarded: i32,
1233
1234	/// 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.
1237	pub points_discarded: i32,
1238}
1239
1240/// 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.
1246	total: IntField<'s>,
1247
1248	/// `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`.
1251	points: IntField<'s>,
1252
1253	/// `m_Shared.m_RoundScoreData.m_iPoints`: the round's score the game last
1254	/// copied into the player's scoring data.
1255	round_points: IntField<'s>,
1256}
1257
1258impl ReportedFields<'_> {
1259	fn read(self) -> ReportedScores {
1260		ReportedScores {
1261			total: self.total.read(),
1262			points: self.points.read(),
1263			round_points: self.round_points.read(),
1264		}
1265	}
1266}
1267
1268/// 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}
1275
1276/// 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,
1280
1281	/// The address of the resource's server class, which `total_score`
1282	/// describes.
1283	class: usize,
1284
1285	/// The array of `m_iTotalScore`, the scoreboard's Score.
1286	total_score: Result<ArrayLayout, ScoreError>,
1287}
1288
1289impl ResourceCache {
1290	fn resolve(dll: ServerGameDll<'_>, entity: Entity<'_>, class: ServerClass<'_>) -> Self {
1291		Self {
1292			handle: entity.handle(),
1293			class: class.as_ptr().addr(),
1294			total_score: ArrayLayout::resolve(dll, class, c"m_iTotalScore"),
1295		}
1296	}
1297}
1298
1299/// 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}
1305
1306impl Scalar {
1307	fn resolve(
1308		dll: ServerGameDll<'_>,
1309		class: ServerClass<'_>,
1310		stat: TeamStat,
1311	) -> Result<Self, ScoreError> {
1312		let variable = dll.net_prop(class, stat.name())?;
1313
1314		Ok(Self {
1315			offset: variable.offset(),
1316			range: int_range(variable.prop(), variable.storage(), stat.display_name())?,
1317		})
1318	}
1319}
1320
1321/// 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)]
1327	GameStats(#[from] GameStatsError),
1328
1329	/// A required engine or game interface is unavailable.
1330	#[error(transparent)]
1331	Interface(#[from] InterfaceError),
1332
1333	/// 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")]
1336	MissingField {
1337		/// The class whose datamap declares the field.
1338		class: &'static str,
1339
1340		/// The field's name.
1341		name: &'static str,
1342	},
1343
1344	/// A networked variable could not be found or accessed.
1345	#[error(transparent)]
1346	NetProp(#[from] NetPropError),
1347
1348	/// No player resource exists, as while no level runs.
1349	#[error("no `tf_player_manager` entity exists; no level is running")]
1350	NoPlayerResource,
1351
1352	/// 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")]
1355	NoPlayerStats,
1356
1357	/// 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())]
1359	NoTeam(ScoringTeam),
1360
1361	/// A variable is not a networked `int`.
1362	#[error("`{name}` is not a networked 32-bit integer")]
1363	NotAnInteger {
1364		/// The variable's name.
1365		name: &'static str,
1366	},
1367
1368	/// The server does not run TF2.
1369	#[error("the scoreboard requires a TF2 server")]
1370	NotTf2,
1371
1372	/// The entity is not a TF2 player.
1373	#[error("the entity is not a TF2 player")]
1374	NotTfPlayer,
1375
1376	/// The value cannot be networked as the variable.
1377	#[error("{value} cannot be networked as `{name}`, which holds {min}..={max}")]
1378	OutOfRange {
1379		/// The variable's name.
1380		name: &'static str,
1381
1382		/// The value given.
1383		value: i32,
1384
1385		/// The lowest value the variable can network.
1386		min: i32,
1387
1388		/// The highest value the variable can network.
1389		max: i32,
1390	},
1391
1392	/// A statistic, score, or count would not fit a 32-bit integer.
1393	#[error("the change would take a value outside 32-bit integers")]
1394	Overflow,
1395
1396	/// 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	)]
1401	UnexpectedLayout {
1402		/// The array's name.
1403		name: &'static str,
1404
1405		/// The number of elements needed.
1406		needed: usize,
1407	},
1408}
1409
1410/// 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.
1422	game_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}
1428
1429impl ScoreboardLayout {
1430	/// An empty layout, which resolves everything at its first use.
1431	pub fn new() -> Self {
1432		Self::default()
1433	}
1434
1435	/// 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.
1440	pub fn add_team_stat(
1441		&mut self,
1442		server: Server<'_>,
1443		team: ScoringTeam,
1444		stat: TeamStat,
1445		delta: i32,
1446	) -> Result<i32, ScoreError> {
1447		let (variable, range, edict, engine) = self.team_variable(server, team, stat)?;
1448		let value = variable
1449			.read()
1450			.checked_add(delta)
1451			.ok_or(ScoreError::Overflow)?;
1452
1453		range.check(stat.display_name(), value)?;
1454		write_team_variable(variable, value, edict, engine);
1455		Ok(value)
1456	}
1457
1458	/// 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.
1471	pub fn player<'s>(
1472		&mut self,
1473		server: Server<'s>,
1474		player: Entity<'s>,
1475	) -> Result<PlayerScore<'s>, ScoreError> {
1476		self.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.
1485			unsafe { GameStats::cached(server.game_server_factory().as_raw()) }
1486		})
1487	}
1488
1489	/// The layout of `class`, a player's server class.
1490	fn player_layout(
1491		&mut self,
1492		dll: ServerGameDll<'_>,
1493		class: ServerClass<'_>,
1494	) -> Result<PlayerLayout, ScoreError> {
1495		if let Some(layout) = self.player
1496			&& layout.class == class.as_ptr().addr()
1497		{
1498			return Ok(layout);
1499		}
1500
1501		let layout = PlayerLayout::resolve(dll, class)?;
1502
1503		self.player = Some(layout);
1504		Ok(layout)
1505	}
1506
1507	/// [`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.
1510	fn 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)?;
1517
1518		if !player.has_data_map_class(c"CTFPlayer") {
1519			return Err(ScoreError::NotTfPlayer);
1520		}
1521
1522		let index = player
1523			.index()
1524			.filter(|index| (1..MAX_PLAYERS_ARRAY_SAFE).contains(index))
1525			.ok_or(ScoreError::NoPlayerStats)?;
1526		let (class, edict) = networking(player)?;
1527		let context = Context::new(server)?;
1528		let factory = server.game_server_factory().as_raw() as usize;
1529
1530		let game_stats = match self.game_stats {
1531			Some((module, game_stats)) if module == factory => game_stats,
1532
1533			_ => {
1534				let game_stats = game_stats()?;
1535
1536				self.game_stats = Some((factory, game_stats));
1537				game_stats
1538			}
1539		};
1540
1541		let base = NonNull::new(player.as_ptr().cast::<sys::CBasePlayer>())
1542			.ok_or(ScoreError::NotTfPlayer)?;
1543
1544		// 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.
1551		let stats =
1552			unsafe { game_stats.player_stats(base, index) }.ok_or(ScoreError::NoPlayerStats)?;
1553
1554		let layout = self.player_layout(context.dll, class)?;
1555		let (resource, resource_edict, cache) = resource_entity(&mut self.resource, context)?;
1556		let total_score = cache.total_score.clone()?.offset("m_iTotalScore", index)?;
1557
1558		Ok(PlayerScore {
1559			edict,
1560			game_stats,
1561			index,
1562			layout,
1563			player,
1564			resource,
1565			resource_edict,
1566			server,
1567			stats,
1568			total_score,
1569		})
1570	}
1571
1572	/// 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"))]
1580	pub fn set_team_stat(
1581		&mut self,
1582		server: Server<'_>,
1583		team: ScoringTeam,
1584		stat: TeamStat,
1585		value: i32,
1586	) -> Result<(), ScoreError> {
1587		let (variable, range, edict, engine) = self.team_variable(server, team, stat)?;
1588
1589		range.check(stat.display_name(), value)?;
1590		write_team_variable(variable, value, edict, engine);
1591		Ok(())
1592	}
1593
1594	/// One of a team's numbers, as the game keeps it.
1595	///
1596	/// Fails as [`Self::set_team_stat`] does.
1597	pub fn team_stat(
1598		&mut self,
1599		server: Server<'_>,
1600		team: ScoringTeam,
1601		stat: TeamStat,
1602	) -> Result<i32, ScoreError> {
1603		let (variable, ..) = self.team_variable(server, team, stat)?;
1604
1605		Ok(variable.read())
1606	}
1607
1608	/// The values the running game can network for one of a team's numbers,
1609	/// from its networked variable's bit count and flags.
1610	pub fn team_stat_range(
1611		&mut self,
1612		server: Server<'_>,
1613		team: ScoringTeam,
1614		stat: TeamStat,
1615	) -> Result<RangeInclusive<i32>, ScoreError> {
1616		let (_, range, ..) = self.team_variable(server, team, stat)?;
1617
1618		Ok(range.inclusive())
1619	}
1620
1621	/// One of a team's numbers in its live entity, with what it can network,
1622	/// the entity's edict, and the engine.
1623	fn 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)?;
1630
1631		let context = Context::new(server)?;
1632		let (entity, edict, cache) = team_entity(&mut self.teams[team.index()], context, team)?;
1633		let scalar = cache.fields[stat.index()].clone()?;
1634
1635		// 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.
1638		let variable = unsafe { IntField::new(entity, scalar.offset) };
1639
1640		Ok((variable, scalar.range, edict, context.engine))
1641	}
1642}
1643
1644/// 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"))]
1651	Red = TF_TEAM_RED,
1652
1653	/// BLU.
1654	#[doc(alias("TF_TEAM_BLUE"))]
1655	Blue = TF_TEAM_BLUE,
1656}
1657
1658impl ScoringTeam {
1659	/// Both teams.
1660	pub const ALL: [Self; 2] = [Self::Red, Self::Blue];
1661
1662	/// The team with this team number, or `None` for any other number.
1663	pub const fn from_raw(raw: i32) -> Option<Self> {
1664		match raw {
1665			TF_TEAM_RED => Some(Self::Red),
1666			TF_TEAM_BLUE => Some(Self::Blue),
1667			_ => None,
1668		}
1669	}
1670
1671	/// The team's position in [`Self::ALL`].
1672	const fn index(self) -> usize {
1673		match self {
1674			Self::Red => 0,
1675			Self::Blue => 1,
1676		}
1677	}
1678
1679	/// The team's number, as `m_iTeamNum` holds it.
1680	pub const fn to_raw(self) -> i32 {
1681		self as i32
1682	}
1683}
1684
1685/// 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"))]
1697	ShotsHit = stat::SHOTS_HIT,
1698
1699	/// Shots fired.
1700	#[doc(alias("TFSTAT_SHOTS_FIRED"))]
1701	ShotsFired = stat::SHOTS_FIRED,
1702
1703	/// Kills, one point each, and one more with the `scoreboard_minigame`
1704	/// attribute.
1705	#[doc(alias("TFSTAT_KILLS"))]
1706	Kills = stat::KILLS,
1707
1708	/// Deaths, which only score with the `scoreboard_minigame` attribute,
1709	/// minus three points each.
1710	#[doc(alias("TFSTAT_DEATHS"))]
1711	Deaths = stat::DEATHS,
1712
1713	/// 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"))]
1716	Damage = stat::DAMAGE,
1717
1718	/// 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"))]
1721	Captures = stat::CAPTURES,
1722
1723	/// Defenses of control points and flags, one point each, and one more with
1724	/// the `scoreboard_minigame` attribute.
1725	#[doc(alias("TFSTAT_DEFENSES"))]
1726	Defenses = stat::DEFENSES,
1727
1728	/// Dominations.
1729	#[doc(alias("TFSTAT_DOMINATIONS"))]
1730	Dominations = stat::DOMINATIONS,
1731
1732	/// Revenges, one point each.
1733	#[doc(alias("TFSTAT_REVENGE"))]
1734	Revenge = stat::REVENGE,
1735
1736	/// Points scored, which the game counts per life only.
1737	#[doc(alias("TFSTAT_POINTSSCORED"))]
1738	PointsScored = stat::POINTSSCORED,
1739
1740	/// Enemy buildings destroyed, one point each, and one more with the
1741	/// `scoreboard_minigame` attribute.
1742	#[doc(alias("TFSTAT_BUILDINGSDESTROYED"))]
1743	BuildingsDestroyed = stat::BUILDINGSDESTROYED,
1744
1745	/// Headshots, a point per two.
1746	#[doc(alias("TFSTAT_HEADSHOTS"))]
1747	Headshots = stat::HEADSHOTS,
1748
1749	/// Seconds played.
1750	#[doc(alias("TFSTAT_PLAYTIME"))]
1751	PlayTime = stat::PLAYTIME,
1752
1753	/// Healing given, scored as [`Self::Damage`] is, while it lies within 0 to
1754	/// 10,000,000.
1755	#[doc(alias("TFSTAT_HEALING"))]
1756	Healing = stat::HEALING,
1757
1758	/// Invulnerabilities given, one point each.
1759	#[doc(alias("TFSTAT_INVULNS"))]
1760	Invulns = stat::INVULNS,
1761
1762	/// Kill assists, a point per two.
1763	#[doc(alias("TFSTAT_KILLASSISTS"))]
1764	KillAssists = stat::KILLASSISTS,
1765
1766	/// Backstabs, one point each.
1767	#[doc(alias("TFSTAT_BACKSTABS"))]
1768	Backstabs = stat::BACKSTABS,
1769
1770	/// Health leached.
1771	#[doc(alias("TFSTAT_HEALTHLEACHED"))]
1772	HealthLeached = stat::HEALTHLEACHED,
1773
1774	/// Buildings built.
1775	#[doc(alias("TFSTAT_BUILDINGSBUILT"))]
1776	BuildingsBuilt = stat::BUILDINGSBUILT,
1777
1778	/// The most kills of one sentry gun.
1779	#[doc(alias("TFSTAT_MAXSENTRYKILLS"))]
1780	MaxSentryKills = stat::MAXSENTRYKILLS,
1781
1782	/// Teleports given by the player's teleporters, a point per two.
1783	#[doc(alias("TFSTAT_TELEPORTS"))]
1784	Teleports = stat::TELEPORTS,
1785
1786	/// Damage dealt by fire.
1787	#[doc(alias("TFSTAT_FIREDAMAGE"))]
1788	FireDamage = stat::FIREDAMAGE,
1789
1790	/// Bonus points, a point per ten. The player resource's bonus column shows
1791	/// the round's.
1792	#[doc(alias("TFSTAT_BONUS_POINTS"))]
1793	BonusPoints = stat::BONUS_POINTS,
1794
1795	/// Damage dealt by explosions.
1796	#[doc(alias("TFSTAT_BLASTDAMAGE"))]
1797	BlastDamage = stat::BLASTDAMAGE,
1798
1799	/// Damage taken.
1800	#[doc(alias("TFSTAT_DAMAGETAKEN"))]
1801	DamageTaken = stat::DAMAGETAKEN,
1802
1803	/// Health kits picked up.
1804	#[doc(alias("TFSTAT_HEALTHKITS"))]
1805	HealthKits = stat::HEALTHKITS,
1806
1807	/// Ammunition kits picked up.
1808	#[doc(alias("TFSTAT_AMMOKITS"))]
1809	AmmoKits = stat::AMMOKITS,
1810
1811	/// Changes of class.
1812	#[doc(alias("TFSTAT_CLASSCHANGES"))]
1813	ClassChanges = stat::CLASSCHANGES,
1814
1815	/// Critical hits.
1816	#[doc(alias("TFSTAT_CRITS"))]
1817	Crits = stat::CRITS,
1818
1819	/// Suicides.
1820	#[doc(alias("TFSTAT_SUICIDES"))]
1821	Suicides = stat::SUICIDES,
1822
1823	/// Credits collected in Mann vs. Machine, a point per twenty.
1824	#[doc(alias("TFSTAT_CURRENCY_COLLECTED"))]
1825	CurrencyCollected = stat::CURRENCY_COLLECTED,
1826
1827	/// Damage dealt by others with the player's help, scored as
1828	/// [`Self::Damage`] is.
1829	#[doc(alias("TFSTAT_DAMAGE_ASSIST"))]
1830	DamageAssist = stat::DAMAGE_ASSIST,
1831
1832	/// Healing given by others with the player's help, scored as
1833	/// [`Self::Damage`] is.
1834	#[doc(alias("TFSTAT_HEALING_ASSIST"))]
1835	HealingAssist = stat::HEALING_ASSIST,
1836
1837	/// Damage dealt to bosses, scored as [`Self::Damage`] is.
1838	#[doc(alias("TFSTAT_DAMAGE_BOSS"))]
1839	DamageBoss = stat::DAMAGE_BOSS,
1840
1841	/// Damage blocked, scored as [`Self::Damage`] is.
1842	#[doc(alias("TFSTAT_DAMAGE_BLOCKED"))]
1843	DamageBlocked = stat::DAMAGE_BLOCKED,
1844
1845	/// Damage dealt at range.
1846	#[doc(alias("TFSTAT_DAMAGE_RANGED"))]
1847	DamageRanged = stat::DAMAGE_RANGED,
1848
1849	/// Damage dealt at range by random critical hits.
1850	#[doc(alias("TFSTAT_DAMAGE_RANGED_CRIT_RANDOM"))]
1851	DamageRangedCritRandom = stat::DAMAGE_RANGED_CRIT_RANDOM,
1852
1853	/// Damage dealt at range by crit-boosted hits.
1854	#[doc(alias("TFSTAT_DAMAGE_RANGED_CRIT_BOOSTED"))]
1855	DamageRangedCritBoosted = stat::DAMAGE_RANGED_CRIT_BOOSTED,
1856
1857	/// Revivals.
1858	#[doc(alias("TFSTAT_REVIVED"))]
1859	Revived = stat::REVIVED,
1860
1861	/// Hits with throwables.
1862	#[doc(alias("TFSTAT_THROWABLEHIT"))]
1863	ThrowableHit = stat::THROWABLEHIT,
1864
1865	/// Kills with throwables.
1866	#[doc(alias("TFSTAT_THROWABLEKILL"))]
1867	ThrowableKill = stat::THROWABLEKILL,
1868
1869	/// The longest kill streak.
1870	#[doc(alias("TFSTAT_KILLSTREAK_MAX"))]
1871	KillstreakMax = stat::KILLSTREAK_MAX,
1872
1873	/// 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"))]
1878	KillsRuneCarrier = stat::KILLS_RUNECARRIER,
1879
1880	/// Flags returned, four points each.
1881	#[doc(alias("TFSTAT_FLAGRETURNS"))]
1882	FlagReturns = stat::FLAGRETURNS,
1883}
1884
1885impl Stat {
1886	/// The statistic's index in a `RoundStats_t`, its `TFStatType_t` value.
1887	pub const fn index(self) -> usize {
1888		self as usize
1889	}
1890}
1891
1892/// 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"))]
1899	Session,
1900
1901	/// Since the current round started, `statsCurrentRound`, which the
1902	/// round's score is computed from.
1903	#[doc(alias("statsCurrentRound"))]
1904	Round,
1905}
1906
1907/// 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,
1911
1912	/// The address of the team's server class, which `fields` describes.
1913	class: usize,
1914
1915	/// Each number's variable, in [`TeamStat::ALL`]'s order.
1916	fields: [Result<Scalar, ScoreError>; TeamStat::ALL.len()],
1917}
1918
1919impl TeamCache {
1920	fn resolve(dll: ServerGameDll<'_>, entity: Entity<'_>, class: ServerClass<'_>) -> Self {
1921		Self {
1922			handle: entity.handle(),
1923			class: class.as_ptr().addr(),
1924			fields: TeamStat::ALL.map(|stat| Scalar::resolve(dll, class, stat)),
1925		}
1926	}
1927}
1928
1929/// 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"))]
1934	Score,
1935
1936	/// 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"))]
1942	FlagCaptures,
1943}
1944
1945impl TeamStat {
1946	/// Both numbers.
1947	pub const ALL: [Self; 2] = [Self::Score, Self::FlagCaptures];
1948
1949	/// The networked variable's name, as an `&str` for errors.
1950	fn display_name(self) -> &'static str {
1951		self.name().to_str().unwrap_or_default()
1952	}
1953
1954	/// The number's position in [`Self::ALL`].
1955	const fn index(self) -> usize {
1956		match self {
1957			Self::Score => 0,
1958			Self::FlagCaptures => 1,
1959		}
1960	}
1961
1962	/// The networked variable's name in `DT_TFTeam`, such as
1963	/// `m_nFlagCaptures`.
1964	pub const fn name(self) -> &'static CStr {
1965		match self {
1966			Self::Score => c"m_iScore",
1967			Self::FlagCaptures => c"m_nFlagCaptures",
1968		}
1969	}
1970}
1971
1972/// 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> {
1982	ScoreboardLayout::new().add_team_stat(server, team, TeamStat::Score, delta)
1983}
1984
1985/// Fails with [`ScoreError::NotTf2`] unless the server runs TF2.
1986fn check_game(server: Server<'_>) -> Result<(), ScoreError> {
1987	if server.game() == Game::TeamFortress2 {
1988		Ok(())
1989	} else {
1990		Err(ScoreError::NotTf2)
1991	}
1992}
1993
1994/// 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 {
2002	let flags = prop.flags();
2003	let bits = match prop.bits() {
2004		_ if flags.contains(PropFlags::NORMAL) => 32,
2005		bits @ 1..=32 => bits,
2006		_ => 32,
2007	};
2008
2009	let (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	};
2014
2015	NetRange {
2016		min: saturate(min),
2017		max: saturate(max),
2018	}
2019}
2020
2021/// Finds the `tf_team` entity of `team`.
2022fn find_team<'s>(context: Context<'s>, team: ScoringTeam) -> Result<Entity<'s>, ScoreError> {
2023	let mut after = None;
2024
2025	for _ in 0..EntityHandle::SLOTS {
2026		let Some(entity) = context.tools.find_by_class_name(after, TEAM_CLASS_NAME) else {
2027			break;
2028		};
2029
2030		let number = context
2031			.dll
2032			.entity_net_prop(entity, c"m_iTeamNum")?
2033			.get::<i32>(entity)?;
2034
2035		if number == team.to_raw() {
2036			return Ok(entity);
2037		}
2038
2039		after = Some(entity);
2040	}
2041
2042	Err(ScoreError::NoTeam(team))
2043}
2044
2045/// 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> {
2051	if prop.kind() == PropKind::Int && storage.is_compatible(Storage::I32) {
2052		Ok(encodable_range(prop))
2053	} else {
2054		Err(ScoreError::NotAnInteger { name })
2055	}
2056}
2057
2058/// 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>)> {
2065	let entity = tools.entity_by_handle(handle)?;
2066	let (found, edict) = networking(entity).ok()?;
2067
2068	(found.as_ptr().addr() == class).then_some((entity, edict))
2069}
2070
2071/// An entity's server class and edict, which every networked entity has.
2072fn networking(entity: Entity<'_>) -> Result<(ServerClass<'_>, Edict<'_>), ScoreError> {
2073	match (entity.server_class(), entity.edict()) {
2074		(Some(class), Some(edict)) => Ok((class, edict)),
2075
2076		_ => Err(NetPropError::NotNetworked {
2077			class_name: entity.class_name().to_string_lossy().into_owned(),
2078		}
2079		.into()),
2080	}
2081}
2082
2083/// 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)?;
2114
2115	if !player.has_data_map_class(c"CTFPlayer") {
2116		return Err(ScoreError::NotTfPlayer);
2117	}
2118
2119	let player = player.as_ptr().cast::<sys::CTFPlayer>();
2120
2121	// 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.
2129	unsafe {
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	}
2132
2133	Ok(())
2134}
2135
2136/// 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> {
2142	let found = cache
2143		.as_ref()
2144		.and_then(|cached| live(context.tools, cached.handle, cached.class));
2145
2146	let (entity, edict) = match found {
2147		Some(found) => found,
2148
2149		None => {
2150			*cache = None;
2151
2152			let entity = context
2153				.tools
2154				.find_by_class_name(None, RESOURCE_CLASS_NAME)
2155				.ok_or(ScoreError::NoPlayerResource)?;
2156			let (class, edict) = networking(entity)?;
2157
2158			*cache = Some(ResourceCache::resolve(context.dll, entity, class));
2159			(entity, edict)
2160		}
2161	};
2162
2163	let cached = cache.as_ref().ok_or(ScoreError::NoPlayerResource)?;
2164
2165	Ok((entity, edict, cached))
2166}
2167
2168/// Converts to `i32`, saturating.
2169fn saturate(value: i64) -> i32 {
2170	i32::try_from(value).unwrap_or(if value < 0 { i32::MIN } else { i32::MAX })
2171}
2172
2173/// 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> {
2183	let data = dll.net_prop(class, table)?;
2184
2185	if data.prop().kind() == PropKind::DataTable {
2186		for index in 0..data.element_count().unwrap_or(0) {
2187			let element = data.element(index)?;
2188
2189			if element.prop().name() == c"m_iPoints" {
2190				int_range(element.prop(), element.storage(), "m_iPoints")?;
2191
2192				return Ok(element.offset());
2193			}
2194		}
2195	}
2196
2197	Err(NetPropError::NotFound {
2198		table: table.to_string_lossy().into_owned(),
2199		name: "m_iPoints".to_owned(),
2200	}
2201	.into())
2202}
2203
2204/// 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> {
2231	ScoreboardLayout::new().set_team_stat(server, team, TeamStat::FlagCaptures, captures)
2232}
2233
2234/// 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> {
2247	ScoreboardLayout::new().set_team_stat(server, team, TeamStat::Score, score)
2248}
2249
2250/// 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> {
2257	let found = cache
2258		.as_ref()
2259		.and_then(|cached| live(context.tools, cached.handle, cached.class));
2260
2261	let (entity, edict) = match found {
2262		Some(found) => found,
2263
2264		None => {
2265			*cache = None;
2266
2267			let entity = find_team(context, team)?;
2268			let (class, edict) = networking(entity)?;
2269
2270			*cache = Some(TeamCache::resolve(context.dll, entity, class));
2271			(entity, edict)
2272		}
2273	};
2274
2275	let cached = cache.as_ref().ok_or(ScoreError::NoTeam(team))?;
2276
2277	Ok((entity, edict, cached))
2278}
2279
2280/// 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) {
2287	let mut changes = Changes::default();
2288
2289	variable.write(value);
2290	changes.mark(variable.offset);
2291	changes.flush(engine, edict);
2292}