-- -- racemidvote_server.lua -- -- Mid-race random map vote and -- NextMapVote handled in this file --   local lastVoteStarterName = '' local lastVoteStarterCount = 0 g_ForcedNextMap = false   ---------------------------------------------------------------------------- -- displayHilariarseMessage -- -- Comedy gold ---------------------------------------------------------------------------- function displayHilariarseMessage( player )     if not player then         lastVoteStarterName = ''     else         local playerName = getPlayerName(player)         local msg = ''         if playerName == lastVoteStarterName then             lastVoteStarterCount = lastVoteStarterCount + 1             if lastVoteStarterCount == 5 then                 msg = playerName .. ' started a vote. Hardly a suprise.'             elseif lastVoteStarterCount == 10 then                 msg = 'Guess what! '..playerName .. ' started ANOTHER vote!'             elseif lastVoteStarterCount < 5 then                 msg = playerName .. ' started another vote.'             else                 msg = playerName .. ' continues to abuse the vote system.'             end         else             lastVoteStarterCount = 0             lastVoteStarterName = playerName             msg = playerName .. ' started a vote.'         end         outputRace( msg )     end end     ---------------------------------------------------------------------------- -- displayKillerPunchLine -- -- Sewing kits available in the foyer ---------------------------------------------------------------------------- function displayKillerPunchLine( player )     if lastVoteStarterName ~= '' then         outputRace( 'Offical news: Everybody hates ' .. lastVoteStarterName )     end end     ---------------------------------------------------------------------------- -- startMidMapVoteForRandomMap -- -- Start the vote menu if during a race and more than 30 seconds from the end -- No messages if this was not started by a player ---------------------------------------------------------------------------- function startMidMapVoteForRandomMap(player)       -- Check state and race time left     if not stateAllowsRandomMapVote() or g_CurrentRaceMode:getTimeRemaining() < 30000 then         if player then             outputRace( "I'm afraid I can't let you do that, " .. getPlayerName(player) .. ".", player )         end         return     end       displayHilariarseMessage( player )     exports.votemanager:stopPoll()       -- Actual vote started here     local pollDidStart = exports.votemanager:startPoll {             title='Do you want to change to a random map?',             percentage=51,             timeout=15,             allowchange=true,             visibleTo=getRootElement(),             [1]={'Yes', 'midMapVoteResult', getRootElement(), true},             [2]={'No', 'midMapVoteResult', getRootElement(), false;default=true},     }       -- Change state if vote did start     if pollDidStart then         gotoState('MidMapVote')     end   end addCommandHandler('new',startMidMapVoteForRandomMap)     ---------------------------------------------------------------------------- -- event midMapVoteResult -- -- Called from the votemanager when the poll has completed ---------------------------------------------------------------------------- addEvent('midMapVoteResult') addEventHandler('midMapVoteResult', getRootElement(),     function( votedYes )         -- Change state back         if stateAllowsRandomMapVoteResult() then             gotoState('Running')             if votedYes then                 startRandomMap()             else                 displayKillerPunchLine()             end         end     end )       ---------------------------------------------------------------------------- -- startRandomMap -- -- Changes the current map to a random race map ---------------------------------------------------------------------------- function startRandomMap()       -- Handle forced nextmap setting     if maybeApplyForcedNextMap() then         return     end       -- Get a random map chosen from the 10% of least recently player maps, with enough spawn points for all the players (if required)     local map = getRandomMapCompatibleWithGamemode( getThisResource(), 10, g_GameOptions.ghostmode and 0 or getTotalPlayerCount() )     if map then         g_IgnoreSpawnCountProblems = map    -- Uber hack 4000         if not exports.mapmanager:changeGamemodeMap ( map, nil, true ) then             problemChangingMap()         end     else         outputWarning( 'startRandomMap failed' )     end end addEvent("chooseRandomMap",true) addEventHandler("chooseRandomMap",root,startRandomMap)     ---------------------------------------------------------------------------- -- outputRace -- -- Race color is defined in the settings ---------------------------------------------------------------------------- function outputRace(message, toElement)     toElement = toElement or g_Root     local r, g, b = getColorFromString(string.upper(get("color")))     if getElementType(toElement) == 'console' then         outputServerLog(message)     else         if toElement == rootElement then             outputServerLog(message)         end         if getElementType(toElement) == 'player' then             message = '[PM] ' .. message         end         outputChatBox(message, toElement, r, g, b)     end end     ---------------------------------------------------------------------------- -- problemChangingMap -- -- Sort it ---------------------------------------------------------------------------- function problemChangingMap()     outputRace( 'Changing to random map in 5 seconds' )     local currentMap = exports.mapmanager:getRunningGamemodeMap()     TimerManager.createTimerFor("resource","mapproblem"):setTimer(         function()             -- Check that something else hasn't already changed the map             if currentMap == exports.mapmanager:getRunningGamemodeMap() then                 startRandomMap()             end         end,         math.random(4500,5500), 1 ) end       -- -- -- NextMapVote -- -- --   local g_Poll   ---------------------------------------------------------------------------- -- startNextMapVote -- -- Start a votemap for the next map. Should only be called during the -- race state 'NextMapSelect' ---------------------------------------------------------------------------- function startNextMapVote()       exports.votemanager:stopPoll()       -- Handle forced nextmap setting     if maybeApplyForcedNextMap() then         return     end       -- Get all maps     local compatibleMaps = exports.mapmanager:getMapsCompatibleWithGamemode(getThisResource())         -- limit it to eight random maps     if #compatibleMaps > 8 then         math.randomseed(getTickCount())         repeat             table.remove(compatibleMaps, math.random(1, #compatibleMaps))         until #compatibleMaps == 8     elseif #compatibleMaps < 2 then         return false, errorCode.onlyOneCompatibleMap     end       -- mix up the list order     for i,map in ipairs(compatibleMaps) do         local swapWith = math.random(1, #compatibleMaps)         local temp = compatibleMaps[i]         compatibleMaps[i] = compatibleMaps[swapWith]         compatibleMaps[swapWith] = temp     end         local poll = {         title="Choose the next map:",         visibleTo=getRootElement(),         percentage=51,         timeout=15,         allowchange=true;         }             table.insert(poll, {"Random", 'chooseRandomMap', getRootElement()})     for index, map in ipairs(compatibleMaps) do         local mapName = getResourceInfo(map, "name") or getResourceName(map)         table.insert(poll, {mapName, 'nextMapVoteResult', getRootElement(), map})     end         local currentMap = exports.mapmanager:getRunningGamemodeMap()     if currentMap then         table.insert(poll, {"Play again", 'nextMapVoteResult', getRootElement(), currentMap})     end       -- Allow addons to modify the poll     g_Poll = poll     triggerEvent('onPollStarting', g_Root, poll )     poll = g_Poll     g_Poll = nil       local pollDidStart = exports.votemanager:startPoll(poll)       if pollDidStart then         gotoState('NextMapVote')         addEventHandler("onPollEnd", getRootElement(), chooseRandomMap)     end       return pollDidStart end     -- Used by addons in response to onPollStarting addEvent('onPollModified') addEventHandler('onPollModified', getRootElement(),     function( poll )         g_Poll = poll     end )     function chooseRandomMap (chosen)     if not chosen then         cancelEvent()         math.randomseed(getTickCount())         exports.votemanager:finishPoll(1)     end     removeEventHandler("onPollEnd", getRootElement(), chooseRandomMap) end       ---------------------------------------------------------------------------- -- event nextMapVoteResult -- -- Called from the votemanager when the poll has completed ---------------------------------------------------------------------------- addEvent('nextMapVoteResult') addEventHandler('nextMapVoteResult', getRootElement(),     function( map )         if stateAllowsNextMapVoteResult() then             if not exports.mapmanager:changeGamemodeMap ( map, nil, true ) then                 problemChangingMap()             end         end     end )       ---------------------------------------------------------------------------- -- startMidMapVoteForRestartMap -- -- Start the vote menu to restart the current map if during a race -- No messages if this was not started by a player ---------------------------------------------------------------------------- function startMidMapVoteForRestartMap(player)       -- Check state and race time left     if not stateAllowsRestartMapVote() then         if player then             outputRace( "I'm afraid I can't let you do that, " .. getPlayerName(player) .. ".", player )         end         return     end       displayHilariarseMessage( player )     exports.votemanager:stopPoll()       -- Actual vote started here     local pollDidStart = exports.votemanager:startPoll {             title='Do you want to restart the current map?',             percentage=51,             timeout=15,             allowchange=true,             visibleTo=getRootElement(),             [1]={'Yes', 'midMapRestartVoteResult', getRootElement(), true},             [2]={'No', 'midMapRestartVoteResult', getRootElement(), false;default=true},     }       -- Change state if vote did start     if pollDidStart then         gotoState('MidMapVote')     end   end addCommandHandler('voteredo',startMidMapVoteForRestartMap)     ---------------------------------------------------------------------------- -- event midMapRestartVoteResult -- -- Called from the votemanager when the poll has completed ---------------------------------------------------------------------------- addEvent('midMapRestartVoteResult') addEventHandler('midMapRestartVoteResult', getRootElement(),     function( votedYes )         -- Change state back         if stateAllowsRandomMapVoteResult() then             gotoState('Running')             if votedYes then                 if not exports.mapmanager:changeGamemodeMap ( exports.mapmanager:getRunningGamemodeMap(), nil, true ) then                     problemChangingMap()                 end             else                 displayKillerPunchLine()             end         end     end )   addCommandHandler('redo',     function( player, command, value )         if isPlayerInACLGroup(player, g_GameOptions.admingroup) then             local currentMap = exports.mapmanager:getRunningGamemodeMap()             if currentMap then                 outputChatBox('Map restarted by ' .. getPlayerName(player), g_Root, 0, 240, 0)                 if not exports.mapmanager:changeGamemodeMap (currentMap, nil, true) then                     problemChangingMap()                 end             else                 outputRace("You can't restart the map because no map is running", player)             end         else             outputRace("You are not an Admin", player)         end     end )     addCommandHandler('random',     function( player, command, value )         if isPlayerInACLGroup(player, g_GameOptions.admingroup) then             if not stateAllowsRandomMapVote() or g_CurrentRaceMode:getTimeRemaining() < 1000 then                 outputRace( "Random command only works during a race and when no polls are running.", player )             else                 local choice = {'curtailed', 'cut short', 'terminated', 'given the heave ho', 'dropkicked', 'expunged', 'put out of our misery', 'got rid of'}                 outputChatBox('Current map ' .. choice[math.random( 1, #choice )] .. ' by ' .. getPlayerName(player), g_Root, 0, 240, 0)                 startRandomMap()             end         end     end )     ---------------------------------------------------------------------------- -- maybeApplyForcedNextMap -- -- Returns true if nextmap did override ---------------------------------------------------------------------------- function maybeApplyForcedNextMap()     if g_ForcedNextMap then         local map = g_ForcedNextMap         g_ForcedNextMap = nil         g_IgnoreSpawnCountProblems = map    -- Uber hack 4000         if not exports.mapmanager:changeGamemodeMap ( map, nil, true ) then             outputWarning( 'Forced next map failed' )             return false         end         return true     end     return false end   --------------------------------------------------------------------------- -- -- Testing -- -- -- --------------------------------------------------------------------------- addCommandHandler('forcevote',     function( player, command, value )         if not _TESTING and not isPlayerInACLGroup(player, g_GameOptions.admingroup) then             return         end         startNextMapVote()     end )     --------------------------------------------------------------------------- -- -- getRandomMapCompatibleWithGamemode -- -- This should go in mapmanager, but ACL needs doing -- ---------------------------------------------------------------------------   addEventHandler('onResourceStart', getRootElement(),     function( res )         if exports.mapmanager:isMap( res ) then             setMapLastTimePlayed( res )         end     end )   function getRandomMapCompatibleWithGamemode( gamemode, oldestPercentage, minSpawnCount )       -- Get all relevant maps     local compatibleMaps = exports.mapmanager:getMapsCompatibleWithGamemode( gamemode )       if #compatibleMaps == 0 then         outputDebugString( 'getRandomMapCompatibleWithGamemode: No maps.', 1 )         return false     end       -- Sort maps by time since played     local sortList = {}     for i,map in ipairs(compatibleMaps) do         sortList[i] = {}         sortList[i].map = map         sortList[i].lastTimePlayed = getMapLastTimePlayed( map )     end       table.sort( sortList, function(a, b) return a.lastTimePlayed > b.lastTimePlayed end )       -- Use the bottom n% of maps as the initial selection pool     local cutoff = #sortList - math.floor( #sortList * oldestPercentage / 100 )       outputDebug( 'RANDMAP', 'getRandomMapCompatibleWithGamemode' )     outputDebug( 'RANDMAP', ''             .. ' minSpawns:' .. tostring( minSpawnCount )             .. ' nummaps:' .. tostring( #sortList )             .. ' cutoff:' .. tostring( cutoff )             .. ' poolsize:' .. tostring( #sortList - cutoff + 1 )             )       math.randomseed( getTickCount() % 50000 )     local fallbackMap     while #sortList > 0 do         -- Get random item from range         local idx = math.random( cutoff, #sortList )         local map = sortList[idx].map           if not minSpawnCount or minSpawnCount <= getMapSpawnPointCount( map ) then             outputDebug( 'RANDMAP', ''                     .. ' ++ using map:' .. tostring( getResourceName( map ) )                     .. ' spawns:' .. tostring( getMapSpawnPointCount( map ) )                     .. ' age:' .. tostring( getRealTimeSeconds() - getMapLastTimePlayed( map ) )                     )             return map         end           -- Remember best match incase we cant find any with enough spawn points         if not fallbackMap or getMapSpawnPointCount( fallbackMap ) < getMapSpawnPointCount( map ) then             fallbackMap = map         end           outputDebug( 'RANDMAP', ''                 .. ' skip:' .. tostring( getResourceName( map ) )                 .. ' spawns:' .. tostring( getMapSpawnPointCount( map ) )                 .. ' age:' .. tostring( getRealTimeSeconds() - getMapLastTimePlayed( map ) )                 )           -- If map not good enough, remove from the list and try another         table.remove( sortList, idx )         -- Move cutoff up the list if required         cutoff = math.min( cutoff, #sortList )     end       -- No maps found - use best match     outputDebug( 'RANDMAP', ''             .. ' ** fallback map:' .. tostring( getResourceName( fallbackMap ) )             .. ' spawns:' .. tostring( getMapSpawnPointCount( fallbackMap ) )             .. ' ageLstPlyd:' .. tostring( getRealTimeSeconds() - getMapLastTimePlayed( fallbackMap ) )             )     return fallbackMap end   -- Look for spawnpoints in map file -- Not very quick as it loads the map file everytime function countSpawnPointsInMap(res)     local count = 0     local meta = xmlLoadFile(':' .. getResourceName(res) .. '/' .. 'meta.xml')     if meta then         local mapnode = xmlFindChild(meta, 'map', 0) or xmlFindChild(meta, 'race', 0)         local filename = mapnode and xmlNodeGetAttribute(mapnode, 'src')         xmlUnloadFile(meta)         if filename then             local map = xmlLoadFile(':' .. getResourceName(res) .. '/' .. filename)             if map then                 while xmlFindChild(map, 'spawnpoint', count) do                     count = count + 1                 end                 xmlUnloadFile(map)             end         end     end     return count end   --------------------------------------------------------------------------- -- g_MapInfoList access --------------------------------------------------------------------------- local g_MapInfoList   function getMapLastTimePlayed( map )     local mapInfo = getMapInfo( map )     return mapInfo.lastTimePlayed or 0 end   function setMapLastTimePlayed( map, time )     time = time or getRealTimeSeconds()     local mapInfo = getMapInfo( map )     mapInfo.lastTimePlayed = time     mapInfo.playedCount = ( mapInfo.playedCount or 0 ) + 1     saveMapInfoItem( map, mapInfo ) end   function getMapSpawnPointCount( map )     local mapInfo = getMapInfo( map )     if not mapInfo.spawnPointCount then         mapInfo.spawnPointCount = countSpawnPointsInMap( map )         saveMapInfoItem( map, mapInfo )     end     return mapInfo.spawnPointCount end   function getMapInfo( map )     if not g_MapInfoList then         loadMapInfoAll()     end     if not g_MapInfoList[map] then         g_MapInfoList[map] = {}     end     local mapInfo = g_MapInfoList[map]     if mapInfo.loadTime ~= getResourceLoadTime(map) then         -- Reset or clear data that may change between loads         mapInfo.loadTime = getResourceLoadTime( map )         mapInfo.spawnPointCount = false     end     return mapInfo end     --------------------------------------------------------------------------- -- g_MapInfoList <-> database --------------------------------------------------------------------------- function sqlString(value)     value = tostring(value) or ''     return "'" .. value:gsub( "(['])", "''" ) .. "'" end   function sqlInt(value)     return tonumber(value) or 0 end   function getTableName(value)     return sqlString( 'race_mapmanager_maps' ) end   function ensureTableExists()     local cmd = ( 'CREATE TABLE IF NOT EXISTS ' .. getTableName() .. ' ('                      .. 'resName TEXT UNIQUE'                      .. ', infoName TEXT '                      .. ', spawnPointCount INTEGER'                      .. ', playedCount INTEGER'                      .. ', lastTimePlayedText TEXT'                      .. ', lastTimePlayed INTEGER'             .. ')' )     executeSQLQuery( cmd ) end   -- Load all rows into g_MapInfoList function loadMapInfoAll()     ensureTableExists()     local rows = executeSQLQuery( 'SELECT * FROM ' .. getTableName() )     g_MapInfoList = {}     for i,row in ipairs(rows) do         local map = getResourceFromName( row.resName )         if map then             local mapInfo = getMapInfo( map )             mapInfo.playedCount = row.playedCount             mapInfo.lastTimePlayed = row.lastTimePlayed         end     end end   -- Save one row function saveMapInfoItem( map, info )     executeSQLQuery( 'BEGIN TRANSACTION' )       ensureTableExists()       local cmd = ( 'INSERT OR IGNORE INTO ' .. getTableName() .. ' VALUES ('                     .. ''       .. sqlString( getResourceName( map ) )                     .. ','      .. sqlString( "" )                     .. ','      .. sqlInt( 0 )                     .. ','      .. sqlInt( 0 )                     .. ','      .. sqlString( "" )                     .. ','      .. sqlInt( 0 )             .. ')' )     executeSQLQuery( cmd )       cmd = ( 'UPDATE ' .. getTableName() .. ' SET '                     .. 'infoName='              .. sqlString( getResourceInfo( map, "name" ) )                     .. ',spawnPointCount='      .. sqlInt( info.spawnPointCount )                     .. ',playedCount='          .. sqlInt( info.playedCount )                     .. ',lastTimePlayedText='   .. sqlString( info.lastTimePlayed and info.lastTimePlayed > 0 and getRealDateTimeString(getRealTime(info.lastTimePlayed)) or "-" )                     .. ',lastTimePlayed='       .. sqlInt( info.lastTimePlayed )             .. ' WHERE '                     .. 'resName='               .. sqlString( getResourceName( map ) )              )     executeSQLQuery( cmd )       executeSQLQuery( 'END TRANSACTION' ) end       --------------------------------------------------------------------------- -- -- More things that should go in mapmanager -- ---------------------------------------------------------------------------   addCommandHandler('checkmap',     function( player, command, ... )         local query = #{...}>0 and table.concat({...},' ') or nil         if query then             local map, errormsg = findMap( query )             outputRace( errormsg, player )         end     end )   addCommandHandler('nextmap',     function( player, command, ... )         local query = #{...}>0 and table.concat({...},' ') or nil         if not query then             if g_ForcedNextMap then                 outputRace( 'Next map is ' .. getMapName( g_ForcedNextMap ), player )             else                 outputRace( 'Next map is not set', player )             end             return         end         if not _TESTING and not isPlayerInACLGroup(player, g_GameOptions.admingroup) then             return         end         local map, errormsg = findMap( query )         if not map then             outputRace( errormsg, player )             return         end         if g_ForcedNextMap == map then             outputRace( 'Next map is already set to ' .. getMapName( g_ForcedNextMap ), player )             return         end         g_ForcedNextMap = map         outputChatBox('Next map set to ' .. getMapName( g_ForcedNextMap ) .. ' by ' .. getPlayerName( player ), g_Root, 0, 240, 0)     end )   --Find a map which matches, or nil and a text message if there is not one match function findMap( query )     local maps = findMaps( query )       -- Make status string     local status = "Found " .. #maps .. " match" .. ( #maps==1 and "" or "es" )     for i=1,math.min(5,#maps) do         status = status .. ( i==1 and ": " or ", " ) .. "'" .. getMapName( maps[i] ) .. "'"     end     if #maps > 5 then         status = status .. " (" .. #maps - 5 .. " more)"     end       if #maps == 0 then         return nil, status .. " for '" .. query .. "'"     end     if #maps == 1 then         return maps[1], status     end     if #maps > 1 then         return nil, status     end end   -- Find all maps which match the query string function findMaps( query )     local results = {}     --escape all meta chars     query = string.gsub(query, "([%*%+%?%.%(%)%[%]%{%}%\%/%|%^%$%-])","%%%1")     -- Loop through and find matching maps     for i,resource in ipairs(exports.mapmanager:getMapsCompatibleWithGamemode(getThisResource())) do         local resName = getResourceName( resource )         local infoName = getMapName( resource  )           -- Look for exact match first         if query == resName or query == infoName then             return {resource}         end           -- Find match for query within infoName         if string.find( infoName:lower(), query:lower() ) then             table.insert( results, resource )         end     end     return results end   function getMapName( map )     return getResourceInfo( map, "name" ) or getResourceName( map ) or "unknown" end