This commit is contained in:
Robin Appelman 2024-12-08 15:34:27 +01:00
commit 72027c010c
4 changed files with 96 additions and 6 deletions

4
Cargo.lock generated
View file

@ -561,9 +561,11 @@ dependencies = [
[[package]] [[package]]
name = "tf-demos-viewer" name = "tf-demos-viewer"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde",
"serde_json",
"tf-demo-parser", "tf-demo-parser",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",

View file

@ -1,7 +1,7 @@
[package] [package]
name = "tf-demos-viewer" name = "tf-demos-viewer"
description = "JS bindings for demo parser" description = "JS bindings for demo parser"
version = "0.2.0" version = "0.2.1"
authors = ["Robin Appelman <robin@icewind.nl>"] authors = ["Robin Appelman <robin@icewind.nl>"]
categories = ["wasm"] categories = ["wasm"]
edition = "2021" edition = "2021"
@ -27,4 +27,6 @@ wee_alloc = { version = "0.4.2", optional = true }
web-sys = { version = "0.3.22", features = ["console"] } web-sys = { version = "0.3.22", features = ["console"] }
js-sys = "0.3.22" js-sys = "0.3.22"
tf-demo-parser = { version = "0.5.1", path = "../tf-demo-parser" } tf-demo-parser = { version = "0.5.1", path = "../tf-demo-parser" }
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"

View file

@ -1,6 +1,6 @@
#![macro_use] #![macro_use]
use crate::state::ParsedDemo; use crate::state::{ParsedDemo, SearchableEvent};
use js_sys::Function; use js_sys::Function;
use tf_demo_parser::demo::header::Header; use tf_demo_parser::demo::header::Header;
use tf_demo_parser::demo::parser::analyser::UserInfo; use tf_demo_parser::demo::parser::analyser::UserInfo;
@ -54,6 +54,7 @@ pub struct FlatState {
victims: Box<[u8]>, victims: Box<[u8]>,
weapons: Vec<String>, weapons: Vec<String>,
player_info: Vec<UserInfo>, player_info: Vec<UserInfo>,
events: Vec<SearchableEvent>,
data: Box<[u8]>, data: Box<[u8]>,
header: Header, header: Header,
} }
@ -108,6 +109,7 @@ impl FlatState {
.collect(), .collect(),
weapons: parsed.kills.into_iter().map(|kill| kill.weapon).collect(), weapons: parsed.kills.into_iter().map(|kill| kill.weapon).collect(),
player_info: parsed.player_info, player_info: parsed.player_info,
events: parsed.events,
header, header,
} }
} }
@ -178,6 +180,16 @@ pub fn get_player_steam_id(state: &FlatState, player_id: usize) -> String {
state.player_info[player_id].steam_id.clone() state.player_info[player_id].steam_id.clone()
} }
#[wasm_bindgen]
pub fn get_event_count(state: &FlatState) -> usize {
state.events.len()
}
#[wasm_bindgen]
pub fn get_event(state: &FlatState, id: usize) -> String {
serde_json::to_string(&state.events[id]).unwrap_or_default()
}
pub fn parse_demo_inner( pub fn parse_demo_inner(
buffer: &[u8], buffer: &[u8],
progress: &Function, progress: &Function,
@ -201,7 +213,7 @@ pub fn parse_demo_inner(
} }
} }
parsed_demo.finish(); parsed_demo.finish(ticker.state());
let state = ticker.into_state(); let state = ticker.into_state();

View file

