package HLstats_Server;
# 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 POSIX;
use IO::Socket;
use Socket;
use Encode;
use Syntax::Keyword::Try;

do "$::opt_libdir/HLstats_GameConstants.plib";

sub new
{
	my ($class_name, $serverId, $address, $port, $server_name, $rcon_pass, $game, $publicaddress, $gameengine, $realgame, $maxplayers) = @_;
	
	my ($self) = {};
	
	bless($self, $class_name);
	
	$self->{id}             = $serverId;
	$self->{address}        = $address;
	$self->{port}           = $port;
	$self->{game}           = $game;
	$self->{rcon}           = $rcon_pass;
	$self->{srv_players}	= ();
	
	# Game Engine
	# HL1 - 1
	# HL2 (original) - 2
	# HL2ep2 ("OrangeBox") - 3
	$self->{game_engine}	= $gameengine;
	
    $self->{rcon_obj}	    = undef;
    $self->{name}           = $server_name;
    $self->{auto_ban}       = 0;
    $self->{contact}        = "";
    $self->{hlstats_url}    = "";
    $self->{publicaddress}  = $publicaddress;
	$self->{play_game}      = -1;
	
	$self->{last_event}     = 0;
	$self->{last_check}     = 0;
    
	$self->{lines}         = 0;
	$self->{map}           = "";
	$self->{numplayers}    = 0;
	$self->{num_trackable_players} = 0;
	$self->{minplayers}    = 6;
	$self->{maxplayers}    = $maxplayers;
	$self->{difficulty}    = 0;
	
    $self->{players}       = 0;
 	$self->{rounds}        = 0;
	$self->{kills}         = 0;
	$self->{suicides}      = 0;
	$self->{headshots}     = 0;
	$self->{ct_shots}      = 0;
	$self->{ct_hits}       = 0;
	$self->{ts_shots}      = 0;
	$self->{ts_hits}       = 0;
	$self->{bombs_planted} = 0;
	$self->{bombs_defused} = 0;
	$self->{ct_wins}       = 0;
	$self->{ts_wins}       = 0; 
	$self->{map_started}   = time();
	$self->{map_changes}   = 0;
	$self->{map_rounds}    = 0;
	$self->{map_ct_wins}   = 0;
	$self->{map_ts_wins}   = 0; 
	$self->{map_ct_shots}  = 0;
	$self->{map_ct_hits}   = 0;
	$self->{map_ts_shots}  = 0; 
	$self->{map_ts_hits}   = 0;

	# team balancer
	$self->{ba_enabled}       = 0;
	$self->{ba_ct_wins}       = 0;
	$self->{ba_ts_win}        = 0;
	$self->{ba_ct_frags}      = 0;
	$self->{ba_ts_frags}      = 0;
	$self->{ba_winner}        = ();
	$self->{ba_map_rounds}    = 0;
	$self->{ba_last_swap}     = 0;
	$self->{ba_player_switch} = 0;  # player switched on his own
	
	# Messaging commands
	$self->{show_stats}                  	= 0;
	$self->{broadcasting_events}         	= 0;
	$self->{broadcasting_player_actions} 	= 0;
	$self->{broadcasting_command}        	= "";
  	$self->{broadcasting_command_announce}	= "say";
	$self->{player_events}               	= 1; 
 	$self->{player_command}              	= "say";
 	$self->{player_command_osd}          	= "";
	$self->{player_command_hint}			= "";
	$self->{player_admin_command}        	= 0;
	$self->{default_display_events}			= 1;
	$self->{browse_command} 				= "";
	$self->{swap_command}					= "";
	$self->{exec_command}					= "";
	$self->{global_chat_command}			= "say";
	
	# Message format operators
	$self->{format_color}		= "";
	$self->{format_action}		= "";
	$self->{format_actionend}	= "";
	
	$self->{total_kills}                   = 0;
	$self->{total_headshots}               = 0;
	$self->{total_suicides}                = 0;
	$self->{total_rounds}                  = 0;
	$self->{total_shots}                   = 0;
	$self->{total_hits}                    = 0;

	$self->{track_server_load}             = 0;
	$self->{track_server_timestamp}        = 0;
	
	$self->{ignore_nextban}                = ();
	$self->{use_browser}                   = 0;
	$self->{round_status}                  = 0;
	$self->{min_players_rank}              = 1;
	$self->{admins}                        = ();
	$self->{ignore_bots}                   = 1;
	$self->{tk_penalty}                    = 0;
	$self->{suicide_penalty}               = 0;
	$self->{skill_mode}                    = 0;
	$self->{game_type}                     = 0;
    $self->{bonusroundignore}              = 0;
    $self->{bonusroundtime}                = 0;
    $self->{bonusroundtime_ts}             = 0;
    $self->{bonusroundtime_state}          = 0;
	$self->{lastdisabledbonus}			   = $::ev_unixtime;
	$self->{mod}                           = "";
	$self->{switch_admins}                 = 0;
	$self->{public_commands}               = 1;
	$self->{connect_announce}			   = 0;
	$self->{update_hostname}			   = 0;
	
	$self->{lastblueflagdefend}			   = 0;
	$self->{lastredflagdefend}			   = 0;
	
	# location hax
	$self->{nextkillx}					= "";
	$self->{nextkilly}					= "";
	$self->{nextkillz}					= "";
	$self->{nextkillvicx}					= "";
	$self->{nextkillvicy}					= "";
	$self->{nextkillvicz}					= "";
	
	$self->{nextkillheadshot}			= 0;
	
	$self->{next_timeout} = 0;
	$self->{next_flush} = 0;
	$self->{next_plyr_flush} = 0;
	$self->{needsupdate} = 0;
	
	$self->set_play_game($realgame);
	
	if ($self->{rcon})
	{
		$self->init_rcon();
	}
	
	$self->updateDB();
	$self->update_server_loc();

	return $self;
}

