more and better

This commit is contained in:
Robin Appelman 2022-08-25 23:55:50 +02:00
commit 0572f34c99
6 changed files with 745 additions and 177 deletions

View file

@ -1,9 +1,8 @@
#![feature(const_generics)]
#![allow(incomplete_features)]
#![macro_use]
use crate::state::ParsedDemo;
use tf_demo_parser::demo::header::Header;
use tf_demo_parser::demo::parser::analyser::UserInfo;
use tf_demo_parser::demo::parser::gamestateanalyser::{GameStateAnalyser, World};
use tf_demo_parser::demo::vector::Vector;
use tf_demo_parser::{Demo, DemoParser, ParseError};
@ -43,8 +42,15 @@ impl From<World> for WorldBoundaries {
#[wasm_bindgen]
pub struct FlatState {
pub player_count: usize,
pub building_count: usize,
pub boundaries: WorldBoundaries,
pub interval_per_tick: f32,
kill_ticks: Box<[u32]>,
attackers: Box<[u8]>,
assisters: Box<[u8]>,
victims: Box<[u8]>,
weapons: Vec<String>,
player_info: Vec<UserInfo>,
data: Box<[u8]>,
header: Header,
}
@ -52,21 +58,45 @@ pub struct FlatState {
impl FlatState {
pub fn new(parsed: ParsedDemo, world: World) -> Self {
let ParsedDemo {
players, header, ..
players,
header,
buildings,
..
} = parsed;
let player_count = players.len();
let building_count = buildings.len();
let flat: Vec<_> = players
.into_iter()
.chain(buildings.into_iter())
.flat_map(|player| player.into_iter())
.collect();
FlatState {
player_count,
building_count,
boundaries: world.into(),
interval_per_tick: header.duration / (header.ticks as f32),
data: flat.into_boxed_slice(),
kill_ticks: parsed.kills.iter().map(|kill| kill.tick as u32).collect(),
attackers: parsed
.kills
.iter()
.map(|kill| kill.attacker_id as u8)
.collect(),
assisters: parsed
.kills
.iter()
.map(|kill| kill.assister_id as u8)
.collect(),
victims: parsed
.kills
.iter()
.map(|kill| kill.victim_id as u8)
.collect(),
weapons: parsed.kills.into_iter().map(|kill| kill.weapon).collect(),
player_info: parsed.player_info,
header,
}
}
@ -91,6 +121,46 @@ pub fn get_map(state: &FlatState) -> String {
state.header.map.clone()
}
#[wasm_bindgen]
pub fn get_kill_ticks(state: &FlatState) -> Box<[u32]> {
state.kill_ticks.clone()
}
#[wasm_bindgen]
pub fn get_attacker_ids(state: &FlatState) -> Box<[u8]> {
state.attackers.clone()
}
#[wasm_bindgen]
pub fn get_assister_ids(state: &FlatState) -> Box<[u8]> {
state.assisters.clone()
}
#[wasm_bindgen]
pub fn get_victim_ids(state: &FlatState) -> Box<[u8]> {
state.victims.clone()
}
#[wasm_bindgen]
pub fn get_weapon(state: &FlatState, kill_id: usize) -> String {
state.weapons[kill_id].clone()
}
#[wasm_bindgen]
pub fn get_player_name(state: &FlatState, player_id: usize) -> String {
state.player_info[player_id].name.clone()
}
#[wasm_bindgen]
pub fn get_player_entity_id(state: &FlatState, player_id: usize) -> u32 {
state.player_info[player_id].entity_id.into()
}
#[wasm_bindgen]
pub fn get_player_steam_id(state: &FlatState, player_id: usize) -> String {
state.player_info[player_id].steam_id.clone()
}
pub fn parse_demo_inner(buffer: &[u8]) -> Result<(ParsedDemo, Option<World>), ParseError> {
let demo = Demo::new(buffer);
let parser = DemoParser::new_with_analyser(demo.get_stream(), GameStateAnalyser::default());
@ -108,6 +178,7 @@ pub fn parse_demo_inner(buffer: &[u8]) -> Result<(ParsedDemo, Option<World>), Pa
}
let world: Option<&World> = ticker.state().world.as_ref();
parsed_demo.kills = ticker.state().kills.clone();
Ok((parsed_demo, world.map(|w| w.clone())))
}

View file

@ -1,5 +1,8 @@
use tf_demo_parser::demo::header::Header;
use tf_demo_parser::demo::parser::gamestateanalyser::{Class, GameState, Team, World};
use tf_demo_parser::demo::parser::analyser::UserInfo;
use tf_demo_parser::demo::parser::gamestateanalyser::{
Building, Class, GameState, Kill, PlayerState as PlayerAliveState, Team, World,
};
use tf_demo_parser::demo::vector::VectorXY;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@ -23,7 +26,10 @@ impl From<Angle> for f32 {
pub struct ParsedDemo {
pub tick: usize,
pub players: Vec<Vec<u8>>,
pub buildings: Vec<Vec<u8>>,
pub kills: Vec<Kill>,
pub header: Header,
pub player_info: Vec<UserInfo>,
}
impl ParsedDemo {
@ -31,6 +37,9 @@ impl ParsedDemo {
ParsedDemo {
tick: 0,
players: Vec::new(),
buildings: Vec::new(),
kills: Vec::new(),
player_info: Vec::new(),
header,
}
}
@ -41,18 +50,42 @@ impl ParsedDemo {
let state = PlayerState {
position: player.position.into(),
angle: Angle::from(player.view_angle),
health: player.health,
health: if player.state == PlayerAliveState::Alive {
player.health
} else {
0
},
team: player.team,
class: player.class,
charge: player.charge,
};
if let None = self.players.get(index) {
let mut new_player = Vec::default();
let mut new_player =
Vec::with_capacity(self.header.ticks as usize * PlayerState::PACKET_SIZE);
// backfill with defaults
new_player.resize(self.tick * PlayerState::PACKET_SIZE, 0);
self.players.push(new_player);
};
match (self.player_info.get(index), player.info.as_ref()) {
(None, Some(info)) => self.player_info.push(info.clone()),
_ => {}
}
let parsed_player = &mut self.players[index];
parsed_player.extend_from_slice(&state.pack(world));
}
for (index, building) in game_state.buildings.iter().enumerate() {
let state = BuildingState::new(building);
if let None = self.buildings.get(index) {
let mut new_building =
Vec::with_capacity(self.header.ticks as usize * BuildingState::PACKET_SIZE);
new_building.resize(self.tick * BuildingState::PACKET_SIZE, 0);
self.buildings.push(new_building);
};
let parsed_player = &mut self.players[index];
parsed_player.extend_from_slice(&state.pack(world));
}
@ -74,12 +107,13 @@ pub struct PlayerState {
health: u16,
team: Team,
class: Class,
charge: u8,
}
impl PlayerState {
const PACKET_SIZE: usize = 8;
pub fn pack(&self, world: &World) -> [u8; 7] {
pub fn pack(&self, world: &World) -> [u8; Self::PACKET_SIZE] {
// for the purpose of viewing the demo in the browser we dont really need high accuracy for
// position or angle, so we save a bunch of space by truncating those down to half the number
// of bits
@ -97,6 +131,223 @@ impl PlayerState {
((self.team as u16) << 14) + ((self.class as u16) << 10) + self.health;
let combined_bytes = team_class_health.to_le_bytes();
[
x[0],
x[1],
y[0],
y[1],
combined_bytes[0],
combined_bytes[1],
self.angle.0,
self.charge,
]
}
#[allow(dead_code)]
pub fn unpack(bytes: [u8; 8], world: &World) -> Self {
fn unpack_f32(val: u16, min: f32, max: f32) -> f32 {
let ratio = val as f32 / (u16::max_value() as f32);
ratio * (max - min) + min
}
let x = unpack_f32(
u16::from_le_bytes([bytes[0], bytes[1]]),
world.boundary_min.x,
world.boundary_max.x,
);
let y = unpack_f32(
u16::from_le_bytes([bytes[2], bytes[3]]),
world.boundary_min.y,
world.boundary_max.y,
);
let team_class_health = u16::from_le_bytes([bytes[4], bytes[5]]);
let health = team_class_health & 1023;
let angle = Angle(bytes[6]);
let team = Team::new(team_class_health >> 14);
let class = Class::new((team_class_health >> 10) & 15);
let charge = bytes[7];
PlayerState {
position: VectorXY { x, y },
angle,
health,
team,
class,
charge,
}
}
}
#[test]
fn test_player_packing() {
use tf_demo_parser::demo::vector::Vector;
let world = World {
boundary_max: Vector {
x: 10000.0,
y: 10000.0,
z: 100.0,
},
boundary_min: Vector {
x: -10000.0,
y: -10000.0,
z: -100.0,
},
};
let input = PlayerState {
position: VectorXY {
x: 100.0,
y: -5000.0,
},
angle: Angle::from(213.0),
health: 250,
team: Team::Blue,
class: Class::Demoman,
charge: 7,
};
let bytes = input.pack(&world);
let unpacked = PlayerState::unpack(bytes, &world);
assert_eq!(input.angle, unpacked.angle);
assert_eq!(input.health, unpacked.health);
assert_eq!(input.class, unpacked.class);
assert_eq!(input.team, unpacked.team);
assert_eq!(input.charge, unpacked.charge);
assert!(f32::abs(input.position.x - unpacked.position.x) < 0.5);
assert!(f32::abs(input.position.y - unpacked.position.y) < 0.5);
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum BuildingType {
TeleporterEntrance = 0,
TeleporterExit = 1,
Dispenser = 2,
Level1Sentry = 3,
Level2Sentry = 4,
Level3Sentry = 5,
MiniSentry = 6,
Unknown = 7,
}
impl Default for BuildingType {
fn default() -> Self {
BuildingType::Unknown
}
}
impl BuildingType {
pub fn new(raw: u8) -> BuildingType {
match raw {
0 => Self::TeleporterEntrance,
1 => Self::TeleporterExit,
2 => Self::Dispenser,
3 => Self::Level1Sentry,
4 => Self::Level2Sentry,
5 => Self::Level3Sentry,
6 => Self::MiniSentry,
_ => Self::Unknown,
}
}
pub fn from_building(building: &Building) -> Self {
match building {
Building::Sentry { is_mini: true, .. } => BuildingType::MiniSentry,
Building::Sentry {
is_mini: false,
level: 1,
..
} => BuildingType::Level1Sentry,
Building::Sentry {
is_mini: false,
level: 2,
..
} => BuildingType::Level2Sentry,
Building::Sentry {
is_mini: false,
level: 3,
..
} => BuildingType::Level3Sentry,
Building::Dispenser { .. } => BuildingType::Dispenser,
Building::Teleporter {
is_entrance: true, ..
} => BuildingType::TeleporterEntrance,
Building::Teleporter {
is_entrance: false, ..
} => BuildingType::TeleporterExit,
_ => BuildingType::Unknown,
}
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct BuildingState {
position: VectorXY,
angle: Angle,
health: u16,
team: Team,
ty: BuildingType,
}
impl BuildingState {
const PACKET_SIZE: usize = 7;
pub fn new(building: &Building) -> Self {
match building {
Building::Sentry {
position,
angle,
health,
team,
..
}
| Building::Dispenser {
position,
angle,
health,
team,
..
}
| Building::Teleporter {
position,
angle,
health,
team,
..
} => BuildingState {
position: VectorXY {
x: position.x,
y: position.y,
},
angle: Angle::from(*angle),
health: *health,
team: *team,
ty: BuildingType::from_building(building),
},
}
}
pub fn pack(&self, world: &World) -> [u8; 7] {
// for the purpose of viewing the demo in the browser we dont really need high accuracy for
// position or angle, so we save a bunch of space by truncating those down to half the number
// of bits
fn pack_f32(val: f32, min: f32, max: f32) -> u16 {
let ratio = (val - min) / (max - min);
(ratio * u16::max_value() as f32) as u16
}
let x = pack_f32(self.position.x, world.boundary_min.x, world.boundary_max.x).to_le_bytes();
let y = pack_f32(self.position.y, world.boundary_min.y, world.boundary_max.y).to_le_bytes();
// 1 bits reserved
// 2 bits reserved
// 3 bits for type
// 10 bits for health
let team_type_health = ((self.team as u16) << 13) + ((self.ty as u16) << 10) + self.health;
let combined_bytes = team_type_health.to_le_bytes();
[
x[0],
x[1],
@ -125,24 +376,24 @@ impl PlayerState {
world.boundary_min.y,
world.boundary_max.y,
);
let team_class_health = u16::from_le_bytes([bytes[4], bytes[5]]);
let health = team_class_health & 1023;
let team_type_health = u16::from_le_bytes([bytes[4], bytes[5]]);
let health = team_type_health & 1023;
let angle = Angle(bytes[6]);
let team = Team::new(team_class_health >> 14);
let class = Class::new((team_class_health >> 10) & 15);
let team = Team::new(team_type_health >> 13);
let ty = BuildingType::new((team_type_health >> 10) as u8 & 7);
PlayerState {
BuildingState {
position: VectorXY { x, y },
angle,
health,
team,
class,
ty,
}
}
}
#[test]
fn test_packing() {
fn test_building_packing() {
use tf_demo_parser::demo::vector::Vector;
let world = World {
@ -158,7 +409,7 @@ fn test_packing() {
},
};
let input = PlayerState {
let input = BuildingState {
position: VectorXY {
x: 100.0,
y: -5000.0,
@ -166,15 +417,15 @@ fn test_packing() {
angle: Angle::from(213.0),
health: 250,
team: Team::Blue,
class: Class::Demoman,
ty: BuildingType::Level1Sentry,
};
let bytes = input.pack(&world);
let unpacked = PlayerState::unpack(bytes, &world);
let unpacked = BuildingState::unpack(bytes, &world);
assert_eq!(input.angle, unpacked.angle);
assert_eq!(input.health, unpacked.health);
assert_eq!(input.class, unpacked.class);
assert_eq!(input.ty, unpacked.ty);
assert_eq!(input.team, unpacked.team);
assert!(f32::abs(input.position.x - unpacked.position.x) < 0.5);