projectiles

This commit is contained in:
Robin Appelman 2024-12-05 21:25:52 +01:00
commit 0dd6692af7
4 changed files with 226 additions and 105 deletions

161
Cargo.lock generated
View file

@ -19,27 +19,28 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "bitbuffer" name = "bitbuffer"
version = "0.10.9" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525586993a118417512a49bada2d143319310891f48b0b116c8f64fbb6486c87" checksum = "367fd16a8072d50e8e205d88e684a7eec9ddfb756165a076ebe9ecc41fd82192"
dependencies = [ dependencies = [
"bitbuffer_derive", "bitbuffer_derive",
"err-derive",
"memchr", "memchr",
"num-traits 0.2.19", "num-traits 0.2.19",
"serde", "serde",
"thiserror 1.0.69",
] ]
[[package]] [[package]]
name = "bitbuffer_derive" name = "bitbuffer_derive"
version = "0.10.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052a5a614540ae9bb7de25c2c86a94b6de7374cb7e3230f3128955bdaea62c3f" checksum = "6616c9bbc0c159b0d2473c3c5b40ce36c83fd9e2ece2c96129cd135355c6cccb"
dependencies = [ dependencies = [
"merge",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "structmeta",
"syn_util", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -102,20 +103,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "err-derive"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
"synstructure",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -140,9 +127,9 @@ dependencies = [
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [ dependencies = [
"either", "either",
] ]
@ -198,6 +185,28 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "merge"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9"
dependencies = [
"merge_derive",
"num-traits 0.2.19",
]
[[package]]
name = "merge_derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "num" name = "num"
version = "0.3.1" version = "0.3.1"
@ -284,23 +293,23 @@ dependencies = [
[[package]] [[package]]
name = "num_enum" name = "num_enum"
version = "0.5.11" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
dependencies = [ dependencies = [
"num_enum_derive", "num_enum_derive",
] ]
[[package]] [[package]]
name = "num_enum_derive" name = "num_enum_derive"
version = "0.5.11" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -311,26 +320,25 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "parse-display" name = "parse-display"
version = "0.8.2" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c" checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
dependencies = [ dependencies = [
"once_cell",
"parse-display-derive", "parse-display-derive",
"regex", "regex",
"regex-syntax",
] ]
[[package]] [[package]]
name = "parse-display-derive" name = "parse-display-derive"
version = "0.8.2" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68517892c8daf78da08c0db777fcc17e07f2f63ef70041718f8a7630ad84f341" checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
dependencies = [ dependencies = [
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"regex-syntax 0.7.5", "regex-syntax",
"structmeta", "structmeta",
"syn 2.0.90", "syn 2.0.90",
] ]
@ -396,7 +404,7 @@ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata", "regex-automata",
"regex-syntax 0.8.5", "regex-syntax",
] ]
[[package]] [[package]]
@ -407,27 +415,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax 0.8.5", "regex-syntax",
] ]
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -495,14 +491,14 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_derive", "serde_derive",
"thiserror", "thiserror 1.0.69",
] ]
[[package]] [[package]]
name = "structmeta" name = "structmeta"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -512,9 +508,9 @@ dependencies = [
[[package]] [[package]]
name = "structmeta-derive" name = "structmeta-derive"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -543,38 +539,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "syn_util"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6754c4559b79657554e9d8a0d56e65e490c76d382b9c23108364ec4125dea23c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"unicode-xid",
]
[[package]] [[package]]
name = "tf-demo-parser" name = "tf-demo-parser"
version = "0.5.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77f64e1dac5137a201d92afaf441fbe4018595bb8ec770fe6e02f6cad4410ef"
dependencies = [ dependencies = [
"bitbuffer", "bitbuffer",
"enumflags2", "enumflags2",
"err-derive",
"fnv", "fnv",
"itertools", "itertools",
"main_error", "main_error",
@ -586,11 +556,12 @@ dependencies = [
"serde_repr", "serde_repr",
"snap", "snap",
"steamid-ng", "steamid-ng",
"thiserror 2.0.4",
] ]
[[package]] [[package]]
name = "tf-demos-viewer" name = "tf-demos-viewer"
version = "0.1.2" version = "0.2.0"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"tf-demo-parser", "tf-demo-parser",
@ -605,7 +576,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490"
dependencies = [
"thiserror-impl 2.0.4",
] ]
[[package]] [[package]]
@ -619,6 +599,17 @@ dependencies = [
"syn 2.0.90", "syn 2.0.90",
] ]
[[package]]
name = "thiserror-impl"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.8" version = "0.6.8"
@ -642,12 +633,6 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"

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.1.2" version = "0.2.0"
authors = ["Robin Appelman <robin@icewind.nl>"] authors = ["Robin Appelman <robin@icewind.nl>"]
categories = ["wasm"] categories = ["wasm"]
edition = "2021" edition = "2021"
@ -26,5 +26,5 @@ 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.5.1" tf-demo-parser = { version = "0.5.1", path = "../tf-demo-parser" }