sub set_play_game
{
	my ($self, $realgame) = @_;
	
	if (exists($gamecode_to_game{$realgame}))
	{
		$self->{play_game} = $gamecode_to_game{$realgame};
	}
}

sub is_admin
{
	my($self, $steam_id) = @_;
	for (@{$self->{admins}}) {
		if ($_ eq $steam_id) {
			return 1;
		}
	}
	return 0;
}

sub get_game_mod_opts
{
	# Runs immediately after server object is created and options are loaded.
	my($self) = @_;

	if ($self->{mod} ne "") {
		my $mod = $self->{mod};
			
		if ($mod eq "SOURCEMOD") {
			$self->{browse_command} = "hlx_sm_browse";
			$self->{swap_command} = "hlx_sm_swap";
			$self->{global_chat_command} = "hlx_sm_psay";
			$self->setHlxCvars();
		} elsif ($mod eq "MANI") {
			$self->{browse_command} = "ma_hlx_browse";
			$self->{swap_command} = "ma_swapteam";
			$self->{exec_command} = "ma_cexec";
			$self->{global_chat_command} = "ma_psay";
		} elsif ($mod eq "AMXX") {
			$self->{browse_command} = "hlx_amx_browse";
			$self->{swap_command} = "hlx_amx_swap";
			$self->{global_chat_command} = "hlx_amx_psay";
			$self->setHlxCvars();
		} elsif ($mod eq "BEETLE") {
			$self->{browse_command} = "hlx_browse";
			$self->{swap_command} = "hlx_swap";
			$self->{exec_command} = "hlx_exec";
			$self->{global_chat_command} = "admin_psay";
		} elsif ($mod eq "MINISTATS") {
			$self->{browse_command} = "ms_browse";
			$self->{swap_command} = "ms_swap";
			$self->{global_chat_command} = "ms_psay";
		}
		
		# Turn on color and add game-specific color modifiers for when using hlx:ce sourcemod plugin
		if (($self->{mod} eq "SOURCEMOD" &&
				(
				$self->{play_game} == CSS()
				|| $self->{play_game} == TF()
				|| $self->{play_game} == L4D()
				|| $self->{play_game} == DODS()
				|| $self->{play_game} == HL2MP()
				|| $self->{play_game} == AOC()
				|| $self->{play_game} == ZPS()
				|| $self->{play_game} == FF()
				|| $self->{play_game} == GES()
				|| $self->{play_game} == FOF()
				|| $self->{play_game} == PVKII()
				|| $self->{play_game} == CSP()
				|| $self->{play_game} == NUCLEARDAWN()
				|| $self->{play_game} == DDD()
				)
			)
			|| ($self->{mod} eq "AMXX"
				&& $self->{play_game} == CSTRIKE())
		) {
	
			$self->{format_color} = " 1";
			if ($self->{play_game} == ZPS() || $self->{play_game} == GES()) {
				$self->{format_action} = "\x05";
			} elsif ($self->{play_game} == FF()) {
				$self->{format_action} = "^4";
			} else {
				$self->{format_action} = "\x04";
			}
			
			if ($self->{play_game} == FF()) {
				$self->{format_actionend} = "^0";
			} else {
				$self->{format_actionend} = "\x01";
			}
		}
		# Insurgency can only do one solid color afaik. The rest is handled in the plugin
		if ($self->{mod} eq "SOURCEMOD" && $self->{play_game} == INSMOD()) {
			$self->{format_color} = " 1";
		}
	}
}

sub format_userid {
	my($self, $userid) = @_;
	if ($self->{mod} eq "AMXX") {
		return "#".$userid;
	}
	return "\"".$userid."\"";
}

sub quoteparam {
	my($self, $message) = @_;
	$message =~ s/'/ ' /g;
	$message =~ s/"/ '' /g;
	
	if (($self->{game_engine} != 2 || $self->{mod} eq "SOURCEMOD") && $self->{mod} ne "MANI") {
		return "\"".$message."\"";
	}
	return $message;
}

#
# Set property 'key' to 'value'
#

sub set
{
	my ($self, $key, $value) = @_;
	
	if (defined($self->{$key}))
	{
		if ($self->{$key} eq $value)
		{
			if ($::g_debug > 2)
			{
				&::printNotice("Hlstats_Server->set ignored: Value of \"$key\" is already \"$value\"");
			}
			return 0;
		}
		
		$self->{$key} = $value;
		
		if ($key eq "hlstats_url")  {
        	# so ingame browsing will work correctly
            $self->{ingame_url}  = $value;
            $self->{ingame_url}  =~ s/\/hlstats.php//i;
			$self->{ingame_url}  =~ s/\/$//;
         	&::printEvent("SERVER", "Ingame-URL: ".$self->{ingame_url}, 1);
		}
		return 1;
	}
	else
	{
		warn("HLstats_Server->set: \"$key\" is not a valid property name\n");
		return 0;
	}
}


