#!/usr/bin/perl
# 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

use strict;
no strict 'vars';

$SIG{HUP} = 'HUP_handler';
$SIG{INT} = 'INT_handler';  # unix
$SIG{INT2} = 'INT_handler';  # windows

##
## Settings
##

# $opt_configfile - Absolute path and filename of configuration file.
$opt_configfile = "./hlstats.conf";

# $opt_libdir - Directory to look in for local required files
#               (our *.plib, *.pm files).
$opt_libdir = "./";


##
##
################################################################################
## No need to edit below this line
##

use Getopt::Long;
use Time::Local;
use IO::Socket;
use IO::Select;
use DBI;
use Digest::MD5;
use Encode;
use bytes;

require "$opt_libdir/ConfigReaderSimple.pm";
require "$opt_libdir/TRcon.pm";
require "$opt_libdir/BASTARDrcon.pm";
require "$opt_libdir/HLstats_Server.pm";
require "$opt_libdir/HLstats_Player.pm";
require "$opt_libdir/HLstats_Game.pm";
do "$opt_libdir/HLstats_GameConstants.plib";
do "$opt_libdir/HLstats.plib";
do "$opt_libdir/HLstats_EventHandlers.plib";

$|=1;
Getopt::Long::Configure ("bundling");

$last_trend_timestamp = 0;

binmode STDIN, ":utf8";
binmode STDOUT, ":utf8";

##
## Functions
##

