# HLstatsX Community Edition - Real-time player and clan rankings and statistics
# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com)
# http://www.hlxcommunity.com
#
# HLstatsX Community Edition is a continuation of 
# ELstatsNEO - Real-time player and clan rankings and statistics
# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org)
# http://ovrsized.neo-soft.org/
# 
# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX
# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2
# http://www.hlstatsx.com/
# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com)
#
# HLstatsX is an enhanced version of HLstats made by Simon Garner
# HLstats - Real-time player and clan rankings and statistics for Half-Life
# http://sourceforge.net/projects/hlstats/
# Copyright (C) 2001  Simon Garner
#             
# 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.
# 
# For support and installation notes visit http://www.hlxcommunity.com


# HLstatsX CE release version number

$g_version = "<Unable to Detect>";
my %db_stmt_cache = ();

%g_eventTables = (
	"TeamBonuses",
		["playerId", "actionId", "bonus"],
	"ChangeRole",
		["playerId", "role"],
	"ChangeName",
		["playerId", "oldName", "newName"],
	"ChangeTeam",
		["playerId", "team"],
	"Connects",
		["playerId", "ipAddress", "hostname", "hostgroup"],
	"Disconnects",
		["playerId"],
	"Entries",
		["playerId"],
	"Frags",
		["killerId", "victimId", "weapon", "headshot", "killerRole", "victimRole", "pos_x","pos_y","pos_z", "pos_victim_x","pos_victim_y","pos_victim_z"],
	"PlayerActions",
		["playerId", "actionId", "bonus", "pos_x","pos_y","pos_z"],
	"PlayerPlayerActions",
		["playerId", "victimId", "actionId", "bonus", "pos_x","pos_y","pos_z", "pos_victim_x","pos_victim_y","pos_victim_z"],
	"Suicides",
		["playerId", "weapon", "pos_x","pos_y","pos_z"],
	"Teamkills",
		["killerId", "victimId", "weapon", "pos_x","pos_y","pos_z", "pos_victim_x","pos_victim_y","pos_victim_z"],
	"Rcon",
		["type", "remoteIp", "password", "command"],
	"Admin",
		["type", "message", "playerName"],
	"Statsme",
		["playerId", "weapon", "shots", "hits", "headshots", "damage", "kills", "deaths"],
	"Statsme2",
		["playerId", "weapon", "head", "chest", "stomach", "leftarm", "rightarm", "leftleg", "rightleg"],
	"StatsmeLatency",
		["playerId", "ping"],
	"StatsmeTime",
		["playerId", "time"],
	"Latency",
		["playerId", "ping"],
	"Chat",
		["playerId", "message_mode", "message"]
);


##
## Common Functions
##

sub number_format {
  local $_  = shift;
  1 while s/^(-?\d+)(\d{3})/$1,$2/;
  return $_;
}

sub date_format {
  my $timestamp = shift;
  return sprintf('%dd %02d:%02d:%02dh', 
                  $timestamp / 86400, 
                  $timestamp / 3600 % 24, 
                  $timestamp / 60 % 60, 
                  $timestamp % 60 
                 );     
}



#
# void error (string errormsg)
#
# Dies, and optionally mails error messages to $g_mailto.
#

sub error
{
	my $errormsg = $_[0];
	
	if ($g_mailto && $g_mailpath)
	{
		system("echo \"$errormsg\" | $g_mailpath -s \"HLstatsX:CE crashed `date`\" $g_mailto");
	}

	die("$errormsg\n");
}


#
# string quoteSQL (string varQuote)
#
# Escapes all quote characters in a variable, making it suitable for use in an
# SQL query. Returns the escaped version.
#

sub quoteSQL
{
	my $varQuote = $_[0];

	$varQuote =~ s/\\/\\\\/g;	# replace \ with \\
	$varQuote =~ s/'/\\'/g;		# replace ' with \'
	
	return $varQuote;
}

#
# void doConnect
#
# Connects to the HLstatsX database
#

