Skip to main content

subtr_actor/ballchasing/
compare.rs

1use std::path::Path;
2
3use anyhow::Context;
4use serde::Serialize;
5use serde_json::Value;
6
7use super::comparison::{
8    build_actual_comparable_stats, build_expected_comparable_stats, compute_comparable_stats,
9    MatchConfig, StatMatcher,
10};
11use super::report::BallchasingComparisonReport;
12use crate::*;
13
14#[derive(Debug, Clone, Serialize)]
15pub struct BallchasingComparableStats {
16    pub actual: Value,
17    pub expected: Value,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct BallchasingComparisonBreakdown {
22    pub is_match: bool,
23    pub mismatches: Vec<String>,
24    pub comparable_stats: BallchasingComparableStats,
25}
26
27pub fn parse_replay_bytes(data: &[u8]) -> anyhow::Result<boxcars::Replay> {
28    boxcars::ParserBuilder::new(data)
29        .always_check_crc()
30        .must_parse_network_data()
31        .parse()
32        .context("Failed to parse replay")
33}
34
35pub fn parse_replay_file(path: impl AsRef<Path>) -> anyhow::Result<boxcars::Replay> {
36    let path = path.as_ref();
37    let data = std::fs::read(path)
38        .with_context(|| format!("Failed to read replay file: {}", path.display()))?;
39    parse_replay_bytes(&data).with_context(|| format!("Failed to parse replay: {}", path.display()))
40}
41
42pub fn compare_replay_against_ballchasing(
43    replay: &boxcars::Replay,
44    ballchasing: &Value,
45    config: &MatchConfig,
46) -> SubtrActorResult<BallchasingComparisonReport> {
47    let computed = compute_comparable_stats(replay)?;
48    let actual = build_actual_comparable_stats(&computed);
49    let expected = build_expected_comparable_stats(ballchasing);
50
51    let mut matcher = StatMatcher::default();
52    expected.compare(&actual, &mut matcher, config);
53    Ok(BallchasingComparisonReport {
54        mismatches: matcher.into_mismatches(),
55    })
56}
57
58pub fn compare_replay_against_ballchasing_with_breakdown(
59    replay: &boxcars::Replay,
60    ballchasing: &Value,
61    config: &MatchConfig,
62) -> SubtrActorResult<BallchasingComparisonBreakdown> {
63    let computed = compute_comparable_stats(replay)?;
64    let actual = build_actual_comparable_stats(&computed);
65    let expected = build_expected_comparable_stats(ballchasing);
66
67    let mut matcher = StatMatcher::default();
68    expected.compare(&actual, &mut matcher, config);
69    let mismatches = matcher.into_mismatches();
70
71    Ok(BallchasingComparisonBreakdown {
72        is_match: mismatches.is_empty(),
73        mismatches,
74        comparable_stats: BallchasingComparableStats {
75            actual: serde_json::to_value(&actual).expect("comparable stats should serialize"),
76            expected: serde_json::to_value(&expected).expect("comparable stats should serialize"),
77        },
78    })
79}
80
81pub fn compare_replay_against_ballchasing_json_with_breakdown(
82    replay_path: impl AsRef<Path>,
83    json_path: impl AsRef<Path>,
84    config: &MatchConfig,
85) -> anyhow::Result<BallchasingComparisonBreakdown> {
86    let replay_path = replay_path.as_ref();
87    let json_path = json_path.as_ref();
88    let replay = parse_replay_file(replay_path)?;
89    let json_file = std::fs::File::open(json_path)
90        .with_context(|| format!("Failed to open ballchasing json: {}", json_path.display()))?;
91    let ballchasing: Value = serde_json::from_reader(json_file)
92        .with_context(|| format!("Failed to parse ballchasing json: {}", json_path.display()))?;
93
94    compare_replay_against_ballchasing_with_breakdown(&replay, &ballchasing, config)
95        .map_err(|error| anyhow::Error::new(error.variant))
96}
97
98pub fn compare_replay_against_ballchasing_json(
99    replay_path: impl AsRef<Path>,
100    json_path: impl AsRef<Path>,
101    config: &MatchConfig,
102) -> anyhow::Result<BallchasingComparisonReport> {
103    let replay_path = replay_path.as_ref();
104    let json_path = json_path.as_ref();
105    let replay = parse_replay_file(replay_path)?;
106    let json_file = std::fs::File::open(json_path)
107        .with_context(|| format!("Failed to open ballchasing json: {}", json_path.display()))?;
108    let ballchasing: Value = serde_json::from_reader(json_file)
109        .with_context(|| format!("Failed to parse ballchasing json: {}", json_path.display()))?;
110
111    compare_replay_against_ballchasing(&replay, &ballchasing, config)
112        .map_err(|error| anyhow::Error::new(error.variant))
113}
114
115pub fn compare_fixture_directory(
116    path: &Path,
117    config: &MatchConfig,
118) -> anyhow::Result<BallchasingComparisonReport> {
119    let (replay_path, json_path) = if path.is_dir() {
120        (path.join("replay.replay"), path.join("ballchasing.json"))
121    } else {
122        (
123            path.with_extension("replay"),
124            path.with_extension("ballchasing.json"),
125        )
126    };
127    compare_replay_against_ballchasing_json(&replay_path, &json_path, config)
128}