csgsi_rust/
lib.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::sync::Arc;
5
6pub mod utils;
7use utils::*;
8
9#[derive(Clone)]
10pub struct ListenerDescriptor {
11    pub once: bool,
12    pub listener: Arc<dyn Fn(&Value, Option<&Value>) + Send + Sync>,
13}
14
15#[derive(Default)]
16pub struct EventEmitter {
17    descriptors: HashMap<String, Vec<ListenerDescriptor>>,
18}
19
20impl EventEmitter {
21    pub fn new() -> Self {
22        Self {
23            descriptors: HashMap::new(),
24        }
25    }
26
27    pub fn on<F>(&mut self, event_name: &str, listener: F)
28    where
29        F: Fn(&Value, Option<&Value>) + Send + Sync + 'static,
30    {
31        self.emit_internal("newListener", Value::String(event_name.to_string()), None);
32
33        let list = self.descriptors.entry(event_name.to_string()).or_default();
34        list.push(ListenerDescriptor {
35            once: false,
36            listener: Arc::new(listener),
37        });
38    }
39
40    pub fn once<F>(&mut self, event_name: &str, listener: F)
41    where
42        F: Fn(&Value, Option<&Value>) + Send + Sync + 'static,
43    {
44        let list = self.descriptors.entry(event_name.to_string()).or_default();
45        list.push(ListenerDescriptor {
46            once: true,
47            listener: Arc::new(listener),
48        });
49    }
50
51    pub fn off(&mut self, event_name: &str, listener_ptr: *const ()) {
52        if let Some(list) = self.descriptors.get_mut(event_name) {
53            list.retain(|d| Arc::as_ptr(&d.listener) as *const () != listener_ptr);
54        }
55        self.emit_internal(
56            "removeListener",
57            Value::String(event_name.to_string()),
58            Some(Value::Null),
59        );
60    }
61
62    pub fn remove_all_listeners(&mut self, event_name: &str) {
63        self.descriptors.insert(event_name.to_string(), vec![]);
64    }
65
66    pub fn emit(&mut self, event_name: &str, arg: &Value, arg2: Option<&Value>) -> bool {
67        self.emit_internal(event_name, arg.clone(), arg2.cloned())
68    }
69
70    fn emit_internal(&mut self, event_name: &str, arg: Value, arg2: Option<Value>) -> bool {
71        let Some(listeners) = self.descriptors.get_mut(event_name) else {
72            return false;
73        };
74        if listeners.is_empty() {
75            return false;
76        }
77
78        let snapshot = listeners.clone();
79        let mut remove_indices = Vec::new();
80
81        for (idx, d) in snapshot.iter().enumerate() {
82            (d.listener)(&arg, arg2.as_ref());
83            if d.once {
84                remove_indices.push(idx);
85            }
86        }
87
88        if !remove_indices.is_empty() {
89            let mut kept = Vec::new();
90            for (idx, d) in listeners.iter().cloned().enumerate() {
91                if !remove_indices.contains(&idx) {
92                    kept.push(d);
93                }
94            }
95            *listeners = kept;
96        }
97
98        true
99    }
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
103pub struct ObserverInfo {
104    #[serde(default)]
105    pub activity: Option<String>,
106    #[serde(default)]
107    pub spectarget: Option<String>,
108    #[serde(default)]
109    pub position: Option<Vec<f64>>,
110    #[serde(default)]
111    pub forward: Option<Vec<f64>>,
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct BombInfo {
116    pub state: String,
117    #[serde(default)]
118    pub countdown: Option<f64>,
119    pub position: Vec<f64>,
120    #[serde(default)]
121    pub player: Option<ParsedPlayer>,
122    #[serde(default)]
123    pub site: Option<String>,
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize)]
127pub struct PhaseCountdowns {
128    pub phase: String,
129    pub phase_ends_in: f64,
130    #[serde(default)]
131    pub timeout_team: Option<ParsedTeam>,
132}
133
134#[derive(Clone, Debug, Serialize, Deserialize)]
135pub struct ParsedRoundInfo {
136    pub phase: String,
137    #[serde(default)]
138    pub bomb: Option<String>,
139    #[serde(default)]
140    pub win_team: Option<String>,
141}
142
143#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct ParsedMapInfo {
145    #[serde(default)]
146    pub mode: Option<String>,
147    pub name: String,
148    #[serde(default)]
149    pub phase: Option<String>,
150    #[serde(default)]
151    pub round: i32,
152    pub team_ct: ParsedTeam,
153    pub team_t: ParsedTeam,
154    #[serde(default)]
155    pub num_matches_to_win_series: Option<i32>,
156    #[serde(default)]
157    pub current_spectators: Option<i32>,
158    #[serde(default)]
159    pub souvenirs_total: Option<i32>,
160    #[serde(default)]
161    pub round_wins: Option<HashMap<String, String>>,
162    #[serde(default)]
163    pub rounds: Vec<RoundWin>,
164}
165
166#[derive(Clone, Debug, Serialize, Deserialize)]
167pub struct CSGSIData {
168    #[serde(default)]
169    pub provider: Option<Value>,
170    pub observer: ObserverInfo,
171    #[serde(default)]
172    pub round: Option<ParsedRoundInfo>,
173    #[serde(default)]
174    pub player: Option<ParsedPlayer>,
175    pub players: Vec<ParsedPlayer>,
176    #[serde(default)]
177    pub bomb: Option<BombInfo>,
178    pub grenades: Vec<ParsedGrenade>,
179    pub phase_countdowns: PhaseCountdowns,
180    pub auth: Option<Value>,
181    pub map: ParsedMapInfo,
182}
183#[derive(Clone)]
184struct TeamBuild {
185    ct: ParsedTeam,
186    t: ParsedTeam,
187    map_name: String,
188    map_round: i32,
189}
190
191#[derive(Clone)]
192struct PlayersBuild {
193    players: Vec<ParsedPlayer>,
194    observed: Option<ParsedPlayer>,
195    observer: ObserverInfo,
196}
197
198#[derive(Clone)]
199struct RoundsBuild {
200    rounds: Vec<RoundWin>,
201    current_round_for_damage: i32,
202    freeze_phase: String,
203}
204
205pub struct CSGSI {
206    pub emitter: EventEmitter,
207    pub teams_left: Option<TeamExtension>,
208    pub teams_right: Option<TeamExtension>,
209    pub players_ext: Vec<PlayerExtension>,
210    pub overtime_mr: i32,
211    pub regulation_mr: i32,
212    pub damage: Vec<DamageRound>,
213    pub last: Option<CSGSIData>,
214    pub current: Option<CSGSIData>,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize)]
218pub struct DamagePlayer {
219    pub steamid: String,
220    pub damage: i32,
221}
222
223#[derive(Clone, Debug, Serialize, Deserialize)]
224pub struct DamageRound {
225    pub round: i32,
226    pub players: Vec<DamagePlayer>,
227}
228
229impl CSGSI {
230    pub fn new() -> Self {
231        Self {
232            emitter: EventEmitter::new(),
233            teams_left: None,
234            teams_right: None,
235            players_ext: vec![],
236            overtime_mr: 3,
237            regulation_mr: 15,
238            damage: vec![],
239            last: None,
240            current: None,
241        }
242    }
243
244    pub fn on<F>(&mut self, event: &str, listener: F)
245    where
246        F: Fn(&Value, Option<&Value>) + Send + Sync + 'static,
247    {
248        self.emitter.on(event, listener);
249    }
250
251    pub fn emit(&mut self, event: &str, arg: &Value, arg2: Option<&Value>) -> bool {
252        self.emitter.emit(event, arg, arg2)
253    }
254
255    pub fn digest(&mut self, raw: &Value) -> Option<CSGSIData> {
256        let (allplayers_obj, map, phase_countdowns) = self.validate_raw(raw)?;
257        self.emit("raw", raw, None);
258
259        let team_build = self.build_teams(map, allplayers_obj)?;
260        let mut players_build = self.build_players(raw, allplayers_obj, &team_build)?;
261
262        let rounds_build = self.build_rounds_and_damage_context(raw, map, phase_countdowns, &team_build)?;
263
264        self.reset_damage_on_map_change(&team_build.map_name);
265        self.reset_damage_on_freeze_or_warmup(team_build.map_round, &rounds_build.freeze_phase);
266
267        self.upsert_damage_snapshot(&players_build.players, rounds_build.current_round_for_damage);
268        self.apply_adr_if_possible(&mut players_build.players, team_build.map_round, rounds_build.current_round_for_damage);
269
270        let bomb = self.build_bomb(raw, &team_build.map_name, &players_build.players);
271        let grenades = self.build_grenades(raw);
272
273        let phase = self.build_phase_countdowns(phase_countdowns, &team_build.ct, &team_build.t);
274        let round_info = self.build_round_info(raw);
275
276        let map_round_wins = map
277            .get("round_wins")
278            .and_then(|v| serde_json::from_value::<HashMap<String, String>>(v.clone()).ok());
279
280        let data = CSGSIData {
281            provider: raw.get("provider").cloned(),
282            observer: players_build.observer,
283            round: round_info,
284            player: players_build.observed,
285            players: players_build.players,
286            bomb,
287            grenades,
288            phase_countdowns: phase,
289            auth: raw.get("auth").cloned(),
290            map: ParsedMapInfo {
291                mode: map.get("mode").and_then(|v| v.as_str()).map(|s| s.to_string()),
292                name: team_build.map_name.clone(),
293                phase: map.get("phase").and_then(|v| v.as_str()).map(|s| s.to_string()),
294                round: team_build.map_round,
295                team_ct: team_build.ct.clone(),
296                team_t: team_build.t.clone(),
297                num_matches_to_win_series: map.get("num_matches_to_win_series").and_then(|v| v.as_i64()).map(|n| n as i32),
298                current_spectators: map.get("current_spectators").and_then(|v| v.as_i64()).map(|n| n as i32),
299                souvenirs_total: map.get("souvenirs_total").and_then(|v| v.as_i64()).map(|n| n as i32),
300                round_wins: map_round_wins,
301                rounds: rounds_build.rounds,
302            },
303        };
304
305        self.current = Some(data.clone());
306        self.emit_data_and_update_last(data)
307    }
308
309    fn validate_raw<'a>(
310        &self,
311        raw: &'a Value,
312    ) -> Option<(&'a serde_json::Map<String, Value>, &'a Value, &'a Value)> {
313        if raw.get("allplayers").is_none() || raw.get("map").is_none() || raw.get("phase_countdowns").is_none() {
314            return None;
315        }
316        let allplayers_obj = raw.get("allplayers")?.as_object()?;
317        let map = raw.get("map")?;
318        let phase_countdowns = raw.get("phase_countdowns")?;
319        Some((allplayers_obj, map, phase_countdowns))
320    }
321
322    fn build_teams(
323        &self,
324        map: &Value,
325        allplayers_obj: &serde_json::Map<String, Value>,
326    ) -> Option<TeamBuild> {
327        let map_name = map.get("name")?.as_str()?.to_string();
328        let map_round = map.get("round").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
329
330        let is_ct_left = self.detect_ct_left(allplayers_obj);
331
332        let team_ct_raw: RawTeam = serde_json::from_value(map.get("team_ct")?.clone()).ok()?;
333        let team_t_raw: RawTeam = serde_json::from_value(map.get("team_t")?.clone()).ok()?;
334
335        let ct = parse_team(
336            &team_ct_raw,
337            if is_ct_left { "left" } else { "right" },
338            "CT",
339            if is_ct_left { self.teams_left.as_ref() } else { self.teams_right.as_ref() },
340        );
341
342        let t = parse_team(
343            &team_t_raw,
344            if is_ct_left { "right" } else { "left" },
345            "T",
346            if is_ct_left { self.teams_right.as_ref() } else { self.teams_left.as_ref() },
347        );
348
349        Some(TeamBuild {
350            ct,
351            t,
352            map_name,
353            map_round,
354        })
355    }
356
357    fn detect_ct_left(&self, allplayers_obj: &serde_json::Map<String, Value>) -> bool {
358        let mut is_ct_left = true;
359
360        let mut example_t: Option<i32> = None;
361        let mut example_ct: Option<i32> = None;
362
363        for (_steamid, p) in allplayers_obj.iter() {
364            let team = p.get("team").and_then(|v| v.as_str()).unwrap_or("");
365            let slot = p.get("observer_slot").and_then(|v| v.as_i64()).map(|n| n as i32);
366            if let Some(slot) = slot {
367                if team == "T" && example_t.is_none() {
368                    example_t = Some(slot);
369                }
370                if team == "CT" && example_ct.is_none() {
371                    example_ct = Some(slot);
372                }
373            }
374        }
375
376        if let (Some(slot_ct), Some(slot_t)) = (example_ct, example_t) {
377            if slot_ct > slot_t {
378                is_ct_left = false;
379            }
380        }
381
382        is_ct_left
383    }
384
385    fn build_players(
386        &self,
387        raw: &Value,
388        allplayers_obj: &serde_json::Map<String, Value>,
389        team_build: &TeamBuild,
390    ) -> Option<PlayersBuild> {
391        let mut base_players: HashMap<String, BasePlayer> = HashMap::new();
392        for (steamid, p) in allplayers_obj.iter() {
393            if let Ok(bp) = serde_json::from_value::<BasePlayer>(p.clone()) {
394                base_players.insert(steamid.clone(), bp);
395            }
396        }
397
398        let mut parsed_players = Vec::new();
399        for (steamid, bp) in base_players.iter() {
400            let team = if bp.team == "CT" { team_build.ct.clone() } else { team_build.t.clone() };
401            parsed_players.push(parse_player(bp, steamid, team, &self.players_ext));
402        }
403
404        let observed = raw
405            .get("player")
406            .and_then(|p| p.get("steamid"))
407            .and_then(|v| v.as_str())
408            .and_then(|sid| parsed_players.iter().find(|pl| pl.steamid == sid).cloned());
409
410        let observer = ObserverInfo {
411            activity: raw
412                .get("player")
413                .and_then(|p| p.get("activity"))
414                .and_then(|v| v.as_str())
415                .map(|s| s.to_string()),
416            spectarget: raw
417                .get("player")
418                .and_then(|p| p.get("spectarget"))
419                .and_then(|v| v.as_str())
420                .map(|s| s.to_string()),
421            position: parse_vec3_from_opt_str(raw.get("player").and_then(|p| p.get("position"))),
422            forward: parse_vec3_from_opt_str(raw.get("player").and_then(|p| p.get("forward"))),
423        };
424
425        Some(PlayersBuild {
426            players: parsed_players,
427            observed,
428            observer,
429        })
430    }
431
432    fn build_rounds_and_damage_context(
433        &self,
434        raw: &Value,
435        map: &Value,
436        phase_countdowns: &Value,
437        team_build: &TeamBuild,
438    ) -> Option<RoundsBuild> {
439        let map_round = team_build.map_round;
440
441        let rounds = self.compute_round_wins(raw, map, &team_build.ct, &team_build.t, map_round);
442        let current_round_for_damage = self.current_round_index(raw, map, map_round);
443        let freeze_phase = phase_countdowns
444            .get("phase")
445            .and_then(|v| v.as_str())
446            .unwrap_or("")
447            .to_string();
448
449        Some(RoundsBuild {
450            rounds,
451            current_round_for_damage,
452            freeze_phase,
453        })
454    }
455
456    fn is_round_finalized(&self, raw: &Value, map: &Value) -> bool {
457        let phase = raw
458            .get("round")
459            .and_then(|r| r.get("phase"))
460            .and_then(|v| v.as_str())
461            .unwrap_or("");
462
463        let map_phase = map
464            .get("phase")
465            .and_then(|v| v.as_str())
466            .unwrap_or("");
467
468        phase == "over" || map_phase == "gameover"
469    }
470
471    fn current_round_index(&self, raw: &Value, map: &Value, map_round: i32) -> i32 {
472        if self.is_round_finalized(raw, map) {
473            map_round
474        } else {
475            map_round + 1
476        }
477    }
478
479    fn compute_round_wins(
480        &self,
481        raw: &Value,
482        map: &Value,
483        team_ct: &ParsedTeam,
484        team_t: &ParsedTeam,
485        map_round: i32,
486    ) -> Vec<RoundWin> {
487        let Some(round_wins) = map.get("round_wins").and_then(|v| v.as_object()) else {
488            return vec![];
489        };
490
491        let rw: HashMap<String, String> = round_wins
492            .iter()
493            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
494            .collect();
495
496        let current_round = self.current_round_index(raw, map, map_round);
497
498        let mut rounds = Vec::new();
499        for i in 1..=current_round {
500            if let Some(res) = get_round_win(
501                current_round,
502                team_ct,
503                team_t,
504                &rw,
505                i,
506                self.regulation_mr,
507                self.overtime_mr,
508            ) {
509                rounds.push(res);
510            }
511        }
512
513        rounds
514    }
515
516    fn reset_damage_on_map_change(&mut self, map_name: &str) {
517        if let Some(last) = &self.last {
518            if last.map.name != map_name {
519                self.damage.clear();
520            }
521        }
522    }
523
524    fn reset_damage_on_freeze_or_warmup(&mut self, map_round: i32, phase: &str) {
525        if (map_round == 0 && phase == "freezetime") || phase == "warmup" {
526            self.damage.clear();
527        }
528    }
529
530    fn upsert_damage_snapshot(&mut self, players: &[ParsedPlayer], round: i32) {
531        if !self.damage.iter().any(|d| d.round == round) {
532            self.damage.push(DamageRound {
533                round,
534                players: vec![],
535            });
536        }
537
538        if let Some(dmg_round) = self.damage.iter_mut().find(|d| d.round == round) {
539            dmg_round.players = players
540                .iter()
541                .map(|p| DamagePlayer {
542                    steamid: p.steamid.clone(),
543                    damage: p.state.round_totaldmg,
544                })
545                .collect();
546        }
547    }
548
549    fn apply_adr_if_possible(&self, players: &mut [ParsedPlayer], map_round: i32, current_round_for_damage: i32) {
550        if self.current.is_none() {
551            return;
552        }
553
554        let damage_for_round = self
555            .damage
556            .iter()
557            .filter(|d| d.round < current_round_for_damage)
558            .collect::<Vec<_>>();
559
560        if damage_for_round.is_empty() {
561            return;
562        }
563
564        let denom = if map_round == 0 { 1 } else { map_round } as f64;
565
566        for pl in players.iter_mut() {
567            let mut sum = 0i32;
568            for dr in damage_for_round.iter() {
569                let v = dr
570                    .players
571                    .iter()
572                    .find(|x| x.steamid == pl.steamid)
573                    .map(|x| x.damage)
574                    .unwrap_or(0);
575                sum += v;
576            }
577            let adr = (sum as f64) / denom;
578            pl.state.adr = adr.floor() as i32;
579        }
580    }
581
582    fn build_bomb(&self, raw: &Value, map_name: &str, players: &[ParsedPlayer]) -> Option<BombInfo> {
583        let bomb = raw.get("bomb")?;
584        let state = bomb.get("state").and_then(|v| v.as_str()).unwrap_or("").to_string();
585
586        let position = bomb
587            .get("position")
588            .and_then(|v| v.as_str())
589            .map(|s| {
590                s.split(", ")
591                    .map(|p| p.parse::<f64>().unwrap_or(0.0))
592                    .collect::<Vec<_>>()
593            })
594            .unwrap_or_else(|| vec![0.0, 0.0, 0.0]);
595
596        let countdown = parse_f64_from_str(bomb.get("countdown"));
597
598        let bomb_player = bomb
599            .get("player")
600            .and_then(|v| v.as_str())
601            .and_then(|sid| players.iter().find(|p| p.steamid == sid).cloned());
602
603        let site = if state == "planted" || state == "defused" || state == "defusing" || state == "planting" {
604            Self::find_site(map_name, &position)
605        } else {
606            None
607        };
608
609        Some(BombInfo {
610            state,
611            countdown,
612            position,
613            player: bomb_player,
614            site,
615        })
616    }
617
618    fn build_grenades(&self, raw: &Value) -> Vec<ParsedGrenade> {
619        parse_grenades(raw.get("grenades").or_else(|| raw.get("grenades")))
620    }
621
622    fn build_phase_countdowns(&self, phase_countdowns: &Value, team_ct: &ParsedTeam, team_t: &ParsedTeam) -> PhaseCountdowns {
623        let phase = phase_countdowns
624            .get("phase")
625            .and_then(|v| v.as_str())
626            .unwrap_or("")
627            .to_string();
628
629        let phase_ends_in = phase_countdowns
630            .get("phase_ends_in")
631            .and_then(|v| v.as_str())
632            .and_then(|s| s.parse::<f64>().ok())
633            .unwrap_or(0.0);
634
635        let timeout_team = if phase == "timeout_ct" {
636            Some(team_ct.clone())
637        } else if phase == "timeout_t" {
638            Some(team_t.clone())
639        } else {
640            None
641        };
642
643        PhaseCountdowns {
644            phase,
645            phase_ends_in,
646            timeout_team,
647        }
648    }
649
650    fn build_round_info(&self, raw: &Value) -> Option<ParsedRoundInfo> {
651        raw.get("round").and_then(|r| {
652            Some(ParsedRoundInfo {
653                phase: r.get("phase")?.as_str()?.to_string(),
654                bomb: r.get("bomb").and_then(|v| v.as_str()).map(|s| s.to_string()),
655                win_team: r.get("win_team").and_then(|v| v.as_str()).map(|s| s.to_string()),
656            })
657        })
658    }
659
660    fn emit_data_and_update_last(&mut self, data: CSGSIData) -> Option<CSGSIData> {
661        if self.last.is_none() {
662            self.last = Some(data.clone());
663            let v = serde_json::to_value(&data).ok().unwrap_or(Value::Null);
664            self.emit("data", &v, None);
665            return Some(data);
666        }
667
668        self.last = Some(data.clone());
669        let v = serde_json::to_value(&data).ok().unwrap_or(Value::Null);
670        self.emit("data", &v, None);
671        Some(data)
672    }
673
674    pub fn find_site(map_name: &str, position: &[f64]) -> Option<String> {
675        let real = map_name.rsplit('/').next().unwrap_or(map_name);
676        let site = match real {
677            "de_mirage" => if position.get(1).copied().unwrap_or(0.0) < -600.0 { "A" } else { "B" },
678            "de_cache" => if position.get(1).copied().unwrap_or(0.0) > 0.0 { "A" } else { "B" },
679            "de_overpass" => if position.get(2).copied().unwrap_or(0.0) > 400.0 { "A" } else { "B" },
680            "de_nuke" => if position.get(2).copied().unwrap_or(0.0) > -500.0 { "A" } else { "B" },
681            "de_dust2" => if position.get(0).copied().unwrap_or(0.0) > -500.0 { "A" } else { "B" },
682            "de_inferno" => if position.get(0).copied().unwrap_or(0.0) > 1400.0 { "A" } else { "B" },
683            "de_vertigo" => if position.get(0).copied().unwrap_or(0.0) > -1400.0 { "A" } else { "B" },
684            "de_train" => if position.get(1).copied().unwrap_or(0.0) > -450.0 { "A" } else { "B" },
685            "de_ancient" => if position.get(0).copied().unwrap_or(0.0) < -500.0 { "A" } else { "B" },
686            "de_anubis" => if position.get(0).copied().unwrap_or(0.0) > 0.0 { "A" } else { "B" },
687            _ => return None,
688        };
689        Some(site.to_string())
690    }
691}