-- Leaderboards written by Not local FILENAME = "leaderboard.txt" local lb = {} local timeFinished = 0 local disable = false local prevLap = 0 local splits = {} local PATCH = nil local help = true -- Retry / changelevel map local nextMap = nil local Flags = 0 -- SPB flags with the least significance first local F_SPBATK = 0x1 local F_SPBJUS = 0x2 local F_SPBBIG = 0x4 local F_SPBEXP = 0x8 local clearcheats = false local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) local AFK_TIMEOUT = TICRATE * 5 local AFK_BALANCE = TICRATE * 60 local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5 -- patch caching function local cachePatches local function lbID(map, flags) local id = tostring(map) if flags & F_SPBATK then id = id + "S" end return id end local function score_t(map, name, skin, color, time, splits, flags) return { ["map"] = map, ["name"] = name, ["skin"] = skin, ["color"] = color, ["time"] = time, ["splits"] = splits, ["flags"] = flags } end -- Read the leaderboard local f = io.open(FILENAME, "r") if f then for l in f:lines() do -- Leaderboard is stored in the following tab separated format -- name, skin, color, time, splits, flags local t = {} for word in (l+"\t"):gmatch("(.-)\t") do table.insert(t, word) end local flags = 0 if t[7] != nil then flags = tonumber(t[7]) end local lbt = lb[lbID(t[1], flags)] if lbt == nil then lbt = {} end local spl = {} if t[6] != nil then for str in t[6]:gmatch("([^ ]+)") do table.insert(spl, tonumber(str)) end end table.insert( lbt, score_t( t[1], t[2], t[3], t[4], tonumber(t[5]), spl, flags ) ) lb[lbID(t[1], flags)] = lbt end f:close() else print("Failed to open file: ", FILENAME) end local function allowJoin(v) local y if v then y = "yes" hud.enable("freeplay") else y = "no" hud.disable("freeplay") end COM_BufInsertText(server, "allowteamchange " + y) end local function ingame() local n = 0 for p in players.iterate do if p.valid and not p.spectator then n = $ + 1 end end return n end local function initLeaderboard(player) if disable and leveltime < START_TIME then disable = ingame() > 1 else disable = disable or ingame() > 1 end player.afkTimeout = leveltime end addHook("PlayerSpawn", initLeaderboard) local function doyoudare(player) if ingame() > 1 or player.spectator then CONS_Printf(player, "How dare you") return false end return true end local function retry(player, ...) if doyoudare(player) then -- Prevents bind crash if leveltime < 20 then return end nextMap = G_BuildMapName(gamemap) end end COM_AddCommand("retry", retry) local function exitlevel(player, ...) if doyoudare(player) then G_ExitLevel() end end COM_AddCommand("exit", exitlevel) local function mapNotExists(player, map) CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper())) end local ALPH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" local function changelevel(player, ...) if not doyoudare(player) then return end if leveltime < 20 then return end local map = ... if map == nil then CONS_Printf(player, "Usage: changelevel MAPXX") return end local p, q = map:upper():match("MAP(%w)(%w)$", 1) if not (p and q) then CONS_Printf(player, string.format("Invalid map name: %s", map)) return end local mapnum = 0 if tonumber(p) != nil then -- Non extended map numbers if tonumber(q) == nil then mapNotExists(player, map) return end mapnum = tonumber(p) * 10 + tonumber(q) else --Extended map numbers p = ALPH:find(p) - 1 q = (tonumber(q) or ALPH:find(q) + 9) mapnum = 36 * p + q + 100 end if mapheaderinfo[mapnum] == nil then mapNotExists(player, map) return end nextMap = G_BuildMapName(mapnum) end COM_AddCommand("changelevel", changelevel) local function clearcheats(player) if not player.spectator then clearcheats = true CONS_Printf(player, "SPB Attack cheats will be cleared on next round") end end COM_AddCommand("spba_clearcheats", clearcheats) --DEBUGGING --local function printTable(tb) -- for k, v in pairs(tb) do -- for i = 1, #v do -- print("TABLE: " + k, tb[k]) -- if v[i] != nil then -- print( -- v[i]["name"], -- v[i]["skin"], -- v[i]["color"], -- v[i]["time"], -- table.concat(v[i]["splits"]), -- v[i]["flags"], -- "," -- ) -- -- end -- end -- end --end addHook("MapLoad", function() timeFinished = 0 splits = {} prevLap = 0 allowJoin(true) --printTable(lb) end ) local function ticsToTime(tics) return string.format( "%d:%02d:%02d", G_TicsToMinutes(tics), G_TicsToSeconds(tics), G_TicsToCentiseconds(tics) ) end -- Item patches have the amazing property of being displaced 12x 13y pixels local iXoffset = 13 * FRACUNIT local iYoffset = 12 * FRACUNIT local function drawitem(v, x, y, scale, itempatch, vflags) v.drawScaled( x * FRACUNIT - FixedMul(iXoffset, scale), y * FRACUNIT - FixedMul(iYoffset, scale), scale, itempatch, vflags ) end local function marquee(text, maxwidth) if #text <= maxwidth then return text end local shift = 16 local pos = ((leveltime / 16) % (#text - maxwidth + shift * 2)) + 1 - shift pos = min(max(pos, 1), #text - maxwidth + 1) return text:sub(pos, pos + maxwidth - 1) end local bodium = {V_YELLOWMAP, V_GRAYMAP, V_BROWNMAP, 0} local splitColor = {[true]=V_SKYMAP, [false]=V_REDMAP} local splitSymbol = {[true]="-", [false]="+"} local showSplit = 0 local VFLAGS = V_SNAPTOLEFT local function drawScoreboard(v, player) if disable then return end if player != displayplayers[0] then return end cachePatches(v) local m = lb[lbID(gamemap, Flags)] if m == nil then return end for i, score in ipairs(m) do local name = score["name"] local skin = skins[score["skin"]] if skin == nil then skin = skins["sonic"] end local skinPatch = PATCH["FACERANK"][skin.name] -- | OFFSET | + | PADDING | * |INDEX| local h = ((200 / 4) + 4) + (skinPatch.height + 4) * (i - 1) v.draw(4, h, skinPatch, V_HUDTRANS | VFLAGS, v.getColormap("sonic", score["color"])) if player.name == name then v.draw(4, h, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS) end -- SPB if score["flags"] & F_SPBATK then local scale = FRACUNIT / 4 drawitem( v, 4 - 2, h - 2, scale, PATCH["SPB"], V_HUDTRANS | VFLAGS ) if score["flags"] & F_SPBEXP then drawitem( v, skinPatch.width, h - 2, scale, PATCH["INV"][(leveltime / 4) % 6], V_HUDTRANS | VFLAGS ) end if score["flags"] & F_SPBBIG then drawitem( v, 4 - 2, h + skinPatch.height - 4, scale, PATCH["BIG"], V_HUDTRANS | VFLAGS ) end if score["flags"] & F_SPBJUS then drawitem( v, skinPatch.width, h + skinPatch.height - 4, scale, PATCH["HYUD"], V_HUDTRANS | VFLAGS ) end end -- Shorten long names local stralign = "left" local MAXWIDTH = 70 local px = 6 local py = 0 if v.stringWidth(name) > MAXWIDTH then stralign = "thin" py = -1 if v.stringWidth(name, 0, "thin") > MAXWIDTH then stralign = "small" py = 2 if v.stringWidth(name, 0, "small") > MAXWIDTH then name = marquee(name, 15) end end end v.drawString( px + skinPatch.width, h + py, name, V_HUDTRANSHALF | V_ALLOWLOWERCASE | VFLAGS, stralign ) -- Draw splits if showSplit > 0 and score["splits"][prevLap] != nil then local split = splits[prevLap] - score["splits"][prevLap] v.drawString( px + skinPatch.width, h + 8, splitSymbol[split < 0] + ticsToTime(abs(split)), V_HUDTRANSHALF | splitColor[split < 0] | VFLAGS ) else v.drawString( px + skinPatch.width, h + 8, ticsToTime(score["time"]), V_HUDTRANSHALF | bodium[min(i, 4)] | VFLAGS ) end end end hud.add(drawScoreboard, "game") function cachePatches(v) if PATCH == nil then PATCH = {} PATCH["CHILI"] = {} for i = 1, 8 do PATCH["CHILI"][i-1] = v.cachePatch("K_CHILI" + i) end PATCH["FACERANK"] = {} for skin in skins.iterate do PATCH["FACERANK"][skin.name] = v.cachePatch(skin.facerank) end PATCH["SPB"] = v.cachePatch("K_ISSPB") PATCH["INV"] = {} for i = 1, 6 do PATCH["INV"][i - 1] = v.cachePatch("K_ISINV" + i) end PATCH["BIG"] = v.cachePatch("K_ISGROW") PATCH["HYUD"] = v.cachePatch("K_ISHYUD") end end -- True if a is better than b local function lbComp(a, b) -- Calculates the difficulty, harder has higher priority -- if s is positive then a is harder -- if s is negative then b is harder -- if s is 0 then compare time local s = (a["flags"] & (F_SPBEXP | F_SPBBIG)) - (b["flags"] & (F_SPBEXP | F_SPBBIG)) return s > 0 or not(s < 0 or a["time"] > b["time"]) end local function saveTime(player) local m = lb[lbID(gamemap, Flags)] if m == nil then m = {} end local newscore = score_t( gamemap, player.name, player.mo.skin, player.skincolor, timeFinished, splits, Flags ) -- Check if you beat your previous best for i = 1, #m do if m[i]["name"] == player.name then if lbComp(newscore, m[i]) then table.remove(m, i) S_StartSound(nil, 130) break else -- You suck lol S_StartSound(nil, 201) return end end end print("Saving score") table.insert( m, newscore ) table.sort(m, lbComp) while #m > 5 do table.remove(m) end lb[lbID(gamemap, Flags)] = m local f = assert(io.open(FILENAME, "w")) if f == nil then print("Failed to open file for writing: " + FILENAME) return end for k, v in pairs(lb) do for i = 1, #v do local s = v[i] f:write( s["map"], "\t", s["name"], "\t", s["skin"], "\t", s["color"], "\t", s["time"], "\t", table.concat(s["splits"], " "), "\t", s["flags"], "\n" ) end end f:close() end -- DEBUGGING --local function saveLeaderboard(player, ...) -- timeFinished = player.realtime -- splits = {1000, 2000, 3000} -- saveTime(player) --end --COM_AddCommand("save", saveLeaderboard) local function regLap(player) if player.laps > prevLap and timeFinished == 0 then prevLap = player.laps table.insert(splits, player.realtime) showSplit = 5 * TICRATE end end local function getGamer() for p in players.iterate do if p.valid and not p.spectator then return p end end end local function think() if nextMap then COM_BufInsertText(server, "map " + nextMap) nextMap = nil end if disable then if ingame() > 1 then for p in players.iterate do if p.valid and not p.spectator and not p.exiting then if p.cmd.buttons then p.afkTimeout = leveltime end if p.afkTimeout + AFK_BALANCE < leveltime then p.spectator = true chatprint("\x89" + p.name + " was moved to the other team for game balance", true) end end end else for p in players.iterate do if p.valid and not p.spectator then p.afkTimeout = leveltime end end end help = true return end if showSplit > 0 then showSplit = showSplit - 1 end local p = getGamer() if leveltime < START_TIME then -- Help message if leveltime == START_TIME - TICRATE * 3 then if ingame() == 1 then if help then help = false chatprint("\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats", true) end else help = true end end -- Autospec if leveltime == 1 then if p then for s in players.iterate do if s.valid and s.spectator then COM_BufInsertText(s, string.format("view \"%d\"", #p)) end end end end -- Gamemode flags Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS) if leveltime > START_TIME - (3 * TICRATE) / 2 and server.SPBArunning then Flags = $ | F_SPBATK if server.SPBAexpert then Flags = $ | F_SPBEXP end if clearcheats then clearcheats = false for p in players.iterate do p.SPBAKARTBIG = false p.SPBAjustice = false p.SPBAshutup = false end end for p in players.iterate do if not p.spectator then if p.SPBAKARTBIG then Flags = $ | F_SPBBIG end if p.SPBAjustice then Flags = $ | F_SPBJUS end end end end end local cv_teamchange = CV_FindVar("allowteamchange") if p then if p.cmd.buttons then p.afkTimeout = leveltime end if leveltime > PREVENT_JOIN_TIME and p.afkTimeout + AFK_TIMEOUT > leveltime then if cv_teamchange.value then allowJoin(false) end elseif p.afkTimeout + AFK_TIMEOUT < leveltime then if not cv_teamchange.value then allowJoin(true) end end if p.laps >= mapheaderinfo[gamemap].numlaps and timeFinished == 0 then timeFinished = p.realtime saveTime(p) end regLap(p) elseif cv_teamchange.value == 0 then allowJoin(true) end end addHook("ThinkFrame", think) local function interThink() if nextMap then COM_BufInsertText(server, "map " + nextMap) nextMap = nil end if not CV_FindVar("allowteamchange").value then allowJoin(true) end end addHook("IntermissionThinker", interThink) local function netvars(net) lb = net($) end addHook("NetVars", netvars)