spy, medigun type, building progress

This commit is contained in:
Robin Appelman 2025-06-26 20:22:16 +02:00
commit 775c8080ac
3 changed files with 145 additions and 29 deletions

6
Cargo.lock generated
View file

@ -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",

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.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"

View file

@ -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,
} }
} }
} }