#!/usr/bin/perl

#
# A Quake3 Arena log parser written in Perl.
#
# qplog3 - version 1.11 - Dec 17, 1999
#
# Copyright (C) 1999  Michael Q. Le, mle@eelinux.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

use Socket;
use FileHandle;

  # Command line arguments
undef($LogFile);
$StatFile = 'q3stats';
$LogMax = 7;
$SleepTime = 3600;
$MapsMin = 5;
$GamePort = '27960';
$MasterHost = 'www.eelinux.com';
$MasterPort = '27955';
$DefaultTimeout = 120;
$ReportMaster = 0;
$Version = '1.11';

    # Get command line args
GET_ARGS();

  # Main loop
STDOUT->autoflush;
print STDOUT "Sending data for game port \"$GamePort\".\n";
print STDOUT "Using the log file \"$LogFile\".\n";
print STDOUT "Stats will be saved as \"${StatFile}.*\".\n";
$udt = sprintf("%1.1f", $SleepTime/3600);
for (;;) {
    if ((-e "$LogFile") && (!(-z "$LogFile"))) {
        $date = `date`; chop($date);
        print STDOUT "Checking the logfile... $date\n";
        PARSE_LOG($LogFile);
    }
    sleep ($SleepTime);
}

  # Get command line arguments
sub GET_ARGS {
    while(@ARGV) {
        my $arg = shift(@ARGV);
        if    ($arg eq '-l') { $LogFile = shift(@ARGV); }
        elsif ($arg eq '-o') { $StatFile = shift(@ARGV); }
        elsif ($arg eq '-s') { $SleepTime = shift(@ARGV); }
        elsif ($arg eq '-d') { $LogMax = shift(@ARGV); }
        elsif ($arg eq '-m') { $MapsMin = shift(@ARGV); }
        elsif ($arg eq '-p') { $GamePort = shift(@ARGV); }
        elsif ($arg eq '-h') { USAGE(''); }
        elsif ($arg eq '-r') { $ReportMaster = 1; }
        else  { USAGE($arg); }
    }
    if (!(defined $LogFile)) { USAGE(''); }
    if ($LogFile eq "qconsole.log") {
        print STDOUT "Do not use $LogFile with this program.\n";
        USAGE('');
    }
}

  # Parse the log file
sub PARSE_LOG {

      # Read previously saved data
    my @game_data = ();

      # Read the new log data
    system "cat $LogFile >> ${LogFile}.save";
    unlink("$LogFile");
    open(LOG,"<${LogFile}.save") || die "Could not open ${LogFile}.save\n";
    my $found = 0;
    while(<LOG>) {
      # Get the data for each map
        s/^(\d+:\d\d)/$1 /;
        my @data = split(' ', $_);
        next if (!(defined $data[1]));
        if ($found) {
            push(@game_data, $_);
            if ($data[1] eq "ShutdownGame:") {
                $found = 0;
                print STDOUT "  Generating game stats...\n";
                PARSE_STATS(@game_data);
                @game_data = ();
            }
        }
        elsif ($data[1] eq "InitGame:") {
            $found = 1;
            @game_data = ();
            push(@game_data, $_);
        }
    }
    close(LOG);

      # Save data for the current map
    open(LOG,">${LogFile}.save") || die "Could not open ${LogFile}.save\n";
    foreach(@game_data) { print LOG $_; }
    close(LOG);

      # Create the HTML files
    my @hs = @{ MAKE_HTML() };

      # Send data to the master
    my $pkt = "$GamePort\n$Version $LogMax $udt $MapsMin\n$hs[0]\n$hs[1]\n$hs[2]\n$hs[3]\n$hs[4]\n$hs[5]\n$hs[6]\n$hs[7]\n";
    if ($ReportMaster) {
        open(STAT,"<${StatFile}.raw") || return;
        while(<STAT>) { $pkt .= $_; }
        close(STAT);
    }
    my $bytes = TALK_MASTER($MasterHost, $MasterPort, $pkt);
    if ($bytes) { print STDOUT "  Sent $bytes bytes to $MasterHost\n"; }
}

  # Parse the stats for 1 game
