--Version 2.0 local timeToStartResetTicks = 25 * 60 * 60 * 24 local timeWarningM = 5 local file_name = "server/heartbeat.txt" local vip_names = { "Farris", "dunkic p", "kurobutt", "Kunio", "Hans", "Moogy", "Ori", "Icey" } local timeCurWarningM = 0 local heartbeatCount = 0 local loopCount = 0 local file = "" local playerCount = 0 local ratings_file = "maprating_maps.txt" local high_map_chance = 80 local hell_map_chance = 10 local high_rating_numerator = 15 local high_rating_denominator = 4 local debug_console_vote = false local rating_hud_x = 260 local rating_hud_ys = {30, 60, 90} local rating_hud_line_height = 7 local color_def = "\x80" local color_vip_full = "\xA2" local color_vip_half = "\xB7" local color_full = "\xA4" local color_half = "\xBC" local HAS_SATURN_VOTE_API = _G["FEATURE_VOTEHUD"] ~= nil local VotePacketGeneration = 0 local ReceivedVoteHUDData = nil local VoteRatingsHUDEnabled = true local function checkmap() if (CV_FindVar("elimination") != nil) then if (CV_FindVar("elimination").value == 1) then print("Wrong number of laps, but skipping because elim is on") return end end if (mapheaderinfo[gamemap].numlaps != numlaps) then print("Wrong number of laps! Resetting to map default") COM_BufInsertText(server, "numlaps " .. string.format("%02d", mapheaderinfo[gamemap].numlaps)) end end addHook("MapLoad", checkmap) local shownSong = false local map = nil local function showSong() S_ShowMusicCredit() end COM_AddCommand("showsong", showSong) COM_AddCommand("songname", showSong) COM_AddCommand("songid", showSong) addHook("MusicChange", function(showSong) if (consoleplayer==nil or consoleplayer.valid == false) then return end if (consoleplayer.laps>=numlaps and shownSong == false) then S_ShowMusicCredit() shownSong = true end end) COM_AddCommand("modeswitch", function(player,arg) if arg == nil then CONS_Printf(player,"This command takes a string as a argument. streamlined, uncut, agony.") return end local num = -1; arg = string.lower(arg) if arg == "streamlined" then num = 0 elseif arg == "uncut" then num = 1 elseif arg == "agony" then num = 2 else CONS_Printf(player,"This command takes a string as a argument. streamlined, uncut, agony.") return end chatprint("Stopping server to switch mode.") file = assert(io.open("server/modeswitch.txt", "w")) file:write(num) file:close() COM_BufInsertText(server, "quit") end, COM_ADMIN) local mapticks = 0 addHook("ThinkFrame", function(checkTime) if (map != gamemap) then map = gamemap shownSong = false mapticks = 0 end mapticks = mapticks + 1 if (mapticks==480) then checkmap() end loopCount = loopCount + 1 if (loopCount == 1) then print("PPPPMaintenance.lua loaded") end if (loopCount % 35 == 1) then --Check gamemode and clear playerCount = 0 for p in players.iterate do if p and p.valid and p.mo ~= nil then playerCount = playerCount + 1 end end local carrier_mode = CV_FindVar("carrier_collide") local speedmod = CV_FindVar("speedmodauto") local stevofools = CV_FindVar("stevofools") local firmsneaker = CV_FindVar("firmsneaker") if (playerCount < 2) then if (carrier_mode ~= nil and carrier_mode.value > 0) then COM_BufInsertText(server, "carrier_collide off") print("Last player. Turning off carriers collision."); end local cd_mode = CV_FindVar("cd_mode") if (cd_mode ~= nil and cd_mode.value > 0) then COM_BufInsertText(server, "cd_mode off") print("Last player. Turning off Chaos Drives for Time Attack") end local driftnitro = CV_FindVar("driftnitro") if (driftnitro ~= nil and driftnitro.value == 1) then COM_BufInsertText(server, "driftnitro off") print("Last player. Turning off Drift Nitro for Time Attack, then restarting map.") COM_BufInsertText(server, "map map" .. string.format("%02d", gamemap)) end if (speedmod ~= nil and speedmod.value == 1) then COM_BufInsertText(server, "speedmodauto off") print("Last player. Turning off Speedmod for Time Attack.") end if (stevofools ~= nil and stevofools.value == 1) then COM_BufInsertText(server, "stevofools off") print("Last player. Turning off Stevo Fools for Time Attack.") end if (firmsneaker ~= nil and firmsneaker.value == 0) then COM_BufInsertText(server, "firmsneaker on") print("Last player. Turning on firmsneaker for Time Attack.") end else if (carrier_mode ~= nil and carrier_mode.value < 1) then COM_BufInsertText(server, "carrier_collide on") print("More players. Turning on carriers collision."); end end end if (loopCount % 120 == 1) then ---Should be once per minute heartbeatCount = heartbeatCount + 1 --file = assert(io.open(file_name, "w")) --file:write(heartbeatCount.."\n") if isserver then print("Beat: " .. heartbeatCount) end file:close() end --Do only once in awhile for performance purposes. if (loopCount % 200 == 1) then if (loopCount > timeToStartResetTicks) then local shuttingDownTimeMin = (loopCount - timeToStartResetTicks) / 25 / 60 if (shuttingDownTimeMin > timeCurWarningM) then chatprint("Server restarting in " .. timeWarningM - timeCurWarningM .. " minutes.") timeCurWarningM = timeCurWarningM + 1 end if (timeCurWarningM > timeWarningM) then chatprint("Stopping server.") COM_BufInsertText(server, "quit") end end end end) local PLAYERS_FILE = "maprating_players.txt" local ratings_file = "maprating_maps.txt" local REQUIRED_RACES = 15 local MIN_RATING = 0 local MAX_RATING = 5 local PROMPT_DELAY = 2*TICRATE local PlayerStats = {} local MapRatings = {} local CurrentMapID = nil local CurrentMapChecksum = nil local CurrentParticipants = {} local PromptedThisMap = {} local RaceCountCommitted = false local function openDataFile(filename,mode) if io.openlocal then return io.openlocal(filename, mode) end return io.open(filename, mode) end local function sendPrivateMessage(player,text) if not isserver or not player or not player.valid then return end local message = tostring(text or "") message = message:gsub("\r", " "):gsub("\n", " ") message = message:gsub(";", ",") if #message > 220 then message = message:sub(1, 220) end COM_BufAddText(consoleplayer, "sayto " .. tostring(#player) .. " " .. message) end local function normalizeName(name) return string.lower(tostring(name or "")) end local VIPLookup = {} for _, name in ipairs(vip_names) do VIPLookup[normalizeName(name)] = true end local function isVIP(name) return VIPLookup[normalizeName(name)] == true end local function encodeField(value) local text = tostring(value or "") text = text:gsub("%%", "%%25") text = text:gsub("\t", "%%09") text = text:gsub("\r", "%%0D") text = text:gsub("\n", "%%0A") return text end local function decodeField(value) return (tostring(value or ""):gsub("%%(%x%x)", function(hex) return string.char(tonumber(hex, 16)) end)) end local function splitTabs(line) local fields = {} for value in (line .. "\t"):gmatch("(.-)\t") do table.insert(fields, value) end return fields end local function getMapID(mapnum) return G_BuildMapName(mapnum) end local checksumWarningShown = false local function getMapChecksum(mapnum) local checksumFunction = _G["lb_map_checksum"] if checksumFunction == nil then if not checksumWarningShown then print("mapratings: lb_map_checksum is unavailable. Load the leaderboard checksum script before this addon.") checksumWarningShown = true end return nil end local checksum = checksumFunction(mapnum) if checksum == nil or checksum == "" then print("mapratings: lb_map_checksum returned no checksum for " .. tostring(getMapID(mapnum)) .. ".") return nil end return string.lower(tostring(checksum)) end local function makeMapRatingKey(mapid,checksum) return tostring(mapid) .. "\t" .. tostring(checksum) end local function getPlayerStats(name,create) local key = normalizeName(name) local stats = PlayerStats[key] if not stats and create then stats = { name = tostring(name), races = 0, } PlayerStats[key] = stats end if stats and name and name ~= "" then stats.name = tostring(name) end return stats, key end local function getMapRatings(mapid,checksum,create) if not mapid or not checksum then return nil end local key = makeMapRatingKey(mapid, checksum) local data = MapRatings[key] if not data and create then data = { mapid = mapid, checksum = checksum, ratings = {}, } MapRatings[key] = data end return data end local function playerIsEligible(name) if isVIP(name) then return true end local stats = getPlayerStats(name, false) return stats and stats.races >= REQUIRED_RACES end local function calculateMapTotals(data) local userSum = 0 local userCount = 0 local vipSum = 0 local vipCount = 0 for _, rating in pairs(data.ratings) do if rating.vip then vipSum = vipSum + rating.value vipCount = vipCount + 1 else userSum = userSum + rating.value userCount = userCount + 1 end end return userSum, userCount, vipSum, vipCount end local function integerDivide(numerator,denominator) return (numerator - (numerator%denominator))/denominator end local function formatAverage(sum,count) if count <= 0 then return "-1" end local rounding = integerDivide(count, 2) local hundredths = integerDivide(sum*100 + rounding, count) local fraction = hundredths%100 local whole = integerDivide(hundredths, 100) return string.format("%d.%02d", whole, fraction) end local function sortedPlayerKeys() local keys = {} for key in pairs(PlayerStats) do table.insert(keys, key) end table.sort(keys) return keys end local function sortedMapIDs() local ids = {} for mapid in pairs(MapRatings) do table.insert(ids, mapid) end table.sort(ids) return ids end local function sortedRatingKeys(data) local keys = {} for key in pairs(data.ratings) do table.insert(keys, key) end table.sort(keys) return keys end local function savePlayers() if not isserver then return false end local file, errorMessage = openDataFile(PLAYERS_FILE, "w") if not file then print("mapratings: could not write " .. PLAYERS_FILE .. ": " .. tostring(errorMessage)) return false end file:write("# MAPRATING_PLAYERS_V1\n") file:write("# PLAYER\\tname\\tcompleted-races\n") for _, key in ipairs(sortedPlayerKeys()) do local stats = PlayerStats[key] file:write("PLAYER\t", encodeField(stats.name), "\t", stats.races, "\n") end file:close() return true end local function saveMapRatings() if not isserver then return false end local file, errorMessage = openDataFile(ratings_file, "w") if not file then print("mapratings: could not write " .. ratings_file .. ": " .. tostring(errorMessage)) return false end file:write("# MAPRATING_MAPS_V2\n") file:write("# MAP\\tmap-id\\tchecksum\\tuser-average\\tuser-count\\tvip-average\\tvip-count\n") file:write("# RATING\\tplayer\\trating\\twas-vip\n") file:write("# ENDMAP\n") for _, key in ipairs(sortedMapIDs()) do local data = MapRatings[key] local userSum, userCount, vipSum, vipCount = calculateMapTotals(data) file:write( "MAP\t", encodeField(data.mapid), "\t", encodeField(data.checksum), "\t", formatAverage(userSum, userCount), "\t", userCount, "\t", formatAverage(vipSum, vipCount), "\t", vipCount, "\n" ) for _, playerKey in ipairs(sortedRatingKeys(data)) do local rating = data.ratings[playerKey] file:write("RATING\t", encodeField(rating.name), "\t", rating.value, "\t", rating.vip and 1 or 0, "\n") end file:write("ENDMAP\n") end file:close() return true end local function loadPlayers() if not isserver then return end local file = openDataFile(PLAYERS_FILE, "r") if not file then print("mapratings: no existing " .. PLAYERS_FILE .. "; starting a new player database.") return end for line in file:lines() do if line ~= "" and line:sub(1, 1) ~= "#" then local fields = splitTabs(line) if fields[1] == "PLAYER" then local name = decodeField(fields[2]) local races = tonumber(fields[3]) or 0 if name ~= "" then local stats = getPlayerStats(name, true) stats.races = races end end end end file:close() print("mapratings: loaded " .. PLAYERS_FILE .. ".") end local function loadMapRatings() if not isserver then return end local file = openDataFile(ratings_file, "r") if not file then print("mapratings: no existing " .. ratings_file .. "; starting a new map database.") return end local currentData = nil for line in file:lines() do if line ~= "" and line:sub(1, 1) ~= "#" then local fields = splitTabs(line) local recordType = fields[1] if recordType == "MAP" then local mapid = decodeField(fields[2]) local checksum = string.lower(decodeField(fields[3])) if mapid ~= "" and checksum ~= "" then currentData = getMapRatings(mapid, checksum, true) else currentData = nil end elseif recordType == "RATING" and currentData then local name = decodeField(fields[2]) local value = tonumber(fields[3]) local wasVIP = tonumber(fields[4]) == 1 if name ~= "" and value ~= nil and value >= MIN_RATING and value <= MAX_RATING then currentData.ratings[normalizeName(name)] = { name = name, value = value, vip = wasVIP, } end elseif recordType == "ENDMAP" then currentData = nil end end end file:close() print("mapratings: loaded " .. ratings_file .. ".") end local function markParticipant(player) if not player or not player.valid or player.spectator then return end local name = tostring(player.name or "") if name == "" then return end CurrentParticipants[normalizeName(name)] = name getPlayerStats(name, true) end local function playerRatedCurrentMap(name) if not CurrentMapID then return false end local data = getMapRatings(CurrentMapID, CurrentMapChecksum, false) if not data then return false end return data.ratings[normalizeName(name)] ~= nil end local function maybePromptPlayer(player) if not isserver or not CurrentMapID or not CurrentMapChecksum or not player or not player.valid or player.spectator then return end local playerKey = normalizeName(player.name) if PromptedThisMap[playerKey] then return end PromptedThisMap[playerKey] = true if not playerIsEligible(player.name) then return end if playerRatedCurrentMap(player.name) then return end sendPrivateMessage(player, "Reminder that you can rank this map before the next map starts using '?rate <0-5>'") end local function beginMap(mapnum) if not isserver then return end local currentMapNumber = mapnum or gamemap CurrentMapID = getMapID(currentMapNumber) CurrentMapChecksum = getMapChecksum(currentMapNumber) CurrentParticipants = {} PromptedThisMap = {} RaceCountCommitted = false if CurrentMapChecksum then getMapRatings(CurrentMapID, CurrentMapChecksum, true) end for player in players.iterate do markParticipant(player) end end local function commitCompletedRace() if not isserver or RaceCountCommitted or not CurrentMapID then return end RaceCountCommitted = true for playerKey, playerName in pairs(CurrentParticipants) do local stats = PlayerStats[playerKey] if not stats then stats = getPlayerStats(playerName, true) end stats.races = stats.races + 1 end savePlayers() end local function parseRating(valueText) local text = tostring(valueText or "") text = text:match("^%s*(.-)%s*$") or "" if not text:match("^%d+$") then return nil end local value = tonumber(text) if value == nil or value < MIN_RATING or value > MAX_RATING then return nil end return value end local function submitRating(player,valueText) if not isserver or not player or not player.valid then return end if not CurrentMapID then sendPrivateMessage(player, "There is no active map to rate.") return end if not CurrentMapChecksum then sendPrivateMessage(player, "This map cannot be rated because its checksum is unavailable.") return end markParticipant(player) local playerKey = normalizeName(player.name) local stats = getPlayerStats(player.name, true) if not playerIsEligible(player.name) then sendPrivateMessage( player, "You need at least " .. REQUIRED_RACES .. " completed races before rating maps. You currently have " .. stats.races .. "." ) return end if not CurrentParticipants[playerKey] then sendPrivateMessage(player, "You must participate in this map before rating it.") return end local value = parseRating(valueText) if value == nil then sendPrivateMessage(player, "Usage: ?rate <0-5>") return end local data = getMapRatings(CurrentMapID, CurrentMapChecksum, true) local previous = data.ratings[playerKey] data.ratings[playerKey] = { name = player.name, value = value, vip = isVIP(player.name), } if not saveMapRatings() then data.ratings[playerKey] = nil sendPrivateMessage(player, "Your rating could not be saved. Please tell a server administrator.") return end sendPrivateMessage(player, "Recorded your " .. value .. "/5 rating for " .. CurrentMapID .. ".") end addHook("MapLoad", beginMap) addHook("IntermissionThinker", commitCompletedRace) addHook("PlayerThink", function(player) if not isserver or not CurrentMapID then return end markParticipant(player) if leveltime >= PROMPT_DELAY then maybePromptPlayer(player) end end) addHook("PlayerMsg", function(player,msgtype,target,message,muted) if not message then return end local command, argument = message:match("^%s*%?([%w_]+)%s*(.-)%s*$") if not command or string.lower(command) ~= "rate" then return end if isserver then if muted then sendPrivateMessage(player, "Your rating command was blocked by chat spam protection.") else submitRating(player, argument) end end return true end) COM_AddCommand("mapratings_status", function(player) CONS_Printf( player, "mapratings: checksum support loaded; current map=" .. tostring(CurrentMapID) .. ", checksum=" .. tostring(CurrentMapChecksum) .. ", player records=" .. tostring(#sortedPlayerKeys()) .. ", map records=" .. tostring(#sortedMapIDs()) ) end, COM_ADMIN) print("mapratings: MAP-RATINGS-CHECKSUM-V3 loaded; isserver=" .. tostring(isserver)) if isserver then loadPlayers() loadMapRatings() if gamestate == GS_LEVEL then beginMap(gamemap) end end COM_AddCommand("mapratingshud", function(player,option) option = string.lower(tostring(option or "")) if option == "" or option == "toggle" then VoteRatingsHUDEnabled = not VoteRatingsHUDEnabled elseif option == "on" or option == "1" then VoteRatingsHUDEnabled = true elseif option == "off" or option == "0" then VoteRatingsHUDEnabled = false else CONS_Printf(player, "Usage: mapratingshud ") return end CONS_Printf(player, "Vote-screen map ratings: " .. (VoteRatingsHUDEnabled and "enabled" or "disabled")) end, COM_LOCAL) local LastVoteStatus = { highPoolSize = 0, normalPoolSize = 0, hellPoolSize = 0, topRoll = -1, bottomRoll = -1, topMap = nil, middleMap = nil, bottomMap = nil, ratingsLoaded = false, packetQueued = false, } local function openDataFile(filename,mode) if io.openlocal then return io.openlocal(filename, mode) end return io.open(filename, mode) end local function normalizeMapID(mapid) return string.upper(tostring(mapid or "")) end local function decodeField(value) return (tostring(value or ""):gsub("%%(%x%x)", function(hex) return string.char(tonumber(hex, 16)) end)) end local function splitTabs(line) local fields = {} for value in (line .. "\t"):gmatch("(.-)\t") do table.insert(fields, value) end return fields end local function integerDivide(numerator,denominator) return (numerator - (numerator % denominator)) / denominator end local function hasFlag(value,flag) value = tonumber(value) or 0 flag = tonumber(flag) or 0 if flag <= 0 then return false end return (value % (flag * 2)) >= flag end local function baseGametype(gt) gt = tonumber(gt) or 0 if gt >= 128 then gt = gt - 128 end return gt end local function mapSupportsRace(header) if not header then return false end local raceFlag = _G["TOL_RACE"] if raceFlag == nil then return false end return hasFlag(tonumber(header.typeoflevel) or 0, raceFlag) end local function mapForcesThirdSlotRace(header,secondgt) if not mapSupportsRace(header) then return false end local matchGT = _G["GT_MATCH"] local matchFlag = _G["TOL_MATCH"] if matchGT ~= nil and matchFlag ~= nil and baseGametype(secondgt) == matchGT then return not hasFlag(tonumber(header.typeoflevel) or 0, matchFlag) end return true end local function isHiddenFromMenu(header) if not header then return false end local hideFlag = _G["LF2_HIDEINMENU"] or 1 return hasFlag(tonumber(header.menuflags) or 0, hideFlag) end local function mapNumberToID(mapnum) return normalizeMapID(G_BuildMapName(mapnum)) end local function buildMapLookup() local byID = {} for mapnum = 1, #mapheaderinfo do local header = mapheaderinfo[mapnum] if header then byID[mapNumberToID(mapnum)] = mapnum end end return byID end local function buildHellLookup(mapLookup) local lookup = {} for mapnum = 1, #mapheaderinfo do local header = mapheaderinfo[mapnum] if header and isHiddenFromMenu(header) then lookup[mapnum] = true end end return lookup end local function getRatingTotalsForSource(data) return data.userSum + data.vipSum, data.userCount + data.vipCount end local function mapIsHighlyRated(data) local sum, count = getRatingTotalsForSource(data) if count < 1 then return false end return sum * high_rating_denominator >= count * high_rating_numerator end local function loadRatingTotals() local totals = {} local file, errorMessage = openDataFile(ratings_file, "r") if not file then print("ratedvote: could not read " .. ratings_file .. ": " .. tostring(errorMessage)) return totals, false end local current = nil for line in file:lines() do if line ~= "" and line:sub(1, 1) ~= "#" then local fields = splitTabs(line) local recordType = fields[1] if recordType == "MAP" then local mapid = normalizeMapID(decodeField(fields[2])) local checksum = string.lower(decodeField(fields[3])) if mapid ~= "" and checksum ~= "" then current = { mapid = mapid, checksum = checksum, userSum = 0, userCount = 0, vipSum = 0, vipCount = 0, } totals[makeMapRatingKey(mapid, checksum)] = current else current = nil end elseif recordType == "RATING" and current then local value = tonumber(fields[3]) local wasVIP = tonumber(fields[4]) == 1 if value ~= nil and value >= 0 and value <= 5 then if wasVIP then current.vipSum = current.vipSum + value current.vipCount = current.vipCount + 1 else current.userSum = current.userSum + value current.userCount = current.userCount + 1 end end elseif recordType == "ENDMAP" then current = nil end end end file:close() return totals, true end local function buildNormalPool(prevmap,hellLookup,compatibilityCheck) local pool = {} compatibilityCheck = compatibilityCheck or mapSupportsRace for mapnum = 1, #mapheaderinfo do local header = mapheaderinfo[mapnum] if header and mapnum ~= prevmap and not hellLookup[mapnum] and not isHiddenFromMenu(header) and compatibilityCheck(header) then table.insert(pool, mapnum) end end return pool end local function buildHighRatedPool(ratingTotals,mapLookup,prevmap,hellLookup) local pool = {} for mapid, mapnum in pairs(mapLookup) do local header = mapheaderinfo[mapnum] local checksum = getMapChecksum(mapnum) local data = checksum and ratingTotals[makeMapRatingKey(mapid, checksum)] or nil if data and mapnum ~= prevmap and not hellLookup[mapnum] and header and not isHiddenFromMenu(header) and mapSupportsRace(header) and mapIsHighlyRated(data) then table.insert(pool, mapnum) end end return pool end local function buildHellPool(hellLookup,prevmap,secondgt) local pool = {} for mapnum in pairs(hellLookup) do local header = mapheaderinfo[mapnum] if mapnum ~= prevmap and header and mapForcesThirdSlotRace(header, secondgt) then table.insert(pool, mapnum) end end return pool end local function buildEmergencyPool(prevmap,compatibilityCheck) local pool = {} compatibilityCheck = compatibilityCheck or mapSupportsRace for mapnum = 1, #mapheaderinfo do local header = mapheaderinfo[mapnum] if header and mapnum ~= prevmap and compatibilityCheck(header) then table.insert(pool, mapnum) end end return pool end local function chooseRandom(pool) if #pool == 0 then return nil end return pool[P_RandomKey(#pool) + 1] end local function chooseRandomUnused(pool,used) local available = {} for _, mapnum in ipairs(pool) do if not used[mapnum] then table.insert(available, mapnum) end end return chooseRandom(available) end local function chooseRequiredMap(preferredPool,normalPool,emergencyPool,used) local mapnum = chooseRandomUnused(preferredPool or {}, used) if not mapnum then mapnum = chooseRandomUnused(normalPool or {}, used) end if not mapnum then mapnum = chooseRandomUnused(emergencyPool or {}, used) end if not mapnum then mapnum = chooseRandom(normalPool or {}) end if not mapnum then mapnum = chooseRandom(emergencyPool or {}) end if mapnum then used[mapnum] = true end return mapnum end local function averageToTenths(sum,count) if not count or count <= 0 then return -1 end local rounding = integerDivide(count, 2) return integerDivide(sum * 10 + rounding, count) end local function getMapPacketValues(mapnum,ratingTotals,hellLookup) local mapid = mapNumberToID(mapnum) local checksum = getMapChecksum(mapnum) local data = checksum and ratingTotals[makeMapRatingKey(mapid, checksum)] or nil local userAverage = -1 local vipAverage = -1 if data then userAverage = averageToTenths(data.userSum, data.userCount) vipAverage = averageToTenths(data.vipSum, data.vipCount) end return userAverage, vipAverage, hellLookup[mapnum] and 1 or 0 end local function queueVoteHUDPacket(result,ratingTotals,hellLookup) if not isserver then return false end if not result[1] or not result[2] or not result[3] then return false end VotePacketGeneration = VotePacketGeneration + 1 local command = { "_ratedvotehud_data", tostring(VotePacketGeneration), } for slot = 1, 3 do local mapnum = tonumber(result[slot]) local userAverage, vipAverage, isHell = getMapPacketValues(mapnum, ratingTotals, hellLookup) table.insert(command, tostring(mapnum)) table.insert(command, tostring(userAverage)) table.insert(command, tostring(vipAverage)) table.insert(command, tostring(isHell)) end COM_BufAddText(consoleplayer, table.concat(command, " ")) return true end local function displayMap(mapnum) if not mapnum then return "none" end return mapNumberToID(mapnum) end local function setupVote(result,gt,secondgt,prevmap) if not isserver then return end local raceGT = _G["GT_RACE"] if raceGT ~= nil and baseGametype(gt) ~= raceGT then print("ratedvote: WARNING: primary vote gametype is not Race; set kartgametypepreference Race.") end local ratingTotals, ratingsLoaded = loadRatingTotals() local mapLookup = buildMapLookup() local hellLookup = buildHellLookup(mapLookup) local normalPool = buildNormalPool(prevmap, hellLookup, mapSupportsRace) local bottomNormalPool = buildNormalPool( prevmap, hellLookup, function(header) return mapForcesThirdSlotRace(header, secondgt) end ) local highPool = buildHighRatedPool(ratingTotals, mapLookup, prevmap, hellLookup) local hellPool = buildHellPool(hellLookup, prevmap, secondgt) local emergencyRacePool = buildEmergencyPool(prevmap, mapSupportsRace) local emergencyBottomRacePool = buildEmergencyPool( prevmap, function(header) return mapForcesThirdSlotRace(header, secondgt) end ) local topRoll = P_RandomKey(100) local bottomRoll = P_RandomKey(100) local used = {} local topPreferred = nil local bottomPreferred = nil if topRoll < high_map_chance then topPreferred = highPool end if bottomRoll < hell_map_chance then bottomPreferred = hellPool end local topMap = chooseRequiredMap(topPreferred, normalPool, emergencyRacePool, used) local middleMap = chooseRequiredMap(nil, normalPool, emergencyRacePool, used) local bottomMap = chooseRequiredMap(bottomPreferred, bottomNormalPool, emergencyBottomRacePool, used) if not topMap or not middleMap or not bottomMap then print("ratedvote: could not generate all three visible vote maps.") return end result[1] = topMap result[2] = middleMap result[3] = bottomMap local packetQueued = queueVoteHUDPacket(result, ratingTotals, hellLookup) LastVoteStatus = { highPoolSize = #highPool, normalPoolSize = #normalPool, hellPoolSize = #hellPool, topRoll = topRoll, bottomRoll = bottomRoll, topMap = topMap, middleMap = middleMap, bottomMap = bottomMap, ratingsLoaded = ratingsLoaded, packetQueued = packetQueued, } if debug_console_vote then print( "ratedvote: high pool=" .. #highPool .. ", normal pool=" .. #normalPool .. ", bottom race pool=" .. #bottomNormalPool .. ", hell pool=" .. #hellPool .. "; top roll=" .. topRoll .. ", maps=" .. displayMap(topMap) .. "/" .. displayMap(middleMap) .. "/" .. displayMap(bottomMap) .. ", packet=" .. (packetQueued and "queued" or "not queued") ) end end if HAS_SATURN_VOTE_API then addHook("SetupVote", setupVote) end local function serverSentCommand(sender) if not sender or not sender.valid then return false end local serverPlayerNumber = _G["serverplayer"] if serverPlayerNumber ~= nil then return #sender == serverPlayerNumber end return true end local function parsePacketSlot(mapText,userText,vipText,hellText) local mapnum = tonumber(mapText) local userAverage = tonumber(userText) local vipAverage = tonumber(vipText) local hellValue = tonumber(hellText) if not mapnum or mapnum <= 0 then return nil end if userAverage == nil or vipAverage == nil or hellValue == nil then return nil end return { map = mapnum, userAverage = userAverage, vipAverage = vipAverage, hell = hellValue ~= 0, } end COM_AddCommand("_ratedvotehud_data", function( sender, generationText, map1, user1, vip1, hell1, map2, user2, vip2, hell2, map3, user3, vip3, hell3 ) if not serverSentCommand(sender) then return end if not HAS_SATURN_VOTE_API then return end local generation = tonumber(generationText) local slot1 = parsePacketSlot(map1, user1, vip1, hell1) local slot2 = parsePacketSlot(map2, user2, vip2, hell2) local slot3 = parsePacketSlot(map3, user3, vip3, hell3) if not generation or not slot1 or not slot2 or not slot3 then return end ReceivedVoteHUDData = { generation = generation, slots = {slot1, slot2, slot3}, } end, COM_ADMIN) addHook("MapLoad", function() ReceivedVoteHUDData = nil DebugVoteHUDData = nil DebugVoteGeneration = nil end) local function buildStarRating(value,fullColor,halfColor) if value == nil or value < 0 then return nil end local halfSteps = integerDivide(value + 2, 5) if halfSteps < 0 then halfSteps = 0 end if halfSteps > 10 then halfSteps = 10 end local fullStars = integerDivide(halfSteps, 2) local hasHalfStar = (halfSteps % 2) ~= 0 local text = "" if fullStars > 0 then text = text .. fullColor .. string.rep("*", fullStars) end if hasHalfStar then text = text .. halfColor .. "*" end return text .. color_def end local function drawVoteRatingLine(v,x,y,text,flags) v.drawString(x, y, text, flags | V_SNAPTORIGHT, "thin") end if HAS_SATURN_VOTE_API then hud.add(function(v) if not VoteRatingsHUDEnabled then return end local packet = ReceivedVoteHUDData if not packet then return end for slot = 1, 3 do local data = packet.slots[slot] local y = rating_hud_ys[slot] if data.userAverage >= 0 then local stars = buildStarRating(data.userAverage, color_full, color_half) drawVoteRatingLine(v, rating_hud_x, y, stars, 0) y = y + rating_hud_line_height end if data.vipAverage >= 0 then local stars = buildStarRating(data.vipAverage, color_vip_full, color_vip_half) drawVoteRatingLine(v, rating_hud_x, y, stars, 0) y = y + rating_hud_line_height end if data.hell then drawVoteRatingLine(v, rating_hud_x, y, "HELL", V_REDMAP) end end end, "vote") end COM_AddCommand("ratedvote_status", function(player) CONS_Printf( player, "ratedvote: Saturn vote API=" .. tostring(HAS_SATURN_VOTE_API) .. ", ratings=" .. (LastVoteStatus.ratingsLoaded and "loaded" or "unavailable") .. ", packet=" .. (LastVoteStatus.packetQueued and "queued" or "not queued") ) CONS_Printf( player, "ratedvote: pools high=" .. LastVoteStatus.highPoolSize .. ", normal=" .. LastVoteStatus.normalPoolSize .. ", hell=" .. LastVoteStatus.hellPoolSize ) CONS_Printf( player, "ratedvote: rolls top=" .. LastVoteStatus.topRoll .. ", bottom=" .. LastVoteStatus.bottomRoll .. ", maps=" .. displayMap(LastVoteStatus.topMap) .. "/" .. displayMap(LastVoteStatus.middleMap) .. "/" .. displayMap(LastVoteStatus.bottomMap) ) end, COM_ADMIN)