From 647e4a02990169122b68f4dbfb33ffa5dca0bff4 Mon Sep 17 00:00:00 2001 From: Not Date: Mon, 28 Mar 2022 13:15:29 +0200 Subject: [PATCH] v1.2.17 --- leaderboard.lua | 655 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 497 insertions(+), 158 deletions(-) diff --git a/leaderboard.lua b/leaderboard.lua index 2908dec..13d1b1a 100644 --- a/leaderboard.lua +++ b/leaderboard.lua @@ -1,15 +1,20 @@ -- Leaderboards written by Not -- Reusable -local FILENAME = "leaderboard.txt" +-- 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 UNCLAIMED = "Unclaimed Record" +local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll" +local FILENAME = "leaderboard.txt" -- Retry / changelevel map local nextMap = nil @@ -33,9 +38,31 @@ local GUI_OFF = 0x0 local GUI_SPLITS = 0x1 local GUI_ON = 0x2 --- patch caching function +-- 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 cv_gui = CV_RegisterVar({ name = "lb_gui", defaultvalue = GUI_ON, @@ -79,17 +106,25 @@ local cv_interrupt = CV_RegisterVar({ end }) - -local function lbID(map, flags) - local id = tostring(map) - if flags & F_SPBATK then - id = id + "S" - end - - return id +local function setScoreTable(map, flags, scoreTable) + local mode = flags & F_SPBATK + lb[mode] = lb[mode] or {} + lb[mode][map] = scoreTable end -local function score_t(map, name, skin, color, time, splits, flags) +local function getScoreTable(map, flags) + --local id = tostring(map) + --if flags & F_SPBATK then + -- id = id + "S" + --end + + --return id + + local mode = flags & F_SPBATK + return lb[mode] and lb[mode][map] or {} +end + +local function score_t(map, name, skin, color, time, splits, flags, restat) return { ["map"] = map, ["name"] = name, @@ -97,16 +132,35 @@ local function score_t(map, name, skin, color, time, splits, flags) ["color"] = color, ["time"] = time, ["splits"] = splits, - ["flags"] = flags + ["flags"] = flags, + ["restat"] = restat } end +local function restat_t(speed, weight) + if speed and weight then + return { + ["speed"] = speed, + ["weight"] = weight + } + end + return nil +end + +local function restat_str(restat) + if restat then + return string.format("%d%d", restat["speed"], restat["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 - -- name, skin, color, time, splits, flags + -- mapnum, name, skin, color, time, splits, flags, restat local t = {} for word in (l+"\t"):gmatch("(.-)\t") do table.insert(t, word) @@ -117,10 +171,7 @@ if f then flags = tonumber(t[7]) end - local lbt = lb[lbID(t[1], flags)] - if lbt == nil then - lbt = {} - end + local scoreTable = getScoreTable(tonumber(t[1]), flags) local spl = {} if t[6] != nil then @@ -129,20 +180,31 @@ if f then end end + local stats = nil + if t[8] != nil then + if #t[8] >= 2 then + local speed = string.sub(t[8], 1, 1) + local weight = string.sub(t[8], 2, 2) + stats = restat_t(speed, weight) + end + end + table.insert( - lbt, + scoreTable, score_t( - t[1], + tonumber(t[1]), t[2], t[3], t[4], tonumber(t[5]), spl, - flags + flags, + stats ) ) - lb[lbID(t[1], flags)] = lbt + setScoreTable(tonumber(t[1]), flags, scoreTable) end + f:close() else print("Failed to open file: ", FILENAME) @@ -298,23 +360,173 @@ local function clearcheats(player) 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) + + local spb = mode & F_SPBATK and "SPB" or "TA" + + for _, score in ipairs(tbl) do + if o then + o = o - 1 + continue + end + if i >= stop then break end + i = i + 1 + + 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)), + spb + ) + ) + else + CONS_Printf( + player, + string.format( + "%s %8s %9s %s", + G_BuildMapName(score["rival"]["map"]), + ticsToTime(score["rival"]["time"]), + ticsToTime(0, true), + spb + ) + ) + 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 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 +-- 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 @@ -323,14 +535,17 @@ addHook("MapLoad", function() timeFinished = 0 splits = {} prevLap = 0 + drawState = DS_DEFAULT + scrollY = 50 * FRACUNIT + scrollAcc = 0 allowJoin(true) --printTable(lb) end ) -local function ticsToTime(tics) - if tics == 0 then +function ticsToTime(tics, pure) + if tics == 0 and pure then return "-:--:--" end @@ -384,30 +599,93 @@ end -- Bats on ... local bodium = {V_YELLOWMAP, V_GRAYMAP, V_BROWNMAP, 0} -local splitColor = {[true]=V_SKYMAP, [false]=V_REDMAP} -local splitSymbol = {[true]="-", [false]="+"} +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 function drawScore(v, player, i, gui, faceRank, color, name, time, sSplits, flags) +local FACERANK_SPC = FACERANK_DIM + 4 +local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, textVFlags) + textVFlags = textVFlags or V_HUDTRANSHALF --draw Patch/chili - -- | OFFSET | + | PADDING | * |INDEX| - local h = ((200 / 4) + 4) + (FACERANK_DIM + 4) * (i - 1) - - v.draw(4, h, faceRank, V_HUDTRANS | VFLAGS, v.getColormap("sonic", color)) - if player.name == name then - v.draw(4, h, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS) + v.draw(x, y, faceRank, V_HUDTRANS | VFLAGS, v.getColormap("sonic", score["color"])) + if player.name == score["name"] then + v.draw(x, y, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS) end + + -- SPB + if score["flags"] & F_SPBATK then + local scale = FRACUNIT / 4 + drawitem( + v, + x - 2, + y - 2, + scale, + PATCH["SPB"], + V_HUDTRANS | VFLAGS + ) + if score["flags"] & F_SPBEXP then + drawitem( + v, + x + FACERANK_DIM - 4, + y - 2, + scale, + PATCH["INV"][(leveltime / 4) % 6], + V_HUDTRANS | VFLAGS + ) + end + if score["flags"] & F_SPBBIG then + drawitem( + v, + x - 2, + y + FACERANK_DIM - 4, + scale, + PATCH["BIG"], + V_HUDTRANS | VFLAGS + ) + end + if score["flags"] & F_SPBJUS then + drawitem( + v, + x + FACERANK_DIM - 4, + y + FACERANK_DIM - 4, + scale, + PATCH["HYUD"], + V_HUDTRANS | VFLAGS + ) + end + end + + -- Position + if drawPos then + v.drawNum(x, y + 3, pos, textVFlags | VFLAGS) + end + + -- Restats + local restat = score["restat"] + if restat then + v.drawString(x + FACERANK_DIM - 2, y + 4, restat["speed"], V_HUDTRANS | VFLAGS, "small") + v.drawString(x + FACERANK_DIM - 2, y + 8, restat["weight"], V_HUDTRANS | VFLAGS, "small") + end + - --draw icons - --draw name 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 = 6 + local px = 2 local py = 0 if v.stringWidth(name) > MAXWIDTH then stralign = "thin" @@ -421,87 +699,122 @@ local function drawScore(v, player, i, gui, faceRank, color, name, time, sSplits end end - -- SPB - if flags & F_SPBATK then - local scale = FRACUNIT / 4 - drawitem( - v, - 4 - 2, - h - 2, - scale, - PATCH["SPB"], - V_HUDTRANS | VFLAGS - ) - if flags & F_SPBEXP then - drawitem( - v, - FACERANK_DIM, - h - 2, - scale, - PATCH["INV"][(leveltime / 4) % 6], - V_HUDTRANS | VFLAGS - ) - end - if flags & F_SPBBIG then - drawitem( - v, - 4 - 2, - h + FACERANK_DIM - 4, - scale, - PATCH["BIG"], - V_HUDTRANS | VFLAGS - ) - end - if flags & F_SPBJUS then - drawitem( - v, - FACERANK_DIM, - h + FACERANK_DIM - 4, - scale, - PATCH["HYUD"], - V_HUDTRANS | VFLAGS - ) - end - end - v.drawString( - px + FACERANK_DIM, - h + py, + x + FACERANK_DIM + px, + y + py, name, - V_HUDTRANSHALF | V_ALLOWLOWERCASE | VFLAGS, + textVFlags | V_ALLOWLOWERCASE | VFLAGS, stralign ) -- Draw splits - if showSplit and sSplits and sSplits[prevLap] != nil then - local split = splits[prevLap] - sSplits[prevLap] + if showSplit and score["splits"] and score["splits"][prevLap] != nil then + local split = splits[prevLap] - score["splits"][prevLap] v.drawString( - px + FACERANK_DIM, - h + 8, - splitSymbol[split < 0] + ticsToTime(abs(split)), - V_HUDTRANSHALF | splitColor[split < 0] | VFLAGS + x + px + FACERANK_DIM, + y + 8, + splitSymbol[clamp(-1, split, 1)] + ticsToTime(abs(split)), + textVFlags | splitColor[clamp(-1, split, 1)] | VFLAGS ) else v.drawString( - px + FACERANK_DIM, - h + 8, - ticsToTime(time), - V_HUDTRANSHALF | bodium[min(i, 4)] | VFLAGS + x + px + FACERANK_DIM, + y + 8, + ticsToTime(score["time"], true), + textVFlags | bodium[min(pos, 4)] | VFLAGS ) 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 + end + if #scoreTable > 100 then + x = x + 8 + 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 +} + 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 + local scoreTable = getScoreTable(gamemap, Flags) local gui = cv_gui.value if leveltime < START_TIME or player.exiting or player.lives == 0 then @@ -509,24 +822,7 @@ local function drawScoreboard(v, player) end if gui then - -- Draw placeholder score - if m == nil then - drawScore(v, player, 1, gui, PATCH["NORANK"], nil, UNCLAIMED, 0, nil, 0) - else - for i, score in ipairs(m) do - if i > 5 then - break - end - - local name = score["name"] - local skin = skins[score["skin"]] - if skin == nil then - skin = skins["sonic"] - end - local faceRank = PATCH["FACERANK"][skin.name] - drawScore(v, player, i, gui, faceRank, score["color"], score["name"], score["time"], score["splits"], score["flags"]) - end - end + stateFunctions[drawState](v, player, scoreTable, gui) end end hud.add(drawScoreboard, "game") @@ -567,11 +863,24 @@ local function lbComp(a, b) 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 = {} +-- Find location of player and scroll to it +function scroll_to(player) + local m = getScoreTable(gamemap, Flags) + + 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 + +local function saveTime(player) + local scoreTable = getScoreTable(gamemap, Flags) + local newscore = score_t( gamemap, player.name, @@ -579,19 +888,21 @@ local function saveTime(player) player.skincolor, timeFinished, splits, - Flags + Flags, + restat_t(player.HMRs, player.HMRw) ) -- 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) + 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) break else -- You suck lol S_StartSound(nil, 201) + scroll_to(player) return end end @@ -599,16 +910,18 @@ local function saveTime(player) print("Saving score") table.insert( - m, + scoreTable, newscore ) - table.sort(m, lbComp) - while #m > cv_saves.value do - table.remove(m) + table.sort(scoreTable, lbComp) + while #scoreTable > cv_saves.value do + table.remove(scoreTable) end - lb[lbID(gamemap, Flags)] = m + scroll_to(player) + + setScoreTable(gamemap, Flags, scoreTable) local f = assert(io.open(FILENAME, "w")) if f == nil then @@ -616,18 +929,20 @@ local function saveTime(player) 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" - ) + 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", + restat_str(score["restat"]), "\n" + ) + end end end @@ -688,9 +1003,8 @@ local function think() help = true return end - if showSplit > 0 then - showSplit = showSplit - 1 - end + + showSplit = max(0, showSplit - 1) local p = getGamer() if leveltime < START_TIME then @@ -699,7 +1013,7 @@ local function think() if ingame() == 1 then if help then help = false - chatprint("\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui", true) + chatprint(HELP_MESSAGE, true) end else help = true @@ -750,6 +1064,25 @@ local function think() local cv_teamchange = CV_FindVar("allowteamchange") 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 @@ -786,9 +1119,15 @@ local function interThink() end addHook("IntermissionThinker", 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) lb = net($) splits = net($) prevLap = net($) + drawState = net($) end addHook("NetVars", netvars)