This commit is contained in:
Robin Appelman 2021-08-07 19:03:58 +02:00
commit ca61f4ea6a
8 changed files with 531 additions and 11 deletions

View file

@ -7,4 +7,8 @@ edition = "2018"
steamid-ng = "1"
nom = "6"
enum-iterator = "0.7"
chrono = "0.4"
chrono = "0.4"
thiserror = "1"
[dev-dependencies]
main_error = "0.1"

24
examples/lobby_chat.rs Normal file
View file

@ -0,0 +1,24 @@
use main_error::MainError;
use std::env::args;
use std::fs;
use tf_log_parser::module::{ChatHandler, HandlerStack, LobbySettingsHandler, OptionalHandler};
use tf_log_parser::parse_with_handler;
type Handler = HandlerStack<ChatHandler, OptionalHandler<LobbySettingsHandler>>;
fn main() -> Result<(), MainError> {
let path = args().skip(1).next().expect("No path provided");
let content = fs::read_to_string(path)?;
let (chat, lobby_settings) = parse_with_handler::<Handler>(&content)?;
if let Ok(Some(settings)) = lobby_settings {
println!("Lobby settings: {:#?}", settings);
println!();
}
for message in chat {
println!("{}: {}", message.time, message.message);
}
Ok(())
}

42
src/common.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::raw_event::RawSubject;
use std::str::FromStr;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Team {
Red,
Blue,
}
impl FromStr for Team {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Blue" => Ok(Team::Blue),
"Red" => Ok(Team::Red),
_ => Err(()),
}
}
}
/// Optimized subject id
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum SubjectId {
Player(u8),
Team(Team),
System,
World,
Console,
}
impl From<&RawSubject<'_>> for SubjectId {
fn from(raw: &RawSubject) -> Self {
match raw {
RawSubject::Player { user_id, .. } => SubjectId::Player(user_id.parse().unwrap()),
RawSubject::Team(team) => SubjectId::Team(team.parse().unwrap()),
RawSubject::System(_) => SubjectId::System,
RawSubject::Console => SubjectId::Console,
RawSubject::World => SubjectId::World,
}
}
}

View file

@ -1,9 +1,65 @@
use crate::module::EventHandler;
use crate::raw_event::RawEvent;
use chrono::{DateTime, Utc};
use std::convert::TryInto;
use std::fmt::{Debug, Formatter};
use thiserror::Error;
mod common;
pub mod module;
mod raw_event;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
#[derive(Error)]
pub enum Error<Handler: EventHandler> {
#[error("Malformed logfile: {0}")]
Malformed(String),
#[error("{0}")]
HandlerError(Handler::Error),
}
impl<Handler: EventHandler> Debug for Error<Handler> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::Malformed(e) => e.fmt(f),
Error::HandlerError(e) => e.fmt(f),
}
}
}
impl<Handler: EventHandler> From<nom::error::Error<&'_ str>> for Error<Handler> {
fn from(e: nom::error::Error<&str>) -> Self {
Error::Malformed(e.to_string())
}
}
pub fn parse_with_handler<Handler: EventHandler>(
log: &str,
) -> Result<Handler::Output, Error<Handler>> {
let events = log
.lines()
.filter(|line| line.starts_with("L "))
.map(RawEvent::parse);
let mut handler = Handler::default();
let mut start_time: Option<DateTime<Utc>> = None;
for event_res in events {
let event = event_res?;
if handler.does_handle(event.ty) || start_time.is_none() {
let event_time: DateTime<Utc> = (&event.date).try_into().unwrap();
let match_time = match start_time {
Some(start_time) => (event_time - start_time).num_seconds() as u32,
None => {
start_time = Some(event_time);
0
}
};
handler
.handle(match_time, (&event.subject).into(), &event)
.map_err(Error::HandlerError)?;
}
}
Ok(handler.finish())
}

