A Rust library for parsing Company of Heroes 3 replay files, extracting build orders, and accessing versioned game entity data.
cohlib centralises all CoH3 data parsing into a single dependency. It embeds 32 versions of historical game data directly into the compiled binary so consumers get a fully functional VersionedStore with no external setup required.
- Replay parsing — parse
.recfiles into structured Rust types - Build order extraction — classify player commands into a chronological build order with suspect-building detection
- Versioned game data — entity, squad, upgrade, and ability lookups with automatic version fallback (exact → nearest older → nearest newer)
- Bundled data — 32 historical game versions compiled into the library binary;
VersionedStore::bundled()works without any external files - SGA archive extraction — read Relic's
.sgaarchive format - Icon extraction — convert RRTEX icon files to WebP (BC1/BC3)
- Locale resolution — English localization strings from
.ucs, tab-separated.txt, or JSON sources - Ruby bindings — see cohlib-rb
# Cargo.toml
[dependencies]
cohlib = { path = "path/to/cohlib" }use cohlib::{parse_replay, extract_build_order, VersionedStore};
fn main() -> Result<(), cohlib::Error> {
let bytes = std::fs::read("match.rec")?;
let replay = parse_replay(&bytes)?;
println!("version: {}", replay.version());
println!("players: {}", replay.players().len());
let store = VersionedStore::bundled();
let build_order = extract_build_order(&replay, 0, &store)?;
for action in &build_order.actions {
println!("{:?} tick={} pbgid={}", action.kind, action.tick, action.pbgid);
}
Ok(())
}| Module | Description |
|---|---|
replay |
Parse .rec files into Replay, Player, Message types |
build_order |
Extract a player's build order from a parsed replay |
data |
VersionedStore with typed lookups for entities, squads, upgrades, abilities, and locale |
sga |
Open Relic SGA archives and enumerate their entries |
attrib |
Parse XML attribute files from ReferenceAttributes.sga into GameData |
locale |
Parse English localization from SGA archives, .ucs text, tab-separated .txt, or JSON |
json_import |
Import GameData from per-version JSON files (coh3-data format) |
image |
Convert RRTEX icon files to WebP |
scenario |
Extract map metadata (points, sectors, playable area) from ScenariosMP.sga |
pub fn parse_replay(bytes: &[u8]) -> Result<Replay, Error>Parse the raw bytes of a .rec file. Returns an error if the bytes are not a valid CoH3 replay.
impl Replay {
pub fn from_bytes(input: &[u8]) -> Result<Replay, ParseError>
pub fn version(&self) -> u16 // build number, e.g. 10612
pub fn timestamp(&self) -> &str // local recording time
pub fn game_type(&self) -> GameType
pub fn matchhistory_id(&self) -> Option<u64> // None for skirmish
pub fn mod_uuid(&self) -> Uuid // all-zeros = base game
pub fn map(&self) -> Map
pub fn map_filename(&self) -> &str
pub fn map_localized_name_id(&self) -> &str
pub fn map_localized_description_id(&self) -> &str
pub fn players(&self) -> Vec<Player>
pub fn length(&self) -> usize // total ticks; divide by 8 for seconds
}
pub enum GameType { Skirmish, Multiplayer, Automatch, Custom }impl Player {
pub fn name(&self) -> &str
pub fn human(&self) -> bool
pub fn faction(&self) -> Faction
pub fn team(&self) -> Team
pub fn battlegroup(&self) -> Option<u32> // pbgid of selected battlegroup
pub fn battlegroup_selected_at(&self) -> Option<u32>
pub fn ai_takeover_at(&self) -> Option<u32>
pub fn steam_id(&self) -> Option<u64>
pub fn profile_id(&self) -> Option<u64>
pub fn messages(&self) -> Vec<Message>
pub fn commands(&self) -> Vec<Command>
pub fn build_commands(&self) -> Vec<Command>
pub fn battlegroup_commands(&self) -> Vec<Command>
}
pub enum Faction { Americans, British, Wehrmacht, AfrikaKorps }
pub enum Team { First, Second } // Team::First.value() == 0impl Message {
pub fn tick(&self) -> u32 // divide by 8 for seconds
pub fn message(&self) -> &str
}pub fn extract_build_order(
replay: &Replay,
player_index: usize,
store: &VersionedStore,
) -> Result<BuildOrder, Error>Classify a player's commands into a chronological build order. player_index is a zero-based index into replay.players(). Returns an error if the index is out of range.
pub struct BuildOrder {
pub actions: Vec<BuildAction>,
}
pub struct BuildAction {
pub tick: u32, // divide by 8 for seconds
pub index: u32, // command index within tick (tie-breaking)
pub kind: BuildActionKind,
pub pbgid: u32, // entity/ability/upgrade pbgid
pub suspect: bool, // building may have been cancelled before first use
pub cancelled: bool, // action was explicitly cancelled
}
pub enum BuildActionKind {
ConstructBuilding, // UseAbility (autobuild)
TrainUnit, // BuildSquad or spawner ability
ResearchUpgrade, // BuildGlobalUpgrade
SelectBattlegroup,
SelectBattlegroupAbility,
UseBattlegroupAbility,
AITakeover, // player dropped; terminates the build order
}Suspect buildings: when a building is cancelled, cohlib marks subsequent buildings of the same type as suspects until production resumes from one of them. Suspect actions should be validated against actual production before being displayed. Actions with cancelled: true are always excluded from the returned actions list.
impl VersionedStore {
/// Load all compiled-in historical game data. No external files required.
pub fn bundled() -> Self
/// Start empty.
pub fn new() -> Self
/// Load from a directory of per-version game_data.json files.
pub fn from_dir(dir: &Path) -> Result<Self, Error>
/// Add or replace a version at runtime.
pub fn add_version(&mut self, data: GameData)
/// Number of loaded versions.
pub fn version_count(&self) -> usize
// Typed lookups — all use version fallback:
// exact build → nearest older → nearest newer
pub fn get_entity(&self, pbgid: u32, build: Version) -> Option<&Entity>
pub fn get_squad(&self, pbgid: u32, build: Version) -> Option<&Squad>
pub fn get_upgrade(&self, pbgid: u32, build: Version) -> Option<&Upgrade>
pub fn get_ability(&self, pbgid: u32, build: Version) -> Option<&Ability>
pub fn get_entity_by_path(&self, path: &str, build: Version) -> Option<&Entity>
pub fn localize(&self, loc_id: u32, build: Version) -> Option<&str>
}Version is u32 — the numeric build number from the replay header (e.g. 10612).
pub struct Entity {
pub pbgid: u32,
pub path: Vec<String>, // hierarchical path, e.g. ["ebps", "races", "american", ...]
pub loc_id: u32,
pub icon_name: String,
pub spawns: Vec<String>, // squad paths this building can produce
pub upgrades: Vec<String>, // upgrade paths available from this building
pub resource: Option<ResourceIncome>, // territory point ebps only
pub capture: Option<CaptureInfo>, // territory point ebps only
}
pub struct Squad {
pub pbgid: u32,
pub path: Vec<String>,
pub loc_id: u32,
pub icon_name: String,
}
pub struct Upgrade {
pub pbgid: u32,
pub path: Vec<String>,
pub loc_id: u32,
pub icon_name: String,
}
pub struct Ability {
pub pbgid: u32,
pub path: Vec<String>,
pub loc_id: u32,
pub icon_name: String,
pub autobuild: bool, // true → UseAbility of this ability places a building
pub builds: Option<String>, // entity path of the building it places
}
pub struct LocaleStore(pub HashMap<u32, String>);
impl LocaleStore {
pub fn get(&self, id: u32) -> Option<&str>
}
pub struct GameData {
pub version: Version,
pub entities: HashMap<u32, Entity>,
pub squads: HashMap<u32, Squad>,
pub upgrades: HashMap<u32, Upgrade>,
pub abilities: HashMap<u32, Ability>,
pub locale: LocaleStore,
}impl VersionedStore {
/// Full scenario record: dimensions, points, sectors, playable area.
pub fn get_scenario(&self, scenario: &str, build: Version) -> Option<&Scenario>
/// Shorthand for get_scenario(...).map(|s| &s.size).
pub fn get_map_size(&self, scenario: &str, build: Version) -> Option<&MapSize>
}scenario accepts either a replay's raw map_filename() form (data:scenarios\...) or the normalized forward-slash form.
pub struct Scenario {
pub size: MapSize,
pub playable_area: Option<Rect>, // authored playable region; smaller than `size`
pub max_players: u32,
pub teams: [u32; 2],
pub author: String,
pub name_loc_id: u32,
pub description_loc_id: u32,
pub scenario_type: u32,
pub map_origin: u32, // 2 == community (Workshop) map
pub visible_in_lobby: bool,
pub points: Vec<ScenarioPoint>,
pub sectors: Vec<Sector>,
}
pub struct ScenarioPoint {
pub ebp: String, // e.g. "territory_fuel_point_low_smaller"
pub x: f32,
pub y: f32,
pub kind: PointKind, // Fuel | Munitions | Manpower | Victory | Start | Other
pub tier: Option<PointTier>, // ExtraLow | Low | Medium | ExtraMedium | High
pub owner: Option<u32>, // player index, for Start points
pub income_per_minute: f32,
pub capture_time: Option<f32>,
pub sector: Option<u32>,
}
pub struct Sector {
pub id: u32,
pub is_base: bool, // player base/HQ sector
pub neighbors: Vec<u32>,
pub bounds: Rect,
pub points: Vec<usize>, // indices into Scenario::points
pub rings: Vec<Vec<[f32; 2]>>, // outline(s), sharing exact coordinates with neighbors
}
pub struct Rect { pub min_x: f32, pub min_y: f32, pub max_x: f32, pub max_y: f32 }
impl Rect {
pub fn width(&self) -> f32
pub fn height(&self) -> f32
}Entity also carries resource income and capture timing for territory point ebps:
pub struct ResourceIncome { pub kind: ResourceKind, pub per_second: f32 }
pub enum ResourceKind { Fuel, Munitions, Manpower }
pub struct CaptureInfo { pub capture_time: f32, pub revert_time: f32 }pub struct ArchiveEntry {
pub path: String, // normalized forward-slash path relative to archive root
pub bytes: Vec<u8>,
}
impl ArchiveEntry {
pub fn extension(&self) -> Option<&str>
}
/// Open an SGA archive and return all of its entries.
pub fn open_archive(path: &Path) -> Result<Vec<ArchiveEntry>, Error>
/// Read the NiceName string from an SGA archive header.
pub fn read_archive_name(path: &Path) -> Result<String, Error>/// Process XML attribute entries from ReferenceAttributes.sga into GameData.
pub fn extract_game_data(
entries: &[ArchiveEntry],
locale: LocaleStore,
version: Version,
) -> Result<GameData, Error>/// Decrypt and parse LocaleEnglish.sga (AES-128-CBC + zlib + UCS).
pub fn parse_locale_sga(path: &Path) -> Result<LocaleStore, Error>
/// Parse a decrypted .ucs text (UTF-16 LE or UTF-8).
pub fn parse_locale_ucs(text: &str) -> Result<LocaleStore, Error>
/// Parse tab-separated locale text (cohdata format).
pub fn parse_locale_txt(text: &str) -> Result<LocaleStore, Error>
/// Parse a JSON locale map.
pub fn parse_locale_json(json: &str) -> Result<LocaleStore, Error>/// Import GameData from a directory containing abilities.json, ebps.json,
/// sbps.json, upgrade.json, and locale.json or locale.txt.
pub fn import_version(data_dir: &Path, version: Version) -> Result<GameData, Error>/// Convert RRTEX bytes to WebP bytes. Supports BC1 (DXT1) and BC3 (DXT5).
pub fn extract_icon(rrtex_bytes: &[u8]) -> Result<Vec<u8>, Error>pub enum Error {
Sga(String),
Attrib(String),
Locale(String),
JsonImport(String),
Replay(String),
BuildOrder(String),
}All public functions return Result<T, cohlib::Error> and never panic.
| Flag | Description |
|---|---|
magnus |
Enables #[magnus::wrap] on public types for use in Ruby FFI bindings. Used by cohlib-rb. |
trace |
Enables nom-tracable tracing in the replay parser. For debugging only. |
The cohlib binary provides maintainer tooling for managing the bundled game data. It is not required for normal library use.
Import historical game data from cohdata-format source directories.
cohlib populate <source_dir>... --output <data_dir>
Each <source_dir> is scanned for numeric subdirectories (e.g. 10612/). Each subdirectory must contain abilities.json, ebps.json, sbps.json, upgrade.json, and optionally locale.txt or locale.json. The result is written to <data_dir>/<version>/game_data.json.
If multiple source directories contain the same version, the first one wins.
Example:
cohlib populate ~/cohdata/data ~/Code/reinforce/data --output data/Extract game data from a CoH3 SGA depot for a specific build.
cohlib import <depot_path> --version <build_number> --output <data_dir>
Reads <depot_path>/anvil/archives/ReferenceAttributes.sga for entity data and LocaleEnglish.sga for locale strings, and ScenariosMP.sga for map metadata (points, sectors, playable area). Writes <data_dir>/<version>/game_data.json plus any new/changed scenario records under <data_dir>/scenarios/ — see Scenario (map) data.
Example:
cohlib import ~/Steam/steamapps/common/coh3/depot_1677281 \
--version 21283 \
--output data/After running import, commit the new data/<version>/ directory. The next cargo build picks it up and incorporates it into the compiled-in bundle automatically.
When a new CoH3 patch ships:
- Download the new depot via SteamCMD.
- Run
cohlib import <depot_path> --version <build_number> --output data/. - Commit
data/<build_number>/game_data.json. - Run
cargo build— the new version is incorporated into the bundle. - Publish a new crate release. All consumers of
VersionedStore::bundled()get the new version automatically.
cargo test # all unit + integration tests
cargo test --test build_order # end-to-end build order comparison vs reinforce
cargo test --test bundled # bundled data version and lookup tests
cargo test --test replay # replay parse tests
cargo clippy -- -D warnings
cargo fmt --checkScenario map metadata extraction (crates/scenario/) — specifically the .layer/.scenario placed-entity scan and the sector boundary/geometry approach — is informed by cohstats/coh3-data's scripts/mp-maps/, the only other public CoH3 scenario/territory parser and the source of that format documentation. See the relevant module doc comments in crates/scenario/src/ for what's derived from it versus this crate's own work (notably, playable-area extraction uses the map's authored soft-edge mask rather than their point-bounding-box estimate).