class stats

This commit is contained in:
Robin Appelman 2021-08-08 15:39:07 +02:00
commit 9c63c8b658
8 changed files with 354 additions and 8 deletions

View file

@ -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
View 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 }))
}

View file

@ -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)?;

View file

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

View file

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

View file

@ -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;

View file

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