/*****************************************************************************
*
*  Module:  plugin_mapvote
*  
*  Description:  Handles voting to change the current map, and helper 
*                functions to display and administrate map changes.
*
*  Revision History:  08-16-01, initial creation by zagor
*                     10-01-01, revised by dodger, bug fixes
*                     11-28-01, revised by dodger, mass rewrite, restructure,
*                               and documentation, uses LogD 1.00
*                     12-01-01, revised by dodger, vote ratio bug fixed
*                     12-02-01, revised by dodger, only changes maps on round
*                               completion
*                     12-02-01, revised by dodger, fixed quarantine bug, 
*                               incorporated substring validation (written by
*                               <niklas_martinsson@yahoo.se>)
*                     12-10-01, revised by dodger and zyrain, extended
*                               function of denymap, fixed numerous bugs, 
*                               prints errors privately, etc.
*                     04-15-02, revised by dodger, added no player timeout fix
*                               to the vote_start() function
*                     04-17-02, revised by dodger, no longer randomly picks 
*                               extend 
*                     05-06-01, revised by dodger, uses new changelevel() API
*
*  DevNotes:  I should fix the cancelvote to wait admin_vote_freq minutes 
*             before letting people vote again.
*
*****************************************************************************/
 
#include <core>
#include <console>
#include <string>
#include <admin>
#include <adminlib>
 
#define ACCESS_HLDSLD_VOTE 1
#define ACCESS_CONTROL_VOTE 2
#define ACCESS_CONSOLE 131072
 
/*****************************************************************************
*  MAP_INVALID must be negative, or else functions which return map indexes
*  may fail.
*****************************************************************************/
#define MAP_INVALID -1;
 
new STRING_VERSION[MAX_DATA_LENGTH] = "3.20";
 
/*****************************************************************************
*  Little array of strings to make sentences plural.
*****************************************************************************/
new s[2][] = { "s", "" };
 
/*****************************************************************************
*  MAX_MAPS is the maximum number of maps in rotation.  This number should be
*  slightly greater than the number of entries in the mapcycle.txt file.
*****************************************************************************/
const MAX_MAPS = 64;
 
/*****************************************************************************
*  i_LastMapCount is the number of maps denied for any reason.
*****************************************************************************/
new i_LastMapCount = 0;
 
/*****************************************************************************
*  QuarantinedCount is the number of valid maps in the quarantine file.
*****************************************************************************/
new i_QuarantineCount = 0;
 
/*****************************************************************************
*  VOTE_TIME is the length of time, in minutes, for which voting will be 
*  allowed once a map vote has begun.
*****************************************************************************/
new VOTE_TIME = 4;
 
/*****************************************************************************
*  i_DefaultMap is the next map to which the server will transition if there is
*  no successful map vote.  i_NextMap is the next map to which the server will
*  transition if there is a successful map vote.
*****************************************************************************/
new i_DefaultMap = MAP_INVALID;
new i_NextMap = MAP_INVALID;
 
/*****************************************************************************
*  epoch_VoteTimeStart contains the timestamp when a map vote is started, so 
*  that ending time may be calculated.
*****************************************************************************/
new epoch_VoteTimeStart = 0;
 
/*****************************************************************************
*  i_MPTimelimit contains the locally cached value of the server variable 
*  mp_timelimit.
*****************************************************************************/
new i_MPTimelimit = 30;
 
/*****************************************************************************
*  MapNames is a list of the string names of all the maps in mapcycle.txt
*****************************************************************************/
new MapNames[MAX_MAPS][MAX_NAME_LENGTH];
 
/*****************************************************************************
*  i_MapCount is the number of array entries in MapNames
*****************************************************************************/
new i_MapCount = 0;
 
/*****************************************************************************
*  LastMaps is a list of the string names of all the maps in lastmaps.txt
*****************************************************************************/
new LastMaps[MAX_MAPS][MAX_NAME_LENGTH];
 
/*****************************************************************************
*  CurrentMap is the string name of the current map
*****************************************************************************/
new CurrentMap[MAX_NAME_LENGTH];
 
/*****************************************************************************
*  epoch_MapStart is a timestamp for the start of the map.  epoch_MapEnd is 
*  the calculated time for the end of the map.  epoch_NextVote is the 
*  timestamp for when the next vote is allowed.
*****************************************************************************/
new epoch_MapStart = 0;
new epoch_MapEnd = 0;
new epoch_NextVote = 0;
 
/*****************************************************************************
*  e_MapVoteStatus is an enum that contains the status of voting on the 
*  current map.  The enum values are detailed in the vote_status enum type.
*****************************************************************************/
enum vote_status {
  NOTALLOWED,     /* The map is less that admin_vote_freq minutes 
                     old or a vote has been called within admin_vote_freq
                     minutes, so a new vote may not be called. */
  ALLOWED,        /* A vote may be called. */
  PENDING,        /* A vote has been called, but is not yet complete. */
  COMPLETEVALID,  /* A vote has been completed and one map has won. */
}
 