#
# Increment (or decrement) the value of 'key' by 'amount' (or 1 by default)
#

sub increment
{
	my ($self, $key, $amount) = @_;
	if ($amount) {
		$amount = int($amount);
	} else {
		$amount = 1
	}
	
	my $value = $self->{$key};
	$self->set($key, $value + $amount);
}


sub init_rcon
{
	my ($self)      = @_;
	my $server_ip   = $self->{address};
	my $server_port = $self->{port};
	my $rcon_pass   = $self->{rcon};
	my $game        = $self->{game};

	if ($::g_rcon && $rcon_pass) {
		if ($self->{game_engine} == 1) {
			$self->{rcon_obj} = new BASTARDrcon($self);
		} else {
			$self->{rcon_obj} = new TRcon($self);
		}
	}
   	if ($self->{rcon_obj}) {
		&::printEvent ("SERVER", "Connecting to rcon on $server_ip:$server_port ... ok");
		#&::printEvent("SERVER", "Server running game: ".$self->{play_game}, 1);
		&::printEvent("SERVER", "Server running map: ".$self->get_map(), 1);
		if ($::g_mode eq "LAN") {
			$self->get_lan_players();
		}
	}
}

sub dorcon
{
	my ($self, $command)      = @_;
    my $result;
    my $rcon_obj = $self->{rcon_obj};
	if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) {
	 
		# replace ; to avoid executing multiple rcon commands.
		$command  =~ s/;//g;
	
		&::printNotice("RCON", $command, 1);
		$result = $rcon_obj->execute($command);
    } else {
		&::printNotice("Rcon error: No Object available");
    }
    return $result;
}

sub dorcon_multi
{
	my ($self, @commands)      = @_;
    my $result;
    my $rcon_obj = $self->{rcon_obj};
	if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) {
		if ($self->{game_engine} > 1)
		{
			my $fullcmd = "";
			foreach (@commands)
			{
				# replace ; to avoid executing multiple rcon commands.
				my $cmd = $_;
				$cmd =~ s/;//g;
				$fullcmd .="$cmd;";
			}
			&::printNotice("RCON", $fullcmd, 1);
			$result = $rcon_obj->execute($fullcmd);
		}
		else
		{
			foreach (@commands)
			{
				&::printNotice("RCON", $_, 1);
				$result = $rcon_obj->execute($_);
			}
		}
    } else {
      &::printNotice("Rcon error: No Object available");
    }
    return $result;
}	

sub rcon_getaddress
{
	my ($self, $uniqueid) = @_;
	my $result;
	my $rcon_obj = $self->{rcon_obj};
	if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne ""))
	{
		$result = $rcon_obj->getPlayer($uniqueid);
	}
	else
	{
    	 	 &::printNotice("Rcon error: No Object available");
	}
	return $result;
}

sub rcon_getStatus
{
	my ($self) = @_;
    my $rcon_obj = $self->{rcon_obj};
    my $map_result = "";
    my $max_player_result = -1;
	my $servhostname = "";
	my $difficulty = 0;
	
	if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) {
		($servhostname, $map_result, $max_player_result, $difficulty)    = $rcon_obj->getServerData();
		($visible_maxplayers)                = $rcon_obj->getVisiblePlayers();
		if (($visible_maxplayers != -1) && ($visible_maxplayers < $max_player_result)) {
			$max_player_result = $visible_maxplayers;
		}
	} else {
		&::printNotice("Rcon error: No Object available");
    }
    return ($map_result, $max_player_result, $servhostname, $difficulty);
}

sub rcon_getplayers
{
	my ($self) = @_;
	my %result;
	my $rcon_obj = $self->{rcon_obj};
	if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne ""))
	{
		%result = $rcon_obj->getPlayers();
	} else {
	&::printNotice("Rcon error: No Object available");
	}
	return %result;
}

