w3grs 0.1.0

A Rust port of w3gjs for parsing Warcraft III replay files.
Documentation
use std::path::{Path, PathBuf};

use serde_json::Value;
use w3grs::{ReplayParser, W3GReplay, game_data::GameDataBlock, replay::ObserverMode};

fn replay_fixtures() -> Vec<PathBuf> {
    let Some(root) = upstream_replay_root() else {
        return Vec::new();
    };
    let mut files = Vec::new();
    for version_dir in std::fs::read_dir(root).unwrap() {
        let version_dir = version_dir.unwrap();
        if !version_dir.file_type().unwrap().is_dir() {
            continue;
        }
        for entry in std::fs::read_dir(version_dir.path()).unwrap() {
            let entry = entry.unwrap();
            let path = entry.path();
            if matches!(
                path.extension().and_then(|extension| extension.to_str()),
                Some("w3g" | "nwg")
            ) {
                files.push(path);
            }
        }
    }
    files.sort();
    files
}

fn upstream_replay_root() -> Option<PathBuf> {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("upstream/w3gjs/test/replays");
    root.exists().then_some(root)
}

fn upstream_replay_path(relative: &str) -> Option<PathBuf> {
    upstream_replay_root().map(|root| root.join(relative))
}

#[test]
fn parses_all_upstream_replay_fixtures() {
    let fixtures = replay_fixtures();
    if fixtures.is_empty() {
        return;
    }

    let mut parser = W3GReplay::new();
    let mut parsed = 0;

    for fixture in fixtures {
        parser
            .parse_file(&fixture)
            .unwrap_or_else(|error| panic!("failed to parse {}: {error}", fixture.display()));
        parsed += 1;
    }

    assert!(parsed > 40);
}

#[test]
fn matches_selected_upstream_high_level_expectations() {
    let Some(root) = upstream_replay_path("132") else {
        return;
    };
    let mut parser = W3GReplay::new();

    let release = parser
        .parse_file(root.join("reforged_release.w3g"))
        .unwrap();
    assert_eq!(release.version, "1.32");
    assert_eq!(release.build_number, 6105);
    assert_eq!(release.players.len(), 2);
    assert_eq!(release.players[0].name, "anXieTy#2932");
    assert_eq!(release.players[1].name, "IroNSoul#22724");
    assert_eq!(release.winning_team_id, 0);

    let random = parser
        .parse_file(root.join("replay_randomhero_randomraces.w3g"))
        .unwrap();
    assert!(random.settings.random_hero);
    assert!(random.settings.random_races);
    assert_eq!(random.winning_team_id, 0);

    let full_obs = parser.parse_file(root.join("replay_fullobs.w3g")).unwrap();
    assert_eq!(full_obs.settings.observer_mode, ObserverMode::Full);

    let referee = parser.parse_file(root.join("replay_referee.w3g")).unwrap();
    assert_eq!(referee.settings.observer_mode, ObserverMode::Referees);
}

#[test]
fn serializes_high_level_output_with_w3gjs_field_names() {
    let Some(fixture) = upstream_replay_path("132/replay_fullobs.w3g") else {
        return;
    };
    let mut parser = W3GReplay::new();
    let output = parser.parse_file(fixture).unwrap();
    let json = serde_json::to_value(&output).unwrap();

    assert!(json.get("gamename").is_some());
    assert!(json.get("randomseed").is_some());
    assert!(json.get("startSpots").is_some());
    assert!(json.get("buildNumber").is_some());
    assert!(json.get("parseTime").is_some());
    assert!(json.get("winningTeamId").is_some());
    assert!(json.get("game_name").is_none());
    assert!(json.get("random_seed").is_none());
    assert!(json.get("game_type").is_none());
    assert_eq!(json["type"], Value::String("1on1".to_string()));
    assert_eq!(json["settings"]["observerMode"], "FULL");
    assert!(json["settings"].get("fullSharedUnitControl").is_some());
    assert!(json["map"].get("checksumSha1").is_some());

    let first_player = &json["players"][0];
    assert!(first_player.get("raceDetected").is_some());
    assert!(first_player.get("groupHotkeys").is_some());
    assert!(first_player.get("resourceTransfers").is_some());
    assert!(first_player.get("current_time_played").is_none());
}

#[test]
fn serializes_low_level_output_with_w3gjs_event_shapes() {
    let Some(fixture) = upstream_replay_path("132/netease_132.nwg") else {
        return;
    };
    let bytes = std::fs::read(fixture).unwrap();
    let output = ReplayParser::new().parse(&bytes).unwrap();
    let json = serde_json::to_value(&output).unwrap();

    assert!(json["header"].get("compressedSize").is_some());
    assert!(json["subheader"].get("gameIdentifier").is_some());
    assert!(json["metadata"].get("playerRecords").is_some());
    assert!(json["metadata"]["map"].get("mapChecksumSha1").is_some());
    assert!(json.get("gameDataBlocks").is_some());

    let timeslot = output
        .game_data_blocks
        .iter()
        .find_map(|block| match block {
            GameDataBlock::Timeslot(timeslot) if !timeslot.command_blocks.is_empty() => {
                Some(timeslot)
            }
            _ => None,
        })
        .unwrap();
    let block_json = serde_json::to_value(GameDataBlock::Timeslot(timeslot.clone())).unwrap();
    assert_eq!(block_json["id"], 31);
    assert!(block_json.get("timeIncrement").is_some());
    assert!(block_json.get("commandBlocks").is_some());

    let action = timeslot
        .command_blocks
        .iter()
        .flat_map(|command| &command.actions)
        .next()
        .unwrap();
    let action_json = serde_json::to_value(action).unwrap();
    assert!(action_json.get("id").and_then(Value::as_u64).is_some());
    assert!(!action_json.as_object().unwrap().is_empty());
}