new vote_status:e_MapVoteStatus = NOTALLOWED;
 
/*****************************************************************************
*  MAP_EXTEND is a string containing the keyword to be used when a user
*  wishes to extend the current map.
*****************************************************************************/
new MAP_EXTEND[] = "extend";
 
/*****************************************************************************
*  i_ExtendCount is an integer containing the number of times the current map 
*  has been extended due to previous votes.
*****************************************************************************/
new i_ExtendCount = 0;
 
/*****************************************************************************
*  UserVote is an array containing the current votes of all the users.  Each
*  element is either MAP_INVALID, or contains a valid map index.
*****************************************************************************/
new UserVote[MAX_PLAYERS + 1] = {MAP_INVALID,...};
 
 
/*****************************************************************************
*
*  Function:  admin_cancelvote
*  
*  Description:  cancels the current vote, but allows a new vote to be called
*                at any time.
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, matches new functions
*
*  DevNotes:  We may want to switch this to change the status to NOTALLOWED, 
*             and start the normal admin_vote_freq timer to allow a new vote.
*             We'll see after some user testing.
*
*****************************************************************************/
public admin_cancelvote( HLCommand, HLData, HLUserName, UserIndex ) {
 
  new s_user[MAX_NAME_LENGTH];
  convert_string( HLUserName, s_user, MAX_NAME_LENGTH );
 
  if( e_MapVoteStatus == PENDING ) {
    say( "[VOTE] Vote has been cancelled." );
    e_MapVoteStatus = ALLOWED;
  } else {
    messageex( s_user, "[VOTE] No map vote in progress.", print_console );
  }
 
  return PLUGIN_HANDLED;
 
}
 
 
/*****************************************************************************
*
*  Function:  admin_denymap
*  
*  Description:  removes any votes which may exist for a particular map, and
*                denies any additional votes for that map.  the deny list is
*                reset at the end of the current map.
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, matches new functions
*                     12-10-01, revised by dodger and zyrain, now removes 
*                               votes and denies further votes
*
*****************************************************************************/
public admin_denymap( HLCommand, HLData, HLUserName, UserIndex ) {
 
  new s_map[MAX_DATA_LENGTH];
  convert_string( HLData, s_map, MAX_DATA_LENGTH );
  new i_mapindex = helper_getmapindex( s_map );
  if( i_mapindex == MAP_INVALID ) {
 
    new s_user[MAX_NAME_LENGTH];
    convert_string( HLUserName, s_user, MAX_NAME_LENGTH );
    messageex( s_user, "[VOTE] Map does not exist.", print_console );
 
  } else {
 
    new i;
    new i_votesremoved = 0;
    for( i = 0; i < MAX_PLAYERS; i++ ) {
      if( UserVote[i] == i_mapindex ) {
        UserVote[i] = MAP_INVALID;
        i_votesremoved++;
      }
    }
 
    new s_text[MAX_TEXT_LENGTH];
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] %s is now off limits.  %d vote%s for %s removed.", s_map, i_votesremoved, s[i_votesremoved == 1], s_map );
    say( s_text );
 
    strcpy( LastMaps[i_LastMapCount++], s_map, MAX_DATA_LENGTH );
 
  }
 
  return PLUGIN_HANDLED;
 
}
 
 
/*****************************************************************************
*
*  Function:  admin_map
*  
*  Description:  changes the map immediately
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, matches new functions
*  
*****************************************************************************/
public admin_map( HLCommand, HLData, HLUserName, UserIndex ) {
 
  new s_command[MAX_COMMAND_LENGTH];
  new s_data[MAX_DATA_LENGTH];
  new s_user[MAX_NAME_LENGTH];
 
  convert_string( HLCommand, s_command, MAX_COMMAND_LENGTH );
  convert_string( HLData, s_data, MAX_DATA_LENGTH );
  convert_string( HLUserName, s_user, MAX_NAME_LENGTH );
 
  new i_targetmapindex = helper_getmapindex( s_data );
 
  if( i_targetmapindex != MAP_INVALID ) {
    say_command( s_user, s_command, s_data );
    changelevel( s_data, 0 );
  } else {
    messageex( s_user, "[VOTE] Map not found on server.", print_console );
  }
 
  return PLUGIN_HANDLED;
 
}
 
 
/*****************************************************************************
*
*  Function:  admin_startvote
*  
*  Description:  starts a map vote, regardless of whether or not a vote is 
*                normally allowed.
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, matches new functions
*                     12-10-01, revised by dodger and zyrain, simplified
*                               interface.
*
*****************************************************************************/
public admin_startvote( HLCommand, HLData, HLUserName, UserIndex ) {
 
  e_MapVoteStatus = ALLOWED;
  vote_start();
 
  return PLUGIN_HANDLED;
 
}
 
 
/*****************************************************************************
*
*  Function:  file_loadmaps
*  
*  Description:  loads the mapcycle.txt file and generates an array of maps
*                based on the contents.  if MAX_MAPS is too small, the 
*                remaining maps will be ignored.
*
*  
*  Revision History:  08-16-01, initial creation by zagor
*  
*****************************************************************************/
file_loadmaps() {
 
  new s_buffer[MAX_NAME_LENGTH];
  new i_count = 0;
  new i;
 
  for( i = 0; i < MAX_MAPS - 1; i++ ) {
 
    /* read the next map from file */
    if( !readfile( "mapcycle.txt", s_buffer, i + 1, MAX_NAME_LENGTH ) ) {
      break;
    }
 
    /* if the map name can be validated, insert it into the array */
    if( valid_map( s_buffer ) ) {
      strcpy( MapNames[i_count++], s_buffer, MAX_NAME_LENGTH );
 
    }
  }
 
  /* insert the extend option */
  strcpy( MapNames[i_count++], MAP_EXTEND, MAX_NAME_LENGTH );
  i_MapCount = i_count;
 
  return;
 
}
 
 
/*****************************************************************************
*
*  Function:  file_loadLastMaps
*  
*  Description:  loads the LastMaps.txt file and generates an array of 
*                quarantined maps based on the contents.  the number of
*                quarantined maps is based on the number of maps in the file,
*                so it must be seeded with default values.
*
*  Revision History:  08-16-01, initial creation by zagor
*                     12-10-01, revised by dodger and zyrain, dynamically
*                               counts maps
*  
*****************************************************************************/
file_loadLastMaps() {
 
  new s_buffer[MAX_NAME_LENGTH];
  new i;
 
  /* load past maps */
  for( i = 0; i < MAX_MAPS; i++ ) {
    if( !readfile( "lastmaps.txt", s_buffer, i + 1, MAX_NAME_LENGTH ) ) {
      break;
    }
    if( valid_map( s_buffer ) ) {
      strcpy( LastMaps[i_QuarantineCount++], s_buffer, MAX_NAME_LENGTH );
    }
  }
 
  /* add current map to the top of list */
  resetfile( "lastmaps.txt" );
  writefile( "lastmaps.txt", CurrentMap );
  for( i = 0; i < ( i_QuarantineCount - 1 ); i++ ) {
    if( !writefile( "lastmaps.txt", LastMaps[i] ) ) {
      break;
    }
  }
 
  i_LastMapCount = i_QuarantineCount;
 
  return;
 
}
 
 
/*****************************************************************************
*
*  Function:  helper_countdownallow
*  
*  Description:  helper_countdownallow loads the server variable admin_vote_freq, 
*                then waits that many seconds before switching the vote status
*                to allow a vote to take palce.
*
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, no longer clobbers a vote
*                               in progress that was started by an admin
*  
*****************************************************************************/
helper_countdownallow() {
 
  new admin_vote_freq = getvar( "admin_vote_freq" );
 
  if( admin_vote_freq > 0 ) {
    e_MapVoteStatus = NOTALLOWED;
    epoch_NextVote = systemtime() + admin_vote_freq;
  } else {
    e_MapVoteStatus = ALLOWED;
  }
 
  return;
 
}
 
 
/*****************************************************************************
*
*  Function:  helper_getmapindex
*  
*  Description:  Searches for a map index corresponding to a string map name.
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, properly returns 
*                               MAP_INVALID on failure to find map
*
*****************************************************************************/
helper_getmapindex( s_map[] ) {
 
  new i;
  for( i = 0; i < MAX_MAPS; i++ ) {
    if( streq( s_map, MapNames[i] ) || ( strcasestrx( MapNames[i], s_map ) != -1 ) ) {
      return i;
    }
  }
 
  return MAP_INVALID;
 
}
 
 
/*****************************************************************************
*
*  Function:  helper_mapend
*  
*  Description:  an interrupt vector style handler that parses worldspawn 
*                events to find the end of round, and handles map changes
*                appropriately.
*  
*  Preconditions:  i_DefaultMap must contain a valid map index, or this
*                  function will throw an AMX_ERR_BOUNDS exception
*
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, uses new LogD 1.00 Events
*
*****************************************************************************/
public helper_mapend( HLCommand, HLData, HLUserName, UserIndex ) {
 
  /* if Worldspawn signals anything besides the end of a round,
     ignore the signal */
  new s_data[MAX_DATA_LENGTH];
  convert_string( HLData, s_data, MAX_DATA_LENGTH );
  if( strcasecmp( s_data, "Round_End" ) != 0 ) {
    return PLUGIN_CONTINUE;
  }
 
  /* If a vote has been completed successfully, go to the voted on map */
  if( e_MapVoteStatus == COMPLETEVALID ) {
    if( i_NextMap != MAP_INVALID ) {
      changelevel( MapNames[i_NextMap], 0 );
      return PLUGIN_CONTINUE;
    } else {      
      say( "[VOTE] ERROR - Next map index is invalid.  Switching to default map." );
      changelevel( MapNames[i_DefaultMap], 0 );
      return PLUGIN_CONTINUE;
    }
  }
 
  /* otherwise, check for end of map timeout, but only transition on a
     round boundary.  go to the default map, randomly generated at 
     beginning */
  new i_timeleft = epoch_MapEnd - systemtime();
  new i_timeneeded = 300;
  i_timeneeded = ( getvar( "mp_roundtime" ) * 60 );
  if( i_timeneeded > i_timeleft ) {
    changelevel( MapNames[i_DefaultMap], 0 );
    return PLUGIN_CONTINUE;
  }
 
  return PLUGIN_CONTINUE;
 
}
 
 
/*****************************************************************************
*
*  Function:  helper_randommap
*  
*  Description:  randomly chooses a new map, excluding all quarantined maps 
*                and the current map
*  
*  Revision History:  08-16-01, initial creation by zagor
*  Revision History:  04-17-02, revised by dodger, no longer picks extend
*
*  DevNotes:  We may want to pick a non-random map instead, by picking a map 
*             from the bottom half/third of the least frequently played maps
*
*****************************************************************************/
helper_randommap( notthis ) {
 
  new i_next;
  new i;
  for( ; ; ) {
    i_next = random( i_MapCount - 1 );
    for ( i = 0; i < i_LastMapCount; i++ ) {
      if( streq( MapNames[i_next], LastMaps[i] ) ) {
        continue;
      }
    }
    if( i_next == notthis ) {
      continue;
    }
    break;
  }
 
  return i_next;
 
}
 
 
/*****************************************************************************
*
*  Function:  helper_resetvotes
*  
*  Description:  resets all user votes to newly initialized state in 
*                preparation for a new map vote
*  
*  Revision History:  08-16-01, initial creation by zagor
*  
*****************************************************************************/
helper_resetvotes() {
  new i;
  for( i = 0; i < MAX_PLAYERS; i++ ) {
    UserVote[i] = MAP_INVALID;
  }
 
  i_NextMap = MAP_INVALID;
 
  return;
 
}
 
 
/*****************************************************************************
*
*  Function:  helper_validatemap
*  
*  Description:  Attempts to validate map, even if the prefix is left off.
*                and the current map.  Returns map index if successful, or
*                -1 if failed.
*  
*  Caveat:  The string copy length is hard coded at 30 characters.  It should
*           be plenty, and it keeps it fast, but if map names get too long, 
*           this will fail to validate those maps.
*
*  Revision History:  08-16-01, initial creation by zagor
*                     11-24-01, revised by <niklas_martinsson@yahoo.se>,
*                               improved version
*                     11-28-01, revised by dodger, uses the same getmapindex
*                               function as everyone else now
*                     12-10-01, revised by dodger and zyrain, now the extend 
*                               option is a map choice just like other maps
*
*****************************************************************************/
helper_validatemap( s_map[] ) {
 
  new i;
  new i_bestrank = 0;  /* 0=none, 1=middle, 2=last, 3=start, 4="xx_"-equal */
  new i_bestindex;     /* index of map with highest score */
  new i_score;
  new i_substringstatus;
 
  /* if the vote is for extend, or for too short of a string to match, fail */
  if( strlen( s_map ) < 2 ) {
    return MAP_INVALID;
  }
 
  /* check for different types of matches */
  for( i = 0; i < i_MapCount; i++ ) {
    /* if it's a direct match, succeed */
    if( streq( MapNames[i], s_map ) ) {
      return i;
    }
 
    i_score = 0;
    i_substringstatus = strstr( MapNames[i], s_map );
    switch( i_substringstatus ) {
      /* substring does not match at all */
      case -1: { }
 
      case 0:
        if( ( strlen( s_map ) > 3 ) && ( i_bestrank < 3 ) ) {
          /* substring match at beginning, including prefix */
          i_score = 3;
        }
 
      /* substring matches except for prefix */  
      case 3:
        if( strlen( s_map ) == strlen( MapNames[i] ) - 3 ) {
          /* perfect match */
          i_score = 4;
        } else {
          /* substring match, except for prefix */
          i_score = 3;
        }
 
      default:
        if( i_substringstatus + strlen( s_map ) == strlen( MapNames[i] ) ) {
          /* substring matches at end */
          i_score = 2;
        } else {
          /* substring matches at end */
          i_score = 1;
        }
    }
 
    /* if this match is better than the previous best, promote it */    
    if( i_score > i_bestrank ) {
      i_bestrank = i_score;
      i_bestindex = i;
    }
 
  }
 
  /* if there is a best map, return success */
  if( i_bestrank > 0 ) {
    strcpy( s_map, MapNames[i_bestindex], MAX_NAME_LENGTH );
    return i_bestindex;
  }
 
  return MAP_INVALID;
 
}
 
 
/*****************************************************************************
*
*  Function:  say_listmaps
*  
*  Description:  prints list of maps, number of votes, and status to console
*
*  Revision History:  08-16-01, initial creation by zagor
*                     12-10-01, revised by dodger and zyrain, now prints 
*                               deny status and current status (wasn't 
*                               printing current map at all!)
*  
*****************************************************************************/
public say_listmaps( HLCommand, HLData, HLUserName, UserIndex ) {
 
  new s_text[MAX_TEXT_LENGTH];
  new s_user[MAX_NAME_LENGTH];
  convert_string( HLUserName, s_user, MAX_NAME_LENGTH );
 
  new i;
  new i_map;
  new mapvotes[MAX_MAPS] = {0,...};
  for( i = 1; i <= maxplayercount(); i++ ) {
    i_map = UserVote[i];
    if( i_map != MAP_INVALID ) {
      mapvotes[i_map] = mapvotes[i_map] + 1;
    }
  }
 
  new j;
  new q[MAX_TEXT_LENGTH];
  for( i = 0; i < i_MapCount; i++ ) {
 
    q[0] = 0;
    if( streq( CurrentMap, MapNames[i] ) ) {
      strcpy( q, "(CURRENT)", MAX_TEXT_LENGTH );        
    } else {
      for( j = 0; j < i_LastMapCount; j++ ) {
        if( streq( LastMaps[j], MapNames[i] ) ) {
          if( j < i_QuarantineCount ) {
            snprintf( q, MAX_TEXT_LENGTH, "(QUARANTINED)" );
          } else {
            snprintf( q, MAX_TEXT_LENGTH, "(DENIED)" );
          }
          break;
        }
      }
    }
 
    snprintf( s_text, MAX_TEXT_LENGTH,"%2d:  %s  (%d votes)  %s", i, MapNames[i], mapvotes[i], q );
    messageex( s_user, s_text, print_console );
  }
 
  return PLUGIN_HANDLED;
}
 
 
/*****************************************************************************
*
*  Function:  vote_calcwinner
*  
*  Description:  vote_calcwinner calculates the winning map (if any) during a
*                vote.  if the saynow parameter is 0, it will do so silently,
*                otherwise it will announce the winning map to all the users.
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     12-10-01, revised by dodger and zyrain, calculates votes
*                               properly
*                     05-06-02, revised by dodger, fixed i_maxvotes count
*
*****************************************************************************/
vote_calcwinner( saynow = 0 ) {
 
  new i_currentvotes[MAX_MAPS] = {0,...};
 
  /* calculate the minimum number of votes needed to win the map vote.
     reduce the number by one, so you have a number you need to beat to
     change maps. */
  new i_ratio = getvar( "map_ratio" );
  new i_maxvotes = 1;
 
  if( i_ratio > 0 ) {
    i_maxvotes = ( playercount() * i_ratio ) / 100;
    i_maxvotes--;
  }
 
  if( i_maxvotes < 0 ) {
    i_maxvotes = 0;
  }
 
  /* tally the votes in i_currentvotes. */
  new i_player;
  new i_maxplayers = maxplayercount();
  new i_map;
  new i_winner = MAP_INVALID;
  for( i_player = 1; i_player <= i_maxplayers; i_player++ ) {
    i_map = UserVote[i_player];
    if( i_map != MAP_INVALID ) {
      /* if this map is winning, raise i_maxvotes to show new threshold */
      i_currentvotes[i_map] = i_currentvotes[i_map] + 1;
      if( i_currentvotes[i_map] > i_maxvotes ) {
        i_maxvotes = i_currentvotes[i_map];
        i_winner = i_map;
      }
    }
  }
 
  new s_text[MAX_TEXT_LENGTH];
  new s_prepend[MAX_NAME_LENGTH];
  if( saynow != 0 ) {
    if( i_winner == MAP_INVALID ) {
      snprintf( s_text, MAX_TEXT_LENGTH, "No map has the %d votes required yet.", i_maxvotes + 1 );
      say( s_text );
    } else {
      if( i_NextMap != i_winner ) {
        if( streq( MapNames[i_winner], MAP_EXTEND ) == 1 ) {
          snprintf( s_prepend, MAX_NAME_LENGTH, "[VOTE] Extend for %i minutes", i_MPTimelimit );
        } else {
          strcpy( s_prepend, MapNames[i_winner], MAX_NAME_LENGTH );
        }
 
        snprintf( s_text, MAX_TEXT_LENGTH, "%s is winning with %d vote%s.", s_prepend, i_maxvotes, s[i_maxvotes == 1] );
        say( s_text );
      }
    } 
 
  }
 
  i_NextMap = i_winner;
  return i_winner;
}
 
 
/*****************************************************************************
*
*  Function:  vote_process
*  
*  Description:  validates and processes a user's vote, including removing old 
*                votes if necessary
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, matches new functions
*                     12-10-01, revised by dodger and zyrain, doesn't reverse
*                               text, nor is it dependent on global text 
*                               variable
*
*****************************************************************************/
vote_process( s_user[], i_userindex, map[] ) {
 
  new i;
  new s_text[MAX_TEXT_LENGTH];
 
  new i_newmap = helper_validatemap( map );
  if( i_newmap == MAP_INVALID ) {
    return 0;
  }
 
  /* user is attempting to vote for a map */
  if( e_MapVoteStatus != PENDING ) {
    messageex( s_user, "[VOTE] No map vote in progress.", print_chat );
    return 1;
  }
 
  if( streq( CurrentMap, map ) ) {
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] %s is currently running.  Vote extend instead.", map );
    messageex( s_user, s_text, print_chat );    
    return 1;
  }
 
  if( streq( map, MAP_EXTEND ) == 1 ) {
    new i_maxextend = getvar( "admin_vote_maxextend" );
    if( i_maxextend && i_ExtendCount >= i_maxextend ) {
      messageex( s_user, "[VOTE] Map cannot be extended any longer.", print_chat );
      return 1;
    }
  }    
 
  for( i = 0; i < i_LastMapCount; i++ ) {
 
    if( streq( LastMaps[i], map ) ) {
      if( i < i_QuarantineCount ) {
        snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] %s was played %d map%s ago.  %d maps must pass before it is played again.", map, i_QuarantineCount - i, s[i_QuarantineCount - i == 1], i_QuarantineCount );
      } else {
        snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] %s was denied by admin.", map );
      }
 
      say( s_text );
      return 1;
    }
 
  }
 
  if( UserVote[i_userindex] != MAP_INVALID ) {
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Removing your previous vote for %s.", MapNames[UserVote[i_userindex]] );
    messageex( s_user, s_text, print_chat );
  }
 
  UserVote[i_userindex] = i_newmap;
 
 
  /* calculate total number of votes for this map */
  new i_player;
  new i_votecount = 0;
  new i_maxplayers = maxplayercount();
  for( i_player = 0; i_player <= i_maxplayers; i_player++ ) {
    if( UserVote[i_player] == i_newmap ) {
      i_votecount++;
    }
  }
 
  new s_maptext[MAX_NAME_LENGTH];
  if( streq( MapNames[i_newmap], MAP_EXTEND ) == 1 ) {
    snprintf( s_maptext, MAX_NAME_LENGTH, "Extend for %i minutes", i_MPTimelimit);
  } else {
    strcpy( s_maptext, MapNames[i_newmap], MAX_NAME_LENGTH );
  }
 
  snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Vote by %s -- %s now has %d vote%s.", s_user, s_maptext, i_votecount, s[i_votecount == 1] );
  say( s_text );
 
  vote_calcwinner( 1 );
 
  return 1;
}
 
 
/*****************************************************************************
*
*  Function:  vote_start
*  
*  Description:  vote_start attempts to start a new map vote
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, matches new functions
*                     04-15-02, revised by dodger, added no player timeout fix
*
*****************************************************************************/
vote_start() {
 
  new s_text[MAX_TEXT_LENGTH];
 
  /* check to see if user has enough access to vote */
  /* DevNotes:  This should be modified to use some other say routine
     than reject_message
  if( check_auth( ACCESS_HLDSLD_VOTE ) == 0 ) {
    reject_message( 0 );
    return;
  }
  */
 
  /* check to see if there is enough time left in the round to vote */
  new i_timeleft = epoch_MapEnd - systemtime();
  if( i_timeleft < ( VOTE_TIME * 60 ) ) {
    say( "[VOTE] Not enough time left on map to vote." );
 
    /* this is a weird boundary condition that should not exist, since
       we should be getting Map End events when there are no players
       on the server.  since it doesn't work, though, we can brute-force
       a correction here when someone tries to start a vote */
    if( i_timeleft < 0 ) {
      changelevel( MapNames[i_DefaultMap], 0 );
    }
 
    return;
  }
 
  if( e_MapVoteStatus == PENDING ) {
    say( "[VOTE] Map vote already in progress." );
    return;
  }
 
  if( e_MapVoteStatus == ALLOWED ) {
 
    helper_resetvotes();
 
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Map voting enabled for %d minutes.", VOTE_TIME );
    say( s_text );
    say( "[VOTE] Say 'vote mapname' to vote." );
 
    new i_maxextend = getvar( "admin_vote_maxextend" );
    if( ( i_ExtendCount < i_maxextend ) || ( i_maxextend == 0 ) ) {
      snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Say 'vote %s' to extend map.", MAP_EXTEND );
      say( s_text );
    }
 
    epoch_VoteTimeStart = systemtime();
    e_MapVoteStatus = PENDING;
 
    return;
 
  }
 
  if( e_MapVoteStatus == NOTALLOWED ) {
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Map vote not allowed for another %d minutes.", ( ( epoch_NextVote - systemtime() ) / 60 ) + 1 );
    say( s_text );
    return;
  }
 
  if( e_MapVoteStatus == COMPLETEVALID ) {
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Map vote already completed." );
    say( s_text );    
    return;
  }
 
  return;
 
}
 
 
/*****************************************************************************
*
*  Function:  HandleSay
*  
*  Description:  an interrupt vector style handler that parses user say 
*                commands for vote related messages.
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, cut down on parse commands
*                               to improve performance
*                     12-17-01, revised by dodger, hammered performance but
*                               dropped reliance on timer, so it should work
*                               properly with anticheat timer-abusing scripts
*                     01-08-02, revised by dodger, fixed stupid global replace
*                               error introduced last revision
*
*****************************************************************************/
public HandleSay( HLCommand, HLData, HLUserName, UserIndex ) {
 
  new s_data[MAX_DATA_LENGTH];
  new s_text[MAX_TEXT_LENGTH];
  new s_user[MAX_NAME_LENGTH];
 
  /* this is a bit of a hack to fix the negative vote time problem caused
     by excessive use of timer events.  now, whenever someone says something,
     we check to see if there is a current vote, and if so, if it should be
     ended. */
  if( e_MapVoteStatus == PENDING ) {
    if( systemtime() > ( epoch_VoteTimeStart + ( VOTE_TIME * 60 ) ) ) {
 
      say( "[VOTE] Voting is now over." );
 
      /* calculate the winning map, if any */
      new i_winner = vote_calcwinner( 0 );
      if( i_winner == MAP_INVALID ) {
        say( "[VOTE] No map got enough votes to win." );
        /* assume timer doesn't cross map boundaries */
        helper_countdownallow();
 
      } else if( streq( MapNames[i_winner], MAP_EXTEND ) == 1 ) {
 
        snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Map will be extended for %d minutes.", i_MPTimelimit );
        say( s_text );    
 
        i_ExtendCount++;
        epoch_MapEnd = epoch_MapEnd + ( i_MPTimelimit * 60 );
        helper_countdownallow();
 
      } else {
        snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Next map will be %s.", MapNames[i_NextMap] );
        say( s_text );
        e_MapVoteStatus = COMPLETEVALID;
      }
    }
  } else if( e_MapVoteStatus == NOTALLOWED ) {
    if( systemtime() > epoch_NextVote ) {
      e_MapVoteStatus = ALLOWED;
    }
  }
 
  new word[MAX_DATA_LENGTH];
  new word2[MAX_DATA_LENGTH];
 
  new voteresult = MAP_INVALID;
 
  convert_string( HLData, s_data, MAX_DATA_LENGTH );
  convert_string( HLUserName, s_user, MAX_DATA_LENGTH );
  strstripquotes( s_data );
  strsplit( s_data, " ", word, MAX_DATA_LENGTH, word2, MAX_DATA_LENGTH );
 
  if( streq( word, "rockthevote" ) || streq( word, "mapvote" ) ) {
    /* user is attempting to start a new vote */
    vote_start();
    return PLUGIN_HANDLED;
  }
 
  if( streq( word, "timeleft" ) ) {
    /* user is attempting to determine time left in map */
    new left = epoch_MapEnd - systemtime();
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Time remaining on map: %d:%02d", left / 60, left % 60 );
    messageex( s_user, s_text, print_chat );
    return PLUGIN_HANDLED;
  }
 
  if( streq( word, "votetime" ) ) {
    /* user is attempting to determine time left in vote */
    if( e_MapVoteStatus == PENDING ) {
      new i_checktime = epoch_VoteTimeStart + ( VOTE_TIME * 60 ) - systemtime();
      snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Time remaining to vote: %d:%02d", i_checktime / 60, i_checktime % 60 );
      messageex( s_user, s_text, print_chat );
    } else {
      messageex( s_user, "[VOTE] No map vote in progress.", print_chat );
    }
    return PLUGIN_HANDLED;
  }
 
  if( streq( word, "nextmap" ) ) {
    /* user is attempting to determine next map */
    voteresult = vote_calcwinner( 0 );      
    if( voteresult == MAP_INVALID ) {
      snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Next map will be: %s", MapNames[i_DefaultMap] );
    } else {
      snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Next map will be: %s", MapNames[i_NextMap] );
    }
    messageex( s_user, s_text, print_chat );
    return PLUGIN_HANDLED;
  }
 
  if( streq( word, "currentmap" ) ) {
    /* user is attempting to determine current map (!) */
    snprintf( s_text, MAX_TEXT_LENGTH, "[VOTE] Current map is %s.  Press i for info.", CurrentMap );
    messageex( s_user, s_text, print_chat );
    return PLUGIN_HANDLED;    
  }
 
  if( streq( word, "vote" ) || streq( word, "votemap" ) ) {
    if( vote_process( s_user, UserIndex, word2 ) ) {
      return PLUGIN_HANDLED;
    } else {
      messageex( s_user, "[VOTE] Map not found on server.", print_chat );
    }
  }
 
  return PLUGIN_CONTINUE;
 
}
 
 
/*****************************************************************************
*
*  Function:  plugin_connect
*  
*  Description:  Run on user connect, resets user's current vote.
*  
*  Revision History:  08-16-01, initial creation by zagor
*
*****************************************************************************/
public plugin_connect( HLUserName, HLIP, UserIndex ) {
  if( ( UserIndex >= 1 ) && ( UserIndex <= MAX_PLAYERS ) ) {
    UserVote[UserIndex] = MAP_INVALID;
  }
  return PLUGIN_CONTINUE;
}
 
 
/*****************************************************************************
*
*  Function:  plugin_disconnect
*  
*  Description:  Run on user disconnect, resets user's current vote.
*  
*  Revision History:  08-16-01, initial creation by zagor
*
*****************************************************************************/
public plugin_disconnect( HLUserName, UserIndex ) {
  if( ( UserIndex >= 1 ) && ( UserIndex <= MAX_PLAYERS ) ) {
    UserVote[UserIndex] = MAP_INVALID;
  }
  return PLUGIN_CONTINUE;
}
 
 
/*****************************************************************************
*
*  Function:  plugin_init
*  
*  Description:  Register functions with halflife engine and with LogD event
*                handler.
*  
*  Revision History:  08-16-01, initial creation by zagor
*                     11-28-01, revised by dodger, matches new functions
*                     05-03-02, revised by dodger, hardened random map picker
*                               to guarantee i_DefaultMap is valid
*  
*****************************************************************************/
public plugin_init() {
 
  plugin_registerinfo( "Admin hlds_ld-style Map Vote Plugin", "Runs a chat-based interface map vote, similar to hlds_ld.", STRING_VERSION );
 
  plugin_registercmd( "admin_cancelvote", "admin_cancelvote", ACCESS_CONTROL_VOTE, "admin_cancelvote: Cancels the current hlds_ld vote." );
  plugin_registercmd( "admin_denymap", "admin_denymap", ACCESS_CONTROL_VOTE, "admin_denymap <map>: Removes all votes for map." );
  plugin_registercmd( "admin_map", "admin_map", ACCESS_CONTROL_VOTE, "admin_map <map>: Changes map." );
  plugin_registercmd( "admin_startvote", "admin_startvote", ACCESS_CONTROL_VOTE, "admin_startvote: Starts an hlds_ld vote." );
  plugin_registercmd( "listmaps", "say_listmaps", ACCESS_HLDSLD_VOTE );  
  plugin_registercmd( "say", "HandleSay", ACCESS_ALL );
 
  plugin_registercmd( "helper_mapend", "helper_mapend", ACCESS_CONSOLE, "" );
 
  plugin_registerhelp( "say", ACCESS_HLDSLD_VOTE, "say mapvote: Starts an hlds_ld vote." );
  plugin_registerhelp( "say", ACCESS_HLDSLD_VOTE, "say rockthevote: Starts an hlds_ld vote." );
  plugin_registerhelp( "say", ACCESS_HLDSLD_VOTE, "say timeleft: Returns the amount of time left in the current map." );
  plugin_registerhelp( "say", ACCESS_HLDSLD_VOTE, "say vote <map>: Places a vote for the map." );
  plugin_registerhelp( "say", ACCESS_HLDSLD_VOTE, "say votetime: Returns the amount of time left in current map vote." );
 
  /* register helper_mapend to receive all type 62 (World Action) messages */
  exec( "logd_reg 62 admin_command helper_mapend" );
 
  /* read mp_timelimit so that we can use the value, then clobber the server
     cvar so that we don't accidentally get the map switching out from under 
     us.  we'll assume that five hours is plenty of time */
  i_MPTimelimit = getvar( "mp_timelimit" );
  exec( "mp_timelimit 300" );
 
  /* calculate start and end of map */
  epoch_MapStart = systemtime();
  epoch_MapEnd = epoch_MapStart + ( i_MPTimelimit * 60 );
 
  currentmap( CurrentMap, MAX_NAME_LENGTH );
  file_loadmaps();
  file_loadLastMaps();
 
  /* try to pick a random map.  if it keeps failing, give up and pick map 1 */
  new counter = 0;
  while( ( i_DefaultMap == MAP_INVALID ) && ( counter < 5 ) ) {
    i_DefaultMap = helper_randommap( helper_getmapindex( CurrentMap ) );
  }
  if( i_DefaultMap == MAP_INVALID ) {
    i_DefaultMap = 1;
  }
 
  helper_countdownallow();
 
  return PLUGIN_CONTINUE;
 
}