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 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
126const 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
139const 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
152const 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 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
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 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 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}