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}