csgo_gsi2/
config.rs

1use std::path::PathBuf;
2use std::collections::{HashMap, HashSet};
3use std::time::Duration;
4
5use fehler::throws;
6
7use crate::Error;
8
9/// which pieces of information to subscribe to
10///
11/// [source](https://developer.valvesoftware.com/wiki/Counter-Strike:_Global_Offensive_Game_State_Integration#List_of_Gamestate_Integrations)
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub enum Subscription {
14    /// history of round wins
15    MapRoundWins,
16    /// mode, map, phase, team scores
17    Map,
18    /// steamid
19    PlayerID,
20    /// scoreboard info
21    PlayerMatchStats,
22    /// armor, flashed, equip_value, health, etc.
23    PlayerState,
24    /// list of player weapons and weapon state
25    PlayerWeapons,
26    /// info about the game providing info
27    Provider,
28    /// round phase and the winning team
29    Round,
30    /// grenade effecttime, lifetime, owner, position, type, velocity
31    AllGrenades,
32    /// the steam id of each player
33    AllPlayersID,
34    /// the scoreboard info for each player
35    AllPlayersMatchStats,
36    /// player_position but for each player
37    AllPlayersPosition,
38    /// the player_state for each player
39    AllPlayersState,
40    /// the player_weapons for each player
41    AllPlayersWeapons,
42    /// location of the bomb, who's carrying it, dropped or not
43    Bomb,
44    /// time remaining in tenths of a second, which phase
45    PhaseCountdowns,
46    /// forward direction, position for currently spectated player
47    PlayerPosition,
48}
49
50impl Subscription {
51    /// The subscriptions available in every context
52    pub const UNRESTRICTED: &'static [Subscription] = &[
53        Subscription::MapRoundWins,
54        Subscription::Map,
55        Subscription::PlayerID,
56        Subscription::PlayerMatchStats,
57        Subscription::PlayerState,
58        Subscription::PlayerWeapons,
59        Subscription::Provider,
60        Subscription::Round,
61    ];
62
63    /// The subscriptions only available to spectators (**UNTESTED**)
64    pub const SPECTATOR_ONLY: &'static [Subscription] = &[
65        Subscription::AllGrenades,
66        Subscription::AllPlayersID,
67        Subscription::AllPlayersMatchStats,
68        Subscription::AllPlayersPosition,
69        Subscription::AllPlayersState,
70        Subscription::AllPlayersWeapons,
71        Subscription::Bomb,
72        Subscription::PhaseCountdowns,
73        Subscription::PlayerPosition,
74    ];
75}
76
77impl From<&Subscription> for Subscription {
78    fn from(x: &Subscription) -> Self {
79        *x
80    }
81}
82
83/// Builder struct for GSIConfig
84#[derive(Clone)]
85pub struct GSIConfigBuilder {
86    name: String,
87    timeout: Option<Duration>,
88    buffer: Option<Duration>,
89    throttle: Option<Duration>,
90    heartbeat: Option<Duration>,
91    auth: HashMap<String, String>,
92    precision_time: Option<u8>,
93    precision_position: Option<u8>,
94    precision_vector: Option<u8>,
95    subscriptions: HashSet<Subscription>,
96}
97
98impl GSIConfigBuilder {
99    /// Initialize the builder, with the given service name
100    pub fn new<S: Into<String>>(name: S) -> GSIConfigBuilder {
101        GSIConfigBuilder {
102            name: name.into(),
103            timeout: None,
104            buffer: None,
105            throttle: None,
106            heartbeat: None,
107            auth: HashMap::new(),
108            precision_time: None,
109            precision_position: None,
110            precision_vector: None,
111            subscriptions: HashSet::new()
112        }
113    }
114
115    /// CS:GO's client timeout for requests (default is 1.1 seconds)
116    pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
117        self.timeout = Some(timeout);
118        self
119    }
120
121    /// minimum wait between sending updates (default is 0.1 seconds)
122    pub fn buffer(&mut self, buffer: Duration) -> &mut Self {
123        self.buffer = Some(buffer);
124        self
125    }
126
127    /// minimum wait between response to one update and sending the next (default is 1.0 seconds)
128    pub fn throttle(&mut self, throttle: Duration) -> &mut Self {
129        self.throttle = Some(throttle);
130        self
131    }
132
133    /// maximum time between updates (default is 60 seconds)
134    pub fn heartbeat(&mut self, heartbeat: Duration) -> &mut Self {
135        self.heartbeat = Some(heartbeat);
136        self
137    }
138
139    /// adds an authorization key/value pair (**not currently verified**)
140    pub fn auth<S1: Into<String>, S2: Into<String>>(&mut self, key: S1, value: S2) -> &mut Self {
141        self.auth.insert(key.into(), value.into());
142        self
143    }
144
145    /// digits after the decimal point in time values (default is 2)
146    pub fn precision_time(&mut self, precision: u8) -> &mut Self {
147        self.precision_time = Some(precision);
148        self
149    }
150
151    /// digits after the decimal point in position values (default is 2)
152    pub fn precision_position(&mut self, precision: u8) -> &mut Self {
153        self.precision_position = Some(precision);
154        self
155    }
156
157    /// digits after the decimal point in vector values (default is 2)
158    pub fn precision_vector(&mut self, precision: u8) -> &mut Self {
159        self.precision_vector = Some(precision);
160        self
161    }
162
163    /// subscribe to a certain set of update info
164    pub fn subscribe(&mut self, subscription: Subscription) -> &mut Self {
165        self.subscriptions.insert(subscription);
166        self
167    }
168
169    /// subscribe to several sets of update info
170    pub fn subscribe_multiple<I: IntoIterator<Item=S>, S: Into<Subscription>>(&mut self, subscriptions: I) -> &mut Self {
171        self.subscriptions.extend(subscriptions.into_iter().map(|x| x.into()));
172        self
173    }
174
175    /// create the config object
176    pub fn build(&self) -> GSIConfig {
177        GSIConfig::from(self)
178    }
179}
180
181/// Game State Integration configuration
182pub struct GSIConfig {
183    service_name: String,
184    timeout: Duration,
185    buffer: Duration,
186    throttle: Duration,
187    heartbeat: Duration,
188    auth: HashMap<String, String>,
189    precision_time: u8,
190    precision_position: u8,
191    precision_vector: u8,
192    subscriptions: HashSet<Subscription>,
193}
194
195impl From<GSIConfigBuilder> for GSIConfig {
196    fn from(builder: GSIConfigBuilder) -> Self {
197        GSIConfig {
198            service_name: builder.name,
199            timeout: builder.timeout.unwrap_or_else(|| Duration::from_secs_f64(1.1)),
200            buffer: builder.buffer.unwrap_or_else(|| Duration::from_secs_f64(0.1)),
201            throttle: builder.throttle.unwrap_or_else(|| Duration::from_secs_f64(1.0)),
202            heartbeat: builder.throttle.unwrap_or_else(|| Duration::from_secs(60)),
203            auth: builder.auth,
204            precision_time: builder.precision_time.unwrap_or(2),
205            precision_position: builder.precision_position.unwrap_or(2),
206            precision_vector: builder.precision_vector.unwrap_or(2),
207            subscriptions: builder.subscriptions,
208        }
209    }
210}
211
212impl From<&GSIConfigBuilder> for GSIConfig {
213    fn from(builder: &GSIConfigBuilder) -> Self {
214        Self::from(builder.clone())
215    }
216}
217
218impl GSIConfig {
219    #[throws]
220    pub(crate) fn install_into<P: Into<PathBuf>>(&self, cfg_folder: P, port: u16) {
221        let mut cfg_path = cfg_folder.into();
222        cfg_path.push(&format!("gamestate_integration_{}.cfg", &self.service_name));
223        let config = config_file::ConfigFile::new(&self, port);
224        let config = vdf_serde::to_string(&config)
225            .map_err(|err| Error::ConfigInstallError { description: "failed to serialize config for installation", cause: Some(Box::new(err)) })?;
226        ::std::fs::write(cfg_path, config.as_bytes())
227            .map_err(|err| Error::ConfigInstallError { description: "failed to write config file", cause: Some(Box::new(err)) })?;
228    }
229}
230
231mod config_file {
232    use std::collections::HashMap;
233
234    use serde::Serialize;
235    use crate::config::GSIConfig;
236
237    #[derive(Serialize)]
238    struct Precision {
239        precision_time: u8,
240        precision_position: u8,
241        precision_vector: u8,
242    }
243
244    #[derive(Serialize)]
245    struct Data {
246        map_round_wins: bool,
247        map: bool,
248        player_id: bool,
249        player_match_stats: bool,
250        player_state: bool,
251        player_weapons: bool,
252        provider: bool,
253        round: bool,
254
255        // Below this line must be spectating or observing
256        allgrenades: bool,
257        allplayers_id: bool,
258        allplayers_match_stats: bool,
259        allplayers_position: bool,
260        allplayers_state: bool,
261        allplayers_weapons: bool,
262        bomb: bool,
263        phase_countdowns: bool,
264        player_position: bool,
265    }
266
267    #[derive(Serialize)]
268    #[serde(rename = "Main gamestate integration")]
269    pub struct ConfigFile {
270        uri: String,
271        timeout: f64,
272        buffer: f64,
273        throttle: f64,
274        heartbeat: f64,
275        auth: HashMap<String, String>,
276        output: Precision,
277        data: Data,
278    }
279
280    impl ConfigFile {
281        pub fn new(config: &GSIConfig, port: u16) -> Self {
282            use super::Subscription;
283            ConfigFile {
284                uri: format!("http://127.0.0.1:{}", port),
285                timeout: config.timeout.as_secs_f64(),
286                buffer: config.buffer.as_secs_f64(),
287                throttle: config.throttle.as_secs_f64(),
288                heartbeat: config.heartbeat.as_secs_f64(),
289                auth: config.auth.clone(),
290                output: Precision {
291                    precision_time: config.precision_time,
292                    precision_position: config.precision_position,
293                    precision_vector: config.precision_vector,
294                },
295                data: Data {
296                    map_round_wins: config.subscriptions.contains(&Subscription::MapRoundWins),
297                    map: config.subscriptions.contains(&Subscription::Map),
298                    player_id: config.subscriptions.contains(&Subscription::PlayerID),
299                    player_match_stats: config.subscriptions.contains(&Subscription::PlayerMatchStats),
300                    player_state: config.subscriptions.contains(&Subscription::PlayerState),
301                    player_weapons: config.subscriptions.contains(&Subscription::PlayerWeapons),
302                    provider: config.subscriptions.contains(&Subscription::Provider),
303                    round: config.subscriptions.contains(&Subscription::Round),
304                    allgrenades: config.subscriptions.contains(&Subscription::AllGrenades),
305                    allplayers_id: config.subscriptions.contains(&Subscription::AllPlayersID),
306                    allplayers_match_stats: config.subscriptions.contains(&Subscription::AllPlayersMatchStats),
307                    allplayers_position: config.subscriptions.contains(&Subscription::AllPlayersPosition),
308                    allplayers_state: config.subscriptions.contains(&Subscription::AllPlayersState),
309                    allplayers_weapons: config.subscriptions.contains(&Subscription::AllPlayersWeapons),
310                    bomb: config.subscriptions.contains(&Subscription::Bomb),
311                    phase_countdowns: config.subscriptions.contains(&Subscription::PhaseCountdowns),
312                    player_position: config.subscriptions.contains(&Subscription::PlayerPosition),
313                },
314            }
315        }
316    }
317}