· 7 years ago · Feb 22, 2019, 08:28 PM
1--
2-- racemidvote_server.lua
3--
4-- Mid-race random map vote and
5-- NextMapVote handled in this file
6--
7
8local lastVoteStarterName = ''
9local lastVoteStarterCount = 0
10
11----------------------------------------------------------------------------
12-- displayHilariarseMessage
13--
14-- Comedy gold
15----------------------------------------------------------------------------
16function displayHilariarseMessage( player )
17 if not player then
18 lastVoteStarterName = ''
19 else
20 local playerName = getPlayerName(player)
21 local msg = ''
22 if playerName == lastVoteStarterName then
23 lastVoteStarterCount = lastVoteStarterCount + 1
24 if lastVoteStarterCount == 5 then
25 msg = playerName .. ' started a vote. Hardly a suprise.'
26 elseif lastVoteStarterCount == 10 then
27 msg = 'Guess what! '..playerName .. ' started ANOTHER vote!'
28 elseif lastVoteStarterCount < 5 then
29 msg = playerName .. ' started another vote.'
30 else
31 msg = playerName .. ' continues to abuse the vote system.'
32 end
33 else
34 lastVoteStarterCount = 0
35 lastVoteStarterName = playerName
36 msg = playerName .. ' started a vote.'
37 end
38 outputRace( msg )
39 end
40end
41
42
43----------------------------------------------------------------------------
44-- displayKillerPunchLine
45--
46-- Sewing kits available in the foyer
47----------------------------------------------------------------------------
48function displayKillerPunchLine( player )
49 if lastVoteStarterName ~= '' then
50 outputRace( 'Offical news: Everybody hates ' .. lastVoteStarterName )
51 end
52end
53
54
55----------------------------------------------------------------------------
56-- startMidMapVoteForRandomMap
57--
58-- Start the vote menu if during a race and more than 30 seconds from the end
59-- No messages if this was not started by a player
60----------------------------------------------------------------------------
61function startMidMapVoteForRandomMap(player)
62
63 -- Check state and race time left
64 if not stateAllowsRandomMapVote() or g_CurrentRaceMode:getTimeRemaining() < 30000 then
65 if player then
66 outputRace( "I'm afraid I can't let you do that, " .. getPlayerName(player) .. ".", player )
67 end
68 return
69 end
70
71 displayHilariarseMessage( player )
72 exports.votemanager:stopPoll()
73
74 -- Actual vote started here
75 local pollDidStart = exports.votemanager:startPoll {
76 title='Do you want to change to a random map?',
77 percentage=51,
78 timeout=15,
79 allowchange=true,
80 visibleTo=getRootElement(),
81 [1]={'Yes', 'midMapVoteResult', getRootElement(), true},
82 [2]={'No', 'midMapVoteResult', getRootElement(), false;default=true},
83 }
84
85 -- Change state if vote did start
86 if pollDidStart then
87 gotoState('MidMapVote')
88 end
89
90end
91addCommandHandler('new',startMidMapVoteForRandomMap)
92
93
94----------------------------------------------------------------------------
95-- event midMapVoteResult
96--
97-- Called from the votemanager when the poll has completed
98----------------------------------------------------------------------------
99addEvent('midMapVoteResult')
100addEventHandler('midMapVoteResult', getRootElement(),
101 function( votedYes )
102 -- Change state back
103 if stateAllowsRandomMapVoteResult() then
104 gotoState('Running')
105 if votedYes then
106 startRandomMap()
107 else
108 displayKillerPunchLine()
109 end
110 end
111 end
112)
113
114
115
116----------------------------------------------------------------------------
117-- startRandomMap
118--
119-- Changes the current map to a random race map
120----------------------------------------------------------------------------
121function startRandomMap()
122
123 -- Handle forced nextmap setting
124 if maybeApplyForcedNextMap() then
125 return
126 end
127
128 -- Get a random map chosen from the 10% of least recently player maps, with enough spawn points for all the players (if required)
129 local map = getRandomMapCompatibleWithGamemode( getThisResource(), 10, g_GameOptions.ghostmode and 0 or getTotalPlayerCount() )
130 if map then
131 g_IgnoreSpawnCountProblems = map -- Uber hack 4000
132 if not exports.mapmanager:changeGamemodeMap ( map, nil, true ) then
133 problemChangingMap()
134 end
135 else
136 outputWarning( 'startRandomMap failed' )
137 end
138end
139
140
141----------------------------------------------------------------------------
142-- outputRace
143--
144-- Race color is defined in the settings
145----------------------------------------------------------------------------
146function outputRace(message, toElement)
147 toElement = toElement or g_Root
148 local r, g, b = getColorFromString(string.upper(get("color")))
149 if getElementType(toElement) == 'console' then
150 outputServerLog(message)
151 else
152 if toElement == rootElement then
153 outputServerLog(message)
154 end
155 if getElementType(toElement) == 'player' then
156 message = '[PM] ' .. message
157 end
158 outputChatBox(message, toElement, r, g, b)
159 end
160end
161
162
163----------------------------------------------------------------------------
164-- problemChangingMap
165--
166-- Sort it
167----------------------------------------------------------------------------
168function problemChangingMap()
169 outputRace( 'Changing to random map in 5 seconds' )
170 local currentMap = exports.mapmanager:getRunningGamemodeMap()
171 TimerManager.createTimerFor("resource","mapproblem"):setTimer(
172 function()
173 -- Check that something else hasn't already changed the map
174 if currentMap == exports.mapmanager:getRunningGamemodeMap() then
175 startRandomMap()
176 end
177 end,
178 math.random(4500,5500), 1 )
179end
180
181
182
183--
184--
185-- NextMapVote
186--
187--
188--
189
190local g_Poll
191
192----------------------------------------------------------------------------
193-- startNextMapVote
194--
195-- Start a votemap for the next map. Should only be called during the
196-- race state 'NextMapSelect'
197----------------------------------------------------------------------------
198function startNextMapVote()
199
200 exports.votemanager:stopPoll()
201
202 -- Handle forced nextmap setting
203 if maybeApplyForcedNextMap() then
204 return
205 end
206
207 -- Get all maps
208 local compatibleMaps = exports.mapmanager:getMapsCompatibleWithGamemode(getThisResource())
209
210 -- limit it to eight random maps
211 if #compatibleMaps > 8 then
212 math.randomseed(getTickCount())
213 repeat
214 table.remove(compatibleMaps, math.random(1, #compatibleMaps))
215 until #compatibleMaps == 8
216 elseif #compatibleMaps < 2 then
217 return false, errorCode.onlyOneCompatibleMap
218 end
219
220 -- mix up the list order
221 for i,map in ipairs(compatibleMaps) do
222 local swapWith = math.random(1, #compatibleMaps)
223 local temp = compatibleMaps[i]
224 compatibleMaps[i] = compatibleMaps[swapWith]
225 compatibleMaps[swapWith] = temp
226 end
227
228 local poll = {
229 title="Choose the next map:",
230 visibleTo=getRootElement(),
231 percentage=51,
232 timeout=15,
233 allowchange=true;
234 }
235
236 for index, map in ipairs(compatibleMaps) do
237 local mapName = getResourceInfo(map, "name") or getResourceName(map)
238 table.insert(poll, {mapName, 'nextMapVoteResult', getRootElement(), map})
239 end
240
241 local currentMap = exports.mapmanager:getRunningGamemodeMap()
242 if currentMap then
243 table.insert(poll, {"Play again", 'nextMapVoteResult', getRootElement(), currentMap})
244 end
245
246 -- Allow addons to modify the poll
247 g_Poll = poll
248 triggerEvent('onPollStarting', g_Root, poll )
249 poll = g_Poll
250 g_Poll = nil
251
252 local pollDidStart = exports.votemanager:startPoll(poll)
253
254 if pollDidStart then
255 gotoState('NextMapVote')
256 addEventHandler("onPollEnd", getRootElement(), chooseRandomMap)
257 end
258
259 return pollDidStart
260end
261
262
263-- Used by addons in response to onPollStarting
264addEvent('onPollModified')
265addEventHandler('onPollModified', getRootElement(),
266 function( poll )
267 g_Poll = poll
268 end
269)
270
271
272function chooseRandomMap (chosen)
273 if not chosen then
274 cancelEvent()
275 math.randomseed(getTickCount())
276 exports.votemanager:finishPoll(1)
277 end
278 removeEventHandler("onPollEnd", getRootElement(), chooseRandomMap)
279end
280
281
282
283----------------------------------------------------------------------------
284-- event nextMapVoteResult
285--
286-- Called from the votemanager when the poll has completed
287----------------------------------------------------------------------------
288addEvent('nextMapVoteResult')
289addEventHandler('nextMapVoteResult', getRootElement(),
290 function( map )
291 if stateAllowsNextMapVoteResult() then
292 if not exports.mapmanager:changeGamemodeMap ( map, nil, true ) then
293 problemChangingMap()
294 end
295 end
296 end
297)
298
299
300
301----------------------------------------------------------------------------
302-- startMidMapVoteForRestartMap
303--
304-- Start the vote menu to restart the current map if during a race
305-- No messages if this was not started by a player
306----------------------------------------------------------------------------
307function startMidMapVoteForRestartMap(player)
308
309 -- Check state and race time left
310 if not stateAllowsRestartMapVote() then
311 if player then
312 outputRace( "I'm afraid I can't let you do that, " .. getPlayerName(player) .. ".", player )
313 end
314 return
315 end
316
317 displayHilariarseMessage( player )
318 exports.votemanager:stopPoll()
319
320 -- Actual vote started here
321 local pollDidStart = exports.votemanager:startPoll {
322 title='Do you want to restart the current map?',
323 percentage=51,
324 timeout=15,
325 allowchange=true,
326 visibleTo=getRootElement(),
327 [1]={'Yes', 'midMapRestartVoteResult', getRootElement(), true},
328 [2]={'No', 'midMapRestartVoteResult', getRootElement(), false;default=true},
329 }
330
331 -- Change state if vote did start
332 if pollDidStart then
333 gotoState('MidMapVote')
334 end
335
336end
337addCommandHandler('voteredo',startMidMapVoteForRestartMap)
338
339
340----------------------------------------------------------------------------
341-- event midMapRestartVoteResult
342--
343-- Called from the votemanager when the poll has completed
344----------------------------------------------------------------------------
345addEvent('midMapRestartVoteResult')
346addEventHandler('midMapRestartVoteResult', getRootElement(),
347 function( votedYes )
348 -- Change state back
349 if stateAllowsRandomMapVoteResult() then
350 gotoState('Running')
351 if votedYes then
352 if not exports.mapmanager:changeGamemodeMap ( exports.mapmanager:getRunningGamemodeMap(), nil, true ) then
353 problemChangingMap()
354 end
355 else
356 displayKillerPunchLine()
357 end
358 end
359 end
360)
361
362addCommandHandler('redo',
363 function( player, command, value )
364 if isPlayerInACLGroup(player, g_GameOptions.admingroup) then
365 local currentMap = exports.mapmanager:getRunningGamemodeMap()
366 if currentMap then
367 outputChatBox('Map restarted by ' .. getPlayerName(player), g_Root, 0, 240, 0)
368 if not exports.mapmanager:changeGamemodeMap (currentMap, nil, true) then
369 problemChangingMap()
370 end
371 else
372 outputRace("You can't restart the map because no map is running", player)
373 end
374 else
375 outputRace("You are not an Admin", player)
376 end
377 end
378)
379
380
381addCommandHandler('random',
382 function( player, command, value )
383 if isPlayerInACLGroup(player, g_GameOptions.admingroup) then
384 if not stateAllowsRandomMapVote() or g_CurrentRaceMode:getTimeRemaining() < 1000 then
385 outputRace( "Random command only works during a race and when no polls are running.", player )
386 else
387 local choice = {'curtailed', 'cut short', 'terminated', 'given the heave ho', 'dropkicked', 'expunged', 'put out of our misery', 'got rid of'}
388 outputChatBox('Current map ' .. choice[math.random( 1, #choice )] .. ' by ' .. getPlayerName(player), g_Root, 0, 240, 0)
389 startRandomMap()
390 end
391 end
392 end
393)
394
395
396----------------------------------------------------------------------------
397-- maybeApplyForcedNextMap
398--
399-- Returns true if nextmap did override
400----------------------------------------------------------------------------
401function maybeApplyForcedNextMap()
402 if g_ForcedNextMap then
403 local map = g_ForcedNextMap
404 g_ForcedNextMap = nil
405 g_IgnoreSpawnCountProblems = map -- Uber hack 4000
406 if not exports.mapmanager:changeGamemodeMap ( map, nil, true ) then
407 outputWarning( 'Forced next map failed' )
408 return false
409 end
410 return true
411 end
412 return false
413end
414
415---------------------------------------------------------------------------
416--
417-- Testing
418--
419--
420--
421---------------------------------------------------------------------------
422addCommandHandler('forcevote',
423 function( player, command, value )
424 if not _TESTING and not isPlayerInACLGroup(player, g_GameOptions.admingroup) then
425 return
426 end
427 startNextMapVote()
428 end
429)
430
431
432---------------------------------------------------------------------------
433--
434-- getRandomMapCompatibleWithGamemode
435--
436-- This should go in mapmanager, but ACL needs doing
437--
438---------------------------------------------------------------------------
439
440addEventHandler('onResourceStart', getRootElement(),
441 function( res )
442 if exports.mapmanager:isMap( res ) then
443 setMapLastTimePlayed( res )
444 end
445 end
446)
447
448function getRandomMapCompatibleWithGamemode( gamemode, oldestPercentage, minSpawnCount )
449
450 -- Get all relevant maps
451 local compatibleMaps = exports.mapmanager:getMapsCompatibleWithGamemode( gamemode )
452
453 if #compatibleMaps == 0 then
454 outputDebugString( 'getRandomMapCompatibleWithGamemode: No maps.', 1 )
455 return false
456 end
457
458 -- Sort maps by time since played
459 local sortList = {}
460 for i,map in ipairs(compatibleMaps) do
461 sortList[i] = {}
462 sortList[i].map = map
463 sortList[i].lastTimePlayed = getMapLastTimePlayed( map )
464 end
465
466 table.sort( sortList, function(a, b) return a.lastTimePlayed > b.lastTimePlayed end )
467
468 -- Use the bottom n% of maps as the initial selection pool
469 local cutoff = #sortList - math.floor( #sortList * oldestPercentage / 100 )
470
471 outputDebug( 'RANDMAP', 'getRandomMapCompatibleWithGamemode' )
472 outputDebug( 'RANDMAP', ''
473 .. ' minSpawns:' .. tostring( minSpawnCount )
474 .. ' nummaps:' .. tostring( #sortList )
475 .. ' cutoff:' .. tostring( cutoff )
476 .. ' poolsize:' .. tostring( #sortList - cutoff + 1 )
477 )
478
479 math.randomseed( getTickCount() % 50000 )
480 local fallbackMap
481 while #sortList > 0 do
482 -- Get random item from range
483 local idx = math.random( cutoff, #sortList )
484 local map = sortList[idx].map
485
486 if not minSpawnCount or minSpawnCount <= getMapSpawnPointCount( map ) then
487 outputDebug( 'RANDMAP', ''
488 .. ' ++ using map:' .. tostring( getResourceName( map ) )
489 .. ' spawns:' .. tostring( getMapSpawnPointCount( map ) )
490 .. ' age:' .. tostring( getRealTimeSeconds() - getMapLastTimePlayed( map ) )
491 )
492 return map
493 end
494
495 -- Remember best match incase we cant find any with enough spawn points
496 if not fallbackMap or getMapSpawnPointCount( fallbackMap ) < getMapSpawnPointCount( map ) then
497 fallbackMap = map
498 end
499
500 outputDebug( 'RANDMAP', ''
501 .. ' skip:' .. tostring( getResourceName( map ) )
502 .. ' spawns:' .. tostring( getMapSpawnPointCount( map ) )
503 .. ' age:' .. tostring( getRealTimeSeconds() - getMapLastTimePlayed( map ) )
504 )
505
506 -- If map not good enough, remove from the list and try another
507 table.remove( sortList, idx )
508 -- Move cutoff up the list if required
509 cutoff = math.min( cutoff, #sortList )
510 end
511
512 -- No maps found - use best match
513 outputDebug( 'RANDMAP', ''
514 .. ' ** fallback map:' .. tostring( getResourceName( fallbackMap ) )
515 .. ' spawns:' .. tostring( getMapSpawnPointCount( fallbackMap ) )
516 .. ' ageLstPlyd:' .. tostring( getRealTimeSeconds() - getMapLastTimePlayed( fallbackMap ) )
517 )
518 return fallbackMap
519end
520
521-- Look for spawnpoints in map file
522-- Not very quick as it loads the map file everytime
523function countSpawnPointsInMap(res)
524 local count = 0
525 local meta = xmlLoadFile(':' .. getResourceName(res) .. '/' .. 'meta.xml')
526 if meta then
527 local mapnode = xmlFindChild(meta, 'map', 0) or xmlFindChild(meta, 'race', 0)
528 local filename = mapnode and xmlNodeGetAttribute(mapnode, 'src')
529 xmlUnloadFile(meta)
530 if filename then
531 local map = xmlLoadFile(':' .. getResourceName(res) .. '/' .. filename)
532 if map then
533 while xmlFindChild(map, 'spawnpoint', count) do
534 count = count + 1
535 end
536 xmlUnloadFile(map)
537 end
538 end
539 end
540 return count
541end
542
543---------------------------------------------------------------------------
544-- g_MapInfoList access
545---------------------------------------------------------------------------
546local g_MapInfoList
547
548function getMapLastTimePlayed( map )
549 local mapInfo = getMapInfo( map )
550 return mapInfo.lastTimePlayed or 0
551end
552
553function setMapLastTimePlayed( map, time )
554 time = time or getRealTimeSeconds()
555 local mapInfo = getMapInfo( map )
556 mapInfo.lastTimePlayed = time
557 mapInfo.playedCount = ( mapInfo.playedCount or 0 ) + 1
558 saveMapInfoItem( map, mapInfo )
559end
560
561function getMapSpawnPointCount( map )
562 local mapInfo = getMapInfo( map )
563 if not mapInfo.spawnPointCount then
564 mapInfo.spawnPointCount = countSpawnPointsInMap( map )
565 saveMapInfoItem( map, mapInfo )
566 end
567 return mapInfo.spawnPointCount
568end
569
570function getMapInfo( map )
571 if not g_MapInfoList then
572 loadMapInfoAll()
573 end
574 if not g_MapInfoList[map] then
575 g_MapInfoList[map] = {}
576 end
577 local mapInfo = g_MapInfoList[map]
578 if mapInfo.loadTime ~= getResourceLoadTime(map) then
579 -- Reset or clear data that may change between loads
580 mapInfo.loadTime = getResourceLoadTime( map )
581 mapInfo.spawnPointCount = false
582 end
583 return mapInfo
584end
585
586
587---------------------------------------------------------------------------
588-- g_MapInfoList <-> database
589---------------------------------------------------------------------------
590function sqlString(value)
591 value = tostring(value) or ''
592 return "'" .. value:gsub( "(['])", "''" ) .. "'"
593end
594
595function sqlInt(value)
596 return tonumber(value) or 0
597end
598
599function getTableName(value)
600 return sqlString( 'race_mapmanager_maps' )
601end
602
603function ensureTableExists()
604 local cmd = ( 'CREATE TABLE IF NOT EXISTS ' .. getTableName() .. ' ('
605 .. 'resName TEXT UNIQUE'
606 .. ', infoName TEXT '
607 .. ', spawnPointCount INTEGER'
608 .. ', playedCount INTEGER'
609 .. ', lastTimePlayedText TEXT'
610 .. ', lastTimePlayed INTEGER'
611 .. ')' )
612 executeSQLQuery( cmd )
613end
614
615-- Load all rows into g_MapInfoList
616function loadMapInfoAll()
617 ensureTableExists()
618 local rows = executeSQLQuery( 'SELECT * FROM ' .. getTableName() )
619 g_MapInfoList = {}
620 for i,row in ipairs(rows) do
621 local map = getResourceFromName( row.resName )
622 if map then
623 local mapInfo = getMapInfo( map )
624 mapInfo.playedCount = row.playedCount
625 mapInfo.lastTimePlayed = row.lastTimePlayed
626 end
627 end
628end
629
630-- Save one row
631function saveMapInfoItem( map, info )
632 executeSQLQuery( 'BEGIN TRANSACTION' )
633
634 ensureTableExists()
635
636 local cmd = ( 'INSERT OR IGNORE INTO ' .. getTableName() .. ' VALUES ('
637 .. '' .. sqlString( getResourceName( map ) )
638 .. ',' .. sqlString( "" )
639 .. ',' .. sqlInt( 0 )
640 .. ',' .. sqlInt( 0 )
641 .. ',' .. sqlString( "" )
642 .. ',' .. sqlInt( 0 )
643 .. ')' )
644 executeSQLQuery( cmd )
645
646 cmd = ( 'UPDATE ' .. getTableName() .. ' SET '
647 .. 'infoName=' .. sqlString( getResourceInfo( map, "name" ) )
648 .. ',spawnPointCount=' .. sqlInt( info.spawnPointCount )
649 .. ',playedCount=' .. sqlInt( info.playedCount )
650 .. ',lastTimePlayedText=' .. sqlString( info.lastTimePlayed and info.lastTimePlayed > 0 and getRealDateTimeString(getRealTime(info.lastTimePlayed)) or "-" )
651 .. ',lastTimePlayed=' .. sqlInt( info.lastTimePlayed )
652 .. ' WHERE '
653 .. 'resName=' .. sqlString( getResourceName( map ) )
654 )
655 executeSQLQuery( cmd )
656
657 executeSQLQuery( 'END TRANSACTION' )
658end
659
660
661
662---------------------------------------------------------------------------
663--
664-- More things that should go in mapmanager
665--
666---------------------------------------------------------------------------
667
668addCommandHandler('checkmap',
669 function( player, command, ... )
670 local query = #{...}>0 and table.concat({...},' ') or nil
671 if query then
672 local map, errormsg = findMap( query )
673 outputRace( errormsg, player )
674 end
675 end
676)
677
678addCommandHandler('nextmap',
679 function( player, command, ... )
680 local query = #{...}>0 and table.concat({...},' ') or nil
681 if not query then
682 if g_ForcedNextMap then
683 outputRace( 'Next map is ' .. getMapName( g_ForcedNextMap ), player )
684 else
685 outputRace( 'Next map is not set', player )
686 end
687 return
688 end
689 if not _TESTING and not isPlayerInACLGroup(player, g_GameOptions.admingroup) then
690 return
691 end
692 local map, errormsg = findMap( query )
693 if not map then
694 outputRace( errormsg, player )
695 return
696 end
697 if g_ForcedNextMap == map then
698 outputRace( 'Next map is already set to ' .. getMapName( g_ForcedNextMap ), player )
699 return
700 end
701 g_ForcedNextMap = map
702 outputChatBox('Next map set to ' .. getMapName( g_ForcedNextMap ) .. ' by ' .. getPlayerName( player ), g_Root, 0, 240, 0)
703 end
704)
705
706--Find a map which matches, or nil and a text message if there is not one match
707function findMap( query )
708 local maps = findMaps( query )
709
710 -- Make status string
711 local status = "Found " .. #maps .. " match" .. ( #maps==1 and "" or "es" )
712 for i=1,math.min(5,#maps) do
713 status = status .. ( i==1 and ": " or ", " ) .. "'" .. getMapName( maps[i] ) .. "'"
714 end
715 if #maps > 5 then
716 status = status .. " (" .. #maps - 5 .. " more)"
717 end
718
719 if #maps == 0 then
720 return nil, status .. " for '" .. query .. "'"
721 end
722 if #maps == 1 then
723 return maps[1], status
724 end
725 if #maps > 1 then
726 return nil, status
727 end
728end
729
730-- Find all maps which match the query string
731function findMaps( query )
732 local results = {}
733 --escape all meta chars
734 query = string.gsub(query, "([%*%+%?%.%(%)%[%]%{%}%\%/%|%^%$%-])","%%%1")
735 -- Loop through and find matching maps
736 for i,resource in ipairs(exports.mapmanager:getMapsCompatibleWithGamemode(getThisResource())) do
737 local resName = getResourceName( resource )
738 local infoName = getMapName( resource )
739
740 -- Look for exact match first
741 if query == resName or query == infoName then
742 return {resource}
743 end
744
745 -- Find match for query within infoName
746 if string.find( infoName:lower(), query:lower() ) then
747 table.insert( results, resource )
748 end
749 end
750 return results
751end
752
753function getMapName( map )
754 return getResourceInfo( map, "name" ) or getResourceName( map ) or "unknown"
755end