sub PARSE_STATS {

    my (@data) = @_;
    my %Clients;
    my %Players;
    my $end;
    my $begin;
    my $cnt = 0;
    my $MinTime = 0;
    my $HighScore = 0;

      # Hash keys:
      #   b=begin_time, d=deaths, k=kills, n=name, p=ping,
      #   s=suicides, t=time(sec) w=wins
    foreach (@data) {
        chop;
        my @dat = split(' ', $_);
          # Kills
        if ($dat[1] eq "Kill:") {
            if ($dat[2] eq $dat[3])  { $Clients{$dat[2]}{"s"} += 1; }
            elsif ($dat[2] eq "1022") { $Clients{$dat[3]}{"s"} += 1; }
            else { $Clients{$dat[2]}{"k"} += 1; $Clients{$dat[3]}{"d"} += 1; }
        }
          # Connects
        elsif ($dat[1] eq "ClientBegin:") { $Clients{$dat[2]}{"b"} = $dat[0]; }
          # Disconnects
        elsif ($dat[1] eq "ClientDisconnect:") { delete $Clients{$dat[2]}; }
          # Shutdown
        elsif ($dat[1] eq "score:") {
            next if (!(defined $dat[7]));
            next if ($dat[7] eq "UnnamedPlayer");

            my $start = $begin;
            if (defined $Clients{$dat[6]}{"b"}) {
                my @t = split(':', $Clients{$dat[6]}{"b"});
                $start = $t[0] * 60 + $t[1];
            }
            $start = $end - $start;
            next if ($start < $MinTime);

            while(@dat > 8) { $dat[7] .= " " . pop(@dat); }
            my $name = $dat[7];
            $name =~ s/\^[0-9]//g;
            $name =~ s/</[/g;
            $name =~ s/>/]/g;
            $name =~ s/[\cA-\cZ]//g;
            if (defined $Players{$name}) { $name .= "_$cnt"; }
            if (length($name) > 20) { $name = substr($name, 0, 20); }
            $Players{$name}{"t"} = $start;
            $Players{$name}{"m"} = 1;
            $Players{$name}{"p"} = $dat[4];
            if (!(defined $Clients{$dat[6]}{"d"})) {
                $Players{$name}{"d"} = 0; }
            else { $Players{$name}{"d"} = $Clients{$dat[6]}{"d"}; }
            if (!(defined $Clients{$dat[6]}{"k"})) {
                $Players{$name}{"k"} = 0; }
            else { $Players{$name}{"k"} = $Clients{$dat[6]}{"k"}; }
            if (!(defined $Clients{$dat[6]}{"s"})) {
                $Players{$name}{"s"} = 0; }
            else { $Players{$name}{"s"} = $Clients{$dat[6]}{"s"}; }
            if ($dat[2] > $HighScore) { $HighScore = $dat[2]; }
        }
        elsif ($dat[1] eq "Exit:") {
            my @t = split(':', $dat[0]); $end = $t[0] * 60 + $t[1];
            $MinTime = 0.75 * ($end - $begin);
        }
        elsif ($dat[1] eq "InitGame:") {
            my @t = split(':', $dat[0]); $begin = $t[0] * 60 + $t[1];
        }
    }
      # Determine winner of the map
    foreach (keys %Players) {
        my $score = $Players{$_}{"k"} - $Players{$_}{"s"};
        if ($score == $HighScore) { $Players{$_}{"w"} = 1; }
        else { $Players{$_}{"w"} = 0; }
    }

          # Open the old logfile and add the old data
    my $file = $StatFile . ".1";
    my @lt = localtime(time); $lt[4]++;
    if ($lt[1] < 10) { $lt[1] = "0" . $lt[1]; }
    my $LocalTime = "$lt[2]:$lt[1] $lt[4]/$lt[3]/$lt[5]";
    my $LocalDay = "$lt[4]/$lt[3]/$lt[5]";
    if (-e $file) {
        open(STAT,"<$file") || die "Could not open $file.\n";
        my $tmp = <STAT>;
          # Check for a new date and rotate the log files
        if ($tmp !~ /${LocalDay}$/) {
            close(STAT);
            for ($i = 1; $i < $LogMax; $i++) {
                my $j = $LogMax - $i; my $k = $j + 1;
                rename("${StatFile}.${j}", "${StatFile}.${k}");
            }
        }
        else {
            while(<STAT>) {
                next if (/^--/);
                my @tmp = split(' ', $_);
                while(@tmp > 8) { $tmp[7] .= " " . pop(@tmp); }
                $Players{"$tmp[7]"}{"d"} += $tmp[0];
                $Players{"$tmp[7]"}{"k"} += $tmp[1];
                $Players{"$tmp[7]"}{"m"} += $tmp[2];
                $Players{"$tmp[7]"}{"p"} += $tmp[3];
                $Players{"$tmp[7]"}{"s"} += $tmp[4];
                $Players{"$tmp[7]"}{"t"} += $tmp[5];
                $Players{"$tmp[7]"}{"w"} += $tmp[6];
            }
            close(STAT);
        }
    }

      # Save the data to the log file
    open(STAT,">$file") || die "Could not stats file for $StatFile.\n";
    print STAT "-- Last updated:  $LocalTime\n";
    foreach (keys %Players) {
        print STAT "$Players{$_}{d} $Players{$_}{k} $Players{$_}{m} $Players{$_}{p} $Players{$_}{s} $Players{$_}{t} $Players{$_}{w} $_\n";
    }
    close(STAT);
}

  # Make the HTML file, there's a more efficient way to do this...
