Skip to content

Commit

Permalink
Merge pull request #22 from ryantaylor/cpu
Browse files Browse the repository at this point in the history
Support Parsing CPU and Modded Replays
  • Loading branch information
ryantaylor authored Jan 21, 2024
2 parents 9d1abde + 09a08c1 commit d17bdb1
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 57 deletions.
25 changes: 12 additions & 13 deletions CoH3Rec.bt
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ struct PLAYER {
uint32 zero_six;
};

struct OPTIONCONFIG {
uint32 name_length;
char name[name_length];
uint32 value;
};

struct DATADATACHUNK {
CHUNKHEADER header;
local uint32 start = FTell();
Expand All @@ -145,19 +151,12 @@ struct DATADATACHUNK {
char data_c[length_c];
uint64 matchhistory_id;
uint32 some_flag_probs_not_length;
char data_b[20];
uint32 resource_section_length;
char resource_section[resource_section_length];
uint32 one_a;
uint32 option_resources_length;
char option_resources[option_resources_length];
uint32 zero_c;
uint32 section_tickets_length;
char section_tickets[section_tickets_length];
uint32 one_b;
uint32 option_tickets_length;
char option_tickets[option_tickets_length];
uint32 flag_b;
uint32 usually_zero;
uint32 usually_one;
uint32 one_or_zero;
uint32 option_group_count;
uint32 options_per_group;
OPTIONCONFIG options[option_group_count * options_per_group] <optimize=false>;
uint32 zero_d;
uint32 zero_e;
uint32 zero_f;
Expand Down
Binary file added replays/vs_ai.rec
Binary file not shown.
56 changes: 29 additions & 27 deletions src/data/chunks/data_data_chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,34 @@ use crate::data::parser::parse_utf8_variable;
use crate::data::{ParserResult, Player, Span};
use nom::bytes::complete::take;
use nom::combinator::{cut, map, map_parser};
use nom::multi::{length_count, length_data};
use nom::multi::{fold_many_m_n, length_count, length_data};
use nom::number::complete::{le_u32, le_u64};
use nom::sequence::tuple;
use nom_tracable::tracable_parser;

#[derive(Debug)]
pub struct Option {
pub name: String,
pub value: u32,
}

impl Option {
#[tracable_parser]
pub fn parse_option(input: Span) -> ParserResult<Option> {
map(
tuple((parse_utf8_variable(le_u32), le_u32)),
|((_, name), value)| Option { name, value },
)(input)
}
}

#[derive(Debug)]
pub struct DataDataChunk {
pub header: Header,
pub opponent_type: u32,
pub players: Vec<Player>,
pub matchhistory_id: u64,
pub section_resources: String,
pub option_resources: String,
pub section_tickets: String,
pub option_tickets: String,
pub options: Vec<Option>,
pub unknown_string: String,
}

Expand All @@ -36,18 +49,11 @@ impl DataDataChunk {
take(6u32),
Self::parse_players(version),
length_data(le_u32),
take(4u32),
length_data(le_u32),
le_u64,
take(4u32),
take(20u32),
Self::parse_resource_string,
take(4u32),
Self::parse_resource_string,
take(4u32),
Self::parse_resource_string,
take(4u32),
Self::parse_resource_string,
take(16u32),
length_count(Self::parse_options_length, Option::parse_option),
take(12u32),
Self::parse_resource_string,
)),
|(
Expand All @@ -58,14 +64,7 @@ impl DataDataChunk {
_,
matchhistory_id,
_,
_,
section_resources,
_,
option_resources,
_,
section_tickets,
_,
option_tickets,
options,
_,
unknown_string,
)| {
Expand All @@ -74,10 +73,7 @@ impl DataDataChunk {
opponent_type,
players,
matchhistory_id,
section_resources,
option_resources,
section_tickets,
option_tickets,
options,
unknown_string,
})
},
Expand All @@ -94,8 +90,14 @@ impl DataDataChunk {
move |input: Span| length_count(le_u32, Player::parse_player(version))(input)
}

#[tracable_parser]
fn parse_resource_string(input: Span) -> ParserResult<String> {
let (input, (_, section_resources)) = parse_utf8_variable(le_u32)(input)?;
Ok((input, section_resources))
}

#[tracable_parser]
fn parse_options_length(input: Span) -> ParserResult<u32> {
fold_many_m_n(2, 2, le_u32, || -> u32 { 1 }, |acc: u32, item| acc * item)(input)
}
}
25 changes: 19 additions & 6 deletions src/data/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ use crate::data::{ParserResult, Span};
use nom::bytes::complete::take;
use nom::combinator::{cut, map};
use nom::multi::many_m_n;
use nom::number::complete::{le_u32, le_u64};
use nom::number::complete::{le_u32, le_u64, le_u8};
use nom::sequence::tuple;
use nom::IResult;
use nom_tracable::tracable_parser;

