From 3a61ffcabd96a9e81c248d39734a57c4ac74aabc Mon Sep 17 00:00:00 2001 From: Not Date: Fri, 7 Oct 2022 01:35:24 +0200 Subject: [PATCH] allow non netvar stored, cold record loading from lua --- browser.lua | 44 ++++---- lb_common.lua | 20 ++++ lb_store.lua | 218 ++++++++++++++++++++++++++++++++++++++ leaderboard.lua | 274 ++++++++++-------------------------------------- 4 files changed, 319 insertions(+), 237 deletions(-) create mode 100644 lb_store.lua diff --git a/browser.lua b/browser.lua index b493e81..c2e1d02 100644 --- a/browser.lua +++ b/browser.lua @@ -1,15 +1,21 @@ -local leaderboard = nil +local MapRecords local maps local mapIndex = 1 local scrollPos = 1 local modes = nil local mode = 1 local prefMode = nil +local ModeSep + +---- Imported functions ---- --- Imported functions ------- -- lb_common.lua local ZoneAct = lb_ZoneAct local TicsToTime = lb_TicsToTime + +-- lb_store.lua +local GetMapRecords = lb_get_map_records + ----------------------------- local cv_kartencore @@ -25,10 +31,8 @@ end local function updateModes() -- set available modes for this map modes = {} - for mode, scoreTable in pairs(leaderboard) do - if scoreTable[getMap()] then - table.insert(modes, mode) - end + for mode, _ in pairs(MapRecords) do + table.insert(modes, mode) end table.sort(modes) @@ -46,6 +50,8 @@ local function updateMapIndex(n) mapIndex = mapIndexOffset(n) scrollPos = 1 + MapRecords = GetMapRecords(maps[mapIndex], ModeSep) + updateModes() end @@ -337,7 +343,7 @@ local function drawScore(v, i, pos, score, highlight) end local function drawBrowser(v, player) - if not leaderboard then return end + if not MapRecords then return end v.fadeScreen(0xFF00, 16) @@ -358,23 +364,20 @@ local function drawBrowser(v, player) if not modes then return end - local gamemode = leaderboard[modes[mode]] - if not gamemode then return end + local records = MapRecords[modes[mode]] + if not records then return end - local scoreTable = gamemode[getMap()] - if not scoreTable then return end - - local scores = #scoreTable - scrollPos = max(min(scrollPos, scores - 3), 1) - local endi = min(scrollPos + 7, scores) + local record_count = #records + scrollPos = max(min(scrollPos, record_count - 3), 1) + local endi = min(scrollPos + 7, record_count) for i = scrollPos, endi do - drawScore(v, i - scrollPos + 1, i, scoreTable[i], scoreTable[i].name == player.name) + drawScore(v, i - scrollPos + 1, i, records[i], records[i].name == player.name) end end rawset(_G, "DrawBrowser", drawBrowser) -local function initBrowser(lb) - leaderboard = lb +local function initBrowser(modeSep) + ModeSep = modeSep -- set mapIndex to current map for i, m in ipairs(maps) do @@ -384,6 +387,9 @@ local function initBrowser(lb) end end + -- initialize MapRecords + MapRecords = GetMapRecords(gamemap, ModeSep) + scrollPos = 1 updateModes() end @@ -485,6 +491,6 @@ local function netvars(net) mode = net($) prefMode = net($) scrollPos = net($) - leaderboard = net($) + MapRecords = net($) end addHook("NetVars", netvars) diff --git a/lb_common.lua b/lb_common.lua index ac7f479..0557e81 100644 --- a/lb_common.lua +++ b/lb_common.lua @@ -24,3 +24,23 @@ rawset(_G, "lb_ZoneAct", function(map) return z end) + +rawset(_G, "lb_stat_t", function(speed, weight) + if speed and weight then + return (speed << 4) | weight + end + return 0 +end) + + +local F_SPBBIG = 0x4 +local F_SPBEXP = 0x8 +-- True if a is better than b +rawset(_G, "lb_comp", function(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) diff --git a/lb_store.lua b/lb_store.lua new file mode 100644 index 0000000..d8684b5 --- /dev/null +++ b/lb_store.lua @@ -0,0 +1,218 @@ +-- This file handles the storage and related netvars of the leaderboard + +---- Imported functions ---- + +-- lb_common.lua +local stat_t = lb_stat_t +local lbComp = lb_comp + +---------------------------- + +local LEADERBOARD_FILE = "leaderboard.txt" + +-- ColdStore are records loaded from lua addons +-- this table should never be modified outside of the AddColdStore function +local ColdStore = {} + +-- Livestore are new records nad records loaded from leaderboard.txt file +local LiveStore = {} + + +-- GLOBAL +-- Function for adding records from lua +local function AddColdStore(record) + ColdStore[record.map] = $ or {} + table.insert(ColdStore[record.map], record) +end +rawset(_G, "lb_add_coldstore_record", AddColdStore) + + +-- Insert mode separated records from the flat sourceTable into dest +local function insertRecords(dest, sourceTable, modeSep) + if not sourceTable then return end + + local mode = nil + for _, record in ipairs(sourceTable) do + mode = record.flags & modeSep + dest[mode] = $ or {} + table.insert(dest[mode], record) + end +end + +-- GLOBAL +-- Construct the leaderboard table of the supplied mapid +-- combines the ColdStore and LiveStore records +local function GetMapRecords(map, modeSep) + local mapRecords = {} + + -- Insert ColdStore records + insertRecords(mapRecords, ColdStore[map], modeSep) + + -- Insert LiveStore records + insertRecords(mapRecords, LiveStore[map], modeSep) + + -- Sort records + for _, records in pairs(mapRecords) do + table.sort(records, lbComp) + end + + -- Remove duplicate entries + for _, records in pairs(mapRecords) do + local players = {} + local i = 1 + while i <= #records do + if players[records[i].name] then + table.remove(records, i) + else + players[records[i].name] = true + i = i + 1 + end + end + end + + return mapRecords +end +rawset(_G, "lb_get_map_records", GetMapRecords) + +local function insertOrReplaceRecord(map, score, modeSep) + LiveStore[map] = $ or {} + + for i, record in ipairs(LiveStore[map]) do + -- Replace the record + if record.name == score.name + and (record.flags & modeSep) == (score.flags & modeSep) then + LiveStore[map][i] = score + return + end + end + + table.insert(LiveStore[map], score) + + -- TODO: remove excess records +end + +local MSK_SPEED = 0xF0 +local MSK_WEIGHT = 0xF +local function stat_str(stat) + if stat then + return string.format("%d%d", (stat & MSK_SPEED) >> 4, stat & MSK_WEIGHT) + end + + return "0" +end + +-- GLOBAL +-- Save a record to the LiveStore and write to disk +-- SaveRecord will replace the record holders previous record but it will not compare any record times +local function SaveRecord(score, map, modeSep) + insertOrReplaceRecord(map, score, modeSep) + + print("Saving score") + + if not isserver then return end + + local f = assert( + io.open(LEADERBOARD_FILE, "w"), + "Failed to open file for writing: "..LEADERBOARD_FILE + ) + + f:setvbuf("line") + + for mapid, records in pairs(LiveStore) do + for _, record in ipairs(records) do + f:write( + mapid, "\t", + record.name, "\t", + record.skin, "\t", + record.color, "\t", + record.time, "\t", + table.concat(record.splits, " "), "\t", + record.flags, "\t", + stat_str(record.stat), "\n" + ) + end + end + + f:close() +end +rawset(_G, "lb_save_record", SaveRecord) + +local function netvars(net) + LiveStore = net($) +end + +addHook("NetVars", netvars) + +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 parseScore(str) + -- Leaderboard is stored in the following tab separated format + -- mapnum, name, skin, color, time, splits, flags, stat + local t = {} + for word in (str.."\t"):gmatch("(.-)\t") do + table.insert(t, word) + end + + local splits = {} + if t[6] != nil then + for str in t[6]:gmatch("([^ ]+)") do + table.insert(splits, tonumber(str)) + end + end + + local flags = 0 + if t[7] != nil then + flags = tonumber(t[7]) + 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 + + return score_t( + tonumber(t[1]), -- Map + t[2], -- Name + t[3], -- Skin + t[4], -- Color + tonumber(t[5]), -- Time + splits, + flags, + stats + ) +end +rawset(_G, "lb_parse_score", parseScore) + +-- Load the livestore +do + if isserver then + local f = assert( + io.open(LEADERBOARD_FILE, "r"), + "Failed to open file: "..LEADERBOARD_FILE + ) + + for l in f:lines() do + local score = parseScore(l) + print(score.name) + LiveStore[score.map] = $ or {} + table.insert(LiveStore[score.map], score) + end + + f:close() + end +end diff --git a/leaderboard.lua b/leaderboard.lua index fd79d4b..c8abf6b 100644 --- a/leaderboard.lua +++ b/leaderboard.lua @@ -1,9 +1,8 @@ -- Leaderboards written by Not -- Reusable --- Leaderboard Table --- [mode][mapnum][scoreTable] -local lb = {} +-- Holds the current maps records table including all modes +local MapRecords = {} local timeFinished = 0 local disable = false @@ -12,7 +11,7 @@ local splits = {} local PATCH = nil local help = true local EncoreInitial = nil -local scoreTable +local ScoreTable -- Text flash on finish @@ -94,11 +93,17 @@ local allowJoin -- lb_common.lua local ticsToTime = lb_TicsToTime local zoneAct = lb_ZoneAct +local stat_t = lb_stat_t +local lbComp = lb_comp -- browser.lua local InitBrowser = InitBrowser local DrawBrowser = DrawBrowser local BrowserController = BrowserController + +-- lb_store.lua +local GetMapRecords = lb_get_map_records +local SaveRecord = lb_save_record --------------- -- cvars @@ -164,79 +169,17 @@ local cv_interrupt = CV_RegisterVar({ 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 | CV_NOINIT, PossibleValue = CV_YesNo, func = function(v) - local curSep = ST_SEP - if v.value then ST_SEP = F_SPBATK else ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP end - - if curSep != ST_SEP then - reinit_lb() - end end }) @@ -256,80 +199,6 @@ end local MSK_SPEED = 0xF0 local MSK_WEIGHT = 0xF -local function stat_t(speed, weight) - if speed and weight then - return (speed << 4) | weight - end - return 0 -end - -local function stat_str(stat) - if stat then - return string.format("%d%d", (stat & MSK_SPEED) >> 4, stat & MSK_WEIGHT) - end - - return "0" -end - --- Read the leaderboard -if isserver then - 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 -end - function allowJoin(v) if not cv_interrupt.value then local y @@ -423,7 +292,7 @@ local function initBrowser(player) return end - InitBrowser(lb) + InitBrowser(ST_SEP) drawState = DS_BROWSER player.afkTime = leveltime @@ -840,6 +709,8 @@ addHook("MapLoad", function() allowJoin(true) --printTable(lb) + + MapRecords = GetMapRecords(gamemap, ST_SEP) end ) @@ -1150,12 +1021,14 @@ local function drawScoreboard(v, player) cachePatches(v) local gui = cv_gui.value + + -- Force enable gui at start and end of the race 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) + stateFunctions[drawState](v, player, ScoreTable, gui) end local pos = 0 @@ -1195,7 +1068,7 @@ end -- Find location of player and scroll to it function scroll_to(player) - local m = scoreTable or {} + local m = ScoreTable or {} scrollToPos = 2 for pos, score in ipairs(m) do @@ -1209,19 +1082,19 @@ function scroll_to(player) 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 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 checkFlags(p) local flags = 0 @@ -1261,7 +1134,7 @@ local function saveTime(player) return end - scoreTable = $ or {} + ScoreTable = $ or {} local pskin = skins[player.mo.skin] local newscore = score_t( @@ -1276,16 +1149,9 @@ local function saveTime(player) ) -- 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 + for i = 1, #ScoreTable do + if ScoreTable[i].name == player.name then + if not lbComp(newscore, ScoreTable[i]) then -- You suck lol S_StartSound(nil, 201) FlashTics = leveltime + TICRATE * 3 @@ -1297,61 +1163,33 @@ local function saveTime(player) end end - print("Saving score") - table.insert( - scoreTable, - newscore - ) - table.sort(scoreTable, lbComp) - while #scoreTable > cv_saves.value do - table.remove(scoreTable) - end + -- Save the record + SaveRecord(newscore, gamemap, ST_SEP) + -- Set players text flash and play chime sfx + S_StartSound(nil, 130) + FlashTics = leveltime + TICRATE * 3 + FlashRate = 1 + FlashVFlags = YellowFlash + + -- Reload the MapRecords + MapRecords = GetMapRecords(gamemap, ST_SEP) + + -- Set the updated ScoreTable + ScoreTable = MapRecords[Flags] + + -- Scroll the gui to the player entry scroll_to(player) - - setScoreTable(gamemap, Flags, scoreTable) - - if not StatTrack then - writeStats() - StatTrack = true - end - - if isserver then - 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 end -- DEBUGGING ---local function saveLeaderboard(player, ...) --- timeFinished = tonumber(... or player.realtime) --- splits = {1000, 2000, 3000} --- saveTime(player) ---end ---COM_AddCommand("save", saveLeaderboard) +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 @@ -1461,7 +1299,7 @@ local function think() end end - scoreTable = getScoreTable(gamemap, Flags) + ScoreTable = MapRecords[ST_SEP & Flags] if not cv_teamchange then cv_teamchange = CV_FindVar("allowteamchange") @@ -1562,6 +1400,6 @@ local function netvars(net) drawState = net($) StatTrack = net($) EncoreInitial = net($) - lb = net($) + MapRecords = net($) end addHook("NetVars", netvars)