sub track_server_load
{
	my ($self) = @_;

	if (($::g_stdin == 0) && ($self->{track_server_load} > 0))
	{
		my $last_timestamp = $self->{track_server_timestamp};
		my $new_timestamp  = time();
		if ($last_timestamp > 0)
		{
			if ($last_timestamp+299 < $new_timestamp)
			{
				# fetch fps and uptime via rcon
				
				# Old style stats output:
				#$string = "          0.00  0.00  0.00      54     1  249.81       0 dhjdsk";

				# New style stats output:
				#CPU    In (KB/s)  Out (KB/s)  Uptime  Map changes  FPS      Players  Connects
				#0.00   0.00       0.00        0      0            00.00    0        0


				$string = $self->dorcon("stats");

				# Remove first line of output
				$string =~ /CPU.*\n(.*)\n*L{0,1}.*\Z/;
				$string = $1;

				# Grab FPS and Uptime from the output
				$string =~ /([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s*([^ ]*)/;
				$uptime = $4;
				$fps = $6;

				my $act_players  = $self->{numplayers};
				my $max_players  = $self->{maxplayers};
				if ($max_players > 0) {
					if ($act_players > $max_players)  {
						$act_players = $max_players;
					}
				}
				&::execCached("flush_server_load",
					"INSERT IGNORE INTO hlstats_server_load
						SET 
							server_id=?,
							timestamp=?,
							act_players=?,
							min_players=?,
							max_players=?,
							map=?,
							uptime=?,
							fps=?",
					$self->{id},
					$new_timestamp,
					$act_players,
					$self->{minplayers},
					$max_players,
					$self->{map},
					(($uptime)?$uptime:0),
					(($fps)?$fps:0)
				);
				$self->set("track_server_timestamp", $new_timestamp);
				&::printEvent("SERVER", "Insert new server load timestamp", 1);
			}   
		} else {
			$self->set("track_server_timestamp", $new_timestamp);
		}  
	}
}

sub dostats 
{
	my ($self) = @_;
	my $rcon_obj = $self->{rcon_obj};
	$rcmd = $self->{broadcasting_command_announce};

	if (($::g_stdin == 0) && ($rcon_obj) && ($self->{rcon} ne ""))  
	{
		if ($self->{broadcasting_events} == 1)
		{
			my $hpk = sprintf("%.0f", 0);
			if ($self->{total_kills} > 0) {
				$hpk = sprintf("%.2f", (100/$self->{total_kills})*$self->{total_headshots});
			}  
			if ($rcmd ne "") {
				$self->dorcon("$rcmd ".$self->quoteparam("HLstatsX:CE - Tracking ".&::number_format($self->{players})." players with ".&::number_format($self->{total_kills})." kills and ".&::number_format($self->{total_headshots})." headshots ($hpk%)"));
			} else {
				$self->messageAll("HLstatsX:CE - Tracking ".&::number_format($self->{players})." players with ".&::number_format($self->{total_kills})." kills and ".&::number_format($self->{total_headshots})." headshots ($hpk%)");
			}  
		}  
	}  
}	  

sub get_map
{
	my ($self, $fromupdate) = @_;

	if ($::g_stdin == 0) {
		if ((time() - $self->{last_check})>120) {
			$self->{last_check} = time();
			&::printNotice("get_rcon_status");
			my $temp_map        = "";
			my $temp_maxplayers = -1;
			my $servhostname	  = "";
			my $difficulty      = 0;
			my $update		  = 0;
			
			if ($self->{rcon_obj})
			{
				($temp_map, $temp_maxplayers, $servhostname, $difficulty) = $self->rcon_getStatus();
				
				if ($temp_map eq "") {
					goto STATUSFAIL;
				}
				
				if ($self->{map} ne $temp_map) {
					$self->{map} = $temp_map;
					$update++;
				}
			
				if (($temp_maxplayers != -1) && ($temp_maxplayers > 0) && ($temp_maxplayers ne "")) {
					if ($self->{maxplayers} != $temp_maxplayers) {
						$self->{maxplayers} = $temp_maxplayers;
						$update++;
					}
				}
				if (($difficulty > 0) && ($self->{play_game} == L4D())) {
					$self->{difficulty} = $difficulty;
				}
				if (($self->{update_hostname} > 0) && ($self->{name} ne $servhostname) && ($servhostname ne "")) {
						$self->{name} = $servhostname;
						$update++;
				}
			}
			else
			{  # no rcon or status command failed
			STATUSFAIL:
				my ($querymap, $queryhost, $querymax) = &::queryServer($self->{address}, $self->{port}, 'mapname', 'hostname', 'maxplayers');
				if ($querymap ne "") {
					$self->{map} = $querymap;
					$update++;
					
					#if map is blank, query likely failed as a whole
					
					if (($querymax != -1) && ($querymax > 0)) {
						if ($self->{maxplayers} != $querymax) {
							$self->{maxplayers} = $querymax;
							$update++;
						}
					}
					if ($self->{update_hostname} > 0 && $queryhost ne "" && $self->{name} ne $queryhost) {
						$self->{name} = $queryhost;
						$update++;
					}
				}
			}
				
			if ($update > 0 && $fromupdate == 0) {
				$self->updateDB();
			}
		  
			&::printNotice("get_rcon_status successfully");
		}
	}  
	return $self->{map};
}


sub update_players_pings
{
	my ($self) = @_;

	if ($self->{num_trackable_players} < $self->{minplayers}) 
	{
		&::printNotice("(IGNORED) NOTMINPLAYERS: Update_player_pings");
	}
	else
	{
		&::printNotice("update_player_pings");
		&::printEvent("RCON", "Update Player pings", 1);
		my %players = $self->rcon_getplayers();
		while ( my($pl, $player) = each(%{$self->{srv_players}}) )
		{
			my $uniqueid = $player->{uniqueid};
			if (defined($players{$uniqueid})) 
			{
				if ($player->{is_bot} == 0 && $player->{userid} > 0) {
					my $ping = $players{$uniqueid}->{"Ping"};
					$player->set("ping", $ping);
					if ($ping > 0) {
						&::recordEvent(
							"Latency", 0,
							$player->{playerid},
							$ping
						);
					}
				}
			}
		}
		&::printNotice("update_player_pings successfully");
	}
}

sub get_lan_players
{
	my ($self) = @_;

	if ($::g_mode eq "LAN")  {
		&::printNotice("get_lan_players");
		&::printEvent("RCON", "Get LAN players", 1);
		my %players = $self->rcon_getplayers();
		while ( my($p_uid, $p_obj) = each(%players) )
		{	
			my $srv_addr = $self->{address}.":".$self->{port};
			my $userid   = $p_obj->{"UserID"};
			my $name     = $p_obj->{"Name"};
			my $address  = $p_obj->{"Address"};
			::g_lan_noplayerinfo->{"$srv_addr/$userid/$name"} = {
				ipaddress => $address,
				userid => $userid,
				name => $name,
				server => $srv_addr
			};
		}
		&::printNotice("get_lan_players successfully");
	}
}

sub clear_winner
{
  my ($self) = @_;
  &::printNotice("clear_winner");
  @{$self->{winner}} = ();
}

sub add_round_winner
{
	my ($self, $team) = @_;
  
	&::printNotice("add_round_winner");
	$self->{winner}[($self->{map_rounds} % 7)] = $team;
	$self->increment("ba_map_rounds");
	$self->increment("map_rounds");
	$self->increment("rounds");
	$self->increment("total_rounds");
  
	$self->{ba_ct_wins} = 0;
	$self->{ba_ts_wins} = 0;
  
	for (@{$self->{winner}}) 
	{
		if ($_ eq "ct") {
			$self->increment("ba_ct_wins");
		} elsif ($_ eq "ts") {
			$self->increment("ba_ts_wins");
		}
	}
}

sub switch_player
{
	my ($self, $playerid, $name) = @_;
	my $rcmd = $self->{player_command_hint};
	
	$self->dorcon($self->{swap_command}." ".$self->format_userid($playerid));
	if ($self->{player_command_hint} eq "") {
		$rcmd = $self->{player_command};
	}
	$self->dorcon(sprintf("%s %s %s", $rcmd, $self->format_userid($playerid), $self->quoteparam("HLstatsX:CE - You were switched to balance teams")));
	if ($self->{player_admin_command} ne "") {
		$self->dorcon(sprintf("%s %s",$self->{player_admin_command}, $self->quoteparam("HLstatsX:CE - $name was switched to balance teams")));
	}
}


sub analyze_teams
{
	my ($self) = @_;
  
	if (($::g_stdin == 0) && ($self->{num_trackable_players} < $self->{minplayers})) 
	{
		&::printNotice("(IGNORED) NOTMINPLAYERS: analyze_teams");
	}
	elsif (($::g_stdin == 0) && ($self->{ba_enabled} > 0))
	{
		&::printNotice("analyze_teams");
		my $ts_skill     = 0;
		my $ts_avg_skill = 0;
		my $ts_count     = 0;
		my $ts_wins      = $self->{ba_ts_wins};
		my $ts_kills     = 0;
		my $ts_deaths    = 0;
		my $ts_diff      = 0;
		my @ts_players   = ();

		my $ct_skill     = 0;
		my $ct_avg_skill = 0;
		my $ct_count     = 0;
		my $ct_wins      = $self->{ba_ct_wins};
		my $ct_kills     = 0;
		my $ct_deaths    = 0;
		my $ct_diff      = 0;
		my @ct_players   = ();
		
		my $server_id   = $self->{id};
		while ( my($pl, $player) = each(%{$self->{srv_players}}) )
		{	
			my @Player      = ( $player->{name},			#0
								$player->{uniqueid},		#1
								$player->{skill},   		#2
								$player->{team},			#3
								$player->{map_kills},		#4
								$player->{map_deaths},		#5
								($player->{map_kills}-$player->{map_deaths}), #6
								$player->{is_dead},			#7
								$player->{userid},			#8
								);
    
			if ($Player[3] eq "TERRORIST")
			{
				push(@{$ts_players[$ts_count]}, @Player);
				$ts_skill   += $Player[2]; 
				$ts_count   += 1;
				$ts_kills   += $Player[4];
				$ts_deaths  += $Player[5];
			}
			elsif ($Player[3] eq "CT")
			{
				push(@{$ct_players[$ct_count]}, @Player);
				$ct_skill   += $Player[2]; 
				$ct_count   += 1;
				$ct_kills   += $Player[4]; 
				$ct_deaths  += $Player[5]; 
			}
		}
		@ct_players = sort { $b->[6] <=> $a->[6]} @ct_players;
		@ts_players = sort { $b->[6] <=> $a->[6]} @ts_players;
    
		&::printEvent("TEAM", "Checking Teams", 1);
		$admin_msg = "AUTO-TEAM BALANCER: CT ($ct_count) $ct_kills:$ct_deaths  [$ct_wins - $ts_wins] $ts_kills:$ts_deaths ($ts_count) TS";
		if ($self->{player_events} == 1)  
		{
			if ($self->{player_admin_command} ne "") {
				$cmd_str = $self->{player_admin_command}." $admin_msg";
				$self->dorcon($cmd_str);
			}  
		}
    
		$self->messageAll("HLstatsX:CE - ATB - Checking Teams", 0, 1);

		if ($self->{ba_map_rounds} >= 2)    # need all players for numerical balacing, at least 2 for getting all players
		{
			my $action_done = 0;
			if ($self->{ba_last_swap} > 0)
			{
				$self->{ba_last_swap}--;
			}
      
			if ($ct_count + 1 < $ts_count)         # ct need players
			{
				$needed_players = floor( ($ts_count - $ct_count) / 2);
				if ($ct_wins < 2)
				{
					@ts_players = sort { $b->[7] <=> $a->[7]} @ts_players;
				}
				else
				{
					@ts_players = sort { $a->[7] <=> $b->[7]} @ts_players;
				}
				foreach my $entry (@ts_players) 
				{
					if ($needed_players > 0) # how many we need to make teams even (only numerical)
					{
						if (@{$entry}[7] == 1)  # only dead players!!
						{
							if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
								$self->switch_player(@{$entry}[8], @{$entry}[0]); 
								$action_done++;
								$needed_players--;
							}
						}
					}
				}
			}
			elsif  ($ts_count + 1 < $ct_count)  # ts need players
			{
				$needed_players = floor( ($ct_count - $ts_count) / 2);
				if ($ts_wins < 2)
				{
					@ct_players = sort { $b->[6] <=> $a->[6]} @ct_players;  # best player
				}
				else
				{
					@ct_players = sort { $a->[6] <=> $b->[6]} @ct_players;  # worst player
				}
				foreach my $entry (@ct_players) 
				{
					if ($needed_players > 0) # how many we need to make teams even (only numerical)
					{
						if (@{$entry}[7] == 1)  # only dead players!!
						{
							if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
								$self->switch_player(@{$entry}[8], @{$entry}[0]); 
								$action_done++;
								$needed_players--;
							}
						}
					}
				}
			}
      
			if (($action_done == 0) && ($self->{ba_last_swap} == 0) && ($self->{ba_map_rounds} >= 7) && ($self->{ba_player_switch} == 0)) # frags balancing (last swap 3 rounds before)
			{
				if ($ct_wins < 2)
				{
					if ($ct_count < $ts_count)     # one player less we dont need swap just bring one over
					{
						my $ts_found = 0;
						@ts_players = sort { $b->[6] <=> $a->[6]} @ts_players;  # best player
						foreach my $entry (@ts_players) 
						{
							if ($ts_found == 0)
							{
								if (@{$entry}[7] == 1)  # only dead players!!
								{
									if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
										$self->{ba_last_swap} = 3;
										$self->switch_player(@{$entry}[9], @{$entry}[0]); 
										$ts_found++;
									}
								}
							}
						}
					}
					else                  # need to swap to players
					{
						my $ts_playerid = 0;
						my $ts_name     = "";
						my $ts_kills    = 0;
						my $ts_deaths   = 0;
						my $ct_playerid = 0;
						my $ct_name     = "";
						my $ct_kills    = 0;
						my $ct_deaths   = 0;
						my $ts_found = 0;
						@ts_players = sort { $b->[6] <=> $a->[6]} @ts_players;  # best player
						foreach my $entry (@ts_players) 
						{
							if ($ts_found == 0)
							{
								if (@{$entry}[7] == 1)  # only dead players!!
								{
									if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
										$ts_playerid = @{$entry}[8];
										$ts_name     = @{$entry}[0];
										$ts_found++;
									}
								}
							}
						}

						my $ct_found = 0;
						@ct_players = sort { $a->[6] <=> $b->[6]} @ct_players;  # worst player
						foreach my $entry (@ct_players) 
						{
							if ($ct_found == 0)
							{
								if (@{$entry}[7] == 1)  # only dead players!!
								{
									if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
										$ct_playerid = @{$entry}[8];
										$ct_name     = @{$entry}[0];
										$ct_found++;
									}
								}
							}
						}
						if (($ts_found>0) && ($ct_found>0))
						{
							$self->{ba_last_swap} = 3;
							$self->switch_player($ts_playerid, $ts_name); 
							$self->switch_player($ct_playerid, $ct_name); 
						}
					}
				}
				elsif ($ts_wins < 2)
				{
					if ($ts_count < $ct_count)     # one player less we dont need swap just bring one over
					{
						my $ct_found = 0;
						@ct_players = sort { $b->[6] <=> $a->[6]} @ct_players;  # best player
						foreach my $entry (@ct_players) 
						{
							if ($ct_found == 0)
							{
								if (@{$entry}[7] == 1)  # only dead players!!
								{
									if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
										$self->{ba_last_swap} = 3;
										$self->switch_player(@{$entry}[8], @{$entry}[0]); 
										$ct_found++;
									}
								}
							}
						}
					}
					else                  # need to swap to players
					{
						my $ts_playerid  = 0;
						my $ts_name      = "";
						my $ct_playerid  = 0;
						my $ct_name      = "";             
						my $ct_found = 0;
						@ct_players = sort { $b->[6] <=> $a->[6]} @ct_players;  # best player
						foreach my $entry (@ct_players) 
						{
							if ($ct_found == 0)
							{
								if (@{$entry}[7] == 1)  # only dead players!!
								{
									if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
										$ct_playerid = @{$entry}[8];
										$ct_name     = @{$entry}[0];
										$ct_found++;
									}
								}
							}
						}

						my $ts_found = 0;
						@ts_players = sort { $a->[6] <=> $b->[6]} @ts_players;  # worst player
						foreach my $entry (@ts_players) 
						{
							if ($ts_found == 0)
							{
								if (@{$entry}[7] == 1)  # only dead players!!
								{
									if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0)))  {
										$ts_playerid = @{$entry}[8];
										$ts_name     = @{$entry}[0];
										$ts_found++;
									}
								}
							}
						}
						if (($ts_found > 0) && ($ct_found > 0))
						{
							$self->{ba_last_swap} = 3;
							$self->switch_player($ts_playerid, $ts_name); 
							$self->switch_player($ct_playerid, $ct_name); 
						}
					}
				}
			}
		} # end if rounds > 1  
	}
}