View file

@ -44,6 +44,7 @@ impl From<World> for WorldBoundaries {
pub struct FlatState { pub struct FlatState {
pub player_count: usize, pub player_count: usize,
pub building_count: usize, pub building_count: usize,
pub projectile_count: usize,
pub boundaries: WorldBoundaries, pub boundaries: WorldBoundaries,
pub interval_per_tick: f32, pub interval_per_tick: f32,
pub tick_count: u32, pub tick_count: u32,
@ -63,23 +64,28 @@ impl FlatState {
players, players,
header, header,
buildings, buildings,
projectiles,
max_building_count, max_building_count,
max_projectile_count,
tick, tick,
.. ..
} = parsed; } = parsed;
let player_count = players.len(); let player_count = players.len();
let building_count = max_building_count; let building_count = max_building_count;
let projectile_count = max_projectile_count;
let flat: Vec<_> = players let flat: Vec<_> = players
.into_iter() .into_iter()
.chain(buildings) .chain(buildings)
.chain(projectiles)
.flat_map(Vec::into_iter) .flat_map(Vec::into_iter)
.collect(); .collect();
FlatState { FlatState {
player_count, player_count,
building_count, building_count,
projectile_count,
tick_count: tick as u32, tick_count: tick as u32,
boundaries: world.into(), boundaries: world.into(),
interval_per_tick: header.duration / (header.ticks as f32), interval_per_tick: header.duration / (header.ticks as f32),

View file

@ -1,3 +1,4 @@
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::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;
@ -30,10 +31,12 @@ pub struct ParsedDemo {
pub tick: usize, pub tick: usize,
pub players: Vec<Vec<u8>>, pub players: Vec<Vec<u8>>,
pub buildings: Vec<Vec<u8>>, pub buildings: Vec<Vec<u8>>,
pub projectiles: Vec<Vec<u8>>,
pub kills: Vec<Kill>, pub kills: Vec<Kill>,
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,
pub max_projectile_count: usize,
} }
impl ParsedDemo { impl ParsedDemo {
@ -43,9 +46,11 @@ impl ParsedDemo {
tick: 0, tick: 0,
players: Vec::new(), players: Vec::new(),
buildings: Vec::new(), buildings: Vec::new(),
projectiles: Vec::new(),
kills: Vec::new(), kills: Vec::new(),
player_info: Vec::new(), player_info: Vec::new(),
max_building_count: 0, max_building_count: 0,
max_projectile_count: 0,
header, header,
} }
} }
@ -101,6 +106,24 @@ impl ParsedDemo {
parsed_building.extend_from_slice(&state.pack(world)); parsed_building.extend_from_slice(&state.pack(world));
} }
self.max_projectile_count =
self.max_projectile_count.max(game_state.projectiles.len());
for (index, projectile) in game_state.projectiles.values().enumerate() {
let state = ProjectileState::new(projectile);
if self.projectiles.get(index).is_none() {
let new_projectile = Vec::with_capacity(
self.header.ticks as usize * ProjectileState::PACKET_SIZE,
);
self.projectiles.push(new_projectile);
};
let parsed_projectiles = &mut self.projectiles[index];
parsed_projectiles.resize(self.tick * ProjectileState::PACKET_SIZE, 0);
parsed_projectiles.extend_from_slice(&state.pack(world));
}
self.tick += 1; self.tick += 1;
} }
self.last_tick = game_state.tick; self.last_tick = game_state.tick;
@ -111,6 +134,9 @@ impl ParsedDemo {
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() {
parsed_projectiles.resize(self.tick * ProjectileState::PACKET_SIZE, 0);
}
} }
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
@ -308,6 +334,18 @@ pub struct BuildingState {
level: u8, level: u8,
} }
// 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
}
fn unpack_f32(val: u16, min: f32, max: f32) -> f32 {
let ratio = val as f32 / (u16::MAX as f32);
ratio * (max - min) + min
}
impl BuildingState { impl BuildingState {
const PACKET_SIZE: usize = 7; const PACKET_SIZE: usize = 7;
@ -326,15 +364,7 @@ impl BuildingState {
} }
} }
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
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 level // 2 bits level
@ -360,12 +390,7 @@ impl BuildingState {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn unpack(bytes: [u8; 7], world: &World) -> Self { pub fn unpack(bytes: [u8; Self::PACKET_SIZE], world: &World) -> Self {
fn unpack_f32(val: u16, min: f32, max: f32) -> f32 {
let ratio = val as f32 / (u16::MAX as f32);
ratio * (max - min) + min
}
let x = unpack_f32( let x = unpack_f32(
u16::from_le_bytes([bytes[0], bytes[1]]), u16::from_le_bytes([bytes[0], bytes[1]]),
world.boundary_min.x, world.boundary_min.x,
@ -440,3 +465,108 @@ fn test_building_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, Default, Clone, PartialEq)]
pub struct ProjectileState {
position: VectorXY,
team: Team,
ty: ProjectileType,
angle: Angle,
}
impl ProjectileState {
const PACKET_SIZE: usize = 6;
pub fn new(projectile: &Projectile) -> Self {
let position = projectile.position;
ProjectileState {
position: VectorXY {
x: position.x,
y: position.y,
},
angle: Angle::from(projectile.rotation.y),
team: projectile.team,
ty: projectile.ty,
}
}
pub fn pack(&self, world: &World) -> [u8; Self::PACKET_SIZE] {
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 bit team
// 3 bits for type
// 4 bits for angle, 16 angles should be enough for projectiles
let team = if self.team == Team::Blue { 0 } else { 1 };
let team_type = ((self.ty as u8) << 5) + ((team as u8) << 4);
[x[0], x[1], y[0], y[1], team_type, self.angle.0]
}
#[allow(dead_code)]
pub fn unpack(bytes: [u8; Self::PACKET_SIZE], world: &World) -> Self {
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_type = bytes[4];
let packed_team = (team_type >> 4) & 1;
let team = if packed_team == 0 {
Team::Blue
} else {
Team::Red
};
let ty = ProjectileType::from((team_type >> 5) & 7);
let angle = Angle(bytes[5]);
ProjectileState {
position: VectorXY { x, y },
angle,
team,
ty,
}
}
}
#[test]
fn test_projectile_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 = ProjectileState {
position: VectorXY {
x: 100.0,
y: -5000.0,
},
angle: Angle::from(123.0),
team: Team::Blue,
ty: ProjectileType::Flare,
};
let bytes = input.pack(&world);
let unpacked = ProjectileState::unpack(bytes, &world);
assert_eq!(input.ty, unpacked.ty);
assert_eq!(input.team, unpacked.team);
assert_eq!(input.angle, unpacked.angle);
assert!(f32::abs(input.position.x - unpacked.position.x) < 0.5);
assert!(f32::abs(input.position.y - unpacked.position.y) < 0.5);
}