-- 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 mapChecksum = lb_map_checksum ---------------------------- 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 = {} -- parse score function local parseScore -- GLOBAL -- Returns a list of all maps with records local function MapList() local maps = {} for mapid, checksums in pairs(ColdStore) do maps[mapid] = $ or {} for checksum in pairs(checksums) do maps[mapid][checksum] = true end end for mapid, checksums in pairs(LiveStore) do maps[mapid] = $ or {} for checksum in pairs(checksums) do maps[mapid][checksum] = true end end local maplist = {} for mapid, checksums in pairs(maps) do for checksum in pairs(checksums) do table.insert(maplist, {["id"] = mapid, ["checksum"] = checksum}) end end table.sort(maplist, function(a, b) return a.id < b.id end) return maplist end rawset(_G, "lb_map_list", MapList) -- GLOBAL -- Function for adding a single record from lua local function AddColdStore(record) ColdStore[record.map] = $ or {} ColdStore[record.map][record.checksum] = $ or {} table.insert(ColdStore[record.map][record.checksum], record) end rawset(_G, "lb_add_coldstore_record", AddColdStore) -- GLOBAL -- Function for adding a single record in string form from lua local function AddColdStoreString(record) AddColdStore(parseScore(record)) end rawset(_G, "lb_add_coldstore_record_string", AddColdStoreString) -- Insert mode separated records from the flat sourceTable into dest local function insertRecords(dest, sourceTable, checksum, modeSep) if not sourceTable then return end if not sourceTable[checksum] then return end local mode = nil for _, record in ipairs(sourceTable[checksum]) 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, checksum, modeSep) local mapRecords = {} -- Insert ColdStore records insertRecords(mapRecords, ColdStore[map], checksum, modeSep) -- Insert LiveStore records insertRecords(mapRecords, LiveStore[map], checksum, 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) 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) 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 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, 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 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 local checksum = t[9] return score_t( tonumber(t[1]), -- Map t[2], -- Name t[3], -- Skin t[4], -- Color tonumber(t[5]), -- Time splits, flags, stats, checksum ) end rawset(_G, "lb_parse_score", parseScore) -- Load the livestore 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) LiveStore[score.map] = $ or {} LiveStore[score.map][score.checksum] = $ or {} table.insert(LiveStore[score.map][score.checksum], score) end f:close() end