#[derive(Debug)]
pub struct Player {
pub id: u32,
pub human: u8,
pub name: String,
pub team: u32,
pub faction: String,
Expand All @@ -25,7 +27,7 @@ impl Player {
move |input: Span| {
let (input, player) = cut(map(
tuple((
take(1u32),
le_u8,
Self::parse_name,
Self::parse_team,
le_u32,
Expand All @@ -39,9 +41,10 @@ impl Player {
Self::parse_steam_id,
take(18u32),
)),
|(_, name, team, id, _, faction, _, ai_type, _, profile_id, _, steam_id, _)| {
|(human, name, team, id, _, faction, _, ai_type, _, profile_id, _, steam_id, _)| {
Player {
id,
human,
name,
team,
faction,
Expand All @@ -53,40 +56,50 @@ impl Player {
},
))(input)?;

let (input, items) = Self::parse_items(input, &player.faction, version)?;
let (input, items) = Self::parse_items(input, &player, version)?;
let (input, _) = take(4u32)(input)?;
Ok((input, Player { items, ..player }))
}
}

#[tracable_parser]
fn parse_name(input: Span) -> ParserResult<String> {
let (input, (_, name)) = parse_utf16_variable(le_u32)(input)?;
Ok((input, name))
}
#[tracable_parser]
fn parse_team(input: Span) -> ParserResult<u32> {
le_u32(input)
}
#[tracable_parser]
fn parse_faction(input: Span) -> ParserResult<String> {
let (input, (_, faction)) = parse_utf8_variable(le_u32)(input)?;
Ok((input, faction))
}
#[tracable_parser]
fn parse_ai(input: Span) -> ParserResult<String> {
let (input, (_, ai)) = parse_utf8_variable(le_u32)(input)?;
Ok((input, ai))
}
#[tracable_parser]
fn parse_steam_id(input: Span) -> ParserResult<String> {
let (input, (_, steam_id)) = parse_utf16_variable(le_u32)(input)?;
Ok((input, steam_id))
}

fn parse_items<'a>(
input: Span<'a>,
faction: &str,
player: &Player,
version: u16,
) -> IResult<Span<'a>, Vec<Item>> {
if player.human == 0 {
let (input, _) = take(44u32)(input)?;
return Ok((input, vec![]));
}

cut(many_m_n(
0,
Item::get_item_count(faction, version),
Item::get_item_count(&player.faction, version),
Item::parse_item,
))(input)
}
Expand Down
33 changes: 22 additions & 11 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ use std::fmt::{Display, Formatter};
#[cfg_attr(feature = "magnus", magnus::wrap(class = "VaultCoh::Player"))]
pub struct Player {
name: String,
human: bool,
faction: Faction,
team: Team,
battlegroup: Option<u32>,
steam_id: u64,
profile_id: u64,
steam_id: Option<u64>,
profile_id: Option<u64>,
messages: Vec<Message>,
commands: Vec<Command>,
}
Expand All @@ -35,6 +36,10 @@ impl Player {
pub fn name(&self) -> &str {
&self.name
}
// Whether or not the player was a human or an AI/CPU player.
pub fn human(&self) -> bool {
self.human
}
/// The faction selected by the player in this match.
pub fn faction(&self) -> Faction {
self.faction
Expand All @@ -49,15 +54,15 @@ impl Player {
pub fn battlegroup(&self) -> Option<u32> {
self.battlegroup
}
/// The Steam ID of the player. This ID can be used to uniquely identify a player between
/// replays, and connect them to their Steam profile.
pub fn steam_id(&self) -> u64 {
/// The Steam ID of the player, or `None` if the player is AI. This ID can be used to uniquely
/// identify a player between replays, and connect them to their Steam profile.
pub fn steam_id(&self) -> Option<u64> {
self.steam_id
}
/// The Relic profile ID of the player. This ID can be used to uniquely identify a player
/// between replays, and can be used to query statistical information about the player from
/// Relic's stats API.
pub fn profile_id(&self) -> u64 {
/// The Relic profile ID of the player, or `None` if the player is AI. This ID can be used to
/// uniquely identify a player between replays, and can be used to query statistical information
/// about the player from Relic's stats API.
pub fn profile_id(&self) -> Option<u64> {
self.profile_id
}
/// A list of all messages sent by the player in the match. Sorted chronologically from first
Expand Down Expand Up @@ -110,15 +115,21 @@ impl Player {
pub(crate) fn player_from_data(player_data: &PlayerData, ticks: Vec<&Tick>) -> Player {
let mut player = Player {
name: player_data.name.clone(),
human: player_data.human != 0,
faction: Faction::try_from(player_data.faction.as_ref()).unwrap(),
team: Team::try_from(player_data.team).unwrap(),
steam_id: str::parse(&player_data.steam_id).unwrap(),
profile_id: player_data.profile_id,
steam_id: None,
profile_id: None,
messages: messages_from_data(&ticks, &player_data.name),
commands: commands_from_data(&ticks, player_data.id),
battlegroup: None,
};

if player.human {
player.steam_id = Some(str::parse(&player_data.steam_id).unwrap());
player.profile_id = Some(player_data.profile_id);
}

player.battlegroup = match player
.commands
.iter()
Expand Down
17 changes: 17 additions & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ fn parse_failure() {
assert!(replay.is_err())
}

#[test]
fn parse_success_ai() {
let data = include_bytes!("../replays/vs_ai.rec");
let replay = Replay::from_bytes(data);
assert!(replay.is_ok());
let unwrapped = replay.unwrap();
assert_eq!(unwrapped.version(), 21283);
assert_eq!(
unwrapped
.players()
.iter()
.map(|player| { player.name() })
.collect::<Vec<&str>>(),
vec!["Janne252", "CPU - Standard"]
);
}

#[test]
fn parse_weird_description() {
let data = include_bytes!("../replays/weird_description.rec");
Expand Down

0 comments on commit d17bdb1

Please sign in to comment.