sub lookupPlayer
{
	my ($saddr, $id, $uniqueid) = @_;
	if (defined($g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"}))
	{
		return $g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"};
	}
	return undef;
}

sub removePlayer
{
	my ($saddr, $id, $uniqueid, $dontUpdateCount) = @_;
	my $deleteplayer = 0;
	if(defined($g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"}))
	{
		$deleteplayer = 1;
	}
	else
	{
		&::printEvent("400", "Bad attempted delete ($saddr) ($id/$uniqueid)");
	}

	if ($deleteplayer == 1) {
		$g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"}->playerCleanup();
		delete($g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"});
		if (!$dontUpdateCount)  # double negative, i know...
		{
			$g_servers{$saddr}->updatePlayerCount();
		}
	}
}

sub checkBonusRound
{
   if ($g_servers{$s_addr}->{bonusroundtime} > 0 && ($::ev_remotetime > ($g_servers{$s_addr}->{bonusroundtime_ts} + $g_servers{$s_addr}->{bonusroundtime}))) {
		if ($g_servers{$s_addr}->{bonusroundtime_state} == 1) {
			&printEvent("SERVER", "Bonus Round Expired",1);
		}
		$g_servers{$s_addr}->set("bonusroundtime_state",0);
	}
	
	if($g_servers{$s_addr}->{bonusroundignore} == 1 && $g_servers{$s_addr}->{bonusroundtime_state} == 1) {
		return 1;
	}
	return 0;
}

sub is_number ($) { ( $_[0] ^ $_[0] ) eq '0' }


#
# void printNotice (string notice)
#
# Prins a debugging notice to stdout.
#

sub printNotice
{
	my ($notice) = @_;
	
	if ($g_debug > 1) {
		print ">> $notice\n";
	}
}

sub track_hlstats_trend
{
	if ($last_trend_timestamp > 0) {
		if ($last_trend_timestamp+299 < $ev_daemontime) {
			my $query = "
				SELECT 
					COUNT(*),
					a.game
				FROM
					hlstats_Players a
				INNER JOIN
					(
						SELECT
							game
						FROM
							hlstats_Servers
						GROUP BY
							game
					) AS b 
				ON
					a.game = b.game
				GROUP BY
					a.game
			";
			my $result = &execCached("get_total_player_counts", $query);
			my $insvalues = "";
			while ( my($total_players, $game) = $result->fetchrow_array) {
				my $query = "
					SELECT
						SUM(kills),
						SUM(headshots),
						COUNT(serverId),
						SUM(act_players),
						SUM(max_players)
					FROM
						hlstats_Servers
					WHERE
						game=?
				";
				my $data = &execCached("get_game_stat_counts", $query, &quoteSQL($game));
				my ($total_kills, $total_headshots, $total_servers, $act_slots, $max_slots) = $data->fetchrow_array;
				if ($max_slots > 0) {
					if ($act_slots > $max_slots) {
						$act_slots = $max_slots;
					}
				}
				if ($insvalues ne "") {
					$insvalues .= ",";
				}
				$insvalues .= "
					(
						$ev_daemontime,
						'".&quoteSQL($game)."',
						$total_players,
						$total_kills,
						$total_headshots,
						$total_servers,
						$act_slots,
						$max_slots
					)
				";
			}
			if ($insvalues ne "") {
				&execNonQuery("
					INSERT INTO
						hlstats_Trend
						(
							timestamp,
							game,
							players,
							kills,
							headshots,
							servers,
							act_slots,
							max_slots
						)
						VALUES $insvalues
				");
			}
			$last_trend_timestamp = $ev_daemontime;
			&::printEvent("HLSTATSX", "Insert new server trend timestamp", 1);
		}
	} else {
		$last_trend_timestamp = $ev_daemontime;
	}  
}

sub send_global_chat
{
	my ($message) = @_;
	while( my($server) = each(%g_servers))
	{	
		if ($server ne $s_addr && $g_servers{$server}->{"srv_players"})
		{
			my @userlist;
			my %players_temp=%{$g_servers{$server}->{"srv_players"}};
			my $pcount = scalar keys %players_temp;
			
			if ($pcount > 0) {
				while ( my($pl, $b_player) = each(%players_temp) ) {
					my $b_userid  = $b_player->{userid};
					if ($g_global_chat == 2)  {
						my $b_steamid = $b_player->{uniqueid};
						if ($g_servers{$server}->is_admin($b_steamid) == 1) {
							if (($b_player->{display_events} == 1) && ($b_player->{display_chat} == 1)) {
								push(@userlist, $b_player->{userid});
							} 
						}
					} else {  
						if (($b_player->{display_events} == 1) && ($b_player->{display_chat} == 1)) {
							push(@userlist, $b_player->{userid});
						}
					}
				}
				$g_servers{$server}->messageMany($message, 0, @userlist);
			}
		}
	}
}

#
# void buildEventInsertData ()
#
# Ran at startup to init event table queues, build initial queries, and set allowed-null columns
#

my %g_eventtable_data = ();

sub buildEventInsertData
{
	my $insertType = "";
	$insertType = " DELAYED" if ($db_lowpriority);
	while ( my ($table, $colsref) = each(%g_eventTables) )
	{
		$g_eventtable_data{$table}{queue} = [];
		$g_eventtable_data{$table}{nullallowed} = 0;
		$g_eventtable_data{$table}{lastflush} = $ev_daemontime;
		$g_eventtable_data{$table}{query} = "
		INSERT$insertType INTO
			hlstats_Events_$table
			(
				eventTime,
				serverId,
				map"
				;
		my $j = 0;
		foreach $i (@{$colsref})
		{
			$g_eventtable_data{$table}{query} .= ",\n$i";
			if (substr($i, 0, 4) eq 'pos_') {
				$g_eventtable_data{$table}{nullallowed} |= (1 << $j);
			}
			$j++;
		}
		$g_eventtable_data{$table}{query} .= ") VALUES\n";
	}
}

#
# void recordEvent (string table, array cols, bool getid, [mixed eventData ...])
#
# Queues an event for addition to an Events table, flushing when hitting table queue limit.
#

sub recordEvent
{
	my $table = shift;
	my $unused = shift;
	my @coldata = @_;
	
	my $value = "(FROM_UNIXTIME($::ev_unixtime),".$g_servers{$s_addr}->{'id'}.",'".quoteSQL($g_servers{$s_addr}->get_map())."'";
	$j = 0;
	for $i (@coldata) {
		if ($g_eventtable_data{$table}{nullallowed} & (1 << $j) && (!defined($i) || $i eq "")) {
			$value .= ",NULL";
		} elsif (!defined($i)) {
			$value .= ",''";
		} else {
			$value .= ",'".quoteSQL($i)."'";
		}
		$j++;
	}
	$value .= ")";
	
	push(@{$g_eventtable_data{$table}{queue}}, $value);
	
	if (scalar(@{$g_eventtable_data{$table}{queue}}) > $g_event_queue_size)
	{
		flushEventTable($table);
	}
}

sub flushEventTable
{
	my ($table) = @_;
	
	if (scalar(@{$g_eventtable_data{$table}{queue}}) == 0)
	{
		return;
	}
	
	my $query = $g_eventtable_data{$table}{query};
	foreach (@{$g_eventtable_data{$table}{queue}})
	{
		$query .= $_.",";
	}
	$query =~ s/,$//;
	execNonQuery($query);
	$g_eventtable_data{$table}{lastflush} = $ev_daemontime;
	$g_eventtable_data{$table}{queue} = [];
}


#
# array calcSkill (int skill_mode, int killerSkill, int killerKills, int victimSkill, int victimKills, string weapon)
#
# Returns an array, where the first index contains the killer's new skill, and
# the second index contains the victim's new skill. 
#

sub calcSkill
{
  my ($skill_mode, $killerSkill, $killerKills, $victimSkill, $victimKills, $weapon, $killerTeam) = @_;
  my @newSkill;
  
  # ignored bots never do a "comeback"
  return ($g_skill_minchange, $victimSkill) if ($killerSkill < 1);
  return ($killerSkill + $g_skill_minchange, $victimSkill) if ($victimSkill < 1);
  
  if ($g_debug > 2) {
    &printNotice("Begin calcSkill: killerSkill=$killerSkill");
    &printNotice("Begin calcSkill: victimSkill=$victimSkill");
  }

  my $modifier = 1.00;
  # Look up the weapon's skill modifier
  if (defined($g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon})) {
    $modifier = $g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon}{modifier};
  }

  # Calculate the new skills
  
  my $killerSkillChange = 0;
  if ($g_skill_ratio_cap > 0) {
    # SkillRatioCap, from *XYZ*SaYnt
    #
    # dgh...we want to cap the ratio between the victimkill and killerskill.  For example, if the number 1 player
    # kills a newbie, he gets 1000/5000 * 5 * 1 = 1 points.  If gets killed by the newbie, he gets 5000/1000 * 5 *1
    # = -25 points.   Not exactly fair.  To fix this, I'm going to cap the ratio to 1/2 and 2/1.
    # these numbers are designed such that an excellent player will have to get about a 2:1 ratio against noobs to
    # hold steady in points.
    my $lowratio = 0.7;
    my $highratio = 1.0 / $lowratio;
    my $ratio = ($victimSkill / $killerSkill);
    if ($ratio < $lowratio) { $ratio = $lowratio; }
    if ($ratio > $highratio) { $ratio = $highratio; }
    $killerSkillChange = $ratio * 5 * $modifier;
  } else {
    $killerSkillChange = ($victimSkill / $killerSkill) * 5 * $modifier;
  }

  if ($killerSkillChange > $g_skill_maxchange) {
    &printNotice("Capping killer skill change of $killerSkillChange to $g_skill_maxchange") if ($g_debug > 2);
    $killerSkillChange = $g_skill_maxchange;
  }
  
  my $victimSkillChange = $killerSkillChange;

  if ($skill_mode == 1)
  {
    $victimSkillChange = $killerSkillChange * 0.75;
  }
  elsif ($skill_mode == 2)
  {
    $victimSkillChange = $killerSkillChange * 0.5;
  }
  elsif ($skill_mode == 3)
  {
    $victimSkillChange = $killerSkillChange * 0.25;
  }
  elsif ($skill_mode == 4)
  {
    $victimSkillChange = 0;
  }
  elsif ($skill_mode == 5)
  {
    #Zombie Panic: Source only
    #Method suggested by heimer. Survivor's lose half of killer's gain when dying, but Zombie's only lose a quarter. 
    if ($killerTeam eq "Undead")
    {
      $victimSkillChange = $killerSkillChange * 0.5;
    }
    elsif ($killerTeam eq "Survivor")
    {
      $victimSkillChange = $killerSkillChange * 0.25;
    }
  }
  
  if ($victimSkillChange > $g_skill_maxchange) {
    &printNotice("Capping victim skill change of $victimSkillChange to $g_skill_maxchange") if ($g_debug > 2);
    $victimSkillChange = $g_skill_maxchange;
  }
  
  if ($g_skill_maxchange >= $g_skill_minchange) {
    if ($killerSkillChange < $g_skill_minchange) {
      &printNotice("Capping killer skill change of $killerSkillChange to $g_skill_minchange") if ($g_debug > 2);
      $killerSkillChange = $g_skill_minchange;
    } 
  
    if (($victimSkillChange < $g_skill_minchange) && ($skill_mode != 4)) {
      &printNotice("Capping victim skill change of $victimSkillChange to $g_skill_minchange") if ($g_debug > 2);
      $victimSkillChange = $g_skill_minchange;
    }
  }
  if (($killerKills < $g_player_minkills ) || ($victimKills < $g_player_minkills )) {
    $killerSkillChange = $g_skill_minchange;
    if ($skill_mode != 4) {
      $victimSkillChange = $g_skill_minchange;
    } else {
      $victimSkillChange = 0;
    }  
  }
  
  $killerSkill += $killerSkillChange;
  $victimSkill -= $victimSkillChange;
  
  # we want int not float
  $killerSkill = sprintf("%d", $killerSkill + 0.5);
  $victimSkill = sprintf("%d", $victimSkill + 0.5);
  
  if ($g_debug > 2) {
    &printNotice("End calcSkill: killerSkill=$killerSkill");
    &printNotice("End calcSkill: victimSkill=$victimSkill");
  }

  return ($killerSkill, $victimSkill);
}

sub calcL4DSkill
{
	my ($killerSkill, $weapon, $difficulty) = @_;
	
	# ignored bots never do a "comeback"
	#return ($killerSkill, $victimSkill) if ($killerSkill < 1);
	#return ($killerSkill, $victimSkill)	if ($victimSkill < 1);
	
	if ($g_debug > 2) {
		&printNotice("Begin calcSkill: killerSkill=$killerSkill");
		&printNotice("Begin calcSkill: victimSkill=$victimSkill");
	}

	my $modifier = 1.00;
	# Look up the weapon's skill modifier
	if (defined($g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon})) {
		$modifier = $g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon}{modifier};
	}
	
	# Calculate the new skills
	
	$diffweight=0.5;
	if ($difficulty > 0) {
			$diffweight = $difficulty / 2;
	}	
	
	my $killerSkillChange = $pointvalue * $diffweight;

	if ($killerSkillChange > $g_skill_maxchange) {
		&printNotice("Capping killer skill change of $killerSkillChange to $g_skill_maxchange") if ($g_debug > 2);
		$killerSkillChange = $g_skill_maxchange;
	}

	if ($g_skill_maxchange >= $g_skill_minchange) {
		if ($killerSkillChange < $g_skill_minchange) {
			&printNotice("Capping killer skill change of $killerSkillChange to $g_skill_minchange") if ($g_debug > 2);
			$killerSkillChange = $g_skill_minchange;
		} 
	}
	
	$killerSkill += $killerSkillChange;
	# we want int not float
	$killerSkill = sprintf("%d", $killerSkill + 0.5);
	
	if ($g_debug > 2) {
		&printNotice("End calcSkill: killerSkill=$killerSkill");
	}
	
	return $killerSkill;
}


# Gives members of 'team' an extra 'reward' skill points. Members of the team
# who have been inactive (no events) for more than 2 minutes are not rewarded.
#

sub rewardTeam
{
	my ($team, $reward, $actionid, $actionname, $actioncode) = @_;
	$rcmd = $g_servers{$s_addr}->{broadcasting_command};
	
	my $player;
	
	&printNotice("Rewarding team \"$team\" with \"$reward\" skill for action \"$actionid\" ...");
	my @userlist;
	foreach $player (values(%g_players)) {
		my $player_team      = $player->{team};
		my $player_timestamp = $player->{timestamp};
		if (($g_servers{$s_addr}->{ignore_bots} == 1) && (($player->{is_bot} == 1) || ($player->{userid} <= 0))) {
			$desc = "(IGNORED) BOT: ";
		} else {
			if ($player_team eq $team) {
				if ($g_debug > 2) {
					&printNotice("Rewarding " . $player->getInfoString() . " with \"$reward\" skill for action \"$actionid\"");
				}
				
				&recordEvent(
					"TeamBonuses", 0,
					$player->{playerid},
					$actionid,
					$reward
					);
				$player->increment("skill", $reward, 1);
				$player->increment("session_skill", $reward, 1);
				$player->updateDB();
			}
			if ($player->{is_bot} == 0 && $player->{userid} > 0 && $player->{display_events} == 1) {
				push(@userlist, $player->{userid});
			}    
		}
	}
	if (($g_servers{$s_addr}->{broadcasting_events} == 1) && ($g_servers{$s_addr}->{broadcasting_player_actions} == 1)) {
		my $coloraction = $g_servers{$s_addr}->{format_action};
		my $verb = "got";
		if ($reward < 0) {
			$verb = "lost";
		}
		my $msg = sprintf("%s %s %s points for %s%s", $team, $verb, abs($reward), $coloraction, $actionname);
		$g_servers{$s_addr}->messageMany($msg, 0, @userlist);
	}
}


#
# int getPlayerId (uniqueId)
#
# Looks up a player's ID number, from their unique (WON) ID. Returns their PID.
#

sub getPlayerId
{
	my ($uniqueId) = @_;

	my $query = "
		SELECT
			playerId
		FROM
			hlstats_PlayerUniqueIds
		WHERE
			uniqueId='" . &::quoteSQL($uniqueId) . "' AND
			game='" . $g_servers{$s_addr}->{game} . "'
	";
	my $result = &doQuery($query);

	if ($result->rows > 0) {
		my ($playerId) = $result->fetchrow_array;
		$result->finish;
		return $playerId;
	} else {
		$result->finish;
		return 0;
	}
}


#
# int updatePlayerProfile (object player, string field, string value)
#
# Updates a player's profile information in the database.
#

sub updatePlayerProfile
{
	my ($player, $field, $value) = @_;
	$rcmd = $g_servers{$s_addr}->{player_command};
	
	unless ($player) {
		&printNotice("updatePlayerInfo: Bad player");
		return 0;
	}

	$value = &quoteSQL($value);
	if ($value eq "none" || $value eq " ") {
		$value = "";
	}
	
	my $playerName = &abbreviate($player->{name});
	my $playerId   = $player->{playerid};

	&execNonQuery("
		UPDATE
			hlstats_Players
		SET
			$field='$value'
		WHERE
			playerId=$playerId
	");
	
	if ($g_servers{$s_addr}->{player_events} == 1) {
		my $p_userid  = $g_servers{$s_addr}->format_userid($player->{userid});
		my $p_is_bot  = $player->{is_bot};
		$cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam("SET command successful for '$playerName'.");
		$g_servers{$s_addr}->dorcon($cmd_str);
	}
	return 1;
}

#
# mixed getClanId (string name)
#
# Looks up a player's clan ID from their name. Compares the player's name to tag
# patterns in hlstats_ClanTags. Patterns look like:  [AXXXXX] (matches 1 to 6
# letters inside square braces, e.g. [ZOOM]Player)  or  =\*AAXX\*= (matches
# 2 to 4 letters between an equals sign and an asterisk, e.g.  =*RAGE*=Player).
#
# Special characters in the pattern:
#    A    matches one character  (i.e. a character is required)
#    X    matches zero or one characters  (i.e. a character is optional)
#    a    matches literal A or a
#    x    matches literal X or x
#
# If no clan exists for the tag, it will be created. Returns the clan's ID, or
# 0 if the player is not in a clan.
#

sub getClanId
{
	my ($name) = @_;
	my $clanTag  = "";
	my $clanName = "";
	my $clanId   = 0;
	my $result = &doQuery("
		SELECT
			pattern,
			position,
			LENGTH(pattern) AS pattern_length
		FROM
			hlstats_ClanTags
		ORDER BY
			pattern_length DESC,
			id
	");
	
	while ( my($pattern, $position) = $result->fetchrow_array) {
		my $regpattern = quotemeta($pattern);
		$regpattern =~ s/([A-Za-z0-9]+[A-Za-z0-9_-]*)/\($1\)/; # to find clan name from tag
		$regpattern =~ s/A/./g;
		$regpattern =~ s/X/.?/g;
		
		if ($g_debug > 2) {
			&printNotice("regpattern=$regpattern");
		}
		
		if ((($position eq "START" || $position eq "EITHER") && $name =~ /^($regpattern).+/i) ||
			(($position eq "END"   || $position eq "EITHER") && $name =~ /.+($regpattern)$/i)) {
			
			if ($g_debug > 2) {
				&printNotice("pattern \"$regpattern\" matches \"$name\"! 1=\"$1\" 2=\"$2\"");
			}
			
			$clanTag  = $1;
			$clanName = $2;
			last;
		}
	}
	
	unless ($clanTag) {
		return 0;
	}

	my $query = "
		SELECT
			clanId
		FROM
			hlstats_Clans
		WHERE
			tag='" . &quoteSQL($clanTag) . "' AND
			game='$g_servers{$s_addr}->{game}'
		";
	$result = &doQuery($query);

	if ($result->rows) {
		($clanId) = $result->fetchrow_array;
		$result->finish;
		return $clanId;
	} else {
		# The clan doesn't exist yet, so we create it.
		$query = "
			REPLACE INTO
				hlstats_Clans
				(
					tag,
					name,
					game
				)
			VALUES
			(
				'" . &quoteSQL($clanTag)  . "',
				'" . &quoteSQL($clanName) . "',
				'".&quoteSQL($g_servers{$s_addr}->{game})."'
			)
		";
		&execNonQuery($query);
		
		$clanId = $db_conn->{'mysql_insertid'};

		&printNotice("Created clan \"$clanName\" <C:$clanId> with tag "
				. "\"$clanTag\" for player \"$name\"");
		return $clanId;
	}
}

#
# object getServer (string address, int port)
#
# Looks up a server's ID number in the Servers table, by searching for a
# matching IP address and port. NOTE you must specify IP addresses in the
# Servers table, NOT hostnames.
#
# Returns a new "Server object".
#

sub getServer
{
	my ($address, $port) = @_;

	my $query = "
		SELECT
			a.serverId,
			a.game,
			a.name,
			a.rcon_password,
			a.publicaddress,
			IFNULL(b.`value`,3) AS game_engine,
			IFNULL(c.`realgame`, 'hl2mp') AS realgame,
			IFNULL(a.max_players, 0) AS maxplayers
			
		FROM
			hlstats_Servers a LEFT JOIN hlstats_Servers_Config b on a.serverId = b.serverId AND b.`parameter` = 'GameEngine' LEFT JOIN `hlstats_Games` c ON a.game = c.code
		WHERE
			address=? AND
			port=? LIMIT 1
		";
	my @vals = (
		$address,
		$port
	);
	my $result = &execCached("get_server_information", $query, @vals);

	if ($result->rows) {
		my ($serverId, $game, $name, $rcon_pass, $publicaddress, $gameengine, $realgame, $maxplayers) = $result->fetchrow_array;
		$result->finish;
		if (!defined($g_games{$game})) {
			$g_games{$game} = new HLstats_Game($game);
		}
		# l4d code should be reused for l4d2
		# trying first using l4d as "realgame" code for l4d2 in db. if default server config settings won't work, will leave as own "realgame" code in db but uncomment line.
		#$realgame = "l4d" if $realgame eq "l4d2";
		
		return new HLstats_Server($serverId, $address, $port, $name, $rcon_pass, $game, $publicaddress, $gameengine, $realgame, $maxplayers);
	} else {
		$result->finish;
		return 0;
	}
}

#
# 
#
#
#

sub queryServer
{
    my ($iaddr, $iport, @query)            = @_;
    my $socket;
    $iaddr = gethostbyname($iaddr) or die("queryServer: could not resolve Host \"" . $iaddr . "\"\n");
    my $proto;

    # Open socket
    socket($socket, PF_INET, SOCK_DGRAM, $proto) or die("queryServer(141): socket: $!\n");
    my $hispaddr = sockaddr_in($iport, $iaddr);

    my $msg = "\xFF\xFF\xFF\xFFTSource Engine Query\x00";

    die("queryServer: send $iaddr:$iport : $!") unless(defined(send($socket, $msg, 0, $hispaddr)));

    my %hash = ();

    my $rin = "";
    vec($rin, fileno($socket), 1) = 1;
    my $ans = "TIMEOUT";
    if (select($rin, undef, undef, 1.0))
    {
        $ans = "";
        $hispaddr = recv($socket, $ans, 1024, 0);

        if (length($ans) eq 9 && substr($ans, 4, 1) eq "\x41") {
            # Counter-Strike 2 responds with challenge if client is non-local
            $challenge = substr($ans, 5, 4);
            my $msg = "\xFF\xFF\xFF\xFFTSource Engine Query\x00" . $challenge;
            die("queryServer: send $iaddr:$iport : $!") unless(defined(send($socket, $msg, 0, sockaddr_in($iport, $iaddr))));
            vec($rin, fileno($socket), 1) = 1;
            $ans = "TIMEOUT";
            if (select($rin, undef, undef, 1.0)) {
                $ans = "";
                $hispaddr = recv($socket, $ans, 1024, 0);
            }
        }

        @hash{qw/key type netver hostname mapname gamedir gamename id numplayers maxplayers numbots dedicated os passreq secure gamever edf port/} = unpack("LCCZ*Z*Z*Z*vCCCCCCCZ*Cv",$ans);
    }
    # Close socket
    close($socket);

    if ($ans eq "TIMEOUT")
    {
        return @hash{@query};
    }

    return @hash{@query};
}


sub getServerMod
{
	my ($address, $port) = @_;
	my ($playgame);

	&printEvent ("DETECT", "Querying $address".":$port for gametype");

	my @query = (
			'gamename',
			'gamedir',
			'hostname',
			'numplayers',
			'maxplayers',
			'mapname'
			);

	my ($gamename, $gamedir, $hostname, $numplayers, $maxplayers, $mapname) = &queryServer($address, $port, @query);

	if ($gamename =~ /^Counter-Strike$/i) {
		$playgame = "cstrike";
	} elsif ($gamename =~ /^Counter-Strike/i) {
		$playgame = "css";
	} elsif ($gamename =~ /^Team Fortress C/i) {
		$playgame = "tfc";
	} elsif ($gamename =~ /^Team Fortress/i) {
		$playgame = "tf";
	} elsif ($gamename =~ /^Day of Defeat$/i) {
		$playgame = "dod";
	} elsif ($gamename =~ /^Day of Defeat/i) {
		$playgame = "dods";
	} elsif ($gamename =~ /^Insurgency/i) {
		$playgame = "insmod";
	} elsif ($gamename =~ /^Neotokyo/i) {
		$playgame = "nts";
	} elsif ($gamename =~ /^Fortress Forever/i) {
		$playgame = "ff";
	} elsif ($gamename =~ /^Age of Chivalry/i) {
		$playgame = "aoc";
	} elsif ($gamename =~ /^Dystopia/i) {
		$playgame = "dystopia";
	} elsif ($gamename =~ /^Stargate/i) {
		$playgame = "sgtls";
	} elsif ($gamename =~ /^Battle Grounds/i) {
		$playgame = "bg2";
	} elsif ($gamename =~ /^Hidden/i) {
		$playgame = "hidden";
	} elsif ($gamename =~ /^L4D /i) {
		$playgame = "l4d";
	} elsif ($gamename =~ /^Left 4 Dead 2/i) {
		$playgame = "l4d2";
	} elsif ($gamename =~ /^ZPS /i) {
		$playgame = "zps";
	} elsif ($gamename =~ /^NS /i) {
		$playgame = "ns";
	} elsif ($gamename =~ /^pvkii/i) {
		$playgame = "pvkii";
	} elsif ($gamename =~ /^CSPromod/i) {
		$playgame = "csp";
	} elsif ($gamename eq "Half-Life") {
		$playgame = "valve";
	} elsif ($gamename eq "Nuclear Dawn") {
		$playgame = "nucleardawn";
    
	# We didn't found our mod, trying secondary way. This is required for some games such as FOF and GES and is a fallback for others
	} elsif ($gamedir =~ /^ges/i) {
		$playgame = "ges";
	} elsif ($gamedir =~ /^fistful_of_frags/i || $gamedir =~ /^fof/i) {
		$playgame = "fof";
	} elsif ($gamedir =~ /^hl2mp/i) {
		$playgame = "hl2mp";
	} elsif ($gamedir =~ /^tfc/i) {
		$playgame = "tfc";
	} elsif ($gamedir =~ /^tf/i) {
		$playgame = "tf";
	} elsif ($gamedir =~ /^ins/i) {
		$playgame = "insmod";
	} elsif ($gamedir =~ /^neotokyo/i) {
		$playgame = "nts";
	} elsif ($gamedir =~ /^fortressforever/i) {
		$playgame = "ff";
	} elsif ($gamedir =~ /^ageofchivalry/i) {
		$playgame = "aoc";
	} elsif ($gamedir =~ /^dystopia/i) {
		$playgame = "dystopia";
	} elsif ($gamedir =~ /^sgtls/i) {
		$playgame = "sgtls";
	} elsif ($gamedir =~ /^hidden/i) {
		$playgame = "hidden";
	} elsif ($gamedir =~ /^left4dead/i) {
		$playgame = "l4d";
	} elsif ($gamedir =~ /^left4dead2/i) {
		$playgame = "l4d2";
	} elsif ($gamedir =~ /^zps/i) {
		$playgame = "zps";
	} elsif ($gamedir =~ /^ns/i) {
		$playgame = "ns";
	} elsif ($gamedir =~ /^bg/i) {
		$playgame = "bg2";
	} elsif ($gamedir =~ /^pvkii/i) {
		$playgame = "pvkii";
	} elsif ($gamedir =~ /^cspromod/i) {
		$playgame = "csp";
	} elsif ($gamedir =~ /^valve$/i) {
		$playgame = "valve";
    } elsif ($gamedir =~ /^nucleardawn$/i) {
		$playgame = "nucleardawn";
	} elsif ($gamedir =~ /^dinodday$/i) {
		$playgame = "dinodday";
	} else {
		# We didn't found our mod, giving up.
		&printEvent("DETECT", "Failed to get Server Mod");
		return 0;
	}
	&printEvent("DETECT", "Saving server " . $address . ":" . $port . " with gametype " . $playgame);
	&addServerToDB($address, $port, $hostname, $playgame, $numplayers, $maxplayers, $mapname);
	return $playgame;
}

sub addServerToDB
{
	my ($address, $port, $name, $game, $act_players, $max_players, $act_map) = @_;
	my $sql = "INSERT INTO hlstats_Servers (address, port, name, game, act_players, max_players, act_map) VALUES ('$address', $port, '".&quoteSQL($name)."', '".&quoteSQL($game)."', $act_players, $max_players, '".&quoteSQL($act_map)."')";
	&execNonQuery($sql);
   
	my $last_id = $db_conn->{'mysql_insertid'};
	&execNonQuery("DELETE FROM `hlstats_Servers_Config` WHERE serverId = $last_id");
	&execNonQuery("INSERT INTO `hlstats_Servers_Config` (`serverId`, `parameter`, `value`)
				SELECT $last_id, `parameter`, `value`
				FROM `hlstats_Mods_Defaults` WHERE `code` = '';");
	&execNonQuery("INSERT INTO `hlstats_Servers_Config` (`serverId`, `parameter`, `value`) VALUES
				($last_id, 'Mod', '');");
	&execNonQuery("INSERT INTO `hlstats_Servers_Config` (`serverId`, `parameter`, `value`)
				SELECT $last_id, `parameter`, `value`
				FROM `hlstats_Games_Defaults` WHERE `code` = '".&quoteSQL($game)."'
				ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");   
	&readDatabaseConfig();

	return 1;
}

#
# boolean sameTeam (string team1, string team2)
#
# This should be expanded later to allow for team alliances (e.g. TFC-hunted).
#

sub sameTeam
{
	my ($team1, $team2) = @_;
	
	if (($team1 eq $team2) && (($team1 ne "Unassigned") || ($team2 ne "Unassigned"))) {
		return 1;
	} else {
		return 0;
	}
}


#
# string getPlayerInfoString (object player, string ident)
#

sub getPlayerInfoString
{
	my ($player) = shift;
	my @ident = @_;
	
	if ($player) {
		return $player->getInfoString();
	} else {
		return "(" . join(",", @ident) . ")";
	}
}



#
# array getPlayerInfo (string player, string $ipAddr)
#
# Get a player's name, uid, wonid and team from "Name<uid><wonid><team>".
#

sub getPlayerInfo
{
	my ($player, $create_player, $ipAddr) = @_;

	if ($player =~ /^(.*?)<(\d+)><([^<>]*)><([^<>]*)>(?:<([^<>]*)>)?.*$/) {
		my $name		= $1;
		my $userid		= $2;
		my $uniqueid	= $3;
		my $team		= $4;
		my $role		= $5;
		my $bot			= 0;
		my $haveplayer  = 0;
		
		$plainuniqueid = $uniqueid;
		$uniqueid =~ s!\[U:1:(\d+)\]!'STEAM_0:'.($1 % 2).':'.int($1 / 2)!eg;
		$uniqueid =~ s/^STEAM_[0-9]+?\://;
		
		if (($uniqueid eq "Console") && ($team eq "Console")) {
		  return 0;
		}
		if ($g_servers{$s_addr}->{play_game} == L4D()) {
		#for l4d, create meta player object for each role
			if ($uniqueid eq "") {
				#infected & witch have blank steamid
				if ($name eq "infected") {
					$uniqueid = "BOT-Horde";
					$team = "Infected";
					$userid = -9;
				} elsif ($name eq "witch") {
					$uniqueid = "BOT-Witch";
					$team = "Infected";
					$userid = -10;
				} else {
					return 0;
				}
			} elsif ($uniqueid eq "BOT") {
				#all other bots have BOT for steamid
				if ($team eq "Survivor") {
					if ($name eq "Nick") {
						$userid = -11;
					} elsif ($name eq "Ellis") {
						$userid = -13;
					} elsif ($name eq "Rochelle") {
						$userid = -14;
					} elsif ($name eq "Coach") {
						$userid = -12;
					} elsif ($name eq "Louis") {
						$userid = -4;
					} elsif ($name eq "Zoey") {
						$userid = -1;
					} elsif ($name eq "Francis") {
						$userid = -2;
					} elsif ($name eq "Bill") {
						$userid = -3;
					} else {
						&printEvent("ERROR", "No survivor match for $name",0,1);
						$userid = -4;
					}
				} else {
					if ($name eq "Smoker") {
						$userid = -5;
					} elsif ($name eq "Boomer") {
						$userid = -6;
					} elsif ($name eq "Hunter") {
						$userid = -7;
					} elsif ($name eq "Spitter") {
						$userid = -15;
					} elsif ($name eq "Jockey") {
						$userid = -16;
					} elsif ($name eq "Charger") {
						$userid = -17;
					} elsif ($name eq "Tank") {
						$userid = -8;
					} else {
						&printEvent("DEBUG", "No infected match for $name",0,1);
						$userid = -8;
					}
				}
				$uniqueid = "BOT-".$name;
				$name = "BOT-".$name;
			}
		}

		if ($ipAddr eq "none") {
			$ipAddr = "";
		}
		
		$bot = botidcheck($uniqueid);
		
        if ($bot) {
            if ($g_servers{$s_addr}->{play_game} == CSGO() || $g_servers{$s_addr}->{play_game} == CS2()) {
                # leo - Give CSGO's GOTV BOT a team called 'TV', on doEvent_Connect and doEvent_EnterGame:
                # 		e.g. L 05/14/2017 - 09:09:24: "GOTV<2><BOT><>" connected, address ""
                #		e.g. L 06/09/2017 - 03:36:49: "GOTV<2><BOT><>" entered the game
                #		Note that GOTV's $player->{userid} > 0 by default.  This means in the HLStats_Player constructor's it will call $self->insertPlayerLivestats() and GOTV will be added to the LiveStats table.
                # For cs2 - Give the bot userid < 0, so it registers as a HLstats_Player
                #       E.g. L 11/15/2023 - 03:22:54.003 - "SourceTV<0><BOT><>" connected, address "none"
				# 		E.g. L 12/06/2023 - 07:34:16.564 - "Romanov<0><BOT><>" entered the game
				# 		E.g. L 12/06/2023 - 11:03:48.261 - "DemoRecorder<0><BOT><>" connected, address "none"
				# 		E.g. L 12/06/2023 - 11:03:48.261 - "DemoRecorder<0><UNKNOWN><>" entered the game
                if ($uniqueid eq "BOT" && index($name, "GOTV") != -1) {
                    $team = 'TV';
                }elsif ($uniqueid eq "BOT" && index($name, "SourceTV") != -1) {
                    $userid = -1;
                    $team = 'TV';
                }elsif ($uniqueid eq "BOT" && $userid == 0) {
                    $userid = -2;
                }elsif ($uniqueid eq "UNKNOWN" && $userid == 0) {
                    $uniqueid = 'BOT';
                    $userid = -3;
                }
            }
        }
		
		if ($g_mode eq "NameTrack") {
			$uniqueid = $name;
		} else {
			if ($g_mode eq "LAN" && !$bot && $userid > 0) {
				if ($ipAddr ne "") {
					$g_lan_noplayerinfo->{"$s_addr/$userid/$name"} = {
						ipaddress => $ipAddr,
						userid => $userid,
						name => $name,
						server => $s_addr
						};
					$uniqueid = $ipAddr;
				} else {
					while ( my($index, $player) = each(%g_players) ) {
						if (($player->{userid} eq $userid) &&
							($player->{name}   eq $name)) {
						
							$uniqueid = $player->{uniqueid}; 
							$haveplayer = 1;
							last;
						}   
					}
					if (!$haveplayer) {
						while ( my($index, $player) = each(%g_lan_noplayerinfo) ) {
							if (($player->{server} eq $s_addr) &&
								($player->{userid} eq $userid) &&
								($player->{name}   eq $name)) {
						
								$uniqueid = $player->{ipaddress}; 
								$haveplayer = 1;
							}    
						}  
					}
					if (!$haveplayer) {
						$uniqueid = "UNKNOWN";
					}
				}
			} else {
				# Normal (steamid) mode player and bot, as well as lan mode bots
				if ($bot) {
					$md5 = Digest::MD5->new;
					$md5->add($name);
					$md5->add($s_addr);
					$uniqueid = "BOT:" . $md5->hexdigest;
					$unique_id = $uniqueid if ($g_mode eq "LAN");
				}
			
				if ($uniqueid eq "UNKNOWN"
					|| $uniqueid eq "STEAM_ID_PENDING" || $uniqueid eq "STEAM_ID_LAN"
					|| $uniqueid eq "VALVE_ID_PENDING" || $uniqueid eq "VALVE_ID_LAN"
				) {
					return {
						name     => $name,
						userid   => $userid,
						uniqueid => $uniqueid,
						team     => $team,
						is_bot   => $bot
					};
				}
			}
		}
		
		if (!$haveplayer)
		{
			while ( my ($index, $player) = each(%g_players) ) {
				# Cannot exit loop early as more than one player can exist with same uniqueid
				# (bug? or just bad logging)
				# Either way, we disconnect any that don't match the current line
				if ($player->{uniqueid} eq $uniqueid) {
					$haveplayer = 1;
					# Catch players reconnecting without first disconnecting
					if ($player->{userid} != $userid) {
					
						&doEvent_Disconnect(
							$player->{"userid"},
							$uniqueid,
							""
						);
						$haveplayer = 0;
					}
				}
			}
		}
		
		if ($haveplayer) {
			my $player = lookupPlayer($s_addr, $userid, $uniqueid);
			if ($player) {
				#  The only time team should go /back/ to unassigned ("") is on mapchange
				#  (which is already handled in the ChangeMap handler)
				#  So ignore when team is blank (<>) from lazy log lines
				if ($team ne "" && $player->{team} ne $team) {
					&doEvent_TeamSelection(
						$userid,
						$uniqueid,
						$team
					);
				}
				if ($role ne "" && $role ne $player->{role}) {
					&doEvent_RoleSelection(
						$player->{"userid"},
						$player->{"uniqueid"},
						$role
					);
				}
				
				$player->updateTimestamp();
			}  
		} else {
			# In CS2, the first user is always assigned userid of '0'. Always create a player regardless of its value
			#       E.g. L 02/01/2024 - 21:02:01.253 - "X<0><[U:Y]><>" entered the game
			# For all other games, userid is always above '0'
			if ($userid != 0 || $g_servers{$s_addr}->{play_game} == CS2()) {
				if ($create_player > 0) {
					my $preIpAddr = "";
					if ($g_preconnect->{"$s_addr/$userid/$name"}) {
						$preIpAddr = $g_preconnect->{"$s_addr/$userid/$name"}->{"ipaddress"};
					}
					# Add the player to our hash of player objects
					$g_servers{$s_addr}->{"srv_players"}->{"$userid/$uniqueid"} = new HLstats_Player(
						server => $s_addr,
						server_id => $g_servers{$s_addr}->{id},
						userid => $userid,
						uniqueid => $uniqueid,
						plain_uniqueid => $plainuniqueid,
						game => $g_servers{$s_addr}->{game},
						name => $name,
						team => $team,
						role => $role,
						is_bot => $bot,
						display_events => $g_servers{$s_addr}->{default_display_events},
						address => (($preIpAddr ne "") ? $preIpAddr : $ipAddr)
					);
					
					if ($preIpAddr ne "") {
						&printEvent("SERVER", "LATE CONNECT [$name/$userid] - steam userid validated");
						&doEvent_Connect($userid, $uniqueid, $preIpAddr);
						delete($g_preconnect->{"$s_addr/$userid/$name"});
					}
					# Increment number of players on server
					$g_servers{$s_addr}->updatePlayerCount();
				}  
			} elsif (($g_mode eq "LAN") && (defined($g_lan_noplayerinfo{"$s_addr/$userid/$name"}))) {
				if ((!$haveplayer) && ($uniqueid ne "UNKNOWN") && ($create_player > 0)) {
					$g_servers{$s_addr}->{srv_players}->{"$userid/$uniqueid"} = new HLstats_Player(
						server => $s_addr,
						server_id => $g_servers{$s_addr}->{id},
						userid => $userid,
						uniqueid => $uniqueid,
						plain_uniqueid => $plainuniqueid,
						game => $g_servers{$s_addr}->{game},
						name => $name,
						team => $team,
						role => $role,
						is_bot => $bot
					);
					delete($g_lan_noplayerinfo{"$s_addr/$userid/$name"});
					# Increment number of players on server
					
					$g_servers{$s_addr}->updatePlayerCount();
				} 
			} else {
				&printNotice("No player object available for player \"$name\" <U:$userid>");
			}
		}
		
		return {
			name     => $name,
			userid   => $userid,
			uniqueid => $uniqueid,
			team     => $team,
			is_bot   => $bot
		};
	} elsif ($player =~ /^(.+)<([^<>]+)>$/) {
		my $name     = $1;
		my $uniqueid = $2;
		my $bot      = 0;
		
		if (&botidcheck($uniqueid)) {
			$md5 = Digest::MD5->new;
			$md5->add($ev_daemontime);
			$md5->add($s_addr);
			$uniqueid = "BOT:" . $md5->hexdigest;
			$bot = 1;
		}
		return {
			name     => $name,
			uniqueid => $uniqueid,
			is_bot   => $bot
		};
	} elsif ($player =~ /^<><([^<>]+)><>$/) {
		my $uniqueid = $1;
		my $bot      = 0;
		if (&botidcheck($uniqueid)) {
			$md5 = Digest::MD5->new;
			$md5->add($ev_daemontime);
			$md5->add($s_addr);
			$uniqueid = "BOT:" . $md5->hexdigest;
			$bot = 1;
		}
		return {
			uniqueid => $uniqueid,
			is_bot   => $bot
		};
	} else {
		return 0;
	}
}


#
# hash getProperties (string propstring)
#
# Parse (key "value") properties into a hash.
#

sub getProperties
{
	my ($propstring) = @_;
	my %properties;
	my $dods_flag = 0;
	
	while ($propstring =~ s/^\s*\((\S+)(?:(?: "(.+?)")|(?: ([^\)]+)))?\)//) {
		my $key = $1;
		if (defined($2)) {
			if ($key eq "player") {
				if ($dods_flag == 1) {
					$key = "player_a";
					$dods_flag++;
				} elsif ($dods_flag == 2) {
					$key = "player_b";
				}
			}
			$properties{$key} = $2;
		} elsif (defined($3)) {
			$properties{$key} = $3;
		} else {
			$properties{$key} = 1; # boolean property
		}
		if ($key eq "flagindex") {
			$dods_flag++;
		}
	}
	
	return %properties;
}


# 
# boolean like (string subject, string compare)
#
# Returns true if 'subject' equals 'compare' with optional whitespace.
#

sub like
{
	my ($subject, $compare) = @_;
	
	if ($subject =~ /^\s*\Q$compare\E\s*$/) {
		return 1;
	} else {
		return 0;
	}
}


# 
# boolean botidcheck (string uniqueid)
#
# Returns true if 'uniqueid' is that of a bot.
#

sub botidcheck
{
	# needs cleaned up
	# added /^00000000\:\d+\:0$/ check for "whichbot"
	my ($uniqueid) = @_;
	if ($uniqueid eq "BOT" || $uniqueid eq "0" || $uniqueid =~ /^00000000\:\d+\:0$/) {
		return 1
	}
	return 0;
}

sub isTrackableTeam
{
	my ($team) = @_;
	#if ($team =~ /spectator/i || $team =~ /unassigned/i || $team eq "") {
	if ($team =~ /spectator/i || $team eq "") {
		return 0;
	}
	return 1;
}

sub reloadConfiguration
{
	&flushAll;
	&readDatabaseConfig;
}


sub flushAll
{
	# we only need to flush events if we're about to shut down. they are unaffected by server/player deletion
	my ($flushevents) = @_;
	if ($flushevents)
	{
		while ( my ($table, $colsref) = each(%g_eventTables) )
		{
			flushEventTable($table);
		}
	}
	
	while( my($se, $server) = each(%g_servers))
	{	
		while ( my($pl, $player) = each(%{$server->{"srv_players"}}) )
		{
			if ($player)
			{
				$player->playerCleanup();
			}
		}
		$server->flushDB();
	}
}


##
## MAIN
##

# Options

$opt_help = 0;
$opt_version = 0;

$db_host = "localhost";
$db_user = "";
$db_pass = "";
$db_name = "hlstats";
$db_lowpriority = 1;

$s_ip = "";
$s_port = "27500";

$g_mailto = "";
$g_mailpath = "/bin/mail";
$g_mode = "Normal";
$g_deletedays = 5;
$g_requiremap = 0;
$g_debug = 1;
$g_nodebug = 0;
$g_rcon = 1;
$g_rcon_ignoreself = 0;
$g_rcon_record = 1;
$g_stdin = 0;
$g_server_ip = "";
$g_server_port = 27015;
$g_timestamp = 0;
$g_cpanelhack = 0;
$g_event_queue_size = 10;
$g_dns_resolveip = 1;
$g_dns_timeout = 5;
$g_skill_maxchange = 100;
$g_skill_minchange = 2;
$g_skill_ratio_cap = 0;
$g_geoip_binary = 0;
$g_player_minkills = 50;
$g_onlyconfig_servers = 1;
$g_track_stats_trend = 0;
%g_lan_noplayerinfo = ();
%g_preconnect = ();
$g_global_banning = 0;
$g_log_chat = 0;
$g_log_chat_admins = 0;
$g_global_chat = 0;
$g_ranktype = "skill";
$g_gi = undef;

my %dysweaponcodes = (
	"1" => "Light Katana",
	"2" => "Medium Katana",
	"3" => "Fatman Fist",
	"4" => "Machine Pistol",
	"5" => "Shotgun",
	"6" => "Laser Rifle",
	"7" => "BoltGun",
	"8" => "SmartLock Pistols",
	"9" => "Assault Rifle",
	"10" => "Grenade Launcher",
	"11" => "MK-808 Rifle",
	"12" => "Tesla Rifle",
	"13" => "Rocket Launcher",
	"14" => "Minigun",
	"15" => "Ion Cannon",
	"16" => "Basilisk",
	"17" => "Frag Grenade",
	"18" => "EMP Grenade",
	"19" => "Spider Grenade",
	"22" => "Cortex Bomb"
);

# Usage message

$usage = <<EOT
Usage: hlstats.pl [OPTION]...
Collect statistics from one or more Half-Life2 servers for insertion into
a MySQL database.

  -h, --help                      display this help and exit  
  -v, --version                   output version information and exit
  -d, --debug                     enable debugging output (-dd for more)
  -n, --nodebug                   disables above; reduces debug level
  -m, --mode=MODE                 player tracking mode (Normal, LAN or NameTrack)  [$g_mode]
      --db-host=HOST              database ip or ip:port  [$db_host]
      --db-name=DATABASE          database name  [$db_name]
      --db-password=PASSWORD      database password (WARNING: specifying the
                                    password on the command line is insecure.
                                    Use the configuration file instead.)
      --db-username=USERNAME      database username
      --dns-resolveip             resolve player IP addresses to hostnames
                                    (requires working DNS)
   -c,--configfile                Specific configfile to use, settings in this file can now
                                    be overidden with commandline settings.
      --nodns-resolveip           disables above
      --dns-timeout=SEC           timeout DNS queries after SEC seconds  [$g_dns_timeout]
  -i, --ip=IP                     set IP address to listen on for UDP log data
  -p, --port=PORT                 set port to listen on for UDP log data  [$s_port]
  -r, --rcon                      enables rcon command exec support (the default)
      --norcon                    disables rcon command exec support
  -s, --stdin                     read log data from standard input, instead of
                                    from UDP socket. Must specify --server-ip
                                    and --server-port to indicate the generator
                                    of the inputted log data (implies --norcon)
      --nostdin                   disables above
      --server-ip                 specify data source IP address for --stdin
      --server-port               specify data source port for --stdin  [$g_server_port]
  -t, --timestamp                 tells HLstatsX:CE to use the timestamp in the log
                                    data, instead of the current time on the
                                    database server, when recording events
      --notimestamp               disables above
      --event-queue-size=SIZE     manually set event queue size to control flushing
                                    (recommend 100+ for STDIN)

Long options can be abbreviated, where such abbreviation is not ambiguous.
Default values for options are indicated in square brackets [...].

Most options can be specified in the configuration file:
  $opt_configfile
Note: Options set on the command line take precedence over options set in the
configuration file. The configuration file name is set at the top of hlstats.pl.

HLstatsX: Community Edition http://www.hlxcommunity.com
EOT
;

%g_config_servers = ();


sub readDatabaseConfig()
{
	&printEvent("CONFIG", "Reading database config...", 1);
	%g_config_servers = ();
	%g_servers = ();
	%g_games = ();

	# elstatsneo: read the servers portion from the mysql database
	my $srv_id = &doQuery("SELECT serverId,CONCAT(address,':',port) AS addr FROM hlstats_Servers");
	while ( my($serverId,$addr) = $srv_id->fetchrow_array) {
		$g_config_servers{$addr} = ();
		my $serverConfig = &doQuery("SELECT parameter,value FROM hlstats_Servers_Config WHERE serverId=$serverId");
		while ( my($p,$v) = $serverConfig->fetchrow_array) {
			$g_config_servers{$addr}{$p} = $v;
		}
	}
	$srv_id->finish;
	# hlxce: read the global settings from the database!
	my $gsettings = &doQuery("SELECT keyname,value FROM hlstats_Options WHERE opttype <= 1");
	while ( my($p,$v) = $gsettings->fetchrow_array) {
		if ($g_debug > 1) {
			print "Config parameter '$p' = '$v'\n";
		}
		$tmp = "\$".$directives_mysql{$p}." = '$v'";
		#print " -> setting ".$tmp."\n";
		eval $tmp;
	}
	$gsettings->finish;
	# setting defaults

	&printEvent("DAEMON", "Proxy_Key DISABLED", 1) if ($proxy_key eq "");
	while (my($addr, $server) = each(%g_config_servers)) {
		
		if (!defined($g_config_servers{$addr}{"MinPlayers"})) {
			$g_config_servers{$addr}{"MinPlayers"}						= 6;
		}  
		if (!defined($g_config_servers{$addr}{"DisplayResultsInBrowser"})) {
			$g_config_servers{$addr}{"DisplayResultsInBrowser"}			= 0;
		}  
		if (!defined($g_config_servers{$addr}{"BroadCastEvents"})) {
			$g_config_servers{$addr}{"BroadCastEvents"}					= 0;
		}
		if (!defined($g_config_servers{$addr}{"BroadCastPlayerActions"})) {
			$g_config_servers{$addr}{"BroadCastPlayerActions"}			= 0;
		}
		if (!defined($g_config_servers{$addr}{"BroadCastEventsCommand"})) {
			$g_config_servers{$addr}{"BroadCastEventsCommand"}			= "say";
		}
		if (!defined($g_config_servers{$addr}{"BroadCastEventsCommandAnnounce"})) {
			$g_config_servers{$addr}{"BroadCastEventsCommandAnnounce"}	= "say";
		}
		if (!defined($g_config_servers{$addr}{"PlayerEvents"})) {
			$g_config_servers{$addr}{"PlayerEvents"}					= 1;
		}
		if (!defined($g_config_servers{$addr}{"PlayerEventsCommand"})) {
			$g_config_servers{$addr}{"PlayerEventsCommand"}				= "say";
		}
		if (!defined($g_config_servers{$addr}{"PlayerEventsCommandOSD"})) {
			$g_config_servers{$addr}{"PlayerEventsCommandOSD"}			= "";
		}
		if (!defined($g_config_servers{$addr}{"PlayerEventsCommandHint"})) {
			$g_config_servers{$addr}{"PlayerEventsCommandHint"}			= "";
		}
		if (!defined($g_config_servers{$addr}{"PlayerEventsAdminCommand"})) {
			$g_config_servers{$addr}{"PlayerEventsAdminCommand"}		= "";
		}
		if (!defined($g_config_servers{$addr}{"ShowStats"})) {
			$g_config_servers{$addr}{"ShowStats"}						= 1;
		}
		if (!defined($g_config_servers{$addr}{"AutoTeamBalance"})) {
			$g_config_servers{$addr}{"AutoTeamBalance"}					= 0;
		}
		if (!defined($g_config_servers{$addr}{"AutoBanRetry"})) {
			$g_config_servers{$addr}{"AutoBanRetry"}					= 0;
		}
		if (!defined($g_config_servers{$addr}{"TrackServerLoad"})) {
			$g_config_servers{$addr}{"TrackServerLoad"}					= 0;
		}
		if (!defined($g_config_servers{$addr}{"MinimumPlayersRank"})) {
			$g_config_servers{$addr}{"MinimumPlayersRank"}				= 0;
		}
		if (!defined($g_config_servers{$addr}{"Admins"})) {
			$g_config_servers{$addr}{"Admins"}							= "";
		}
		if (!defined($g_config_servers{$addr}{"SwitchAdmins"})) {
			$g_config_servers{$addr}{"SwitchAdmins"}					= 0;
		}
		if (!defined($g_config_servers{$addr}{"IgnoreBots"})) {
			$g_config_servers{$addr}{"IgnoreBots"}						= 1;
		}
		if (!defined($g_config_servers{$addr}{"SkillMode"})) {
			$g_config_servers{$addr}{"SkillMode"}						= 0;
		}
		if (!defined($g_config_servers{$addr}{"GameType"})) {
			$g_config_servers{$addr}{"GameType"}						= 0;
		}
		if (!defined($g_config_servers{$addr}{"BonusRoundTime"})) {
			$g_config_servers{$addr}{"BonusRoundTime"}					= 0;
		}
		if (!defined($g_config_servers{$addr}{"BonusRoundIgnore"})) {
			$g_config_servers{$addr}{"BonusRoundIgnore"}				= 0;
		}
		if (!defined($g_config_servers{$addr}{"Mod"})) {
			$g_config_servers{$addr}{"Mod"}								= "";
		}
		if (!defined($g_config_servers{$addr}{"EnablePublicCommands"})) {
			$g_config_servers{$addr}{"EnablePublicCommands"}			= 1;
		}
		if (!defined($g_config_servers{$addr}{"ConnectAnnounce"})) {
			$g_config_servers{$addr}{"ConnectAnnounce"}					= 1;
		}
		if (!defined($g_config_servers{$addr}{"UpdateHostname"})) {
			$g_config_servers{$addr}{"UpdateHostname"}					= 0;
		}
		if (!defined($g_config_servers{$addr}{"DefaultDisplayEvents"})) {
			$g_config_servers{$addr}{"DefaultDisplayEvents"}			= 1;
		}
	}

	&printEvent("CONFIG", "I have found the following server configs in database:", 1);
	while (my($addr, $server) = each(%g_config_servers)) {
		&printEvent("S_CONFIG", $addr, 1);
	}
	
	if ($g_geoip_binary > 0 && !defined($g_gi)) {
		my $geoipfile = "$opt_libdir/GeoLiteCity/GeoLite2-City.mmdb";
		if (-r $geoipfile) {
			eval "use GeoIP2::Database::Reader"; my $hasGeoIP = $@ ? 0 : 1;
			if ($hasGeoIP) {
				$g_gi = GeoIP2::Database::Reader->new(
						file    => $geoipfile,
						locales => [ 'en' ]
				);
			} else {
				&printEvent("ERROR", "GeoIP method set to binary file lookup but GeoIP2::Database::Reader module NOT FOUND", 1);
				$g_gi = undef;
			}
		} else {
			&printEvent("ERROR", "GeoIP method set to binary file lookup but $geoipfile NOT FOUND", 1);
			$g_gi = undef;
		}
	} elsif ($g_geoip_binary == 0 && defined($g_gi)) {
		$g_gi->close();
		$g_gi = undef;
	}
} 

# Read Config File

if ($opt_configfile && -r $opt_configfile) {
	$conf = ConfigReaderSimple->new($opt_configfile);
	$conf->parse();
	%directives = (
		"DBHost",					"db_host",
		"DBUsername",			"db_user",
		"DBPassword",			"db_pass",
		"DBName",					"db_name",
		"DBLowPriority",		"db_lowpriority",
		"BindIP",					"s_ip",
		"Port",						"s_port",
		"DebugLevel",			"g_debug",
		"CpanelHack",			"g_cpanelhack",
		"EventQueueSize",		"g_event_queue_size"
	);

	%directives_mysql = (
		"version",					"g_version",
		"MailTo",					"g_mailto",
		"MailPath",					"g_mailpath",
		"Mode",						"g_mode",
		"DeleteDays",				"g_deletedays",
		"UseTimestamp",				"g_timestamp",
		"DNSResolveIP",				"g_dns_resolveip",
		"DNSTimeout",				"g_dns_timeout",
		"RconIgnoreSelf",			"g_rcon_ignoreself",
		"Rcon",						"g_rcon",
		"RconRecord",				"g_rcon_record",
		"MinPlayers",				"g_minplayers",
		"SkillMaxChange",			"g_skill_maxchange",
		"SkillMinChange",			"g_skill_minchange",
		"PlayerMinKills",			"g_player_minkills",
		"AllowOnlyConfigServers",	"g_onlyconfig_servers",
		"TrackStatsTrend",			"g_track_stats_trend",
		"GlobalBanning",			"g_global_banning",
		"LogChat",					"g_log_chat",
		"LogChatAdmins",			"g_log_chat_admins",
		"GlobalChat",				"g_global_chat",
		"SkillRatioCap",			"g_skill_ratio_cap",
		"rankingtype",				"g_ranktype",
		"UseGeoIPBinary",			"g_geoip_binary",
		"Proxy_Key",				"proxy_key"
	);

#		"Servers",                "g_config_servers"
	&doConf($conf, %directives);

} else {
	print "-- Warning: unable to open configuration file '$opt_configfile'\n";
}

# Read Command Line Arguments

%copts = ();

GetOptions(
	"help|h"			=> \$copts{opt_help},
	"version|v"			=> \$copts{opt_version},
	"debug|d+"			=> \$copts{g_debug},
	"nodebug|n+"		=> \$copts{g_nodebug},
	"mode|m=s"			=> \$copts{g_mode},
	"configfile|c=s"	=> \$copts{configfile},
	"db-host=s"			=> \$copts{db_host},
	"db-name=s"			=> \$copts{db_name},
	"db-password=s"		=> \$copts{db_pass},
	"db-username=s"		=> \$copts{db_user},
	"dns-resolveip!"	=> \$copts{g_dns_resolveip},
	"dns-timeout=i"		=> \$copts{g_dns_timeout},
	"ip|i=s"			=> \$copts{s_ip},
	"port|p=i"			=> \$copts{s_port},
	"rcon!"				=> \$copts{g_rcon},
	"r"					=> \$copts{g_rcon},
	"stdin!"			=> \$copts{g_stdin},
	"s"					=> \$copts{g_stdin},
	"server-ip=s"		=> \$copts{g_server_ip},
	"server-port=i"		=> \$copts{g_server_port},
	"timestamp!"		=> \$copts{g_timestamp},
	"t"					=> \$copts{g_timestamp},
	"event-queue-size"  => \$copts{g_event_queue_size}
) or die($usage);


if ($configfile && -r $configfile) {
	$conf = '';
    $conf = ConfigReaderSimple->new($configfile);
    $conf->parse();
	&doConf($conf, %directives);
}

# these are set above, we then reload them to override values in the actual config
setOptionsConf(%copts);

if ($g_cpanelhack) {
	my $home_dir = $ENV{ HOME };
	my $base_module_dir = (-d "$home_dir/perl" ? "$home_dir/perl" : ( getpwuid($>) )[7] . '/perl/');
	unshift @INC, map { $base_module_dir . $_ } @INC;
}

eval {
  require Geo::IP::PurePerl;
};
import Geo::IP::PurePerl;

if ($opt_help) {
	print $usage;
	exit(0);
}

if ($opt_version) {
	&doConnect;
	my $result = &doQuery("
		SELECT
			value
		FROM
			hlstats_Options
		WHERE
			keyname='version'
	");

	if ($result->rows > 0) {
		$g_version = $result->fetchrow_array;
	}
	$result->finish;
	print "\nhlstats.pl (HLstatsX Community Edition) Version $g_version\n"
		. "Real-time player and clan rankings and statistics for Half-Life 2\n"
		. "Modified (C) 2008-20XX  Nicholas Hastings (nshastings@gmail.com)\n"
		. "Copyleft (L) 2007-2008  Malte Bayer\n"
		. "Modified (C) 2005-2007  Tobias Oetzel (Tobi@hlstatsx.com)\n"
		. "Original (C) 2001 by Simon Garner \n\n";
	
	print "Using ConfigReaderSimple module version $ConfigReaderSimple::VERSION\n";
	if ($g_rcon) {
		print "Using rcon module\n";
	}
	
	print "\nThis is free software; see the source for copying conditions.  There is NO\n"
		. "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n";
	exit(0);
}

# Connect to the database

&doConnect;
&readDatabaseConfig;
&buildEventInsertData;

if ($g_mode ne "Normal" && $g_mode ne "LAN" && $g_mode ne "NameTrack") {
	$g_mode = "Normal";
}

$g_debug -= $g_nodebug;
$g_debug = 0 if ($g_debug < 0);


# Init Timestamp
my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time());
$ev_timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
$ev_unixtime  = time();
$ev_daemontime = $ev_unixtime;

# Startup

&printEvent("HLSTATSX", "HLstatsX:CE $g_version starting...", 1);

# Create the UDP & TCP socket

if ($g_stdin) {
	$g_rcon = 0;
	&printEvent("UDP", "UDP listen socket disabled, reading log data from STDIN.", 1);
	if (!$g_server_ip || !$g_server_port) {
		&printEvent("UDP", "ERROR: You must specify source of STDIN data using --server-ip and --server-port", 1);
		&printEvent("UDP", "Example: ./hlstats.pl --stdin --server-ip 12.34.56.78 --server-port 27015", 1);
		exit(255);
	} else {
		&printEvent("UDP", "All data from STDIN will be allocated to server '$g_server_ip:$g_server_port'.", 1);
		$s_peerhost = $g_server_ip;
		$s_peerport = $g_server_port;
		$s_addr = "$s_peerhost:$s_peerport";
	}
} else {
	if ($s_ip) { $ip = $s_ip . ":"; } else { $ip = "port "; }
	$s_socket = IO::Socket::INET->new(
		Proto=>"udp",
		LocalAddr=>"$s_ip",
		LocalPort=>"$s_port"
	) or die ("\nCan't setup UDP socket on $ip$s_port: $!\n");
	
	&printEvent("UDP", "Opening UDP listen socket on $ip$s_port ... ok", 1);
}

if ($g_track_stats_trend > 0) {
	&printEvent("HLSTATSX", "Tracking Trend of the stats are enabled", 1);
}

if ($g_global_banning > 0) {
	&printEvent("HLSTATSX", "Global Banning on all servers is enabled", 1);
}

&printEvent("HLSTATSX", "Maximum Skill Change on all servers are ".$g_skill_maxchange." points", 1);
&printEvent("HLSTATSX", "Minimum Skill Change on all servers are ".$g_skill_minchange." points", 1);
&printEvent("HLSTATSX", "Minimum Players Kills on all servers are ".$g_player_minkills." kills", 1);

if ($g_log_chat > 0) {
	&printEvent("HLSTATSX", "Players chat logging is enabled", 1);
	if ($g_log_chat_admins > 0) {
		&printEvent("HLSTATSX", "Admins chat logging is enabled", 1);
	}
}

if ($g_global_chat == 1) {
	&printEvent("HLSTATSX", "Broadcasting public chat to all players is enabled", 1);
} elsif ($g_gloabl_chat == 2) {
	&printEvent("HLSTATSX", "Broadcasting public chat to admins is enabled", 1);
} else {
	&printEvent("HLSTATSX", "Broadcasting public chat is disabled", 1);
}

&printEvent("HLSTATSX", "Event queue size is set to ".$g_event_queue_size, 1);


%g_servers = ();

&printEvent("HLSTATSX", "HLstatsX:CE is now running ($g_mode mode, debug level $g_debug)", 1);

$start_time    = time();
if ($g_stdin) {
  $g_timestamp       = 1;
  $start_parse_time  = time();
  $import_logs_count = 0;
  &printEvent("IMPORT", "Start importing logs. Every dot signs 500 parsed lines", 1, 1);
}

# Main data loop
$c = 0;

sub getLine
{
	if ($g_stdin) {
		return <STDIN>;
	} else {
		return 1;
	}
}


&execNonQuery("TRUNCATE TABLE hlstats_Livestats");
$timeout    = 0;
$s_output = "";
my ($proxy_s_peerhost, $proxy_s_peerport);
while ($loop = &getLine()) {

    my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time());
    $ev_timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
	$ev_unixtime  = time();
	$ev_daemontime = $ev_unixtime; #time()

	if ($g_stdin) {
		$s_output = $loop;
		if (($import_logs_count > 0) && ($import_logs_count % 500 == 0)) {
			$parse_time = $ev_unixtime - $start_parse_time;
			if ($parse_time == 0) {
				$parse_time++;
			}
			print ". [".($parse_time)." sec (".sprintf("%.3f", (500 / $parse_time)).")]\n";
			$start_parse_time = $ev_unixtime;
		}
	} else {
		if(IO::Select->new($s_socket)->can_read(2)) {  # 2 second timeout
			$s_socket->recv($s_output, 1024);
			$s_output = decode( 'utf8', $s_output );
			$timeout = 0;
		} else {
			$timeout++;
			if ($timeout % 60 == 0) {
				&printEvent("HLSTATSX", "No data since 120 seconds");
			}    
		}

		if (($s_output =~ /^.*PROXY Key=(.+) (.*)PROXY.+/) && $proxy_key ne "") {
			$rproxy_key = $1;
			$s_addr = $2;

			if ($s_addr ne "") {
				($s_peerhost, $s_peerport) = split(/:/, $s_addr);
			}

			$proxy_s_peerhost = $s_socket->peerhost;
			$proxy_s_peerport  = $s_socket->peerport;
			&printEvent("PROXY", "Detected proxy call from $proxy_s_peerhost:$proxy_s_peerport") if ($d_debug > 2);


			if ($proxy_key eq $rproxy_key) {
				$s_output =~ s/PROXY.*PROXY //;
				if ($s_output =~ /^C;HEARTBEAT;/) {
					&printEvent("PROXY, Heartbeat request from $proxy_s_peerhost:$proxy_s_peerport");
				} elsif ($s_output =~ /^C;RELOAD;/) {
					&printEvent("PROXY, Reload request from $proxy_s_peerhost:$proxy_s_peerport");
				} elsif ($s_output =~ /^C;KILL;/) {
					&printEvent("PROXY, Kill request from $proxy_s_peerhost:$proxy_s_peerport");
				} else {
					&printEvent("PROXY", $s_output);
				}
			} else {
				&printEvent("PROXY", "proxy_key mismatch, dropping package");
				&printEvent("PROXY", $s_output) if ($g_debug > 2);
				$s_output = "";
				next;
			}
		} else {
			# Reset the proxy stuff and use it as "normal"
			$rproxy_key = "";
			$proxy_s_peerhost = "";
			$proxy_s_peerport = "";

			$s_peerhost  = $s_socket->peerhost;
			$s_peerport  = $s_socket->peerport;

			$s_addr = "$s_peerhost:$s_peerport";
		}
	}

	if ($timeout == 0) {
		my ($address, $port);
		my @data = split ";", $s_output;
		$cmd = $data[0];
		if ($cmd eq "C" && ($s_peerhost eq "127.0.0.1" || (($proxy_key eq $rproxy_key) && $proxy_key ne ""))) {
			&printEvent("CONTROL", "Command received: ".$data[1], 1);
			if ($proxy_s_peerhost ne "" && $proxy_s_peerport ne "") {
				$address = $proxy_s_peerhost;
				$port = $proxy_s_peerport;
			} else {
				$address = $s_peerhost;
				$port = $s_peerport;
			}

			$s_addr = "$address:$port";

			my $dest = sockaddr_in($port, inet_aton($address));
			if ($data[1] eq "HEARTBEAT") {
				my $msg = "Heartbeat OK";
				$bytes = send($::s_socket, $msg, 0, $dest);
				&printEvent("CONTROL", "Send heartbeat status to frontend at '$address:$port'", 1);
			} else {
				my $msg = "OK, EXECUTING COMMAND: ".$data[1];
				$bytes = send($::s_socket, $msg, 0, $dest);
				&printEvent("CONTROL", "Sent $bytes bytes to frontend at '$address:$port'", 1);
			}

			if ($data[1] eq "RELOAD") {
				&printEvent("CONTROL", "Re-Reading Configuration by request from Frontend...", 1);
				&reloadConfiguration;
			} 

			if ($data[1] eq "KILL") {
				&printEvent("CONTROL", "SHUTTING DOWN SCRIPT", 1);
				&flushAll;
				die "Exit script by request";
			} 
			
			next;
		}  
		$s_output =~ s/[\r\n\0]//g;	# remove naughty characters
		$s_output =~ s/\[No.C-D\]//g;	# remove [No C-D] tag
		$s_output =~ s/\[OLD.C-D\]//g;	# remove [OLD C-D] tag
		$s_output =~ s/\[NOCL\]//g;	# remove [NOCL] tag

		# Get the server info, if we know the server, otherwise ignore the data
		if (!defined($g_servers{$s_addr})) {
			if (($g_onlyconfig_servers == 1) && (!defined($g_config_servers{$s_addr}))) {
				# HELLRAISER disabled this for testing
				&printEvent(997, "NOT ALLOWED SERVER: " . $s_output);
				next;
			} elsif (!defined($g_config_servers{$s_addr})) { # create std cfg.
				my %std_cfg;
				$std_cfg{"MinPlayers"}						= 6;
				$std_cfg{"HLStatsURL"}						= "";
				$std_cfg{"DisplayResultsInBrowser"}			= 0;
				$std_cfg{"BroadCastEvents"}					= 0;
				$std_cfg{"BroadCastPlayerActions"}			= 0;
				$std_cfg{"BroadCastEventsCommand"}			= "say";
				$std_cfg{"BroadCastEventsCommandAnnounce"}	= "say",
				$std_cfg{"PlayerEvents"}					= 1;
				$std_cfg{"PlayerEventsCommand"}				= "say";
				$std_cfg{"PlayerEventsCommandOSD"}			= "",
				$std_cfg{"PlayerEventsCommandHint"}			= "",
				$std_cfg{"PlayerEventsAdminCommand"}		= "";
				$std_cfg{"ShowStats"}						= 1;
				$std_cfg{"TKPenalty"}						= 50;
				$std_cfg{"SuicidePenalty"}					= 5;
				$std_cfg{"AutoTeamBalance"}					= 0;
				$std_cfg{"AutobanRetry"}					= 0;
				$std_cfg{"TrackServerLoad"}					= 0;
				$std_cfg{"MinimumPlayersRank"}				= 0;
				$std_cfg{"EnablePublicCommands"}			= 1;
				$std_cfg{"Admins"}							= "";
				$std_cfg{"SwitchAdmins"}					= 0;
				$std_cfg{"IgnoreBots"}						= 1;
				$std_cfg{"SkillMode"}						= 0;
				$std_cfg{"GameType"}						= 0;
				$std_cfg{"Mod"}								= "";
				$std_cfg{"BonusRoundIgnore"}				= 0;
				$std_cfg{"BonusRoundTime"}					= 20;
				$std_cfg{"UpdateHostname"}					= 0;
				$std_cfg{"ConnectAnnounce"}					= 1;
				$std_cfg{"DefaultDisplayEvents"}			= 1;
				%{$g_config_servers{$s_addr}}				= %std_cfg;
				&printEvent("CFG", "Created default config for unknown server [$s_addr]");
				&printEvent("DETECT", "New server with game: " . &getServerMod($s_peerhost, $s_peerport));
			}
			
			if ($g_config_servers{$s_addr}) {
				my $tempsrv = &getServer($s_peerhost, $s_peerport);
				next if ($tempsrv == 0);
				$g_servers{$s_addr} = $tempsrv;
				my %s_cfg = %{$g_config_servers{$s_addr}};
				$g_servers{$s_addr}->set("minplayers", $s_cfg{"MinPlayers"});
				$g_servers{$s_addr}->set("hlstats_url", $s_cfg{"HLStatsURL"});
				if ($s_cfg{"DisplayResultsInBrowser"} > 0) {
					$g_servers{$s_addr}->set("use_browser",  1);
					&printEvent("SERVER", "Query results will displayed in valve browser", 1); 
				} else { 
					$g_servers{$s_addr}->set("use_browser",  0);
					&printEvent("SERVER", "Query results will not displayed in valve browser", 1); 
				}
				if ($s_cfg{"ShowStats"} == 1) {
					$g_servers{$s_addr}->set("show_stats",  1);
					&printEvent("SERVER", "Showing stats is enabled", 1); 
				} else {
					$g_servers{$s_addr}->set("show_stats",  0);
					&printEvent("SERVER", "Showing stats is disabled", 1); 
				}
				if ($s_cfg{"BroadCastEvents"} == 1) {  
					$g_servers{$s_addr}->set("broadcasting_events",  1);
					$g_servers{$s_addr}->set("broadcasting_player_actions",  $s_cfg{"BroadCastPlayerActions"});
					$g_servers{$s_addr}->set("broadcasting_command", $s_cfg{"BroadCastEventsCommand"});
					if ($s_cfg{"BroadCastEventsCommandAnnounce"} eq "ma_hlx_csay") {
						$s_cfg{"BroadCastEventsCommandAnnounce"} = $s_cfg{"BroadCastEventsCommandAnnounce"}." #all";
					}
					$g_servers{$s_addr}->set("broadcasting_command_announce", $s_cfg{"BroadCastEventsCommandAnnounce"});

					&printEvent("SERVER", "Broadcasting Live-Events with \"".$s_cfg{"BroadCastEventsCommand"}."\" is enabled", 1); 
					if ($s_cfg{"BroadCastEventsCommandAnnounce"} ne "") {
						&printEvent("SERVER", "Broadcasting Announcements with \"".$s_cfg{"BroadCastEventsCommandAnnounce"}."\" is enabled", 1); 
					}  
				} else {
					$g_servers{$s_addr}->set("broadcasting_events",               0);
					&printEvent("SERVER", "Broadcasting Live-Events is disabled", 1); 
				}
				if ($s_cfg{"PlayerEvents"} == 1) {
					$g_servers{$s_addr}->set("player_events",  1);
					$g_servers{$s_addr}->set("player_command", $s_cfg{"PlayerEventsCommand"});
					$g_servers{$s_addr}->set("player_command_osd", $s_cfg{"PlayerEventsCommandOSD"});
					$g_servers{$s_addr}->set("player_command_hint", $s_cfg{"PlayerEventsCommandHint"});
					$g_servers{$s_addr}->set("player_admin_command", $s_cfg{"PlayerEventsAdminCommand"});
					&printEvent("SERVER", "Player Event-Handler with \"".$s_cfg{"PlayerEventsCommand"}."\" is enabled", 1); 
					if ($s_cfg{"PlayerEventsCommandOSD"} ne "") {
						&printEvent("SERVER", "Displaying amx style menu with \"".$s_cfg{"PlayerEventsCommandOSD"}."\" is enabled", 1); 
					}
				} else {
					$g_servers{$s_addr}->set("player_events",                 0);
					&printEvent("SERVER", "Player Event-Handler is disabled", 1); 
				}
				if ($s_cfg{"DefaultDisplayEvents"} > 0) {
					$g_servers{$s_addr}->set("default_display_events", "1");
					&printEvent("SERVER", "New Players defaulting to show event messages", 1);
				} else {
					$g_servers{$s_addr}->set("default_display_events", "0");
					&printEvent("SERVER", "New Players defaulting to NOT show event messages", 1);
				}
				if ($s_cfg{"TrackServerLoad"} > 0) {
					$g_servers{$s_addr}->set("track_server_load", "1");
					&printEvent("SERVER", "Tracking server load is enabled", 1);
				} else {
					$g_servers{$s_addr}->set("track_server_load", "0");
					&printEvent("SERVER", "Tracking server load is disabled", 1);
				}

				if ($s_cfg{"TKPenalty"} > 0) {
					$g_servers{$s_addr}->set("tk_penalty", $s_cfg{"TKPenalty"});
					&printEvent("SERVER", "Penalty team kills with ".$s_cfg{"TKPenalty"}." points", 1);
				}  
				if ($s_cfg{"SuicidePenalty"} > 0) {
					$g_servers{$s_addr}->set("suicide_penalty", $s_cfg{"SuicidePenalty"});
					&printEvent("SERVER", "Penalty suicides with ".$s_cfg{"SuicidePenalty"}." points", 1);
				}  
				if ($s_cfg{"BonusRoundTime"} > 0) {
					$g_servers{$s_addr}->set("bonusroundtime", $s_cfg{"BonusRoundTime"});
					&printEvent("SERVER", "Bonus Round time set to: ".$s_cfg{"BonusRoundTime"}, 1);
				} 
				if ($s_cfg{"BonusRoundIgnore"} > 0) {
					$g_servers{$s_addr}->set("bonusroundignore", $s_cfg{"BonusRoundIgnore"});
					&printEvent("SERVER", "Bonus Round is being ignored. Length: (".$s_cfg{"BonusRoundTime"}.")", 1);
				}
				if ($s_cfg{"AutoTeamBalance"} > 0) {
					$g_servers{$s_addr}->set("ba_enabled", "1");
					&printEvent("TEAMS", "Auto-Team-Balancing is enabled", 1);
				} else {
					$g_servers{$s_addr}->set("ba_enabled", "0");
					&printEvent("TEAMS", "Auto-Team-Balancing is disabled", 1);
				}
				if ($s_cfg{"AutoBanRetry"} > 0) {
					$g_servers{$s_addr}->set("auto_ban", "1");
					&printEvent("TEAMS", "Auto-Retry-Banning is enabled", 1);
				} else {
					$g_servers{$s_addr}->set("auto_ban", "0");
					&printEvent("TEAMS", "Auto-Retry-Banning is disabled", 1);
				}
				
				if ($s_cfg{"MinimumPlayersRank"} > 0) {
					$g_servers{$s_addr}->set("min_players_rank", $s_cfg{"MinimumPlayersRank"});
					&printEvent("SERVER", "Requires minimum players rank is enabled [MinPos:".$s_cfg{"MinimumPlayersRank"}."]", 1);
				} else {
					$g_servers{$s_addr}->set("min_players_rank", "0");
					&printEvent("SERVER", "Requires minimum players rank is disabled", 1);
				}
				
				if ($s_cfg{"EnablePublicCommands"} > 0) {
					$g_servers{$s_addr}->set("public_commands", $s_cfg{"EnablePublicCommands"});
					&printEvent("SERVER", "Public chat commands are enabled", 1);
				} else {
					$g_servers{$s_addr}->set("public_commands", "0");
					&printEvent("SERVER", "Public chat commands are disabled", 1);
				}

				if ($s_cfg{"Admins"} ne "") {
					@{$g_servers{$s_addr}->{admins}} = split(/,/, $s_cfg{"Admins"});
					foreach(@{$g_servers{$s_addr}->{admins}})
					{
						$_ =~ s/^STEAM_[0-9]+?\://i;
					}
					&printEvent("SERVER", "Admins: ".$s_cfg{"Admins"}, 1);
				}

				if ($s_cfg{"SwitchAdmins"} > 0) {
					$g_servers{$s_addr}->set("switch_admins", "1");
					&printEvent("TEAMS", "Switching Admins on Auto-Team-Balance is enabled", 1);
				} else {
					$g_servers{$s_addr}->set("switch_admins", "0");
					&printEvent("TEAMS", "Switching Admins on Auto-Team-Balance is disabled", 1);
				}
				
				if ($s_cfg{"IgnoreBots"} > 0) {
					$g_servers{$s_addr}->set("ignore_bots", "1");
					&printEvent("SERVER", "Ignoring bots is enabled", 1);
				} else {
					$g_servers{$s_addr}->set("ignore_bots", "0");
					&printEvent("SERVER", "Ignoring bots is disabled", 1);
				}
				$g_servers{$s_addr}->set("skill_mode", $s_cfg{"SkillMode"});
				&printEvent("SERVER", "Using skill mode ".$s_cfg{"SkillMode"}, 1);
				
				if ($s_cfg{"GameType"} == 1) {
					$g_servers{$s_addr}->set("game_type", $s_cfg{"GameType"});
					&printEvent("SERVER", "Game type: Counter-Strike: Source - Deathmatch", 1);
				} else {
					$g_servers{$s_addr}->set("game_type", "0");
					&printEvent("SERVER", "Game type: Normal", 1);
				}

				$g_servers{$s_addr}->set("mod", $s_cfg{"Mod"});
				
				if ($s_cfg{"Mod"} ne "") {
					&printEvent("SERVER", "Using plugin ".$s_cfg{"Mod"}." for internal functions!", 1);
				}
				if ($s_cfg{"ConnectAnnounce"} == 1) {
					$g_servers{$s_addr}->set("connect_announce", $s_cfg{"ConnectAnnounce"});
					&printEvent("SERVER", "Connect Announce is enabled", 1);
				} else {
					$g_servers{$s_addr}->set("connect_announce", "0");
					&printEvent("SERVER", "Connect Announce is disabled", 1);
				}
				if ($s_cfg{"UpdateHostname"} == 1) {
					$g_servers{$s_addr}->set("update_hostname", $s_cfg{"UpdateHostname"});
					&printEvent("SERVER", "Auto-updating hostname is enabled", 1);
				} else {
					$g_servers{$s_addr}->set("update_hostname", "0");
					&printEvent("SERVER", "Auto-updating hostname is disabled", 1);
				}
				$g_servers{$s_addr}->get_game_mod_opts();
			}
		}

		if (!$g_servers{$s_addr}->{"srv_players"})
		{
			$g_servers{$s_addr}->{"srv_players"} = ();
			%g_players=();
		}
		else
		{	
			%g_players=%{$g_servers{$s_addr}->{"srv_players"}};
		}
		
		# Get the datestamp (or complain)
		#if ($s_output =~ s/^.*L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d):\s*//)
		
		#$is_streamed = 0;
		#$test_for_date = 0;
		#$is_streamed = ($s_output !~ m/^L\s*/);

		#if ( !$is_streamed ) {
		# $test_for_date = ($s_output =~ s/^L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d):\s*//);
		#} else {
		# $test_for_date = ($s_output =~ s/^\S*L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d):\s*//);
		#}
		
		#if ($test_for_date)
		
		# EXPLOIT FIX
		
		if ($s_output =~ s/^(?:.*?)?L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d)\.?(\d\d\d)?\s*[:-]\s*//) {
			$ev_month = $1;
			$ev_day   = $2;
			$ev_year  = $3;
			$ev_hour  = $4;
			$ev_min   = $5;
			$ev_sec   = $6;
			$ev_time  = "$ev_hour:$ev_min:$ev_sec";
			$ev_remotetime  = timelocal($ev_sec,$ev_min,$ev_hour,$ev_day,$ev_month-1,$ev_year);
			
			if ($g_timestamp) {
				$ev_timestamp = "$ev_year-$ev_month-$ev_day $ev_time";
				$ev_unixtime  = $ev_remotetime;
				if ($g_stdin)
				{
					$ev_daemontime = $ev_unixtime;
				}
			}
		} else {
			&printEvent(998, "MALFORMED DATA: " . $s_output);
			next;
		}
		
		if ($g_debug >= 4) {
			print $s_addr.": \"".$s_output."\"\n";
		}
		
		if (($g_stdin == 0) && ($g_servers{$s_addr}->{last_event} > 0) && ( ($ev_unixtime - $g_servers{$s_addr}->{last_event}) > 299) ) {
			$g_servers{$s_addr}->set("map", "");
			$g_servers{$s_addr}->get_map();
		}
		
		$g_servers{$s_addr}->set("last_event", $ev_unixtime);

		# Now we parse the events.
		
		my $ev_type   = 0;
		my $ev_status = "";
		my $ev_team   = "";
		my $ev_player = 0;
		my $ev_verb   = "";
		my $ev_obj_a  = "";
		my $ev_obj_b  = "";
		my $ev_obj_c  = "";
		my $ev_properties = "";
		my %ev_properties = ();
		my %ev_player = ();
	
		# pvkii parrot log lines also fit the death line parsing
		if ($g_servers{$s_addr}->{play_game} == PVKII()
			&& $s_output =~ /^
				"(.+?(?:<[^>]*>){3})"		# player string
				\s[a-z]{6}\s				# 'killed'
				"npc_parrot<.+?>"			# parrot string
				\s[a-z]{5}\s[a-z]{2}\s		# 'owned by'
				"(.+?(?:<[^>]*>){3})"		# owner string
				\s[a-z]{4}\s				# 'with'
				"([^"]*)"				#weapon
				(.*)					#properties
				/x)
		{
			$ev_player = $1; # player
			$ev_obj_b  = $2; # victim
			$ev_obj_c  = $3; # weapon
			$ev_properties = $4;
			%ev_properties_hash = &getProperties($ev_properties);
			
			my $playerinfo = &getPlayerInfo($ev_player, 1);
			my $victiminfo = &getPlayerInfo($ev_obj_b, 1);
			$ev_type = 10;

			if ($playerinfo) {
				if ($victiminfo) {
					$ev_status = &doEvent_PlayerPlayerAction(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$victiminfo->{"userid"},
						$victiminfo->{"uniqueid"},
						"killed_parrot",
						undef,
						undef,
						undef,
						undef,
						undef,
						undef,
						&getProperties($ev_properties)
					);
				}

				$ev_type = 11;
				
				$ev_status = &doEvent_PlayerAction(
					$playerinfo->{"userid"},
					$playerinfo->{"uniqueid"},
					"killed_parrot",
					undef,
					undef,
					undef,
					&getProperties($ev_properties)
				);
			}
		} elsif ($s_output =~ /^
				(?:\(DEATH\))?		# l4d prefix, such as (DEATH) or (INCAP)
				"(.+?(?:<.+?>)*?
				(?:<setpos_exact\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d);[^"]*)?
				)"						# player string with or without l4d-style location coords
				(?:\s\[(-?\d+)\s(-?\d+)\s(-?\d+)\])?
				\skilled\s			# verb (ex. attacked, killed, triggered)
				"(.+?(?:<.+?>)*?
				(?:<setpos_exact\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d);[^"]*)?
				)"						# player string as above or action name
				(?:\s\[(-?\d+)\s(-?\d+)\s(-?\d+)\])?
				\swith\s				# (ex. with, against)
				"([^"]*)"
				(.*)					#properties
				/x)
		{

			# Prototype: "player" verb "obj_a" ?... "obj_b"[properties]
			# Matches:
			#  8. Kills
			
			$ev_player = $1;
			$ev_Xcoord = $2; # attacker/player coords (L4D)
			$ev_Ycoord = $3;
			$ev_Zcoord = $4;
			if( !defined($ev_Xcoord) ) {
				# if we didn't get L4D style, overwrite with CSGO style (which we may still not have)
				$ev_Xcoord = $5;
				$ev_Ycoord = $6;
				$ev_Zcoord = $7;
			}
			$ev_obj_a  = $8; # victim
			$ev_XcoordKV = $9; # kill victim coords (L4D)
			$ev_YcoordKV = $10;
			$ev_ZcoordKV = $11;
			if( !defined($ev_XcoordKV) ) {
				$ev_XcoordKV = $12; # kill victim coords (CSGO)
				$ev_YcoordKV = $13;
				$ev_ZcoordKV = $14;
			}
			$ev_obj_b  = $15; # weapon
			$ev_properties = $16;
			%ev_properties_hash = &getProperties($ev_properties);
			
			my $killerinfo = &getPlayerInfo($ev_player, 1);
			my $victiminfo = &getPlayerInfo($ev_obj_a, 1);
			$ev_type = 8;
			
			$headshot = 0;
			if ($ev_properties =~ m/headshot/) {
				$headshot = 1;
			}
			
			if ($killerinfo && $victiminfo) {
				my $killerId       = $killerinfo->{"userid"};
				my $killerUniqueId = $killerinfo->{"uniqueid"};
				my $killer         = lookupPlayer($s_addr, $killerId, $killerUniqueId);
				
				# octo
				if($killer->{role} eq "scout") {
					$ev_status = &doEvent_PlayerAction(
						$killerinfo->{"userid"},
						$killerinfo->{"uniqueid"},
						"kill_as_scout",
						"kill_as_scout"
					);
				}
				if($killer->{role} eq "spy") {
					$ev_status = &doEvent_PlayerAction(
						$killerinfo->{"userid"},
						$killerinfo->{"uniqueid"},
						"kill_as_spy",
						"kill_as_spy"
					);
				}

				my $victimId       = $victiminfo->{"userid"};
				my $victimUniqueId = $victiminfo->{"uniqueid"};
				my $victim         = lookupPlayer($s_addr, $victimId, $victimUniqueId);

				$ev_status = &doEvent_Frag(
					$killerinfo->{"userid"},
					$killerinfo->{"uniqueid"},
					$victiminfo->{"userid"},
					$victiminfo->{"uniqueid"},
					$ev_obj_b,
					$headshot,
					$ev_Xcoord,
					$ev_Ycoord,
					$ev_Zcoord,
					$ev_XcoordKV,
					$ev_YcoordKV,
					$ev_ZcoordKV,
					%ev_properties_hash
				);
			} 
		} elsif ($g_servers{$s_addr}->{play_game} == L4D() && $s_output =~ /^
				\(INCAP\)		# l4d prefix, such as (DEATH) or (INCAP)
				"(.+?(?:<.+?>)*?
				<setpos_exact\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d);[^"]*
				)"						# player string with or without l4d-style location coords
				\swas\sincapped\sby\s			# verb (ex. attacked, killed, triggered)
				"(.+?(?:<.+?>)*?
				<setpos_exact\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d)\s(-?\d+?\.\d\d);[^"]*
				)"						# player string as above or action name
				\swith\s				# (ex. with, against)
				"([^"]*)"					# weapon name
				(.*)					#properties
				/x)
		{
			#  800. L4D Incapacitation
			
			$ev_player = $1;
			$ev_l4dXcoord = $2; # attacker/player coords (L4D)
			$ev_l4dYcoord = $3;
			$ev_l4dZcoord = $4;
			$ev_obj_a  = $5; # victim
			$ev_l4dXcoordKV = $6; # kill victim coords (L4D)
			$ev_l4dYcoordKV = $7;
			$ev_l4dZcoordKV = $8;
			$ev_obj_b  = $9; # weapon
			$ev_properties = $10;
			%ev_properties_hash = &getProperties($ev_properties);
			
			# reverse killer/victim (x was incapped by y = y killed x)
			my $killerinfo = &getPlayerInfo($ev_obj_a, 1);
			my $victiminfo = &getPlayerInfo($ev_player, 1);
			
			if ($victiminfo->{team} eq "Infected") {
				$victiminfo = undef;
			}
			$ev_type = 800;
							
			$headshot = 0;
			if ($ev_properties =~ m/headshot/) {
				$headshot = 1;
			}
			if ($killerinfo && $victiminfo) {
				my $killerId       = $killerinfo->{"userid"};
				my $killerUniqueId = $killerinfo->{"uniqueid"};
				my $killer         = lookupPlayer($s_addr, $killerId, $killerUniqueId);

				my $victimId       = $victiminfo->{"userid"};
				my $victimUniqueId = $victiminfo->{"uniqueid"};
				my $victim         = lookupPlayer($s_addr, $victimId, $victimUniqueId);

				$ev_status = &doEvent_Frag(
					$killerinfo->{"userid"},
					$killerinfo->{"uniqueid"},
					$victiminfo->{"userid"},
					$victiminfo->{"uniqueid"},
					$ev_obj_b,
					$headshot,
					$ev_l4dXcoord,
					$ev_l4dYcoord,
					$ev_l4dZcoord,
					$ev_l4dXcoordKV,
					$ev_l4dYcoordKV,
					$ev_l4dZcoordKV,
					&getProperties($ev_properties)
				);
			}
		} elsif ($g_servers{$s_addr}->{play_game} == L4D() && $s_output =~ /^\(TONGUE\)\sTongue\sgrab\sstarting\.\s+Smoker:"(.+?(?:<.+?>)*?(?:|<setpos_exact ((?:|-)\d+?\.\d\d) ((?:|-)\d+?\.\d\d) ((?:|-)\d+?\.\d\d);.*?))"\.\s+Victim:"(.+?(?:<.+?>)*?(?:|<setpos_exact ((?:|-)\d+?\.\d\d) ((?:|-)\d+?\.\d\d) ((?:|-)\d+?\.\d\d);.*?))".*/) {
			# Prototype: (TONGUE) Tongue grab starting.  Smoker:"player". Victim:"victim".
			# Matches:
			# 11. Player Action
			
			$ev_player = $1;
			$ev_l4dXcoord = $2;
			$ev_l4dYcoord = $3;
			$ev_l4dZcoord = $4;
			$ev_victim = $5;
			$ev_l4dXcoordV = $6;
			$ev_l4dYcoordV = $7;
			$ev_l4dZcoordV = $8;
			
			$playerinfo = &getPlayerInfo($ev_player, 1);
			$victiminfo = &getPlayerInfo($ev_victim, 1);

			$ev_type = 11;
				
			if ($playerinfo) {
				$ev_status = &doEvent_PlayerAction(
					$playerinfo->{"userid"},
					$playerinfo->{"uniqueid"},
					"tongue_grab"
				);
			}
			if ($playerinfo && $victiminfo) {
					$ev_status = &doEvent_PlayerPlayerAction(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$victiminfo->{"userid"},
						$victiminfo->{"uniqueid"},
						"tongue_grab",
						$ev_l4dXcoord,
						$ev_l4dYcoord,
						$ev_l4dZcoord,
						$ev_l4dXcoordV,
						$ev_l4dYcoordV,
						$ev_l4dZcoordV
					);
			}
		} elsif ($s_output =~ /^
				"(.+?(?:<.+?>)*?
				)"						# player string
				\s(triggered(?:\sa)?)\s			# verb (ex. attacked, killed, triggered)
				"(.+?(?:<.+?>)*?
				)"						# player string as above or action name
				\s[a-zA-Z]+\s				# (ex. with, against)
				"(.+?(?:<.+?>)*?
				)"						# player string as above or weapon name
				(?:\s[a-zA-Z]+\s"(.+?)")?	# weapon name on plyrplyr actions
				(.*)					#properties
				/x)
		{			
		
			# 10. Player-Player Actions
			
			# no l4d/2 actions are logged with the locations (in fact, very few are logged period) so the l4d/2 location parsing can be skipped
			
			$ev_player = $1;
			$ev_verb   = $2; # triggered or triggered a
			$ev_obj_a  = $3; # action
			$ev_obj_b  = $4; # victim
			$ev_obj_c  = $5; # weapon (optional)
			$ev_properties = $6;
			%ev_properties_hash = &getProperties($ev_properties);
			

			if ($ev_verb eq "triggered") {  # it's either 'triggered' or 'triggered a'
			
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				my $victiminfo = &getPlayerInfo($ev_obj_b, 1);
				$ev_type = 10;

				if ($playerinfo) {
					if ($victiminfo) {
						$ev_status = &doEvent_PlayerPlayerAction(
							$playerinfo->{"userid"},
							$playerinfo->{"uniqueid"},
							$victiminfo->{"userid"},
							$victiminfo->{"uniqueid"},
							$ev_obj_a,
							undef,
							undef,
							undef,
							undef,
							undef,
							undef,
							&getProperties($ev_properties)
						);
					}

					$ev_type = 11;
					
					$ev_status = &doEvent_PlayerAction(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a,
						undef,
						undef,
						undef,
						&getProperties($ev_properties)
					);
				}
			} else {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				$ev_type = 11;
				
				if ($playerinfo) {
					$ev_status = &doEvent_PlayerAction(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a,
						undef,
						undef,
						undef,
				        &getProperties($ev_properties)
					);
				}
			}
		} elsif ($s_output =~ /^(?:\[STATSME\] )?"(.+?(?:<.+?>)*)" triggered "(weaponstats\d{0,1})"(.*)$/ ) {
			# Prototype: [STATSME] "player" triggered "weaponstats?"[properties]
			# Matches:
			# 501. Statsme weaponstats
			# 502. Statsme weaponstats2
	
			$ev_player = $1;
			$ev_verb   = $2; # weaponstats; weaponstats2
			$ev_properties = $3;
			%ev_properties = &getProperties($ev_properties);
	
			if (like($ev_verb, "weaponstats")) {
				$ev_type = 501;
				my $playerinfo = &getPlayerInfo($ev_player, 0);
				
				if ($playerinfo) {
					my $playerId = $playerinfo->{"userid"};
					my $playerUniqueId = $playerinfo->{"uniqueid"};
					my $ingame = 0;
					
					$ingame = 1 if (lookupPlayer($s_addr, $playerId, $playerUniqueId));
					
					if (!$ingame) {
						&getPlayerInfo($ev_player, 1);
					}
					
					$ev_status = &doEvent_Statsme(
						$playerId,
						$playerUniqueId,
						$ev_properties{"weapon"},
						$ev_properties{"shots"},
						$ev_properties{"hits"},
						$ev_properties{"headshots"},
						$ev_properties{"damage"},
						$ev_properties{"kills"},
						$ev_properties{"deaths"}
					);

					if (!$ingame) {
						&doEvent_Disconnect(
							$playerId,
							$playerUniqueId,
							""
						);
					}
				}
			} elsif (like($ev_verb, "weaponstats2")) {
				$ev_type = 502;
				my $playerinfo = &getPlayerInfo($ev_player, 0);
				
				if ($playerinfo) {
					my $playerId = $playerinfo->{"userid"};
					my $playerUniqueId = $playerinfo->{"uniqueid"};
					my $ingame = 0;
					
					$ingame = 1 if (lookupPlayer($s_addr, $playerId, $playerUniqueId));
					
					if (!$ingame) {
						&getPlayerInfo($ev_player, 1);
					}
					
					$ev_status = &doEvent_Statsme2(
						$playerId,
						$playerUniqueId,
						$ev_properties{"weapon"},
						$ev_properties{"head"},
						$ev_properties{"chest"},
						$ev_properties{"stomach"},
						$ev_properties{"leftarm"},
						$ev_properties{"rightarm"},
						$ev_properties{"leftleg"},
						$ev_properties{"rightleg"}
					);
					
					if (!$ingame) {
						&doEvent_Disconnect(
							$playerId,
							$playerUniqueId,
							""
						);
					}
				}
			}
		} elsif ($s_output =~ /^(?:\[STATSME\] )?"(.+?(?:<.+?>)*)" triggered "(latency|time)"(.*)$/ ) {
			# Prototype: [STATSME] "player" triggered "latency|time"[properties]
			# Matches:
			# 503. Statsme latency
			# 504. Statsme time
	
			$ev_player = $1;
			$ev_verb   = $2; # latency; time
			$ev_properties = $3;
			%ev_properties = &getProperties($ev_properties);
	
			if ($ev_verb eq "time") {
				$ev_type = 504;
				my $playerinfo = &getPlayerInfo($ev_player, 0);
	
				if ($playerinfo) {
					my ($min, $sec) = split(/:/, $ev_properties{"time"});
					my $hour = sprintf("%d", $min / 60);
	
					if ($hour) {
						$min = $min % 60;
					}
	
					$ev_status = &doEvent_Statsme_Time(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						"$hour:$min:$sec"
					);
				}
			} else { # latency
				$ev_type = 503;
				my $playerinfo = &getPlayerInfo($ev_player, 0);
	
				if ($playerinfo) {
					$ev_status = &doEvent_Statsme_Latency(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_properties{"ping"}
					);
				}
			}
		} elsif ($s_output =~ /^"(.+?(?:<.+?>)*?)" triggered "clantag" \(value "(.+?)?"\)$/) {
			# L 08/14/2014 - 18:04:21: "Laam4<7><STEAM_1:0:106564><Unassigned>" triggered "clantag" (value "Kunіngas")
			$ev_player = $1;
			$ev_clantag = $2;
			
			if ($ev_clantag) {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				$ev_type = 600;
				
				if ($playerinfo) {
					$ev_status = &doEvent_Clan(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_clantag
					);
				}
			}
		} elsif ($s_output =~ /^"(.+?(?:<.+?>)*?)" api_request "playerinfo" \(value "(.+?)?"\)$/) {

			$ev_player = $1;
			$ev_queryId = $2;
			
			if ($ev_queryId) {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				if ($playerinfo) {
					$ev_type = 101;
					$ev_status = &doEvent_ApiReqestPlayerInfo($playerinfo->{"userid"}, $playerinfo->{"uniqueid"}, $ev_queryId);
				}
			}
		} elsif ($s_output =~ /^"(.+?(?:<.+?>)*?)"(?:\s\[(-?\d+)\s(-?\d+)\s(-?\d+)\]) ([a-zA-Z,_\s]+) "(.+?)"(.*)$/) {
			# 4. Suicide for CS:GO
			$ev_player = $1;
			$ev_Xcoord = $2;
			$ev_Ycoord = $3;
			$ev_Zcoord = $4;
			$ev_verb   = $5;
			$ev_obj_a  = $6;
			
			if ($ev_verb eq "committed suicide with") {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				$ev_type = 4;
				
				if ($playerinfo) {
					$ev_status = &doEvent_Suicide(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a,
						$ev_Xcoord,
						$ev_Ycoord,
						$ev_Zcoord
					);
				}
			}
		} elsif ($s_output =~ /^"(.+?(?:<.+?>)*?)" ([a-zA-Z,_\s]+) "(.+?)"(.*)$/) {
			# Prototype: "player" verb "obj_a"[properties]
			# Matches:
			#  1. Connection
			#  4. Suicides (Fixed above for CSGO)
			#  5. Team Selection
			#  6. Role Selection
			#  7. Change Name
			# 11. Player Objectives/Actions
			# 14. a) Chat; b) Team Chat
			
			$ev_player = $1;
			$ev_verb   = $2;
			$ev_obj_a  = $3;
			$ev_properties = $4;
			%ev_properties = &getProperties($ev_properties);
			
			if ($ev_verb eq "connected, address") {
				my $ipAddr = $ev_obj_a;
				my $playerinfo;
				
				if ($ipAddr =~ /([\d\.]+):(\d+)/) {
					$ipAddr = $1;
				}
				
				$playerinfo = &getPlayerInfo($ev_player, 1, $ipAddr);
				
				$ev_type = 1;
				
				if ($playerinfo) {
					if (($playerinfo->{"uniqueid"} =~ /UNKNOWN/) || ($playerinfo->{"uniqueid"} =~ /PENDING/) || ($playerinfo->{"uniqueid"} =~ /VALVE_ID_LAN/)) {
						$ev_status = "(DELAYING CONNECTION): $s_output";
	
	                    if ($g_mode ne "LAN")  {
							my $p_name   = $playerinfo->{"name"};
							my $p_userid = $playerinfo->{"userid"};
							&printEvent("SERVER", "LATE CONNECT [$p_name/$p_userid] - STEAM_ID_PENDING");
							$g_preconnect->{"$s_addr/$p_userid/$p_name"} = {
								ipaddress => $ipAddr,
								name => $p_name,
								server => $s_addr,
								timestamp => $ev_daemontime
							};
						}   
					} else {
						$ev_status = &doEvent_Connect(
							$playerinfo->{"userid"},
							$playerinfo->{"uniqueid"},
							$ipAddr
						);
					}
				}
			} elsif ($ev_verb eq "joined team") {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				$ev_type = 5;
				
				if ($playerinfo) {
					$ev_status = &doEvent_TeamSelection(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a
					);
				}
			} elsif ($ev_verb eq "changed role to") {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				$ev_type = 6;
				
				if ($playerinfo) {
					$ev_status = &doEvent_RoleSelection(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a
					);
				}
			} elsif ($ev_verb eq "changed name to") {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				$ev_type = 7;
				
				if ($playerinfo) {
					$ev_status = &doEvent_ChangeName(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a
					);
				}
			} elsif ($ev_verb eq "triggered") {

			    # in cs:s players dropp the bomb if they are the only ts
			    # and disconnect...the dropp the bomb after they disconnected :/
			    if ($ev_obj_a eq "Dropped_The_Bomb") {
				  $playerinfo = &getPlayerInfo($ev_player, 0);
			    } else {
				  $playerinfo = &getPlayerInfo($ev_player, 1);
				}
				if ($playerinfo) {
					if ($ev_obj_a eq "player_changeclass" && defined($ev_properties{newclass})) {
						
						$ev_type = 6;
						
						$ev_status = &doEvent_RoleSelection(
							$playerinfo->{"userid"},
							$playerinfo->{"uniqueid"},
							$ev_properties{newclass}
						);
					} else {					
						if ($g_servers{$s_addr}->{play_game} == TFC())
						{
							if ($ev_obj_a eq "Sentry_Destroyed")
							{
								$ev_obj_a = "Sentry_Dismantle";
							}
							elsif ($ev_obj_a eq "Dispenser_Destroyed")
							{
								$ev_obj_a = "Dispenser_Dismantle";
							}
							elsif ($ev_obj_a eq "Teleporter_Entrance_Destroyed")
							{
								$ev_obj_a = "Teleporter_Entrance_Dismantle"
							}
							elsif ($ev_obj_a eq "Teleporter_Exit_Destroyed")
							{
								$ev_obj_a = "Teleporter_Exit_Dismantle"
							}
						}
						
						$ev_type = 11;
				
						$ev_status = &doEvent_PlayerAction(
							$playerinfo->{"userid"},
							$playerinfo->{"uniqueid"},
							$ev_obj_a,
							undef,
							undef,
							undef,
							%ev_properties
						);
					}
				}
			} elsif ($ev_verb eq "triggered a") {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				$ev_type = 11;
				
				if ($playerinfo)
				{
					$ev_status = &doEvent_PlayerAction(
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a,
						undef,
						undef,
						undef,
						%ev_properties
					);
				}
			} elsif ($ev_verb eq "say" || $ev_verb eq "say_team" || $ev_verb eq "say_squad") {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				$ev_type = 14;
				
				if ($playerinfo) {
					$ev_status = &doEvent_Chat(
						$ev_verb,
						$playerinfo->{"userid"},
						$playerinfo->{"uniqueid"},
						$ev_obj_a
					);
				}
			}
		} elsif ($s_output =~ /^(?:Kick: )?"(.+?(?:<.+?>)*)" ([^\(]+)(.*)$/) {
			# Prototype: "player" verb[properties]
			# Matches:
			#	 1. Connection (CS:GO only)
			#  2. Enter Game
			#  3. Disconnection
			
			$ev_player = $1;
			$ev_verb   = $2;
			$ev_properties = $3;
			%ev_properties = &getProperties($ev_properties);
			
			if (like($ev_verb, "entered the game")) {
				my $playerinfo = &getPlayerInfo($ev_player, 1);
				
				if ($playerinfo) {
					$ev_type = 2;
					$ev_status = &doEvent_EnterGame($playerinfo->{"userid"}, $playerinfo->{"uniqueid"}, $ev_obj_a);
				}
			} elsif (like($ev_verb, "disconnected") || like($ev_verb, "was kicked")) {
				my $playerinfo = &getPlayerInfo($ev_player, 0);
				
				if ($playerinfo) {
					$ev_type = 3;
	
					$userid   = $playerinfo->{userid};
					$uniqueid = $playerinfo->{uniqueid};
	
					$ev_status = &doEvent_Disconnect(
						$playerinfo->{userid},
						$playerinfo->{uniqueid},
						$ev_properties
					);
				}
			}
			elsif (like($ev_verb, "STEAM USERID validated") || like($ev_verb, "VALVE USERID validated")) {               
				
				my $isCSGO = ($g_servers{$s_addr}->{play_game} == CSGO());
				my $playerinfo = &getPlayerInfo($ev_player, $isCSGO ? 1 : 0);
	
				if ($playerinfo) {                       
					
					$ev_type = 1;
					
					if ($isCSGO) {
						$ev_status = &doEvent_Connect($playerinfo->{userid}, $playerinfo->{uniqueid}, $playerinfo->{address});
					}	
				}
			}
		} elsif ($s_output =~ /^Team "(.+?)" ([^"\(]+) "([^"]+)"(.*)$/) {
			# Prototype: Team "team" verb "obj_a"[properties]
			# Matches:
			# 12. Team Objectives/Actions
			# 1200. Team Objective With Players involved
			# 15. Team Alliances
			
			$ev_team   = $1;
			$ev_verb   = $2;
			$ev_obj_a  = $3;
			$ev_properties = $4;
			%ev_properties_hash = &getProperties($ev_properties);
			
			if ($ev_obj_a eq "pointcaptured") {
				$numcappers = $ev_properties_hash{numcappers};
				if ($g_debug > 1) {
					print "NumCappers = ".$numcappers."\n";
				}
				foreach ($i = 1; $i <= $numcappers; $i++) {
					# reward each player involved in capturing
					$player = $ev_properties_hash{"player".$i};
					if ($g_debug > 1) {
						print $i." -> ".$player."\n";
					}
					#$position = $ev_properties_hash{"position".$i};
					my $playerinfo = &getPlayerInfo($player, 1);
					if ($playerinfo) {
						$ev_status = &doEvent_PlayerAction(
							$playerinfo->{"userid"},
							$playerinfo->{"uniqueid"},
							$ev_obj_a,
							"",
							"",
							"",
							&getProperties($ev_properties)
						);
					}
				}
			}
 			if ($ev_obj_a eq "captured_loc") {
			#	$flag_name = $ev_properties_hash{flagname};
				$player_a  = $ev_properties_hash{player_a};
				$player_b  = $ev_properties_hash{player_b};
  			  
				my $playerinfo_a = &getPlayerInfo($player_a, 1);
				if ($playerinfo_a) {
					$ev_status = &doEvent_PlayerAction(
						$playerinfo_a->{"userid"},
						$playerinfo_a->{"uniqueid"},
						$ev_obj_a,
						"",
						"",
						"",
						&getProperties($ev_properties)
					);
				}

				my $playerinfo_b = &getPlayerInfo($player_b, 1);
				if ($playerinfo_b) {
					$ev_status = &doEvent_PlayerAction(
						$playerinfo_b->{"userid"},
						$playerinfo_b->{"uniqueid"},
						$ev_obj_a,
						"",
						"",
						"",
						&getProperties($ev_properties)
					);
				}
			}  
			
			if (like($ev_verb, "triggered")) {
				if ($ev_obj_a ne "captured_loc") {
					$ev_type = 12;
					$ev_status = &doEvent_TeamAction(
						$ev_team,
						$ev_obj_a,
						&getProperties($ev_properties)
					);
				}
			} elsif (like($ev_verb, "triggered a")) {
				$ev_type = 12;
				$ev_status = &doEvent_TeamAction(
					$ev_team,
					$ev_obj_a
				);
			}
		} elsif ($s_output =~ /^(Rcon|Bad Rcon): "rcon [^"]+"([^"]+)"\s+(.+)" from "([0-9\.]+?):(\d+?)"(.*)$/) {
			# Prototype: verb: "rcon ?..."obj_a" obj_b" from "obj_c"[properties]
			# Matches:
		    # 20. HL1 a) Rcon; b) Bad Rcon
			
			$ev_verb   = $1;
			$ev_obj_a  = $2; # password
			$ev_obj_b  = $3; # command
			$ev_obj_c  = $4; # ip
			$ev_obj_d  = $5; # port
			$ev_properties = $6;
			%ev_properties = &getProperties($ev_properties);
			if ($g_rcon_ignoreself == 0 || $ev_obj_c ne $s_ip) {
				$ev_obj_b = substr($ev_obj_b, 0, 255);
				if (like($ev_verb, "Rcon")) {
					$ev_type = 20;
					$ev_status = &doEvent_Rcon(
						"OK",
						$ev_obj_b,
						"",
						$ev_obj_c
					);
				} elsif (like($ev_verb, "Bad Rcon")) {
					$ev_type = 20;
					$ev_status = &doEvent_Rcon(
						"BAD",
						$ev_obj_b,
						$ev_obj_a,
						$ev_obj_c
					);
				}
			} else {
				$ev_status = "(IGNORED) Rcon from \"$ev_obj_a:$ev_obj_b\": \"$ev_obj_c\"";
			}
		} elsif ($s_output =~ /^rcon from "(.+?):(.+?)": (?:command "(.*)".*|(Bad) Password)$/) {
			# Prototype: verb: "rcon ?..."obj_a" obj_b" from "obj_c"[properties]
			# Matches:
		    # 20. a) Rcon;
			
			$ev_obj_a  = $1; # ip
			$ev_obj_b  = $2; # port
			$ev_obj_c  = $3; # command
			$ev_isbad  = $4; # if bad, "Bad"
			if ($g_rcon_ignoreself == 0 || $ev_obj_a ne $s_ip) {
				if ($ev_isbad ne "Bad") {
					$ev_type = 20;
					@cmds = split(/;/,$ev_obj_c);
					foreach(@cmds)
					{
						$ev_status = &doEvent_Rcon(
							"OK",
							substr($_, 0, 255),
							"",
							$ev_obj_a
						);
					}
				} else {
					$ev_type = 20;
					$ev_status = &doEvent_Rcon(
						"BAD",
						"",
						"",
						$ev_obj_a
					);
				}
			} else {
				$ev_status = "(IGNORED) Rcon from \"$ev_obj_a:$ev_obj_b\": \"$ev_obj_c\"";
			}
		} elsif ($s_output =~ /^\[(.+)\.(smx|amxx)\]\s*(.+)$/i) {
			# Prototype: Cmd:[SM] obj_a
			# Matches:
		    # Admin Mod messages
			
			my $ev_plugin = $1;
			my $ev_adminmod = $2;
			$ev_obj_a  = $3;
			$ev_type = 500;
			$ev_status = &doEvent_Admin(
				(($ev_adminmod eq "smx")?"Sourcemod":"AMXX")." ($ev_plugin)",
				substr($ev_obj_a, 0, 255)
			);
		} elsif ($s_output =~ /^([^"\(]+) "([^"]+)"(.*)$/) {
			# Prototype: verb "obj_a"[properties]
			# Matches:
			# 13. World Objectives/Actions
			# 19. a) Loading map; b) Started map
			# 21. Server Name
			
			$ev_verb   = $1;
			$ev_obj_a  = $2;
			$ev_properties = $3;
			%ev_properties = &getProperties($ev_properties);
			
			if (like($ev_verb, "World triggered")) {
				$ev_type = 13;
				if ($ev_obj_a eq "killlocation") {
					$ev_status = &doEvent_Kill_Loc(
						%ev_properties
					);
				} else {
					$ev_status = &doEvent_WorldAction(
						$ev_obj_a
					);
					if ($ev_obj_a eq "Round_Win" || $ev_obj_a eq "Mini_Round_Win") {
						$ev_team = $ev_properties{"winner"};
						$ev_status = &doEvent_TeamAction(
						$ev_team,
						$ev_obj_a
						);
					}
				}
			} elsif (like($ev_verb, "Loading map")) {
				$ev_type = 19;
				$ev_status = &doEvent_ChangeMap(
					"loading",
					$ev_obj_a
				);
			} elsif (like($ev_verb, "Started map")) {
				$ev_type = 19;
				$ev_status = &doEvent_ChangeMap(
					"started",
					$ev_obj_a
				);
			}
		} elsif ($s_output =~ /^([^"\(]+):\s*"([^"]*)"$/) {
            # Matches Counter-Strike 2 line
            # 19. L 11/15/2023 - 18:54:31.849 - Started:  ""
            $ev_verb   = $1;
            $ev_obj_a  = $2;

            if (like($ev_verb, "Started")) {
                # leo - debug
                printEvent('[MAP]', "line: $s_output");

                $ev_type = 19;
                $ev_status = &doEvent_ChangeMap(
                    "started",
                    ""
                );
            }
		} elsif ($s_output =~ /^\[MANI_ADMIN_PLUGIN\]\s*(.+)$/) {
			# Prototype: [MANI_ADMIN_PLUGIN] obj_a
			# Matches:
		    # Mani-Admin-Plugin messages
			
			$ev_obj_a  = $1;
			$ev_type = 500;
			$ev_status = &doEvent_Admin(
				"Mani Admin Plugin",
				substr($ev_obj_a, 0, 255)
			);
		} elsif ($s_output =~ /^\[BeetlesMod\]\s*(.+)$/) {
			# Prototype: Cmd:[BeetlesMod] obj_a
			# Matches:
		    # Beetles Mod messages
			
			$ev_obj_a  = $1;
			$ev_type = 500;
			$ev_status = &doEvent_Admin(
				"Beetles Mod",
				substr($ev_obj_a, 0, 255)
			);
		} elsif ($s_output =~ /^\[ADMIN:(.+)\] ADMIN Command: \1 used command (.+)$/) {
			# Prototype: [ADMIN] obj_a
			# Matches:
		    # Admin Mod messages
			
			$ev_obj_a  = $1;
			$ev_obj_b  = $2;
			$ev_type = 500;
			$ev_status = &doEvent_Admin(
				"Admin Mod",
				substr($ev_obj_b, 0, 255),
				$ev_obj_a
			);
		} elsif ($g_servers{$s_addr}->{play_game} == DYSTOPIA()) {
				if ($s_output =~ /^weapon \{ steam_id: 'STEAM_\d+:(.+?)', weapon_id: (\d+), class: \d+, team: \d+, shots: \((\d+),(\d+)\), hits: \((\d+),(\d+)\), damage: \((\d+),(\d+)\), headshots: \((\d+),(\d+)\), kills: \(\d+,\d+\) \}$/ && $g_mode eq "Normal") {
			
				# Prototype: weapon { steam_id: 'STEAMID', weapon_id: X, class: X, team: X, shots: (X,X), hits: (X,X), damage: (X,X), headshots: (X,X), kills: (X,X) }
				# Matches:
				# 501. Statsme weaponstats (Dystopia)
		
				my $steamid = $1;
				my $weapon = $2;
				my $shots = $3 + $4;
				my $hits = $5 + $6;
				my $damage = $7 + $8;
				my $headshots = $9 + $10;
				my $kills = $11 + $12;
				
				$ev_type = 501;
				
				my $weapcode = $dysweaponcodes{$weapon};
				
				foreach $player (values(%g_players)) {
					if ($player->{uniqueid} eq $steamid) {
						$ev_status = &doEvent_Statsme(
							$player->{"userid"},
							$steamid,
							$weapcode,
							$shots,
							$hits,
							$headshots,
							$damage,
							$kills,
							0
						);
						last;
					}
				}
			} elsif ($s_output =~ /^(?:join|change)_class \{ steam_id: 'STEAM_\d+:(.+?)', .* (?:new_|)class: (\d+), .* \}$/ && $g_mode eq "Normal") {
				# Prototype: join_class { steam_id: 'STEAMID', team: X, class: Y, time: ZZZZZZZZZ }
				# Matches:
				#  6. Role Selection (Dystopia)
				
				my $steamid = $1;
				my $role = $2;
				$ev_type = 6;
				
				foreach $player (values(%g_players)) {
					if ($player->{uniqueid} eq $steamid) {
						$ev_status = &doEvent_RoleSelection(
							$player->{"userid"},
							$steamid,
							$role
						);
						last;
					}
				}
			} elsif ($s_output =~ /^objective \{ steam_id: 'STEAM_\d+:(.+?)', class: \d+, team: \d+, objective: '(.+?)', time: \d+ \}$/ && $g_mode eq "Normal") {
				# Prototype: objective { steam_id: 'STEAMID', class: X, team: X, objective: 'TEXT', time: X }
				# Matches:
				# 11. Player Action (Dystopia Objectives)
				
				my $steamid = $1;
				my $action = $2;
				foreach $player (values(%g_players)) {
					if ($player->{uniqueid} eq $steamid) {
						$ev_status = &doEvent_PlayerAction(
							$player->{"userid"},
							$steamid,
							$action
						);
						last;
					}
				}
			}
		}

		if ($ev_type) {
			if ($g_debug > 2) {
				print <<EOT
					type   = "$ev_type"
					team   = "$ev_team"
					player = "$ev_player"
					verb   = "$ev_verb"
					obj_a  = "$ev_obj_a"
					obj_b  = "$ev_obj_b"
					obj_c  = "$ev_obj_c"
					properties = "$ev_properties"
EOT
;
				while (my($key, $value) = each(%ev_properties)) {
					print "property: \"$key\" = \"$value\"\n";
				}
				
				while (my($key, $value) = each(%ev_player)) {
					print "player $key = \"$value\"\n";
				}
			}
			
			if ($ev_status ne "") {
				&printEvent($ev_type, $ev_status);
			} else {
				&printEvent($ev_type, "BAD DATA: $s_output");
			}
		} elsif (($s_output =~ /^Banid: "(.+?(?:<.+?>)*)" was (?:kicked and )?banned "for ([0-9]+).00 minutes" by "Console"$/) ||
				($s_output =~ /^Banid: "(.+?(?:<.+?>)*)" was (?:kicked and )?banned "(permanently)" by "Console"$/)) {
			
			# Prototype: "player" verb[properties]
    		# Banid: huaaa<1804><STEAM_0:1:10769><>" was kicked and banned "permanently" by "Console"
			
			$ev_player  = $1;
			$ev_bantime = $2;
			my $playerinfo = &getPlayerInfo($ev_player, 1);

			if ($ev_bantime eq "5") {
				&printEvent("BAN", "Auto Ban - ignored");
			} elsif ($playerinfo) {
				if (($g_global_banning > 0) && ($g_servers{$s_addr}->{ignore_nextban}->{$playerinfo->{"uniqueid"}} == 1)) {
					delete($g_servers{$s_addr}->{ignore_nextban}->{$playerinfo->{"uniqueid"}});
					&printEvent("BAN", "Global Ban - ignored");
				} elsif (!$g_servers{$s_addr}->{ignore_nextban}->{$playerinfo->{"uniqueid"}}) {
					my $p_steamid  = $playerinfo->{"uniqueid"};
					my $player_obj = lookupPlayer($s_addr, $playerId, $p_steamid);
					&printEvent("BAN", "Steamid: ".$p_steamid);

					if ($player_obj) {
						$player_obj->{"is_banned"} = 1;
					}  
					if (($p_steamid ne "") && ($playerinfo->{"is_bot"} == 0) && ($playerinfo->{"userid"} > 0)) {
						if ($g_global_banning > 0) {
							if ($ev_bantime eq "permanently") {
								&printEvent("BAN", "Hide player!");
								&execNonQuery("UPDATE hlstats_Players SET hideranking=2 WHERE playerId IN (SELECT playerId FROM hlstats_PlayerUniqueIds WHERE uniqueId='".&quoteSQL($p_steamid)."')");
								$ev_bantime = 0;
							}
							my $pl_steamid  = $playerinfo->{"plain_uniqueid"};
							while (my($addr, $server) = each(%g_servers)) {
								if ($addr ne $s_addr) {
									&printEvent("BAN", "Global banning on ".$addr);
									$server->{ignore_nextban}->{$p_steamid} = 1;
									$server->dorcon("banid ".$ev_bantime." $pl_steamid");
									$server->dorcon("writeid");
								}  
							}
						} 
					}  
				}  
			} else {
				&printEvent("BAN", "No playerinfo");
			}
			&printEvent("BAN", $s_output);
		} else {
			# Unrecognized event
			# HELLRAISER
			if ($g_debug > 1) {
				&printEvent(999, "UNRECOGNIZED: " . $s_output);
			}
		}
		
		if (!$g_stdin && defined($g_servers{$s_addr}) && $ev_daemontime > $g_servers{$s_addr}->{next_plyr_flush}) {
			&printEvent("MYSQL", "Flushing player updates to database...",1);
			if ($g_servers{$s_addr}->{"srv_players"}) {
				while ( my($pl, $player) = each(%{$g_servers{$s_addr}->{"srv_players"}}) ) {
					if ($player->{needsupdate}) {
						$player->flushDB();
					}
				}
			}
			&printEvent("MYSQL", "Flushing player updates to database is complete.",1);
			
			$g_servers{$s_addr}->{next_plyr_flush} = $ev_daemontime + 15+int(rand(15));
		}

		if (($g_stdin == 0) && defined($g_servers{$s_addr})) {
			$s_lines = $g_servers{$s_addr}->{lines};
			# get ping from players
			if ($s_lines % 1000 == 0) {
				$g_servers{$s_addr}->update_players_pings();
			}

			if ($g_servers{$s_addr}->{show_stats} == 1) {
				# show stats
				if ($s_lines % 2500 == 40) {
					$g_servers{$s_addr}->dostats();
				}
			}
		
			if ($s_lines > 500000)	{
				$g_servers{$s_addr}->set("lines", 0);	
			}  else	{
				$g_servers{$s_addr}->increment("lines");	
			} 
		}
	} else {
		$s_addr = "";
	}

	while( my($server) = each(%g_servers))
	{	
		if($g_servers{$server}->{next_timeout}<$ev_daemontime)
		{
			#print "checking $ev_unixtime\n";
			# Clean up
			# look
			if($g_servers{$server}->{"srv_players"})
			{
				my %players_temp=%{$g_servers{$server}->{"srv_players"}};
				while ( my($pl, $player) = each(%players_temp) ) {
					my $timeout = 250; # 250;
					if ($g_mode eq "LAN")  {
						$timeout = $timeout * 2;
					}
					#if (($player->{is_bot} == 0) && (($ev_unixtime - $player->get("timestamp")) > ($timeout - 20))) {
					#	&printEvent(400, $player->getInfoString()." is ".($ev_unixtime - $player->get("timestamp"))."s idle");
					#}  
					my $userid = $player->{userid};
					my $uniqueid = $player->{uniqueid};
					if ( ($ev_daemontime - $player->{timestamp}) > $timeout ) {
						#printf("%s - %s %s\n",$server, $player->{userid}, $player->{uniqueid});
						# we delete any player who is inactive for over $timeout sec
						# - they probably disconnected silently somehow.
						if (($player->{is_bot} == 0) || ($g_stdin)) {
							&printEvent(400, "Auto-disconnecting " . $player->getInfoString() ." for idling (" . ($ev_daemontime - $player->{timestamp}) . " sec) on server (".$server.")");
							removePlayer($server, $userid, $uniqueid);
						}
					}
				}
			}
			$g_servers{$server}->{next_timeout}=$ev_daemontime+30+rand(30);
		}
		
		if ($ev_daemontime > $g_servers{$server}->{next_flush}
			&& $g_servers{$server}->{needsupdate}
			)
		{
			$g_servers{$server}->flushDB();
			$g_servers{$server}->{next_flush} = $ev_daemontime + 20;
		}
	}

	while ( my($pl, $player) = each(%g_preconnect) ) {
		my $timeout = 600;
		if ( ($ev_unixtime - $player->{"timestamp"}) > $timeout ) {
			&printEvent(401, "Clearing pre-connect entry with key ".$pl);
			delete($g_preconnect{$pl});
		}
	}
	
	if ($g_stdin == 0) {
		# Track the Trend
		if ($g_track_stats_trend > 0) {
			track_hlstats_trend();
		}  
		while (my($addr, $server) = each(%g_servers)) {
			if (defined($server)) {
				$server->track_server_load();
			}
		}
		
		while( my($table) = each(%g_eventtable_data))
		{
			if ($g_eventtable_data{$table}{lastflush} + 30 < $ev_daemontime)
			{
				flushEventTable($table);
			}
		}
		
		if (defined($g_servers{$s_addr})) {
			if (($g_servers{$s_addr}->{map} eq "") && (($timeout > 0) && ($timeout % 60 == 0))) {
				$g_servers{$s_addr}->get_map();
			}
		}
	}  
	
	$c++;
	$c = 1 if ($c > 500000);
	$import_logs_count++ if ($g_stdin);
}

$end_time = time();
if ($g_stdin) {
	if ($import_logs_count > 0) {
		print "\n";
	}
	
	&flushAll(1);
	&execNonQuery("UPDATE hlstats_Players SET last_event=UNIX_TIMESTAMP();");
	&printEvent("IMPORT", "Import of log file complete. Scanned ".$import_logs_count." lines in ".($end_time-$start_time)." seconds", 1, 1);
}

sub INT_handler
{
	print "SIGINT received. Flushing data and shutting down...\n";
	flushAll(1);
	exit(0);
}

sub HUP_handler
{
	print "SIGHUP received. Flushing data and reloading configuration...\n";
	&reloadConfiguration;
}
