mirror of
https://codeberg.org/demostf/parser.git
synced 2026-06-03 18:24:05 +02:00
Merge pull request #10 from yep-tf2/player_summaries
Add an analyzer to extract player scoreboard information
This commit is contained in:
commit
b26f775c2b
5 changed files with 391 additions and 13 deletions
|
|
@ -21,9 +21,12 @@ which will place the binary at `target/release/parse_demo`
|
||||||
|
|
||||||
Basic usage is as simple as `parse_demo demofile.dem` which will output a "summary" of the demo file in JSON format.
|
Basic usage is as simple as `parse_demo demofile.dem` which will output a "summary" of the demo file in JSON format.
|
||||||
|
|
||||||
|
Passing the `detailed_summary` argument to the end of `parse_demo` will output a table with scoreboard information for all players who were ever on the server while the demo
|
||||||
|
was being recorded. The player who created the demo will be highlighted in the output.
|
||||||
|
|
||||||
## Advanced usage
|
## Advanced usage
|
||||||
|
|
||||||
### Loop trough every packet
|
### Loop through every packet
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bitbuffer::BitRead;
|
use bitbuffer::BitRead;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ use std::fs;
|
||||||
|
|
||||||
use main_error::MainError;
|
use main_error::MainError;
|
||||||
pub use tf_demo_parser::{Demo, DemoParser, Parse, ParseError, ParserState, Stream};
|
pub use tf_demo_parser::{Demo, DemoParser, Parse, ParseError, ParserState, Stream};
|
||||||
|
use tf_demo_parser::demo::parser::player_summary_analyzer::PlayerSummaryAnalyzer;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(feature = "jemallocator")]
|
#[cfg(feature = "jemallocator")]
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
|
|
@ -21,12 +23,13 @@ fn main() -> Result<(), MainError> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let path = args[1].clone();
|
let path = args[1].clone();
|
||||||
let all = args
|
let all = args.contains(&std::string::String::from("all"));
|
||||||
.get(2)
|
let detailed_summaries = args.contains(&std::string::String::from("detailed_summaries"));
|
||||||
.map(|arg| arg.as_str() == "all")
|
|
||||||
.unwrap_or_default();
|
|
||||||
let file = fs::read(path)?;
|
let file = fs::read(path)?;
|
||||||
let demo = Demo::new(&file);
|
let demo = Demo::new(&file);
|
||||||
|
|
||||||
|
if !detailed_summaries {
|
||||||
|
// Use the default (simple) analyzer to track kills, assists, and deaths
|
||||||
let parser = if all {
|
let parser = if all {
|
||||||
DemoParser::new_all(demo.get_stream())
|
DemoParser::new_all(demo.get_stream())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -34,5 +37,63 @@ fn main() -> Result<(), MainError> {
|
||||||
};
|
};
|
||||||
let (_, state) = parser.parse()?;
|
let (_, state) = parser.parse()?;
|
||||||
println!("{}", serde_json::to_string(&state)?);
|
println!("{}", serde_json::to_string(&state)?);
|
||||||
|
} else {
|
||||||
|
let parser = DemoParser::new_with_analyser(demo.get_stream(), PlayerSummaryAnalyzer::new());
|
||||||
|
let (header, state) = parser.parse()?;
|
||||||
|
|
||||||
|
println!("{:?}", header);
|
||||||
|
|
||||||
|
let table_header = "Player | Points | Kills | Deaths | Assists | Destruction | Captures | Defenses | Domination | Revenge | Ubers | Headshots | Teleports | Healing | Backstabs | Bonus | Support | Damage Dealt";
|
||||||
|
let divider = "---------------------------------|------------|------------|------------|------------|-------------|------------|------------|------------|------------|------------|------------|------------|------------|------------|------------|------------|-------------";
|
||||||
|
println!("{}", table_header);
|
||||||
|
println!("{}", divider);
|
||||||
|
|
||||||
|
for (user_id, user_data) in state.users {
|
||||||
|
let player_name = user_data.name;
|
||||||
|
let summary = state.player_summaries.get(&user_id);
|
||||||
|
match summary {
|
||||||
|
Some(s) => {
|
||||||
|
let (color_code_start, color_code_end) = if player_name == header.nick {
|
||||||
|
// Give the line for the player a green background with white text
|
||||||
|
// ANSI color codes are in hex, since rust doesn't support octal literals in strings
|
||||||
|
// See: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
||||||
|
("\x1b[1;42;37m", "\x1b[0m")
|
||||||
|
} else {
|
||||||
|
("", "")
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}{:32} | {:10} | {:10} | {:10} | {:10} | {:11} | {:10} | {:10} | {:10} | {:10} | {:10} | {:10} | {:10} | {:10} | {:10} | {:10} | {:10} | {:12}{}",
|
||||||
|
color_code_start,
|
||||||
|
|
||||||
|
player_name,
|
||||||
|
s.points,
|
||||||
|
s.kills,
|
||||||
|
s.deaths,
|
||||||
|
s.assists,
|
||||||
|
s.buildings_destroyed,
|
||||||
|
s.captures,
|
||||||
|
s.defenses,
|
||||||
|
s.dominations,
|
||||||
|
s.revenges,
|
||||||
|
s.ubercharges,
|
||||||
|
s.headshots,
|
||||||
|
s.teleports,
|
||||||
|
s.healing,
|
||||||
|
s.backstabs,
|
||||||
|
s.bonus_points,
|
||||||
|
s.support,
|
||||||
|
s.damage_dealt,
|
||||||
|
|
||||||
|
color_code_end,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
// No summary for this player - they likely joined at the end of the match, or left before they did anything noteworthy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ struct RawPlayerInfo {
|
||||||
pub more_extra: u8,
|
pub more_extra: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(BitWrite, Debug, Clone)]
|
#[derive(BitWrite, Debug, Clone, Default)]
|
||||||
pub struct PlayerInfo {
|
pub struct PlayerInfo {
|
||||||
#[size = 32]
|
#[size = 32]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -60,7 +60,7 @@ impl From<RawPlayerInfo> for PlayerInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct UserInfo {
|
pub struct UserInfo {
|
||||||
pub entity_id: EntityId,
|
pub entity_id: EntityId,
|
||||||
pub player_info: PlayerInfo,
|
pub player_info: PlayerInfo,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ pub mod error;
|
||||||
pub mod gamestateanalyser;
|
pub mod gamestateanalyser;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod messagetypeanalyser;
|
pub mod messagetypeanalyser;
|
||||||
|
pub mod player_summary_analyzer;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
pub use self::error::*;
|
pub use self::error::*;
|
||||||
|
|
|
||||||
313
src/demo/parser/player_summary_analyzer.rs
Normal file
313
src/demo/parser/player_summary_analyzer.rs
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
use crate::demo::data::DemoTick;
|
||||||
|
use crate::demo::message::packetentities::EntityId;
|
||||||
|
use crate::demo::message::packetentities::PacketEntity;
|
||||||
|
use crate::demo::message::{Message, MessageType};
|
||||||
|
use crate::demo::packet::datatable::ClassId;
|
||||||
|
use crate::demo::packet::stringtable::StringTableEntry;
|
||||||
|
use crate::demo::parser::analyser::UserInfo;
|
||||||
|
use crate::demo::parser::gamestateanalyser::UserId;
|
||||||
|
use crate::demo::parser::handler::{BorrowMessageHandler, MessageHandler};
|
||||||
|
use crate::{ParserState, ReadResult, Stream};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An analyzer that extracts player scoreboard information to get the stats for every player by the
|
||||||
|
* end of the demo. Essentially, this will capture all the information that would appear on the
|
||||||
|
* scoreboard for every player if they took a snapshot at the time the demo finishes (such as the end
|
||||||
|
* of a match or round).
|
||||||
|
*/
|
||||||
|
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct PlayerSummaryAnalyzer {
|
||||||
|
state: PlayerSummaryState,
|
||||||
|
user_id_map: HashMap<EntityId, UserId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||||
|
pub struct PlayerSummary {
|
||||||
|
pub points: u32,
|
||||||
|
pub kills: u32,
|
||||||
|
pub assists: u32,
|
||||||
|
pub deaths: u32,
|
||||||
|
pub buildings_destroyed: u32,
|
||||||
|
pub captures: u32,
|
||||||
|
pub defenses: u32,
|
||||||
|
pub dominations: u32,
|
||||||
|
pub revenges: u32,
|
||||||
|
pub ubercharges: u32,
|
||||||
|
pub headshots: u32,
|
||||||
|
pub teleports: u32,
|
||||||
|
pub healing: u32,
|
||||||
|
pub backstabs: u32,
|
||||||
|
pub bonus_points: u32,
|
||||||
|
pub support: u32,
|
||||||
|
pub damage_dealt: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct PlayerSummaryState {
|
||||||
|
pub player_summaries: HashMap<UserId, PlayerSummary>,
|
||||||
|
pub users: BTreeMap<UserId, UserInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageHandler for PlayerSummaryAnalyzer {
|
||||||
|
type Output = PlayerSummaryState;
|
||||||
|
|
||||||
|
fn does_handle(message_type: MessageType) -> bool {
|
||||||
|
matches!(
|
||||||
|
message_type,
|
||||||
|
MessageType::PacketEntities
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(&mut self, message: &Message, tick: DemoTick, parser_state: &ParserState) {
|
||||||
|
match message {
|
||||||
|
Message::PacketEntities(message) => {
|
||||||
|
for entity in message.entities.iter() {
|
||||||
|
self.handle_packet_entity(&entity, parser_state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_output(self, _parser_state: &ParserState) -> <Self as MessageHandler>::Output {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_string_entry(
|
||||||
|
&mut self,
|
||||||
|
table: &str,
|
||||||
|
index: usize,
|
||||||
|
entry: &StringTableEntry,
|
||||||
|
_parser_state: &ParserState,
|
||||||
|
) {
|
||||||
|
if table == "userinfo" {
|
||||||
|
let _ = self.parse_user_info(
|
||||||
|
index,
|
||||||
|
entry.text.as_ref().map(|s| s.as_ref()),
|
||||||
|
entry.extra_data.as_ref().map(|data| data.data.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorrowMessageHandler for PlayerSummaryAnalyzer {
|
||||||
|
fn borrow_output(&self, _state: &ParserState) -> &Self::Output {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to make processing integer properties easier.
|
||||||
|
*
|
||||||
|
* parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iPoints", |points| { println!("Scored {} points", points) });
|
||||||
|
*/
|
||||||
|
fn parse_integer_prop<F>(packet: &PacketEntity, table: &str, name: &str, parser_state: &ParserState, handler: F)
|
||||||
|
where F: FnOnce(u32) {
|
||||||
|
use crate::demo::sendprop::SendPropValue;
|
||||||
|
|
||||||
|
match packet.get_prop_by_name(table, name, parser_state) {
|
||||||
|
Some(prop) => {
|
||||||
|
match prop.value {
|
||||||
|
SendPropValue::Integer(val) => handler(val as u32),
|
||||||
|
_ => {} // not an integer, ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {} // the packet doesn't have this property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerSummaryAnalyzer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_packet_entity(&mut self, packet: &PacketEntity, parser_state: &ParserState) {
|
||||||
|
use crate::demo::sendprop::SendPropValue;
|
||||||
|
|
||||||
|
// println!("Known server classes: {:?}", parser_state.server_classes);
|
||||||
|
|
||||||
|
if let Some(class) = parser_state.server_classes.get(<ClassId as Into<usize>>::into(packet.server_class)) {
|
||||||
|
// println!("Got a {} data packet: {:?}", class.name, packet);
|
||||||
|
match class.name.as_str() {
|
||||||
|
"CTFPlayer" => {
|
||||||
|
match self.user_id_map.get(&packet.entity_index) {
|
||||||
|
Some(user_id) => {
|
||||||
|
let summaries = &mut self.state.player_summaries;
|
||||||
|
let player_summary = summaries.entry(*user_id).or_default();
|
||||||
|
|
||||||
|
// Extract scoreboard information, if present, and update the player's summary accordingly
|
||||||
|
// NOTE: Multiple DT_TFPlayerScoringDataExclusive structures may be present - one for the entire match,
|
||||||
|
// and one for just the current round. Since we're only interested in the overall match scores,
|
||||||
|
// we need to ignore the round-specific values. Fortunately, this is easy - just ignore the
|
||||||
|
// lesser value (if multiple values are present), since none of these scores are able to decrement.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Member: m_iCaptures (offset 4) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iDefenses (offset 8) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iKills (offset 12) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iDeaths (offset 16) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iSuicides (offset 20) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iDominations (offset 24) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iRevenge (offset 28) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iBuildingsBuilt (offset 32) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iBuildingsDestroyed (offset 36) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iHeadshots (offset 40) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iBackstabs (offset 44) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iHealPoints (offset 48) (type integer) (bits 20) (Unsigned)
|
||||||
|
* Member: m_iInvulns (offset 52) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iTeleports (offset 56) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iDamageDone (offset 60) (type integer) (bits 20) (Unsigned)
|
||||||
|
* Member: m_iCrits (offset 64) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iResupplyPoints (offset 68) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iKillAssists (offset 72) (type integer) (bits 12) (Unsigned)
|
||||||
|
* Member: m_iBonusPoints (offset 76) (type integer) (bits 10) (Unsigned)
|
||||||
|
* Member: m_iPoints (offset 80) (type integer) (bits 10) (Unsigned)
|
||||||
|
*
|
||||||
|
* NOTE: support points aren't included here, but is equal to the sum of m_iHealingAssist and m_iDamageAssist
|
||||||
|
* TODO: pull data for support points
|
||||||
|
*/
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iCaptures", parser_state, |captures| {
|
||||||
|
if captures > player_summary.captures {
|
||||||
|
player_summary.captures = captures;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iDefenses", parser_state, |defenses| {
|
||||||
|
if defenses > player_summary.defenses {
|
||||||
|
player_summary.defenses = defenses;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iKills", parser_state, |kills| {
|
||||||
|
if kills > player_summary.kills {
|
||||||
|
// TODO: This might not be accruate. Tested with a demo file with 89 kills (88 on the scoreboard),
|
||||||
|
// but only a 83 were reported in the scoring data.
|
||||||
|
player_summary.kills = kills;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iDeaths", parser_state, |deaths| {
|
||||||
|
if deaths > player_summary.deaths {
|
||||||
|
player_summary.deaths = deaths;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ignore m_iSuicides
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iDominations", parser_state, |dominations| {
|
||||||
|
if dominations > player_summary.dominations {
|
||||||
|
player_summary.dominations = dominations;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iRevenge", parser_state, |revenges| {
|
||||||
|
if revenges > player_summary.revenges {
|
||||||
|
player_summary.revenges = revenges;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ignore m_iBuildingsBuilt
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iBuildingsDestroyed", parser_state, |buildings_destroyed| {
|
||||||
|
if buildings_destroyed > player_summary.buildings_destroyed {
|
||||||
|
player_summary.buildings_destroyed = buildings_destroyed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iHeadshots", parser_state, |headshots| {
|
||||||
|
if headshots > player_summary.headshots {
|
||||||
|
player_summary.headshots = headshots;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iBackstabs", parser_state, |backstabs| {
|
||||||
|
if backstabs > player_summary.backstabs {
|
||||||
|
player_summary.backstabs = backstabs;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iHealPoints", parser_state, |healing| {
|
||||||
|
if healing > player_summary.healing {
|
||||||
|
player_summary.healing = healing;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iInvulns", parser_state, |ubercharges| {
|
||||||
|
if ubercharges > player_summary.ubercharges {
|
||||||
|
player_summary.ubercharges = ubercharges;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iTeleports", parser_state, |teleports| {
|
||||||
|
if teleports > player_summary.teleports {
|
||||||
|
player_summary.teleports = teleports;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iDamageDone", parser_state, |damage_dealt| {
|
||||||
|
if damage_dealt > player_summary.damage_dealt {
|
||||||
|
player_summary.damage_dealt = damage_dealt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ignore m_iCrits
|
||||||
|
// ignore m_iResupplyPoints
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iKillAssists", parser_state, |assists| {
|
||||||
|
if assists > player_summary.assists {
|
||||||
|
player_summary.assists = assists;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iBonusPoints", parser_state, |bonus_points| {
|
||||||
|
if bonus_points > player_summary.bonus_points {
|
||||||
|
player_summary.bonus_points = bonus_points;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parse_integer_prop(packet, "DT_TFPlayerScoringDataExclusive", "m_iPoints", parser_state, |points| {
|
||||||
|
if points > player_summary.points {
|
||||||
|
player_summary.points = points;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
// Player entity likely spawned before the player was assigned to it?
|
||||||
|
// This can rarely happen, but doesn't seem to affect the end result
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CTFPlayerResource" => {
|
||||||
|
// Player summaries - including entity IDs!
|
||||||
|
// look for props like m_iUserID.<entity_id> = <user_id>
|
||||||
|
// for example, `m_iUserID.024 = 2523` means entity 24 is user 2523
|
||||||
|
for i in 0..33 { // 0 to 32, inclusive (1..33 might also work, not sure if there's a user 0 or not). Not exhaustive and doesn't work for servers with > 32 players
|
||||||
|
match packet.get_prop_by_name("m_iUserID", format!("{:0>3}", i).as_str(), parser_state) {
|
||||||
|
Some(prop) => {
|
||||||
|
match prop.value {
|
||||||
|
SendPropValue::Integer(x) => {
|
||||||
|
let entity_id = EntityId::from(i as u32);
|
||||||
|
let user_id = UserId::from(x as u32);
|
||||||
|
self.user_id_map.insert(entity_id, user_id);
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// These properties should all be integers...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {} // ignore, no property for this entity was included
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_other => {
|
||||||
|
// Don't care
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_user_info(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
text: Option<&str>,
|
||||||
|
data: Option<Stream>,
|
||||||
|
) -> ReadResult<()> {
|
||||||
|
if let Some(user_info) =
|
||||||
|
crate::demo::data::UserInfo::parse_from_string_table(index as u16, text, data)?
|
||||||
|
{
|
||||||
|
self.state
|
||||||
|
.users
|
||||||
|
.entry(user_info.player_info.user_id.into())
|
||||||
|
.and_modify(|info| {
|
||||||
|
info.entity_id = user_info.entity_id;
|
||||||
|
})
|
||||||
|
.or_insert_with(|| user_info.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue