3 Commits

Author SHA1 Message Date
Not
db0696294b 4char padding, do proper string concat on map..checksum 2022-07-19 21:03:40 +02:00
Not
907c07a25d resave scores if checksums have been written 2022-07-19 20:31:03 +02:00
Not
00aa2060f3 use checksums to identify maps 2022-07-19 20:31:03 +02:00
5 changed files with 411 additions and 1271 deletions

View File

@ -1,497 +0,0 @@
local MapRecords
local maps
local mapIndex = 1
local scrollPos = 1
local modes = nil
local mode = 1
local prefMode = nil
local ModeSep
---- 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
local function mapIndexOffset(n)
return (mapIndex + n + #maps - 1) % #maps + 1
end
local function getMap(offset)
return maps[mapIndexOffset(offset or 0)]
end
local function updateModes()
-- set available modes for this map
modes = {}
for mode, _ in pairs(MapRecords) do
table.insert(modes, mode)
end
table.sort(modes)
mode = 1
-- select pref mode
for i, m in ipairs(modes) do
if m == prefMode then
mode = i
break
end
end
end
local function updateMapIndex(n)
mapIndex = mapIndexOffset(n)
scrollPos = 1
MapRecords = GetMapRecords(maps[mapIndex], ModeSep)
updateModes()
end
local scalar = 2
local hlfScrnWdth = 320 / 2
local mappY = 26
local ttlY = mappY + FixedMul(30, FRACUNIT / scalar)
local scoresY = ttlY + 16
local sin = sin
local function drawMapPatch(v, offset)
local scale = FRACUNIT / (abs(offset) + scalar)
local mapName = G_BuildMapName(getMap(offset))
local patchName = mapName.."P"
local mapp = v.patchExists(patchName) and v.cachePatch(patchName) or v.cachePatch("BLANKLVL")
local scaledWidth = FixedMul(mapp.width, scale)
local scaledHeight = FixedMul(mapp.height, scale)
v.drawScaled(
(hlfScrnWdth + offset * scaledWidth - scaledWidth / 2) * FRACUNIT,
(mappY - scaledHeight / 2) * FRACUNIT,
scale,
mapp
)
end
local function drawEncore(v)
if not cv_kartencore then
cv_kartencore = CV_FindVar("kartencore")
end
if not cv_kartencore.value then
return
end
local rubyp = v.cachePatch("RUBYICON")
local bob = sin(leveltime * ANG10) * 2
v.drawScaled(
hlfScrnWdth * FRACUNIT,
mappY * FRACUNIT + bob,
FRACUNIT,
rubyp
)
end
local colors = {
[0] = 0,
[1] = 215
}
local function drawMapBorder(v)
local mapWidth = FixedMul(160, FRACUNIT / scalar)
local mapHeight = FixedMul(100, FRACUNIT / scalar)
v.drawFill(
hlfScrnWdth - mapWidth / 2 - 1,
mappY - mapHeight / 2 -1,
mapWidth + 2,
mapHeight + 2,
colors[leveltime / 4 % 2]
)
end
local function drawMapStrings(v)
local map = mapheaderinfo[getMap()]
local titleWidth = v.stringWidth(map.lvlttl)
-- title
v.drawString(
hlfScrnWdth,
ttlY,
map.lvlttl,
V_SKYMAP,
"center"
)
-- zone/act
local zone = ZoneAct(map)
local zoneWidth = v.stringWidth(zone)
v.drawString(
hlfScrnWdth + titleWidth / 2,
ttlY + 8,
zone,
V_SKYMAP,
"right"
)
-- subtitle
v.drawString(
hlfScrnWdth + titleWidth / 2 - zoneWidth,
ttlY + 8,
map.subttl,
V_MAGENTAMAP,
"small-right"
)
-- hell
if map.menuflags & LF2_HIDEINMENU then
v.drawString(
300,
ttlY + 16,
"HELL",
V_REDMAP,
"right"
)
end
end
local F_SPBATK = 0x1
local F_SPBJUS = 0x2
local F_SPBBIG = 0x4
local F_SPBEXP = 0x8
local F_ENCORE = 0x80
local function drawGamemode(v)
local m = modes[mode] or 0
local modeX = 20
local modeY = scoresY
local scale = FRACUNIT / 2
if m == 0 then
local clockp = v.cachePatch("K_LAPE02")
v.drawScaled(
modeX * FRACUNIT,
modeY * FRACUNIT,
scale,
clockp
)
v.drawString(
modeX,
modeY,
"Time Attack!"
)
elseif m & F_SPBATK then
local scaledHalf = FixedMul(50 * FRACUNIT, scale) / 2
local xoff = 0
if m & F_SPBBIG then
xoff = $ + scaledHalf
end
if m & F_SPBEXP then
xoff = $ + scaledHalf
end
if m & F_SPBBIG then
local growp = v.cachePatch("K_ITGROW")
v.drawScaled(
modeX * FRACUNIT - scaledHalf + xoff,
modeY * FRACUNIT - scaledHalf,
scale,
growp
)
xoff = $ - scaledHalf
end
if m & F_SPBEXP then
local invp = v.cachePatch("K_ITINV"..(leveltime / 3 % 7 + 1))
v.drawScaled(
modeX * FRACUNIT - scaledHalf + xoff,
modeY * FRACUNIT - scaledHalf,
scale,
invp
)
end
local spbp = v.cachePatch("K_ITSPB")
v.drawScaled(
modeX * FRACUNIT - scaledHalf,
modeY * FRACUNIT - scaledHalf,
scale,
spbp
)
v.drawString(
modeX,
modeY,
"SPB Attack!"
)
end
end
local function drawFlags(v, x, y, flags)
local nx = x * FRACUNIT
local ny = y * FRACUNIT + 2 * FRACUNIT
local margin = 4 * FRACUNIT
if flags & F_ENCORE then
local encp = v.cachePatch("RUBYICON")
v.drawScaled(
nx,
ny + 2 * FRACUNIT,
FRACUNIT / 5,
encp
)
nx = $ + margin
end
if flags & F_SPBATK then
local scale = FRACUNIT / 3
local shift = 6 * FRACUNIT
nx = $ - shift
ny = $ - shift
if flags & F_SPBJUS then
local hyup = v.cachePatch("K_ISHYUD")
v.drawScaled(nx, ny, scale, hyup)
nx = $ + margin
end
if flags & F_SPBBIG then
local growp = v.cachePatch("K_ISGROW")
v.drawScaled(nx - FRACUNIT / 2, ny, scale, growp)
nx = $ + margin
end
if flags & F_SPBEXP then
local invp = v.cachePatch("K_ISINV"..(leveltime / 3 % 6 + 1))
v.drawScaled(nx, ny, scale, invp)
nx = $ + margin
end
end
end
local MSK_SPEED = 0xF0
local MSK_WEIGHT = 0xF
local function drawStats(v, x, y, skin, stats)
local s = skins[skin]
if stats
and not (s
and s.kartspeed == (stats & MSK_SPEED) >> 4
and s.kartweight == stats & MSK_WEIGHT
) then
v.drawString(x-2, y-2, (stats & MSK_SPEED) >> 4, V_ALLOWLOWERCASE, "thin")
v.drawString(x + 13, y + 9, stats & MSK_WEIGHT, V_ALLOWLOWERCASE, "thin")
end
end
-- draw in columns
-- pos, facerank, name, time, flags
-- ______________________________________________
-- | 3|[O]|InsertNameHere | 01:02:03 | EXB |
-- ----------------------------------------------
-- defined are widths of each column, x value is calculated below
local column = {
[1] = 18, -- facerank, pos, drawNum is right aligned
[2] = 170, -- name
[3] = 60, -- time
[4] = 0 -- flags
}
do
local w = 32 -- starting offset
local t
for i = 1, #column do
t = column[i]
column[i] = w
w = $ + t
end
end
local colorFlags = {
[0] = V_SKYMAP,
[1] = 0
}
local function drawScore(v, i, pos, score, highlight)
local y = scoresY + i * 18
local textFlag = colorFlags[pos%2]
-- position
v.drawNum(column[1], y, pos)
-- facerank
local skin = skins[score["skin"]]
local facerank = skin and v.cachePatch(skin.facerank) or v.cachePatch("M_NORANK")
v.draw(column[1], y, facerank, 0, v.getColormap("sonic", score["color"]))
-- chili
if highlight then
local chilip = v.cachePatch("K_CHILI"..leveltime/4%8+1)
v.draw(column[1], y, chilip)
textFlag = V_YELLOWMAP
end
-- stats
drawStats(v, column[1], y, score["skin"], score["stat"])
-- name
v.drawString(column[2], y, score["name"], V_ALLOWLOWERCASE | textFlag)
-- time
v.drawString(column[3], y, TicsToTime(score["time"]), textFlag)
-- flags
drawFlags(v, column[4], y, score["flags"])
end
local function drawBrowser(v, player)
if not MapRecords then return end
v.fadeScreen(0xFF00, 16)
-- previous, next maps
for i = 5, 1, -1 do
drawMapPatch(v, -i)
drawMapPatch(v, i)
end
-- draw map border
drawMapBorder(v)
-- current map
drawMapPatch(v, 0)
drawEncore(v)
drawMapStrings(v)
drawGamemode(v)
if not modes then return end
local records = MapRecords[modes[mode]]
if not records then return end
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, records[i], records[i].name == player.name)
end
end
rawset(_G, "DrawBrowser", drawBrowser)
local function initBrowser(modeSep)
ModeSep = modeSep
-- set mapIndex to current map
for i, m in ipairs(maps) do
if m == gamemap then
mapIndex = i
break
end
end
-- initialize MapRecords
MapRecords = GetMapRecords(gamemap, ModeSep)
scrollPos = 1
updateModes()
end
rawset(_G, "InitBrowser", initBrowser)
-- initialize maps with racemaps only
local function loadMaps()
maps = {}
local hell = {}
for i = 0, #mapheaderinfo do
local map = mapheaderinfo[i]
if map and map.typeoflevel & TOL_RACE then
if map.menuflags & LF2_HIDEINMENU then
table.insert(hell, i)
else
table.insert(maps, i)
end
end
end
-- append hell maps
for _, map in ipairs(hell) do
table.insert(maps, map)
end
end
addHook("MapLoad", loadMaps)
local repeatCount = 0
local keyRepeat = 0
local function updateKeyRepeat()
S_StartSound(nil, 143)
if repeatCount < 1 then
keyRepeat = TICRATE / 4
else
keyRepeat = TICRATE / 15
end
repeatCount = $ + 1
end
local function resetKeyRepeat()
keyRepeat = 0
repeatCount = 0
end
local ValidButtons = BT_ACCELERATE | BT_BRAKE | BT_FORWARD | BT_BACKWARD | BT_DRIFT | BT_ATTACK
-- return value indicates we want to exit the browser
local function controller(player)
keyRepeat = max(0, $ - 1)
if not (player.cmd.driftturn or player.cmd.buttons) then
resetKeyRepeat()
end
local cmd = player.cmd
if not keyRepeat then
if not (cmd.buttons & ValidButtons or cmd.driftturn) then
return
end
updateKeyRepeat()
if cmd.buttons & BT_BRAKE then
S_StartSound(nil, 115)
return true
elseif cmd.buttons & BT_ACCELERATE then
COM_BufInsertText(player, "changelevel "..G_BuildMapName(maps[mapIndex]))
return true
elseif cmd.buttons & BT_ATTACK then
COM_BufInsertText(player, "encore")
elseif cmd.driftturn then
local dir = cmd.driftturn > 0 and -1 or 1
if encoremode then
updateMapIndex(-dir)
else
updateMapIndex(dir)
end
elseif cmd.buttons & BT_FORWARD then
scrollPos = $ - 1
elseif cmd.buttons & BT_BACKWARD then
scrollPos = $ + 1
elseif cmd.buttons & BT_DRIFT then
scrollPos = 1
if modes and #modes then
mode = $ % #modes + 1
prefMode = modes[mode]
end
end
end
end
rawset(_G, "BrowserController", controller)
local function netvars(net)
maps = net($)
mapIndex = net($)
modes = net($)
mode = net($)
prefMode = net($)
scrollPos = net($)
MapRecords = net($)
ModeSep = net($)
end
addHook("NetVars", netvars)

