wt_battle_report/
parser.rs

1//! Battle Report Parser
2
3use 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 = "    "; // 4 spaces
23
24#[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
111/// parse the first line in a battle report
112fn 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
143/// parse a table
144///
145/// # Example
146/// ```text
147/// Destruction of ground vehicles and fleets     6    5820 SL     413 RP    
148///     7:13     Concept 3          M6A1            1010 SL    77 RP
149///     8:17     Concept 3          ISU-122()       1010 SL    80 RP
150///     8:31     Concept 3          Chi-To Late     1010 SL    73 RP
151///     11:47    Sherman Firefly    T-34 (1942)     930 SL     58 RP
152///     13:14    Sherman Firefly    Chi-Nu II       930 SL     61 RP
153///     13:43    Sherman Firefly    KV-85           930 SL     64 RP
154///
155/// ```
156fn 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)?; // empty line
164
165    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, _, reward)) = tuple((
176    //    context("table name", terminated(take_until(INDENT), row_separator)),
177    //    context("row count", terminated(digit1, row_separator)),
178    //    context("total reward", terminated(parse_reward, row_ending)),
179    //))(input)?;
180
181    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
197/// parse a table row
198///
199/// # Examples
200/// ```text
201///     7:13     Concept 3          M6A1            1010 SL    77 RP
202///     8:17     Concept 3          ISU-122()       1010 SL    80 RP
203///     8:31     Concept 3          Chi-To Late     1010 SL    73 RP
204///     10:07    Wyvern S4          Pe-8            440 SL    11 + (Talismans)11 = 22 RP
205///     13:14    Sherman Firefly    Chi-Nu II       930 SL     61 RP
206///     13:43    Sherman Firefly    KV-85           930 SL     64 RP
207///     3:45    Concept 3    M36 GMC()     ×    505 SL    10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP
208/// ```
209fn 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
244/// parse a reward
245///
246/// # Examples
247/// ```text
248/// 5820 SL     413 RP
249/// ```
250/// ```text
251/// 1000 SL
252/// ```
253/// ```text
254/// 505 SL    10 + (PA)10 + (Booster)10 + (Talismans)10 = 40 RP
255/// ```
256fn 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)?; // empty line
369
370    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    // activity time
392    let (input, activity_rows) = preceded(table_header, many1(short_row))(input)?;
393    let (input, _) = line_ending(input)?; // empty line
394
395    // time played
396    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)), // name
406        terminated(terminated(u8, tag("%")), row_separator),                  // activity
407        terminated(timestamp, row_separator),                                 // time played
408        terminated(parse_research_points, row_ending),                        // reward
409    )))(input)?;
410
411    let (input, _) = line_ending(input)?; // empty line
412
413    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
448// FIXME: too greedy :(
449fn 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}