-- Leaderboards written by Not -- Reusable -- Leaderboard Table -- [mode][mapnum][scoreTable] local lb = {} local timeFinished = 0 local disable = false local prevLap = 0 local splits = {} local PATCH = nil local help = true local EncoreInitial = nil local scoreTable -- Text flash on finish local FlashTics = 0 local FlashRate local FlashVFlags local YellowFlash = { [0] = V_YELLOWMAP, [1] = V_ORANGEMAP, [2] = 0 } local RedFlash = { [0] = V_REDMAP, [1] = 0 } -- Tracks if stats have been written or not local StatTrack = false local UNCLAIMED = "Unclaimed Record" local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records" local FILENAME = "leaderboard.txt" -- Retry / changelevel map local nextMap = nil local Flags = 0 local F_ENCORE = 0x80 -- SPB flags with the least significance first local F_SPBATK = 0x1 local F_SPBJUS = 0x2 local F_SPBBIG = 0x4 local F_SPBEXP = 0x8 -- Score table separator local ST_SEP = F_SPBATK local clearcheats = false local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1 local AFK_TIMEOUT = TICRATE * 5 local AFK_BALANCE = TICRATE * 60 local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10 local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5 local GUI_OFF = 0x0 local GUI_SPLITS = 0x1 local GUI_ON = 0x2 -- Draw states local DS_DEFAULT = 0x0 local DS_SCROLL = 0x1 local DS_AUTO = 0x2 local DS_SCRLTO = 0x4 local drawState = DS_DEFAULT -- fixed_t scroll position local scrollY = 50 * FRACUNIT local scrollAcc = 0 -- functions -- -- patch caching local cachePatches -- clamp(min, v, max) local clamp local scroll_to local ticsToTime local allowJoin --------------- -- cvars local cv_teamchange local cv_spbatk local cv_gui = CV_RegisterVar({ name = "lb_gui", defaultvalue = GUI_ON, flags = 0, PossibleValue = {Off = GUI_OFF, Splits = GUI_SPLITS, On = GUI_ON} }) local AntiAFK = true CV_RegisterVar({ name = "lb_afk", defaultvalue = 1, flags = CV_NETVAR | CV_CALL, PossibleValue = CV_OnOff, func = function(v) -- Set players afkTime and toggle AntiAFK if v.value then for p in players.iterate do p.afkTime = leveltime end AntiAFK = true else AntiAFK = false end end }) local cv_enable = CV_RegisterVar({ name = "lb_enable", defaultvalue = 1, flags = CV_NETVAR | CV_CALL, PossibleValue = CV_OnOff, func = function(v) disable = $ or not v.value if disable then allowJoin(true) end end }) local cv_saves = CV_RegisterVar({ name = "lb_save_count", defaultvalue = 20, flags = CV_NETVAR, PossibleValue = CV_Natural }) local cv_interrupt = CV_RegisterVar({ name = "lb_interrupt", defaultvalue = 0, flags = CV_NETVAR | CV_CALL, PossibleValue = CV_OnOff, func = function(v) if v.value then COM_BufInsertText(server, "allowteamchange yes") end end }) local function setST(t, map, flags, scoreTable) local mode = flags & ST_SEP t[mode] = t[mode] or {} t[mode][map] = scoreTable end local function getST(t, map, flags) local mode = flags & ST_SEP return t[mode] and t[mode][map] or nil end local function setScoreTable(map, flags, scoreTable) setST(lb, map, flags, scoreTable) end local function getScoreTable(map, flags) return getST(lb, map, flags) 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 sortScores() for mode, t in pairs(lb) do for map, scoreTable in pairs(t) do table.sort(scoreTable, lbComp) setScoreTable(map, mode, scoreTable) end end end -- Reinitialize lb based on new ST_SEP value local function reinit_lb() local nlb = {} for mode, t in pairs(lb) do for map, scoreTable in pairs(t) do for i, score in ipairs(scoreTable) do local st = getST(nlb, map, score["flags"]) or {} table.insert(st, score) setST(nlb, map, score["flags"], st) end end end lb = nlb sortScores() end local cv_spb_separate = CV_RegisterVar({ name = "lb_spb_combined", defaultvalue = 1, flags = CV_NETVAR | CV_CALL, PossibleValue = CV_YesNo, func = function(v) if v.value then ST_SEP = F_SPBATK else ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP end reinit_lb() end }) local function score_t(map, name, skin, color, time, splits, flags, stat) return { ["map"] = map, ["name"] = name, ["skin"] = skin, ["color"] = color, ["time"] = time, ["splits"] = splits, ["flags"] = flags, ["stat"] = stat } end local function stat_t(speed, weight) if speed and weight then return { ["speed"] = speed, ["weight"] = weight } end return nil end local function stat_str(stat) if stat then return string.format("%d%d", stat["speed"], stat["weight"]) end return "0" 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 -- mapnum, name, skin, color, time, splits, flags, stat 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 scoreTable = getScoreTable(tonumber(t[1]), flags) or {} local spl = {} if t[6] != nil then for str in t[6]:gmatch("([^ ]+)") do table.insert(spl, tonumber(str)) end end local stats = nil if t[8] != nil then if #t[8] >= 2 then local speed = tonumber(string.sub(t[8], 1, 1)) local weight = tonumber(string.sub(t[8], 2, 2)) stats = stat_t(speed, weight) end end table.insert( scoreTable, score_t( tonumber(t[1]), t[2], t[3], t[4], tonumber(t[5]), spl, flags, stats ) ) setScoreTable(tonumber(t[1]), flags, scoreTable) end sortScores() f:close() else print("Failed to open file: ", FILENAME) end function allowJoin(v) if not cv_interrupt.value then local y if v then y = "yes" hud.enable("freeplay") else y = "no" hud.disable("freeplay") end COM_BufInsertText(server, "allowteamchange " + y) end end -- Returns true if there is a single player ingame local function singleplayer() local n = 0 for p in players.iterate do if p.valid and not p.spectator then n = $ + 1 if n > 1 then return false end end end return true end local function initLeaderboard(player) if disable and leveltime < START_TIME then disable = not singleplayer() else disable = disable or not singleplayer() end disable = $ or not cv_enable.value or not (maptol & (TOL_SP | TOL_RACE)) -- Restore encore mode to initial value if disable and EncoreInitial != nil then COM_BufInsertText(server, string.format("kartencore %d", EncoreInitial)) EncoreInitial = nil end player.afkTime = leveltime end addHook("PlayerSpawn", initLeaderboard) local function doyoudare(player) if not singleplayer() 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 zoneAct(map) local z = "" if map.zonttl != "" then z = " " + map.zonttl elseif not(map.levelflags & LF_NOZONE) then z = " Zone" end if map.actnum != "" then z = $ + " " + map.actnum end return z end local function findMap(player, ...) local search = ... local hell = "\x85HELL" local tol = { [TOL_SP] = "\x81Race\x80", -- Nuked race maps [TOL_COOP] = "\x8D\Battle\x80", -- Nuked battle maps [TOL_RACE] = "\x88Race\x80", [TOL_MATCH] = "\x87\Battle\x80" } local lvltype, map, lvlttl for i = 1, #mapheaderinfo do map = mapheaderinfo[i] if map == nil then continue end lvlttl = map.lvlttl + zoneAct(map) if not search or lvlttl:lower():find(search:lower()) then -- Only care for up to TOL_MATCH (0x10) lvltype = tol[map.typeoflevel & 0x1F] or map.typeoflevel -- If not battle print numlaps lvltype = (map.typeoflevel & (TOL_MATCH | TOL_COOP) and lvltype) or string.format("%s \x82%-2d\x80", lvltype, map.numlaps) CONS_Printf( player, string.format( "%s %-9s %-30s - %s\t%s", G_BuildMapName(i), lvltype, lvlttl, map.subttl, (map.menuflags & LF2_HIDEINMENU and hell) or "" ) ) end end end COM_AddCommand("findmap", findMap) local function mapnumFromExtended(map) local p, q = map:upper():match("MAP(%w)(%w)$", 1) if not (p and q) then return nil end local mapnum = 0 local A = string.byte("A") if tonumber(p) != nil then -- Non extended map numbers if tonumber(q) == nil then return nil end mapnum = tonumber(p) * 10 + tonumber(q) else --Extended map numbers p = string.byte(p) - A local qn = tonumber(q) if qn == nil then qn = string.byte(q) - A + 10 end mapnum = 36 * p + qn + 100 end return mapnum end local SPBModeSym = { [F_SPBEXP] = "X", [F_SPBBIG] = "B", [F_SPBJUS] = "J", } local function modeToString(mode) local modestr = "Time Attack" if mode & F_SPBATK then modestr = "SPB" for k, v in pairs(SPBModeSym) do if mode & k then modestr = $ + v end end end return modestr end local function records(player, ...) local mapid = ... local mapnum = gamemap if mapid then mapnum = mapnumFromExtended(mapid) if not mapnum then CONS_Printf(player, string.format("Invalid map name: %s", mapid)) return end end local map = mapheaderinfo[mapnum] if map then CONS_Printf(player, string.format( "\x83%s%8s", map.lvlttl, (map.menuflags & LF2_HIDEINMENU and "\x85HELL") or "" ) ) local zoneact = zoneAct(map) -- print the zone/act on the right hand size under the title CONS_Printf( player, string.format( string.format("\x83%%%ds%%s\x80 - \x88%%s", #map.lvlttl - #zoneact / 2 - 1), " ", zoneAct(map), map.subttl ) ) else CONS_Printf(player, "\x85UNKNOWN MAP") end for mode, maps in pairs(lb) do local maptbl = maps[mapnum] if not maptbl then continue end CONS_Printf(player, "") CONS_Printf(player, modeToString(mode)) -- don't print flags for time attack if mode then for i, tbl in ipairs(maptbl) do CONS_Printf( player, string.format( "%2d %-21s \x89%8s \x80%s", i, tbl["name"], ticsToTime(tbl["time"]), modeToString(tbl["flags"]) ) ) end else for i, tbl in ipairs(maptbl) do CONS_Printf( player, string.format( "%2d %-21s \x89%8s", i, tbl["name"], ticsToTime(tbl["time"]) ) ) end end end end COM_AddCommand("records", records) 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 mapnum = mapnumFromExtended(map) if not mapnum then CONS_Printf(player, string.format("Invalid map name: %s", map)) end if mapheaderinfo[mapnum] == nil then CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper())) return end -- Verify valid race level if not (mapheaderinfo[mapnum].typeoflevel & (TOL_SP | TOL_RACE)) then CONS_Printf(player, "Battle maps are not supported") return end nextMap = G_BuildMapName(mapnum) end COM_AddCommand("changelevel", changelevel) local function toggleEncore(player) if not doyoudare(player) then return end local enc = CV_FindVar("kartencore") if EncoreInitial == nil then EncoreInitial = enc.value end if enc.value then COM_BufInsertText(server, "kartencore off") else COM_BufInsertText(server, "kartencore on") end end COM_AddCommand("encore", toggleEncore) 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) local function scrollGUI(player, ...) if not doyoudare(player) then return end if drawState == DS_DEFAULT then scroll_to(player) else drawState = DS_DEFAULT end end COM_AddCommand("scroll", scrollGUI) local function findRival(player, ...) local rival, page = ... page = (tonumber(page) or 1) - 1 if rival == nil then CONS_Printf(player, "Print the times of your rival.\nUsage: rival ") return end local colors = { [1] = "\x85", [0] = "\x89", [-1] = "\x88" } local sym = { [true] = "-", [false] = "", } local scores = {} local totalScores = 0 local totalDiff = 0 CONS_Printf(player, string.format("\x89%s's times:", rival)) CONS_Printf(player, "MAP Time Diff Mode") for mode, tbl in pairs(lb) do scores[mode] = {} for map, scoreTable in pairs(tbl) do local rivalScore = nil local yourScore = nil for _, score in pairs(scoreTable) do if score["name"] == player.name then yourScore = score elseif score["name"] == rival then rivalScore = score end if rivalScore and yourScore then break end end if rivalScore and yourScore then totalDiff = totalDiff + yourScore["time"] - rivalScore["time"] end if rivalScore then totalScores = totalScores + 1 table.insert( scores[mode], { ["rival"] = rivalScore, ["your"] = yourScore } ) end end end local i = 0 local stop = 19 local o = page * stop local function sortf(a, b) return a["rival"]["map"] < b["rival"]["map"] end for mode, tbl in pairs(scores) do if i >= stop then break end table.sort(tbl, sortf) for _, score in ipairs(tbl) do if o then o = o - 1 continue end if i >= stop then break end i = i + 1 local modestr = modeToString(score["rival"]["flags"]) if score["your"] then local diff = score["your"]["time"] - score["rival"]["time"] local color = colors[clamp(-1, diff, 1)] CONS_Printf( player, string.format( "%s %8s %s%9s \x80%s", G_BuildMapName(score["rival"]["map"]), ticsToTime(score["rival"]["time"]), color, sym[diff<0] + ticsToTime(abs(diff)), modestr ) ) else CONS_Printf( player, string.format( "%s %8s %9s %s", G_BuildMapName(score["rival"]["map"]), ticsToTime(score["rival"]["time"]), ticsToTime(0, true), modestr ) ) end end end CONS_Printf( player, string.format( "Your score = %s%s%s", colors[clamp(-1, totalDiff, 1)], sym[totalDiff<0], ticsToTime(abs(totalDiff)) ) ) CONS_Printf( player, string.format( "Page %d out of %d", page + 1, totalScores / stop + 1 ) ) end COM_AddCommand("rival", findRival) --DEBUGGING --local function printTable(tb) -- for mode, tbl in pairs(tb) do -- for map, scoreTable in pairs(tbl) do -- print(string.format("[%d][%d] #%d", mode, map, #scoreTable)) -- -- --if scoreTable != 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 drawState = DS_DEFAULT scrollY = 50 * FRACUNIT scrollAcc = 0 FlashTics = 0 allowJoin(true) --printTable(lb) end ) function ticsToTime(tics, pure) if tics == 0 and pure then return "-:--:--" end return string.format( "%d:%02d:%02d", G_TicsToMinutes(tics, true), 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 modePatches = { [F_SPBATK] = "SPB", [F_SPBJUS] = "HYUD", [F_SPBBIG] = "BIG", [F_SPBEXP] = "INV" } local function modePatch(flag) if flag == F_SPBEXP then return PATCH[modePatches[flag]][(leveltime / 4) % 6] end return PATCH[modePatches[flag]] end local cursors = { [1] = ". ", [2] = " ." } local function marquee(text, maxwidth) if #text <= maxwidth then return text end local shift = 16 -- Creates an index range ranging from -shift to #text + shift local pos = ((leveltime / 16) % (#text - maxwidth + shift * 2)) + 1 - shift local cursor = "" if pos < #text - maxwidth + 1 then cursor = cursors[((leveltime / 11) % #cursors) + 1] end -- The pos is the index going from -shift to #text + shift -- It's clamped within the text boundaries ie. -- 0 < pos < #text - maxwidth pos = min(max(pos, 1), #text - maxwidth + 1) return text:sub(pos, pos + maxwidth - 1) + cursor end -- Bats on ... local bodium = {V_YELLOWMAP, V_GRAYMAP, V_BROWNMAP, 0} local splitColor = { [-1] = V_SKYMAP, [0] = V_PURPLEMAP, [1] = V_REDMAP } local splitSymbol = { [-1] = "-", [0] = "", [1] = "+" } local showSplit = 0 local VFLAGS = V_SNAPTOLEFT local FACERANK_DIM = 16 local FACERANK_SPC = FACERANK_DIM + 4 local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, textVFlags) textVFlags = textVFlags or V_HUDTRANSHALF local me = player.name == score["name"] --draw Patch/chili v.draw(x, y, faceRank, V_HUDTRANS | VFLAGS, v.getColormap("sonic", score["color"])) if me then v.draw(x, y, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS) end -- Encore if score["flags"] & F_ENCORE then local bob = sin((leveltime + i * 5) * (ANG10)) v.drawScaled( x * FRACUNIT, bob + (y + FACERANK_DIM / 2) * FRACUNIT, FRACUNIT / 6, PATCH["RUBY"], V_HUDTRANS | VFLAGS ) end -- SPB if score["flags"] & F_SPBATK then local scale = FRACUNIT / 4 drawitem( v, x - 2, y - 2, scale, modePatch(F_SPBATK), V_HUDTRANS | VFLAGS ) if score["flags"] & F_SPBEXP then drawitem( v, x + FACERANK_DIM - 4, y - 2, scale, modePatch(F_SPBEXP), V_HUDTRANS | VFLAGS ) end if score["flags"] & F_SPBBIG then drawitem( v, x - 2, y + FACERANK_DIM - 4, scale, modePatch(F_SPBBIG), V_HUDTRANS | VFLAGS ) end if score["flags"] & F_SPBJUS then drawitem( v, x + FACERANK_DIM - 4, y + FACERANK_DIM - 4, scale, modePatch(F_SPBJUS), V_HUDTRANS | VFLAGS ) end end -- Position if drawPos then v.drawNum(x, y + 3, pos, textVFlags | VFLAGS) end -- Stats local stat = score["stat"] local pskin = score["skin"] and skins[score["skin"]] if stat and not ( pskin and pskin.kartweight == stat["weight"] and pskin.kartspeed == stat["speed"] ) then v.drawString(x + FACERANK_DIM - 2, y + 4, stat["speed"], V_HUDTRANS | VFLAGS, "small") v.drawString(x + FACERANK_DIM - 2, y + 8, stat["weight"], V_HUDTRANS | VFLAGS, "small") end if gui == GUI_ON or (gui == GUI_SPLITS and showSplit) then local name = score["name"] -- Shorten long names local stralign = "left" local MAXWIDTH = 70 local px = 2 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 local flashV = 0 if me and FlashTics > leveltime then flashV = FlashVFlags[leveltime / FlashRate % (#FlashVFlags + 1)] end v.drawString( x + FACERANK_DIM + px, y + py, name, textVFlags | V_ALLOWLOWERCASE | VFLAGS | flashV, stralign ) -- Draw splits if showSplit and score["splits"] and score["splits"][prevLap] != nil then local split = splits[prevLap] - score["splits"][prevLap] v.drawString( x + px + FACERANK_DIM, y + 8, splitSymbol[clamp(-1, split, 1)] + ticsToTime(abs(split)), textVFlags | splitColor[clamp(-1, split, 1)] | VFLAGS ) else v.drawString( x + px + FACERANK_DIM, y + 8, ticsToTime(score["time"], true), textVFlags | bodium[min(pos, 4)] | VFLAGS | flashV ) end end end local function drawDefault(v, player, scoreTable, gui) local yoffset = (200 / 4) + 4 local x = 4 -- Draw placeholder score if scoreTable == nil then drawScore(v, player, 1, x, y, gui, PATCH["NORANK"], {["name"] = UNCLAIMED, ["time"] = 0, ["flags"] = 0}) else for pos, score in ipairs(scoreTable) do if pos > 5 then break end local faceRank = PATCH["FACERANK"][score.skin] or PATCH["NORANK"] local y = yoffset + (FACERANK_SPC) * (pos - 1) drawScore( v, player, pos, x, y, gui, faceRank, score ) end end end local function drawScroll(v, player, scoreTable, gui) if scoreTable then scrollY = scrollY + FixedMul(1 * FRACUNIT, scrollAcc) local minim = -((#scoreTable - 1) * FACERANK_SPC * FRACUNIT) local maxim = (200 - FACERANK_DIM) * FRACUNIT scrollY = clamp(minim, scrollY, maxim) -- Bounceback if scrollY == minim or scrollY == maxim then scrollAcc = -FixedMul(scrollAcc, FRACUNIT / 3) end local x = 10 if #scoreTable >= 10 then x = x + 8 if #scoreTable >= 100 then x = x + 8 end end local y = FixedInt(scrollY) for pos, score in ipairs(scoreTable) do local faceRank = PATCH["FACERANK"][score.skin] or PATCH["NORANK"] drawScore( v, player, pos, x, y + ((pos - 1) * FACERANK_SPC), gui, faceRank, score, true, V_HUDTRANS ) end end end local function drawAuto(v, player, scoreTable, gui) end local scrollToPos = nil local function drawScrollTo(v, player, scoreTable, gui) drawState = DS_SCROLL if scrollToPos == nil then return end scrollY = (-(scrollToPos * FACERANK_SPC) + (100 - FACERANK_SPC / 2)) * FRACUNIT scrollToPos = nil drawScroll(v, player, scoreTable, gui) end local stateFunctions = { [DS_DEFAULT] = drawDefault, [DS_SCROLL] = drawScroll, [DS_AUTO] = drawAuto, [DS_SCRLTO] = drawScrollTo } -- Draw mode and return pos + 1 if success local function drawMode(v, pos, flag) if not (Flags & flag) then return pos end drawitem(v, pos * 6 + 1, 194, FRACUNIT / 4, modePatch(flag), V_SNAPTOBOTTOM | V_SNAPTOLEFT) return pos + 1 end local function drawScoreboard(v, player) if disable then return end if player != displayplayers[0] then return end cachePatches(v) local gui = cv_gui.value if leveltime < START_TIME or player.exiting or player.lives == 0 then gui = GUI_ON end if gui then stateFunctions[drawState](v, player, scoreTable, gui) end local pos = 0 -- Draw current active modes bottom left pos = drawMode(v, pos, F_SPBJUS) pos = drawMode(v, pos, F_SPBBIG) pos = drawMode(v, pos, F_SPBEXP) 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["NORANK"] = v.cachePatch("M_NORANK") 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") PATCH["RUBY"] = v.cachePatch("RUBYICON") end end -- Find location of player and scroll to it function scroll_to(player) local m = scoreTable or {} scrollToPos = 2 for pos, score in ipairs(m) do if player.name == score["name"] then scrollToPos = max(2, pos - 1) break end end drawState = DS_SCRLTO end -- Write skin stats to each score where there are none local function writeStats() for _, t in pairs(lb) do for _, scoreTable in pairs(t) do for _, score in ipairs(scoreTable) do local skin = skins[score["skin"]] if skin and not score["stat"] then local stats = stat_t(skin.kartspeed, skin.kartweight) score["stat"] = stats end end end end end local function saveTime(player) scoreTable = $ or {} local pskin = skins[player.mo.skin] local newscore = score_t( gamemap, player.name, player.mo.skin, player.skincolor, timeFinished, splits, Flags, stat_t(player.HMRs or pskin.kartspeed, player.HMRw or pskin.kartweight) ) -- Check if you beat your previous best for i = 1, #scoreTable do if scoreTable[i]["name"] == player.name then if lbComp(newscore, scoreTable[i]) then table.remove(scoreTable, i) S_StartSound(nil, 130) FlashTics = leveltime + TICRATE * 3 FlashRate = 1 FlashVFlags = YellowFlash break else -- You suck lol S_StartSound(nil, 201) FlashTics = leveltime + TICRATE * 3 FlashRate = 3 FlashVFlags = RedFlash scroll_to(player) return end end end print("Saving score") table.insert( scoreTable, newscore ) table.sort(scoreTable, lbComp) while #scoreTable > cv_saves.value do table.remove(scoreTable) end scroll_to(player) setScoreTable(gamemap, Flags, scoreTable) if not StatTrack then writeStats() StatTrack = true end local f = assert(io.open(FILENAME, "w")) if f == nil then print("Failed to open file for writing: " + FILENAME) return end for _, tbl in pairs(lb) do for _, scoreTable in pairs(tbl) do for _, score in ipairs(scoreTable) do f:write( score["map"], "\t", score["name"], "\t", score["skin"], "\t", score["color"], "\t", score["time"], "\t", table.concat(score["splits"], " "), "\t", score["flags"], "\t", stat_str(score["stat"]), "\n" ) end end end f:close() end -- DEBUGGING --local function saveLeaderboard(player, ...) -- timeFinished = tonumber(... or 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 changeMap() COM_BufInsertText(server, "map " + nextMap + " -force -gametype race") nextMap = nil end local function think() if nextMap then changeMap() end if disable then if AntiAFK then if not singleplayer() then for p in players.iterate do if p.valid and not p.spectator and not p.exiting and p.lives > 0 then if p.cmd.buttons then p.afkTime = leveltime end --Away from kart if p.afkTime + AFK_BALANCE_WARN == leveltime then chatprintf(p, "[AFK] \x89You will be moved to spectator in 10 seconds!", false) S_StartSound(nil, 26, p) end if p.afkTime + 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.afkTime = leveltime end end end end help = true return end showSplit = max(0, showSplit - 1) local p = getGamer() if leveltime < START_TIME then -- Help message if leveltime == START_TIME - TICRATE * 3 then if singleplayer() then if help then help = false chatprint(HELP_MESSAGE, true) end else help = true end end -- Autospec -- Encore if leveltime == 1 then Flags = $ & !F_ENCORE if encoremode then Flags = $ | F_ENCORE end 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 if not cv_spbatk then cv_spbatk = CV_FindVar("spbatk") end -- Gamemode flags Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS) if server.SPBArunning and cv_spbatk.value and leveltime > START_TIME - (3 * TICRATE) / 2 then Flags = $ | F_SPBATK if server.SPBAexpert then Flags = $ | F_SPBEXP end if clearcheats then clearcheats = false for q in players.iterate do q.SPBAKARTBIG = false q.SPBAjustice = false q.SPBAshutup = false end end if p then if p.SPBAKARTBIG then Flags = $ | F_SPBBIG end if p.SPBAjustice then Flags = $ | F_SPBJUS end end -- make sure the spb actually spawned if leveltime == START_TIME - 1 then if not (server.SPBAbomb and server.SPBAbomb.valid) then -- it didn't spawn, clear spb flags Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS) end end else hud.enable("freeplay") end end scoreTable = getScoreTable(gamemap, Flags) if not cv_teamchange then cv_teamchange = CV_FindVar("allowteamchange") end if p then -- Scroll controller -- Spectators can't input buttons so let the gamer do it if drawState == DS_SCROLL then if p.cmd.buttons & BT_BACKWARD then scrollAcc = scrollAcc - FRACUNIT / 3 elseif p.cmd.buttons & BT_FORWARD then scrollAcc = scrollAcc + FRACUNIT / 3 else scrollAcc = FixedMul(scrollAcc, (FRACUNIT * 90) / 100) if scrollAcc < FRACUNIT and scrollAcc > -FRACUNIT then scrollAcc = 0 end end end if p.lives == 0 then drawState = DS_SCROLL end if p.cmd.buttons then p.afkTime = leveltime end if not replayplayback then if leveltime > PREVENT_JOIN_TIME and p.afkTime + AFK_TIMEOUT > leveltime then if cv_teamchange.value then allowJoin(false) end elseif p.afkTime + AFK_TIMEOUT < leveltime then if not cv_teamchange.value then allowJoin(true) end 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 changeMap() end if not cv_teamchange then cv_teamchange = CV_FindVar("allowteamchange") end if not cv_teamchange.value then allowJoin(true) end end addHook("IntermissionThinker", interThink) addHook("VoteThinker", interThink) -- Returns the values clamed between min, max function clamp(min_v, v, max_v) return max(min_v, min(v, max_v)) end local function netvars(net) Flags = net($) splits = net($) prevLap = net($) drawState = net($) StatTrack = net($) EncoreInitial = net($) lb = net($) end addHook("NetVars", netvars)