sub doConnect
{
	$db_conn = DBI->connect(
		"DBI:mysql:$db_name:$db_host",
		$db_user, $db_pass, { mysql_enable_utf8 => 1 }
	);
	while(!$db_conn) {
		&printEvent("MYSQL", "\nCan't connect to MySQL database '$db_name' on '$db_host'\n" .
			"Server error: $DBI::errstr\n");
		sleep(5);
		$db_conn = DBI->connect(
			"DBI:mysql:$db_name:$db_host",
			$db_user, $db_pass, { mysql_enable_utf8 => 1 }
		);
	}
	$db_conn->do("SET NAMES 'utf8'");
	&printEvent("MYSQL", "Connecting to MySQL database '$db_name' on '$db_host' as user '$db_user' ... connected ok", 1);
	%db_stmt_cache = ();
}

#
# result doQuery (string query)
#
# Executes the SQL query 'query' and returns the result identifier.
#

sub doQuery
{
	my ($query, $callref) = @_;
	if(!$db_conn->ping()) {
		&printEvent("HLSTATSX", "Lost database connection. Trying to reconnect...", 1);
		&doConnect();
	}

	my $result = $db_conn->prepare($query) or die("Unable to prepare query:\n$query\n$DBI::errstr\n$callref");
	$result->execute or die("Unable to execute query:\n$query\n$DBI::errstr\n$callref");
	
	return $result;
}

sub execNonQuery
{
	my ($query) = @_;
	if(!$db_conn->ping()) {
		&printEvent("HLSTATSX", "Lost database connection. Trying to reconnect...", 1);
		&doConnect();
	}
	#&printEvent("DEBUG","execNonQuery:\n".$query);
	$db_conn->do($query);
}

sub execCached {
	my ($query_id,$query, @bind_args) = @_;
	
	if(!$db_conn->ping()) {
		&printEvent("HLSTATSX", "Lost database connection. Trying to reconnect...", 1);
		&doConnect();
	}
	
	if(!$db_stmt_cache{$query_id}) {
		$db_stmt_cache{$query_id} = $db_conn->prepare($query) or die("Unable to prepare query ($query_id):\n$query\n$DBI::errstr");
		#&printEvent("HLSTATSX", "Prepared a statement ($query_id) for the first time.", 1);
	} 
	$db_stmt_cache{$query_id}->execute(@bind_args) or die ("Unable to execute query ($query_id):\n$query\n$DBI::errstr");
	return $db_stmt_cache{$query_id};
}

#
# string resolveIp (string ip, boolean quiet)
#
# Do a DNS reverse-lookup on an IP address and return the hostname, or empty
# string on error.
#

sub resolveIp
{
	my ($ip, $quiet) = @_;
	my ($host) = "";
	
	unless ($g_dns_resolveip)
	{
		return "";
	}
	
	
	eval
	{
		$SIG{ALRM} = sub { die "DNS Timeout\n" };
		alarm $g_dns_timeout;	# timeout after $g_dns_timeout sec
		$host = gethostbyaddr(inet_aton($ip), AF_INET);
		alarm 0;
	};
	
	if ($@)
	{
		my $error = $@;
		chomp($error);
    	printEvent("DNS", "Resolving hostname (timeout $g_dns_timeout sec) for IP \"$ip\" - $error ", 1);
		$host = "";		# some error occurred
	}
	elsif (!defined($host))
	{
    	printEvent("DNS", "Resolving hostname (timeout $g_dns_timeout sec) for IP \"$ip\" - No Host ", 1);
		$host = "";		# ip did not resolve to any host
	} else {
		$host = lc($host);	# lowercase
		printEvent("DNS", "Resolving hostname (timeout $g_dns_timeout sec) for IP \"$ip\" - $host ", 1);
	}
	chomp($host);
	return $host;
}


#
# object queryHostGroups ()
#
# Returns result identifier.
#

