1use std::fmt::Debug;
4
5use nom::{
6 branch::alt,
7 bytes::complete::{tag, take_until, take_while},
8 character::complete::{alpha1, digit1, hex_digit1, line_ending, space1, u32, u8},
9 combinator::{map, map_parser, opt, success, value},
10 error::{context, convert_error, VerboseError},
11 multi::{many0, many1, many_m_n, separated_list1},
12 sequence::{delimited, pair, preceded, separated_pair, terminated, tuple},
13};
14
15use crate::{
16 battle_report::BattleReport, Award, BattleResult, Event, ModificationResearch, Reward, Vehicle,
17 VehicleResearch,
18};
19
20type IResult<'a, O> = nom::IResult<&'a str, O, VerboseError<&'a str>>;
21
22const INDENT: &str = " "; #[derive(Debug, thiserror::Error)]
25#[error("Error parsing battle report: {message}")]
26pub struct Error {
27 message: String,
28}
29
30pub fn parse(input: &str) -> Result<BattleReport, Error> {
31 battle_report(input)
32 .map(|(_, report)| report)
33 .map_err(|err| {
34 let message = if let nom::Err::Error(err) = err {
35 convert_error(input, err)
36 } else {
37 "Unknown error".to_string()
38 };
39 Error { message }
40 })
41}
42
43fn battle_report(input: &str) -> IResult<BattleReport> {
44 let (input, (result, mission_name)) = context("first line", result_line)(input)?;
45
46 let (
47 input,
48 (
49 events,
50 awards,
51 vehicles,
52 reward_for_winning,
53 other_awards,
54 earned_rewards,
55 activity,
56 damaged_vehicles,
57 automatic_repair,
58 automatic_purchases,
59 _,
60 vehicle_research,
61 modification_research,
62 _,
63 session_id,
64 (balance, _raw_research),
65 ),
66 ) = tuple((
67 context("events", parse_events),
68 context("awards", award_table),
69 context("activity and time played", vehicle_tables),
70 context("reward for winning", opt(parse_reward_for_winning)),
71 context("other awards", parse_other_awards),
72 context("earned", parse_earned),
73 context("activity", parse_activity),
74 context("damaged vehicles", parse_damaged_vehicles),
75 context("automatic repair", parse_automatic_repair),
76 context("automatic purchase", parse_automatic_purchase),
77 line_ending,
78 context("researched vehicles", opt(parse_researched_units)),
79 context(
80 "researched modifications",
81 opt(parse_researched_modifications),
82 ),
83 context("used items", opt(parse_used_items)),
84 context("session id", parse_session_id),
85 context("total", parse_total),
86 ))(input)?;
87
88 Ok((
89 input,
90 BattleReport {
91 session_id,
92 result,
93 mission_name: mission_name.to_string(),
94 events,
95 awards,
96 reward_for_winning,
97 other_awards,
98 vehicles,
99 activity,
100 damaged_vehicles,
101 automatic_repair,
102 automatic_purchases,
103 vehicle_research: vehicle_research.unwrap_or_default(),
104 modification_research: modification_research.unwrap_or_default(),
105 earned_rewards,
106 balance,
107 },
108 ))
109}
110
111fn result_line(input: &str) -> IResult<(BattleResult, &str)> {
113 let (input, result) = battle_result(input)?;
114 let (input, _) = tag(" in the ")(input)?;
115 let (input, mission) = take_until(" mission!")(input)?;
116 let (input, _) = tag(" mission!")(input)?;
117 let (input, _) = line_ending(input)?;
118 let (input, _) = line_ending(input)?;
119
120 Ok((input, (result, mission)))
121}
122
123fn battle_result(input: &str) -> IResult<BattleResult> {
124 alt((
125 map(tag("Victory"), |_| BattleResult::Win),
126 map(tag("Defeat"), |_| BattleResult::Loss),
127 ))(input)
128}
129
130struct Table {
131 name: String,
132 rows: Vec<Row>,
133}
134
135#[derive(Debug)]
136struct Row {
137 time: u32,
138 vehicle: String,
139 enemy_vehicle: String,
140 reward: Reward,
141}
142
143fn table(input: &str) -> IResult<Table> {
157 let (input, (name, count, _)) = context("table header", table_header)(input)?;
158
159 let (input, rows) = context(
160 "table rows",
161 many_m_n(count as usize, count as usize, table_row),
162 )(input)?;
163 let (input, _) = line_ending(input)?; Ok((
166 input,
167 Table {
168 name: name.to_string(),
169 rows,
170 },
171 ))
172}
173
174fn table_header(input: &str) -> IResult<(String, u32, Reward)> {
175 let (input, name) =
182 context("table name", terminated(take_until(INDENT), row_separator))(input)?;
183 let (input, count) = context("row count", terminated(u32, row_separator))(input)?;
184 let (input, reward) = context("total reward", terminated(parse_reward, row_ending))(input)?;
185
186 Ok((input, (name.to_string(), count, reward)))
187}
188
189fn row_separator(input: &str) -> IResult<()> {
190 context("row separator", value((), pair(tag(INDENT), many0(space1))))(input)
191}
192
193fn row_ending(input: &str) -> IResult<()> {
194 context("row ending", value((), pair(many0(space1), line_ending)))(input)
195}
196
197fn table_row(input: &str) -> IResult<Row> {
210 let (input, (time, vehicle, enemy_vehicle, _, reward)) = tuple((
211 context(
212 "time column",
213 preceded(tag(INDENT), terminated(timestamp, row_separator)),
214 ),
215 context(
216 "vehicle column",
217 terminated(take_until(INDENT), row_separator),
218 ),
219 context(
220 "enemy vehicle column",
221 terminated(take_until(INDENT), row_separator),
222 ),
223 context("optional x", opt(pair(tag("\u{d7}"), row_separator))),
224 context("reward column", terminated(parse_reward, row_ending)),
225 ))(input)?;
226
227 Ok((
228 input,
229 Row {
230 time,
231 vehicle: vehicle.to_string(),
232 enemy_vehicle: enemy_vehicle.to_string(),
233 reward,
234 },
235 ))
236}
237
238fn timestamp(input: &str) -> IResult<u32> {
239 map(separated_pair(u32, tag(":"), u32), |(hours, minutes)| {
240 hours * 60 + minutes
241 })(input)
242}
243
244fn parse_reward(input: &str) -> IResult<Reward> {
257 let (input, (silverlions, research)) = alt((
258 pair(
259 parse_silverlions,
260 map(opt(preceded(row_separator, parse_research_points)), |rp| {
261 rp.unwrap_or_default()
262 }),
263 ),
264 pair(success(0), parse_research_points),
265 ))(input)?;
266
267 Ok((
268 input,
269 Reward {
270 silverlions,
271 research,
272 },
273 ))
274}
275
276fn parse_silverlions(input: &str) -> IResult<u32> {
277 context(
278 "silverlions",
279 alt((parse_silverlions_simple, parse_silverlions_complex)),
280 )(input)
281}
282
283fn parse_silverlions_simple(input: &str) -> IResult<u32> {
284 context("silverlions simple", terminated(u32, tag(" SL")))(input)
285}
286
287fn parse_silverlions_complex(input: &str) -> IResult<u32> {
288 let (input, (_, _, silverlions)) = tuple((
289 digit1,
290 context(
291 "additions",
292 many1(tuple((
293 tag(" + "),
294 delimited(tag("("), alpha1, tag(")")),
295 digit1,
296 ))),
297 ),
298 preceded(tag(" = "), parse_silverlions_simple),
299 ))(input)?;
300 Ok((input, silverlions))
301}
302
303fn parse_research_points(input: &str) -> IResult<u32> {
304 context(
305 "research points",
306 alt((parse_research_points_simple, parse_research_points_complex)),
307 )(input)
308}
309
310fn parse_research_points_simple(input: &str) -> IResult<u32> {
311 context("research points simple", terminated(u32, tag(" RP")))(input)
312}
313
314fn parse_research_points_complex(input: &str) -> IResult<u32> {
315 let (input, (_, _, research_points)) = tuple((
316 digit1,
317 context(
318 "additions",
319 many1(tuple((
320 tag(" + "),
321 delimited(tag("("), alpha1, tag(")")),
322 digit1,
323 ))),
324 ),
325 preceded(tag(" = "), parse_research_points_simple),
326 ))(input)?;
327 Ok((input, research_points))
328}
329
330fn parse_crp(input: &str) -> IResult<u32> {
331 terminated(u32, tag(" CRP"))(input)
332}
333
334fn parse_events(input: &str) -> IResult<Vec<Event>> {
335 let (input, tables) = context("event tables", many0(table))(input)?;
336
337 let events = tables
338 .into_iter()
339 .map(|table| {
340 table
341 .rows
342 .into_iter()
343 .map(move |row| {
344 let time = row.time;
345 let vehicle = row.vehicle.to_string();
346 let enemy = Some(row.enemy_vehicle.to_string());
347 let reward = row.reward;
348 let kind = table.name.to_string();
349
350 Event {
351 time,
352 kind,
353 vehicle,
354 enemy,
355 reward,
356 }
357 })
358 .collect::<Vec<_>>()
359 })
360 .flatten()
361 .collect::<Vec<_>>();
362
363 Ok((input, events))
364}
365
366fn award_table(input: &str) -> IResult<Vec<Award>> {
367 let (input, rows) = context("award header", preceded(table_header, many1(short_row)))(input)?;
368 let (input, _) = line_ending(input)?; let awards = rows
371 .into_iter()
372 .map(|(time, name, reward)| Award {
373 time,
374 name: name.to_string(),
375 reward,
376 })
377 .collect();
378
379 Ok((input, awards))
380}
381
382fn short_row(input: &str) -> IResult<(u32, &str, Reward)> {
383 tuple((
384 preceded(tag(INDENT), terminated(timestamp, row_separator)),
385 terminated(take_until(INDENT), row_separator),
386 terminated(parse_reward, row_ending),
387 ))(input)
388}
389
390fn vehicle_tables(input: &str) -> IResult<Vec<Vehicle>> {
391 let (input, activity_rows) = preceded(table_header, many1(short_row))(input)?;
393 let (input, _) = line_ending(input)?; let (input, _) = tuple((
397 context("Time Played literal", tag("Time Played")),
398 pair(many1(space1), digit1),
399 row_separator,
400 parse_research_points,
401 row_ending,
402 ))(input)?;
403
404 let (input, time_played_rows) = many1(tuple((
405 preceded(tag(INDENT), terminated(take_until(INDENT), row_separator)), terminated(terminated(u8, tag("%")), row_separator), terminated(timestamp, row_separator), terminated(parse_research_points, row_ending), )))(input)?;
410
411 let (input, _) = line_ending(input)?; let vehicles = activity_rows
414 .into_iter()
415 .zip(time_played_rows.into_iter())
416 .map(
417 |((_, name, reward), (_, activity, time_played, additional_rp))| Vehicle {
418 name: name.to_string(),
419 activity,
420 time_played,
421 reward: Reward {
422 silverlions: reward.silverlions,
423 research: reward.research + additional_rp,
424 },
425 },
426 )
427 .collect();
428
429 Ok((input, vehicles))
430}
431
432fn parse_other_awards(input: &str) -> IResult<Reward> {
433 delimited(
434 pair(tag("Other awards"), row_separator),
435 parse_reward,
436 pair(row_ending, line_ending),
437 )(input)
438}
439
440fn parse_reward_for_winning(input: &str) -> IResult<Reward> {
441 delimited(
442 pair(tag("Reward for winning"), row_separator),
443 parse_reward,
444 pair(row_ending, line_ending),
445 )(input)
446}
447
448fn vehicle_name(input: &str) -> IResult<String> {
450 map(
451 take_while(|c: char| match c {
452 'a'..='z' | 'A'..='Z' | '0'..='9' | ' ' => true,
453 '#' | '&' | '\'' | '(' | ')' | ',' | '-' | '.' | '/' | '_' => true,
454 _ => false,
455 }),
456 String::from,
457 )(input)
458}
459
460fn parse_earned(input: &str) -> IResult<Reward> {
461 map(
462 delimited(
463 tag("Earned: "),
464 separated_pair(parse_silverlions_simple, tag(", "), parse_crp),
465 line_ending,
466 ),
467 |(silverlions, research)| Reward {
468 silverlions,
469 research,
470 },
471 )(input)
472}
473
474fn parse_activity(input: &str) -> IResult<u8> {
475 map(
476 delimited(tag("Activity: "), terminated(u8, tag("%")), line_ending),
477 |activity| activity,
478 )(input)
479}
480
481fn parse_damaged_vehicles(input: &str) -> IResult<Vec<String>> {
482 delimited(
483 tag("Damaged Vehicles: "),
484 separated_list1(tag(", "), map(vehicle_name, String::from)),
485 line_ending,
486 )(input)
487}
488
489fn parse_automatic_repair(input: &str) -> IResult<u32> {
490 delimited(
491 tag("Automatic repair of all vehicles: -"),
492 parse_silverlions_simple,
493 line_ending,
494 )(input)
495}
496
497fn parse_automatic_purchase(input: &str) -> IResult<u32> {
498 delimited(
499 tag("Automatic purchasing of ammo and \"Crew Replenishment\": -"),
500 parse_silverlions_simple,
501 line_ending,
502 )(input)
503}
504
505fn parse_researched_units(input: &str) -> IResult<Vec<VehicleResearch>> {
506 delimited(
507 pair(tag("Researched unit: "), line_ending),
508 context("researched vehicles", many1(parse_vehicle_research)),
509 line_ending,
510 )(input)
511}
512
513fn parse_vehicle_research(input: &str) -> IResult<VehicleResearch> {
514 map(
515 terminated(
516 separated_pair(vehicle_name, tag(": "), parse_research_points_simple),
517 line_ending,
518 ),
519 |(name, research)| VehicleResearch { name, research },
520 )(input)
521}
522
523fn parse_researched_modifications(input: &str) -> IResult<Vec<ModificationResearch>> {
524 delimited(
525 pair(tag("Researching progress: "), line_ending),
526 many1(parse_modification_research),
527 line_ending,
528 )(input)
529}
530
531fn parse_modification_research(input: &str) -> IResult<ModificationResearch> {
532 dbg!(input);
533 map(
534 terminated(
535 tuple((
536 map_parser(take_until(" - "), vehicle_name),
537 tag(" - "),
538 context(
539 "name",
540 take_while(|c: char| c.is_ascii_alphanumeric() || c == ' '),
541 ),
542 tag(": "),
543 parse_research_points_simple,
544 )),
545 line_ending,
546 ),
547 |(vehicle, _, name, _, research)| ModificationResearch {
548 vehicle,
549 name: name.to_string(),
550 research,
551 },
552 )(input)
553}
554
555fn parse_used_items(input: &str) -> IResult<&str> {
556 preceded(
557 pair(tag("Used items: "), line_ending),
558 take_until("Session: "),
559 )(input)
560}
561
562fn parse_session_id(input: &str) -> IResult<String> {
563 delimited(tag("Session: "), map(hex_digit1, String::from), line_ending)(input)
564}
565
566fn parse_total(input: &str) -> IResult<(Reward, u32)> {
567 map(
568 preceded(
569 tag("Total: "),
570 tuple((
571 parse_silverlions_simple,
572 tag(", "),
573 parse_crp,
574 tag(", "),
575 parse_research_points_simple,
576 )),
577 ),
578 |(silverlions, _, crp, _, research)| {
579 (
580 Reward {
581 silverlions,
582 research,
583 },
584 crp,
585 )
586 },
587 )(input)
588}
589
590#[cfg(test)]
591mod test {
592 use std::path::PathBuf;
593
594 use nom::{error::convert_error, Finish};
595 use rstest::*;
596
597 use crate::*;
598
599 fn run_parser<T, P>(input: &str, parser: P) -> (&str, T)
600 where
601 P: Fn(&str) -> super::IResult<T>,
602 {
603 match parser(input).finish() {
604 Ok(result) => result,
605 Err(err) => panic!("\n{}", convert_error(input, err)),
606 }
607 }
608
609 #[test]
610 fn parse_victory_as_result_name() {
611 let input = "Victory";
612 assert_eq!(super::battle_result(input), Ok(("", BattleResult::Win)))
613 }
614
615 #[test]
616 fn parse_defeat_as_result_name() {
617 let input = "Defeat";
618 assert_eq!(super::battle_result(input), Ok(("", BattleResult::Loss)))
619 }
620
621 #[test]
622 fn test_parse_result_line() {
623 let input = "Victory in the [Domination] Poland (winter) mission!\r\n\n";
624 let result = super::result_line(input).finish();
625 match result {
626 Ok((_, (result, map))) => {
627 assert_eq!(result, BattleResult::Win);
628 assert_eq!(map, "[Domination] Poland (winter)")
629 }
630 Err(err) => {
631 panic!("Error parsing result line:\n{}", convert_error(input, err))
632 }
633 }
634 }
635
636 #[rstest]
637 fn test_real_data(#[files("./data/*.report")] path: PathBuf) {
638 let input = std::fs::read_to_string(&path).unwrap();
639 let result = super::parse(&input);
640 if let Err(err) = result {
641 panic!("\n{err}")
642 }
643 }
644
645 #[rstest]
646 #[case("100 RP", 100)]
647 #[case("3242 RP", 3242)]
648 fn parse_research_points_simple(#[case] input: &str, #[case] expected: u32) {
649 let (input, value) = run_parser(input, super::parse_research_points_simple);
650 assert!(input.is_empty());
651 assert_eq!(value, expected)
652 }
653
654 #[rstest]
655 #[case("10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP", 40)]
656 #[case("96 + (Talismans)96 = 192 RP", 192)]
657 #[case("113 + (Talismans)113 = 226 RP", 226)]
658 fn parse_research_points_complex(#[case] input: &str, #[case] expected: u32) {
659 let (input, value) = run_parser(input, super::parse_research_points_complex);
660 assert!(input.is_empty());
661 assert_eq!(value, expected)
662 }
663
664 #[rstest]
665 #[case("10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP", 40)]
666 #[case("100 RP", 100)]
667 #[case("96 + (Talismans)96 = 192 RP", 192)]
668 #[case("113 + (Talismans)113 = 226 RP", 226)]
669 fn parse_research_points(#[case] input: &str, #[case] expected: u32) {
670 let (input, value) = run_parser(input, super::parse_research_points);
671 assert!(input.is_empty());
672 assert_eq!(value, expected)
673 }
674
675 #[rstest]
676 #[case("5820 SL 413 RP", 5820, 413)]
677 #[case("1000 SL", 1000, 0)]
678 #[case("505 SL 10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP", 505, 40)]
679 #[case("53 + (Booster)8 = 61 SL 3 + (Booster)2 = 5 RP", 61, 5)]
680 fn parse_reward(#[case] input: &str, #[case] silverlions: u32, #[case] research: u32) {
681 let (input, reward) = run_parser(input, super::parse_reward);
682 assert_eq!("", input);
683 assert_eq!(reward.silverlions, silverlions);
684 assert_eq!(reward.research, research);
685 }
686
687 #[test]
688 fn parse_reward_in_table_header() {
689 let input = "255 SL \n 2:05 Concept 3 M36 GMC() 51 SL\n 3:04 Concept 3 M36 GMC() 51 SL\n 5:56 Concept 3 Chi-To Late 51 SL\n
690 6:25 Concept 3 M6A1 51 SL\n 6:51 Concept 3 ISU-122() 51 SL\n\nDamage taken by scouted enemies 1 101 SL \n 3:45 Concept 3 M
69136 GMC() 101 SL\n\nDestruction by allies of scouted enemies 1 505 SL 40 RP \n 3:45 Concept 3 M36 GMC() × 505 SL 10 + (PA)10 + (Booster)10 + (Talismans)10 = 40
692 RP\n";
693 let (input, reward) = run_parser(input, super::parse_reward);
694 assert!(matches!(
695 reward,
696 Reward {
697 silverlions: 255,
698 research: 0
699 }
700 ));
701
702 let leftover = " \n 2:05 Concept 3 M36 GMC() 51 SL\n 3:04 Concept 3 M36 GMC() 51 SL\n 5:56 Concept 3 Chi-To Late 51 SL\n
703 6:25 Concept 3 M6A1 51 SL\n 6:51 Concept 3 ISU-122() 51 SL\n\nDamage taken by scouted enemies 1 101 SL \n 3:45 Concept 3 M
70436 GMC() 101 SL\n\nDestruction by allies of scouted enemies 1 505 SL 40 RP \n 3:45 Concept 3 M36 GMC() × 505 SL 10 + (PA)10 + (Booster)10 + (Talismans)10 = 40
705 RP\n";
706
707 assert_eq!(input, leftover);
708 }
709
710 #[rstest]
711 #[case(
712 " 7:13 Concept 3 M6A1 1010 SL 77 RP\n",
713 7*60+13,
714 "Concept 3",
715 "M6A1",
716 1010,
717 77
718 )]
719 #[case(
720 " 8:17 Concept 3 ISU-122() 1010 SL 80 RP\n",
721 8*60+17,
722 "Concept 3",
723 "ISU-122()",
724 1010,
725 80
726 )]
727 #[case(
728 " 8:31 Concept 3 Chi-To Late 1010 SL 73 RP\n",
729 8*60+31,
730 "Concept 3",
731 "Chi-To Late",
732 1010,
733 73
734 )]
735 #[case(
736 " 10:07 Wyvern S4 Pe-8 440 SL 11 + (Talismans)11 = 22 RP\n",
737 10*60+7,
738 "Wyvern S4",
739 "Pe-8",
740 440,
741 22
742 )]
743 #[case(
744 " 13:14 Sherman Firefly Chi-Nu II 930 SL 61 RP\n",
745 13*60+14,
746 "Sherman Firefly",
747 "Chi-Nu II",
748 930,
749 61
750 )]
751 #[case(
752 " 13:43 Sherman Firefly KV-85 930 SL 64 RP\n",
753 13*60+43,
754 "Sherman Firefly",
755 "KV-85",
756 930,
757 64
758 )]
759 #[case(" 3:45 Concept 3 M36 GMC() × 505 SL 10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP\n", 3*60+45, "Concept 3", "M36 GMC()", 505, 40)]
760 fn parse_row(
761 #[case] input: &str,
762 #[case] time: u32,
763 #[case] vehice: &str,
764 #[case] enemy_vehicle: &str,
765 #[case] silverlions: u32,
766 #[case] research: u32,
767 ) {
768 let (input, row) = super::table_row(input).unwrap();
769 assert_eq!(input, "");
770 assert_eq!(row.time, time);
771 assert_eq!(row.vehicle, vehice);
772 assert_eq!(row.enemy_vehicle, enemy_vehicle);
773 assert_eq!(row.reward.silverlions, silverlions);
774 assert_eq!(row.reward.research, research);
775 }
776
777 #[test]
778 fn parse_scouting_of_the_enemy_table() {
779 let input = r#"Scouting of the enemy 5 255 SL
780 2:05 Concept 3 M36 GMC() 51 SL
781 3:04 Concept 3 M36 GMC() 51 SL
782 5:56 Concept 3 Chi-To Late 51 SL
783 6:25 Concept 3 M6A1 51 SL
784 6:51 Concept 3 ISU-122() 51 SL
785
786Damage taken by scouted enemies 1 101 SL
787 3:45 Concept 3 M36 GMC() 101 SL
788
789Destruction by allies of scouted enemies 1 505 SL 40 RP
790 3:45 Concept 3 M36 GMC() × 505 SL 10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP
791"#;
792 let (input, table) = run_parser(input, super::table);
793 assert!(!input.is_empty());
794 assert_eq!(table.name, "Scouting of the enemy");
795 assert_eq!(table.rows.len(), 5);
796 }
797
798 #[test]
799 fn parse_scouting_table_header_with_leftovers() {
800 let input = r#"Scouting of the enemy 5 255 SL
801 2:05 Concept 3 M36 GMC() 51 SL
802 3:04 Concept 3 M36 GMC() 51 SL
803 5:56 Concept 3 Chi-To Late 51 SL
804 6:25 Concept 3 M6A1 51 SL
805 6:51 Concept 3 ISU-122() 51 SL
806
807Damage taken by scouted enemies 1 101 SL
808 3:45 Concept 3 M36 GMC() 101 SL
809
810Destruction by allies of scouted enemies 1 505 SL 40 RP
811 3:45 Concept 3 M36 GMC() × 505 SL 10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP
812"#;
813 let leftover = r#" 2:05 Concept 3 M36 GMC() 51 SL
814 3:04 Concept 3 M36 GMC() 51 SL
815 5:56 Concept 3 Chi-To Late 51 SL
816 6:25 Concept 3 M6A1 51 SL
817 6:51 Concept 3 ISU-122() 51 SL
818
819Damage taken by scouted enemies 1 101 SL
820 3:45 Concept 3 M36 GMC() 101 SL
821
822Destruction by allies of scouted enemies 1 505 SL 40 RP
823 3:45 Concept 3 M36 GMC() × 505 SL 10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP
824"#;
825
826 let (input, (name, count, reward)) = run_parser(input, super::table_header);
827 assert_eq!(input, leftover);
828 assert_eq!(name, "Scouting of the enemy");
829 assert_eq!(count, 5);
830 assert_eq!(reward.silverlions, 255);
831 assert_eq!(reward.research, 0);
832 }
833
834 #[test]
835 fn parse_awards_table() {
836 let input = r#"Awards 14 3450 SL 100 RP
837 3:46 Intelligence 100 SL
838 7:14 Tank Rescuer 50 SL
839 8:18 Rank does not matter 500 SL
840 8:32 Multi strike! 100 SL
841 8:32 Without a miss 200 SL
842 10:35 Ground Force Rescuer 150 SL
843 11:47 Without a miss 200 SL
844 13:14 Without a miss 200 SL
845 13:43 Eye for Eye 300 SL
846 13:43 Shadow strike streak! 100 SL
847 13:43 Multi strike! 100 SL
848 13:43 Without a miss 200 SL
849 13:55 Final blow! 250 SL
850 13:55 The Best Squad 1000 SL 100 RP
851
852"#;
853 let (input, awards) = run_parser(input, super::award_table);
854 assert_eq!(input, "");
855 assert_eq!(awards.len(), 14);
856 }
857
858 #[test]
859 fn parse_other_awards() {
860 let input = "Other awards 5295 SL 115 RP \n\n";
861 let (input, reward) = super::parse_other_awards(input).unwrap();
862 assert_eq!(input, "");
863 assert_eq!(reward.silverlions, 5295);
864 assert_eq!(reward.research, 115);
865 }
866
867 #[test]
868 fn parse_vehicle_tables() {
869 let input = r#"Activity Time 3 3152 SL 160 RP
870 13:54 Concept 3 730 SL 68 RP
871 13:54 Sherman Firefly 522 SL 56 RP
872 13:54 Wyvern S4 1900 SL 18 + (Talismans)18 = 36 RP
873
874Time Played 3 1057 RP
875 Concept 3 97% 8:21 680 RP
876 Sherman Firefly 84% 2:51 185 RP
877 Wyvern S4 67% 1:33 96 + (Talismans)96 = 192 RP
878
879"#;
880 let (input, vehicles) = run_parser(input, super::vehicle_tables);
881 assert_eq!(input, "");
882 assert_eq!(vehicles.len(), 3);
883 assert_eq!(vehicles[0].name, "Concept 3");
884 assert_eq!(vehicles[0].activity, 97);
885 assert_eq!(vehicles[0].time_played, 8 * 60 + 21);
886 assert_eq!(vehicles[0].reward.silverlions, 730);
887 assert_eq!(vehicles[0].reward.research, 68 + 680);
888 }
889
890 #[test]
891 fn test_parse_vehicle_research() {
892 let input = "T-34 (1941): 1191 RP\n";
893 let (input, research) = run_parser(input, super::parse_vehicle_research);
894 assert_eq!(input, "");
895 assert_eq!(research.name, "T-34 (1941)");
896 assert_eq!(research.research, 1191);
897 }
898
899 #[test]
900 fn test_parse_researched_units() {
901 let input = r#"Researched unit:
902T-34 (1941): 1191 RP
903
904"#;
905 let (input, research) = run_parser(input, super::parse_researched_units);
906 assert_eq!(input, "");
907 assert_eq!(research.len(), 1);
908 assert_eq!(research[0].name, "T-34 (1941)");
909 assert_eq!(research[0].research, 1191);
910 }
911
912 #[test]
913 fn test_parse_modification_research() {
914 let input = "YaG-10 (29-K) - Improved Parts: 220 RP\n";
915 let (input, research) = run_parser(input, super::parse_modification_research);
916 assert_eq!(input, "");
917 assert_eq!(research.vehicle, "YaG-10 (29-K)");
918 assert_eq!(research.name, "Improved Parts");
919 assert_eq!(research.research, 220);
920 }
921}