#
# Marks server as needing flush
#

sub updateDB
{
	my ($self) = @_;
	$self->{needsupdate} = 1;
}

#
# Flushes server information in database
#

sub flushDB
{
	my ($self) = @_;
   	$self->get_map(1);
	
	my $serverid      = $self->{id};

    if ($self->{total_kills} == 0)
    {
		my $result = &::execCached(
			"get_server_player_info",
			"SELECT
				kills,
				headshots,
				suicides,
				rounds,
				ct_shots+ts_shots as shots,
				ct_hits+ts_hits as hits
			FROM
				hlstats_Servers
			WHERE
				serverId=?",
			$self->{id}
			);
		($self->{total_kills}, $self->{total_headshots}, $self->{total_suicides},$self->{total_rounds},$self->{total_shots},$self->{total_hits}) = $result->fetchrow_array();
		$result->finish;
	}   

	my $result = &::execCached(
		"get_player_count",
		"SELECT count(*) as players FROM hlstats_Players WHERE game=? and hideranking<>2",
		&::quoteSQL($self->{game}));
	$self->{players} = $result->fetchrow_array();
	$result->finish;
	
	
	# Update player details
	my $query = "
		UPDATE
			hlstats_Servers
		SET  
			name=?,
			rounds=rounds + ?,
			kills=kills + ?,
			suicides=suicides + ?,
			headshots=headshots + ?,
			bombs_planted=bombs_planted + ?,
			bombs_defused=bombs_defused + ?,
			players=?,
			ct_wins=ct_wins + ?,
			ts_wins=ts_wins + ?,
			act_players=?,
			max_players=?,
			act_map=?,
			map_rounds=?,
			map_ct_wins=?,
			map_ts_wins=?,
			map_started=?,
			map_changes=map_changes + ?,
			ct_shots=ct_shots + ?,
			ct_hits=ct_hits + ?,
			ts_shots=ts_shots + ?,
			ts_hits=ts_hits + ?,
			map_ct_shots=?,
			map_ct_hits=?,
			map_ts_shots=?,
			map_ts_hits=?,
			last_event=?
		WHERE
			serverId=?
	";
	my @vals = (
		&::quoteSQL($self->{name}),
		$self->{rounds},
		$self->{kills},
		$self->{suicides},
		$self->{headshots},
		$self->{bombs_planted},
		$self->{bombs_defused},
		$self->{players},
		$self->{ct_wins},
		$self->{ts_wins},
		$self->{numplayers},
		$self->{maxplayers},
		&::quoteSQL($self->{map}),
		$self->{map_rounds},
		$self->{map_ct_wins},
		$self->{map_ts_wins},
		$self->{map_started},
		$self->{map_changes},
		$self->{ct_shots},
		$self->{ct_hits},
		$self->{ts_shots},
		$self->{ts_hits},
		$self->{map_ct_shots},
		$self->{map_ct_hits},
		$self->{map_ts_shots},
		$self->{map_ts_hits},
		$::ev_unixtime,
		$serverid
	);
	&::execCached("update_server_stats", $query, @vals);

	$self->set("rounds", 0);
	$self->set("kills", 0);
	$self->set("suicides", 0);
	$self->set("headshots", 0);
	$self->set("bombs_planted", 0);
	$self->set("bombs_defused", 0);
	$self->set("ct_wins", 0);
	$self->set("ts_wins", 0);
	$self->set("ct_shots", 0);
	$self->set("ct_hits", 0);
	$self->set("ts_shots", 0);
	$self->set("ts_hits", 0);
	$self->set("map_changes", 0);
	$self->{needsupdate} = 0;
}

