Code Search for Developers
 
 
  

mysql.pm from The Geronimo Project at Krugle


Show mysql.pm syntax highlighted

package PS::Saver::mysql;
use base qw( PS::Saver );

require 5.006_000;

use strict;
use util;
use DBI;
use Time::Local;
use POSIX qw(strftime);
use Data::Dumper;

our $VERSION 	= '1.4';

# --------------------------------------------------------------------------------------------------------------------------------
#
sub init {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $scanner = $self->{ps}{scanner};

  $self->SUPER::init($args);
#  print "DEBUG >>	Initializing " . $self->{classname} . " ...\n" if $self->DEBUG;

  # setup common variables we'll need once ...

  $self->{table} = {};							# lookup tables for various DB related lookups.
  $self->{explained} = {};
  $self->{errstr} = '';
  $self->{compiledprefix} = "c_";
  $self->{uid} = $conf->{uniqueid};

  $self->{tblprefix}		= $conf->{mysql}{tableprefix};
  $self->{t_plr}		= "$self->{tblprefix}plr";
  $self->{t_plrprofile}		= "$self->{tblprefix}plr_profile";
  $self->{t_plrids}		= "$self->{tblprefix}plrids";
  $self->{t_plrdata}		= "$self->{tblprefix}plrdata";
  $self->{t_plrmaps}		= "$self->{tblprefix}plrmaps";
  $self->{t_plrweapons}		= "$self->{tblprefix}plrweapons";
  $self->{t_plrroles}		= "$self->{tblprefix}plrroles";
  $self->{t_plrvictims}		= "$self->{tblprefix}plrvictims";
  $self->{t_clans}		= "$self->{tblprefix}clans";
  $self->{t_clansprofile}	= "$self->{tblprefix}clans_profile";
  $self->{t_gameawards}		= "$self->{tblprefix}gameawards";
  $self->{t_gameawards_plrs}	= "$self->{tblprefix}gameawards_plrs";
  $self->{t_gamemaps}		= "$self->{tblprefix}gamemaps";
  $self->{t_gameweapons}	= "$self->{tblprefix}gameweapons";
  $self->{t_info}		= "$self->{tblprefix}info";
  $self->{t_rcons}		= "$self->{tblprefix}rcons";
  $self->{t_cache}		= "$self->{tblprefix}cache";
  $self->{t_events}		= "$self->{tblprefix}events";
  $self->{t_events_props}	= "$self->{tblprefix}events_props";
  $self->{t_defs_awards}	= "$self->{tblprefix}defs_awards";
  $self->{t_defs_gametypes}	= "$self->{tblprefix}defs_gametypes";
  $self->{t_defs_modtypes}	= "$self->{tblprefix}defs_modtypes";
  $self->{t_defs_maps}		= "$self->{tblprefix}defs_maps";
  $self->{t_defs_roles}		= "$self->{tblprefix}defs_roles";
  $self->{t_defs_teams}		= "$self->{tblprefix}defs_teams";
  $self->{t_defs_weapons}	= "$self->{tblprefix}defs_weapons";
  $self->{t_defs_events}	= "$self->{tblprefix}defs_events";

  # reglardless of the conf->mysql->NOMODTABLES setting we always use the modtype as part of this prefix
  $self->{c_tblprefix} 		= "$conf->{mysql}{tableprefix}$self->{compiledprefix}%s_$conf->{gametype}" . ( $conf->{modtype} ? "_$conf->{modtype}" : "" );
  $self->{c_plrdata} 		= sprintf($self->{c_tblprefix}, "plrdata");
  $self->{c_plrmaps} 		= sprintf($self->{c_tblprefix}, "plrmaps");
  $self->{c_plrweapons} 	= sprintf($self->{c_tblprefix}, "plrweapons");
  $self->{c_plrroles} 		= sprintf($self->{c_tblprefix}, "plrroles");
  $self->{c_plrvictims} 	= sprintf($self->{c_tblprefix}, "plrvictims");
  $self->{c_gamemaps} 		= sprintf($self->{c_tblprefix}, "gamemaps");
  $self->{c_gameweapons} 	= sprintf($self->{c_tblprefix}, "gameweapons");

  if (!$args->{INSTALL}) {
    my $dbistr = "DBI:mysql:$conf->{mysql}{dbname};host=$conf->{mysql}{host}";
    $dbistr .= ";mysql_compression=1" if $conf->{mysql}{compression};
    $self->{dbh} = DBI->connect($dbistr, 
	$conf->{mysql}{username}, 
	$conf->{mysql}{password},
	{ PrintError => 0, RaiseError => 0, AutoCommit => 1 }
    ) or logerror(sprintf($self->{ps}{lang}{err_mysql}, $DBI::errstr),1);

    my $dbh = $self->{dbh};
    my $tables = [];
    if ($dbh) {
      $tables = $self->_check_compiled_tables;
      if (@$tables) {
#         print "\n  The following tables need to be created/compiled:\n    ",  join("\n    ", @$tables), "\n";
        $self->init_db($tables);
      }
    }

    # must be done AFTER the DBH handle is initialized above
    $self->{gid} = $self->get_gametypeid($conf->{gametype});
    $self->{mid} = $conf->{modtype} ? $self->get_modtypeid($conf->{modtype}) : 0;

    $self->{mysql_version} = $self->_get_rows_list("SELECT VERSION()")->[0];
  }

  return $self;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Creates the 'compiled' tables for any table listed in the $tables array ref.
sub init_db {
  my ($self, $tablelist, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $tbls = { map {  $_, 1  } @$tablelist };				# convert tables list into hash
  my $dbh = $self->{dbh};
  my $v = $self->{ps}{verbose};
  my $vdata = {};
  my ($ok,$tbl,$cmd,$values);
  return unless @$tablelist;
  $v->print("\n");
#  $args->{NOCALC} = 1;							# we don't want CALC vars to be included

  $ok = 1;
  if ($tbls->{plrdata}) {
    $vdata->{table} = $tbl = $self->{c_plrdata};
    $v->print($lang->{inittable1}, $vdata);
    while (1) {
      $ok = $dbh->do("DROP TABLE IF EXISTS `$tbl`");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      last if !$ok;
      $values .= "  `plrid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `startdate` date NOT NULL default '0000-00-00',\n";
      $values .= $self->_get_create_vars('plr', $args, 'gametype', 'modtype');
      $cmd = "CREATE TABLE `$tbl` ( \n";
      $cmd .= $values;
      $cmd .= "  PRIMARY KEY (plrid)\n";
      $cmd .= ")";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;

      last;
    }
    $v->print($lang->{inittable2}, $vdata);
  }

  $ok = 1;
  if ($tbls->{plrmaps}) {
    $vdata->{table} = $tbl = $self->{c_plrmaps};
    $v->print($lang->{inittable1}, $vdata);
    while (1) {
      $ok = $dbh->do("DROP TABLE IF EXISTS `$tbl`");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      last if !$ok;
      $values  = "  `plrmapid` int(10) unsigned NOT NULL auto_increment,\n";
      $values .= "  `plrid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `mapid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `startdate` date NOT NULL default '0000-00-00',\n";
      $values .= $self->_get_create_vars('plr', $args, 'maps', 'modtype_maps');
      $cmd = "CREATE TABLE `$tbl` ( \n";
      $cmd .= $values;
      $cmd .= "  PRIMARY KEY (plrmapid),\n";
      $cmd .= "  UNIQUE KEY plrmaps (plrid, mapid)\n";
      $cmd .= ")";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;

      last;
    }
    $v->print($lang->{inittable2}, $vdata);
  }

  $ok = 1;
  if ($tbls->{plrvictims}) {
    $vdata->{table} = $tbl = $self->{c_plrvictims};
    $v->print($lang->{inittable1}, $vdata);
    while (1) {
      $ok = $dbh->do("DROP TABLE IF EXISTS `$tbl`");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      last if !$ok;
      $values  = "  `plrvictimid` int(10) unsigned NOT NULL auto_increment,\n";
      $values .= "  `plrid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `victimid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `startdate` date NOT NULL default '0000-00-00',\n";
      $values .= $self->_get_create_vars('plr', $args, 'victims');
      $cmd = "CREATE TABLE `$tbl` ( \n";
      $cmd .= $values;
      $cmd .= "  PRIMARY KEY (plrvictimid),\n";
      $cmd .= "  UNIQUE KEY plrvictims (plrid, victimid)\n";
      $cmd .= ")";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;

      last;
    }
    $v->print($lang->{inittable2}, $vdata);
  }

  $ok = 1;
  if ($tbls->{plrweapons}) {
    $vdata->{table} = $tbl = $self->{c_plrweapons};
    $v->print($lang->{inittable1}, $vdata);
    while (1) {
      $ok = $dbh->do("DROP TABLE IF EXISTS `$tbl`");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      last if !$ok;
      $values  = "  `plrweaponid` int(10) unsigned NOT NULL auto_increment,\n";
      $values .= "  `plrid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `weaponid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `startdate` date NOT NULL default '0000-00-00',\n";
      $values .= $self->_get_create_vars('plr', $args, 'weapons');
      $cmd = "CREATE TABLE `$tbl` ( \n";
      $cmd .= $values;
      $cmd .= "  PRIMARY KEY (plrweaponid),\n";
      $cmd .= "  UNIQUE KEY plrweapons (plrid, weaponid)\n";
      $cmd .= ")";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;

      last;
    }
    $v->print($lang->{inittable2}, $vdata);
  }

  $ok = 1;
  if ($tbls->{plrroles}) {
    $vdata->{table} = $tbl = $self->{c_plrroles};
    $v->print($lang->{inittable1}, $vdata);
    while (1) {
      $ok = $dbh->do("DROP TABLE IF EXISTS `$tbl`");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      last if !$ok;
      $values  = "  `plrroleid` int(10) unsigned NOT NULL auto_increment,\n";
      $values .= "  `plrid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `roleid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `startdate` date NOT NULL default '0000-00-00',\n";
      $values .= $self->_get_create_vars('plr', $args, 'roles');
      $cmd = "CREATE TABLE `$tbl` ( \n";
      $cmd .= $values;
      $cmd .= "  PRIMARY KEY (plrroleid),\n";
      $cmd .= "  UNIQUE KEY plrroles (plrid, roleid)\n";
      $cmd .= ")";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;

      last;
    }
    $v->print($lang->{inittable2}, $vdata);
  }

  $ok = 1;
  if ($tbls->{gamemaps}) {
    $vdata->{table} = $tbl = $self->{c_gamemaps};
    $v->print($lang->{inittable1}, $vdata);
    while (1) {
      $ok = $dbh->do("DROP TABLE IF EXISTS `$tbl`");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      last if !$ok;
      $values  = "  `mapid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `startdate` date NOT NULL default '0000-00-00',\n";
      $values .= $self->_get_create_vars('map', $args, 'gametype', 'modtype');
      $cmd = "CREATE TABLE `$tbl` ( \n";
      $cmd .= $values;
      $cmd .= "  PRIMARY KEY (mapid)\n";
      $cmd .= ")";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;

      last;
    }
    $v->print($lang->{inittable2}, $vdata);
  }

  $ok = 1;
  if ($tbls->{gameweapons}) {
    $vdata->{table} = $tbl = $self->{c_gameweapons};
    $v->print($lang->{inittable1}, $vdata);
    while (1) {
      $ok = $dbh->do("DROP TABLE IF EXISTS `$tbl`");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      last if !$ok;
      $values  = "  `weaponid` int(10) unsigned NOT NULL default '0',\n";
      $values .= "  `startdate` date NOT NULL default '0000-00-00',\n";
      $values .= $self->_get_create_vars('weapon', $args, 'gametype', 'modtype');
      $cmd = "CREATE TABLE `$tbl` ( \n";
      $cmd .= $values;
      $cmd .= "  PRIMARY KEY (weaponid)\n";
      $cmd .= ")";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;

      last;
    }
    $v->print($lang->{inittable2}, $vdata);
  }

}
# --------------------------------------------------------------------------------------------------------------------------------
sub explainfields {
  my $self = shift;
  my $conf = $self->{ps}{conf};
  my $fields = {};

  while (my $tbl = shift) {
    next unless $tbl;
    $tbl = $conf->{ $conf->{savetype} }{tableprefix} . $tbl;
    if (exists $self->{explained}{$tbl}) {
      $fields = { %$fields, %{copyhash($self->{explained}{$tbl})} };
    } else {
      my $info = $self->_get_rows_hash("explain $tbl");
      my $f = {};
      foreach my $row (@$info) {
        $f->{ $row->{'Field'} } = $row;
        $fields->{ $row->{'Field'} } = $row;
      }
      $self->{explained}{$tbl} = copyhash($f);
    }
  }
  return wantarray ? %$fields : $fields;
}
# --------------------------------------------------------------------------------------------------------------------------------
# internal function that returns a list of 'compiled' tables that need to be created
sub _check_compiled_tables {
  my ($self) = @_;
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my $tables = [];
  my ($ok,$cmd, $tbl);

  foreach ( qw( plrdata plrmaps plrvictims plrweapons plrroles gamemaps gameweapons ) ) {
    $tbl = sprintf($self->{c_tblprefix}, $_);
    $cmd = "explain $tbl";
    $ok = $dbh->do($cmd);
    if (!$ok) {
      push(@$tables, $_);		# use $_ instead of $tbl, we only want the suffix of the table name
    } else {
#      print "$tbl is ok!\n";
    }
  }
  return wantarray ? @$tables : $tables;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Updates an info field in the pstats_info table
sub updateinfo {
  my $self = shift;
  my $args = ref $_[0] eq 'HASH' ? shift : { @_ };
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my ($ok,$cmd, $info);
  # save each variable
  while (my($name,$val) = each(%$args)) {
    next if ref $val;				# ignore any references!
    $info = $self->_get_rows_hash("SELECT `name`,`value` FROM $self->{t_info} WHERE `name`=" . $dbh->quote($name))->[0];
    if (exists $info->{name}) {
      next if $info->{value} eq ($val || '');			# ignore it if the value hasn't changed
      $dbh->do("UPDATE $self->{t_info} SET `value`=" . $dbh->quote($val) . " WHERE `name`=" . $dbh->quote($name));
    } else {
      $dbh->do(sprintf("INSERT INTO $self->{t_info} VALUES (%s,%s)", $dbh->quote($name), $dbh->quote($val)));
    }
  }
}
# --------------------------------------------------------------------------------------------------------------------------------
# Assigns a rank value to all players based on the field given (skill by default)
sub compile_player_ranks {
  my $self = shift;
  my $args = ref $_[0] eq 'HASH' ? shift : { @_ };
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $v = $self->{ps}{verbose};
  my $dbh = $self->{dbh};
  my ($ok,$cmd,$vdata);

  $args->{SORT} ||= $conf->{use}{playerrankfield} || 'skill';
  $args->{ORDER} ||= $conf->{use}{playerrankorder} || 'DESC';

# `oldrank` doesn't exist in the current version of PS (2.2.4), it needs to be added in an upgrade (v2.3)
  if ($self->explainfields('plr')->{oldrank}) {
    if (!$dbh->do("UPDATE $self->{t_plr} SET oldrank=rank")) {		# save the old rank
#      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
    }
  }

  if ($args->{SORT} eq 'skill') {					# optimized method to update rank based on skill
    $self->{mysql_version} =~ /^(\d+(\.\d+)?)/;
    my $mysqlver = $1 || 3.0;						# default to v3.0 if we're unable to determine it
    $dbh->do('SET @newrank := 0');					# initialize the rank variable
    if ($mysqlver >= 4) {						# Only works for v4.0.0+
      $cmd = "UPDATE $self->{t_plr} SET rank=IF(allowrank, \@newrank:=\@newrank+1, 0) ";
      $cmd .= $self->_getsortorder($args);
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
    } else {								# should work on any other pre v4.x server
      $ok = $dbh->do("ALTER TABLE $self->{t_plr} ORDER BY `skill` DESC");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      $ok = $dbh->do("UPDATE $self->{t_plr} SET rank=IF(allowrank, \@newrank:=\@newrank+1, 0)");
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
    }

  } else {					# alternative method to update rank based on any other field
    my $total = $vdata->{totalplayers} = $vdata->{status}{total} = $self->load_registered_plrlist({ TOTAL => 1, ALLOWALL => 0 });
    my $totaldone = $vdata->{status}{totaldone} = 0;
    my $limit = 1000;
    my $rank = $vdata->{status}{rank} = 0;
    my $oldrankexists = $self->explainfields('plr')->{oldrank};
    $vdata->{status}{start} = time();
    $vdata->{validplayers} = 0;

    while ($totaldone < $total) {			# while there are players to process .....
      my $plrlist = $self->stats_load_plrlist({
	START		=> $totaldone,
	SORT		=> $args->{SORT},
	ORDER		=> $args->{ORDER},
	LIMIT		=> $limit,
	ALLOWALL	=> 0,			# don't include players not allowed to rank
#	NOCALC		=> 1,			# don't include calculated variables
      });

      foreach my $plr (@$plrlist) {
        $totaldone++;
        $vdata->{status}{totaldone} = $totaldone;

        if ($conf->{verbose}) {
          $vdata->{status}{pct} = sprintf("%3.0f", calcpct($totaldone, $vdata->{status}{total}));
          $vdata->{status}{time} = compacttime(time() - $vdata->{status}{start}, 'hh:mm:ss');
          $vdata->{status}{msg} = $plr->{name};
        }

        $self->save_registered_plrvar($plr->{plrid}, { oldrank => '`rank`' }) if $oldrankexists;
        $self->save_registered_plrvar($plr->{plrid}, { rank => ++$rank });

        $vdata->{status}{rank} = $rank;
        $v->print($lang->{plrrankprogress}, $vdata);
      }
    }
  }

  return $ok;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Trims the history stats from all required tables. Anything older than the $datelimit is deleted
sub trimhistory {
  my $self = shift;
  my ($datelimit) = @_;
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my ($ok, $cmd, $table, $table2, $where);
  my $keys = {
	t_plrdata	=> { id => 'plrdataid' },
	t_plrmaps	=> { id => 'plrmapid' },
	t_plrweapons	=> { id => '' },
	t_plrroles	=> { id => '' },
	t_plrvictims	=> { id => '' },
	t_rcons		=> { id => '' },
	t_gamemaps	=> { id => 'gamemapid' },
	t_gameweapons	=> { id => '' },
  };
  $datelimit = strftime("%Y-%m-%d", localtime($datelimit)) if index($datelimit,'-') == -1;

  $where = "(statdate < '$datelimit')";

  foreach my $key (keys %$keys) {
    $table = $self->{$key};
    $table2 = $keys->{$key}{id} ? $table . "_" . $conf->{modtype} : '';

    if ($table2) {
      my $id = $keys->{$key}{id};
      my $ids = $self->_get_rows_list("SELECT $id FROM $table WHERE $where");

      # Now we can delete everything that is old in the 2nd table based on the ID's we just loaded
      $dbh->do("DELETE FROM $table2 WHERE $id IN (" . join(', ', @$ids) . ")");
      $dbh->do("OPTIMIZE TABLE $table2");
    }

    # Delete everything from the primary table
    $dbh->do("DELETE FROM $table WHERE $where");
    $dbh->do("OPTIMIZE TABLE $table");
  }
}
# --------------------------------------------------------------------------------------------------------------------------------
# returns a list of weekly or montly dates that an award has been given. Dates are returned as a hash for easy lookup.
sub get_award_dates {
  my $self = shift;
  my ($awardname, $type) = (shift, shift || 'weekly');
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my ($ok,$cmd,$info,$list);
  my $awardtype = "awardtype='" . ((lc $type =~ /^week/) ? 'weekly' : 'monthly') . "'";

  $cmd = "
	SELECT a.*
	FROM $self->{t_gameawards} a, $self->{t_defs_awards} def
	WHERE def.id=a.awardid AND def.name=" . $dbh->quote($awardname) . " AND $awardtype
	GROUP BY `year`,`month`,`day`,`week`
	ORDER BY `year`,`month`,`day`,`week`
  ";
#  print "$cmd\n";
  $info = $self->_get_rows_hash($cmd);

  foreach my $row (@$info) {
    my $date = sprintf("%04d-%02d-%02d", $row->{year}, $row->{month}, $row->{day});
    $list->{ $date } = $date;
  }

  return wantarray ? %$list : $list;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Returns a list of players based on the values given from the RAW data tables (not compiled). Used for award generation.
sub get_award_plrlist {
  my $self = shift;
  my $args = ref $_[0] eq 'HASH' ? shift : { @_ };
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth);
  $stats = [];

  $args->{order} = 'desc' unless defined $args->{order};
  $args->{limit} = '10' unless defined $args->{limit};
  $args->{end} = $args->{start} unless defined $args->{end};

  my $start = index($args->{start},'-') != -1 ? $args->{start} : strftime("%Y-%m-%d", localtime($args->{start}));
  my $end = index($args->{end},'-') != -1 ? $args->{end} : strftime("%Y-%m-%d", localtime($args->{end}));

  $cmd  = "SELECT $args->{expr} as value,plr.plrid ";
  $cmd .= "FROM $self->{t_plrdata} AS data, $self->{t_plr} as plr ";
  $cmd .= "LEFT JOIN $self->{t_plrdata}_$conf->{modtype} AS `mod` ON mod.plrdataid=data.plrdataid " unless $conf->{mysql}{NOMODTABLES}; #if $conf->{modtype};
  $cmd .= "WHERE data.plrid=plr.plrid ";
  $cmd .= "AND plr.allowrank " unless $args->{allowall};
  $cmd .= "AND (statdate BETWEEN '$start' AND '$end') ";
  $cmd .= "AND (plr.gametype=$self->{gid} AND plr.modtype=$self->{mid}) ";
  $cmd .= "GROUP BY data.plrid ";
  $cmd .= "HAVING $args->{where} " if $args->{where};	# must use 'having' and not 'where', since we're using expressions
  $cmd .= "ORDER BY 1 $args->{order} ";
  $cmd .= "LIMIT $args->{limit} ";
#  print "$cmd\n";

  $sth = $dbh->prepare($cmd);
  if ($sth->execute) {
    while (my $row = $sth->fetchrow_hashref) {
      push(@$stats, { %$row }) if defined $row->{value};
#      push(@$stats, { %$row });
    }
  } else {
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
  }

  @$stats = () if list_sum($stats, 'value') == 0;	# if no players in the list have a value other than 0, ignore them

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub delete_awards {
  my $self = shift;
  my ($list) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my @tables = ($self->{t_defs_awards}, $self->{t_gameawards}, $self->{t_gameawards_plrs});
  my @awlist = ();

  foreach my $a (@$list) {
    push(@awlist, split(/,/, $a));
  }

  # delete all award information if the first element is null
  if (!$awlist[0] or $awlist[0] eq '') {
    foreach my $t (@tables) {
      $dbh->do("DELETE FROM $t");
#      $dbh->do("TRUNCATE $t");
      $dbh->do("OPTIMIZE TABLE $t");
      $dbh->do("ALTER TABLE $t AUTO_INCREMENT=1") unless $t eq $self->{t_gameawards_plrs};
    }
  } else {	# delete specific awards specified
    foreach my $name (@awlist) {
      $name =~ s/^\s+//;
      $name =~ s/\s+$//;
      next if !$name or $name eq '';

      # we can't use the "get_awardid" function below, since that would automatically create a new award def, even if an invalid award def name was given
      my $awardid = $self->_get_rows_array("SELECT id FROM $self->{t_defs_awards} WHERE `name`=" . $dbh->quote($name))->[0][0];
      next unless $awardid;

      # Get a list of all game award id's so we know what players to delete
      my @ids = ();
      my $sth = $dbh->prepare("SELECT gameawardid FROM $self->{t_gameawards} WHERE awardid=$awardid");
      if ($sth->execute) {
        while (my $data = $sth->fetchrow_arrayref) {
          push(@ids, $data->[0]);
        }
      }

      if (scalar @ids) {
        my $idlist = join(',', @ids);
        $dbh->do("DELETE FROM $self->{t_gameawards_plrs} WHERE gameawardid IN ($idlist)");
      }
      $dbh->do("DELETE FROM $self->{t_gameawards} WHERE awardid=$awardid");
    }
  }
}
# --------------------------------------------------------------------------------------------------------------------------------
# Saves an award for the week or month given.
sub save_award_plrlist {
  my $self = shift;
  my ($award, $plrlist, $awardtype, $date) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my $awardid = $self->get_awardid($award->{awardname}, $award->{title} || '');
  my ($cmd, $stats, $sth);
  my $weekcode = $conf->{awards}{startofweek} eq 'monday' ? '%W' : '%U';
  my ($week, $day, $month, $year) = split(' ', strftime("$weekcode %d %m %Y", localtime($date)));
#  print "\n($week, $day, $month, $year)\n";

  # Find out if this award already exists
  $cmd = "
	SELECT gameawardid 
	FROM $self->{t_gameawards} a
	WHERE awardid=$awardid AND `awardtype`='$awardtype' AND `year`=$year AND `month`=$month AND `day`=$day
  ";
  my $gameawardid = $self->_get_rows_array($cmd)->[0][0];

  # Award does not exist, so insert a new record for it ...
  if (!$gameawardid) {
    my $w = $awardtype eq 'weekly' ? $week : 0;
    $cmd = "INSERT INTO $self->{t_gameawards} (`awardid`,`awardtype`,`year`,`month`,`day`,`week`,`format`) 
	VALUES ($awardid,'$awardtype',$year,$month,$day,$w," . $dbh->quote($award->{format}) . ")
    ";

    my $ok = $dbh->do($cmd);
    if (!$ok) {
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
      return 0;
    }
    $gameawardid = $dbh->{mysql_insertid};
  } else {
    # delete the current award player list since we're going to rebuild it below
    $dbh->do("DELETE FROM $self->{t_gameawards_plrs} WHERE gameawardid=$gameawardid");
  }

  my $rank = 0;
  $cmd = "INSERT INTO $self->{t_gameawards_plrs} (gameawardid,plrid,awardrank,awardvalue) VALUES ";
  foreach my $p (@$plrlist) {
    $rank++;
    my @values = ();
    push(@values, $gameawardid);
    push(@values, $p->{plrid});
    push(@values, $rank);
    push(@values, $dbh->quote($p->{value}));

    my $sql = "$cmd (" . join(',',@values) . ")";
#    print "$sql\n";
    my $ok = $dbh->do($sql);
    if (!$ok) {
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
    }
  }

}
# --------------------------------------------------------------------------------------------------------------------------------
# The install script will call this during installation. You'll want to initialize any DB connections, etc, here...
# Return a FALSE value if EVERYTHING WORKS. Otherwise return the error string of what went wrong (this is naturally backwards of
# what you're normally used to)
sub install {
  my ($self) = @_;
  my $conf = $self->{ps}{conf};
  $self->{dbh} = DBI->connect("DBI:mysql:$conf->{mysql}{dbname};host=$conf->{mysql}{host}",
    $conf->{mysql}{username},
    $conf->{mysql}{password},
    { PrintError => 0, RaiseError => 0, AutoCommit => 1 } 
  ) or return $DBI::errstr; 

  return 0;
}
# -----------------------------------------------------------------------------------------------------------------------------
# Run after every 'source' is depleted of new log files. Handy to clean up event histories, or whatever
sub cleanup {
  my $self = shift;
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my $oldest = time() - ($conf->{eventmaxdays} * (60*60*24));
  my ($sth, $del, $dates);

  $self->SUPER::cleanup;

  # CLEAN UP OLD EVENTS ...
  $sth = $dbh->prepare("SELECT id FROM $self->{t_events} WHERE timestamp <= $oldest");
  if ($sth->execute()) {
    # we must delete the properties matching expired events
    $del = $dbh->prepare("DELETE FROM $self->{t_events_props} WHERE id=?") or die $dbh->errstr;
    while (my $id = $sth->fetchrow_array) {
      $del->execute($id); 
    }
    $dbh->do("DELETE FROM $self->{t_events} WHERE timestamp <= $oldest");	# now delete the events  
  } else {
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
  }

  $self->cleanup_players;

}
# --------------------------------------------------------------------------------------------------------------------------------
# Cleans up players skill values. If the user's config has a 'decay' value for 'skill' this routine will make sure all players
# will have the proper skill value set based on the decay
sub cleanup_players {
  my $self = shift;
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my $newest = $self->stats_get_date_range->{newest};			# get the newest date with stats
  my $date = date("%Y-%m-%d",$newest);
  my ($cmd, $sth, $diff, $skill, $list, $timestamp, $data);
  return unless $conf->{decay}{skill};					# return if skill decay is disabled

  $list = $self->load_registered_plrlist({ 
	VARS => "plr.plrid,plrlastdate, UNIX_TIMESTAMP(plrlastdate) as timestamp",
	WHERE => "skill > $conf->{decay}{minskill} AND plrlastdate < '$date'",
  });

  # Equalize skill values for all players
  $cmd  = "SELECT plrid,statdate, UNIX_TIMESTAMP(statdate) as timestamp,oldskill ";
  $cmd .= "FROM $self->{t_plrdata} ";
  $cmd .= "WHERE plrid=? ";
  $cmd .= "ORDER BY statdate DESC LIMIT 1";
  $sth = $dbh->prepare($cmd);

  foreach my $plr (@$list) {
    if ($sth->execute($plr->{plrid})) {
      if ($data = $sth->fetchrow_hashref) {
        $skill = $data->{oldskill};
        $diff = sprintf("%.0f", ($newest - $data->{timestamp}) / (60*60*24));
        if ($diff > 0) {
          $skill = $data->{oldskill} - (($skill * ($conf->{decay}{skill} / 100)) * $diff);
          $skill = $conf->{decay}{minskill} if defined $conf->{decay}{minskill} and $skill < $conf->{decay}{minskill};

#         $val = $row[0] - (($row[0] * ($decayconf->{$var} / 100)) * $diff);
#         $val = $decayconf->{"min$var"} if defined $decayconf->{"min$var"} and $val < $decayconf->{"min$var"};

          $self->save_registered_plrvar($plr->{plrid}, { skill => $skill }) if $skill != $data->{oldskill};
        }
      }

    } else {
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
    }
  }
}
# --------------------------------------------------------------------------------------------------------------------------------
# Returns a list of all names that a player has used based on their steamid
sub load_plr_names {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my ($cmd, $list, $sth, $total, $data);
  my $match = $dbh->quote($args->{MATCH});
  $list = [];

  $args->{SORT} = 'totaluses' unless $args->{SORT};
  $args->{ORDER} = 'DESC' unless $args->{ORDER};
  $args->{FIELD} = 'worldid' unless $args->{FIELD};

# OLD WHACKED OUT QUERY! What the hell was I thinking?!
#  $cmd  = "SELECT id.name,id.worldid,id.ipaddr,SUM(id.totaluses) as totaluses, IFNULL(p.plrid,0) AS plrid ";
#  $cmd .= "FROM $self->{t_plrids} AS id ";
#  $cmd .= "LEFT JOIN $self->{t_plr} AS p ON p.plrid=id.plrid ";
#  $cmd .= "WHERE (p.$args->{FIELD}=$match) AND (id.gametype=$self->{gid} AND id.modtype=$self->{mid}) ";
#  $cmd .= "GROUP BY id.name ";

  $cmd .= "SELECT id.name,SUM(id.totaluses) as totaluses ";
  $cmd .= "FROM $self->{t_plrids} AS id ";
  $cmd .= "WHERE (id.$args->{FIELD}=$match) AND (id.gametype=$self->{gid} AND id.modtype=$self->{mid}) ";
  $cmd .= "GROUP BY id.name ";

  $cmd .= $self->_getsortorder($args);
#  print "$cmd\n";
#  print "$cmd\n" if $args->{MATCH} eq 'BOT:realypssd'; 

  $sth = $dbh->prepare($cmd);
  if ($sth->execute) {
    while (my $row = $sth->fetchrow_hashref) {
      push(@$list, $row);
    }
  }

  return wantarray ? @$list : $list;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Returns a list of all 'aliases' (ie: names, wonids or ipaddrs) a player has used
sub stats_load_plraliases {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $total, $match, $data);
  $stats = {};

  if (ref $args->{PLR} eq 'HASH') {
    $args->{NAME} = $args->{PLR}{name};
    $args->{WORLDID} = $args->{PLR}{worldid};
    $args->{IPADDR} = $args->{PLR}{ipaddr};
  }

  $match  = "";
  $match .= "id.name=" . $dbh->quote($args->{NAME}) . " OR " if defined $args->{NAME};
  $match .= "id.ipaddr=" . $dbh->quote($args->{IPADDR}) . " OR " if defined $args->{IPADDR};
  $match .= "id.worldid=" . $dbh->quote($args->{WORLDID} || $args->{WONID}) . " OR " if defined $args->{WORLDID} or defined $args->{WONID};
  $match = $match ? substr($match, 0, -4) : '0';		# strip off trailing ' OR ', or return 0 so we match nothing

  $cmd = "SELECT id.*, IFNULL(p.plrid,0) as plrid ";
  $cmd .= "FROM $self->{t_plrids} AS id ";
  $cmd .= "LEFT JOIN $self->{t_plr} AS p ON p.plrid=id.plrid ";
  $cmd .= "WHERE ($match) AND (id.gametype=$self->{gid} AND id.modtype=$self->{mid}) ";

#  print STDERR "\n$cmd\n";

  $sth = $dbh->prepare($cmd);
  if ($sth->execute) {
    $data = {};
    while (my $row = $sth->fetchrow_hashref) {
      $data->{names}{ $row->{name} }{totaluses} += $row->{totaluses};
      $data->{names}{ $row->{name} }{name} = $row->{name};
      $data->{names}{ $row->{name} }{plrid} = $row->{plrid} if $row->{plrid};

      $data->{ipaddrs}{ $row->{ipaddr} }{totaluses} += $row->{totaluses};
      $data->{ipaddrs}{ $row->{ipaddr} }{ipaddr} = $row->{ipaddr};
      $data->{ipaddrs}{ $row->{ipaddr} }{plrid} = $row->{plrid} if $row->{plrid};

      $data->{worldids}{ $row->{worldid} }{totaluses} += $row->{totaluses};
      $data->{worldids}{ $row->{worldid} }{worldid} = $row->{worldid};
      $data->{worldids}{ $row->{worldid} }{plrid} = $row->{plrid} if $row->{plrid};
    }

    $stats->{names} = [ sort { $b->{totaluses} <=> $a->{totaluses} } map { $data->{names}{$_} } keys %{$data->{names}} ];
    $stats->{ipaddrs} = [ sort { $b->{totaluses} <=> $a->{totaluses} } map { $data->{ipaddrs}{$_} } keys %{$data->{ipaddrs}} ];
    $stats->{worldids} = [ sort { $b->{totaluses} <=> $a->{totaluses} } map { $data->{worldids}{$_} } keys %{$data->{worldids}} ];

#    print STDERR Dumper($data);
#    print STDERR Dumper($stats);
  } else {
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
  }

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
# This returns an array where each element is the value of a plr variable for each day within the date range
# [ { date => 'yyyy-mm-dd', 'value' => 0 }, {...}, ... ]
sub stats_load_plrvar_history {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $var, $start, $end);
  $var = $args->{VAR} || $args->{VARIABLE};
  $stats = [];
  return $stats if !$args->{PLRID} or !$var;

  $start = $args->{STARTDATE} || date("%Y-%m-%d");			# default to todays date
  $start = date('%Y-%m-%d', $start) unless index($start, '-') > 0;
  $end = $args->{ENDDATE} || date("%Y-%m-%d");
  $end = date('%Y-%m-%d', $end) unless index($end, '-') > 0;

  $cmd  = "SELECT statdate as date, `$var` as value FROM $self->{t_plrdata} AS data ";
  $cmd .= "LEFT JOIN $self->{t_plrdata}_$conf->{modtype} AS `mod` ON mod.plrdataid=data.plrdataid " unless $conf->{mysql}{NOMODTABLES}; #if $conf->{modtype};
  $cmd .= "WHERE data.plrid=$args->{PLRID} ";
  $cmd .= "AND (statdate BETWEEN '$start' AND '$end') ";
  $cmd .= "ORDER BY statdate ";

  $sth = $dbh->prepare($cmd);
  if ($sth->execute) {
    if ($args->{RETURNDATES}) {
      while (my $row = $sth->fetchrow_hashref) {
        push(@$stats, { %$row });
      }
    } else {
      while (my $row = $sth->fetchrow_arrayref) {
        push(@$stats, $row->[1]);
      }
    }

#    if (1 or $args->{FILLDATES}) {						# Fill in the missing dates
#      my $diff = diffdays_ymd($end, $start);
#    }

  } else {
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
  }

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_plr {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = {};
  return $stats unless $args->{PLRID};

  $values = $self->_get_simple_vars('plr', $args, 'gametype', 'modtype');
  $cmd  = "SELECT plr.*,cp.*,clan.*,$values FROM $self->{c_plrdata} as data, $self->{t_plr} as plr ";
  $cmd .= "LEFT JOIN $self->{t_clans} as clan ON clan.clanid=plr.clanid ";
  $cmd .= "LEFT JOIN $self->{t_clansprofile} as cp ON cp.clantag=clan.clantag ";
  $cmd .= "WHERE plr.plrid=data.plrid AND plr.plrid=$args->{PLRID} ";
  $cmd .= "GROUP BY plr.plrid ";
  $cmd .= "LIMIT 1 ";
  $stats = $self->_get_rows_hash($cmd)->[0];

	# --- LOAD WEAPONS ---
  $values = $self->_get_simple_vars('plr', $args, 'weapons');
  $cmd = "SELECT $values,weaponid,def.name,def.desc FROM $self->{c_plrweapons} AS data ";
  $cmd .= "LEFT JOIN $self->{t_defs_weapons} AS def ON def.id=data.weaponid ";
  $cmd .= "WHERE data.plrid=$args->{PLRID} ";
  $cmd .= "GROUP BY data.weaponid ";
  $cmd .= $self->_getsortorder($args, 'WEAPON');
  $stats->{weapons} = $self->_get_rows_hash($cmd);

	# --- LOAD MAPS ---
  $values = $self->_get_simple_vars('plr', $args, 'maps', 'modtype_maps');
  $cmd = "SELECT $values,mapid,def.name,def.desc FROM $self->{c_plrmaps} AS data ";
  $cmd .= "LEFT JOIN $self->{t_defs_maps} AS def ON def.id=data.mapid ";
  $cmd .= "WHERE data.plrid=$args->{PLRID} ";
  $cmd .= "GROUP BY data.mapid ";
  $cmd .= $self->_getsortorder($args, 'MAP');
  $stats->{maps} = $self->_get_rows_hash($cmd);

	# --- LOAD VICTIMS ---
  $values = $self->_get_simple_vars('plr', $args, 'victims');
  $cmd = "SELECT $values,victimid,plr.* FROM $self->{c_plrvictims} AS data ";
  $cmd .= "LEFT JOIN $self->{t_plr} AS plr ON plr.plrid=data.victimid ";
  $cmd .= "WHERE data.plrid=$args->{PLRID} ";
  $cmd .= "GROUP BY data.victimid ";
  $cmd .= $self->_getsortorder($args, 'VICTIM');
  $stats->{victims} = $self->_get_rows_hash($cmd);

	# Load player events
  if ($conf->{eventmaxdays}) {
    $cmd = "
	SELECT
		event1.timestamp, e.name, e.desc, m.name as mapname, m.id as mapid,
		IFNULL(t.name,'') as teamname,
		IFNULL(w.name,'') as weaponname,
		IFNULL(w.id,'') as weaponid,
		IFNULL(p1.name,'') as plr1name, IFNULL(p1.plrid,'') as plr1id, IFNULL(p1.worldid,'') as plr1worldid,
		IFNULL(p2.name,'') as plr2name, IFNULL(p2.plrid,'') as plr2id, IFNULL(p2.worldid,'') as plr2worldid
	FROM 
		$self->{t_events} as event1, 
		$self->{t_defs_events} as e 
		LEFT JOIN $self->{t_defs_maps} as m ON m.id=event1.map
		LEFT JOIN $self->{t_defs_weapons} as w ON w.id=event1.weapon
		LEFT JOIN $self->{t_defs_teams} as t ON t.id=event1.team
		LEFT JOIN $self->{t_plr} as p1 ON p1.plrid=event1.plrid1
		LEFT JOIN $self->{t_plr} as p2 ON p2.plrid=event1.plrid2 
	WHERE 
		e.id=event1.eventid AND
		(event1.gametype=$self->{gid} AND event1.modtype=$self->{mid}) AND
		$args->{PLRID} IN (event1.plrid1,event1.plrid2) 
    ";
    $cmd .= "AND (event1.timestamp BETWEEN UNIX_TIMESTAMP('$args->{STARTDATE}') AND UNIX_TIMESTAMP('$args->{ENDDATE}')) " if $args->{STARTDATE} and $args->{ENDDATE};
    $cmd .= "AND (e.name NOT IN (" . join(',',map { $dbh->quote($_) } split(/\s*,\s*/,($args->{EVENTIGNORE}))) . "))" if $args->{EVENTIGNORE};
    $cmd .= "ORDER BY event1.timestamp ";

    if ($args->{EVENTLIMIT} and !$args->{EVENTSTART}) {    
      $cmd .= "LIMIT $args->{EVENTLIMIT} ";    
    } elsif($args->{EVENTLIMIT} and $args->{EVENTSTART}) {    
      $cmd .= "LIMIT $args->{EVENTSTART},$args->{EVENTLIMIT} ";    
    }

    $stats->{events} = $self->_get_rows_hash($cmd);
  }

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_maplist {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = [];

  $values = $self->_get_simple_vars('map', $args, 'gametype', 'modtype');
  $cmd  = "SELECT $values, data.mapid, def.name, def.desc ";
  $cmd .= "FROM $self->{c_gamemaps} as data, $self->{t_defs_maps} as def ";
  $cmd .= "WHERE data.mapid=def.id ";
  $cmd .= "GROUP BY data.mapid ";
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_map {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = {};

  $values = $self->_get_simple_vars("map", $args, 'gametype', 'modtype');
  $cmd = "SELECT $values, data.mapid, def.name, def.desc ";
  $cmd .= "FROM $self->{c_gamemaps} as data, $self->{t_defs_maps} as def ";
  $cmd .= "WHERE data.mapid=def.id AND data.mapid=$args->{MAPID} ";
  $cmd .= "GROUP BY data.mapid ";
  $cmd .= "LIMIT 1 ";
  $stats = $self->_get_rows_hash($cmd)->[0];

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_map_plrdata {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = [];

	# NOTE: We're fetching PLR vars and not MAP vars here
#  $values = $self->_getsumvars("plr", $args, 'maps', 'modtype_maps');
  $values = $self->_getsumvars("plr", $args, 'maps');
  $cmd  = "SELECT $values, data.mapid, data.plrid, plr.* ";
  $cmd .= "FROM $self->{c_plrmaps} as data ";
  $cmd .= "LEFT JOIN $self->{t_plr} as plr on plr.plrid=data.plrid ";
  $cmd .= "WHERE data.mapid=$args->{MAPID} AND plr.plrid=data.plrid ";
  $cmd .= "AND plr.allowrank " unless $args->{ALLOWALL};
  $cmd .= "GROUP BY data.plrid ";
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_global_plrdata {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calc);
  $stats = {};

  $args->{NOCALC} = 1;			# calculated vars will not currently work in this context

  $values = $self->_get_compile_sum_vars('plr', $args, 'gametype', 'modtype');  
  $cmd  = "SELECT $values ";
  $cmd .= "FROM $self->{c_plrdata} AS data ";
  $cmd .= "WHERE $args->{WHERE} " if $args->{WHERE};
  $stats = $self->_get_rows_hash($cmd)->[0];

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
# This returns an array where each element is the value of a MAP variable for each day within the date range
# [ { date => 'yyyy-mm-dd', 'value' => 0 }, {...}, ... ]
sub stats_load_mapvar_history {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $var, $start, $end);
  $var = $args->{VAR} || $args->{VARIABLE};
  $stats = [];
  return $stats if !$args->{MAPID} or !$var;

  $start = $args->{STARTDATE} || date("%Y-%m-%d");			# default to todays date
  $start = date('%Y-%m-%d', $start) unless index($start, '-') > 0;
  $end = $args->{ENDDATE} || date("%Y-%m-%d");
  $end = date('%Y-%m-%d', $end) unless index($end, '-') > 0;

  $cmd = "SELECT statdate as date, `$var` as value FROM $self->{t_gamemaps} AS data ";
  $cmd .= "LEFT JOIN $self->{t_gamemaps}_$conf->{modtype} AS `mod` ON mod.gamemapid=data.gamemapid " unless $conf->{mysql}{NOMODTABLES}; #if $conf->{modtype};
  $cmd .= "WHERE data.mapid=$args->{MAPID} ";
  $cmd .= "AND (statdate BETWEEN '$start' AND '$end') ";
  $cmd .= "ORDER BY statdate ";
#  print STDERR "\n",(caller(1))[3],"\n$cmd\n";
  $sth = $dbh->prepare($cmd);
  if ($sth->execute) {
    if ($args->{RETURNDATES}) {
      while (my $row = $sth->fetchrow_hashref) {
        push(@$stats, { %$row });
      }
    } else {
      while (my $row = $sth->fetchrow_arrayref) {
        push(@$stats, $row->[1]);
      }
    }

#    if (1 or $args->{FILLDATES}) {						# Fill in the missing dates
#      my $diff = diffdays_ymd($end, $start);
#    }

  } else {
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
  }

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
# This returns an array where each element is the value of a WEAPON variable for each day within the date range
# [ { date => 'yyyy-mm-dd', 'value' => 0 }, {...}, ... ]
sub stats_load_weaponvar_history {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $var, $start, $end);
  $var = $args->{VAR} || $args->{VARIABLE};
  $stats = [];
  return $stats if !$args->{WEAPONID} or !$var;

  $start = $args->{STARTDATE} || date("%Y-%m-%d");			# default to todays date
  $start = date('%Y-%m-%d', $start) unless index($start, '-') > 0;
  $end = $args->{ENDDATE} || date("%Y-%m-%d");
  $end = date('%Y-%m-%d', $end) unless index($end, '-') > 0;

  $cmd = "SELECT statdate as date, `$var` as value FROM $self->{t_gameweapons} AS data ";
  $cmd .= "WHERE data.weaponid=$args->{WEAPONID} ";
  $cmd .= "AND (statdate BETWEEN '$start' AND '$end') ";
  $cmd .= "ORDER BY statdate ";

  $sth = $dbh->prepare($cmd);
  if ($sth->execute) {
    if ($args->{RETURNDATES}) {
      while (my $row = $sth->fetchrow_hashref) {
        push(@$stats, { %$row });
      }
    } else {
      while (my $row = $sth->fetchrow_arrayref) {
        push(@$stats, $row->[1]);
      }
    }

#    if (1 or $args->{FILLDATES}) {						# Fill in the missing dates
#      my $diff = diffdays_ymd($end, $start);
#    }

  } else {
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
  }

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub stats_load_globalplrvar_history {
  my $self = shift;
  return $self->_stats_load_globalvar_history('plrdata','plrdataid',@_);
}
# --------------------------------------------------------------------------------------------------------------------------------
sub stats_load_globalmapvar_history {
  my $self = shift;
  return $self->_stats_load_globalvar_history('gamemaps','gamemapid',@_);
}
# --------------------------------------------------------------------------------------------------------------------------------
sub stats_load_globalweaponvar_history {
  my $self = shift;
  return $self->_stats_load_globalvar_history('gameweapons','weaponmapid',@_);
}
# --------------------------------------------------------------------------------------------------------------------------------
# INTERNAL FUNCTION
# This returns an array where each element is the GLOBAL value of a variable for each day within the date range
# [ { date => 'yyyy-mm-dd', 'value' => 0 }, {...}, ... ]
sub _stats_load_globalvar_history {
  my ($self, $table, $joinid, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my $prefix = $conf->{mysql}{tableprefix};				# note to self, do not delete
  my ($cmd, $stats, $sth, $var, $start, $end);
  $var = $args->{VAR} || $args->{VARIABLE};
  $stats = [];
  return $stats if !$var;

  $start = $args->{STARTDATE} || date("%Y-%m-%d");			# default to todays date
  $start = date('%Y-%m-%d', $start) unless index($start, '-') > 0;
  $end = $args->{ENDDATE} || date("%Y-%m-%d");
  $end = date('%Y-%m-%d', $end) unless index($end, '-') > 0;

  $cmd = "SELECT statdate as date, sum(`$var`) as value FROM ${prefix}$table AS data ";
  $cmd .= "LEFT JOIN ${prefix}${table}_$conf->{modtype} AS `mod` ON mod.$joinid=data.$joinid " if $joinid and !$conf->{mysql}{NOMODTABLES}; #$conf->{modtype};
  $cmd .= "AND (statdate BETWEEN '$start' AND '$end') ";
  $cmd .= "GROUP BY statdate ";
  $cmd .= "ORDER BY statdate ";

  $sth = $dbh->prepare($cmd);
  if ($sth->execute) {
    if ($args->{RETURNDATES}) {
      while (my $row = $sth->fetchrow_hashref) {
        push(@$stats, { %$row });
      }
    } else {
      while (my $row = $sth->fetchrow_arrayref) {
        push(@$stats, $row->[1]);
      }
    }

#    if (1 or $args->{FILLDATES}) {						# Fill in the missing dates
#      my $diff = diffdays_ymd($end, $start);
#    }

  } else {
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
  }

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_weaponlist {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = [];
  $values = "";

  $values = $self->_get_simple_vars('weapon', $args, 'gametype');
  $cmd  = "SELECT $values, data.weaponid, def.name, def.desc ";
  $cmd .= "FROM $self->{c_gameweapons} as data, $self->{t_defs_weapons} as def ";
  $cmd .= "WHERE data.weaponid=def.id ";
  $cmd .= " AND ($args->{WHERE}) " if $args->{WHERE};
  $cmd .= "GROUP BY data.weaponid ";
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_weapon {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = {};

  $values = $self->_getsumvars("weapon", $args, 'gametype');
  if (!$args->{PLRID}) {
    $cmd  = "SELECT $values, data.weaponid, def.name, def.desc ";
    $cmd .= "FROM $self->{c_gameweapons} as data, $self->{t_defs_weapons} as def ";
    $cmd .= "WHERE data.weaponid=def.id AND data.weaponid=$args->{WEAPONID} ";
    $cmd .= "GROUP BY data.weaponid ";
  } else {
    $cmd  = "SELECT $values, data.weaponid, def.name, def.desc ";
    $cmd .= "FROM $self->{c_plrweapons} as data, $self->{t_defs_weapons} as def ";
    $cmd .= "WHERE data.weaponid=def.id AND data.weaponid=$args->{WEAPONID} AND data.plrid=$args->{PLRID} ";
    $cmd .= "GROUP BY data.plrid, data.weaponid ";
  }
  $stats = $self->_get_rows_hash($cmd)->[0];

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_weapon_plrdata {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = [];

	# This QUERY is currently somewhat slow and should only generally be used once on a single page

	# NOTE: We're fetching PLR vars and not WEAPON vars here
  $values = $self->_get_simple_vars("plr", $args, 'weapons');
  $cmd  = "SELECT $values, plr.* ";
  $cmd .= "FROM $self->{c_plrweapons} as data ";
  $cmd .= "LEFT JOIN $self->{t_plr} as plr on plr.plrid=data.plrid ";
  $cmd .= "WHERE data.weaponid=$args->{WEAPONID} AND plr.plrid=data.plrid ";
  $cmd .= "AND plr.allowrank " unless $args->{ALLOWALL};
  $cmd .= "GROUP BY data.plrid ";
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub stats_get_totalclans {
  my ($self, $args) = @_;
  return $self->load_registered_clanlist({ TOTAL => 1, %$args });
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_get_totalplrs {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my ($olddate,$newdate) = $self->{ps}{saver}->get_statdate_range;
  my ($cmd, $total);

  $cmd  = "SELECT count(distinct plr.plrid) AS total FROM $self->{t_plr} as plr, $self->{c_plrdata} as data ";
  $cmd .= "WHERE plr.plrid=data.plrid ";
  $cmd .= "AND plr.allowrank != 0 " unless $args->{ALLOWALL};
  $cmd .= "AND ($args->{WHERE}) " if $args->{WHERE};
  $total = $self->_get_rows_array($cmd)->[0][0];		# total = list[first row][first field]

  return defined $total ? $total : 0;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_get_totalrcons {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
#  my ($olddate,$newdate) = $self->{ps}{saver}->get_statdate_range;
  my ($cmd, $total);

  $cmd  = "SELECT count(*) AS total FROM $self->{t_rcons} as data ";
  $cmd .= "WHERE $args->{WHERE} " if $args->{WHERE};
  $total = $self->_get_rows_array($cmd)->[0][0];		# total = list[first row][first field]

  return defined $total ? $total : 0;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_get_totalrconsummary {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
#  my ($olddate,$newdate) = $self->{ps}{saver}->get_statdate_range;
  my ($cmd, $total, $val);

  $val = $args->{VAR} || 'cmd';

  $cmd  = "SELECT count(distinct `$val`) AS total FROM $self->{t_rcons} as data ";
  $cmd .= "WHERE $args->{WHERE} " if $args->{WHERE};
  $total = $self->_get_rows_array($cmd)->[0][0];		# total = list[first row][first field]

  return defined $total ? $total : 0;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_get_totalweapons {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $prefix = $conf->{mysql}{tableprefix};
#  my ($olddate,$newdate) = $self->{ps}{saver}->get_statdate_range;
  my ($cmd, $total);

  $cmd = "SELECT count(*) FROM $self->{c_gameweapons} as data ";
  $cmd .= "WHERE 1 ";
#  $cmd .= "AND data.plrid=$args->{PLRID} " if $args->{PLRID};
  $cmd .= "AND ($args->{WHERE}) " if $args->{WHERE};
  $total = $self->_get_rows_array($cmd)->[0][0];		# total = list[first row][first field]

  return defined $total ? $total : 0;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_get_totalmaps {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my ($olddate,$newdate) = $self->{ps}{saver}->get_statdate_range;
  my ($cmd, $total);

  $cmd = "SELECT count(*) FROM $self->{c_gamemaps} as data ";
  $cmd .= "WHERE 1 ";
#  $cmd .= "AND data.plrid=$args->{PLRID} " if $args->{PLRID};
  $cmd .= "AND ($args->{WHERE}) " if $args->{WHERE};
  $total = $self->_get_rows_array($cmd)->[0][0];		# total = list[first row][first field]

  return defined $total ? $total : 0;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_get_plr_totalvictims {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my ($cmd, $total, $start, $end);

  $cmd = "SELECT count(distinct victimid) FROM $self->{c_plrvictims} as data ";
  $cmd .= "WHERE 1 ";
  $cmd .= "AND data.plrid=$args->{PLRID}" if $args->{PLRID};
  $total = $self->_get_rows_array($cmd)->[0][0];		# total = list[first row][first field]

  return defined $total ? $total : 0;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_get_clan_totalplrs {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my ($olddate,$newdate) = $self->{ps}{saver}->get_statdate_range;
  my ($cmd, $total);

  $cmd = "SELECT count(distinct d.plrid) FROM $self->{t_plrdata} as d, $self->{t_plr} as plr ";
  $cmd .= "WHERE plr.plrid=d.plrid AND plr.clanid=$args->{CLANID} ";
  $cmd .= "AND plr.allowrank " unless $args->{ALLOWALL};
#  $cmd .= "AND ($args->{WHERE}) " if $args->{WHERE};
  $cmd .= "AND (d.statdate BETWEEN '$olddate' AND '$newdate') AND (d.gametype=$self->{gid} AND d.modtype=$self->{mid})";
  $total = $self->_get_rows_array($cmd)->[0][0];		# total = list[first row][first field]

  return defined $total ? $total : 0;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_clan_plrlist {
  my ($self, $args) = @_;
  $args->{WHERE} = (defined $args->{WHERE} and $args->{WHERE} ne '') 
	? "(($args->{WHERE}) AND plr.clanid=$args->{CLANID})" 
	: "plr.clanid=$args->{CLANID}";
  return $self->stats_load_plrlist($args);
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_clan_weaponlist {
  my ($self, $args) = @_;
  my $dbh = $self->{dbh};
  my ($stats, $values, $cmd);
  $stats = [];

  $values = $self->_get_compile_sum_vars('plr', $args, 'weapons');  
  $cmd = "SELECT $values,data.weaponid,def.name,def.desc FROM $self->{t_plr} as plr ";
  $cmd .= "LEFT JOIN $self->{c_plrweapons} AS data ON data.plrid=plr.plrid ";
  $cmd .= "LEFT JOIN $self->{t_defs_weapons} AS def ON def.id=data.weaponid ";
  $cmd .= "WHERE plr.clanid=$args->{CLANID} AND NOT ISNULL(def.id) ";
  $cmd .= "AND plr.allowrank " unless $args->{ALLOWALL};
  $cmd .= "GROUP BY data.weaponid ";
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);
  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_clan_maplist {
  my ($self, $args) = @_;
  my $dbh = $self->{dbh};
  my ($stats, $values, $cmd);
  $stats = [];

  $values = $self->_get_compile_sum_vars('plr', $args, 'maps');
  $cmd = "SELECT $values,data.mapid,def.name,def.desc FROM $self->{t_plr} as plr ";
  $cmd .= "LEFT JOIN $self->{c_plrmaps} AS data ON data.plrid=plr.plrid ";
  $cmd .= "LEFT JOIN $self->{t_defs_maps} AS def ON def.id=data.mapid ";
  $cmd .= "WHERE plr.clanid=$args->{CLANID} AND NOT ISNULL(def.id) ";
  $cmd .= "AND plr.allowrank " unless $args->{ALLOWALL};
  $cmd .= "GROUP BY data.mapid ";
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);
  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_plrlist {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats, $sth, $ok, $data, $values, $vars, $static, $calcvars);
  $stats = [];

  $values = $self->_get_simple_vars('plr', $args, 'gametype', 'modtype');
  $cmd  = "SELECT pp.*,plr.*,cp.*,clan.*,$values FROM $self->{t_plr} as plr ";
  $cmd .= "LEFT JOIN $self->{c_plrdata} as data ON data.plrid=plr.plrid ";
  $cmd .= "LEFT JOIN $self->{t_clans} as clan ON clan.clanid=plr.clanid ";
  $cmd .= "LEFT JOIN $self->{t_plrprofile} as pp ON plr.$self->{uid}=pp.$self->{uid} ";
  $cmd .= "LEFT JOIN $self->{t_clansprofile} as cp ON cp.clantag=clan.clantag ";
  $cmd .= "WHERE 1 ";
  $cmd .= "AND plr.allowrank " unless $args->{ALLOWALL};
  $cmd .= "AND ($args->{WHERE}) " if $args->{WHERE};
  $cmd .= "GROUP BY plr.plrid " unless $args->{NOCALC};
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);
  # print "$cmd\n";
  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub load_map_day {
  my ($self, $mapid, $_date, $opts) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);      # is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my ($cmd, $stats, $sth, $ok, $row);
  $opts->{all} = 1 unless scalar keys %$opts;					# if nothing is specified we load everything
  $stats = {};

  $cmd = "SELECT m.*, def.name, def.desc ";
  $cmd .= "FROM $self->{t_gamemaps} as m, $self->{t_defs_maps} as def ";
  $cmd .= "LEFT JOIN $self->{t_gamemaps}_$conf->{modtype} as mm ON mm.gamemapid=m.gamemapid " unless $conf->{mysql}{NOMODTABLES}; #if $conf->{modtype};
  $cmd .= "WHERE m.mapid=def.id AND m.statdate='$date' AND m.mapid=$mapid ";
  $stats = $self->_get_rows_hash($cmd)->[0];

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub load_weapon_day {
  my ($self, $weaponid, $_date, $opts) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);      # is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my ($cmd, $stats, $sth, $ok, $row);
  $opts->{all} = 1 unless scalar keys %$opts;					# if nothing is specified we load everything
  $stats = {};

  $cmd = "SELECT m.*, def.name, def.desc ";
  $cmd .= "FROM $self->{t_gameweapons} as m, $self->{t_defs_weapons} as def ";
#  $cmd .= "LEFT JOIN $self->{t_gameweapons}_$conf->{modtype} as mm ON mm.gameweaponid=m.gameweaponid " unless $conf->{mysql}{NOMODTABLES}; #if $conf->{modtype};
  $cmd .= "WHERE m.weaponid=def.id AND m.statdate='$date' AND m.weaponid=$weaponid";
  $stats = $self->_get_rows_hash($cmd)->[0];

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Loads a player's information for a SINGLE DAY
# $date can either be an epoch second integer or a string 'YYYY-MM-DD' (recommended)
# $opts is a hash reference that specifies exactly what options are loaded
sub load_plr_day {
  my ($self, $plrid, $_date, $opts) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);      # is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my ($cmd, $stats, $sth, $ok, $mainid, $row);
  $opts->{all} = 1 unless scalar keys %$opts;					# if nothing is specified we load everything
  $stats = {};

  $cmd = "SELECT data.*, plr.* ";
  $cmd .= ", `mod`.* " if $self->{mid} and !$conf->{mysql}{NOMODTABLES};
  $cmd .= "FROM $self->{t_plrdata} as data ";
  $cmd .= "LEFT JOIN $self->{t_plr} as plr ON plr.plrid=$plrid ";
  $cmd .= "LEFT JOIN $self->{t_plrdata}_$conf->{modtype} AS `mod` ON mod.plrdataid=data.plrdataid " if $self->{mid} and !$conf->{mysql}{NOMODTABLES};
  $cmd .= "WHERE data.plrid=$plrid AND data.statdate='$date' ";
  $stats = $self->_get_rows_hash($cmd)->[0];

  if (!$self->{errstr}) {
    if ($opts->{all} or $opts->{weapons}) {
      $cmd  = "SELECT w.*,wd.name,wd.desc FROM $self->{t_plrweapons} as w, $self->{t_defs_weapons} as wd ";
      $cmd .= "WHERE wd.id=w.weaponid AND w.plrid=$plrid AND w.statdate='$date' ";
      my $list = $self->_get_rows_hash($cmd);
      if (defined $list) {
        foreach my $i (@$list) {
          $stats->{weapons}{ $i->{name} } = $i;
        }
      }
    }

    if ($opts->{all} or $opts->{roles}) {
      $cmd  = "SELECT w.*,rd.name,rd.desc FROM $self->{t_plrroles} as w, $self->{t_defs_roles} as rd ";
      $cmd .= "WHERE rd.id=w.roleid AND w.plrid=$plrid AND w.statdate='$date' ";
      my $list = $self->_get_rows_hash($cmd);
      if (defined $list) {
        foreach my $i (@$list) {
          $stats->{roles}{ $i->{name} } = $i;
        }
      }
    }

    if ($opts->{all} or $opts->{maps}) {
      $cmd  = "SELECT m.*,md.name,md.desc ";
      $cmd .= ",mm.* " unless $conf->{mysql}{NOMODTABLES}; #if $conf->{modtype};
      $cmd .= "FROM $self->{t_plrmaps} as m, $self->{t_defs_maps} as md ";
      $cmd .= "LEFT JOIN $self->{t_plrmaps}_$conf->{modtype} as mm ON mm.plrmapid=m.plrmapid " unless $conf->{mysql}{NOMODTABLES}; #if $conf->{modtype};
      $cmd .= "WHERE md.id=m.mapid AND m.plrid=$plrid AND m.statdate='$date'";
      my $list = $self->_get_rows_hash($cmd);     
      if (defined $list) { 
        foreach my $i (@$list) { 
          $stats->{maps}{ $i->{name} } = $i; 
        } 
      } 
    }

    if ($opts->{all} or $opts->{victims}) {
      $cmd = "SELECT v.* FROM $self->{t_plrvictims} as v WHERE v.plrid=$plrid AND v.statdate='$date'";
      my $list = $self->_get_rows_hash($cmd);     
      if (defined $list) { 
        foreach my $i (@$list) { 
          $stats->{victims}{ $i->{victimid} } = $i; 
        } 
      } 
    }

  }

  return wantarray ? %$stats : $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub save_map_day {
  my ($self, $map, $_date, $opts) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);	# is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my $mapid = $map->{gamemapid} || $self->get_mapid($map->{name});		# get map id or generate a new one
  my $oldstats = $self->load_map_day($mapid, $date);
  my $zero = $opts->{zero};							# should original plr values be zeroed?
  my ($vars, $val, $values, $cmd, $ok, $nextid);

  # SAVE COMPILED MAP DATA
  $self->save_compiled_map($map, $date);

  # SAVE GAMETYPE VARIABLES 
  $values = "";
  foreach ($scanner->get_map_vars('gametype')) {
    $val = (exists $map->{$_}) ? $map->{$_} || next : next;			# get value, ignore it if its '0' or ''
    $val += $oldstats->{$_} if exists $oldstats->{$_};
    delete $map->{$_} if $zero;							# remove the original
    $values .= "$_=$val,";
  }
  if ($oldstats->{gamemapid}) {
    $values = substr($values, 0, -1);
    $cmd = "UPDATE $self->{t_gamemaps} SET $values WHERE gamemapid=" . $oldstats->{gamemapid};
  } else {
    $cmd = "INSERT INTO $self->{t_gamemaps} SET $values mapid=$mapid,gametype=$self->{gid},modtype=$self->{mid},statdate='$date'";
  }

  if ($values) {
    $ok = $dbh->do($cmd);
    if (!$ok) {
      logerror(sprintf($lang->{err_savemaperror}, $map->{name}, $dbh->errstr) , 0, $conf->{verbose});
      return 0;							# don't save anything else if the primary stuff didn't work
    }
    $nextid = $dbh->{mysql_insertid} || $oldstats->{gamemapid};

    # SAVE MODTYPE VARIABLES (single row, references the $nextid above)
    unless ($conf->{mysql}{NOMODTABLES}) {
      $values = "";
      foreach ($scanner->get_map_vars('modtype')) {
        $val = (exists $map->{$_}) ? $map->{$_} || next : next;				# get value, ignore it if its 0
        $val += $oldstats->{$_} if exists $oldstats->{$_};
        delete $map->{$_} if $zero;								# remove the original
        $values .= "$_=$val,";
      }
      # reminder: $values HAS A TRAILING comma
      if ($values or !$oldstats->{gamemapid}) {
        if ($oldstats->{gamemapid}) {
          $values = substr($values,0,-1);
          $cmd = "UPDATE $self->{t_gamemaps}_$conf->{modtype} SET $values WHERE gamemapid=" . $oldstats->{gamemapid};
        } else {
          $cmd = "INSERT INTO $self->{t_gamemaps}_$conf->{modtype} SET ${values}gamemapid=$nextid";
        }
        $ok = $dbh->do($cmd);
        logerror(sprintf($lang->{err_savemapmoderror}, $map->{name}, $dbh->errstr) , 0, $conf->{verbose}) if !$ok;
      }
    }
  }
}
# --------------------------------------------------------------------------------------------------------------------------------
sub save_weapon_day {
  my ($self, $weapon, $_date, $opts) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);	# is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my $weaponid = $weapon->{gameweaponid} || $self->get_weaponid($weapon->{name});		# get weapon id or generate a new one
  my $oldstats = $self->load_weapon_day($weaponid, $date);
  my $zero = $opts->{zero};							# should original plr values be zeroed?
  my ($vars, $val, $values, $cmd, $ok, $nextid);

  # SAVE COMPILED WEAPON DATA
  $self->save_compiled_weapon($weapon, $date);

  # SAVE GAMETYPE VARIABLES 
  $values = "";
  foreach ($scanner->get_weapon_vars('gametype')) {
    $val = (exists $weapon->{$_}) ? $weapon->{$_} || next : next;			# get value, ignore it if its '0' or ''
    $val += $oldstats->{$_} if exists $oldstats->{$_};
    delete $weapon->{$_} if $zero;							# remove the original
    $values .= "$_=$val,";
  }
  if ($oldstats->{gameweaponid}) {
    $values = substr($values, 0, -1);
    $cmd = "UPDATE $self->{t_gameweapons} SET $values WHERE gameweaponid=" . $oldstats->{gameweaponid};
  } else {
    $cmd = "INSERT INTO $self->{t_gameweapons} SET $values weaponid=$weaponid,gametype=$self->{gid},modtype=$self->{mid},statdate='$date'";
  }
#  print "$cmd\n";
  $ok = $dbh->do($cmd);
  if (!$ok) {
    logerror(sprintf($lang->{err_saveweaponerror}, $weapon->{name}, $dbh->errstr) , 0, $conf->{verbose});
    return 0;							# don't save anything else if the primary stuff didn't work
  }
  $nextid = $dbh->{mysql_insertid} || $oldstats->{gameweaponid};
}
# --------------------------------------------------------------------------------------------------------------------------------
# Adds the current data in $plr to the compiled data in the DB for the player
sub save_compiled_plr {
  my ($self, $plr, $date) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my $plrid = $plr->{id} || $plr->{plrid} || $self->get_plrid($plr);
  my ($ok, $id, $cmd, $values, $tbl, $vars, $static, $idfunc, $itemid);
  my $plrstuff = [
	{
		'getvars' 	=> [ 'plr', undef, 'gametype', 'modtype' ],
		'calcvars'	=> { $scanner->get_plrcalc_vars('gametype'), $scanner->get_plrcalc_vars('modtype') },
		'table'		=> 'plrdata',
		'compiledid'	=> [ 'plrdata', "plrid=$plrid" ],
		'where'		=> "plrid=$plrid",
		'insert'	=> "startdate='$date',plrid=$plrid",
	},
  ];
  my $extraplrstuff = [
	{
		'getvars' 	=> [ 'plr', undef, 'maps', 'modtype_maps' ],
		'calcvars'	=> scalar $scanner->get_plrcalc_vars('maps'),
		'table'		=> 'plrmaps',
		'list'		=> 'maps',
		'compiledid'	=> [ 'plrmaps', "plrid=$plrid and mapid=%d" ],
		'idfunc'	=> 'get_mapid',
		'where'		=> "plrid=$plrid and mapid=%d",
		'insert'	=> "startdate='$date',plrid=$plrid,mapid=%d",
	},
	{
		'getvars' 	=> [ 'plr', undef, 'weapons' ],
		'calcvars'	=> scalar $scanner->get_plrcalc_vars('weapons'),
		'table'		=> 'plrweapons',
		'list'		=> 'weapons',
		'compiledid'	=> [ 'plrweapons', "plrid=$plrid and weaponid=%d" ],
		'idfunc'	=> 'get_weaponid',
		'where'		=> "plrid=$plrid and weaponid=%d",
		'insert'	=> "startdate='$date',plrid=$plrid,weaponid=%d",
	},
	{
		'getvars' 	=> [ 'plr', undef, 'roles' ],
		'calcvars'	=> scalar $scanner->get_plrcalc_vars('roles'),
		'table'		=> 'plrroles',
		'list'		=> 'roles',
		'compiledid'	=> [ 'plrroles', "plrid=$plrid and roleid=%d" ],
		'idfunc'	=> 'get_roleid',
		'where'		=> "plrid=$plrid and roleid=%d",
		'insert'	=> "startdate='$date',plrid=$plrid,roleid=%d",
	},
	{
		'getvars' 	=> [ 'plr', undef, 'victims' ],
		'calcvars'	=> scalar $scanner->get_plrcalc_vars('victims'),
		'table'		=> 'plrvictims',
		'list'		=> 'victims',
		'compiledid'	=> [ 'plrvictims', "plrid=$plrid and victimid=%d" ],
		'idfunc'	=> undef,
		'where'		=> "plrid=$plrid and victimid=%d",
		'insert'	=> "startdate='$date',plrid=$plrid,victimid=%d",
	},
  ];
  $ok = 0;

  foreach my $data (@$extraplrstuff) {
    ($vars, $static) = $self->_getvars(@{$data->{getvars}});
    $tbl = sprintf($self->{c_tblprefix}, $data->{table});
    $idfunc = $data->{idfunc};
    foreach my $item (keys %{$plr->{ $data->{list} }}) {
      my ($table, $where) = @{$data->{compiledid}};
      $itemid = $idfunc ? $self->$idfunc($item) : $item;
      $where = sprintf($where, $itemid);
      $id = $self->_get_compiledid($table, $where);
      $values = "";
      if ($id) {
        $cmd = "UPDATE $tbl SET ";
        foreach my $v (@$vars) {
          next unless defined $plr->{ $data->{list} }{$item}{$v} and $plr->{ $data->{list} }{$item}{$v} != 0;
          $values .= $static->{$v} ? "`$v`=" . $plr->{ $data->{list} }{$item}{$v} : "`$v`=`$v`+" . $plr->{ $data->{list} }{$item}{$v};
          $values .= ",";
        }

        if ($values) {
          foreach my $v (keys %{$data->{calcvars}}) {
            $values .= "`$v`=IFNULL(" . $data->{calcvars}{$v} . ",0.00),";
          }
          $values = substr($values, 0, -1) . sprintf(" WHERE $data->{where}", $itemid);
        }
      } else {
        $cmd = "INSERT INTO $tbl SET ";
        foreach my $v (@$vars) {
          $values .= "`$v`=" . $plr->{ $data->{list} }{$item}{$v} . "," if defined $plr->{ $data->{list} }{$item}{$v};
        }
        if ($values) {
          foreach my $v (keys %{$data->{calcvars}}) {
            $values .= "`$v`=IFNULL(" . $data->{calcvars}{$v} . ",0.00),";
          }
        }
        $values .= sprintf($data->{insert}, $itemid);
      }
      if ($values) {
        $cmd .= $values;
#        print "\n$cmd\n";
        $ok = $dbh->do($cmd);
        logerror("MYSQL ERROR in " . myfuncname() . "() for table '$data->{table}' at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
      }
    } # foreach $map ...
  } # foreach @extraplrstuff ...

  foreach my $data (@$plrstuff) {
    ($vars, $static) = $self->_getvars(@{$data->{getvars}});
    $tbl = sprintf($self->{c_tblprefix}, $data->{table});
    $id = $self->_get_compiledid(@{$data->{compiledid}});
    $values = "";
    if ($id) {
      $cmd = "UPDATE $tbl SET ";
      foreach my $v (@$vars) {
        next unless defined $plr->{$v} and $plr->{$v} != 0;
        $values .= $static->{$v} ? "`$v`=" . $plr->{$v} : "`$v`=`$v`+" . $plr->{$v};
        $values .= ",";
      }

      if ($values) {
        foreach my $v (keys %{$data->{calcvars}}) {
          $values .= "`$v`=IFNULL(" . $data->{calcvars}{$v} . ",0.00),";
        }
        $values = substr($values, 0, -1) . " WHERE $data->{where}";
      }
    } else {
      $cmd = "INSERT INTO $tbl SET ";
      foreach my $v (@$vars) {
        $values .= "`$v`=" . $plr->{$v} . "," if defined $plr->{$v};
      }
      if ($values) {
        foreach my $v (keys %{$data->{calcvars}}) {
          $values .= "`$v`=IFNULL(" . $data->{calcvars}{$v} . ",0.00),";
        }
      }
      $values .= $data->{insert};
    }
    if ($values) {
      $cmd .= $values;
#      print "\n$cmd\n";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() for table '$data->{table}' at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
    }
  }

  return $ok;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Adds the current data in $map to the compiled data in the DB for the global map
sub save_compiled_map {
  my ($self, $map, $date) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my $mapid = $map->{id} || $map->{mapid} || $self->get_mapid($map->{name});
  my ($ok, $id, $cmd, $values, $tbl, $vars, $static, $idfunc, $itemid);
  my $mapstuff = [
	{
		'getvars' 	=> [ 'map', undef, 'gametype', 'modtype' ],
		'table'		=> 'gamemaps',
		'compiledid'	=> [ 'gamemaps', "mapid=$mapid" ],
		'where'		=> "mapid=$mapid",
		'insert'	=> "startdate='$date',mapid=$mapid",
	},
  ];
  $ok = 0;

  foreach my $data (@$mapstuff) {
    ($vars, $static) = $self->_getvars(@{$data->{getvars}});
    $tbl = sprintf($self->{c_tblprefix}, $data->{table});
    $id = $self->_get_compiledid(@{$data->{compiledid}});
    $values = "";
    if ($id) {
      $cmd = "UPDATE $tbl SET ";
      foreach my $v (@$vars) {
        next unless defined $map->{$v} and $map->{$v} != 0;
        $values .= $static->{$v} ? "`$v`=" . $map->{$v} : "`$v`=`$v`+" . $map->{$v};
        $values .= ",";
      }
      $values = substr($values, 0, -1) . " WHERE $data->{where}" if $values;
    } else {
      $cmd = "INSERT INTO $tbl SET ";
      foreach my $v (@$vars) {
        $values .= "`$v`=" . $map->{$v} . "," if defined $map->{$v};
      }
      $values .= $data->{insert};
    }
    if ($values) {
      $cmd .= $values;
#      print "\n$cmd\n";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() for table '$data->{table}' at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
    }
  }

  return $ok;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Adds the current data in $weapon to the compiled data in the DB for the global weapon
sub save_compiled_weapon {
  my ($self, $weapon, $date) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my $weaponid = $weapon->{id} || $weapon->{weaponid} || $self->get_weaponid($weapon->{name});
  my ($ok, $id, $cmd, $values, $tbl, $vars, $static, $idfunc, $itemid);
  my $weaponstuff = [
	{
		'getvars' 	=> [ 'weapon', undef, 'gametype', 'modtype' ],
		'table'		=> 'gameweapons',
		'compiledid'	=> [ 'gameweapons', "weaponid=$weaponid" ],
		'where'		=> "weaponid=$weaponid",
		'insert'	=> "startdate='$date',weaponid=$weaponid",
	},
  ];
  $ok = 0;

  foreach my $data (@$weaponstuff) {
    ($vars, $static) = $self->_getvars(@{$data->{getvars}});
    $tbl = sprintf($self->{c_tblprefix}, $data->{table});
    $id = $self->_get_compiledid(@{$data->{compiledid}});
    $values = "";
    if ($id) {
      $cmd = "UPDATE $tbl SET ";
      foreach my $v (@$vars) {
        next unless defined $weapon->{$v} and $weapon->{$v} != 0;
        $values .= $static->{$v} ? "`$v`=" . $weapon->{$v} : "`$v`=`$v`+" . $weapon->{$v};
        $values .= ",";
      }
      $values = substr($values, 0, -1) . " WHERE $data->{where}" if $values;
    } else {
      $cmd = "INSERT INTO $tbl SET ";
      foreach my $v (@$vars) {
        $values .= "`$v`=" . $weapon->{$v} . "," if defined $weapon->{$v};
      }
      $values .= $data->{insert};
    }
    if ($values) {
      $cmd .= $values;
#      print "\n$cmd\n";
      $ok = $dbh->do($cmd);
      logerror("MYSQL ERROR in " . myfuncname() . "() for table '$data->{table}' at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
    }
  }

  return $ok;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Saves the player for the date specified. This should only be used to save a SINGLE DAYS worth of player stats
# $plr is a hash reference to the player data to save
# $_date can either be an epoch second integer or a string 'YYYY-MM-DD' (recommended)
# $opts is an optional hash reference of options that change the way the player is saved
sub save_plr_day {
  my ($self, $plr, $_date, $opts) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);	# is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my $plrid = $plr->{id} || $self->get_plrid($plr);				# get plr id or generate a new one
  my $oldstats = $self->load_plr_day($plrid, $date);
  my $zero = $opts->{zero};							# should original plr values be zeroed?
  my ($weaponid, $mapid, $vicid, $vars, $val, $values, $cmd, $mainid, $ptr, $items, $itemstats, $itemid, $key, $ok);
  my ($nextid);
  $plr->{id} = $plrid unless $plr->{id};					# save the new plrid if we just generated it
  $plr->{skill} = $conf->{baseskill} unless defined $plr->{skill};

  if (!$plrid) {			# We have an invalid player! Do not save them!
    logerror(sprintf($lang->{err_invalidplrerror}, $plr->{name}, "Invalid player ID"), 0, $conf->{verbose});
    print "\n--- DEBUGGING INFO: ---\n" . Dumper($plr);
    return 0;
  }

  # REGISTER PLAYER
  $cmd = "SELECT plrid,plrlastdate FROM $self->{t_plr} WHERE plrid=$plrid";
  my @testplr = $dbh->selectrow_array($cmd);
  if (!$testplr[0]) {
    $ok = $dbh->do("INSERT INTO $self->{t_plr} SET plrid=$plrid, skill=$plr->{skill}, prevskill=$plr->{skill}, name=" . 
	$dbh->quote($plr->{name}) . ", worldid=" . $dbh->quote($plr->{worldid}) . ", ipaddr=" . ip2int($plr->{ipaddr}) . 
	", plrlastdate='$date', gametype=$self->{gid}, modtype=$self->{mid}"
    );
    logerror(sprintf($lang->{err_regplrerror}, $plr->{name}, $dbh->errstr) , 0, $conf->{verbose}) if !$ok;
  } else {
    my $prevskill = (!$testplr[1] or $date gt $testplr[1]) ? "prevskill=skill," : "";
    $ok = $dbh->do("UPDATE $self->{t_plr} SET $prevskill skill=$plr->{skill}, plrlastdate='$date' WHERE plrid=$plr->{id}");
    logerror(sprintf($lang->{err_regplrerror}, $plr->{name}, "(updating) :: " . $dbh->errstr) , 0, $conf->{verbose}) if !$ok;
  }

  # SAVE COMPILED PLRDATA
  $ok = $self->save_compiled_plr($plr, $date);

  # SAVE GAMETYPE VARIABLES 
  $values = "";
  foreach ($scanner->get_plr_vars('gametype')) {
    $val = (exists $plr->{$_}) ? $plr->{$_} || next : next;			# get value, ignore it if its '0' or ''
    $val += $oldstats->{$_} if exists $oldstats->{$_};
    delete $plr->{$_} if $zero;							# remove the original
    $values .= "$_=$val,";
  }
  $values .= "oldskill=$plr->{skill}";
  if ($oldstats->{plrdataid}) {
    $cmd = "UPDATE $self->{t_plrdata} SET $values WHERE plrdataid=" . $oldstats->{plrdataid};
  } else {
    $cmd = "INSERT INTO $self->{t_plrdata} SET $values,plrid=$plrid,gametype=$self->{gid},modtype=$self->{mid},statdate='$date'";
  }
  $ok = $dbh->do($cmd);
  if (!$ok) {
    logerror(sprintf($lang->{err_saveplrerror}, $plr->{name}, $dbh->errstr) , 0, $conf->{verbose});
    return 0;							# don't save anything else if the primary stuff didn't work
  }
  $nextid = $dbh->{mysql_insertid} || $oldstats->{plrdataid};

  # SAVE MODTYPE VARIABLES (single row, references the $nextid above)
  unless ($conf->{mysql}{NOMODTABLES}) {
    $values = "";
    foreach ($scanner->get_plr_vars('modtype')) {
      $val = (exists $plr->{$_}) ? $plr->{$_} || next : next;				# get value, ignore it if its 0
      $val += $oldstats->{$_} if exists $oldstats->{$_};
      delete $plr->{$_} if $zero;								# remove the original
      $values .= "$_=$val,";
    }

    # reminder: $values HAS A TRAILING comma
    if ($values or !$oldstats->{plrdataid}) {
      if ($oldstats->{plrdataid}) {
        $values = substr($values,0,-1);
        $cmd = "UPDATE $self->{t_plrdata}_$conf->{modtype} SET $values WHERE plrdataid=" . $oldstats->{plrdataid};
      } else {
        $cmd = "INSERT INTO $self->{t_plrdata}_$conf->{modtype} SET ${values}plrdataid=$nextid";
      }
      $ok = $dbh->do($cmd);
      logerror(sprintf($lang->{err_saveplrmoderror}, $plr->{name}, $dbh->errstr), 0, $conf->{verbose}) if !$ok;
    }
  }

  # SAVE WEAPON VARIABLES
  $values = "";
  $items = $plr->{weapons};
  $itemstats = $oldstats->{weapons};
  foreach my $item (keys %$items) {
    $itemid = $self->get_weaponid($item);
    $values = "";
    foreach ($scanner->get_plr_vars('weapons')) {
      $val = (exists $items->{$item}{$_}) ? $items->{$item}{$_} || next : next;		# get value, ignore it if its 0
      $val += $itemstats->{$item}{$_} if exists $itemstats->{$item}{$_};
      $values .= "$_=$val,";
    }
    # reminder: $values HAS A TRAILING comma
    if ($values) {
      if ($itemstats->{$item}{plrweaponid}) {
        $values = substr($values,0,-1);
        $cmd = "UPDATE $self->{t_plrweapons} SET $values WHERE plrweaponid=" . $itemstats->{$item}{plrweaponid};
      } else {
        $cmd = "INSERT INTO $self->{t_plrweapons} SET ${values}plrid=$plrid,weaponid=$itemid,statdate='$date'";
      }
      $ok = $dbh->do($cmd);
      logerror(sprintf($lang->{err_saveplrweaponerror}, $plr->{name}, $item, $dbh->errstr), 0, $conf->{verbose}) if !$ok;
    }
    delete $items->{$item} if $zero;							# remove the original
  }

  # SAVE ROLE VARIABLES
  $values = "";
  $items = $plr->{roles};
  $itemstats = $oldstats->{roles};
  foreach my $item (keys %$items) {
    $itemid = $self->get_roleid($item);
    $values = "";
    foreach ($scanner->get_plr_vars('roles')) {
      $val = (exists $items->{$item}{$_}) ? $items->{$item}{$_} || next : next;		# get value, ignore it if its 0
      $val += $itemstats->{$item}{$_} if exists $itemstats->{$item}{$_};
      $values .= "$_=$val,";
    }
    # reminder: $values HAS A TRAILING comma
    if ($values) {
      if ($itemstats->{$item}{plrroleid}) {
        $values = substr($values,0,-1);
        $cmd = "UPDATE $self->{t_plrroles} SET $values WHERE plrroleid=" . $itemstats->{$item}{plrroleid};
      } else {
        $cmd = "INSERT INTO $self->{t_plrroles} SET ${values}plrid=$plrid,roleid=$itemid,statdate='$date'";
      }
      $ok = $dbh->do($cmd);
      logerror(sprintf($lang->{err_saveplrroleerror}, $plr->{name}, $item, $dbh->errstr), 0, $conf->{verbose}) if !$ok;
    }
    delete $items->{$item} if $zero;							# remove the original
  }

  # SAVE MAP VARIABLES
  $values = "";
  $items = $plr->{maps};
  $itemstats = $oldstats->{maps};
  foreach my $item (keys %$items) {
    $itemid = $self->get_mapid($item);
    $values = "";
    foreach ($scanner->get_plr_vars('maps')) {
      $val = (exists $items->{$item}{$_}) ? $items->{$item}{$_} || next : next;		# get value, ignore it if its 0
      $val += $itemstats->{$item}{$_} if exists $itemstats->{$item}{$_};
      $values .= "$_=$val,";
    }
    # reminder: $values HAS A TRAILING comma
    if ($values or !$itemstats->{$item}{plrmapid}) {
      if ($itemstats->{$item}{plrmapid}) {
        $values = substr($values,0,-1);
        $cmd = "UPDATE $self->{t_plrmaps} SET $values WHERE plrmapid=" . $itemstats->{$item}{plrmapid};
      } else {
        $cmd = "INSERT INTO $self->{t_plrmaps} SET ${values}plrid=$plrid,mapid=$itemid,statdate='$date'";
      }
#print "MAP    : $cmd\n";
      $ok = $dbh->do($cmd);
      logerror(sprintf($lang->{err_saveplrmaperror}, $plr->{name}, $item, $dbh->errstr), 0, $conf->{verbose}) if !$ok;
    }
    $nextid = $itemstats->{$item}{plrmapid} || $dbh->{mysql_insertid};

    # SAVE MAP MODTYPE VARIABLES
    unless ($conf->{mysql}{NOMODTABLES}) {
      $values = "";
      foreach ($scanner->get_plr_vars('modtype_maps')) {
        $val = (exists $items->{$item}{$_}) ? $items->{$item}{$_} || next : next;         # get value, ignore it if its 0
        $val += $itemstats->{$item}{$_} if exists $itemstats->{$item}{$_};
        $values .= "$_=$val,";
      }
      # reminder: $values HAS A TRAILING comma
      if ($values or !$itemstats->{$item}{plrmapid}) {
        if ($itemstats->{$item}{plrmapid}) {
          $values = substr($values,0,-1);
          $cmd = "UPDATE $self->{t_plrmaps}_$conf->{modtype} SET $values WHERE plrmapid=" . $itemstats->{$item}{plrmapid};
        } else {
          $cmd = "INSERT INTO $self->{t_plrmaps}_$conf->{modtype} SET ${values}plrmapid=$nextid";
        }
#print "MAP MOD: $cmd\n";
        $ok = $dbh->do($cmd);
        logerror(sprintf($lang->{err_saveplrmapmoderror}, $plr->{name}, $cmd, $dbh->errstr), 0, $conf->{verbose}) if !$ok;
      }
      delete $items->{$item} if $zero;							# remove the original
    }
  }

  # SAVE VICTIM VARIABLES
  $values = "";
  $items = $plr->{victims};
  $itemstats = $oldstats->{victims};
  foreach my $item (keys %$items) {
    $values = "";
    foreach ($scanner->get_plr_vars('victims')) {
      $val = (exists $items->{$item}{$_}) ? $items->{$item}{$_} || next : next;		# get value, ignore it if its 0
      $val += $itemstats->{$item}{$_} if exists $itemstats->{$item}{$_};
      $values .= "$_=$val,";
    }
    # reminder: $values HAS A TRAILING comma
    if ($values) {
      if ($itemstats->{$item}{plrvictimid}) {
        $values = substr($values,0,-1);
        $cmd = "UPDATE $self->{t_plrvictims} SET $values WHERE plrvictimid=" . $itemstats->{$item}{plrvictimid};
      } else {
        $cmd = "INSERT INTO $self->{t_plrvictims} SET ${values}plrid=$plrid,victimid=$item,statdate='$date'";
      }
      $ok = $dbh->do($cmd);
      logerror(sprintf($lang->{err_saveplrvictimerror}, $plr->{name}, $item, $dbh->errstr), 0, $conf->{verbose}) if !$ok;
    }
    delete $items->{$item} if $zero;							# remove the original
  }

}
# --------------------------------------------------------------------------------------------------------------------------------
# Keeps track of the 'compiled' tables. If an ID exists then we know we need to UPDATE info instead of INSERTING it in the 'save'
# methods. By caching the returned ID's we save a lot of extra queries to the DB.
sub _get_compiledid {
  my ($self, $tbl, $where) = @_;
  my $id = $self->{tables}{"$self->{compiledprefix}$tbl"}{$where} || 0;
  return $id if defined $self->{tables}{"$self->{compiledprefix}$tbl"}{$where};
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  $id = $dbh->selectrow_array("select count(*) as id from " . sprintf($self->{c_tblprefix}, $tbl) . " where $where");
  $self->{tables}{"$self->{compiledprefix}$tbl"}{$where} = $id if $id;
  return $id;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub save_rcon {
  my ($self, $rcon, $_date, $opts) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);	# is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $scanner = $self->{ps}{scanner};
  my $zero = $opts->{zero};							# should original plr values be zeroed?
  my ($vars, $val, $values, $cmd, $ok, $nextid);

  # SAVE GAMETYPE VARIABLES 
  $values = "";
  foreach (keys %$rcon) {
    delete $rcon->{$_} if $zero;							# remove the original
    $values .= "$_=" . $dbh->quote($rcon->{$_}) . ", ";
  }
  $cmd = "INSERT INTO $self->{t_rcons} SET $values gametype=$self->{gid},modtype=$self->{mid},statdate='$date'";
#  print "$cmd\n";
  $ok = $dbh->do($cmd);
  if (!$ok) {
    logerror(sprintf($lang->{err_saverconerror}, date("%Y-%m-%d %H:%i:%s",$rcon->{timestamp}), $dbh->errstr) , 0, $conf->{verbose});
  }
  return $ok;
}
# --------------------------------------------------------------------------------------------------------------------------------
# returns true if the clanid specified is locked or not
sub clan_is_locked {
  my ($self, $clanid) = @_;
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my ($locked, $cmd);

  $cmd  = "SELECT cp.clanlocked FROM $self->{t_clans} clan ";
  $cmd .= "LEFT JOIN $self->{t_clansprofile} as cp ON cp.clantag=clan.clantag ";
  $cmd .= "WHERE clanid=$clanid ";
  $cmd .= "LIMIT 1 ";
  $locked = $self->_get_rows_array($cmd)->[0][0];

  return $locked;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Fetches the database ID of the $plr given (by matching his name/won/ipaddr to a unique ID 
# If $dousage is true and is a numeric value then the 'total uses' for the player ID matched will be incremented by the amount 
# in $dousage, otherwise the 'total uses' will not be modified
# side-effect: the $plr hash reference passed in will have its {id} key updated with the ID matched in the DB
sub get_plrid {
  my ($self, $plr, $dousage) = @_;
  return 0 if not defined $plr->{name} or $plr->{name} eq '';				# always ignore players w/o a name
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my $ipaddr = ip2int($plr->{ipaddr} || 0);
  my $worldid = $dbh->quote($plr->{worldid});
  my $name = $dbh->quote($plr->{name});
  my ($ok, $id, @row, $cmd);

  $cmd  = "SELECT plrid FROM $self->{t_plrids} ";
  $cmd .= "WHERE name=$name AND worldid=$worldid AND ipaddr='$ipaddr' AND (gametype=$self->{gid} AND modtype=$self->{mid}) ";
  $cmd .= "LIMIT 1";
  $id = $self->_get_rows_array($cmd)->[0][0];

  if (!$id) {							# if there's no ID it doesn't exist, so we add it to the DB
    $ok = $dbh->do(
	"INSERT INTO $self->{t_plrids} SET " . 
	"name=$name, worldid=$worldid, ipaddr='$ipaddr', gametype=$self->{gid}, modtype=$self->{mid}"
    );
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 1) if !$ok;

  } elsif ($dousage) {						# the ID is valid, so we increment the 'totaluses' value
    $ok = $dbh->do("UPDATE $self->{t_plrids} SET totaluses=totaluses+$dousage WHERE plrid=$id");
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !$ok;
  }

  # Now we need to select the main player ID to use. Since multiple ID's will match on the same name, worldid or IP, we only 
  # want to match the first (minimum) ID found, the rest are just for statistical purposes and matching a players aliases.

  $cmd  = "SELECT MIN(plrid) FROM $self->{t_plrids} ";
  $cmd .= "WHERE (gametype=$self->{gid} AND modtype=$self->{mid}) AND $conf->{uniqueid}=";
  $cmd .= $conf->{uniqueid} ne 'ipaddr' ? $dbh->quote($plr->{ $conf->{plrid} }) : $ipaddr;
  $id = $self->_get_rows_array($cmd)->[0][0];
  
  $plr->{id} = $id;
  return $id;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Fetches the database ID of the $clantag given
sub get_clanid {
  my ($self, $clantag) = @_;
  my $id = $self->{table}{clanids}{ $clantag } || 0;
  return $id if $id;					# if ID is true then we already have its ID, no need to search the DB
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my $sql_tag = $dbh->quote($clantag);
  my ($ok, $locked, $profileid, @row, $cmd);
  # note, we're fetching an ARRAY and not a HASH, since its faster

  $cmd  = "SELECT clanid, clanprofileid, cp.clanlocked FROM $self->{t_clans} clan ";
  $cmd .= "LEFT JOIN $self->{t_clansprofile} as cp ON cp.clantag=clan.clantag ";
  $cmd .= "WHERE clan.clantag=$sql_tag AND (gametype=$self->{gid} AND modtype=$self->{mid}) ";
  $cmd .= "LIMIT 1 ";
  ($id, $profileid, $locked) = @$id if ( $id = $self->_get_rows_array($cmd)->[0] );

  return undef if ( $locked );

  if (!$id) {							# if there's no ID it doesn't exist, so we add it to the DB
    $ok = $dbh->do("INSERT INTO $self->{t_clans} SET clantag=$sql_tag, gametype=$self->{gid}, modtype=$self->{mid}, clanupdatetime=" . time());
    if (!$ok) {
      logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 1);
    } else {
      $id = $dbh->{mysql_insertid};
      $dbh->do("INSERT INTO $self->{t_clansprofile} SET clantag=$sql_tag") if !$profileid;	# add an empty profile for clan
    }
  }

  $self->{table}{clanids}{ $clantag } = $id;
  return $id;
}
# --------------------------------------------------------------------------------------------------------------------------------
# generic 'get id' method to fetch an ID for any definition table 
sub _get_id {
  my ($self, $table, $name, $desc, $g, $m) = @_;
  my $id = $self->{table}{ $table }{ $name } || 0;
  return $id if $id;					# if ID is true then we already have its ID, no need to search the DB
  my $dbh = $self->{dbh};
  my $conf = $self->{ps}{conf};
  my $where = "";
  my $set = "";
  my ($ok,$cmd);

  if ($g and $m) {
    $where = " AND (gametype=$g AND modtype=$m)";
    $set = ", gametype=$g, modtype=$m";
  } elsif ($g) {
    $where = " AND gametype=$g";
    $set = ", gametype=$g";
  } elsif ($m) {
    $where = " AND modtype=$m";
    $set = ", modtype=$m";
  }

  $cmd  = "SELECT id,`desc` FROM " . $conf->{mysql}{tableprefix} . "defs_$table ";
  $cmd .= "WHERE name=" . $dbh->quote($name) . " $where "; 
  $cmd .= "LIMIT 1";

  $id = $self->_get_rows_array($cmd)->[0][0];
  $set = ", `desc`=" . $dbh->quote($desc) . $set if defined $desc;

  if (!$id) {
    $ok = $dbh->do("INSERT INTO " . $conf->{mysql}{tableprefix} . "defs_$table SET name=" . $dbh->quote($name) . $set);
    if ($ok) {
      $id = $dbh->{mysql_insertid};
    } else {
      $id = 0;
      logerror("MYSQL ERROR in " . myfuncname() . "() (table: $table) at line " . __LINE__ . ": " . $dbh->errstr, 0, 1);
    }
  }

  $self->{table}{ $table }{ $name } = $id;
  return $id;
}
# --------------------------------------------------------------------------------------------------------------------------------
sub get_gametypeid {
  my ($self, $name) = @_;
  return $self->_get_id('gametypes', $name, undef);
}
sub get_modtypeid {
  my ($self, $name) = @_;
  return $self->_get_id('modtypes', $name, undef, $self->{gid});
}
sub get_eventid {
  my ($self, $name) = @_;
  return $self->_get_id('events', $name, undef, $self->{gid}, $self->{mid});
}
sub get_teamid {
  my ($self, $name) = @_;
  return $self->_get_id('teams', $name, undef, $self->{gid}, $self->{mid});
}
sub get_roleid {
  my ($self, $name) = @_;
  return $self->_get_id('roles', $name, undef, $self->{gid}, $self->{mid});
}
sub get_weaponid {
  my ($self, $name, $desc) = @_;
  return $self->_get_id('weapons', $name, $desc, $self->{gid}, $self->{mid});
}
sub get_mapid {
  my ($self, $name) = @_;
  return $self->_get_id('maps', $name, undef, $self->{gid}, $self->{mid});
}
sub get_awardid {
  my ($self, $name, $desc) = @_;
  return $self->_get_id('awards', $name, $desc, $self->{gid}, $self->{mid});
}
# --------------------------------------------------------------------------------------------------------------------------------
# Loads the most recent value for the player $var. If $decay is set to an epoch timestamp then the value will be 
# decayed from that timestamp and the previous timestamp.
# This is pretty much only useful for getting a player's current 'skill'.
sub load_plr_var_decay {
  my ($self, $plrid, $var, $decay) = @_;
  my $conf = $self->{ps}{conf};
  my $decayconf = $conf->{decay};
  my $dbh = $self->{dbh};
  my $val = undef;

  if ($decay and $decayconf->{$var}) {
#    my @row = $dbh->selectrow_array("SELECT $var,statdate,p.worldid FROM $self->{t_plrdata} as d, $self->{t_plr} as p WHERE d.plrid=$plrid and p.plrid=$plrid ORDER BY statdate DESC LIMIT 1");
    my @row = $dbh->selectrow_array("SELECT $var,statdate FROM $self->{t_plrdata} as d, $self->{t_plr} as p WHERE d.plrid=$plrid and p.plrid=$plrid ORDER BY statdate DESC LIMIT 1");
    if (scalar @row) {
      my $diff = diffdays_ymd(date("%Y-%m-%d",$decay), $row[1]);	# difference from previous and current $decay day
      if ($diff > 1) {							# dont decay unless there's at least 2 days difference
        $val = $row[0] - (($row[0] * ($decayconf->{$var} / 100)) * $diff);
        $val = $decayconf->{"min$var"} if defined $decayconf->{"min$var"} and $val < $decayconf->{"min$var"};
#        print "DECAYED: $row[2]: $val < $row[0] ($diff days)\n";			# $val should now be less-than $row[0] (its orig value)
      } else {
        $val = $row[0];							# nothing to decay, so just return the orig value
      }
    }
  } else {								# no decay, just fetch the value
    $val = $dbh->selectrow_array("SELECT $var FROM $self->{t_plrdata} as d, $self->{t_plr} as p WHERE d.plrid=$plrid AND p.plrid=$plrid ORDER BY statdate DESC LIMIT 1");
  }
#  print $val ? "Previous $var ($plrid) = $val\n" : "No previous value for $var\n";
  return $val;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Loads a variable from the players basic data (or mod data) from the date given or undef if there is no value for the date
sub load_plr_var{
  my ($self, $plrid, $var, $_date) = @_;
  my $date = (index($_date, '-') > 0) ? $_date : date('%Y-%m-%d', $_date);	# is it 'YYYY-MM-DD' or an epoch integer?
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my ($cmd, $value);
  $cmd = "SELECT `$var` FROM $self->{t_plrdata} WHERE plrid='$plrid' AND statdate='$date' LIMIT 1";
  $value = $dbh->selectrow_array($cmd);

#  if (!defined $value and $conf->{modtype}) {					# search the modtype table if plrdata was undef
  if (!defined $value and $conf->{mysql}{NOMODTABLES}) {			# search the modtype table if plrdata was undef
    $cmd  = "SELECT m.$var FROM $self->{t_plrdata} AS d, $self->{t_plrdata}_$conf->{modtype} AS m ";
    $cmd .= "WHERE d.plrid='$plrid' AND d.statdate='$date' AND d.id=m.id LIMIT 1";
    $value = $dbh->selectrow_array($cmd);
  }

  return $value;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Returns the most recent statdate from the database
# Useful for determining an ending point when selecting a bunch of data from the dataset.
# The date will be returned in YYYY-MM-DD format unless $epoch is true in which case the date will be in epoch seconds
# If there is no date in the DB return 0
sub get_last_statdate {
  my ($self, $epoch) = @_;
  my $date;
  if ($epoch) {
    $date = $self->{dbh}->selectrow_array("SELECT MAX(UNIX_TIMESTAMP(statdate)) FROM $self->{ps}{conf}{mysql}{tableprefix}plrdata LIMIT 1");
  } else {
    $date = $self->{dbh}->selectrow_array("SELECT MAX(statdate) FROM $self->{ps}{conf}{mysql}{tableprefix}plrdata LIMIT 1");
  }
  return $date || 0;
}
# -------------------------------------------------------------------------------------------------------------------------------
# Returns a 2 element array containing the OLDEST and NEWEST dates to use for ranking players. The 'oldest' date will not
# necessarily be an actual date with stats. Its the oldest date that is ALLOWED to have stats.
# The dates will be returned in YYYY-MM-DD format unless $epoch is true in which case the date will be in epoch seconds
sub get_statdate_range {
  my ($self, $epoch) = @_;
  my $newest = $self->{ps}{conf}{rankfromtoday} ? time() : $self->get_last_statdate(1) || time();
  my $oldest = $newest - ($self->{ps}{conf}{rankmaxdays} * (60*60*24));
  my $ary = $epoch ? [ $oldest, $newest ] : [ date("%Y-%m-%d", $oldest), date("%Y-%m-%d", $newest) ];
  return wantarray ? @$ary : $ary;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_rcon {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $scanner = $self->{ps}{scanner};
  my $dbh = $self->{dbh};
  my ($cmd, $stats);
  $stats = {};

  $cmd  = "SELECT * ";
  $cmd .= "FROM $self->{t_rcons} as data ";
  $cmd .= "WHERE data.rconid='$args->{RCONID}' ";
  $stats = $self->_get_rows_hash($cmd)->[0];

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
#
sub stats_load_rconlist {
  my ($self, $args) = @_;
  my ($cmd, $stats, $start, $end);
  $stats = [];

  $start = $args->{STARTDATE} || date("%Y-%m-%d");			# default to todays date
  $start = date('%Y-%m-%d', $start) unless index($start, '-') > 0;
  $end = $args->{ENDDATE} || date("%Y-%m-%d");
  $end = date('%Y-%m-%d', $end) unless index($end, '-') > 0;

  $cmd  = "SELECT * FROM $self->{t_rcons} as data ";
  $cmd .= "WHERE (data.gametype=$self->{gid} AND data.modtype=$self->{mid}) ";
  $cmd .= "AND ($args->{WHERE}) " if $args->{WHERE};
  $cmd .= "AND (statdate BETWEEN '$start' AND '$end') " if $args->{STARTDATE};
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);

  return $stats;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Loads a summary rcon list. The list will contain the unique 'value' and the total times that value was seen in the DB.
sub stats_load_rconlist_summary {
  my ($self, $args) = @_;
  my ($cmd, $stats, $start, $end, $val);
  $stats = [];

  $start = $args->{STARTDATE} || date("%Y-%m-%d");			# default to todays date
  $start = date('%Y-%m-%d', $start) unless index($start, '-') > 0;
  $end = $args->{ENDDATE} || date("%Y-%m-%d");
  $end = date('%Y-%m-%d', $end) unless index($end, '-') > 0;

  $val = $args->{VAR} || 'cmd';

  $cmd  = "SELECT `$val`, `$val` as value, count(`$val`) AS total, MIN(`timestamp`) AS min_timestamp, MAX(`timestamp`) AS max_timestamp ";
  $cmd .= "FROM $self->{t_rcons} as data ";
  $cmd .= "WHERE (data.gametype=$self->{gid} AND data.modtype=$self->{mid}) ";
  $cmd .= "AND ($args->{WHERE}) " if $args->{WHERE};
  $cmd .= "AND (statdate BETWEEN '$start' AND '$end') " if $args->{STARTDATE};
  $cmd .= "GROUP BY `$val` ";
  $cmd .= $self->_getsortorder($args);
  $stats = $self->_get_rows_hash($cmd);

  return $stats;
}
# -------------------------------------------------------------------------------------------------------------------------------
# Returns a 2 element HASHREF containing the oldest and newest dates with actual stats. Unlike the get_statdate_range method the 
# dates returned here will be the extreme dates that ACTUALLY have stats recorded.
# argument: 'FORMAT' specifies a date() format to return the dates in. If its not specified the EPOCH timestamp is returned
sub stats_get_date_range {
  my ($self, $args) = @_;
  my $conf = $self->{ps}{conf};
  my $dbh = $self->{dbh};
  my $prefix = $conf->{mysql}{tableprefix};
  my ($stats, $cmd, $newest, $oldest);
  $stats = {};
  $args->{TABLE} ||= 'plrdata';			# what table to query? (any table with a 'statdate' field) do not include 'prefix'

  if ($args->{USECONF}) {			# If 'useconf' then load the entire range .... 
    $newest = $self->{ps}{conf}{rankfromtoday} ? time() : $self->get_last_statdate(1) || time();
    $oldest = $newest - ($self->{ps}{conf}{rankmaxdays} * (60*60*24));

  } else {					# else, load the absolute/actual range ...
    $cmd = "SELECT MIN(UNIX_TIMESTAMP(statdate)), MAX(UNIX_TIMESTAMP(statdate)) FROM ${prefix}$args->{TABLE} LIMIT 1";
    ($oldest, $newest) = $dbh->selectrow_array($cmd);
    logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) if !defined $oldest and $dbh->errstr;
  }

  if (defined $oldest and defined $newest) {
    if ($args->{FORMAT}) {
      $oldest = date($args->{FORMAT}, $oldest);
      $newest = date($args->{FORMAT}, $newest);
    }
    $stats = { $args->{OLDEST} || 'oldest' => $oldest, $args->{NEWEST} || 'newest' => $newest };
  }

  if ($args->{ARRAY}) {
    return wantarray ? ( $oldest, $newest ) : [ $oldest, $newest ];
  } else {
    return wantarray ? %$stats : $stats;
  }
}
# --------------------------------------------------------------------------------------------------------------------------------
sub save_event {
  my $self = shift;
  my $event = ref $_[0] eq 'HASH' ? shift : { @_ };
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my ($ok, $cmd);

  return 0 unless $event->{timestamp};
  my $eventid 	= $self->get_eventid($event->{event});
  my $plrid1 	= $self->get_plrid($event->{plr1});
  my $plrid2 	= ($event->{plr2}) ? $self->get_plrid($event->{plr2}) : 0;
  my $mapid 	= ($event->{map}) ? $self->get_mapid($event->{map}) : 0;
  my $weaponid 	= ($event->{weapon}) ? $self->get_weaponid($event->{weapon}) : 0;
  my $teamid 	= ($event->{plr1}{team}) ? $self->get_teamid($event->{plr1}{team}) : 
	($event->{team}) ? $self->get_teamid($event->{team}) : 0;

  $cmd = "INSERT INTO $conf->{mysql}{tableprefix}events SET
	id=NULL,
	timestamp=$event->{timestamp},
	eventid=$eventid,
	plrid1=$plrid1,
	plrid2=$plrid2,
	weapon=$weaponid,
	map=$mapid,
	team=$teamid,
	gametype=$self->{gid},
	modtype=$self->{mid}
  ";
  $ok = $dbh->do($cmd);
  logerror(sprintf($lang->{err_saveeventerror}, $dbh->errstr)) unless $ok;

#  echo("saving event $event->{event}\n", $event->{timestamp});
  return $ok;
}
# --------------------------------------------------------------------------------------------------------------------------------
# deletes all old pages in the cache. passing $holdtime will override what is currently in the config
sub cache_delete_expired { 
  my ($self, $holdtime) = @_;
  my $conf = $self->{ps}{conf};
  my $lang = $self->{ps}{lang};
  my $dbh = $self->{dbh};
  my $now = time;
  $holdtime = $conf->{cache}{holdtime} unless defined $holdtime;
  return unless $holdtime;
  $holdtime *= 60;
  my $res = $dbh->do("DELETE FROM $self->{t_cache} WHERE createdate+$holdtime < $now");
  $dbh->do("OPTIMIZE TABLE $self->{t_cache}");
  logerror("MYSQL ERROR in " . myfuncname() . "() at line " . __LINE__ . ": " . $dbh->errstr, 0, 1) unless $res;
}
# --------------------------------------------------------------------------------------------------------------------------------
# Loads a cached dynamic page that matches the page and the query string given. 
# $contentref is a reference to a scalar to append the cached page content too (if present).
# Returns the CREATE DATE of the cached page if present, or an undef/false value if there is no cached page
sub cache_load_page {
  my ($self, $page, $query, $contentref) = @_;
  my $conf = $self->{ps}{conf};
  my $