parser/
lib.rs

1use chrono::{prelude::*, TimeDelta};
2use log::{error, info, warn};
3use regex::Regex;
4use rev_lines::RevLines;
5use std::collections::HashMap;
6use std::ffi::OsStr;
7use std::fs::File;
8use std::path::{Path, PathBuf};
9
10#[derive(PartialEq, Default, Hash, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
11pub enum Factions {
12    #[default]
13    Sol,
14    Centauri,
15    Alien,
16    Wildlife,
17}
18
19#[derive(Default, Hash, PartialEq, Eq, Debug)]
20pub enum Modes {
21    #[default]
22    SolVsAlien,
23    CentauriVsSol,
24    CentauriVsSolVsAlien,
25}
26
27#[derive(Default, Debug)]
28struct CommanderDataStructure {
29    current_commander: HashMap<(i64, Factions), NaiveDateTime>,
30    commander_faction: HashMap<Factions, i64>,
31    commander_time: HashMap<(i64, Factions), TimeDelta>,
32}
33
34impl CommanderDataStructure {
35    fn add_commander_start(
36        &mut self,
37        player_id: i64,
38        faction_type: Factions,
39        time_start: NaiveDateTime,
40    ) {
41        self.commander_faction.insert(faction_type, player_id);
42        // TODO No checks for if there was a previous commander in the position already
43        self.current_commander
44            .insert((player_id, faction_type), time_start);
45    }
46
47    fn add_commander_end(
48        &mut self,
49        player_id: i64,
50        faction_type: Factions,
51        time_end: NaiveDateTime,
52    ) {
53        if let Some(time_start) = self.current_commander.remove(&(player_id, faction_type)) {
54            let duration = time_end.signed_duration_since(time_start);
55            let _ = self
56                .commander_time
57                .entry((player_id, faction_type))
58                .and_modify(|e| {
59                    *e += duration;
60                })
61                .or_insert(duration);
62        }
63    }
64
65    fn is_current_commander(&self, faction_type: Factions, player_id: i64) -> bool {
66        match self.commander_faction.get(&faction_type) {
67            Some(&commander) => commander == player_id,
68            None => false,
69        }
70    }
71
72    fn get_all_commander(&self) -> HashMap<Factions, i64> {
73        let mut max_duration: HashMap<Factions, (i64, TimeDelta)> = HashMap::new();
74        for ((id, faction), time_delta) in &self.commander_time {
75            max_duration
76                .entry(*faction)
77                .and_modify(|e| {
78                    if e.1 < *time_delta {
79                        *e = (id.to_owned(), time_delta.to_owned())
80                    }
81                })
82                .or_insert((*id, *time_delta));
83        }
84        let mut to_return: HashMap<Factions, i64> = HashMap::new();
85        for (faction, (player_id, _)) in max_duration {
86            to_return.insert(faction, player_id);
87        }
88        to_return
89    }
90}
91
92#[derive(Debug)]
93pub struct Player {
94    pub player_id: i64,
95    pub player_name: String,
96    pub faction_type: Factions,
97    pub is_commander: bool,
98    pub unit_kill: [i32; 3],
99    pub total_unit_kills: i32,
100    pub structure_kill: [i32; 3],
101    pub total_structure_kills: i32,
102    pub death: i32,
103    pub points: i32,
104    pub winner: bool,
105    is_in_game: bool,
106    pub last_entered_time: NaiveDateTime,
107    pub last_left_time: NaiveDateTime,
108    pub duration_played: TimeDelta,
109}
110
111#[derive(Debug)]
112pub struct Game {
113    pub start_time: NaiveDateTime,
114    pub end_time: NaiveDateTime,
115    pub current_match: Vec<String>,
116    pub match_type: Modes,
117    pub map: Maps,
118    pub winning_team: Factions,
119    pub players: HashMap<(i64, Factions), Player>,
120}
121
122const SOL_VS_ALIEN: &str = "HUMANS_VS_ALIENS";
123const CENTAURI_VS_SOL: &str = "HUMANS_VS_HUMANS";
124const CENTAURI_VS_SOL_VS_ALIEN: &str = "HUMANS_VS_HUMANS_VS_ALIENS";
125
126//Chat message consts
127const STRUCTURE_KILL: &str = "\"structure_kill\"";
128const KILLED: &str = "killed";
129const JOINED_TEAM: &str = "joined team";
130const CHAT: &str = "say";
131const TEAM_CHAT: &str = "say_team";
132const ROUND_START: &str = "World triggered \"Round_Start\"";
133const ROUND_END: &str = "World triggered \"Round_Win\"";
134const LOADING_MAP: &str = "Loading map";
135const TRIGGERED: &str = "triggered";
136const CHANGED_ROLE: &str = "changed role";
137const DISCONNECTED: &str = "disconnected";
138
139//Point allocation consts
140const TIER_ONE_STRUCTURE_POINTS: i32 = 10;
141const TIER_TWO_STRUCTURE_POINTS: i32 = 50;
142const TIER_THREE_STRUCTURE_POINTS: i32 = 100;
143pub const TIER_ONE: usize = 0;
144pub const TIER_TWO: usize = 1;
145pub const TIER_THREE: usize = 2;
146
147const TIER_ONE_UNIT_POINTS: i32 = 1;
148const TIER_TWO_UNIT_POINTS: i32 = 10;
149const TIER_THREE_UNIT_POINTS: i32 = 50;
150const QUEEN_UNIT_POINTS: i32 = 100;
151
152//Log range consts
153const DATETIME_RANGE: std::ops::Range<usize> = 1..23;
154const ROUND_START_RANGE: std::ops::Range<usize> = 25..54;
155const ROUND_END_RANGE: std::ops::Range<usize> = 25..52;
156const MATCH_TYPE_RANGE: usize = 54;
157const DATETIME_END: usize = 25;
158
159#[derive(Debug, Default, Hash, Eq, PartialEq)]
160pub enum Maps {
161    #[default]
162    NarakaCity,
163    MonumentValley,
164    RiftBasin,
165    Badlands,
166    GreatErg,
167    TheMaw,
168    CrimsonPeak,
169    NorthPolarCap,
170}
171
172const TIER_ONE_UNITS: &[&str] = &[
173    "Crab",
174    "Crab_Horned",
175    "Shocker",
176    "Shrimp",
177    "Soldier_Rifleman",
178    "Soldier_Scout",
179    "Soldier_Heavy",
180    "Soldier_Marksman",
181    "LightQuad",
182    "Wasp",
183    "HoverBike",
184    "Worm",
185    "FlakTruck",
186    "Squid",
187];
188const TIER_TWO_UNITS: &[&str] = &[
189    "Behemoth",
190    "Hunter",
191    "LightArmoredCar",
192    "ArmedTransport",
193    "HeavyArmoredCar",
194    "TroopTransport",
195    "HeavyQuad",
196    "RocketLauncher",
197    "PulseTank",
198    "AirGunship",
199    "AirFighter",
200    "AirDropship",
201    "Dragonfly",
202    "Firebug",
203    "Soldier_Commando",
204    "GreatWorm",
205];
206
207const TIER_THREE_UNITS: &[&str] = &[
208    "Queen",
209    "Scorpion",
210    "Goliath",
211    "BomberCraft",
212    "HeavyHarvester",
213    "HoverTank",
214    "RailgunTank",
215    "SiegeTank",
216    "AirBomber",
217    "Defiler",
218    "Colossus",
219];
220
221const TIER_ONE_STRUCTURES: &[&str] = &[
222    "HiveSpire",
223    "LesserSpawningCyst",
224    "Node",
225    "ThornSpire",
226    "Outpost",
227    "RadarStation",
228    "Silo",
229];
230
231const TIER_TWO_STRUCTURES: &[&str] = &[
232    "BioCache",
233    "Barracks",
234    "HeavyVehicleFactory",
235    "LightVehicleFactory",
236    "QuantumCortex",
237    "GreaterSpawningCyst",
238    "Refinery",
239    "Bunker",
240    "ConstructionSite_TurretHeavy",
241    "HeavyTurret",
242    "Turret",
243    "GrandSpawningCyst",
244    "AntiAirRocketTurret",
245];
246
247const TIER_THREE_STRUCTURES: &[&str] = &[
248    "ResearchFacility",
249    "Nest",
250    "UltraHeavyVehicleFactory",
251    "Headquarters",
252    "GrandSpawningCyst",
253    "ColossalSpawningCyst",
254    "AirFactory",
255];
256
257impl Player {
258    fn update_structure_kill(&mut self, structure: &str) {
259        self.total_structure_kills += 1;
260        match structure {
261            s if TIER_ONE_STRUCTURES.contains(&s) => {
262                self.structure_kill[TIER_ONE] += 1;
263                self.points += TIER_ONE_STRUCTURE_POINTS;
264            }
265            s if TIER_TWO_STRUCTURES.contains(&s) => {
266                self.structure_kill[TIER_TWO] += 1;
267                self.points += TIER_TWO_STRUCTURE_POINTS;
268            }
269            s if TIER_THREE_STRUCTURES.contains(&s) => {
270                self.structure_kill[TIER_THREE] += 1;
271                self.points += TIER_THREE_STRUCTURE_POINTS;
272            }
273            _ => (),
274        }
275    }
276
277    fn update_unit_kill(&mut self, unit: &str, enemy_faction: Factions) {
278        let is_enemy = if enemy_faction != self.faction_type {
279            1
280        } else {
281            -1
282        };
283        self.total_unit_kills += is_enemy;
284        match unit {
285            u if TIER_ONE_UNITS.contains(&u) => {
286                self.unit_kill[TIER_ONE] += is_enemy;
287                self.points += TIER_ONE_UNIT_POINTS * is_enemy;
288            }
289            u if TIER_TWO_UNITS.contains(&u) => {
290                self.unit_kill[TIER_TWO] += is_enemy;
291                self.points += TIER_TWO_UNIT_POINTS * is_enemy;
292            }
293            u if TIER_THREE_UNITS.contains(&u) => {
294                self.unit_kill[TIER_THREE] += is_enemy;
295                if u == "Queen" {
296                    self.points += QUEEN_UNIT_POINTS * is_enemy;
297                } else {
298                    self.points += TIER_THREE_UNIT_POINTS * is_enemy;
299                }
300            }
301            _ => (),
302        }
303    }
304
305    fn new(
306        player_id: i64,
307        player_name: String,
308        faction_type: Factions,
309        last_entered_time: NaiveDateTime,
310        last_left_time: NaiveDateTime,
311        is_in_game: bool,
312    ) -> Self {
313        Player {
314            player_id,
315            player_name,
316            faction_type,
317            is_commander: false,
318            unit_kill: [0, 0, 0],
319            total_unit_kills: 0,
320            structure_kill: [0, 0, 0],
321            total_structure_kills: 0,
322            death: 0,
323            points: 0,
324            winner: false,
325            last_entered_time,
326            last_left_time,
327            duration_played: TimeDelta::zero(),
328            is_in_game,
329        }
330    }
331
332    fn update_death(&mut self, unit: &str) {
333        self.death += 1;
334        match unit {
335            u if TIER_ONE_UNITS.contains(&u) => {
336                self.points -= TIER_ONE_UNIT_POINTS;
337            }
338            u if TIER_TWO_UNITS.contains(&u) => {
339                self.points -= TIER_TWO_UNIT_POINTS;
340            }
341            u if TIER_THREE_UNITS.contains(&u) => {
342                if u == "Queen" {
343                    self.points -= QUEEN_UNIT_POINTS;
344                } else {
345                    self.points -= TIER_THREE_UNIT_POINTS;
346                }
347            }
348            _ => (),
349        }
350    }
351
352    pub fn set_commander(&mut self) {
353        self.is_commander = true;
354    }
355    pub fn is_fps(&self) -> bool {
356        let all_sum = self.total_unit_kills + self.total_structure_kills + self.death;
357        all_sum != 0
358    }
359    pub fn did_win(&self, winning_team: Factions) -> bool {
360        //self.winner = winning_team == self.faction_type;
361        winning_team == self.faction_type
362    }
363    fn __str__(&mut self) -> String {
364        let player_name = &self.player_name;
365        let player_id = self.player_id;
366        let faction_type = match self.faction_type {
367            Factions::Sol => "Sol",
368            Factions::Alien => "Alien",
369            Factions::Centauri => "Centauri",
370            Factions::Wildlife => "Wildlife",
371        };
372        let unit_kills = &self.unit_kill;
373        let structure_kill = &self.structure_kill;
374        let deaths = self.death;
375        let winner = self.winner;
376        let is_infantry = self.is_fps();
377        let is_commander = self.is_commander;
378        let points = self.points;
379        format!("name: {player_name}, id: {player_id}, faction_type: {faction_type}, unit_kills: {unit_kills:?},
380                structure_kill: {structure_kill:?}, deaths = {deaths} self.winner = {winner} is_infantry = {is_infantry}
381                is_commander = {is_commander} points= {points}")
382    }
383}
384
385fn _is_valid_faction_type(match_type: Modes, faction_type: Factions) -> bool {
386    match match_type {
387        Modes::SolVsAlien => faction_type != Factions::Centauri,
388        Modes::CentauriVsSol => faction_type != Factions::Alien,
389        _ => true,
390    }
391}
392
393fn get_byte_indices(line: &str, range: std::ops::Range<usize>) -> std::ops::Range<usize> {
394    let valid_start = line
395        .char_indices()
396        .nth(range.start)
397        .map(|(i, _)| i)
398        .unwrap_or(0);
399
400    let valid_end = line
401        .char_indices()
402        .nth(range.end)
403        .map(|(i, _)| i)
404        .unwrap_or(line.len());
405
406    valid_start..valid_end
407}
408
409impl Game {
410    pub fn process_player_durations(&mut self) {
411        for player in self.players.values_mut() {
412            if player.is_in_game {
413                player.last_left_time = self.end_time;
414                player.duration_played += player.last_left_time - player.last_entered_time;
415            }
416        }
417    }
418
419    pub fn get_player_vec(&self) -> Vec<&Player> {
420        self.players.values().collect()
421    }
422
423    pub fn get_match_length(&self) -> i32 {
424        (self.end_time - self.start_time)
425            .num_seconds()
426            .try_into()
427            .unwrap_or_else(|e| panic!("Time couldnt be converted to i32 due to {e}"))
428    }
429
430    pub fn _get_playing_factions(&self) -> Vec<Factions> {
431        match self.match_type {
432            Modes::SolVsAlien => [Factions::Sol, Factions::Alien].to_vec(),
433            Modes::CentauriVsSol => [Factions::Centauri, Factions::Sol].to_vec(),
434            Modes::CentauriVsSolVsAlien => {
435                [Factions::Sol, Factions::Alien, Factions::Centauri].to_vec()
436            }
437        }
438    }
439
440    pub fn get_factions(faction_name: &str) -> Factions {
441        let faction_type: Factions;
442        if faction_name == "Sol" {
443            faction_type = Factions::Sol;
444        } else if faction_name == "Alien" {
445            faction_type = Factions::Alien;
446        } else if faction_name == "Centauri" {
447            faction_type = Factions::Centauri;
448        } else {
449            faction_type = Factions::Wildlife;
450        }
451        faction_type
452    }
453
454    pub fn get_commanders(&mut self) {
455        let role_change_pattern =
456            Regex::new(r#""(.*?)<(.*?)><(.*?)><(.*?)>" changed role to "(.*?)""#).unwrap();
457
458        let player_disconnect = Regex::new(r#""(.*?)<(.*?)><(.*?)><(.*?)>" disconnected"#).unwrap();
459
460        let mut data_structure = CommanderDataStructure::default();
461
462        let req_lines = self.current_match.iter().filter(|x| {
463            remove_chat_messages(x) && (x.contains(CHANGED_ROLE) || x.contains(DISCONNECTED))
464        });
465
466        for line in req_lines {
467            if line.contains(DISCONNECTED) {
468                let pattern_capture = player_disconnect.captures(line);
469                let Some((_, [_, _, player_id, player_faction])) =
470                    pattern_capture.map(|caps| caps.extract())
471                else {
472                    continue;
473                };
474
475                let faction_type = Game::get_factions(player_faction);
476                if faction_type == Factions::Wildlife {
477                    continue;
478                }
479
480                let player_id = player_id.parse::<i64>().unwrap_or_else(|e| {
481                    panic!("Could not parse player id in player disconnect due to {e}")
482                });
483
484                if data_structure.is_current_commander(faction_type, player_id) {
485                    let byte_matched_datetime_range = get_byte_indices(line, DATETIME_RANGE);
486
487                    let time_end = match NaiveDateTime::parse_from_str(
488                        line[byte_matched_datetime_range].trim(),
489                        "%m/%d/%Y - %H:%M:%S",
490                    ) {
491                        Ok(time_end) => time_end,
492                        Err(e) => {
493                            panic!("Error in trying to parse round start time: {e}")
494                        }
495                    };
496                    data_structure.add_commander_end(player_id, faction_type, time_end);
497                }
498            } else {
499                let pattern_capture = role_change_pattern.captures(line);
500                let Some((_, [player_name, _, player_id, player_faction, role])) =
501                    pattern_capture.map(|caps| caps.extract())
502                else {
503                    continue;
504                };
505                let byte_matched_datetime_range = get_byte_indices(line, DATETIME_RANGE);
506
507                let role_change_time = match NaiveDateTime::parse_from_str(
508                    line[byte_matched_datetime_range].trim(),
509                    "%m/%d/%Y - %H:%M:%S",
510                ) {
511                    Ok(datetime) => datetime,
512                    Err(e) => {
513                        panic!("Error in trying to parse round start time: {e}")
514                    }
515                };
516
517                let faction_type = Game::get_factions(player_faction);
518                if faction_type == Factions::Wildlife {
519                    continue;
520                }
521                let player_id = player_id.parse::<i64>().unwrap_or_else(|e| {
522                    panic!("error in parsing the commander player id due to : {e}")
523                });
524
525                self.players
526                    .entry((player_id, faction_type))
527                    .or_insert(Player::new(
528                        player_id,
529                        player_name.to_string(),
530                        faction_type,
531                        self.start_time,
532                        self.end_time,
533                        false,
534                    ));
535
536                if role == "Commander" {
537                    data_structure.add_commander_start(player_id, faction_type, role_change_time);
538                } else if data_structure.is_current_commander(faction_type, player_id) {
539                    data_structure.add_commander_end(player_id, faction_type, role_change_time);
540                }
541            }
542        }
543
544        let mut to_change: Vec<(i64, Factions)> = Vec::new();
545        for ((player_id, faction_type), _) in data_structure.current_commander.iter() {
546            to_change.push((player_id.to_owned(), faction_type.to_owned()));
547        }
548
549        for (player_id, faction_type) in to_change {
550            data_structure.add_commander_end(player_id, faction_type, self.end_time);
551        }
552
553        let final_commander = data_structure.get_all_commander();
554        info!("The commanders are {final_commander:?}");
555
556        for (faction, player_id) in final_commander {
557            self.players
558                .entry((player_id, faction))
559                .and_modify(|e| e.set_commander());
560        }
561    }
562
563    pub fn process_all_players(&mut self) {
564        let joined_team_lines = self
565            .current_match
566            .iter()
567            .filter(|x| remove_chat_messages(x))
568            .filter(|x| x.contains(JOINED_TEAM) || x.contains(DISCONNECTED));
569
570        let join_match_regex =
571            Regex::new(r#""(.*?)<(.*?)><(.*?)><(.*?)>" joined team "(.*)""#).unwrap();
572
573        let player_disconnect = Regex::new(r#""(.*?)<(.*?)><(.*?)><(.*?)>" disconnected"#).unwrap();
574
575        for line in joined_team_lines {
576            let joined_player = join_match_regex.captures(line.trim());
577
578            if let Some((_, [player_name, _, player_id, _, player_faction])) =
579                joined_player.map(|caps| caps.extract())
580            {
581                let faction_type = Game::get_factions(player_faction);
582
583                if faction_type == Factions::Wildlife {
584                    continue;
585                }
586
587                let player_id = player_id
588                    .parse::<i64>()
589                    .unwrap_or_else(|_| panic!("Error in parsing i64"));
590
591                let byte_matched_datetime_range = get_byte_indices(line, DATETIME_RANGE);
592
593                let start_time = match NaiveDateTime::parse_from_str(
594                    line[byte_matched_datetime_range].trim(),
595                    "%m/%d/%Y - %H:%M:%S",
596                ) {
597                    Ok(datetime) => datetime,
598                    Err(e) => {
599                        error!("Error in trying to parse round start time due to {e}");
600                        panic!();
601                    }
602                };
603                let player = self
604                    .players
605                    .entry((player_id, faction_type))
606                    .or_insert_with(|| {
607                        Player::new(
608                            player_id,
609                            player_name.to_string(),
610                            faction_type,
611                            start_time,
612                            NaiveDateTime::MAX,
613                            true,
614                        )
615                    });
616                player.is_in_game = true;
617                player.last_entered_time = start_time;
618                player.last_left_time = NaiveDateTime::MAX;
619            }
620
621            let pattern_capture = player_disconnect.captures(line);
622            if let Some((_, [player_name, _, player_id, player_faction])) =
623                pattern_capture.map(|caps| caps.extract())
624            {
625                let faction_type = Game::get_factions(player_faction);
626
627                if faction_type == Factions::Wildlife {
628                    continue;
629                }
630
631                let player_id = player_id
632                    .parse::<i64>()
633                    .unwrap_or_else(|_| panic!("Error in parsing i64"));
634
635                let byte_matched_datetime_range = get_byte_indices(line, DATETIME_RANGE);
636
637                let disconnect_time = match NaiveDateTime::parse_from_str(
638                    line[byte_matched_datetime_range].trim(),
639                    "%m/%d/%Y - %H:%M:%S",
640                ) {
641                    Ok(datetime) => datetime,
642                    Err(e) => {
643                        error!("Error in trying to parse round start time due to {e}");
644                        panic!();
645                    }
646                };
647                let player = self
648                    .players
649                    .entry((player_id, faction_type))
650                    .or_insert_with(|| {
651                        Player::new(
652                            player_id,
653                            player_name.to_string(),
654                            faction_type,
655                            self.start_time,
656                            disconnect_time,
657                            false,
658                        )
659                    });
660                player.is_in_game = false;
661                player.last_left_time = disconnect_time;
662                player.duration_played += player.last_left_time - player.last_entered_time;
663            }
664        }
665    }
666
667    pub fn get_current_match(&mut self, all_lines: &[PathBuf]) {
668        let mut did_find_world_win = false;
669
670        let mut current_match = Vec::new();
671
672        for file in all_lines.iter().rev() {
673            let reader = match File::open(file) {
674                Ok(open_file) => RevLines::new(open_file),
675                Err(e) => panic!("Error in opening the log file due to: {e}"),
676            };
677
678            for option_line in reader {
679                let line = match option_line {
680                    Ok(line) => line,
681                    Err(e) => {
682                        warn!("Cannot read line due to {e}");
683                        continue;
684                    }
685                };
686                let byte_matched_round_end_range = get_byte_indices(&line, ROUND_END_RANGE);
687                let byte_matched_round_start_range = get_byte_indices(&line, ROUND_START_RANGE);
688                let byte_matched_datetime_range = get_byte_indices(&line, DATETIME_RANGE);
689                if line[byte_matched_round_end_range].trim() == ROUND_END {
690                    self.end_time = match NaiveDateTime::parse_from_str(
691                        line[byte_matched_datetime_range].trim(),
692                        "%m/%d/%Y - %H:%M:%S",
693                    ) {
694                        Ok(datetime) => datetime,
695                        Err(e) => {
696                            error!("Error in trying to parse round start time due to {e}");
697                            panic!()
698                        }
699                    };
700                    did_find_world_win = true;
701                    current_match.push(line);
702                } else if did_find_world_win {
703                    current_match.push(line.clone());
704                    if line[byte_matched_round_start_range].trim() == ROUND_START {
705                        self.start_time = match NaiveDateTime::parse_from_str(
706                            line[DATETIME_RANGE].trim(),
707                            "%m/%d/%Y - %H:%M:%S",
708                        ) {
709                            Ok(datetime) => datetime,
710                            Err(e) => {
711                                error!("Error in trying to parse round start time due to {e}");
712                                panic!()
713                            }
714                        };
715                        current_match.reverse();
716                        self.current_match = current_match;
717                        return;
718                    }
719                }
720            }
721        }
722    }
723
724    pub fn get_match_type(&mut self) {
725        let match_type_thing = self.current_match[0][MATCH_TYPE_RANGE..].trim();
726        let match_type_regex = Regex::new(r#"\(gametype "(.*?)"\)"#).unwrap();
727        //let match_type = match_type_regex.find(match_type_thing).unwrap().as_str();
728        let match_type = match_type_regex
729            .captures(match_type_thing)
730            .unwrap()
731            .get(1)
732            .unwrap_or_else(|| panic!("Couldn't parse the match_type"))
733            .as_str();
734
735        if match_type == SOL_VS_ALIEN {
736            self.match_type = Modes::SolVsAlien
737        } else if match_type == CENTAURI_VS_SOL {
738            self.match_type = Modes::CentauriVsSol
739        } else if match_type == CENTAURI_VS_SOL_VS_ALIEN {
740            self.match_type = Modes::CentauriVsSolVsAlien
741        }
742    }
743
744    pub fn process_kills(&mut self) {
745        let kill_regex = match Regex::new(
746            r#""(.*?)<(.*?)><(.*?)><(.*?)>" killed "(.*?)<(.*?)><(.*?)><(.*?)>" with "(.*)" \(dmgtype "(.*)"\) \(victim "(.*)"\)"#,
747        ) {
748            Ok(kill_regex) => kill_regex,
749            Err(e) => panic!("Error in creating the kill regex: {e}"),
750        };
751
752        for kill_line in &self.current_match {
753            if !kill_line.contains(KILLED) {
754                continue;
755            }
756
757            let kill_matches = kill_regex.captures(kill_line);
758            let Some((
759                _,
760                [player_name, _, player_id, player_faction, enemy_name, _, enemy_id, enemy_faction, _, _, victim],
761            )) = kill_matches.map(|cap| cap.extract())
762            else {
763                continue;
764            };
765
766            if let Ok(player_id) = player_id.parse::<i64>() {
767                let faction_type = Game::get_factions(player_faction);
768                if faction_type == Factions::Wildlife {
769                    continue;
770                }
771                let enemy_faction_type = Game::get_factions(enemy_faction);
772                let player = self
773                    .players
774                    .entry((player_id, faction_type))
775                    .or_insert_with(|| {
776                        Player::new(
777                            player_id,
778                            player_name.to_string(),
779                            faction_type,
780                            self.start_time,
781                            self.end_time,
782                            false,
783                        )
784                    });
785                player.update_unit_kill(victim, enemy_faction_type);
786            };
787
788            if let Ok(enemy_id) = enemy_id.parse::<i64>() {
789                let enemy_faction_type = Game::get_factions(enemy_faction);
790                if enemy_faction_type == Factions::Wildlife {
791                    continue;
792                }
793                let enemy_player = self
794                    .players
795                    .entry((enemy_id, enemy_faction_type))
796                    .or_insert_with(|| {
797                        Player::new(
798                            enemy_id,
799                            enemy_name.to_string(),
800                            enemy_faction_type,
801                            self.start_time,
802                            self.end_time,
803                            false,
804                        )
805                    });
806                enemy_player.update_death(victim);
807            };
808        }
809    }
810
811    pub fn process_structure_kills(&mut self) {
812        let kill_regex = match Regex::new(
813            r#""(.*?)<(.*?)><(.*?)><(.*?)>" triggered "structure_kill" \(structure "(.*)"\) \(struct_team "(.*)"\)"#,
814        ) {
815            Ok(kill_regex) => kill_regex,
816            Err(e) => panic!("Error in creating the kill regex: {e}"),
817        };
818
819        for kill_line in &self.current_match {
820            if !kill_line.contains(STRUCTURE_KILL) {
821                continue;
822            }
823            let kill_matches = kill_regex.captures(kill_line);
824            let Some((_, [player_name, _, player_id, player_faction, enemy_structure, _])) =
825                kill_matches.map(|cap| cap.extract())
826            else {
827                continue;
828            };
829
830            let faction_type = Game::get_factions(player_faction);
831
832            if faction_type == Factions::Wildlife {
833                continue;
834            }
835
836            match player_id.parse::<i64>() {
837                Ok(player_id) => {
838                    //NOTE Why should player not be specified as mut here?
839                    let player = self
840                        .players
841                        .entry((player_id, faction_type))
842                        .or_insert_with(|| {
843                            Player::new(
844                                player_id,
845                                player_name.to_string(),
846                                faction_type,
847                                self.start_time,
848                                self.end_time,
849                                false,
850                            )
851                        });
852                    player.update_structure_kill(enemy_structure);
853                }
854                Err(_) => {
855                    info!("Couldn't parse the player_id. Most likely AI");
856                }
857            };
858        }
859    }
860
861    pub fn get_current_map(&mut self, all_lines: &[PathBuf]) {
862        let map_regex = match Regex::new(r#"Loading map "(.*)""#) {
863            Ok(map_regex) => map_regex,
864            Err(_) => {
865                error!("Error in creating the get_current_map_regex");
866                panic!();
867            }
868        };
869
870        let mut files_read = Vec::new();
871
872        for file in all_lines.iter().rev() {
873            files_read.push(file);
874            let reader = match File::open(file) {
875                Ok(open_file) => RevLines::new(open_file),
876                Err(e) => {
877                    error!("Error in opening the log file due to: {e}");
878                    panic!();
879                }
880            };
881
882            for option_line in reader {
883                let line = match option_line {
884                    Ok(line) => {
885                        if !line.contains(LOADING_MAP) {
886                            continue;
887                        }
888                        line
889                    }
890                    Err(e) => {
891                        warn!("Cannot read line due to {e}");
892                        continue;
893                    }
894                };
895                let map_matched = map_regex.captures(&line);
896                match map_matched {
897                    Some(map) => {
898                        let map_str = map.get(1).unwrap().as_str();
899                        if map_str == "NarakaCity" {
900                            self.map = Maps::NarakaCity;
901                        } else if map_str == "MonumentValley" {
902                            self.map = Maps::MonumentValley;
903                        } else if map_str == "RiftBasin" {
904                            self.map = Maps::RiftBasin;
905                        } else if map_str == "Badlands" {
906                            self.map = Maps::Badlands;
907                        } else if map_str == "GreatErg" {
908                            self.map = Maps::GreatErg;
909                        } else if map_str == "TheMaw" {
910                            self.map = Maps::TheMaw;
911                        } else if map_str == "CrimsonPeak" {
912                            self.map = Maps::CrimsonPeak;
913                        } else if map_str == "NorthPolarCap" {
914                            self.map = Maps::NorthPolarCap;
915                        } else {
916                            error!("Map {map_str} not found. Exiting parsing.");
917                            panic!();
918                        }
919
920                        info!("Files read for finding the current map are {files_read:?}");
921                        return;
922                    }
923                    None => continue,
924                }
925            }
926        }
927    }
928
929    pub fn get_winning_team(&mut self) {
930        let winning_team_log = self
931            .current_match
932            .iter()
933            .rev()
934            .filter(|x| x.contains(TRIGGERED));
935
936        let victory_regex = match Regex::new(r#"Team "(.*?)" triggered "Victory""#) {
937            Ok(map_regex) => map_regex,
938            Err(e) => {
939                error!("Error in creating the get_current_map_regex due to: {e}");
940                panic!()
941            }
942        };
943
944        for line in winning_team_log {
945            let victory_matched = victory_regex.captures(line);
946            match victory_matched {
947                Some(winning_match) => {
948                    let winning_team_str = winning_match.get(1).unwrap().as_str();
949                    if winning_team_str == "Alien" {
950                        self.winning_team = Factions::Alien;
951                    } else if winning_team_str == "Wildlife" {
952                        self.winning_team = Factions::Wildlife;
953                    } else if winning_team_str == "Sol" {
954                        self.winning_team = Factions::Sol;
955                    } else if winning_team_str == "Centauri" {
956                        self.winning_team = Factions::Centauri;
957                    }
958                    return;
959                }
960                None => continue,
961            }
962        }
963    }
964}
965impl Default for Game {
966    fn default() -> Self {
967        let default_time = NaiveDateTime::default();
968        Game {
969            start_time: default_time,
970            end_time: default_time,
971            current_match: Vec::new(),
972            match_type: Modes::default(),
973            map: Maps::default(),
974            winning_team: Factions::default(),
975            players: HashMap::new(),
976        }
977    }
978}
979
980fn remove_chat_messages(line: &str) -> bool {
981    let mut words = line.split_whitespace();
982    let chat_keywords = [CHAT, TEAM_CHAT];
983    !words.any(|i| chat_keywords.contains(&i))
984}
985
986fn _remove_date_data(line: &str) -> &str {
987    if line.len() > DATETIME_END {
988        let byte_corrected_datetimeend = get_byte_indices(line, DATETIME_END..line.len());
989        &line.trim()[byte_corrected_datetimeend]
990    } else {
991        ""
992    }
993}
994
995fn parse_info(all_lines: Vec<PathBuf>) -> Game {
996    let mut game = Game::default();
997    //NOTE Possible to parallelize them, but probably not worth it.
998    game.get_current_map(&all_lines);
999    info!("current map is {:?}", game.map);
1000    game.get_current_match(&all_lines);
1001    game.get_match_type();
1002    game.get_winning_team();
1003    game.process_all_players();
1004    game.process_kills();
1005    game.process_structure_kills();
1006    game.get_commanders();
1007    game.process_player_durations();
1008    game
1009}
1010
1011pub fn checking_folder(path: &Path) -> Game {
1012    info!("The path of the folder is {path:?}");
1013    let entries = match std::fs::read_dir(path) {
1014        Ok(entries) => entries,
1015        Err(_) => panic!("Failed to read directory"),
1016    };
1017
1018    let file_entries = entries
1019        .map(|r| r.unwrap())
1020        .filter(|r| r.path().is_file())
1021        .map(|r| r.path());
1022
1023    let mut log_files: Vec<_> = file_entries
1024        .filter(|r| r.extension().unwrap_or(OsStr::new("")) == "log")
1025        .collect();
1026
1027    log_files.sort();
1028
1029    info!("Parsing the file");
1030
1031    parse_info(log_files)
1032}
1033
1034pub fn checking_file(path: &Path) -> Game {
1035    info!("The path of the folder is {path:?}");
1036    info!("Parsing the file");
1037    parse_info(vec![path.to_path_buf()])
1038}