sub flush_player_count
{
	my ($self) = @_;
	
	&::execCached("flush_plyr_cnt",
		"UPDATE hlstats_Servers SET act_players=? WHERE serverId=?",
		$self->{numplayers},
		$self->{id}
	);
}

sub update_server_loc
{
	my ($self)      = @_;
	my $serverid    = $self->{id};
    my $server_ip   = $self->{address};
	my $publicaddress = $self->{publicaddress};

	if ($publicaddress =~ /^(\d+\.\d+\.\d+\.\d+)/) {
		$server_ip = $publicaddress;
	} elsif ($publicaddress =~ /^([0-9a-zA-Z\-\.]+)\:*.*/) {
		my $hostip = inet_aton($1);
		if ($hostip) {
			$server_ip = inet_ntoa($hostip);
		}
	}
	my $found = 0;
	my $servcity = undef;
	my $servcountry = undef;
	my $servlat=undef;
	my $servlng=undef;
	if ($::g_geoip_binary > 0) {
		if(!defined($::g_gi)) {
			return;
		}

		my $geoCity = undef;

		try { $geoCity = $::g_gi->city( ip => $server_ip ); }
		catch { $geoCity = undef; }

		if ($geoCity) {

			my $geoCityRec = $geoCity->city();
			my $geoCountryRec = $geoCity->country();
			my $geoLocationRec = $geoCity->location();
			my $geoPostalRec = $geoCity->postal();
			my $geoMostSpecificSubdivision = $geoCity->most_specific_subdivision();

			$country_code = $geoCountryRec->iso_code();
			$country_name = $geoCountryRec->name();
			$region = $geoMostSpecificSubdivision->name();
			$city = $geoCityRec->name();
			$postal_code = $geoPostalRec->code();
			$latitude = $geoLocationRec->latitude();
			$longitude = $geoLocationRec->longitude();
		}			
		
		if ($longitude) {

			$found++;
			$servcity = ((defined($city))?encode("utf8",$city):"");
			$servcountry = ((defined($country_name))?encode("utf8",$country_name):"");
			$servlat = $latitude;
			$servlng = $longitude;
		}
	
	} else {
		my @ipp = split (/\./,$server_ip);
		my $ip_number = $ipp[0]*16777216+$ipp[1]*65536+$ipp[2]*256+$ipp[3];
		my $query = "
			SELECT locId FROM geoLiteCity_Blocks WHERE startIpNum<= $ip_number AND endIpNum>= $ip_number LIMIT 1";
		my $result = &::doQuery($query);
		if ($result->rows > 0) {
			my $locid = $result->fetchrow_array;
			$result->finish;
			my $query = "
				SELECT
					city,
					name AS country,
					latitude AS lat,
					longitude AS lng
				FROM
					geoLiteCity_Location a 
				INNER JOIN
					hlstats_Countries b ON a.country=b.flag
				WHERE
					locId= $locid
				LIMIT 1";
			my $result = &::doQuery($query);
			if ($result->rows > 0) {
				$found++;
				($servcity,$servcountry,$servlat,$servlng) = $result->fetchrow_array;
				$result->finish;
			}
		}
	}
	if ($found > 0) {
		my $query = "
			UPDATE
				`hlstats_Servers`
			SET
				city = '".(defined($servcity)?&::quoteSQL($servcity):"")."',
				country='".(defined($servcountry)?&::quoteSQL($servcountry):"")."',
				lat=".((defined($servlat))?$servlat:"NULL").",
				lng=".((defined($servlng))?$servlng:"NULL")."
			WHERE
				serverId =$serverid
		";
		&::execNonQuery($query);
	}
}

