-- -- toptimes_server.lua -- SToptimesManager = {} SToptimesManager.__index = SToptimesManager SToptimesManager.instances = {} --------------------------------------------------------------------------- -- Server -- Handle events from Race -- -- This is the 'interface' from Race -- --------------------------------------------------------------------------- addEvent('onMapStarting') addEventHandler('onMapStarting', g_Root, function(mapInfo, mapOptions, gameOptions) if g_SToptimesManager then g_SToptimesManager:setModeAndMap( mapInfo.modename, mapInfo.name, gameOptions.statsKey ) end end ) g_Root = getRootElement() addEvent('onPlayerPickUpRacePickup',true) addEventHandler('onPlayerPickUpRacePickup', g_Root, function(number, sort, model) if sort == "vehiclechange" then if model == 425 then outputChatBox ("#FF6347[TOPTIMES] #ffffff" .. getPlayerName(source) .. " #ffffffhas got the Hunter", getRoot, 255, 255, 255, true ) if g_SToptimesManager then g_SToptimesManager:playerFinished( source, exports.race:getTimePassed()) end end end end ) addEvent('onPlayerFinish') addEventHandler('onPlayerFinish', g_Root, function(rank, time) if g_SToptimesManager then g_SToptimesManager:playerFinished( source, time) end end ) addEventHandler('onResourceStop', g_ResRoot, function() if g_SToptimesManager then g_SToptimesManager:unloadingMap() end end ) addEventHandler('onPlayerQuit', g_Root, function() if g_SToptimesManager then g_SToptimesManager:removePlayerFromUpdateList(source) g_SToptimesManager:unqueueUpdate(source) end end ) addEventHandler('onResourceStart', g_ResRoot, function() local raceInfo = getRaceInfo() if raceInfo and g_SToptimesManager then g_SToptimesManager:setModeAndMap( raceInfo.mapInfo.modename, raceInfo.mapInfo.name, raceInfo.gameOptions.statsKey ) end end ) function getRaceInfo() local raceResRoot = getResourceRootElement( getResourceFromName( "race" ) ) return raceResRoot and getElementData( raceResRoot, "info" ) end --------------------------------------------------------------------------- -- -- Events fired from here -- --------------------------------------------------------------------------- addEvent("onPlayerToptimeImprovement") --------------------------------------------------------------------------- --------------------------------------------------------------------------- -- -- SToptimesManager:create() -- -- Create a SToptimesManager instance -- --------------------------------------------------------------------------- function SToptimesManager:create() local id = #SToptimesManager.instances + 1 SToptimesManager.instances[id] = setmetatable( { id = id, playersWhoWantUpdates = {}, updateQueue = {}, serviceQueueTimer = nil, displayTopCount = 12, -- Top number of times to display mapTimes = nil, -- SMaptimes:create() serverRevision = 0, -- To prevent redundant updating to clients }, self ) SToptimesManager.instances[id]:postCreate() return SToptimesManager.instances[id] end --------------------------------------------------------------------------- -- -- SToptimesManager:destroy() -- -- Destroy a SToptimesManager instance -- --------------------------------------------------------------------------- function SToptimesManager:destroy() SToptimesManager.instances[self.id] = nil self.id = 0 end --------------------------------------------------------------------------- -- -- SToptimesManager:postCreate() -- -- -- --------------------------------------------------------------------------- function SToptimesManager:postCreate() cacheSettings() self.displayTopCount = g_Settings.numtimes end --------------------------------------------------------------------------- -- -- SToptimesManager:setModeAndMap() -- -- Called when a new map has been loaded -- --------------------------------------------------------------------------- function SToptimesManager:setModeAndMap( raceModeName, mapName, statsKey ) outputDebug( 'TOPTIMES', 'SToptimesManager:setModeAndMap ' .. raceModeName .. '<>' .. mapName ) -- Reset updatings from the previous map self.playersWhoWantUpdates = {} self.updateQueue = {} if self.serviceQueueTimer then killTimer(self.serviceQueueTimer) end self.serviceQueueTimer = nil -- Remove old map times if self.mapTimes then self.mapTimes:flush() -- Ensure last stuff is saved self.mapTimes:destroy() end -- Get map times for this map self.mapTimes = SMaptimes:create( raceModeName, mapName, statsKey ) self.mapTimes:load() -- Get the toptimes data ready to send self:updateTopText() end --------------------------------------------------------------------------- -- -- SToptimesManager:unloadingMap() -- -- Called when unloading -- --------------------------------------------------------------------------- function SToptimesManager:unloadingMap() if self.mapTimes then self.mapTimes:flush() -- Ensure last stuff is saved end end --------------------------------------------------------------------------- -- -- SToptimesManager:playerFinished() -- -- If time is good enough, insert into database -- --------------------------------------------------------------------------- function SToptimesManager:playerFinished( player, newTime, dateRecorded ) -- Check if top time recording is disabled for this player if getElementData ( player, "toptimes" ) == "off" then return end if not self.mapTimes then outputDebug( 'TOPTIMES', 'SToptimesManager:playerFinished - self.mapTimes == nil' ) return end dateRecorded = dateRecorded or getRealDateTimeNowString() local oldTime = self.mapTimes:getTimeForPlayer( player ) -- Can be false if no previous time local newPos = self.mapTimes:getPositionForTime( newTime, dateRecorded ) -- See if time is an improvement for this player if not oldTime or newTime < oldTime then local oldPos = self.mapTimes:getIndexForPlayer( player ) triggerEvent("onPlayerToptimeImprovement", player, newPos, newTime, oldPos, oldTime, self.displayTopCount, self.mapTimes:getValidEntryCount() ) -- See if its in the top display if newPos <= self.displayTopCount then outputDebug( 'TOPTIMES', getPlayerName(player) .. ' got toptime position ' .. newPos ) end if oldTime then outputDebug( 'TOPTIMES', getPlayerName(player) .. ' new personal best ' .. newTime .. ' ' .. oldTime - newTime ) end self.mapTimes:setTimeForPlayer( player, newTime, dateRecorded ) -- updateTopText if database was changed if newPos <= self.displayTopCount then self:updateTopText() end end outputDebug( 'TOPTIMES', '++ SToptimesManager:playerFinished ' .. tostring(getPlayerName(player)) .. ' time:' .. tostring(newTime) ) end --------------------------------------------------------------------------- -- -- SToptimesManager:updateTopText() -- -- Update the toptimes client data for the current map -- --------------------------------------------------------------------------- function SToptimesManager:updateTopText() if not self.mapTimes then return end -- Update data -- Read top rows from map toptimes table and send to all players who want to know self.toptimesDataForMap = self.mapTimes:getToptimes( self.displayTopCount ) self.serverRevision = self.serverRevision + 1 -- Queue send to all players for i,player in ipairs(self.playersWhoWantUpdates) do self:queueUpdate(player) end end --------------------------------------------------------------------------- -- -- SToptimesManager:onServiceQueueTimer() -- -- Pop a player off the updateQueue and send them an update -- --------------------------------------------------------------------------- function SToptimesManager:onServiceQueueTimer() outputDebug( 'TOPTIMES', 'SToptimesManager:onServiceQueueTimer()' ) -- Process next player if #self.updateQueue > 0 and self.mapTimes then local player = self.updateQueue[1] local playerPosition = self.mapTimes:getIndexForPlayer( player ) clientCall( player, 'onServerSentToptimes', self.toptimesDataForMap, self.serverRevision, playerPosition ); end table.remove(self.updateQueue,1) -- Stop timer if end of update queue if #self.updateQueue < 1 then killTimer(self.serviceQueueTimer) self.serviceQueueTimer = nil end end --------------------------------------------------------------------------- -- -- SToptimesManager:addPlayerToUpdateList() -- -- -- --------------------------------------------------------------------------- function SToptimesManager:addPlayerToUpdateList( player ) if not table.find( self.playersWhoWantUpdates, player) then table.insert( self.playersWhoWantUpdates, player ) outputDebug( 'TOPTIMES', 'playersWhoWantUpdates : ' .. #self.playersWhoWantUpdates ) end end function SToptimesManager:removePlayerFromUpdateList( player ) table.removevalue( self.playersWhoWantUpdates, player ) end --------------------------------------------------------------------------- -- -- SToptimesManager:queueUpdate() -- -- -- --------------------------------------------------------------------------- function SToptimesManager:queueUpdate( player ) if not table.find( self.updateQueue, player) then table.insert( self.updateQueue, player ) end if not self.serviceQueueTimer then self.serviceQueueTimer = setTimer( function() self:onServiceQueueTimer() end, 100, 0 ) end end function SToptimesManager:unqueueUpdate( player ) table.removevalue( self.updateQueue, player ) end --------------------------------------------------------------------------- -- -- SToptimesManager:doOnClientRequestToptimesUpdates() -- -- -- --------------------------------------------------------------------------- function SToptimesManager:doOnClientRequestToptimesUpdates( player, bOn, clientRevision ) outputDebug( 'TOPTIMES', 'SToptimesManager:onClientRequestToptimesUpdates: ' .. tostring(getPlayerName(player)) .. '<>' .. tostring(bOn) .. '< crev:' .. tostring(clientRevision) .. '< srev:' .. tostring(self.serverRevision) ) if bOn then self:addPlayerToUpdateList(player) if clientRevision ~= self.serverRevision then outputDebug( 'TOPTIMES', 'queueUpdate for'..getPlayerName(player) ) self:queueUpdate(player) end else self:removePlayerFromUpdateList(player) self:unqueueUpdate(player) end end addEvent('onClientRequestToptimesUpdates', true) addEventHandler('onClientRequestToptimesUpdates', getRootElement(), function( bOn, clientRevision ) g_SToptimesManager:doOnClientRequestToptimesUpdates( source, bOn, clientRevision ) end ) --------------------------------------------------------------------------- -- -- Commands and binds -- -- -- --------------------------------------------------------------------------- addCommandHandler( "deltime", function( player, cmd, place ) if not _TESTING and not isPlayerInACLGroup(player, g_Settings.admingroup) then return end outputChatBox( "Top time"..placeText.." from '" .. tostring(row.playerName) .. "' deleted by " .. getPlayerName(player), 255,55,00 ) if g_SToptimesManager and g_SToptimesManager.mapTimes then local row = g_SToptimesManager.mapTimes:deletetime(place) if row then g_SToptimesManager:updateTopText() local mapName = tostring(g_SToptimesManager.mapTimes.mapName) local placeText = place and " #" .. tostring(place) or "" outputChatBox( "Top time"..placeText.." from '" .. tostring(row.playerName) .. "' deleted by " .. getPlayerName(player), 255,55,00 ) outputServerLog( "INFO: Top time"..placeText.." from '" ..tostring(row.playerName).. "' (" ..tostring(row.timeText).. " in " ..mapName.. ") deleted by " .. getAdminNameForLog(player) ) end end end ) --------------------------------------------------------------------------- -- -- Settings -- -- -- --------------------------------------------------------------------------- function cacheSettings() g_Settings = {} g_Settings.numtimes = getNumber('numtimes',8) g_Settings.startshow = getBool('startshow',false) g_Settings.gui_x = getNumber('gui_x',0.56) g_Settings.gui_y = getNumber('gui_y',0.02) g_Settings.admingroup = getString("admingroup","Admin") end -- React to admin panel changes addEvent ( "onSettingChange" ) addEventHandler('onSettingChange', g_ResRoot, function(name, oldvalue, value, playeradmin) outputDebug( 'MISC', 'Setting changed: ' .. tostring(name) .. ' value:' .. tostring(value) .. ' value:' .. tostring(oldvalue).. ' by:' .. tostring(player and getPlayerName(player) or 'n/a') ) cacheSettings() -- Update here if g_SToptimesManager then g_SToptimesManager.displayTopCount = g_Settings.numtimes g_SToptimesManager:updateTopText() end -- Update clients clientCall(g_Root,'updateSettings', g_Settings, playeradmin) end ) -- New player joined addEvent('onLoadedAtClient_tt', true) addEventHandler('onLoadedAtClient_tt', g_Root, function() -- Tell newly joined client current settings clientCall(source,'updateSettings', g_Settings) -- This could also be the toptimes resource being restarted, so send some mapinfo local raceInfo = getRaceInfo() if raceInfo then triggerClientEvent('onClientSetMapName', source, raceInfo.mapInfo.name ) end end ) --------------------------------------------------------------------------- -- Global instance --------------------------------------------------------------------------- g_SToptimesManager = SToptimesManager:create()