View File

@ -1,46 +0,0 @@
rawset(_G, "lb_TicsToTime", function(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)
rawset(_G, "lb_ZoneAct", function(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)
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)

View File

@ -1,274 +0,0 @@
-- 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 = {}
-- parse score function
local parseScore
-- GLOBAL
-- Returns a list of all maps with records
local function MapList()
local maps = {}
for map in pairs(ColdStore) do
maps[map] = true
end
for map in pairs(LiveStore) do
maps[map] = true
end
local maplist = {}
for map in pairs(maps) do
table.insert(maplist, map)
end
table.sort(maplist)
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 {}
table.insert(ColdStore[record.map], 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, 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
local function djb2(message)
local digest = 5381
for c in message:gmatch(".") do
digest = (($ << 5) + $) + string.byte(c)
end
return digest
end
-- Produce a checksum by using the maps title, subtitle and zone
local function mapChecksum(mapnum)
local mh = mapheaderinfo[mapnum]
if not mh then
return nil
end
local digest = string.format("%04x", djb2(mh.lvlttl..mh.subttl..mh.zonttl))
return string.sub(digest, #digest - 3)
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
-- 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
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 {}
table.insert(LiveStore[score.map], score)
end
f:close()
end

View File

@ -1,17 +1,19 @@
-- Leaderboards written by Not -- Leaderboards written by Not
-- Reusable -- Reusable
-- Holds the current maps records table including all modes -- Leaderboard Table
local MapRecords = {} -- [mode][mapnum][scoreTable]
local lb = {}
local TimeFinished = 0 local timeFinished = 0
local disable = false local disable = false
local prevLap = 0 local prevLap = 0
local splits = {} local splits = {}
local PATCH = nil local PATCH = nil
local help = true local help = true
local EncoreInitial = nil local EncoreInitial = nil
local ScoreTable local cv_teamchange
local scoreTable
-- Text flash on finish -- Text flash on finish
@ -28,9 +30,12 @@ local RedFlash = {
[1] = 0 [1] = 0
} }
-- Tracks if stats have been written or not
local StatTrack = false
local UNCLAIMED = "Unclaimed Record" local UNCLAIMED = "Unclaimed Record"
local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records levelselect" local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore"
local FILENAME = "leaderboard.txt" local FILENAME = "leaderboard"
-- Retry / changelevel map -- Retry / changelevel map
local nextMap = nil local nextMap = nil
@ -51,7 +56,6 @@ local clearcheats = false
local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1 local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1
local AFK_TIMEOUT = TICRATE * 5 local AFK_TIMEOUT = TICRATE * 5
local AFK_BROWSER = TICRATE * 15
local AFK_BALANCE = TICRATE * 60 local AFK_BALANCE = TICRATE * 60
local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10 local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10
local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5 local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5
@ -65,7 +69,6 @@ local DS_DEFAULT = 0x0
local DS_SCROLL = 0x1 local DS_SCROLL = 0x1
local DS_AUTO = 0x2 local DS_AUTO = 0x2
local DS_SCRLTO = 0x4 local DS_SCRLTO = 0x4
local DS_BROWSER = 0x8
local drawState = DS_DEFAULT local drawState = DS_DEFAULT
@ -83,31 +86,11 @@ local clamp
local scroll_to local scroll_to
local ticsToTime
local allowJoin 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
-- 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
local cv_spbatk
local cv_gui = CV_RegisterVar({ local cv_gui = CV_RegisterVar({
name = "lb_gui", name = "lb_gui",
defaultvalue = GUI_ON, defaultvalue = GUI_ON,
@ -167,10 +150,87 @@ local cv_interrupt = CV_RegisterVar({
end end
}) })
local function djb2(message)
local digest = 5381
for c in message:gmatch(".") do
digest = (($ << 5) + $) + string.byte(c)
end
return digest
end
local function mapChecksum(mapnum)
local mh = mapheaderinfo[mapnum]
if not mh then
return nil
end
local digest = string.format("%04x", djb2(mh.lvlttl+mh.subttl+mh.zonttl))
return string.sub(digest, #digest - 3)
end
local function setST(t, map, flags, scoreTable)
local mode = flags & ST_SEP
local cks = mapChecksum(map)
t[mode] = t[mode] or {}
t[mode][tostring(map)..(cks or "")] = scoreTable
end
local function getST(t, map, flags)
local mode = flags & ST_SEP
local cks = mapChecksum(map)
return t[mode] and t[mode][tostring(map)..(cks or "")] 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({ local cv_spb_separate = CV_RegisterVar({
name = "lb_spb_combined", name = "lb_spb_combined",
defaultvalue = 1, defaultvalue = 1,
flags = CV_NETVAR | CV_CALL | CV_NOINIT, flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_YesNo, PossibleValue = CV_YesNo,
func = function(v) func = function(v)
if v.value then if v.value then
@ -178,10 +238,12 @@ local cv_spb_separate = CV_RegisterVar({
else else
ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP
end end
reinit_lb()
end end
}) })
local function score_t(map, name, skin, color, time, splits, flags, stat) local function score_t(map, name, skin, color, time, splits, flags, stat, checksum)
return { return {
["map"] = map, ["map"] = map,
["name"] = name, ["name"] = name,
@ -190,12 +252,149 @@ local function score_t(map, name, skin, color, time, splits, flags, stat)
["time"] = time, ["time"] = time,
["splits"] = splits, ["splits"] = splits,
["flags"] = flags, ["flags"] = flags,
["stat"] = stat ["stat"] = stat,
["checksum"] = checksum
} }
end end
local MSK_SPEED = 0xF0 local function stat_t(speed, weight)
local MSK_WEIGHT = 0xF 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
local function writeScore(fh, score)
fh: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"]), "\t",
score["checksum"], "\n"
)
end
local function saveScores()
local f = assert(io.open(FILENAME + ".txt", "w"))
if f == nil then
print("Failed to open file for writing: " + FILENAME + ".txt")
return
end
for _, tbl in pairs(lb) do
for _, scoreTable in pairs(tbl) do
for _, score in ipairs(scoreTable) do
writeScore(f, score)
end
end
end
f:close()
end
-- Read the leaderboard
local f = io.open(FILENAME + ".txt", "r")
if f then
-- track if scores checksums have been written
local checksumWrite = false
for l in f:lines() do
-- Leaderboard is stored in the following tab separated format
-- mapnum, name, skin, color, time, splits, flags, stat, map_checksum
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
local mapnum = tonumber(t[1])
-- Checksums
local cks = t[9]
if cks == nil then
checksumWrite = true
cks = mapChecksum(mapnum)
-- We have no previous recognition of this map
-- remove it
if cks == nil then
local rsfh = io.open(FILENAME + "_removed_scores.txt", "a")
if not rsfh then
print("Failed to open file: ", FILENAME + "_removed_scores.txt")
continue
end
rsfh:write(l, "\n")
rsfh:close()
continue
end
end
table.insert(
scoreTable,
score_t(
mapnum,
t[2],
t[3],
t[4],
tonumber(t[5]),
spl,
flags,
stats,
cks
)
)
setScoreTable(tonumber(t[1]), flags, scoreTable)
end
sortScores()
f:close()
-- save scores
if checksumWrite then
saveScores()
end
else
print("Failed to open file: ", FILENAME + ".txt")
end
function allowJoin(v) function allowJoin(v)
if not cv_interrupt.value then if not cv_interrupt.value then
@ -254,12 +453,6 @@ end
local function retry(player, ...) local function retry(player, ...)
if doyoudare(player) then if doyoudare(player) then
-- Verify valid race level
if not (mapheaderinfo[gamemap].typeoflevel & (TOL_SP | TOL_RACE)) then
CONS_Printf(player, "Battle maps are not supported")
return
end
-- Prevents bind crash -- Prevents bind crash
if leveltime < 20 then if leveltime < 20 then
return return
@ -276,27 +469,6 @@ local function exitlevel(player, ...)
end end
COM_AddCommand("exit", exitlevel) COM_AddCommand("exit", exitlevel)
local function initBrowser(player)
if not doyoudare(player) then return end
-- TODO: allow in battle
if mapheaderinfo[gamemap].typeoflevel & TOL_MATCH then
CONS_Printf(player, "Please exit battle first")
return
end
if not InitBrowser then
print("Browser is not loaded")
return
end
InitBrowser(ST_SEP)
drawState = DS_BROWSER
player.afkTime = leveltime
end
COM_AddCommand("levelselect", initBrowser)
local function findMap(player, ...) local function findMap(player, ...)
local search = ... local search = ...
@ -309,13 +481,28 @@ local function findMap(player, ...)
} }
local lvltype, map, lvlttl local lvltype, map, lvlttl
local zoneAct = function(zone, act, levelflags)
local z = ""
if zone != "" then
z = " " + zone
elseif not(levelflags & LF_NOZONE) then
z = " Zone"
end
if act != "" then
z = $ + " " + act
end
return z
end
for i = 1, #mapheaderinfo do for i = 1, #mapheaderinfo do
map = mapheaderinfo[i] map = mapheaderinfo[i]
if map == nil then if map == nil then
continue continue
end end
lvlttl = map.lvlttl + zoneAct(map) lvlttl = map.lvlttl + zoneAct(map.zonttl, map.actnum, map.levelflags)
if not search or lvlttl:lower():find(search:lower()) then if not search or lvlttl:lower():find(search:lower()) then
-- Only care for up to TOL_MATCH (0x10) -- Only care for up to TOL_MATCH (0x10)
@ -342,130 +529,10 @@ local function findMap(player, ...)
end end
COM_AddCommand("findmap", findMap) COM_AddCommand("findmap", findMap)
local function mapnumFromExtended(map) local function mapNotExists(player, map)
local p, q = map:upper():match("MAP(%w)(%w)$", 1) CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper()))
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 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
local mapRecords = MapRecords
if mapid then
mapnum = mapnumFromExtended(mapid)
if not mapnum then
CONS_Printf(player, string.format("Invalid map name: %s", mapid))
return
end
mapRecords = GetMapRecords(mapnum, ST_SEP)
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, records in pairs(mapRecords) do
CONS_Printf(player, "")
CONS_Printf(player, modeToString(mode))
-- don't print flags for time attack
if mode then
for i, score in ipairs(records) do
CONS_Printf(
player,
string.format(
"%2d %-21s \x89%8s \x80%s",
i,
score["name"],
ticsToTime(score["time"]),
modeToString(score["flags"])
)
)
end
else
for i, score in ipairs(records) do
CONS_Printf(
player,
string.format(
"%2d %-21s \x89%8s",
i,
score["name"],
ticsToTime(score["time"])
)
)
end
end
end
end
COM_AddCommand("records", records)
local function changelevel(player, ...) local function changelevel(player, ...)
if not doyoudare(player) then if not doyoudare(player) then
return return
@ -480,13 +547,35 @@ local function changelevel(player, ...)
return return
end end
local mapnum = mapnumFromExtended(map) local p, q = map:upper():match("MAP(%w)(%w)$", 1)
if not mapnum then if not (p and q) then
CONS_Printf(player, string.format("Invalid map name: %s", map)) CONS_Printf(player, string.format("Invalid map name: %s", map))
return
end end
local mapnum = 0
local A = string.byte("A")
if tonumber(p) != nil then
-- Non extended map numbers
if tonumber(q) == nil then
mapNotExists(player, map)
return
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
if mapheaderinfo[mapnum] == nil then if mapheaderinfo[mapnum] == nil then
CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper())) mapNotExists(player, map)
return return
end end
@ -551,7 +640,7 @@ local function findRival(player, ...)
[0] = "\x89", [0] = "\x89",
[-1] = "\x88" [-1] = "\x88"
} }
local sym = { local sym = {
[true] = "-", [true] = "-",
[false] = "", [false] = "",
@ -561,26 +650,21 @@ local function findRival(player, ...)
local totalScores = 0 local totalScores = 0
local totalDiff = 0 local totalDiff = 0
CONS_Printf(player, string.format("\x89%s's times:", rival)) CONS_Printf(player, string.format("\x89%s's times:", rival))
CONS_Printf(player, "MAP Time Diff Mode") CONS_Printf(player, "MAP Time Diff Mode")
local maplist = MapList() for mode, tbl in pairs(lb) do
local mapRecords scores[mode] = {}
local rivalScore
local yourScore
for i = 1, #maplist do
mapRecords = GetMapRecords(maplist[i], ST_SEP)
for mode, records in pairs(mapRecords) do for map, scoreTable in pairs(tbl) do
scores[mode] = $ or {} local rivalScore = nil
local yourScore = nil
rivalScore = nil for _, score in pairs(scoreTable) do
yourScore = nil if score["name"] == player.name then
for _, score in ipairs(records) do
if score.name == player.name then
yourScore = score yourScore = score
elseif score.name == rival then elseif score["name"] == rival then
rivalScore = score rivalScore = score
end end
@ -590,7 +674,7 @@ local function findRival(player, ...)
end end
if rivalScore and yourScore then if rivalScore and yourScore then
totalDiff = totalDiff + yourScore.time - rivalScore.time totalDiff = totalDiff + yourScore["time"] - rivalScore["time"]
end end
if rivalScore then if rivalScore then
@ -598,8 +682,8 @@ local function findRival(player, ...)
table.insert( table.insert(
scores[mode], scores[mode],
{ {
rival = rivalScore, ["rival"] = rivalScore,
your = yourScore ["your"] = yourScore
} }
) )
end end
@ -614,6 +698,12 @@ local function findRival(player, ...)
return a["rival"]["map"] < b["rival"]["map"] return a["rival"]["map"] < b["rival"]["map"]
end end
local modestrings = {
[F_SPBEXP] = "X",
[F_SPBBIG] = "B",
[F_SPBJUS] = "J",
}
for mode, tbl in pairs(scores) do for mode, tbl in pairs(scores) do
if i >= stop then break end if i >= stop then break end
@ -627,7 +717,15 @@ local function findRival(player, ...)
if i >= stop then break end if i >= stop then break end
i = i + 1 i = i + 1
local modestr = modeToString(score["rival"]["flags"]) local modestr = "TA"
if score["rival"]["flags"] & F_SPBATK then
modestr = "SPB"
for k, v in pairs(modestrings) do
if score["rival"]["flags"] & k then
modestr = $ + v
end
end
end
if score["your"] then if score["your"] then
local diff = score["your"]["time"] - score["rival"]["time"] local diff = score["your"]["time"] - score["rival"]["time"]
@ -696,14 +794,14 @@ COM_AddCommand("rival", findRival)
-- -- v[i]["flags"], -- -- v[i]["flags"],
-- -- "," -- -- ","
-- -- ) -- -- )
-- -- -- --
-- --end -- --end
-- end -- end
-- end -- end
--end --end
addHook("MapLoad", function() addHook("MapLoad", function()
TimeFinished = 0 timeFinished = 0
splits = {} splits = {}
prevLap = 0 prevLap = 0
drawState = DS_DEFAULT drawState = DS_DEFAULT
@ -713,11 +811,22 @@ addHook("MapLoad", function()
allowJoin(true) allowJoin(true)
--printTable(lb) --printTable(lb)
MapRecords = GetMapRecords(gamemap, ST_SEP)
end 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 -- Item patches have the amazing property of being displaced 12x 13y pixels
local iXoffset = 13 * FRACUNIT local iXoffset = 13 * FRACUNIT
local iYoffset = 12 * FRACUNIT local iYoffset = 12 * FRACUNIT
@ -740,7 +849,7 @@ local modePatches = {
local function modePatch(flag) local function modePatch(flag)
if flag == F_SPBEXP then if flag == F_SPBEXP then
return PATCH[modePatches[flag]][(leveltime / 3) % 6] return PATCH[modePatches[flag]][(leveltime / 4) % 6]
end end
return PATCH[modePatches[flag]] return PATCH[modePatches[flag]]
end end
@ -753,7 +862,7 @@ local function marquee(text, maxwidth)
if #text <= maxwidth then if #text <= maxwidth then
return text return text
end end
local shift = 16 local shift = 16
-- Creates an index range ranging from -shift to #text + shift -- Creates an index range ranging from -shift to #text + shift
@ -864,11 +973,11 @@ local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, te
local pskin = score["skin"] and skins[score["skin"]] local pskin = score["skin"] and skins[score["skin"]]
if stat and not ( if stat and not (
pskin pskin
and pskin.kartweight == stat & MSK_WEIGHT and pskin.kartweight == stat["weight"]
and pskin.kartspeed == (stat & MSK_SPEED) >> 4 and pskin.kartspeed == stat["speed"]
) then ) then
v.drawString(x + FACERANK_DIM - 2, y + 4, (stat & MSK_SPEED) >> 4, V_HUDTRANS | VFLAGS, "small") v.drawString(x + FACERANK_DIM - 2, y + 4, stat["speed"], V_HUDTRANS | VFLAGS, "small")
v.drawString(x + FACERANK_DIM - 2, y + 8, stat & MSK_WEIGHT, V_HUDTRANS | VFLAGS, "small") v.drawString(x + FACERANK_DIM - 2, y + 8, stat["weight"], V_HUDTRANS | VFLAGS, "small")
end end
if gui == GUI_ON or (gui == GUI_SPLITS and showSplit) then if gui == GUI_ON or (gui == GUI_SPLITS and showSplit) then
@ -998,16 +1107,11 @@ local function drawScrollTo(v, player, scoreTable, gui)
drawScroll(v, player, scoreTable, gui) drawScroll(v, player, scoreTable, gui)
end end
local function drawBrowser(v, player)
DrawBrowser(v, player)
end
local stateFunctions = { local stateFunctions = {
[DS_DEFAULT] = drawDefault, [DS_DEFAULT] = drawDefault,
[DS_SCROLL] = drawScroll, [DS_SCROLL] = drawScroll,
[DS_AUTO] = drawAuto, [DS_AUTO] = drawAuto,
[DS_SCRLTO] = drawScrollTo, [DS_SCRLTO] = drawScrollTo
[DS_BROWSER] = drawBrowser
} }
-- Draw mode and return pos + 1 if success -- Draw mode and return pos + 1 if success
@ -1025,14 +1129,12 @@ local function drawScoreboard(v, player)
cachePatches(v) cachePatches(v)
local gui = cv_gui.value 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 if leveltime < START_TIME or player.exiting or player.lives == 0 then
gui = GUI_ON gui = GUI_ON
end end
if gui then if gui then
stateFunctions[drawState](v, player, ScoreTable, gui) stateFunctions[drawState](v, player, scoreTable, gui)
end end
local pos = 0 local pos = 0
@ -1072,7 +1174,7 @@ end
-- Find location of player and scroll to it -- Find location of player and scroll to it
function scroll_to(player) function scroll_to(player)
local m = ScoreTable or {} local m = scoreTable or {}
scrollToPos = 2 scrollToPos = 2
for pos, score in ipairs(m) do for pos, score in ipairs(m) do
@ -1086,59 +1188,23 @@ function scroll_to(player)
end end
-- Write skin stats to each score where there are none -- Write skin stats to each score where there are none
--local function writeStats() local function writeStats()
-- for _, t in pairs(lb) do for _, t in pairs(lb) do
-- for _, scoreTable in pairs(t) do for _, scoreTable in pairs(t) do
-- for _, score in ipairs(scoreTable) do for _, score in ipairs(scoreTable) do
-- local skin = skins[score["skin"]] local skin = skins[score["skin"]]
-- if skin and not score["stat"] then if skin and not score["stat"] then
-- local stats = stat_t(skin.kartspeed, skin.kartweight) local stats = stat_t(skin.kartspeed, skin.kartweight)
-- score["stat"] = stats score["stat"] = stats
-- end end
-- end end
-- end
-- end
--end
local function checkFlags(p)
local flags = 0
-- Encore
if encoremode then
flags = $ | F_ENCORE
end
if not cv_spbatk then
cv_spbatk = CV_FindVar("spbatk")
end
-- SPBAttack
if server.SPBArunning and cv_spbatk.value then
flags = $ | F_SPBATK
if server.SPBAexpert then
flags = $ | F_SPBEXP
end
if p.SPBAKARTBIG then
flags = $ | F_SPBBIG
end
if p.SPBAjustice then
flags = $ | F_SPBJUS
end end
end end
return flags
end end
local function saveTime(player) local function saveTime(player)
-- Disqualify if the flags changed mid trial.
if checkFlags(player) != Flags then
print("Game mode change detected! Time has been disqualified.")
S_StartSound(nil, 110)
return
end
ScoreTable = $ or {} scoreTable = $ or {}
local pskin = skins[player.mo.skin] local pskin = skins[player.mo.skin]
local newscore = score_t( local newscore = score_t(
@ -1146,16 +1212,24 @@ local function saveTime(player)
player.name, player.name,
player.mo.skin, player.mo.skin,
player.skincolor, player.skincolor,
TimeFinished, timeFinished,
splits, splits,
Flags, Flags,
stat_t(player.HMRs or pskin.kartspeed, player.HMRw or pskin.kartweight) stat_t(player.HMRs or pskin.kartspeed, player.HMRw or pskin.kartweight),
mapChecksum(gamemap)
) )
-- Check if you beat your previous best -- Check if you beat your previous best
for i = 1, #ScoreTable do for i = 1, #scoreTable do
if ScoreTable[i].name == player.name then if scoreTable[i]["name"] == player.name then
if not lbComp(newscore, ScoreTable[i]) 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 -- You suck lol
S_StartSound(nil, 201) S_StartSound(nil, 201)
FlashTics = leveltime + TICRATE * 3 FlashTics = leveltime + TICRATE * 3
@ -1167,36 +1241,39 @@ local function saveTime(player)
end end
end end
print("Saving score")
table.insert(
scoreTable,
newscore
)
-- Save the record table.sort(scoreTable, lbComp)
SaveRecord(newscore, gamemap, ST_SEP) while #scoreTable > cv_saves.value do
table.remove(scoreTable)
end
-- 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) scroll_to(player)
setScoreTable(gamemap, Flags, scoreTable)
if not StatTrack then
writeStats()
StatTrack = true
end
saveScores()
end end
-- DEBUGGING -- DEBUGGING
--local function saveLeaderboard(player, ...) --local function saveLeaderboard(player, ...)
-- TimeFinished = tonumber(... or player.realtime) -- timeFinished = tonumber(... or player.realtime)
-- splits = {1000, 2000, 3000} -- splits = {1000, 2000, 3000}
-- saveTime(player) -- saveTime(player)
--end --end
--COM_AddCommand("save", saveLeaderboard) --COM_AddCommand("save", saveLeaderboard)
local function regLap(player) local function regLap(player)
if player.laps > prevLap and TimeFinished == 0 then if player.laps > prevLap and timeFinished == 0 then
prevLap = player.laps prevLap = player.laps
table.insert(splits, player.realtime) table.insert(splits, player.realtime)
showSplit = 5 * TICRATE showSplit = 5 * TICRATE
@ -1224,7 +1301,7 @@ local function think()
if not singleplayer() then if not singleplayer() then
for p in players.iterate do for p in players.iterate do
if p.valid and not p.spectator and not p.exiting and p.lives > 0 then if p.valid and not p.spectator and not p.exiting and p.lives > 0 then
if p.cmd.buttons or p.cmd.driftturn then if p.cmd.buttons then
p.afkTime = leveltime p.afkTime = leveltime
end end
@ -1269,7 +1346,13 @@ local function think()
end end
-- Autospec -- Autospec
-- Encore
if leveltime == 1 then if leveltime == 1 then
Flags = $ & !F_ENCORE
if encoremode then
Flags = $ | F_ENCORE
end
if p then if p then
for s in players.iterate do for s in players.iterate do
if s.valid and s.spectator then if s.valid and s.spectator then
@ -1278,44 +1361,45 @@ local function think()
end end
end end
end end
if leveltime > START_TIME - (3 * TICRATE) / 2 then -- Gamemode flags
Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS)
if leveltime > START_TIME - (3 * TICRATE) / 2 and server.SPBArunning then
Flags = $ | F_SPBATK
if server.SPBAexpert then
Flags = $ | F_SPBEXP
end
if clearcheats then if clearcheats then
clearcheats = false clearcheats = false
if p then for q in players.iterate do
p.SPBAKARTBIG = false q.SPBAKARTBIG = false
p.SPBAjustice = false q.SPBAjustice = false
p.SPBAshutup = false q.SPBAshutup = false
end end
end end
Flags = checkFlags(p) if p then
if p.SPBAKARTBIG then
-- make sure the spb actually spawned Flags = $ | F_SPBBIG
if server.SPBArunning and leveltime == START_TIME - 1 then end
if not (server.SPBAbomb and server.SPBAbomb.valid) then if p.SPBAjustice then
-- it didn't spawn, clear spb flags Flags = $ | F_SPBJUS
Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS)
end end
end end
else
end
if not (Flags & F_SPBATK) then
hud.enable("freeplay") hud.enable("freeplay")
end end
end end
ScoreTable = MapRecords[ST_SEP & Flags] scoreTable = getScoreTable(gamemap, Flags)
if not cv_teamchange then if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange") cv_teamchange = CV_FindVar("allowteamchange")
end end
if p then if p then
-- must be done before browser control
if p.laps >= mapheaderinfo[gamemap].numlaps and TimeFinished == 0 then
TimeFinished = p.realtime
saveTime(p)
end
-- Scroll controller -- Scroll controller
-- Spectators can't input buttons so let the gamer do it -- Spectators can't input buttons so let the gamer do it
if drawState == DS_SCROLL then if drawState == DS_SCROLL then
@ -1329,33 +1413,13 @@ local function think()
scrollAcc = 0 scrollAcc = 0
end end
end end
elseif drawState == DS_BROWSER then end
if BrowserController(p) then
drawState = DS_DEFAULT
end
-- prevent intermission while browsing if p.lives == 0 then
if p.exiting then
p.exiting = $ + 1
end
-- disable spba hud
if server.SPBArunning and server.SPBAdone then
server.SPBArunning = false
p.pflags = $ & !(PF_TIMEOVER)
p.exiting = 100
end
-- prevent softlocking the server
if p.afkTime + AFK_BROWSER < leveltime then
drawState = DS_DEFAULT
S_StartSound(nil, 100)
end
elseif p.lives == 0 then
drawState = DS_SCROLL drawState = DS_SCROLL
end end
if p.cmd.buttons or p.cmd.driftturn then if p.cmd.buttons then
p.afkTime = leveltime p.afkTime = leveltime
end end
@ -1371,6 +1435,10 @@ local function think()
end end
end end
if p.laps >= mapheaderinfo[gamemap].numlaps and timeFinished == 0 then
timeFinished = p.realtime
saveTime(p)
end
regLap(p) regLap(p)
elseif cv_teamchange.value == 0 then elseif cv_teamchange.value == 0 then
allowJoin(true) allowJoin(true)
@ -1402,8 +1470,8 @@ local function netvars(net)
splits = net($) splits = net($)
prevLap = net($) prevLap = net($)
drawState = net($) drawState = net($)
StatTrack = net($)
EncoreInitial = net($) EncoreInitial = net($)
MapRecords = net($) lb = net($)
TimeFinished = net($)
end end
addHook("NetVars", netvars) addHook("NetVars", netvars)

View File

@ -1,111 +0,0 @@
#!/usr/bin/env python3
import sys
from os import linesep
if len(sys.argv) != 4 or not sys.argv[1] or not sys.argv[2] or not sys.argv[3]:
print("Usage: coldstore.py <leaderboard.txt> <coldstore.txt> <leaderboard_records.lua>")
quit()
leaderboard_txt = sys.argv[1]
coldstore_txt = sys.argv[2]
records_lua = sys.argv[3]
def ParseScore(score):
# Map Name Skin Color Time Splits Flags Stat
split = score.split("\t")
checksum = ""
if len(split) > 8:
checksum = split[8]
return {
"map": split[0],
"name": split[1],
"skin": split[2],
"color": split[3],
"time": int(split[4]),
"splits": split[5],
"flags": int(split[6]),
"stat": split[7],
"checksum": checksum
}
# Compare scores
def CompareScore(a, b):
return a["time"] < b["time"]
F_SEP = 0xF
def SameScore(a, b):
return a["name"] == b["name"] and (a["flags"] & F_SEP) == (b["flags"] & F_SEP)
def LoadRecordsFromFile(path):
records = []
with open(path, "r") as f:
for line in f.readlines():
records.append(ParseScore(line.strip()))
return records
def AddScore(records, score):
mapid = score["map"]
mapTable = records.get(mapid) or []
for i in range(len(mapTable)):
scoreb = mapTable[i]
if SameScore(score, scoreb):
if CompareScore(score, scoreb):
mapTable[i] = score
records[mapid] = mapTable
return
mapTable.append(score)
records[mapid] = mapTable
# load leaderboard.txt and coldstore.txt
recordsList = LoadRecordsFromFile(leaderboard_txt)
recordsList.extend(LoadRecordsFromFile(coldstore_txt))
# construct the map tables
records = {}
for score in recordsList:
AddScore(records, score)
# convert records to flat list
recordsList = []
rejected = []
for mapTable in records.values():
for score in mapTable:
scoreStr = "\t".join([str(v) for v in list(score.values())])
# only allow records with checksums
if score["checksum"] != "":
recordsList.append(scoreStr)
else:
rejected.append(scoreStr)
# truncate and write records to coldstore
with open(coldstore_txt, "w") as f:
for score in recordsList:
f.write(score + linesep)
luaA = """do
local AddColdStore = lb_add_coldstore_record_string
local records = {
"""
luaB = """ }
for _, str in ipairs(records) do
AddColdStore(str)
end
end
"""
# pack the records.lua file
with open(records_lua, "w") as f:
f.write(luaA)
for score in recordsList:
score = score.replace("\\", "\\\\")
score = score.replace("\"", "\\\"")
f.write("\t\t\"{}\",{}".format(score, linesep))
f.write(luaB)
# truncate and rewrite rejected scores to leaderboard.txt
with open(leaderboard_txt, "w") as f:
for score in rejected:
f.write(score + linesep)