Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ aoe2rec = "0.1"

## Example

```rust
<TODO>
```
cargo run --package aoe2js -- "replay.aoe2record"
```

## Build the WASM package

```
cargo install wasm-pack
cd crates/aoe2rec-js
wasm-pack build --target bundler
```

This will create a `pkg` directory inside `crates/aoe2rec-js/`.

## License

Expand Down
8 changes: 4 additions & 4 deletions crates/aoe2rec/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,12 @@ pub enum ActionData {
Research {
player_id: u8,
action_length: u16,
building_id: i32,
selected: i16,
building_id: u32,
selected: u16,
technology_type: u16,
unknown1: [u8; 5],
#[br(count = if selected > -1 { selected } else { 0 })]
building_ids: Vec<i32>,
#[br(count = (action_length.saturating_sub(13) / 4) as usize)]
building_ids: Vec<u32>,
},
#[br(magic = 102u8)]
Build {
Expand Down
41 changes: 29 additions & 12 deletions crates/aoe2rec/src/header/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use crate::DeString;

#[binrw]
#[derive(Serialize, Debug)]
#[br(import(speed: f32, n_players: u32))]
pub struct AIInfo {
#[br(parse_with = skip_ai)]
#[br(parse_with = skip_ai, args(speed, n_players))]
skip: (), // #[br(dbg)]
// max_strings: u16,
// #[br(dbg)]
Expand Down Expand Up @@ -76,18 +77,34 @@ pub struct AIFile {
}

#[binrw::parser(reader, endian)]
fn skip_ai() -> BinResult<()> {
let mut null_count = 0;
fn skip_ai(speed: f32, n_players: u32) -> BinResult<()> {
use binrw::io::SeekFrom;
let expected_players = (n_players + 1) as u8;

let mut window = [0u8; 60];
for b in &mut window {
*b = reader.read_type(endian)?;
}

loop {
let next_byte: u8 = reader.read_type(endian)?;
if next_byte == 0 {
null_count += 1
} else {
null_count = 0;
}
if null_count == 4096 {
break;
let candidate_speed = f32::from_le_bytes(window[24..28].try_into().unwrap());
let candidate_players = window[47];
let temp_pause = window[28];
let instant_build = window[48];
let cheats = window[49];

if (candidate_speed - speed).abs() < 0.001
&& candidate_players == expected_players
&& (temp_pause == 0 || temp_pause == 1)
&& (instant_build == 0 || instant_build == 1)
&& (cheats == 0 || cheats == 1)
{
reader.seek(SeekFrom::Current(-60))?;
return Ok(());
}

let next_byte: u8 = reader.read_type(endian)?;
window.copy_within(1..60, 0);
window[59] = next_byte;
}
Ok(())
}
217 changes: 214 additions & 3 deletions crates/aoe2rec/src/header/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ pub struct RecHeader {
pub interval_version: [u16; 2],
#[br(args(version_major))]
pub game_settings: GameSettings,
#[br(args(game_settings.speed, game_settings.n_players))]
pub ai_config: AIConfig,
pub replay: Replay,
pub map_info: MapInfo,
#[br(args(replay.num_players, version_major))]
#[br(args(replay.num_players, version_major, map_info.size_x, map_info.size_y))]
pub initial: Initial,
}

Expand Down Expand Up @@ -292,9 +293,10 @@ pub struct GameSettings {

#[binrw]
#[derive(Serialize, Debug)]
#[br(import(speed: f32, n_players: u32))]
pub enum AIConfig {
#[br(magic = 1u32)]
WithAI(AIInfo),
WithAI(#[br(args(speed, n_players))] AIInfo),
#[br(magic = 0u32)]
WithoutAI {},
}
Expand Down Expand Up @@ -391,7 +393,7 @@ pub struct EmptySlot {

#[binrw]
#[derive(Serialize, Debug)]
#[br(import(num_players: u8, major: u16))]
#[br(import(num_players: u8, major: u16, map_size_x: u32, map_size_y: u32))]
pub struct Initial {
pub restore_time: u32,
pub num_particles: u32,
Expand All @@ -404,6 +406,32 @@ pub struct Initial {
pub players: Vec<PlayerInit>,
#[serde(skip_serializing)]
pub unknown1: [u8; 21],
#[serde(flatten)]
#[bw(ignore)]
#[br(parse_with = parse_initial_tail, args(num_players, map_size_x, map_size_y))]
pub initial_tail: InitialTail,
}

#[derive(Serialize, Debug)]
pub struct InitialObjectsList {
pub count: u32,
pub object_ids: Vec<u32>,
}

#[derive(Serialize, Debug)]
pub struct InitialObjectInstance {
pub object_id: u32,
pub object_type_id: u16,
pub object_kind: u8,
pub player_id: u8,
pub x: f32,
pub y: f32,
}

#[derive(Serialize, Debug)]
pub struct InitialTail {
pub initial_objects: Vec<InitialObjectsList>,
pub initial_object_instances: Vec<InitialObjectInstance>,
}
#[binrw]
#[derive(Serialize, Debug, Default)]
Expand Down Expand Up @@ -522,3 +550,186 @@ pub struct Location {
pub x: u16,
pub y: u16,
}

#[binrw::parser(reader, endian)]
fn parse_initial_tail(
num_players: u8,
map_size_x: u32,
map_size_y: u32,
) -> binrw::BinResult<InitialTail> {
let _ = endian;
let mut rest = Vec::new();
reader.read_to_end(&mut rest)?;

let initial_objects = parse_initial_objects_from_bytes(&rest, num_players);
let initial_object_instances =
parse_initial_object_instances(&rest, num_players, map_size_x, map_size_y);

Ok(InitialTail {
initial_objects,
initial_object_instances,
})
}

fn parse_initial_objects_from_bytes(
rest: &[u8],
num_players: u8,
) -> Vec<InitialObjectsList> {
let mut results: Vec<InitialObjectsList> = Vec::new();
let max_lists = usize::from(num_players) + 1; // include Gaia if present
let mut i = 0usize;

while i + 5 < rest.len() && results.len() < max_lists {
if rest[i] != 0x0B {
i += 1;
continue;
}

let count = u32::from_le_bytes([
rest[i + 1],
rest[i + 2],
rest[i + 3],
rest[i + 4],
]) as usize;

if count < 50 || count > 10_000 {
i += 1;
continue;
}

let end = i + 1 + 4 + count * 4;
if end >= rest.len() {
i += 1;
continue;
}

if rest[end] != 0x0B {
i += 1;
continue;
}

let mut object_ids = Vec::with_capacity(count);
let mut cursor = i + 1 + 4;
for _ in 0..count {
let val = u32::from_le_bytes([
rest[cursor],
rest[cursor + 1],
rest[cursor + 2],
rest[cursor + 3],
]);
object_ids.push(val);
cursor += 4;
}

results.push(InitialObjectsList {
count: count as u32,
object_ids,
});

i = end + 1;
}

results
}

fn parse_initial_object_instances(
rest: &[u8],
num_players: u8,
map_size_x: u32,
map_size_y: u32,
) -> Vec<InitialObjectInstance> {
let mut results = Vec::new();

use std::collections::HashSet;
let mut seen_ids: HashSet<u32> = HashSet::new();

let mut cursor = 0usize;
while cursor + 40 < rest.len() {
let instance =
parse_object_instance_at(rest, cursor, num_players, map_size_x, map_size_y);
if let Some(instance) = instance {
if seen_ids.insert(instance.object_id) {
results.push(instance);
}
cursor += 1;
} else {
cursor += 1;
}
}

results
}

fn parse_object_instance_at(
rest: &[u8],
cursor: usize,
num_players: u8,
map_size_x: u32,
map_size_y: u32,
) -> Option<InitialObjectInstance> {
let obj_type = rest[cursor];
if !matches!(obj_type, 10 | 20 | 25 | 30 | 40 | 70 | 80 | 90) {
return None;
}
if cursor + 35 >= rest.len() {
return None;
}
let player_id = rest[cursor + 1];
if player_id > num_players {
return None;
}
let object_type_id = u16::from_le_bytes([rest[cursor + 2], rest[cursor + 3]]);

let object_id = u32::from_le_bytes([
rest[cursor + 18],
rest[cursor + 19],
rest[cursor + 20],
rest[cursor + 21],
]);
if object_id == 0 || object_id > 5_000_000 {
return None;
}

let x = f32::from_le_bytes([
rest[cursor + 23],
rest[cursor + 24],
rest[cursor + 25],
rest[cursor + 26],
]);
let y = f32::from_le_bytes([
rest[cursor + 27],
rest[cursor + 28],
rest[cursor + 29],
rest[cursor + 30],
]);
// Validate z coordinate at offset +31
let z = f32::from_le_bytes([
rest[cursor + 31],
rest[cursor + 32],
rest[cursor + 33],
rest[cursor + 34],
]);

if !x.is_finite() || !y.is_finite() || !z.is_finite() {
return None;
}
if z < -10.0 || z > 100.0 {
return None;
}
if map_size_x > 0 && map_size_y > 0 {
let max_x = map_size_x as f32 + 8.0;
let max_y = map_size_y as f32 + 8.0;
if x < 0.1 || y < 0.1 || x > max_x || y > max_y {
return None;
}
}

Some(InitialObjectInstance {
object_id,
object_type_id,
object_kind: obj_type,
player_id,
x,
y,
})
}
Loading