sub messageAll
{
	my($self, $msg, $noshow, $force) = @_;
	
	if ($self->{broadcasting_events} == 1 || $force == 1)
	{
		if ($self->{mod} eq "SOURCEMOD" || $self->{mod} eq "AMXX")
		{
			my @userlist;

			foreach $player (values(%{$self->{srv_players}}))
			{
				if (($player->{is_bot} == 0) && ($player->{userid} > 0) && ($player->{playerid} != $noshow) && ($player->{display_events} == 1 || $force == 1))
				{
					push(@userlist, $player->{userid});
				}
			}

			if ($self->{play_game} != FF())
			{
				$msg = $self->{format_action}.$msg;
			}
			$self->messageMany($msg, 1, @userlist);
		}
		else
		{
			$self->dorcon("say ".$msg);
		}
	}
}

sub messageMany
{
	my($self, $msg, $toall, @userlist) = @_;
	if (scalar(@userlist) > 0)
	{
		if ($self->{mod} eq "SOURCEMOD")
		{
			my $usersendlist = "";
			foreach (@userlist)
			{
				$usersendlist .= $_.",";
			}
			$usersendlist =~ s/,$//;
			my $color = $self->{format_color};
			if ($toall > 0 && $color eq " 1")
			{
				$color = " 2";
			}
			$self->dorcon($self->{player_command}." \"$usersendlist\"$color ".$self->quoteparam($msg));
		}
		elsif ($self->{mod} eq "AMXX")
		{
			while (@userlist)
			{
				my $usersendlist = "";
				for ($i = 0; $i < 8; $i++)
				{
					$usersendlist .= shift(@userlist);
					if ($i < 7)
					{
						$usersendlist .= ",";
					}
				}
				$self->dorcon("hlx_amx_bulkpsay \"$usersendlist\"".$self->{format_color}." ".$self->quoteparam($msg));
			}
		}
		else
		{
			$rcmd = $self->{broadcasting_command};
			foreach (@userlist)
			{
				$self->dorcon(sprintf("%s %s%s %s",$rcmd, $self->format_userid($_), $self->{format_color}, $self->quoteparam($msg)));
			}
		}
	}
}


