Skip to main content

cs2_gsi/model/
mod.rs

1//! Top-level GameState model.
2//!
3//! The root JSON document POSTed by Counter-Strike 2 to its Game State
4//! Integration endpoint. Every node is optional — the cfg file decides which
5//! sections CS2 includes — so missing or null sub-objects deserialize to
6//! their `Default` value rather than failing.
7
8mod auth;
9mod bomb;
10mod grenade;
11mod helpers;
12mod map;
13mod player;
14mod provider;
15mod round;
16mod tournament;
17
18pub use auth::Auth;
19pub use bomb::{Bomb, BombState};
20pub use grenade::{Grenade, GrenadeKind};
21pub use map::{Map, MapPhase, TeamStatistics};
22pub use player::{
23    MatchStats, Player, PlayerActivity, PlayerState, PlayerTeam, Weapon, WeaponKind, WeaponState,
24};
25pub use provider::Provider;
26pub use round::{BombRoundState, Round, RoundPhase, WinningTeam};
27pub use tournament::{PhaseCountdowns, TournamentDraft};
28
29use serde::{Deserialize, Serialize};
30use std::collections::BTreeMap;
31
32/// Root GSI document received from CS2.
33#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
34pub struct GameState {
35    /// Authentication block declared in the cfg file. CS2 itself never adds
36    /// a token, but tokens you wrote into the cfg appear here.
37    #[serde(default)]
38    pub auth: Auth,
39
40    /// Producer of the payload (the CS2 client itself).
41    #[serde(default)]
42    pub provider: Provider,
43
44    /// Series / map state. Absent when the player is not in a match.
45    #[serde(default)]
46    pub map: Option<Map>,
47
48    /// Per-round state. Absent outside of an active round.
49    #[serde(default)]
50    pub round: Option<Round>,
51
52    /// Player snapshot from the local client's point-of-view (the player or
53    /// the spectated entity). Absent on the main menu.
54    #[serde(default)]
55    pub player: Option<Player>,
56
57    /// All known players keyed by SteamID. Only populated when the cfg file
58    /// requests `allplayers_*` sections AND the local client is permitted to
59    /// see them (e.g. spectator / replay / GOTV).
60    #[serde(default)]
61    pub allplayers: BTreeMap<String, Player>,
62
63    /// Countdown for the current map / round phase.
64    #[serde(default, rename = "phase_countdowns")]
65    pub phase_countdowns: Option<PhaseCountdowns>,
66
67    /// Detailed bomb info.
68    #[serde(default)]
69    pub bomb: Option<Bomb>,
70
71    /// All in-flight / active grenades, keyed by entity index.
72    #[serde(default, alias = "allgrenades")]
73    pub grenades: BTreeMap<String, Grenade>,
74
75    /// Tournament-mode draft state.
76    #[serde(default, alias = "tournamentdraft")]
77    pub tournament_draft: Option<TournamentDraft>,
78
79    /// CS2 may include a `previously` block listing the values of fields
80    /// that just changed. cs2-gsi performs its own diff and does not rely on
81    /// this — it is exposed here as raw JSON for advanced consumers.
82    #[serde(default)]
83    pub previously: Option<serde_json::Value>,
84
85    /// CS2 may include an `added` block listing fields that appeared for the
86    /// first time. Exposed as raw JSON for advanced consumers.
87    #[serde(default)]
88    pub added: Option<serde_json::Value>,
89}
90
91impl GameState {
92    /// Parse a GSI payload from raw bytes.
93    pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
94        serde_json::from_slice(bytes)
95    }
96
97    /// Parse a GSI payload from a string. Provided as an inherent method for
98    /// discoverability — the conflicting [`std::str::FromStr`] impl has the
99    /// same semantics.
100    #[allow(clippy::should_implement_trait)]
101    pub fn from_str(s: &str) -> Result<Self, serde_json::Error> {
102        serde_json::from_str(s)
103    }
104
105    /// Convenience: returns `true` when the local player is dead (HP == 0
106    /// AND in the `playing` activity). Returns `false` when no player block
107    /// is present.
108    pub fn local_is_dead(&self) -> bool {
109        match &self.player {
110            Some(p) => matches!(p.activity, PlayerActivity::Playing) && p.state.health == 0,
111            None => false,
112        }
113    }
114}