59
src/module/chat.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::common::SubjectId;
use crate::module::EventHandler;
use crate::raw_event::{RawEvent, RawEventType};
use std::convert::Infallible;
pub struct ChatMessage {
pub time: u32,
pub subject: SubjectId,
pub message: String,
pub chat_type: ChatType,
}
pub enum ChatType {
All,
Team,
}
#[derive(Default)]
pub struct ChatHandler(Vec<ChatMessage>);
impl EventHandler for ChatHandler {
type Output = Vec<ChatMessage>;
type Error = Infallible;
fn does_handle(&self, ty: RawEventType) -> bool {
matches!(ty, RawEventType::SayTeam | RawEventType::Say)
}
fn handle(
&mut self,
time: u32,
subject: SubjectId,
event: &RawEvent,
) -> Result<(), Infallible> {
if !matches!(subject, SubjectId::Player(_)) {
return Ok(());
}
match event.ty {
RawEventType::SayTeam => self.0.push(ChatMessage {
time,
subject,
message: event.params.trim_matches('"').into(),
chat_type: ChatType::Team,
}),
RawEventType::Say => self.0.push(ChatMessage {
time,
subject,
message: event.params.trim_matches('"').into(),
chat_type: ChatType::All,
}),
_ => {}
}
Ok(())
}
fn finish(self) -> Self::Output {
self.0
}
}

202
src/module/lobbysettings.rs Normal file
View file