sub setHlxCvars
{
	my ($self) = @_;

	if ($self->{hlstats_url} ne "")
	{
		$self->dorcon("hlxce_webpage \"".$self->{hlstats_url}."\"");
	}
	$self->dorcon("hlxce_version \"".$::g_version."\"");
	
	if ($self->{play_game} eq "MANI" && $self->dorcon("mani_hlx_prefix" =~ /gameme/i))
	{
		$self->dorcon("mani_hlx_prefix \"HLstatsX\"");
	}
}

sub updatePlayerCount
{
	my ($self) = @_;
	
	if ($::g_debug > 1) {
		&::printEvent("SERVER","Updating Player Count");
	}
	
	my $trackable = 0;

	if ($self->{play_game} == L4D()) {
		my $num = 0;
		while (my($pl, $player) = each(%{$self->{srv_players}})) {
			if ($player->{trackable} == 1) {
				$trackable++;
			}
			if ($player->{userid} > 0) {
				$num++;
			}
		}
		$self->{numplayers} = $num;
		$self->{num_trackable_players} = $trackable;
	} else {
		$self->{numplayers} = scalar keys %{$self->{srv_players}};
		while (my($pl, $player) = each(%{$self->{srv_players}})) {
			if ($player->{trackable} == 1) {
				$trackable++;
			}
		}
		$self->{num_trackable_players} = $trackable;
	}
	
	$self->flush_player_count();
}

1;
