csgsi_rust/
utils.rs

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}