--
-- racemidvote_server.lua
--
-- Mid-race random map vote and
-- NextMapVote handled in this file
--
local lastVoteStarterName = ''
local lastVoteStarterCount = 0
----------------------------------------------------------------------------
-- 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
g_playAgainCount = 0
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
----------------------------------------------------------------------------
-- 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 > 5 then
math.randomseed(getTickCount())
repeat
table.remove(compatibleMaps, math.random(1, #compatibleMaps))
until #compatibleMaps == 5
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;
}
for index, map in ipairs(compatibleMaps) do
local mapName = getResourceInfo(map, "name") or getResourceName(map)
table.insert(poll, {mapName, 'nextMapVoteResult', getRootElement(), map})
end
if not g_playAgainCount then
g_playAgainCount = 0
end
g_currentMap = exports.mapmanager:getRunningGamemodeMap()
if g_currentMap then
-- Modify this to change play again limit. Obviously 2 PlayAgains = 3 plays of the map total
if g_playAgainCount < 2 then
table.insert(poll, {"Play again (" .. g_playAgainCount + 1 .. "/2)", 'nextMapVoteResult', getRootElement(), g_currentMap})
end
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 map == g_currentMap then
g_playAgainCount = g_playAgainCount + 1
else
g_playAgainCount = 0
end
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