sub MAKE_HTML {

      # Read all the stat files
    my %Players;
    for ($i = 1; $i <= $LogMax; $i++) {
        if (-e "${StatFile}.${i}") {
            open(STAT,"<${StatFile}.${i}") || die "Could not generate HTML!\n";
            while(<STAT>) {
                next if (/^--/);
                my @tmp = split(' ', $_);
                while(@tmp > 8) { $tmp[7] .= " " . pop(@tmp); }
                $Players{"$tmp[7]"}{"d"} += $tmp[0];
                $Players{"$tmp[7]"}{"k"} += $tmp[1];
                $Players{"$tmp[7]"}{"m"} += $tmp[2];
                $Players{"$tmp[7]"}{"p"} += $tmp[3];
                $Players{"$tmp[7]"}{"s"} += $tmp[4];
                $Players{"$tmp[7]"}{"t"} += $tmp[5];
                $Players{"$tmp[7]"}{"w"} += $tmp[6];
            }
            close(STAT);
            next;
        }
        last;
    }
    if ($ReportMaster) {
        open(STAT,">${StatFile}.raw") || die "Could not generate stats!\n";
        foreach (sort(keys %Players)) {
            print STAT "$Players{$_}{d} $Players{$_}{k} $Players{$_}{m} $Players{$_}{p} $Players{$_}{s} $Players{$_}{t} $Players{$_}{w} $_\n";
        }
        close(STAT);
        my @high = ('0', '0', '0', '0', '0', '0', '0', '0');
        return \@high;
    }
      # HTML header
    open(HTML,">${StatFile}.html") || die "Could not generate HTML!\n";
    print HTML "<html>\n<meta http-equiv=Pragma content=no-cache>\n";
    print HTML "<head><title>Player Stats</title></head>\n";
    print HTML "<body bgcolor=black text=\#ffdeb3 link=green vlink=green>\n";
    print HTML "<h2><font color=red>Player Stats</font></h2>\n";
    print HTML "<font color=red size=2><b>Last updated: $date</b></font><hr>\n";
    print HTML "<pre>\n";

      # Print player stats
    my $cnt = 25;
    my $pcnt = 0;
    my $sum_eff = 0;
    my $sum_win = 0;
    my $sum_fph = 0;
    my $sum_fpm = 0;
    my @hi = ("none", 0, "none", 0, "none", 0, "none", 0);
    foreach (sort(keys %Players)) {
        if ($cnt == 25) {
            print HTML "<font color=red><b>";
            print HTML "  Name                Kills  Deaths  Suic   Maps";
            print HTML "   Ping   Time  Eff(%) Win(%) Frags/Mp Frags/Hr</b></font>\n";
            $cnt = 0;
        }
        $cnt++;
        my $ping = sprintf("%1.0f", $Players{$_}{"p"}/$Players{$_}{"m"});
        my $time = sprintf("%1.0f", $Players{$_}{"t"}/60);
        my $fph  = sprintf("%1.2f", 60*($Players{$_}{"k"} - $Players{$_}{"s"})/$time);
        my $fpm  = sprintf("%1.2f", ($Players{$_}{"k"} - $Players{$_}{"s"})/$Players{$_}{"m"});
        my $e = $Players{$_}{"k"} + $Players{$_}{"d"} + $Players{$_}{"s"};
        my $eff = 0;
        if ($e > 0) { $eff  = sprintf("%1.2f", 100*$Players{$_}{"k"}/$e); }
        my $wp = sprintf("%1.2f", 100*$Players{$_}{"w"}/$Players{$_}{"m"});
        my $line = sprintf("%-20s %6s %6s %6s %6s %6s %6s %6s %6s %8s %8s\n",
        $_, $Players{$_}{"k"}, $Players{$_}{"d"}, $Players{$_}{"s"},
        $Players{$_}{"m"}, $ping, $time, $eff, $wp, $fpm, $fph);
        print HTML $line;

          # Get high scores, most kills wins the tie breaker
        if ($Players{$_}{"m"} >= $MapsMin) {
            if ($eff > $hi[1])     { $hi[0] = $_; $hi[1] = $eff; }
            elsif ($eff == $h[1]) {
               if ($Players{$_}{"k"} > $Players{$h[0]}{"k"}) { $h[0] .= $_; } }

            if ($wp > $hi[3])      { $hi[2] = $_; $hi[3] = $wp; }
            elsif ($wp == $hi[3])  {
               if ($Players{$_}{"k"} > $Players{$h[2]}{"k"}) { $h[2] .= $_; } }

            if ($fph > $hi[5])     { $hi[4] = $_; $hi[5] = $fph; }
            elsif ($fph == $hi[5]) {
               if ($Players{$_}{"k"} > $Players{$h[4]}{"k"}) { $h[4] .= $_; } }

            if ($fpm > $hi[7])     { $hi[6] = $_; $hi[7] = $fpm; }
            elsif ($fpm == $hi[7]) {
               if ($Players{$_}{"k"} > $Players{$h[6]}{"k"}) { $h[6] .= $_; } }
        }
        $sum_eff += $eff;
        $sum_win += $wp;
        $sum_fph += $fph;
        $sum_fpm += $fpm;
        $pcnt++;
    }
    if ($hi[1] > 0) {
        $sum_eff = sprintf("%1.2f", $sum_eff/$pcnt);
        $sum_win = sprintf("%1.2f", $sum_win/$pcnt);
        $sum_fph = sprintf("%1.2f", $sum_fph/$pcnt);
        $sum_fpm = sprintf("%1.2f", $sum_fpm/$pcnt);
        print HTML "\n<font color=red><b> Category   Avg Score   Top Score   Top Player</b></font>\n\n";
        my $line = sprintf("Efficiency %8s %11s     %-20s\n",$sum_eff,$hi[1],$hi[0],);
        print HTML $line;
        $line = sprintf("Win Ratio  %8s %11s     %-20s\n",$sum_win,$hi[3],$hi[2]);
        print HTML $line;
        $line = sprintf("Frags/Hour %8s %11s     %-20s\n",$sum_fph,$hi[5],$hi[4]);
        print HTML $line;
        $line = sprintf("Frags/Map  %8s %11s     %-20s\n",$sum_fpm,$hi[7],$hi[6]);
        print HTML $line;

    }
    print HTML "\n<font color=red><b> Total Players:</b></font> $pcnt\n";
    print HTML "<font color=red><b> Stats Kept:</b></font>    $LogMax days\n";
    print HTML "<font color=red><b> Updated Every:</b></font> $udt hours\n";
    print HTML "</pre><hr>\n";
    print HTML "<p><font color=red size=2><b>Stats generated by <a href=\"http://www.eelinux.com/quake3\">qplog3</a></b></font></p>\n";
    print HTML "</body></html>\n";
    close(HTML);
    return \@hi
}

  # Send data to the master server
sub TALK_MASTER {

    my ($MasterHost, $MasterPort, $pkt) = @_;

      # Create socket, use tcp
    my $tcpProto = getprotobyname('tcp') || 6;
    unless (socket(SOCKET, PF_INET, SOCK_STREAM, $tcpProto)) {
        print STDOUT "Socket error\n";
        return 0;
    }

      # Return if timeout
    $SIG{'ALRM'} = sub { close(SOCKET); print STDOUT "Timeout error: $!\n"; return 0; };
    alarm $DefaultTimeout;

      # Connect to the server
    my $MasterAddr = inet_aton($MasterHost);
    my $PackedAddr = sockaddr_in($MasterPort, $MasterAddr);
    unless (connect(SOCKET, $PackedAddr)) {
        print STDOUT "  Connect error: $!\n";
        alarm 0;
        return 0;
    }

      # Send the data
    my $bytes = 0;
    my $length = length($pkt);
    unless ($bytes = send(SOCKET, $pkt, 0)) {
        print STDOUT "  Send error: $!\n";
        alarm 0;
        return 0;
    }
    close(SOCKET);
    alarm 0;

    return $bytes;
}

  # Prints usage info
sub USAGE {

   my ($arg) = @_;

   print STDOUT "Error, $arg is an invalid argument.\n" if ($arg);
print <<eof;

 Quake3 Player Log - version $Version

 qplog3 -l logfile [-p port] [-o output_file] [-d int] [-s int] [-m int] [-h]

  -d integer     Number of days to keep the stats <$LogMax>. Servers that are
                 very busy should decrease this number. The default should be
                 good for most servers.
  -h             Prints this help message.
  -l logfile     Name of the log file. Do NOT use the qconsole.log file.
                 Start the server with the "+set logfile 1" and use that file.
  -m integer     Min number of maps played to qualify for high score <$MapsMin>.
  -o outputfile  Base name for all generated data files <$StatFile>.
  -p port        Quake3 Arena game port <$GamePort>. If you run multiple
                 servers on the same IP address, use this option to tell the
                 master server what the game port number is for each server.
  -r             Enables sending HTML data to the master server <disabled>.
                 Use this option if you don't have access to a web server.
                 The master server will store and web serve your stats.
                 See server example below for more details.
  -s integer     Frequency of updates (seconds) <$SleepTime>.

  Ex:  qplog3 -r -l /home/quake3/.q3a/baseq3/1 -o /home/quake3/qplog3/q3stats

  server example: linuxq3ded +set dedicated 2 +set logfile 1 +exec server.cfg

    Add the following line to your server.cfg so players can find the stats:
        seta cl_motd 1
        seta g_motd "Stats at www.eelinux.com"

    If you run multiple servers on the same machine, you can use +set g_log file
    to specify a different filename to log the server output.

eof
   exit(1);
}