@ -1,10 +1,12 @@
use serde::{Deserialize, Serialize};
use tf_demo_parser::demo::data::game_state::{Projectile, ProjectileType}; use tf_demo_parser::demo::data::game_state::{Projectile, ProjectileType};
use tf_demo_parser::demo::data::DemoTick; use tf_demo_parser::demo::data::DemoTick;
use tf_demo_parser::demo::gamevent::GameEvent;
use tf_demo_parser::demo::header::Header; use tf_demo_parser::demo::header::Header;
use tf_demo_parser::demo::parser::analyser::UserInfo; use tf_demo_parser::demo::parser::analyser::UserInfo;
use tf_demo_parser::demo::parser::gamestateanalyser::{ use tf_demo_parser::demo::parser::gamestateanalyser::{
Building, Class, Dispenser, GameState, Kill, PlayerState as PlayerAliveState, Sentry, Team, Building, Class, Dispenser, GameState, Kill, PlayerState as PlayerAliveState, Sentry, Team,
Teleporter, World, Teleporter, UserId, World,
}; };
use tf_demo_parser::demo::vector::VectorXY; use tf_demo_parser::demo::vector::VectorXY;
@ -33,6 +35,7 @@ pub struct ParsedDemo {
pub buildings: Vec<Vec<u8>>, pub buildings: Vec<Vec<u8>>,
pub projectiles: Vec<Vec<u8>>, pub projectiles: Vec<Vec<u8>>,
pub kills: Vec<Kill>, pub kills: Vec<Kill>,
pub events: Vec<SearchableEvent>,
pub header: Header, pub header: Header,
pub player_info: Vec<UserInfo>, pub player_info: Vec<UserInfo>,
pub max_building_count: usize, pub max_building_count: usize,
@ -51,6 +54,7 @@ impl ParsedDemo {
player_info: Vec::new(), player_info: Vec::new(),
max_building_count: 0, max_building_count: 0,
max_projectile_count: 0, max_projectile_count: 0,
events: Vec::new(),
header, header,
} }
} }
@ -130,13 +134,19 @@ impl ParsedDemo {
} }
} }
pub fn finish(&mut self) { pub fn finish(&mut self, state: &GameState) {
for parsed_building in self.buildings.iter_mut() { for parsed_building in self.buildings.iter_mut() {
parsed_building.resize(self.tick * BuildingState::PACKET_SIZE, 0); parsed_building.resize(self.tick * BuildingState::PACKET_SIZE, 0);
} }
for parsed_projectiles in self.projectiles.iter_mut() { for parsed_projectiles in self.projectiles.iter_mut() {
parsed_projectiles.resize(self.tick * ProjectileState::PACKET_SIZE, 0); parsed_projectiles.resize(self.tick * ProjectileState::PACKET_SIZE, 0);
} }
self.events = state
.events
.iter()
.flat_map(|(tick, event)| SearchableEvent::from_event(*tick, event))
.collect();
} }
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
@ -570,3 +580,67 @@ fn test_projectile_packing() {
assert!(f32::abs(input.position.x - unpacked.position.x) < 0.5); assert!(f32::abs(input.position.x - unpacked.position.x) < 0.5);
assert!(f32::abs(input.position.y - unpacked.position.y) < 0.5); assert!(f32::abs(input.position.y - unpacked.position.y) < 0.5);
} }
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RawBuildingType {
Dispenser,
Teleporter,
SentryGun,
}
impl TryFrom<u16> for RawBuildingType {
type Error = ();
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
0 => Ok(RawBuildingType::Dispenser),
1 => Ok(RawBuildingType::Teleporter),
2 => Ok(RawBuildingType::SentryGun),
_ => Err(()),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum SearchableEvent {
Uber {
user_id: UserId,
target_id: UserId,
tick: DemoTick,
},
BuildingDestroyed {
attacker_id: UserId,
assister_id: UserId,
victim_id: UserId,
weapon: String,
building_type: RawBuildingType,
tick: DemoTick,
},
}
impl SearchableEvent {
pub fn from_event(tick: DemoTick, event: &GameEvent) -> Option<SearchableEvent> {
match event {
GameEvent::ObjectDestroyed(event) => {
let building_type = RawBuildingType::try_from(event.object_type).ok()?;
Some(SearchableEvent::BuildingDestroyed {
attacker_id: UserId::from(event.attacker),
assister_id: UserId::from(event.assister),
victim_id: UserId::from(event.user_id),
weapon: event.weapon.to_string(),
building_type,
tick,
})
}
GameEvent::PlayerChargeDeployed(event) => Some(SearchableEvent::Uber {
user_id: UserId::from(event.user_id),
target_id: UserId::from(event.target_id),
tick,
}),
_ => None,
}
}
}