diff --git a/lb_common.lua b/lb_common.lua index 5ca198e..9a11c3e 100644 --- a/lb_common.lua +++ b/lb_common.lua @@ -1,3 +1,17 @@ +rawset(_G, "lb_score_t", function(map, name, skin, color, time, splits, flags, stat, checksum) + return { + ["map"] = map, + ["name"] = name, + ["skin"] = skin, + ["color"] = color, + ["time"] = time, + ["splits"] = splits, + ["flags"] = flags, + ["stat"] = stat, + ["checksum"] = checksum + } +end) + rawset(_G, "lb_TicsToTime", function(tics, pure) if tics == 0 and pure then return "-:--:--" @@ -64,3 +78,32 @@ rawset(_G, "lb_map_checksum", function(mapnum) local digest = string.format("%04x", djb2(mh.lvlttl..mh.subttl..mh.zonttl)) return string.sub(digest, #digest - 3) end) + +rawset(_G, "lb_mapnum_from_extended", function(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) diff --git a/lb_store.lua b/lb_store.lua index dfe867f..4dc80d2 100644 --- a/lb_store.lua +++ b/lb_store.lua @@ -5,11 +5,14 @@ -- lb_common.lua local stat_t = lb_stat_t local lbComp = lb_comp +local score_t = lb_score_t local mapChecksum = lb_map_checksum +local mapnumFromExtended = lb_mapnum_from_extended ---------------------------- local LEADERBOARD_FILE = "leaderboard.txt" +local COLDSTORE_FILE = "leaderboard.coldstore.txt" -- ColdStore are records loaded from lua addons -- this table should never be modified outside of the AddColdStore function @@ -21,6 +24,67 @@ local LiveStore = {} -- parse score function local parseScore +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 + +local function isSameRecord(a, b, modeSep) + return a.name == b.name and (a.flags & modeSep) == (b.flags & modeSep) +end + +-- insert or replace the score in dest +local function insertOrReplace(dest, score, modeSep) + for i, record in ipairs(dest) do + if isSameRecord(record, score, modeSep) then + if lbComp(score, record) then + dest[i] = score + end + return + end + end + + table.insert(dest, score) +end + +local function dumpStoreToFile(filename, store) + local f = assert( + io.open(filename, "w"), + "Failed to open file for writing: "..filename + ) + + f:setvbuf("line") + + for mapid, checksums in pairs(store) do + for checksum, records in pairs(checksums) do + for _, record in ipairs(records) do + if not record.checksum or record.checksum == "" then + record.checksum = mapChecksum(record.map) or "" + end + + 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), "\t", + record.checksum, "\n" + ) + end + end + end + + f:close() +end + -- GLOBAL -- Returns a list of all maps with records local function MapList() @@ -115,76 +179,19 @@ local function GetMapRecords(map, checksum, modeSep) end rawset(_G, "lb_get_map_records", GetMapRecords) -local function insertOrReplaceRecord(map, score, modeSep) +-- GLOBAL +-- Save a record to the LiveStore and write to disk +-- SaveRecord will replace the record holders previous record +local function SaveRecord(score, map, modeSep) local checksum = mapChecksum(map) LiveStore[map] = $ or {} LiveStore[map][checksum] = $ or {} - - for i, record in ipairs(LiveStore[map][checksum]) do - -- Replace the record - if record.name == score.name - and (record.flags & modeSep) == (score.flags & modeSep) then - LiveStore[map][checksum][i] = score - return - end - end - - table.insert(LiveStore[map][checksum], 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) + insertOrReplace(LiveStore[map][checksum], 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, checksums in pairs(LiveStore) do - for checksum, records in pairs(checksums) do - for _, record in ipairs(records) do - -- Insert checksum if missing - if (not record.checksum) or record.checksum == "" then - record.checksum = mapChecksum(mapid) - end - - 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), "\t", - record.checksum or "", "\n" - ) - end - end + if isserver then + dumpStoreToFile(LEADERBOARD_FILE, LiveStore) end - - f:close() end rawset(_G, "lb_save_record", SaveRecord) @@ -194,20 +201,6 @@ end addHook("NetVars", netvars) -local function score_t(map, name, skin, color, time, splits, flags, stat, checksum) - return { - ["map"] = map, - ["name"] = name, - ["skin"] = skin, - ["color"] = color, - ["time"] = time, - ["splits"] = splits, - ["flags"] = flags, - ["stat"] = stat, - ["checksum"] = checksum - } -end - function parseScore(str) -- Leaderboard is stored in the following tab separated format -- mapnum, name, skin, color, time, splits, flags, stat @@ -237,7 +230,7 @@ function parseScore(str) end end - local checksum = t[9] + local checksum = t[9] or "" return score_t( tonumber(t[1]), -- Map @@ -253,19 +246,145 @@ function parseScore(str) end rawset(_G, "lb_parse_score", parseScore) --- Load the livestore -if isserver then +-- Read and parse a store file +local function loadStoreFile(filename) local f = assert( - io.open(LEADERBOARD_FILE, "r"), - "Failed to open file: "..LEADERBOARD_FILE + io.open(filename, "r"), + "Failed to open file for reading: "..filename ) + local store = {} + for l in f:lines() do local score = parseScore(l) - LiveStore[score.map] = $ or {} - LiveStore[score.map][score.checksum] = $ or {} - table.insert(LiveStore[score.map][score.checksum], score) + store[score.map] = $ or {} + store[score.map][score.checksum] = $ or {} + table.insert(store[score.map][score.checksum], score) end f:close() + + return store +end + +-- GLOBAL +-- Command for moving records from one map to another +local function moveRecords(from, to, modeSep) + local function moveRecordsInStore(store) + if not (store[from.id] and store[from.id][from.checksum]) then + return 0 + end + + local moveCount = #store[from.id][from.checksum] + + store[to.id] = $ or {} + store[to.id][to.checksum] = $ or {} + for i, score in ipairs(store[from.id][from.checksum]) do + score.map = to.id + score.checksum = to.checksum + insertOrReplace(store[to.id][to.checksum], score, modeSep) + end + + -- Destroy the original table + store[from.id][from.checksum] = nil + + return moveCount + end + + -- move livestore records and write to disk + local moveCount = moveRecordsInStore(LiveStore) + dumpStoreToFile(LEADERBOARD_FILE, LiveStore) + + -- move coldstore records + if isserver then + local ok, coldstore = pcall(loadStoreFile, COLDSTORE_FILE) + if ok and coldstore then + moveRecordsInStore(coldstore) + dumpStoreToFile(COLDSTORE_FILE, coldstore) + end + end + + return moveCount +end +rawset(_G, "lb_move_records", moveRecords) + +-- Helper function for those upgrading from 1.2 to 1.3 +COM_AddCommand("lb_write_checksums", function(player) + local count = 0 + local moved = {} + + -- Gather movable records (no checksum, map loaded) + for map, checksums in pairs(LiveStore) do + for checksum, records in pairs(checksums) do + if checksum == "" then + local sum = mapChecksum(map) + + if not sum then continue end + + moved[map] = {} + moved[map][sum] = {} + + for i, record in ipairs(records) do + record.checksum = sum + table.insert(moved[map][sum], record) + end + end + end + end + + -- Write moved to livestore + for map, checksums in pairs(moved) do + LiveStore[map] = $ or {} + for checksum, records in pairs(checksums) do + LiveStore[map][checksum] = $ or {} + for i, score in ipairs(records) do + table.insert(LiveStore[map][checksum], score) + end + count = $ + #records + end + LiveStore[map][""] = nil + end + + if isserver then + dumpStoreToFile(LEADERBOARD_FILE, LiveStore) + end + + CONS_Printf(player, string.format("Successful operation on %d records", count)) +end, COM_ADMIN) + +COM_AddCommand("lb_known_maps", function(player, map) + if not map then + CONS_Printf(player, "Usage: ") + CONS_Printf(player, "Print all known checksums under ") + return + end + + local mapnum = mapnumFromExtended(map) + if not mapnum then + CONS_Printf(player, string.format("invalid map '%s'", map)) + return + end + + local known = {} + + if LiveStore[mapnum] then + for checksum, records in pairs(LiveStore[mapnum]) do + known[checksum] = #records + end + end + if ColdStore[mapnum] then + for checksum, records in pairs(ColdStore[mapnum]) do + known[checksum] = $ or 0 + #records + end + end + + CONS_Printf(player, "Map Chck Records") + for checksum, count in pairs(known) do + CONS_Printf(player, string.format("%s %s %d", map, checksum, count)) + end +end) + +-- Load the livestore +if isserver then + LiveStore = loadStoreFile(LEADERBOARD_FILE) end diff --git a/leaderboard.lua b/leaderboard.lua index e08cb70..efdd173 100644 --- a/leaderboard.lua +++ b/leaderboard.lua @@ -1,6 +1,28 @@ -- Leaderboards written by Not -- Reusable +---------- Imported functions ------------- +-- lb_common.lua +local ticsToTime = lb_TicsToTime +local zoneAct = lb_ZoneAct +local stat_t = lb_stat_t +local lbComp = lb_comp +local mapChecksum = lb_map_checksum +local score_t = lb_score_t +local mapnumFromExtended = lb_mapnum_from_extended + +-- 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 +local MapList = lb_map_list +local MoveRecords = lb_move_records +-------------------------------------------- + -- Holds the current maps records table including all modes local MapRecords = {} @@ -85,25 +107,6 @@ local scroll_to local allowJoin --- Imported functions -- - --- lb_common.lua -local ticsToTime = lb_TicsToTime -local zoneAct = lb_ZoneAct -local stat_t = lb_stat_t -local lbComp = lb_comp -local mapChecksum = lb_map_checksum - --- 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 -local MapList = lb_map_list ---------------- -- cvars local cv_teamchange @@ -182,19 +185,6 @@ local cv_spb_separate = CV_RegisterVar({ 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 MSK_SPEED = 0xF0 local MSK_WEIGHT = 0xF @@ -343,35 +333,6 @@ local function findMap(player, ...) 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", @@ -683,6 +644,50 @@ local function findRival(player, ...) end COM_AddCommand("rival", findRival) +local function moveRecords(player, from_map, from_checksum, to_map, to_checksum) + if not(from_map and from_checksum and to_map) then + CONS_Printf(player, "Usage: lb_move_records []") + CONS_Printf( + player, + string.format( + "Summary: Move records from one map to another.\n".. + "If no is supplied then the checksum of the current loaded map %s is used.\n".. + "Hint: Use lb_known_maps to find checksums", + to_map or "" + ) + ) + return + end + + local from = { + ["id"] = mapnumFromExtended(from_map), + ["checksum"] = from_checksum + } + + local to = { + ["id"] = mapnumFromExtended(to_map), + } + to.checksum = to_checksum or mapChecksum(to.id) + + if not to.checksum then + CONS_Printf(player, string.format("error: '%s' is missing; provide to_checksum to continue", to.id)) + return + end + + CONS_Printf( + player, + string.format( + "%d records have been moved from %s %s to %s %s", + MoveRecords(from, to, ST_SEP), + from_map, from.checksum, + to_map, to.checksum + ) + ) + + CONS_Printf(player, "Please repack coldstore and restart the server for changes to take effect.") +end +COM_AddCommand("lb_move_records", moveRecords) + --DEBUGGING --local function printTable(tb) -- for mode, tbl in pairs(tb) do @@ -1192,12 +1197,12 @@ local function saveTime(player) 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