1use std::path::PathBuf;
2use std::collections::{HashMap, HashSet};
3use std::time::Duration;
4
5use fehler::throws;
6
7use crate::Error;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub enum Subscription {
14 MapRoundWins,
16 Map,
18 PlayerID,
20 PlayerMatchStats,
22 PlayerState,
24 PlayerWeapons,
26 Provider,
28 Round,
30 AllGrenades,
32 AllPlayersID,
34 AllPlayersMatchStats,
36 AllPlayersPosition,
38 AllPlayersState,
40 AllPlayersWeapons,
42 Bomb,
44 PhaseCountdowns,
46 PlayerPosition,
48}
49
50impl Subscription {
51 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 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#[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 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 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
117 self.timeout = Some(timeout);
118 self
119 }
120
121 pub fn buffer(&mut self, buffer: Duration) -> &mut Self {
123 self.buffer = Some(buffer);
124 self
125 }
126
127 pub fn throttle(&mut self, throttle: Duration) -> &mut Self {
129 self.throttle = Some(throttle);
130 self
131 }
132
133 pub fn heartbeat(&mut self, heartbeat: Duration) -> &mut Self {
135 self.heartbeat = Some(heartbeat);
136 self
137 }
138
139 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 pub fn precision_time(&mut self, precision: u8) -> &mut Self {
147 self.precision_time = Some(precision);
148 self
149 }
150
151 pub fn precision_position(&mut self, precision: u8) -> &mut Self {
153 self.precision_position = Some(precision);
154 self
155 }
156
157 pub fn precision_vector(&mut self, precision: u8) -> &mut Self {
159 self.precision_vector = Some(precision);
160 self
161 }
162
163 pub fn subscribe(&mut self, subscription: Subscription) -> &mut Self {
165 self.subscriptions.insert(subscription);
166 self
167 }
168
169 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 pub fn build(&self) -> GSIConfig {
177 GSIConfig::from(self)
178 }
179}
180
181pub 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 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}