mirror of
https://codeberg.org/demostf/parser.git
synced 2026-06-03 10:14:06 +02:00
gamestate analyser wip
This commit is contained in:
parent
6222258ac2
commit
5f6cfe077e
13 changed files with 379 additions and 9 deletions
|
|
@ -25,11 +25,11 @@ serde_repr = "0.1"
|
|||
err-derive = "0.2"
|
||||
parse-display = "0.1"
|
||||
main_error = "0.1.0"
|
||||
jemallocator = { version = "0.3", optional = true }
|
||||
better-panic = { version = "0.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.6"
|
||||
jemallocator = "0.3"
|
||||
better-panic = "0.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
1
data/gully_game_state.json
Normal file
1
data/gully_game_state.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"players":[{"entity":2,"position":{"x":5209.61,"y":-1013.047,"z":296.03125},"health":125,"max_health":125,"class":1,"team":"blue","view_angle":64.04692,"state":"Alive"},{"entity":3,"position":{"x":4563.5,"y":-1381.375,"z":369.125},"health":135,"max_health":200,"class":3,"team":"red","view_angle":28.504398,"state":"Alive"},{"entity":4,"position":{"x":4334.875,"y":-1428.875,"z":296.0},"health":104,"max_health":150,"class":5,"team":"red","view_angle":355.0733,"state":"Death"},{"entity":5,"position":{"x":5207.0,"y":-1077.0,"z":296.0},"health":175,"max_health":175,"class":4,"team":"blue","view_angle":33.079178,"state":"Alive"},{"entity":6,"position":{"x":5046.0,"y":-1077.0,"z":296.0},"health":125,"max_health":125,"class":1,"team":"blue","view_angle":180.17595,"state":"Alive"},{"entity":7,"position":{"x":5046.0,"y":-1013.0,"z":296.0},"health":150,"max_health":150,"class":5,"team":"blue","view_angle":180.17595,"state":"Alive"},{"entity":8,"position":{"x":4124.375,"y":-990.375,"z":162.0},"health":260,"max_health":175,"class":4,"team":"red","view_angle":355.42523,"state":"Alive"},{"entity":10,"position":{"x":5046.0,"y":-953.25,"z":296.0},"health":200,"max_health":200,"class":3,"team":"blue","view_angle":180.17595,"state":"Alive"},{"entity":12,"position":{"x":3954.25,"y":-1329.875,"z":232.0},"health":132,"max_health":125,"class":2,"team":"red","view_angle":17.947214,"state":"Alive"},{"entity":13,"position":{"x":4563.5,"y":-1381.375,"z":369.125},"health":1,"max_health":200,"class":3,"team":"red","view_angle":9.853373,"state":"Death"},{"entity":11,"position":{"x":5046.0,"y":-1141.0,"z":296.0},"health":175,"max_health":175,"class":7,"team":"Other","view_angle":180.17595,"state":"Alive"},{"entity":9,"position":{"x":3707.125,"y":-444.375,"z":342.5},"health":125,"max_health":125,"class":1,"team":"Other","view_angle":110.14663,"state":"Alive"}],"buildings":[]}
|
||||
1
data/small_game_state.json
Normal file
1
data/small_game_state.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"players":[{"entity":1,"position":{"x":-3599.1775,"y":421.331,"z":298.0},"health":125,"max_health":125,"class":1,"team":"red","view_angle":0.0,"state":"Alive"}],"buildings":[]}
|
||||
|
|
@ -4,10 +4,12 @@ use std::fs;
|
|||
use main_error::MainError;
|
||||
pub use tf_demo_parser::{Demo, DemoParser, Parse, ParseError, ParserState, Stream};
|
||||
|
||||
#[cfg(feature = "jemallocator")]
|
||||
#[global_allocator]
|
||||
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||
|
||||
fn main() -> Result<(), MainError> {
|
||||
#[cfg(feature = "better_panic")]
|
||||
better_panic::install();
|
||||
|
||||
let args: Vec<_> = env::args().collect();
|
||||
|
|
|
|||
|
|
@ -9034,4 +9034,3 @@ pub fn get_sizes() -> std::collections::hash_map::HashMap<&'static str, usize> {
|
|||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ impl From<ClassId> for usize {
|
|||
#[derive(BitRead, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, Clone, Display)]
|
||||
pub struct ServerClassName(Rc<String>);
|
||||
|
||||
impl ServerClassName {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ServerClassName {
|
||||
fn from(value: String) -> Self {
|
||||
Self(Rc::new(value))
|
||||
|
|
@ -50,6 +56,12 @@ pub struct ServerClass {
|
|||
)]
|
||||
pub struct SendTableName(Rc<String>);
|
||||
|
||||
impl SendTableName {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SendTableName {
|
||||
fn from(value: String) -> Self {
|
||||
Self(Rc::new(value))
|
||||
|
|
|
|||
260
src/demo/parser/gamestateanalyser.rs
Normal file
260
src/demo/parser/gamestateanalyser.rs
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
use crate::demo::gameevent_gen::GameEventType::PlayerSappedObject;
|
||||
use crate::demo::message::packetentities::{EntityId, PacketEntity};
|
||||
use crate::demo::message::Message;
|
||||
use crate::demo::packet::datatable::{ParseSendTable, SendTableName, ServerClass, ServerClassName};
|
||||
use crate::demo::parser::analyser::{Class, Team, UserId};
|
||||
use crate::demo::parser::MessageHandler;
|
||||
use crate::demo::vector::{Vector, VectorXY};
|
||||
use crate::{MessageType, ParserState};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct CachedEntities {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PlayerState {
|
||||
Alive = 0,
|
||||
Dying = 1,
|
||||
Death = 2,
|
||||
Respawnable = 3,
|
||||
}
|
||||
|
||||
impl PlayerState {
|
||||
pub fn new(number: i64) -> Self {
|
||||
match number {
|
||||
1 => PlayerState::Dying,
|
||||
2 => PlayerState::Death,
|
||||
3 => PlayerState::Respawnable,
|
||||
_ => PlayerState::Alive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Player {
|
||||
entity: EntityId,
|
||||
position: Vector,
|
||||
health: u16,
|
||||
max_health: u16,
|
||||
class: Class,
|
||||
team: Team,
|
||||
view_angle: f32,
|
||||
state: PlayerState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum Building {
|
||||
Sentry {
|
||||
builder: UserId,
|
||||
position: Vector,
|
||||
level: u8,
|
||||
max_health: u16,
|
||||
health: u16,
|
||||
building: bool,
|
||||
sapped: bool,
|
||||
team: Team,
|
||||
angle: f32,
|
||||
player_controller: bool,
|
||||
auto_aim_target: UserId,
|
||||
shells: u16,
|
||||
rockets: u16,
|
||||
is_mini: bool,
|
||||
},
|
||||
Dispenser {
|
||||
builder: UserId,
|
||||
position: Vector,
|
||||
level: u8,
|
||||
max_health: u16,
|
||||
health: u16,
|
||||
building: bool,
|
||||
sapped: bool,
|
||||
team: Team,
|
||||
angle: f32,
|
||||
healing: Vec<UserId>,
|
||||
metal: u16,
|
||||
},
|
||||
Teleporter {
|
||||
builder: UserId,
|
||||
position: Vector,
|
||||
level: u8,
|
||||
max_health: u16,
|
||||
health: u16,
|
||||
building: bool,
|
||||
sapped: bool,
|
||||
team: Team,
|
||||
angle: f32,
|
||||
is_entrance: bool,
|
||||
other_end: EntityId,
|
||||
recharge_time: f32,
|
||||
recharge_duration: f32,
|
||||
times_used: u16,
|
||||
yaw_to_exit: f32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GameState {
|
||||
players: Vec<Player>,
|
||||
buildings: Vec<Building>,
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
pub fn get_or_create_player(&mut self, entity_id: EntityId) -> &mut Player {
|
||||
let index = match self
|
||||
.players
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find(|(index, player)| player.entity == entity_id)
|
||||
.map(|(index, _)| index)
|
||||
{
|
||||
Some(index) => index,
|
||||
None => {
|
||||
let player = Player {
|
||||
entity: entity_id,
|
||||
position: Vector::default(),
|
||||
health: 0,
|
||||
max_health: 0,
|
||||
class: Class::Other,
|
||||
team: Team::Other,
|
||||
view_angle: 0.0,
|
||||
state: PlayerState::Alive,
|
||||
};
|
||||
|
||||
let index = self.players.len();
|
||||
self.players.push(player);
|
||||
index
|
||||
}
|
||||
};
|
||||
|
||||
&mut self.players[index]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct GameStateAnalyser {
|
||||
pub state: GameState,
|
||||
class_names: Vec<ServerClassName>, // indexed by ClassId
|
||||
}
|
||||
|
||||
impl MessageHandler for GameStateAnalyser {
|
||||
type Output = GameState;
|
||||
|
||||
fn does_handle(message_type: MessageType) -> bool {
|
||||
match message_type {
|
||||
MessageType::PacketEntities => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, message: &Message, tick: u32) {
|
||||
match message {
|
||||
Message::PacketEntities(message) => {
|
||||
for entity in &message.entities {
|
||||
self.handle_entity(entity);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_data_tables(&mut self, tables: &[ParseSendTable], server_classes: &[ServerClass]) {
|
||||
self.class_names = server_classes
|
||||
.iter()
|
||||
.map(|class| &class.name)
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn get_output(self, state: ParserState) -> Self::Output {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl GameStateAnalyser {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn handle_entity(&mut self, entity: &PacketEntity) {
|
||||
let class_name: &str = self
|
||||
.class_names
|
||||
.get(usize::from(entity.server_class))
|
||||
.map(|class_name| class_name.as_str())
|
||||
.unwrap_or("");
|
||||
match class_name {
|
||||
"CTFPlayer" => self.handle_player_entity(entity),
|
||||
"CTFPlayerResource" => self.handle_player_resource(entity),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_player_resource(&mut self, entity: &PacketEntity) {
|
||||
for prop in &entity.props {
|
||||
if let Ok(player_id) = u32::from_str(prop.definition.name.as_str()) {
|
||||
let entity_id = EntityId::from(player_id);
|
||||
if let Some(player) = self
|
||||
.state
|
||||
.players
|
||||
.iter_mut()
|
||||
.find(|player| player.entity == entity_id)
|
||||
{
|
||||
match prop.definition.owner_table.as_str() {
|
||||
"m_iTeam" => {
|
||||
player.team =
|
||||
Team::new(i64::try_from(&prop.value).unwrap_or_default() as u16)
|
||||
}
|
||||
"m_iMaxHealth" => {
|
||||
player.max_health =
|
||||
i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||
}
|
||||
"m_iPlayerClass" => {
|
||||
player.class =
|
||||
Class::new(i64::try_from(&prop.value).unwrap_or_default() as u16)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_player_entity(&mut self, entity: &PacketEntity) {
|
||||
let player = self.state.get_or_create_player(entity.entity_index);
|
||||
|
||||
for prop in &entity.props {
|
||||
match prop.definition.owner_table.as_str() {
|
||||
"DT_BasePlayer" => match prop.definition.name.as_str() {
|
||||
"m_iHealth" => {
|
||||
player.health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||
}
|
||||
"m_iMaxHealth" => {
|
||||
player.max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||
}
|
||||
"m_lifeState" => {
|
||||
player.state =
|
||||
PlayerState::new(i64::try_from(&prop.value).unwrap_or_default())
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
"DT_TFLocalPlayerExclusive" | "DT_TFNonLocalPlayerExclusive" => {
|
||||
match prop.definition.name.as_str() {
|
||||
"m_vecOrigin" => {
|
||||
let pos_xy = VectorXY::try_from(&prop.value).unwrap_or_default();
|
||||
player.position.x = pos_xy.x;
|
||||
player.position.y = pos_xy.y;
|
||||
}
|
||||
"m_vecOrigin[2]" => {
|
||||
player.position.z = f32::try_from(&prop.value).unwrap_or_default()
|
||||
}
|
||||
"m_angEyeAngles[1]" => {
|
||||
player.view_angle = f32::try_from(&prop.value).unwrap_or_default()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ pub trait MessageHandler {
|
|||
|
||||
fn handle_string_entry(&mut self, table: &String, index: usize, entries: &StringTableEntry) {}
|
||||
|
||||
fn handle_data_tables(&mut self, tables: &[ParseSendTable]) {}
|
||||
fn handle_data_tables(&mut self, tables: &[ParseSendTable], server_classes: &[ServerClass]) {}
|
||||
|
||||
fn get_output(self, state: ParserState) -> Self::Output;
|
||||
}
|
||||
|
|
@ -114,7 +114,8 @@ impl<T: MessageHandler> DemoHandler<T> {
|
|||
send_tables: Vec<ParseSendTable>,
|
||||
server_classes: Vec<ServerClass>,
|
||||
) {
|
||||
self.analyser.handle_data_tables(&send_tables);
|
||||
self.analyser
|
||||
.handle_data_tables(&send_tables, &server_classes);
|
||||
self.state_handler
|
||||
.handle_data_table(send_tables, server_classes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use err_derive::Error;
|
|||
|
||||
mod analyser;
|
||||
mod error;
|
||||
pub mod gamestateanalyser;
|
||||
mod handler;
|
||||
mod messagetypeanalyser;
|
||||
mod state;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use crate::demo::message::stringtable::log_base2;
|
|||
use crate::demo::packet::datatable::SendTableName;
|
||||
use crate::demo::parser::MalformedSendPropDefinitionError;
|
||||
use parse_display::Display;
|
||||
use serde::export::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
|
@ -20,6 +21,12 @@ use std::rc::Rc;
|
|||
)]
|
||||
pub struct SendPropName(Rc<String>);
|
||||
|
||||
impl SendPropName {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for SendPropName {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.0.as_str() == *other
|
||||
|
|
@ -528,6 +535,66 @@ impl From<Vec<SendPropValue>> for SendPropValue {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&SendPropValue> for i64 {
|
||||
type Error = ();
|
||||
fn try_from(value: &SendPropValue) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
SendPropValue::Integer(val) => Ok(*val),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&SendPropValue> for Vector {
|
||||
type Error = ();
|
||||
fn try_from(value: &SendPropValue) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
SendPropValue::Vector(val) => Ok(*val),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&SendPropValue> for VectorXY {
|
||||
type Error = ();
|
||||
fn try_from(value: &SendPropValue) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
SendPropValue::VectorXY(val) => Ok(*val),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&SendPropValue> for f32 {
|
||||
type Error = ();
|
||||
fn try_from(value: &SendPropValue) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
SendPropValue::Float(val) => Ok(*val),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a SendPropValue> for &'a str {
|
||||
type Error = ();
|
||||
fn try_from(value: &'a SendPropValue) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
SendPropValue::String(val) => Ok(val.as_str()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a SendPropValue> for &'a [SendPropValue] {
|
||||
type Error = ();
|
||||
fn try_from(value: &'a SendPropValue) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
SendPropValue::Array(val) => Ok(val.as_slice()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub struct SendPropDefinitionIndex(u32);
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,5 @@ fn entity_test(input_file: &str, snapshot_file: &str) {
|
|||
|
||||
#[test]
|
||||
fn entity_test_short() {
|
||||
better_panic::install();
|
||||
|
||||
entity_test("data/small.dem", "data/small_entities.json");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ use std::fs;
|
|||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tf_demo_parser::demo::message::Message;
|
||||
use tf_demo_parser::demo::packet::datatable::{ParseSendTable, SendTable, SendTableName};
|
||||
use tf_demo_parser::demo::packet::datatable::{
|
||||
ParseSendTable, SendTable, SendTableName, ServerClass,
|
||||
};
|
||||
use tf_demo_parser::demo::packet::stringtable::StringTableEntry;
|
||||
use tf_demo_parser::demo::parser::MessageHandler;
|
||||
use tf_demo_parser::demo::sendprop::SendPropDefinition;
|
||||
|
|
@ -30,7 +32,7 @@ impl MessageHandler for SendPropAnalyser {
|
|||
false
|
||||
}
|
||||
|
||||
fn handle_data_tables(&mut self, tables: &[ParseSendTable]) {
|
||||
fn handle_data_tables(&mut self, tables: &[ParseSendTable], server_classes: &[ServerClass]) {
|
||||
self.tables = tables.to_vec()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
|
||||
use tf_demo_parser::demo::parser::gamestateanalyser::{GameState, GameStateAnalyser};
|
||||
use tf_demo_parser::{Demo, DemoParser, MatchState, MessageType, MessageTypeAnalyser};
|
||||
|
||||
fn snapshot_test(input_file: &str, snapshot_file: &str) {
|
||||
|
|
@ -32,6 +33,21 @@ fn test_message_types(input_file: &str, snapshot_file: &str) {
|
|||
assert_eq!(expected, message_types);
|
||||
}
|
||||
|
||||
fn game_state_test(input_file: &str, snapshot_file: &str) {
|
||||
let file = fs::read(input_file).expect("Unable to read file");
|
||||
let demo = Demo::new(file);
|
||||
let (_, state) =
|
||||
DemoParser::parse_with_analyser(demo.get_stream(), GameStateAnalyser::new()).unwrap();
|
||||
|
||||
let expected: GameState = serde_json::from_slice(
|
||||
fs::read(snapshot_file)
|
||||
.expect("Unable to read file")
|
||||
.as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(expected, state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_small() {
|
||||
snapshot_test("data/small.dem", "data/small.json");
|
||||
|
|
@ -56,3 +72,13 @@ fn snapshot_test_malformed_cvar() {
|
|||
fn snapshot_test_decal() {
|
||||
snapshot_test("data/decal.dem", "data/decal.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_state_test_small() {
|
||||
game_state_test("data/small.dem", "data/small_game_state.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_state_test_gully() {
|
||||
game_state_test("data/gully.dem", "data/gully_game_state.json");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue