#!/usr/bin/perl

# no warranty for this script
# and no documentation
# take it or leave it

use strict;
use warnings;
use FindBin; use lib $FindBin::Bin;
use IO::Socket;
use Socket;
use sigtrap qw(die normal-signals);
use WeaponEncounterProfile;

my %weaponmap = (
         1 => "Laser",
         2 => "Shotgun",
         3 => "Uzi",
         4 => "Mortar",
         5 => "Electro",
         6 => "Crylink",
         7 => "Nex",
         8 => "Hagar",
         9 => "Rocket Launcher",
        10 => "Port-O-Launch",
        11 => "MinstaNex",
        12 => "Grappling Hook",
        13 => "Heavy Laser Assault Cannon",
        14 => "T.A.G. Seeker",
        15 => "Camping Rifle",
	0 => "@!#%'n Tuba"
);

my ($statsfile) = @ARGV;
my $password = $ENV{rcon_password};
my $server = $ENV{rcon_address};
my $bind = $ENV{rcon_bindaddress};

my $stats;

sub AddKill($$$$$)
{
	my ($addr, $map, $attackerweapon, $targweapon, $type) = @_;
	$stats->event($addr, $map, $attackerweapon, $targweapon, $type);
}

sub StoreData()
{
	$stats->save();
}

sub LoadData()
{
	$stats = WeaponEncounterProfile->new($statsfile);
}

$SIG{ALRM} = sub
{
	print STDERR "Operation timed out.\n";
	exit 1;
};

our @discosockets = ();
sub LogDestUDP($)
{
	# connects to a DP server using rcon with log_dest_udp
	my ($sock) = @_;
	my $value = sprintf "%s:%d", $sock->sockhost(), $sock->sockport();
	$sock->send("\377\377\377\377rcon $password log_dest_udp", 0)
		or die "send rcon: $!";
	alarm 15;
	for(;;)
	{
		$sock->recv(my $response, 2048, 0)
			or die "recv: $!";
		if($response =~ /^\377\377\377\377n"log_dest_udp" is "(.*)" \[".*"\]\n$/s)
		{
			alarm 0;
			my @dests = split /\s+/, $1;
			return
				if grep { $_ eq $value } @dests;
			push @dests, $value;
			$sock->send("\377\377\377\377rcon $password log_dest_udp \"@dests\"");
			last;
		}
	}
	alarm 0;
	push @discosockets, [$sock, $value];

	END
	{
		for(@discosockets)
		{
			my ($s, $v) = @$_;
			# disconnects (makes the server stop send the data to us)
			$s->send("\377\377\377\377rcon $password log_dest_udp", 0)
				or die "send rcon: $!";
			alarm 15;
			for(;;)
			{
				$s->recv(my $response, 2048, 0)
					or die "recv: $!";
				if($response =~ /^\377\377\377\377n"log_dest_udp" is "(.*)" \[".*"\]\n$/s)
				{
					alarm 0;
					my @dests = split /\s+/, $1;
					return
						if not grep { $_ eq $v } @dests;
					@dests = grep { $_ ne $v } @dests;
					$s->send("\377\377\377\377rcon $password log_dest_udp \"@dests\"");
					last;
				}
			}
			alarm 0;
		}
	}
}

sub sockaddr_readable($)
{
	my ($binary) = @_;
	my ($port, $addr) = sockaddr_in $binary;
	return sprintf "%s:%d", inet_ntoa($addr), $port;
}

my $sock;
if(defined $bind)
{
	# bind to a port and wait for any packets
	$sock = IO::Socket::INET->new(Proto => 'udp', LocalAddr => $bind, LocalPort => 26000)
		or die "socket: $!";
}
else
{
	# connect to a DP server
	$sock = IO::Socket::INET->new(Proto => 'udp', PeerAddr => $server, PeerPort => 26000)
		or die "socket: $!";
	LogDestUDP $sock;
}
my %currentmap = ();

my %bots = ();

LoadData();
while(my $addr = sockaddr_readable $sock->recv($_, 2048, 0))
{
	$addr = $server
		if not defined $bind;
	s/^\377\377\377\377n//
		or next;
	for(split /\r?\n/, $_)
	{
		if(/^:gamestart:([^:]+):/)
		{
			StoreData();
			$currentmap{$addr} = $1;
			$bots{$addr} = {};
			print "($addr) switching to $1\n";
			next;
		}

		next
			unless defined $currentmap{$addr};
		if(/^:join:(\d+):bot:/)
		{
			$bots{$addr}{$1} = 1;
		}
		elsif(/^:kill:frag:(\d+):(\d+):type=(\d+):items=(\d+)([A-Z]*)(?:|(\d+)):victimitems=(\d+)([A-Z]*)(?:|(\d+))$/)
		{
			my ($a, $b, $type, $killweapon, $killflags, $killrunes, $victimweapon, $victimflags, $victimrules) = ($1, $2, $3, $4, $5, $6, $7, $8, $9);
			next
				if exists $bots{$addr}{$a} or exists $bots{$addr}{$b}; # only count REAL kills
			$type &= 0xFF
				if $type < 10000;
			$killweapon = $type
				if defined $weaponmap{$type}; # if $type is not a weapon deathtype, count the weapon of the killer
			$killweapon = 0
				if not defined $weaponmap{$killweapon}; # invalid weapon? that's 0 then
			$victimweapon = 0
				if not defined $weaponmap{$victimweapon}; # dito
			$killweapon = $weaponmap{$killweapon};
			$victimweapon = $weaponmap{$victimweapon};
			next
				if $killflags =~ /S|I/ or $victimflags =~ /T/; # no strength, shield or typekills (these skew the statistics)
			AddKill($addr, $currentmap{$addr}, $killweapon, $victimweapon, +1);
		}
		elsif(/^:kill:suicide:\d+:\d+:type=(\d+):items=(\d+)([A-Z]*)(?:|(\d+))$/)
		{
			my ($type, $killweapon, $killflags, $killrunes) = ($1, $2, $3, $4, $5, $6, $7);
			$type &= 0xFF
				if $type < 10000;
			$killweapon = $type
				if defined $weaponmap{$type};
			$killweapon = 0
				if not defined $weaponmap{$killweapon};
			$killweapon = $weaponmap{$killweapon};
			next
				if $killflags =~ /S/; # no strength suicides (happen too easily accidentally)
			AddKill($addr, $currentmap{$addr}, $killweapon, $killweapon, +1);
		}
	}
}