@ -0,0 +1,202 @@
use crate::common::SubjectId;
use crate::module::EventHandler;
use crate::raw_event::{RawEvent, RawEventType};
use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone, Utc};
use std::str::{FromStr, ParseBoolError};
use steamid_ng::SteamID;
use thiserror::Error;
#[derive(Debug)]
pub enum GameType {
Sixes,
Highlander,
}
impl FromStr for GameType {
type Err = LobbySettingsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"6v6" => Ok(Self::Sixes),
"highlander" => Ok(Self::Highlander),
unknown => Err(LobbySettingsError::UnknownGameType(unknown.into())),
}
}
}
#[derive(Debug)]
pub enum Location {
Europe,
NorthAmerica,
}
impl FromStr for Location {
type Err = LobbySettingsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Europe" => Ok(Self::Europe),
"North America" => Ok(Self::NorthAmerica),
unknown => Err(LobbySettingsError::UnknownLocation(unknown.into())),
}
}
}
#[derive(Debug, Default)]
pub struct LobbyLeader {
name: String,
steam_id: SteamID,
}
impl FromStr for LobbyLeader {
type Err = LobbySettingsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((name, steam_id)) = s.rsplit_once(" (") {
if let Ok(steam_id) = steam_id.trim_end_matches(")").parse::<u64>() {
Ok(LobbyLeader {
name: name.into(),
steam_id: steam_id.into(),
})
} else {
Err(LobbySettingsError::MalformedLeader(s.into()))
}
} else {
Err(LobbySettingsError::MalformedLeader(s.into()))
}
}
}
#[derive(Debug)]
pub struct Settings {
id: u32,
leader: LobbyLeader,
map: String,
game_type: GameType,
location: Location,
advanced: bool,
region_lock: bool,
allow_offclassing: bool,
balancing: bool,
restriction: String,
mumble_required: bool,
date: DateTime<Utc>,
server: String,
}
impl Default for Settings {
fn default() -> Self {
Settings {
id: 0,
leader: LobbyLeader::default(),
map: "".to_string(),
game_type: GameType::Sixes,
location: Location::Europe,
advanced: false,
region_lock: false,
allow_offclassing: false,
balancing: false,
restriction: "".to_string(),
mumble_required: false,
date: Utc.ymd(1, 1, 1).and_hms(0, 0, 0),
server: "".to_string(),
}
}
}
#[derive(Debug, Error)]
pub enum LobbySettingsError {
#[error("Malformed lobby id: {0}")]
InvalidLobbyId(String),
#[error("Unknown game type: {0}")]
UnknownGameType(String),
#[error("Unknown location: {0}")]
UnknownLocation(String),
#[error("Unknown timezone in date: {0}")]
UnknownTimezone(String),
#[error("Malformed leader: {0}")]
MalformedLeader(String),
#[error("{0}")]
InvalidBool(#[from] ParseBoolError),
#[error("{0}")]
InvalidDate(#[from] chrono::ParseError),
}
#[derive(Default)]
pub struct LobbySettingsHandler(Settings);
impl EventHandler for LobbySettingsHandler {
type Output = Option<Settings>;
type Error = LobbySettingsError;
fn does_handle(&self, ty: RawEventType) -> bool {
matches!(ty, RawEventType::Say)
}
fn handle(
&mut self,
_time: u32,
subject: SubjectId,
event: &RawEvent,
) -> Result<(), Self::Error> {
if !matches!(subject, SubjectId::Console) {
return Ok(());
}
if matches!(event.ty, RawEventType::Say) {
let msg = event.params.trim_matches('"');
if let Some((id, _)) = msg
.strip_prefix("TF2Center Lobby #")
.and_then(|s| str::split_once(s, " |"))
{
self.0.id = id
.parse()
.map_err(|_| LobbySettingsError::InvalidLobbyId(id.into()))?;
}
if let Some((key, value)) = msg.split_once(": ") {
match key {
"Leader" => self.0.leader = value.parse()?,
"Map" => self.0.map = value.into(),
"GameType" => self.0.game_type = value.parse()?,
"Location" => self.0.location = value.parse()?,
"Advanced Lobby" => self.0.advanced = value.parse()?,
"Region lock" => self.0.region_lock = value.parse()?,
"Allow offclassing" => self.0.allow_offclassing = value.parse()?,
"Balancing" => self.0.balancing = value.parse()?,
"Restriction" => self.0.restriction = value.into(),
"Mumble required" => self.0.mumble_required = value.parse()?,
"Launch date" => {
self.0.date = get_timezone(value)?
.from_local_datetime(&NaiveDateTime::parse_from_str(
value,
"%a %b %d %H:%M:%S %Z %Y",
)?)
.earliest()
.unwrap()
.into()
}
"Server" => self.0.server = value.into(),
_ => {}
}
}
}
Ok(())
}
fn finish(self) -> Self::Output {
if self.0.id > 0 {
Some(self.0)
} else {
None
}
}
}
fn get_timezone(date: &str) -> Result<FixedOffset, LobbySettingsError> {
if date.contains("CEST") {
Ok(FixedOffset::east(2 * 60 * 60))
} else if date.contains("CET") {
Ok(FixedOffset::east(60 * 60))
} else {
Err(LobbySettingsError::UnknownTimezone(date.into()))
}
}

121
src/module/mod.rs Normal file
View file

@ -0,0 +1,121 @@
use crate::common::SubjectId;
use crate::raw_event::{RawEvent, RawEventType};
pub use chat::{ChatHandler, ChatMessage, ChatType};
pub use lobbysettings::{
LobbySettingsError, LobbySettingsHandler, Location, Settings as LobbySettings,
};
use std::convert::Infallible;
use std::error::Error;
use std::fmt::Debug;
use thiserror::Error;
mod chat;
mod lobbysettings;
pub trait EventHandler: Default {
type Output;
type Error: Error + Debug;
fn does_handle(&self, ty: RawEventType) -> bool;
fn handle(
&mut self,
time: u32,
subject: SubjectId,
event: &RawEvent,
) -> Result<(), Self::Error>;
fn finish(self) -> Self::Output;
}
#[derive(Default)]
pub struct HandlerStack<Head, Tail> {
head: Head,
tail: Tail,
}
impl<Head: EventHandler, Tail: EventHandler> EventHandler for HandlerStack<Head, Tail> {
type Output = (Head::Output, Tail::Output);
type Error = EitherError<Head::Error, Tail::Error>;
fn does_handle(&self, ty: RawEventType) -> bool {
self.head.does_handle(ty) || self.tail.does_handle(ty)
}
fn handle(
&mut self,
time: u32,
subject: SubjectId,
event: &RawEvent,
) -> Result<(), Self::Error> {
self.head
.handle(time, subject, event)
.map_err(|e| EitherError::E1(e))?;
self.tail
.handle(time, subject, event)
.map_err(|e| EitherError::E2(e))?;
Ok(())
}
fn finish(self) -> Self::Output {
(self.head.finish(), self.tail.finish())
}
}
#[derive(Debug, Error)]
pub enum EitherError<E1: Error, E2: Error> {
#[error("{0}")]
E1(E1),
#[error("{0}")]
E2(E2),
}
/// A handler that doesn't stop the parsing on failure
pub enum OptionalHandler<Handler: EventHandler> {
Active(Handler),
Failed(Handler::Error),
}
impl<Handler: EventHandler> Default for OptionalHandler<Handler> {
fn default() -> Self {
OptionalHandler::Active(Handler::default())
}
}
impl<Handler: EventHandler> EventHandler for OptionalHandler<Handler> {
type Output = Result<Handler::Output, Handler::Error>;
type Error = Infallible;
fn does_handle(&self, ty: RawEventType) -> bool {
match self {
OptionalHandler::Active(handler) => handler.does_handle(ty),
OptionalHandler::Failed(_) => false,
}
}
fn handle(
&mut self,
time: u32,
subject: SubjectId,
event: &RawEvent,
) -> Result<(), Self::Error> {
let res = if let OptionalHandler::Active(handler) = self {
handler.handle(time, subject, event)
} else {
Ok(())
};
if let Err(e) = res {
dbg!(&e);
*self = OptionalHandler::Failed(e)
}
Ok(())
}
fn finish(self) -> Self::Output {
match self {
OptionalHandler::Active(handler) => Ok(handler.finish()),
OptionalHandler::Failed(e) => Err(e),
}
}
}

View file

@ -56,10 +56,10 @@ pub struct RawDate<'a> {
pub seconds: &'a str,
}
impl<'a> TryFrom<RawDate<'a>> for DateTime<Utc> {
impl<'a> TryFrom<&RawDate<'a>> for DateTime<Utc> {
type Error = ParseIntError;
fn try_from(value: RawDate<'a>) -> Result<Self, Self::Error> {
fn try_from(value: &RawDate<'a>) -> Result<Self, Self::Error> {
Ok(Utc
.ymd(
value.year.parse()?,
@ -115,20 +115,32 @@ pub enum RawSubject<'a> {
World,
}
impl<'a> RawSubject<'a> {
pub fn name(&self) -> &'a str {
match self {
RawSubject::Player { name, .. } => name,
RawSubject::Team(team) => team,
RawSubject::System(system) => system,
RawSubject::Console => "Console",
RawSubject::World => "World",
}
}
}
fn subject_parser_world(input: &str) -> IResult<&str, RawSubject> {
let (input, _) = tag("World")(input)?;
Ok((input, RawSubject::World))
}
fn subject_parser_console(input: &str) -> IResult<&str, RawSubject> {
let (input, _) = tag("Console<0><Console><Console>")(input)?;
Ok((input, RawSubject::World))
let (input, _) = tag(r#""Console<0><Console><Console>""#)(input)?;
Ok((input, RawSubject::Console))
}
fn subject_parser_team(input: &str) -> IResult<&str, RawSubject> {
let (input, _) = tag(r#"Team ""#)(input)?;
let (input, team) = take_while(|c| c != '"')(input)?;
let (input, team) = alt((tag("Red"), tag("Blue")))(input)?;
let (input, _) = one_of("\"")(input)?;
Ok((input, RawSubject::Team(team)))