mirror of
https://codeberg.org/demostf/tf-demos-viewer.git
synced 2026-06-03 18:14:11 +02:00
spy, medigun type, building progress
This commit is contained in:
parent
8000c148d0
commit
775c8080ac
3 changed files with 145 additions and 29 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -541,9 +541,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tf-demo-parser"
|
name = "tf-demo-parser"
|
||||||
version = "0.6.3"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cbf498d183c4e849900a3f0c2127ba19a273b8969463075f4358edcfa5f740c2"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitbuffer",
|
"bitbuffer",
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
|
|
@ -564,7 +562,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tf-demos-viewer"
|
name = "tf-demos-viewer"
|
||||||
version = "0.2.3"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -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.3"
|
version = "0.3.0"
|
||||||
authors = ["Robin Appelman <robin@icewind.nl>"]
|
authors = ["Robin Appelman <robin@icewind.nl>"]
|
||||||
categories = ["wasm"]
|
categories = ["wasm"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
@ -26,7 +26,8 @@ wasm-bindgen = "0.2.96"
|
||||||
wee_alloc = { version = "0.4.2", optional = true }
|
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 = "0.6.3"
|
# tf-demo-parser = { version = "0.7.0", git = "https://codeberg.org/demostf/parser" }
|
||||||
|
tf-demo-parser = { version = "0.7.0", path = "../tf-demo-parser" }
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.133"
|
||||||
|
|
||||||
|
|
|
||||||
163
src/state.rs
163
src/state.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tf_demo_parser::demo::data::game_state::{
|
use tf_demo_parser::demo::data::game_state::{
|
||||||
Cart, Objective, PlayerCondition, Projectile, ProjectileType,
|
Cart, MedigunType, Objective, PlayerClassData, PlayerCondition, 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::gamevent::GameEvent;
|
||||||
|
|
@ -77,13 +77,17 @@ impl ParsedDemo {
|
||||||
},
|
},
|
||||||
team: player.team,
|
team: player.team,
|
||||||
class: player.class,
|
class: player.class,
|
||||||
charge: player.charge,
|
class_data: player.class_data.clone(),
|
||||||
ubered: player.has_condition(PlayerCondition::Invulnerable)
|
ubered: player.has_condition(PlayerCondition::Invulnerable)
|
||||||
|| player.has_condition(PlayerCondition::MedigunUberBlastResist)
|
|| player.has_condition(PlayerCondition::MedigunUberBlastResist)
|
||||||
|| player.has_condition(PlayerCondition::MedigunUberBulletResist)
|
|| player.has_condition(PlayerCondition::MedigunUberBulletResist)
|
||||||
|| player.has_condition(PlayerCondition::MedigunUberFireResist)
|
|| player.has_condition(PlayerCondition::MedigunUberFireResist)
|
||||||
|| player.has_condition(PlayerCondition::CritBoosted)
|
|| player.has_condition(PlayerCondition::CritBoosted)
|
||||||
|| player.has_condition(PlayerCondition::MegaHeal),
|
|| player.has_condition(PlayerCondition::MegaHeal),
|
||||||
|
cloaked: player.has_condition(PlayerCondition::Stealthed)
|
||||||
|
|| player.has_condition(PlayerCondition::StealthedBlink)
|
||||||
|
|| player.has_condition(PlayerCondition::StealthedUserBuff)
|
||||||
|
|| player.has_condition(PlayerCondition::StealthedUserBuffFading),
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.players.get(index).is_none() {
|
if self.players.get(index).is_none() {
|
||||||
|
|
@ -177,22 +181,15 @@ pub struct PlayerState {
|
||||||
health: u16,
|
health: u16,
|
||||||
team: Team,
|
team: Team,
|
||||||
class: Class,
|
class: Class,
|
||||||
charge: u8,
|
class_data: PlayerClassData,
|
||||||
ubered: bool,
|
ubered: bool,
|
||||||
|
cloaked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerState {
|
impl PlayerState {
|
||||||
const PACKET_SIZE: usize = 8;
|
const PACKET_SIZE: usize = 9;
|
||||||
|
|
||||||
pub fn pack(&self, world: &World) -> [u8; Self::PACKET_SIZE] {
|
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
|
|
||||||
fn pack_f32(val: f32, min: f32, max: f32) -> u16 {
|
|
||||||
let ratio = (val - min) / (max - min);
|
|
||||||
(ratio * u16::MAX as f32) as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
let x = pack_f32(self.position.x, world.boundary_min.x, world.boundary_max.x).to_le_bytes();
|
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();
|
let y = pack_f32(self.position.y, world.boundary_min.y, world.boundary_max.y).to_le_bytes();
|
||||||
// 2 bits for team
|
// 2 bits for team
|
||||||
|
|
@ -205,6 +202,22 @@ impl PlayerState {
|
||||||
+ (self.ubered as u16);
|
+ (self.ubered as u16);
|
||||||
let combined_bytes = team_class_health.to_le_bytes();
|
let combined_bytes = team_class_health.to_le_bytes();
|
||||||
|
|
||||||
|
let class_bits = match self.class_data {
|
||||||
|
PlayerClassData::Medic { charge, medigun } => [charge, medigun as u8],
|
||||||
|
PlayerClassData::Spy {
|
||||||
|
disguise_class,
|
||||||
|
disguise_team,
|
||||||
|
cloak,
|
||||||
|
} => {
|
||||||
|
let team_class = ((disguise_team as u8) << 6) + ((disguise_class as u8) << 2);
|
||||||
|
[
|
||||||
|
team_class,
|
||||||
|
((cloak as u8).min(100) << 1) + (self.cloaked as u8),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
_ => [0, 0],
|
||||||
|
};
|
||||||
|
|
||||||
[
|
[
|
||||||
x[0],
|
x[0],
|
||||||
x[1],
|
x[1],
|
||||||
|
|
@ -213,12 +226,13 @@ impl PlayerState {
|
||||||
combined_bytes[0],
|
combined_bytes[0],
|
||||||
combined_bytes[1],
|
combined_bytes[1],
|
||||||
self.angle.0,
|
self.angle.0,
|
||||||
self.charge,
|
class_bits[0],
|
||||||
|
class_bits[1],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn unpack(bytes: [u8; 8], world: &World) -> Self {
|
pub fn unpack(bytes: [u8; Self::PACKET_SIZE], world: &World) -> Self {
|
||||||
fn unpack_f32(val: u16, min: f32, max: f32) -> f32 {
|
fn unpack_f32(val: u16, min: f32, max: f32) -> f32 {
|
||||||
let ratio = val as f32 / (u16::MAX as f32);
|
let ratio = val as f32 / (u16::MAX as f32);
|
||||||
ratio * (max - min) + min
|
ratio * (max - min) + min
|
||||||
|
|
@ -240,7 +254,27 @@ impl PlayerState {
|
||||||
let angle = Angle(bytes[6]);
|
let angle = Angle(bytes[6]);
|
||||||
let team = Team::new(team_class_health >> 14);
|
let team = Team::new(team_class_health >> 14);
|
||||||
let class = Class::new((team_class_health >> 10) & 15);
|
let class = Class::new((team_class_health >> 10) & 15);
|
||||||
let charge = bytes[7];
|
let class_bits = [bytes[7], bytes[8]];
|
||||||
|
let mut cloaked = false;
|
||||||
|
|
||||||
|
let class_data = match class {
|
||||||
|
Class::Medic => PlayerClassData::Medic {
|
||||||
|
charge: class_bits[0],
|
||||||
|
medigun: MedigunType::try_from(class_bits[1]).unwrap_or_default(),
|
||||||
|
},
|
||||||
|
Class::Spy => {
|
||||||
|
let disguise_team = Team::new(class_bits[0] >> 6);
|
||||||
|
let disguise_class = Class::new((class_bits[0] >> 2) & 15);
|
||||||
|
let cloak = f32::from(class_bits[1] >> 1);
|
||||||
|
cloaked = class_bits[1] & 1 == 1;
|
||||||
|
PlayerClassData::Spy {
|
||||||
|
disguise_class,
|
||||||
|
disguise_team,
|
||||||
|
cloak,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => PlayerClassData::None,
|
||||||
|
};
|
||||||
|
|
||||||
PlayerState {
|
PlayerState {
|
||||||
position: VectorXY { x, y },
|
position: VectorXY { x, y },
|
||||||
|
|
@ -248,8 +282,9 @@ impl PlayerState {
|
||||||
health,
|
health,
|
||||||
team,
|
team,
|
||||||
class,
|
class,
|
||||||
charge,
|
class_data,
|
||||||
ubered,
|
ubered,
|
||||||
|
cloaked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -272,7 +307,7 @@ fn get_world() -> World {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_player_packing() {
|
fn test_player_packing_demo() {
|
||||||
let world = get_world();
|
let world = get_world();
|
||||||
|
|
||||||
let input = PlayerState {
|
let input = PlayerState {
|
||||||
|
|
@ -284,8 +319,9 @@ fn test_player_packing() {
|
||||||
health: 250,
|
health: 250,
|
||||||
team: Team::Blue,
|
team: Team::Blue,
|
||||||
class: Class::Demoman,
|
class: Class::Demoman,
|
||||||
charge: 7,
|
class_data: PlayerClassData::None,
|
||||||
ubered: false,
|
ubered: false,
|
||||||
|
cloaked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bytes = input.pack(&world);
|
let bytes = input.pack(&world);
|
||||||
|
|
@ -295,7 +331,77 @@ fn test_player_packing() {
|
||||||
assert_eq!(input.health, unpacked.health);
|
assert_eq!(input.health, unpacked.health);
|
||||||
assert_eq!(input.class, unpacked.class);
|
assert_eq!(input.class, unpacked.class);
|
||||||
assert_eq!(input.team, unpacked.team);
|
assert_eq!(input.team, unpacked.team);
|
||||||
assert_eq!(input.charge, unpacked.charge);
|
assert_eq!(input.class_data, unpacked.class_data);
|
||||||
|
|
||||||
|
assert!(f32::abs(input.position.x - unpacked.position.x) < 0.5);
|
||||||
|
assert!(f32::abs(input.position.y - unpacked.position.y) < 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_player_packing_medic() {
|
||||||
|
let world = get_world();
|
||||||
|
|
||||||
|
let input = PlayerState {
|
||||||
|
position: VectorXY {
|
||||||
|
x: 100.0,
|
||||||
|
y: -5000.0,
|
||||||
|
},
|
||||||
|
angle: Angle::from(213.0),
|
||||||
|
health: 250,
|
||||||
|
team: Team::Blue,
|
||||||
|
class: Class::Medic,
|
||||||
|
class_data: PlayerClassData::Medic {
|
||||||
|
charge: 52,
|
||||||
|
medigun: MedigunType::Quickfix,
|
||||||
|
},
|
||||||
|
ubered: true,
|
||||||
|
cloaked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.class_data, unpacked.class_data);
|
||||||
|
|
||||||
|
assert!(f32::abs(input.position.x - unpacked.position.x) < 0.5);
|
||||||
|
assert!(f32::abs(input.position.y - unpacked.position.y) < 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_player_packing_spy() {
|
||||||
|
let world = get_world();
|
||||||
|
|
||||||
|
let input = PlayerState {
|
||||||
|
position: VectorXY {
|
||||||
|
x: 100.0,
|
||||||
|
y: -5000.0,
|
||||||
|
},
|
||||||
|
angle: Angle::from(213.0),
|
||||||
|
health: 250,
|
||||||
|
team: Team::Blue,
|
||||||
|
class: Class::Spy,
|
||||||
|
class_data: PlayerClassData::Spy {
|
||||||
|
disguise_team: Team::Red,
|
||||||
|
disguise_class: Class::Engineer,
|
||||||
|
cloak: 57.0,
|
||||||
|
},
|
||||||
|
ubered: true,
|
||||||
|
cloaked: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.class_data, unpacked.class_data);
|
||||||
|
assert_eq!(input.cloaked, unpacked.cloaked);
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -367,9 +473,10 @@ pub struct BuildingState {
|
||||||
team: Team,
|
team: Team,
|
||||||
ty: BuildingType,
|
ty: BuildingType,
|
||||||
level: u8,
|
level: u8,
|
||||||
|
build_progress: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
// for the purpose of viewing the demo in the browser we dont really need high accuracy for
|
// for the purpose of viewing the demo in the browser we don't really need high accuracy for
|
||||||
// position or angle, so we save a bunch of space by truncating those down to half the number
|
// position or angle, so we save a bunch of space by truncating those down to half the number
|
||||||
// of bits
|
// of bits
|
||||||
fn pack_f32(val: f32, min: f32, max: f32) -> u16 {
|
fn pack_f32(val: f32, min: f32, max: f32) -> u16 {
|
||||||
|
|
@ -382,20 +489,27 @@ fn unpack_f32(val: u16, min: f32, max: f32) -> f32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildingState {
|
impl BuildingState {
|
||||||
const PACKET_SIZE: usize = 7;
|
const PACKET_SIZE: usize = 8;
|
||||||
|
|
||||||
pub fn new(building: &Building) -> Self {
|
pub fn new(building: &Building) -> Self {
|
||||||
let position = building.position();
|
let position = building.position();
|
||||||
|
let ty = BuildingType::from_building(building);
|
||||||
|
let angle = match building {
|
||||||
|
Building::Teleporter(teleporter) => teleporter.yaw_to_exit),
|
||||||
|
Building::Sentry(sentry) => sentry.angle,
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
BuildingState {
|
BuildingState {
|
||||||
position: VectorXY {
|
position: VectorXY {
|
||||||
x: position.x,
|
x: position.x,
|
||||||
y: position.y,
|
y: position.y,
|
||||||
},
|
},
|
||||||
angle: Angle::from(building.angle()),
|
angle: Angle::from(angle),
|
||||||
health: building.health(),
|
health: building.health(),
|
||||||
team: building.team(),
|
team: building.team(),
|
||||||
ty: BuildingType::from_building(building),
|
ty,
|
||||||
level: building.level(),
|
level: building.level(),
|
||||||
|
build_progress: ((building.construction_progress() * 100.0) as u8).min(100),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,6 +535,7 @@ impl BuildingState {
|
||||||
combined_bytes[0],
|
combined_bytes[0],
|
||||||
combined_bytes[1],
|
combined_bytes[1],
|
||||||
self.angle.0,
|
self.angle.0,
|
||||||
|
self.build_progress,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,6 +562,7 @@ impl BuildingState {
|
||||||
};
|
};
|
||||||
let ty = BuildingType::new((team_type_health >> 10) as u8 & 7);
|
let ty = BuildingType::new((team_type_health >> 10) as u8 & 7);
|
||||||
let level = (team_type_health >> 14) as u8;
|
let level = (team_type_health >> 14) as u8;
|
||||||
|
let build_progress = bytes[7];
|
||||||
|
|
||||||
BuildingState {
|
BuildingState {
|
||||||
position: VectorXY { x, y },
|
position: VectorXY { x, y },
|
||||||
|
|
@ -455,6 +571,7 @@ impl BuildingState {
|
||||||
team,
|
team,
|
||||||
ty,
|
ty,
|
||||||
level,
|
level,
|
||||||
|
build_progress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue