mirror of
https://codeberg.org/icewind/tf-log-parser.git
synced 2026-06-03 18:24:09 +02:00
handlers
This commit is contained in:
parent
dfc702f43d
commit
ca61f4ea6a
8 changed files with 531 additions and 11 deletions
|
|
@ -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
24
examples/lobby_chat.rs
Normal 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
42
src/common.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/lib.rs
66
src/lib.rs
|
|
@ -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
59
src/module/chat.rs
Normal 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
202
src/module/lobbysettings.rs
Normal 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
121
src/module/mod.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue