1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5#[derive(Clone, Debug, Serialize, Deserialize)]
6pub struct PlayerExtension {
7 pub steamid: String,
8 #[serde(default)]
9 pub name: Option<String>,
10 #[serde(default)]
11 pub avatar: Option<String>,
12 #[serde(default)]
13 pub country: Option<String>,
14 #[serde(default)]
15 pub real_name: Option<String>,
16 #[serde(default)]
17 pub extra: Option<Value>,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct TeamExtension {
22 #[serde(default)]
23 pub logo: Option<String>,
24 #[serde(default)]
25 pub map_score: Option<i32>,
26 #[serde(default)]
27 pub name: Option<String>,
28 #[serde(default)]
29 pub country: Option<String>,
30 #[serde(default)]
31 pub id: Option<String>,
32 #[serde(default)]
33 pub extra: Option<Value>,
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
37#[derive(Default)]
38pub struct MatchStats {
39 #[serde(default)]
40 pub mvps: i32,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct PlayerState {
45 #[serde(default)]
46 pub smoked: i32,
47 #[serde(default)]
48 pub round_totaldmg: i32,
49 #[serde(default)]
50 pub adr: i32,
51 #[serde(flatten)]
52 pub rest: HashMap<String, Value>,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct BasePlayer {
57 pub name: String,
58 #[serde(default)]
59 pub clan: Option<String>,
60 #[serde(default)]
61 pub observer_slot: Option<i32>,
62 #[serde(default)]
63 pub match_stats: MatchStats,
64 #[serde(default)]
65 pub weapons: HashMap<String, Value>,
66 pub state: PlayerState,
67 pub position: String,
68 pub forward: String,
69 pub team: String,
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize)]
73pub struct WeaponWithId {
74 pub id: String,
75 #[serde(flatten)]
76 pub data: Value,
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct ParsedPlayer {
81 pub steamid: String,
82 pub name: String,
83 pub default_name: String,
84 #[serde(default)]
85 pub clan: Option<String>,
86 #[serde(default)]
87 pub observer_slot: Option<i32>,
88 pub stats: MatchStats,
89 pub weapons: Vec<WeaponWithId>,
90 pub state: PlayerState,
91 pub position: Vec<f64>,
92 pub forward: Vec<f64>,
93 pub team: ParsedTeam,
94 pub avatar: Option<String>,
95 pub country: Option<String>,
96 pub real_name: Option<String>,
97 pub extra: Value,
98}
99
100#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct RawTeam {
102 pub score: i32,
103 #[serde(default)]
104 pub consecutive_round_losses: i32,
105 #[serde(default)]
106 pub timeouts_remaining: i32,
107 #[serde(default)]
108 pub matches_won_this_series: i32,
109 #[serde(default)]
110 pub name: Option<String>,
111}
112
113#[derive(Clone, Debug, Serialize, Deserialize)]
114pub struct ParsedTeam {
115 pub score: i32,
116 pub logo: Option<String>,
117 pub consecutive_round_losses: i32,
118 pub timeouts_remaining: i32,
119 pub matches_won_this_series: i32,
120 pub side: String,
121 pub name: String,
122 pub country: Option<String>,
123 pub id: Option<String>,
124 pub orientation: String,
125 pub extra: Value,
126}
127
128fn parse_vec3(s: &str) -> Vec<f64> {
129 s.split(", ")
130 .map(|p| p.parse::<f64>().unwrap_or(0.0))
131 .collect()
132}
133
134pub fn parse_player(
135 base_player: &BasePlayer,
136 steamid: &str,
137 team: ParsedTeam,
138 extensions: &[PlayerExtension],
139) -> ParsedPlayer {
140 let ext = extensions.iter().find(|p| p.steamid == steamid);
141
142 let mut state = base_player.state.clone();
143 if state.smoked == 0 {
144 state.smoked = 0;
145 }
146 state.adr = 0;
147
148 let weapons = base_player
149 .weapons
150 .iter()
151 .map(|(id, weapon)| WeaponWithId {
152 id: id.clone(),
153 data: weapon.clone(),
154 })
155 .collect::<Vec<_>>();
156
157 ParsedPlayer {
158 steamid: steamid.to_string(),
159 name: ext.and_then(|e| e.name.clone()).unwrap_or_else(|| base_player.name.clone()),
160 default_name: base_player.name.clone(),
161 clan: base_player.clan.clone(),
162 observer_slot: base_player.observer_slot,
163 stats: base_player.match_stats.clone(),
164 weapons,
165 state,
166 position: parse_vec3(&base_player.position),
167 forward: parse_vec3(&base_player.forward),
168 team,
169 avatar: ext.and_then(|e| e.avatar.clone()),
170 country: ext.and_then(|e| e.country.clone()),
171 real_name: ext.and_then(|e| e.real_name.clone()),
172 extra: ext
173 .and_then(|e| e.extra.clone())
174 .unwrap_or_else(|| Value::Object(Default::default())),
175 }
176}
177
178pub fn parse_team(
179 team: &RawTeam,
180 orientation: &str,
181 side: &str,
182 ext: Option<&TeamExtension>,
183) -> ParsedTeam {
184 let default_name = if side == "CT" {
185 "Counter-Terrorists"
186 } else {
187 "Terrorists"
188 };
189
190 let name = ext
191 .and_then(|e| e.name.clone())
192 .or_else(|| team.name.clone())
193 .unwrap_or_else(|| default_name.to_string());
194
195 let matches_won = ext
196 .and_then(|e| e.map_score)
197 .unwrap_or(team.matches_won_this_series);
198
199 ParsedTeam {
200 score: team.score,
201 logo: ext.and_then(|e| e.logo.clone()),
202 consecutive_round_losses: team.consecutive_round_losses,
203 timeouts_remaining: team.timeouts_remaining,
204 matches_won_this_series: matches_won,
205 side: side.to_string(),
206 name,
207 country: ext.and_then(|e| e.country.clone()),
208 id: ext.and_then(|e| e.id.clone()),
209 orientation: orientation.to_string(),
210 extra: ext
211 .and_then(|e| e.extra.clone())
212 .unwrap_or_else(|| Value::Object(Default::default())),
213 }
214}
215
216pub fn get_half_from_round(round: i32, regulation_mr: i32, mr: i32) -> i32 {
217 let current_half;
218 if round <= 2 * regulation_mr {
219 current_half = if round <= regulation_mr { 1 } else { 2 };
220 } else {
221 let round_in_ot = (round - (2 * regulation_mr + 1)).rem_euclid(mr * 2) + 1;
222 current_half = if round_in_ot <= mr { 1 } else { 2 };
223 }
224 current_half
225}
226
227pub fn did_team_win_that_round(
228 team: &ParsedTeam,
229 round: i32,
230 won_by: &str,
231 current_round: i32,
232 regulation_mr: i32,
233 mr: i32,
234) -> bool {
235 let current_half = get_half_from_round(current_round, regulation_mr, mr);
236 let check_half = get_half_from_round(round, regulation_mr, mr);
237
238 let a = team.side == won_by;
239 let b = current_half == check_half;
240 a == b
241}
242
243#[derive(Clone, Debug, Serialize, Deserialize)]
244pub struct InfernoFlame {
245 pub id: String,
246 pub position: Vec<f64>,
247}
248
249#[derive(Clone, Debug, Serialize, Deserialize)]
250pub struct ParsedGrenade {
251 pub id: String,
252 #[serde(rename = "type")]
253 pub kind: String,
254 #[serde(default)]
255 pub owner: Option<String>,
256 #[serde(default)]
257 pub velocity: Option<Vec<f64>>,
258 #[serde(default)]
259 pub position: Option<Vec<f64>>,
260 #[serde(default)]
261 pub lifetime: Option<f64>,
262 #[serde(default)]
263 pub effecttime: Option<f64>,
264 #[serde(default)]
265 pub flames: Option<Vec<InfernoFlame>>,
266 #[serde(flatten)]
267 pub rest: HashMap<String, Value>,
268}
269
270fn split_floats(s: &str) -> Vec<f64> {
271 s.split(", ")
272 .map(|p| p.parse::<f64>().unwrap_or(0.0))
273 .collect()
274}
275
276pub fn parse_grenade(grenade: &Value, id: &str) -> Option<ParsedGrenade> {
277 let kind = grenade.get("type")?.as_str()?.to_string();
278
279 if kind == "inferno" {
280 let mut flames_out = Vec::new();
281 if let Some(flames) = grenade.get("flames").and_then(|v| v.as_object()) {
282 for (fid, pos) in flames {
283 let pos_str = pos.as_str().unwrap_or("");
284 flames_out.push(InfernoFlame {
285 id: fid.clone(),
286 position: split_floats(pos_str),
287 });
288 }
289 }
290
291 let lifetime = grenade
292 .get("lifetime")
293 .and_then(|v| v.as_str())
294 .and_then(|s| s.parse::<f64>().ok());
295
296 let mut rest = HashMap::new();
297 if let Some(obj) = grenade.as_object() {
298 for (k, v) in obj {
299 if k != "flames" && k != "lifetime" && k != "type" {
300 rest.insert(k.clone(), v.clone());
301 }
302 }
303 }
304
305 return Some(ParsedGrenade {
306 id: id.to_string(),
307 kind,
308 owner: grenade.get("owner").and_then(|v| v.as_str()).map(|s| s.to_string()),
309 velocity: None,
310 position: None,
311 lifetime,
312 effecttime: None,
313 flames: Some(flames_out),
314 rest,
315 });
316 }
317
318 if kind == "smoke" || kind == "decoy" {
319 let velocity = grenade
320 .get("velocity")
321 .and_then(|v| v.as_str())
322 .map(split_floats);
323
324 let position = grenade
325 .get("position")
326 .and_then(|v| v.as_str())
327 .map(split_floats);
328
329 let lifetime = grenade
330 .get("lifetime")
331 .and_then(|v| v.as_str())
332 .and_then(|s| s.parse::<f64>().ok());
333
334 let effecttime = grenade
335 .get("effecttime")
336 .and_then(|v| v.as_str())
337 .and_then(|s| s.parse::<f64>().ok());
338
339 let mut rest = HashMap::new();
340 if let Some(obj) = grenade.as_object() {
341 for (k, v) in obj {
342 if k != "velocity" && k != "position" && k != "lifetime" && k != "effecttime" && k != "type" {
343 rest.insert(k.clone(), v.clone());
344 }
345 }
346 }
347
348 return Some(ParsedGrenade {
349 id: id.to_string(),
350 kind,
351 owner: grenade.get("owner").and_then(|v| v.as_str()).map(|s| s.to_string()),
352 velocity,
353 position,
354 lifetime,
355 effecttime,
356 flames: None,
357 rest,
358 });
359 }
360
361 let velocity = grenade
362 .get("velocity")
363 .and_then(|v| v.as_str())
364 .map(split_floats);
365
366 let position = grenade
367 .get("position")
368 .and_then(|v| v.as_str())
369 .map(split_floats);
370
371 let lifetime = grenade
372 .get("lifetime")
373 .and_then(|v| v.as_str())
374 .and_then(|s| s.parse::<f64>().ok());
375
376 let mut rest = HashMap::new();
377 if let Some(obj) = grenade.as_object() {
378 for (k, v) in obj {
379 if k != "velocity" && k != "position" && k != "lifetime" && k != "type" && k != "owner" {
380 rest.insert(k.clone(), v.clone());
381 }
382 }
383 }
384
385 Some(ParsedGrenade {
386 id: id.to_string(),
387 kind,
388 owner: grenade.get("owner").and_then(|v| v.as_str()).map(|s| s.to_string()),
389 velocity,
390 position,
391 lifetime,
392 effecttime: None,
393 flames: None,
394 rest,
395 })
396}
397
398pub fn parse_grenades(grenades: Option<&Value>) -> Vec<ParsedGrenade> {
399 let Some(obj) = grenades.and_then(|v| v.as_object()) else {
400 return vec![];
401 };
402
403 obj.iter()
404 .filter_map(|(id, grenade)| parse_grenade(grenade, id))
405 .collect()
406}
407
408#[derive(Clone, Debug, Serialize, Deserialize)]
409pub struct RoundWin {
410 pub team: ParsedTeam,
411 pub round: i32,
412 pub side: String,
413 pub outcome: String,
414}
415
416pub fn get_round_win(
417 map_round: i32,
418 teams_ct: &ParsedTeam,
419 teams_t: &ParsedTeam,
420 round_wins: &HashMap<String, String>,
421 round: i32,
422 regulation_mr: i32,
423 overtime_mr: i32,
424) -> Option<RoundWin> {
425 let mut index_round = round;
426
427 if map_round > 2 * regulation_mr {
428 let max_overtime_rounds = 2 * overtime_mr
429 * ((map_round - (2 * regulation_mr + 1)) / (2 * overtime_mr))
430 + 2 * regulation_mr;
431
432 if round <= max_overtime_rounds {
433 return None;
434 }
435
436 let round_in_ot = (round - (2 * regulation_mr + 1)).rem_euclid(overtime_mr * 2) + 1;
437 index_round = round_in_ot;
438 }
439
440 let key = index_round.to_string();
441 let round_outcome = round_wins.get(&key)?.clone();
442
443 let win_side = round_outcome
444 .split('_')
445 .next()
446 .unwrap_or("")
447 .to_uppercase();
448
449 let mut result = RoundWin {
450 team: teams_ct.clone(),
451 round,
452 side: win_side.clone(),
453 outcome: round_outcome,
454 };
455
456 if did_team_win_that_round(teams_ct, round, &win_side, map_round, regulation_mr, overtime_mr) {
457 return Some(result);
458 }
459
460 result.team = teams_t.clone();
461 Some(result)
462}
463
464pub fn parse_vec3_from_opt_str(v: Option<&Value>) -> Option<Vec<f64>> {
465 let s = v?.as_str()?;
466 Some(
467 s.split(", ")
468 .map(|p| p.parse::<f64>().unwrap_or(0.0))
469 .collect(),
470 )
471}
472
473pub fn parse_f64_from_str(v: Option<&Value>) -> Option<f64> {
474 v.and_then(|x| x.as_str())
475 .and_then(|s| s.parse::<f64>().ok())
476}