sub queryHostGroups
{
	return &doQuery("
		SELECT
			pattern,
			name,
			LENGTH(pattern) AS patternlength
		FROM
			hlstats_HostGroups
		ORDER BY
			patternlength DESC,
			pattern ASC
	");
}


#
# string getHostGroup (string hostname[, object result])
#
# Return host group name if any match, or last 2 or 3 parts of hostname.
#

sub getHostGroup
{
	my ($hostname, $result) = @_;
	my $hostgroup = "";
	
	# User can define special named hostgroups in hlstats_HostGroups, i.e.
	# '.adsl.someisp.net' => 'SomeISP ADSL'
	
	$result = &queryHostGroups()  unless ($result);
	$result->execute();
	
	while (my($pattern, $name) = $result->fetchrow_array())
	{
		$pattern = quotemeta($pattern);
		$pattern =~ s/\\\*/[^.]*/g;	# allow basic shell-style globbing in pattern
		if ($hostname =~ /$pattern$/)
		{
			$hostgroup = $name;
			last;
		}
	}
	$result->finish;
	
	if (!$hostgroup)
	{
		#
		# Group by last 2 or 3 parts of hostname, i.e. 'max1.xyz.someisp.net' as
		# 'someisp.net', and 'max1.xyz.someisp.net.nz' as 'someisp.net.nz'.
		# Unfortunately some countries do not have categorical SLDs, so this
		# becomes more complicated. The dom_nosld array below contains a list of
		# known country codes that do not use categorical second level domains.
		# If a country uses SLDs and is not listed below, then it will be
		# incorrectly grouped, i.e. 'max1.xyz.someisp.yz' will become
		# 'xyz.someisp.yz', instead of just 'someisp.yz'.
		#
		# Please mail sgarner@hlstats.org with any additions.
		#
		
		my @dom_nosld = (
			"ca", # Canada
			"ch", # Switzerland
			"be", # Belgium
			"de", # Germany
			"ee", # Estonia
			"es", # Spain
			"fi", # Finland
			"fr", # France
			"ie", # Ireland
			"nl", # Netherlands
			"no", # Norway
			"ru", # Russia
			"se", # Sweden
		);
		
		my $dom_nosld = join("|", @dom_nosld);
		
		if ($hostname =~ /([\w-]+\.(?:$dom_nosld|\w\w\w))$/)
		{
			$hostgroup = $1;
		}
		elsif ($hostname =~ /([\w-]+\.[\w-]+\.\w\w)$/)
		{
			$hostgroup = $1;
		}
		else
		{
			$hostgroup = $hostname;
		}
	}
	
	return $hostgroup;
}


#
# void doConf (object conf, hash directives)
#
# Walk through configuration directives, setting values of global variables.
#

sub doConf
{
	my ($conf, %directives) = @_;
	
	while (($directive, $variable) = each(%directives))
	{
        if ($directive eq "Servers") {
 	      %$variable = $conf->get($directive);
        } else {
  		  $$variable = $conf->get($directive);
	    }
	}

}

#
# void setOptionsConf (hash optionsconf)
#
# Walk through configuration directives, setting values of global variables.
#

sub setOptionsConf
{
	my (%optionsconf) = @_;
	
	while (($thekey, $theval) = each(%optionsconf))
	{
		if($theval)
		{
  			$$thekey = $theval;
		}
	}

}


#
# string abbreviate (string thestring[, int maxlength)
#
# Returns thestring abbreviated to maxlength-3 characters plus "...", unless
# thestring is shorter than maxlength.
#

sub abbreviate
{
	my ($thestring, $maxlength) = @_;
	
	$maxlength = 12  unless ($maxlength);
	
	if (length($thestring) > $maxlength)
	{
		$thestring = substr($thestring, 0, $maxlength - 3);
		return "$thestring...";
	}
	else
	{
		return $thestring;
	}
}


#
# void printEvent (int code, string description)
#
# Logs event information to stdout.
#

sub printEvent
{
	my ($code, $description, $update_timestamp, $force_output) = @_;
	if ( (($g_debug > 0) && ($g_stdin == 0))|| (($g_stdin == 1) && ($force_output == 1)) ) {
		my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time());
		my $timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
		if ($update_timestamp == 0) {
			$timestamp = $ev_timestamp; 
		}  
		if (is_number($code)) {
			printf("%s: %21s - E%03d: %s\n", $timestamp, $s_addr, $code, $description);
		} else {
			printf("%s: %21s - %s: %s\n", $timestamp, $s_addr, $code, $description);
		}  
	}
}

1;
