mirror of
https://codeberg.org/demostf/tf-demos-viewer.git
synced 2026-06-03 18:14:11 +02:00
more and better
This commit is contained in:
parent
2b45619edc
commit
0572f34c99
6 changed files with 745 additions and 177 deletions
77
src/lib.rs
77
src/lib.rs
|
|
@ -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())))
|
||||
}
|
||||
|
||||
|
|
|
|||
281
src/state.rs
281
src/state.rs
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue