mirror of
https://codeberg.org/icewind/tf-log-parser.git
synced 2026-06-03 18:24:09 +02:00
class stats
This commit is contained in:
parent
cdaa164575
commit
9c63c8b658
8 changed files with 354 additions and 8 deletions
119
src/common.rs
119
src/common.rs
|
|
@ -1,12 +1,15 @@
|
|||
use crate::raw_event::RawSubject;
|
||||
use enum_iterator::IntoEnumIterator;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::str::FromStr;
|
||||
use steamid_ng::{AccountType, Instance, SteamID, Universe};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Serialize, Copy, Clone, Eq, PartialEq, Debug, Ord, PartialOrd)]
|
||||
#[derive(Serialize, Copy, Clone, Eq, PartialEq, Debug, Ord, PartialOrd, Hash)]
|
||||
pub enum Team {
|
||||
Red,
|
||||
Blue,
|
||||
|
|
@ -39,8 +42,118 @@ impl FromStr for Team {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, IntoEnumIterator, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Class {
|
||||
Scout,
|
||||
Soldier,
|
||||
Pyro,
|
||||
DemoMan,
|
||||
HeavyWeapons,
|
||||
Engineer,
|
||||
Medic,
|
||||
Sniper,
|
||||
Spy,
|
||||
}
|
||||
|
||||
impl Class {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Class::Scout => "scout",
|
||||
Class::Soldier => "soldier",
|
||||
Class::Pyro => "pyro",
|
||||
Class::DemoMan => "demoman",
|
||||
Class::HeavyWeapons => "heavyweapons",
|
||||
Class::Engineer => "engineer",
|
||||
Class::Medic => "medic",
|
||||
Class::Sniper => "sniper",
|
||||
Class::Spy => "spy",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Class {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"scout" => Ok(Class::Scout),
|
||||
"soldier" => Ok(Class::Soldier),
|
||||
"pyro" => Ok(Class::Pyro),
|
||||
"demoman" => Ok(Class::DemoMan),
|
||||
"heavyweapons" => Ok(Class::HeavyWeapons),
|
||||
"engineer" => Ok(Class::Engineer),
|
||||
"medic" => Ok(Class::Medic),
|
||||
"sniper" => Ok(Class::Sniper),
|
||||
"spy" => Ok(Class::Spy),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClassMap<T>([T; 9]);
|
||||
|
||||
impl<T> Index<Class> for ClassMap<T> {
|
||||
type Output = T;
|
||||
|
||||
fn index(&self, index: Class) -> &Self::Output {
|
||||
match index {
|
||||
Class::Scout => &self.0[0],
|
||||
Class::Soldier => &self.0[1],
|
||||
Class::Pyro => &self.0[2],
|
||||
Class::DemoMan => &self.0[3],
|
||||
Class::HeavyWeapons => &self.0[4],
|
||||
Class::Engineer => &self.0[5],
|
||||
Class::Medic => &self.0[6],
|
||||
Class::Sniper => &self.0[7],
|
||||
Class::Spy => &self.0[8],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IndexMut<Class> for ClassMap<T> {
|
||||
fn index_mut(&mut self, index: Class) -> &mut Self::Output {
|
||||
match index {
|
||||
Class::Scout => &mut self.0[0],
|
||||
Class::Soldier => &mut self.0[1],
|
||||
Class::Pyro => &mut self.0[2],
|
||||
Class::DemoMan => &mut self.0[3],
|
||||
Class::HeavyWeapons => &mut self.0[4],
|
||||
Class::Engineer => &mut self.0[5],
|
||||
Class::Medic => &mut self.0[6],
|
||||
Class::Sniper => &mut self.0[7],
|
||||
Class::Spy => &mut self.0[8],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Serialize for ClassMap<T>
|
||||
where
|
||||
T: Serialize + Default + PartialEq,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(None)?;
|
||||
for class in Class::into_enum_iter() {
|
||||
let stats = &self[class];
|
||||
if stats != &T::default() {
|
||||
map.serialize_entry(&class, stats)?;
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for ClassMap<T> {
|
||||
fn default() -> Self {
|
||||
ClassMap(<[T; 9]>::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized subject id
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Ord, PartialOrd)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Ord, PartialOrd, Hash)]
|
||||
pub enum SubjectId {
|
||||
Player(u32),
|
||||
Team(Team),
|
||||
|
|
@ -54,7 +167,7 @@ impl SubjectId {
|
|||
match self {
|
||||
SubjectId::Player(account_id) => Some(SteamID::new(
|
||||
*account_id,
|
||||
Instance::All,
|
||||
Instance::Desktop,
|
||||
AccountType::Individual,
|
||||
Universe::Public,
|
||||
)),
|
||||
|
|
|
|||
24
src/event/game.rs
Normal file
24
src/event/game.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use crate::event::{param_parse, param_parse_with};
|
||||
use nom::combinator::opt;
|
||||
use nom::number::complete::float;
|
||||
use nom::IResult;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RoundWinEvent<'a> {
|
||||
pub team: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn round_win_event_parser(input: &str) -> IResult<&str, RoundWinEvent> {
|
||||
let (input, team) = opt(param_parse("against"))(input)?;
|
||||
Ok((input, RoundWinEvent { team }))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RoundLengthEvent {
|
||||
pub length: Option<f32>,
|
||||
}
|
||||
|
||||
pub fn round_length_event_parser(input: &str) -> IResult<&str, RoundLengthEvent> {
|
||||
let (input, length) = opt(param_parse_with("against", float))(input)?;
|
||||
Ok((input, RoundLengthEvent { length }))
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
mod game;
|
||||
mod medic;
|
||||
mod player;
|
||||
|
||||
use crate::event::game::{RoundLengthEvent, RoundWinEvent};
|
||||
use crate::{RawEvent, RawEventType};
|
||||
pub use game::*;
|
||||
pub use medic::*;
|
||||
use nom::bytes::complete::{tag, take_while};
|
||||
use nom::character::complete::{alpha1, digit1};
|
||||
|
|
@ -59,6 +62,12 @@ pub enum GameEvent<'a> {
|
|||
FirstHeal(FirstHealEvent),
|
||||
ChargeReady,
|
||||
MedicDeath(MedicDeathEvent),
|
||||
Spawned(SpawnEvent),
|
||||
RoleChange(RoleChangeEvent),
|
||||
RoundStart,
|
||||
RoundWin(RoundWinEvent<'a>),
|
||||
RoundLength(RoundLengthEvent),
|
||||
RoundOverTime,
|
||||
}
|
||||
|
||||
impl<'a> GameEvent<'a> {
|
||||
|
|
@ -100,6 +109,20 @@ impl<'a> GameEvent<'a> {
|
|||
RawEventType::MedicDeath => {
|
||||
GameEvent::MedicDeath(medic_death_event_parser(raw.params).with_type(raw.ty)?)
|
||||
}
|
||||
RawEventType::Spawned => {
|
||||
GameEvent::Spawned(spawn_event_parser(raw.params).with_type(raw.ty)?)
|
||||
}
|
||||
RawEventType::ChangedRole => {
|
||||
GameEvent::RoleChange(role_changed_event_parser(raw.params).with_type(raw.ty)?)
|
||||
}
|
||||
RawEventType::RoundStart => GameEvent::RoundStart,
|
||||
RawEventType::RoundLength => {
|
||||
GameEvent::RoundLength(round_length_event_parser(raw.params).with_type(raw.ty)?)
|
||||
}
|
||||
RawEventType::RoundWin => {
|
||||
GameEvent::RoundWin(round_win_event_parser(raw.params).with_type(raw.ty)?)
|
||||
}
|
||||
RawEventType::RoundOvertime => GameEvent::RoundOverTime,
|
||||
_ => {
|
||||
todo!("{:?} not parsed yet", raw.ty);
|
||||
}
|
||||
|
|
@ -161,6 +184,7 @@ fn param_parse_with<'a, T, P: Fn(&'a str) -> IResult<&'a str, T>>(
|
|||
parser: P,
|
||||
) -> impl Fn(&'a str) -> IResult<&'a str, T> {
|
||||
move |input: &str| {
|
||||
let (input, _) = opt(tag(" "))(input)?;
|
||||
let (input, open_tag) = opt(tag("("))(input)?;
|
||||
|
||||
let (input, _) = tag(key)(input)?;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::common::Class;
|
||||
use crate::event::{param_parse, param_parse_with, position, u_int, ParamIter};
|
||||
use crate::raw_event::{subject_parser, RawSubject};
|
||||
use nom::combinator::opt;
|
||||
|
|
@ -60,7 +61,7 @@ pub struct KillEvent<'a> {
|
|||
}
|
||||
|
||||
pub fn kill_event_parser(input: &str) -> IResult<&str, KillEvent> {
|
||||
let (input, target) = param_parse_with("against", subject_parser)(input)?;
|
||||
let (input, target) = subject_parser(input)?;
|
||||
let (input, weapon) = param_parse("with")(input)?;
|
||||
let mut event = KillEvent {
|
||||
target,
|
||||
|
|
@ -101,3 +102,33 @@ pub fn kill_assist_event_parser(input: &str) -> IResult<&str, KillAssistEvent> {
|
|||
}
|
||||
Ok(("", event))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpawnEvent {
|
||||
pub class: Option<Class>,
|
||||
}
|
||||
|
||||
pub fn spawn_event_parser(input: &str) -> IResult<&str, SpawnEvent> {
|
||||
let (input, class_str) = param_parse("as")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
SpawnEvent {
|
||||
class: class_str.parse().ok(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RoleChangeEvent {
|
||||
pub class: Option<Class>,
|
||||
}
|
||||
|
||||
pub fn role_changed_event_parser(input: &str) -> IResult<&str, RoleChangeEvent> {
|
||||
let (input, class_str) = param_parse("to")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
RoleChangeEvent {
|
||||
class: class_str.parse().ok(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
pub use crate::common::{SteamId3, SubjectData, SubjectError, SubjectId};
|
||||
use crate::event::GameEventError;
|
||||
pub use crate::module::EventHandler;
|
||||
use crate::module::{ChatHandler, HealSpreadHandler, MedicStatsHandler};
|
||||
use crate::module::{ChatHandler, ClassStatsHandler, HealSpreadHandler, MedicStatsHandler};
|
||||
use crate::raw_event::RawSubject;
|
||||
use chrono::{DateTime, Utc};
|
||||
pub use event::GameEvent;
|
||||
|
|
@ -103,4 +103,5 @@ handler!(LogHandler {
|
|||
chat: ChatHandler,
|
||||
heal_spread: HealSpreadHandler,
|
||||
medic_stats: MedicStatsHandler,
|
||||
class_stats: ClassStatsHandler,
|
||||
});
|
||||
|
|
|
|||
146
src/module/classstats.rs
Normal file
146
src/module/classstats.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
use crate::common::{Class, ClassMap, SteamId3, SubjectId};
|
||||
use crate::event::{DamageEvent, GameEvent, RoleChangeEvent, SpawnEvent};
|
||||
use crate::module::EventHandler;
|
||||
use crate::raw_event::{RawEventType, RawSubject};
|
||||
use crate::SubjectMap;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
#[derive(Debug, Serialize, Default, PartialEq)]
|
||||
pub struct ClassStat {
|
||||
kills: u8,
|
||||
deaths: u8,
|
||||
assists: u8,
|
||||
damage: u16,
|
||||
}
|
||||
|
||||
impl Add for ClassStat {
|
||||
type Output = ClassStat;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
ClassStat {
|
||||
kills: self.kills + rhs.kills,
|
||||
deaths: self.deaths + rhs.deaths,
|
||||
assists: self.assists + rhs.assists,
|
||||
damage: self.damage + rhs.damage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for ClassStat {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.kills += rhs.kills;
|
||||
self.deaths += rhs.deaths;
|
||||
self.assists += rhs.assists;
|
||||
self.damage += rhs.damage;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClassStatsHandler {
|
||||
active: bool,
|
||||
classes: HashMap<SubjectId, Class>,
|
||||
stats: HashMap<SteamId3, ClassMap<ClassStat>>,
|
||||
}
|
||||
|
||||
impl ClassStatsHandler {
|
||||
fn handle_stats(&mut self, subject: SubjectId, target: &RawSubject, stats: ClassStat) {
|
||||
if let Ok(target) = target.id() {
|
||||
self.handle_stats_id(subject, target, stats)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_stats_id(&mut self, subject: SubjectId, target: SubjectId, stats: ClassStat) {
|
||||
let subject = if let Some(steam_id) = subject.steam_id() {
|
||||
steam_id
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(target_class) = self.classes.get(&target) {
|
||||
self.stats.entry(SteamId3(subject)).or_default()[*target_class] += stats;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventHandler for ClassStatsHandler {
|
||||
type Output = HashMap<SteamId3, ClassMap<ClassStat>>;
|
||||
|
||||
fn does_handle(&self, ty: RawEventType) -> bool {
|
||||
matches!(
|
||||
ty,
|
||||
RawEventType::Killed
|
||||
| RawEventType::KillAssist
|
||||
| RawEventType::Damage
|
||||
| RawEventType::Spawned
|
||||
| RawEventType::ChangedRole
|
||||
| RawEventType::RoundWin
|
||||
| RawEventType::RoundStart
|
||||
)
|
||||
}
|
||||
|
||||
fn handle(&mut self, _time: u32, subject: SubjectId, event: &GameEvent) {
|
||||
match event {
|
||||
GameEvent::Spawned(SpawnEvent { class: Some(class) })
|
||||
| GameEvent::RoleChange(RoleChangeEvent { class: Some(class) }) => {
|
||||
self.classes.insert(subject, *class);
|
||||
}
|
||||
GameEvent::RoundStart => {
|
||||
self.active = true;
|
||||
}
|
||||
GameEvent::RoundWin(_) => {
|
||||
self.active = false;
|
||||
}
|
||||
GameEvent::Kill(kill) if self.active => {
|
||||
self.handle_stats(
|
||||
subject,
|
||||
&kill.target,
|
||||
ClassStat {
|
||||
kills: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if let Ok(target) = kill.target.id() {
|
||||
self.handle_stats_id(
|
||||
target,
|
||||
subject,
|
||||
ClassStat {
|
||||
deaths: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
GameEvent::KillAssist(kill) if self.active => {
|
||||
self.handle_stats(
|
||||
subject,
|
||||
&kill.target,
|
||||
ClassStat {
|
||||
assists: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
GameEvent::Damage(DamageEvent {
|
||||
damage: Some(damage),
|
||||
target,
|
||||
..
|
||||
}) if self.active => {
|
||||
self.handle_stats(
|
||||
subject,
|
||||
target,
|
||||
ClassStat {
|
||||
damage: damage.get() as u16,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self, _subjects: &SubjectMap) -> Self::Output {
|
||||
self.stats
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ use crate::event::GameEvent;
|
|||
use crate::raw_event::RawEventType;
|
||||
use crate::SubjectMap;
|
||||
pub use chat::{ChatHandler, ChatMessage, ChatType};
|
||||
pub use classstats::{ClassStat, ClassStatsHandler};
|
||||
pub use healspread::HealSpreadHandler;
|
||||
pub use lobbysettings::{
|
||||
LobbySettingsError, LobbySettingsHandler, Location, Settings as LobbySettings,
|
||||
|
|
@ -10,6 +11,7 @@ pub use lobbysettings::{
|
|||
pub use medicstats::{MedicStats, MedicStatsHandler};
|
||||
|
||||
mod chat;
|
||||
mod classstats;
|
||||
mod healspread;
|
||||
mod lobbysettings;
|
||||
mod medicstats;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::{SubjectError, SubjectId};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use enum_iterator::IntoEnumIterator;
|
||||
use nom::branch::alt;
|
||||
|
|
@ -5,7 +6,7 @@ use nom::bytes::complete::{tag, take_while};
|
|||
use nom::character::complete::{digit1, one_of};
|
||||
use nom::error::{make_error, ErrorKind};
|
||||
use nom::{Finish, IResult};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
|
||||
/// Event that has only been minimally parsed.
|
||||
|
|
@ -125,6 +126,10 @@ impl<'a> RawSubject<'a> {
|
|||
RawSubject::World => "World",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Result<SubjectId, SubjectError> {
|
||||
self.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
fn subject_parser_world(input: &str) -> IResult<&str, RawSubject> {
|
||||
|
|
@ -266,7 +271,7 @@ impl RawEventType {
|
|||
pub fn tag(self) -> &'static str {
|
||||
match self {
|
||||
RawEventType::JoinedTeam => r#"joined team"#,
|
||||
RawEventType::ChangedRole => r#"changed role to"#,
|
||||
RawEventType::ChangedRole => r#"changed role"#,
|
||||
RawEventType::ShotFired => r#"triggered "shot_fired""#,
|
||||
RawEventType::ShotHit => r#"triggered "shot_hit""#,
|
||||
RawEventType::Damage => r#"triggered "damage""#,
|
||||
|
|
@ -277,7 +282,7 @@ impl RawEventType {
|
|||
RawEventType::Suicide => r#"committed suicide"#,
|
||||
RawEventType::Domination => r#"triggered "domination""#,
|
||||
RawEventType::Revenge => r#"triggered "revenge""#,
|
||||
RawEventType::Spawned => r#"spawned as"#,
|
||||
RawEventType::Spawned => r#"spawned"#,
|
||||
RawEventType::SayTeam => r#"say_team"#,
|
||||
RawEventType::Say => r#"say"#,
|
||||
RawEventType::EmptyUber => r#"triggered "empty_uber""#,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue