40 Commits

Author SHA1 Message Date
Not
f824e760e3 remove StatTrack 2022-11-14 14:30:55 +01:00
Not
107a81f67c fix 'rival' command 2022-11-14 14:30:55 +01:00
Not
7bd8a13b14 add state netvars 2022-11-14 14:30:55 +01:00
Not
5d59f6c6fc disable debugging command 2022-11-14 14:30:55 +01:00
Not
3a61ffcabd allow non netvar stored, cold record loading from lua 2022-11-14 14:30:50 +01:00
Not
1d4eb423d6 Merge pull request 'Only save/load files for the server' (#6) from Lonsfor/srb2k-leaderboard:lonsfor-patch-3 into master
Reviewed-on: #6
2022-11-14 14:22:43 +01:00
Not
2d4784f62e compress stats into a single number 2022-09-15 02:08:24 +02:00
Not
6953f343dc keep previously used flag 2022-09-12 17:00:42 +02:00
Not
dfecef7225 add extra game mode check before saving score 2022-09-10 23:03:01 +02:00
Not
e03c9beecc fix broken browser when SPBA is disabled while SPBADone is set 2022-09-10 18:42:24 +02:00
Not
e6aac8b7a1 Merge pull request 'Add level selector / leaderboard browser' (#7) from browser into master 2022-09-06 10:08:41 +02:00
Not
dbf020e320 highlight current player 2022-09-06 09:52:51 +02:00
Not
f0cb051564 speed up invinc icons 2022-09-04 18:32:54 +02:00
Not
5fe7a6966a toggle encore with item button 2022-09-04 18:00:30 +02:00
Not
f21bc7d97c flip left/right in encore 2022-09-04 17:23:32 +02:00
Not
2f5db0f3c7 nevermind 2022-09-04 05:45:08 +02:00
Not
c497581539 invert map selection direction 2022-09-04 05:33:06 +02:00
Not
1f9c00202e group hell maps together 2022-09-03 00:42:38 +02:00
Not
2c11b1e0fc draw hell 2022-09-03 00:42:38 +02:00
Not
e453290419 lb_common.lua 2022-09-03 00:42:38 +02:00
Not
d7dc3336dd fix modulo by zero 2022-09-03 00:42:38 +02:00
Not
4165639282 export functions 2022-09-03 00:42:38 +02:00
Not
6afb884e59 fix uninitialized variables, add browser timeout 2022-09-03 00:42:38 +02:00
Not
407d65b44d reject browser in battle 2022-09-03 00:42:38 +02:00
Not
ec12485c43 actually, don't reset modePref on init 2022-09-03 00:42:38 +02:00
Not
003dd98ce3 rawget once 2022-09-03 00:42:38 +02:00
Not
7642dfaf98 reset prefMode, scrollPos on init 2022-09-03 00:42:38 +02:00
Not
127161fe9c remember preferred gamemode 2022-09-03 00:42:38 +02:00
Not
294ef8d40c use BLANKLVL 2022-09-03 00:42:38 +02:00
Not
fd1827b214 add scrolling to browser 2022-09-03 00:42:38 +02:00
Not
45effdd048 draw flags 2022-09-03 00:42:38 +02:00
Not
45cefc786e disable spba if browser is open 2022-09-03 00:42:38 +02:00
Not
554e329194 delay intermission while browsing 2022-09-03 00:42:38 +02:00
Not
b6e6e82bd6 add levelselector 2022-09-03 00:42:38 +02:00
Not
c83dc1d070 don't allow retry on battlemaps 2022-09-03 00:42:18 +02:00
Not
c150ed6be1 reinitialize lb only if ST_SEP has changed 2022-08-25 00:56:17 +02:00
Not
499f00b6f4 noinit cv_spb_combined 2022-08-25 00:32:03 +02:00
518e9f7893 Only save/load files for the server 2022-08-24 19:25:53 +02:00
Not
a840a5fa83 revert maploaded cvars 2022-08-24 16:18:24 +02:00
Not
bd5e3f24c6 add extra checks for spbattack 2022-08-23 17:10:10 +02:00
4 changed files with 997 additions and 301 deletions

497
browser.lua Normal file
View File

@ -0,0 +1,497 @@
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)

46
lb_common.lua Normal file
View File

@ -0,0 +1,46 @@
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)

239
lb_store.lua Normal file
View File

@ -0,0 +1,239 @@
-- 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
-- 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 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

View File

@ -1,19 +1,17 @@
-- 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 TimeFinished = 0
local disable = false
local prevLap = 0
local splits = {}
local PATCH = nil
local help = true
local EncoreInitial = nil
local cv_teamchange
local scoreTable
local ScoreTable
-- Text flash on finish
@ -30,11 +28,8 @@ local RedFlash = {
[1] = 0
}
-- Tracks if stats have been written or not
local StatTrack = false
local UNCLAIMED = "Unclaimed Record"
local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records"
local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records levelselect"
local FILENAME = "leaderboard.txt"
-- Retry / changelevel map
@ -56,6 +51,7 @@ local clearcheats = false
local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1
local AFK_TIMEOUT = TICRATE * 5
local AFK_BROWSER = TICRATE * 15
local AFK_BALANCE = TICRATE * 60
local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10
local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5
@ -69,6 +65,7 @@ local DS_DEFAULT = 0x0
local DS_SCROLL = 0x1
local DS_AUTO = 0x2
local DS_SCRLTO = 0x4
local DS_BROWSER = 0x8
local drawState = DS_DEFAULT
@ -86,11 +83,31 @@ local clamp
local scroll_to
local ticsToTime
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({
name = "lb_gui",
defaultvalue = GUI_ON,
@ -150,66 +167,10 @@ 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,
flags = CV_NETVAR | CV_CALL | CV_NOINIT,
PossibleValue = CV_YesNo,
func = function(v)
if v.value then
@ -217,8 +178,6 @@ local cv_spb_separate = CV_RegisterVar({
else
ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP
end
reinit_lb()
end
})
@ -235,80 +194,8 @@ local function score_t(map, name, skin, color, time, splits, flags, stat)
}
end
local function stat_t(speed, weight)
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
-- 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
-- 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
local MSK_SPEED = 0xF0
local MSK_WEIGHT = 0xF
function allowJoin(v)
if not cv_interrupt.value then
@ -367,6 +254,12 @@ end
local function retry(player, ...)
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
if leveltime < 20 then
return
@ -383,19 +276,26 @@ local function exitlevel(player, ...)
end
COM_AddCommand("exit", exitlevel)
local function zoneAct(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
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
return z
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 search = ...
@ -494,6 +394,7 @@ end
local function records(player, ...)
local mapid = ...
local mapnum = gamemap
local mapRecords = MapRecords
if mapid then
mapnum = mapnumFromExtended(mapid)
@ -501,6 +402,8 @@ local function records(player, ...)
CONS_Printf(player, string.format("Invalid map name: %s", mapid))
return
end
mapRecords = GetMapRecords(mapnum, ST_SEP)
end
local map = mapheaderinfo[mapnum]
@ -528,35 +431,33 @@ local function records(player, ...)
CONS_Printf(player, "\x85UNKNOWN MAP")
end
for mode, maps in pairs(lb) do
local maptbl = maps[mapnum]
if not maptbl then continue 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, tbl in ipairs(maptbl) do
for i, score in ipairs(records) do
CONS_Printf(
player,
string.format(
"%2d %-21s \x89%8s \x80%s",
i,
tbl["name"],
ticsToTime(tbl["time"]),
modeToString(tbl["flags"])
score["name"],
ticsToTime(score["time"]),
modeToString(score["flags"])
)
)
end
else
for i, tbl in ipairs(maptbl) do
for i, score in ipairs(records) do
CONS_Printf(
player,
string.format(
"%2d %-21s \x89%8s",
i,
tbl["name"],
ticsToTime(tbl["time"])
score["name"],
ticsToTime(score["time"])
)
)
end
@ -660,21 +561,26 @@ local function findRival(player, ...)
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] = {}
local maplist = MapList()
local mapRecords
local rivalScore
local yourScore
for i = 1, #maplist do
mapRecords = GetMapRecords(maplist[i], ST_SEP)
for map, scoreTable in pairs(tbl) do
local rivalScore = nil
local yourScore = nil
for mode, records in pairs(mapRecords) do
scores[mode] = $ or {}
for _, score in pairs(scoreTable) do
if score["name"] == player.name then
rivalScore = nil
yourScore = nil
for _, score in ipairs(records) do
if score.name == player.name then
yourScore = score
elseif score["name"] == rival then
elseif score.name == rival then
rivalScore = score
end
@ -684,7 +590,7 @@ local function findRival(player, ...)
end
if rivalScore and yourScore then
totalDiff = totalDiff + yourScore["time"] - rivalScore["time"]
totalDiff = totalDiff + yourScore.time - rivalScore.time
end
if rivalScore then
@ -692,8 +598,8 @@ local function findRival(player, ...)
table.insert(
scores[mode],
{
["rival"] = rivalScore,
["your"] = yourScore
rival = rivalScore,
your = yourScore
}
)
end
@ -797,7 +703,7 @@ COM_AddCommand("rival", findRival)
--end
addHook("MapLoad", function()
timeFinished = 0
TimeFinished = 0
splits = {}
prevLap = 0
drawState = DS_DEFAULT
@ -807,22 +713,11 @@ addHook("MapLoad", function()
allowJoin(true)
--printTable(lb)
MapRecords = GetMapRecords(gamemap, ST_SEP)
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
local iXoffset = 13 * FRACUNIT
local iYoffset = 12 * FRACUNIT
@ -845,7 +740,7 @@ local modePatches = {
local function modePatch(flag)
if flag == F_SPBEXP then
return PATCH[modePatches[flag]][(leveltime / 4) % 6]
return PATCH[modePatches[flag]][(leveltime / 3) % 6]
end
return PATCH[modePatches[flag]]
end
@ -969,11 +864,11 @@ local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, te
local pskin = score["skin"] and skins[score["skin"]]
if stat and not (
pskin
and pskin.kartweight == stat["weight"]
and pskin.kartspeed == stat["speed"]
and pskin.kartweight == stat & MSK_WEIGHT
and pskin.kartspeed == (stat & MSK_SPEED) >> 4
) then
v.drawString(x + FACERANK_DIM - 2, y + 4, stat["speed"], V_HUDTRANS | VFLAGS, "small")
v.drawString(x + FACERANK_DIM - 2, y + 8, stat["weight"], V_HUDTRANS | VFLAGS, "small")
v.drawString(x + FACERANK_DIM - 2, y + 4, (stat & MSK_SPEED) >> 4, V_HUDTRANS | VFLAGS, "small")
v.drawString(x + FACERANK_DIM - 2, y + 8, stat & MSK_WEIGHT, V_HUDTRANS | VFLAGS, "small")
end
if gui == GUI_ON or (gui == GUI_SPLITS and showSplit) then
@ -1103,11 +998,16 @@ local function drawScrollTo(v, player, scoreTable, gui)
drawScroll(v, player, scoreTable, gui)
end
local function drawBrowser(v, player)
DrawBrowser(v, player)
end
local stateFunctions = {
[DS_DEFAULT] = drawDefault,
[DS_SCROLL] = drawScroll,
[DS_AUTO] = drawAuto,
[DS_SCRLTO] = drawScrollTo
[DS_SCRLTO] = drawScrollTo,
[DS_BROWSER] = drawBrowser
}
-- Draw mode and return pos + 1 if success
@ -1125,12 +1025,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
@ -1170,7 +1072,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
@ -1184,23 +1086,59 @@ 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
--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
-- 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
return flags
end
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 newscore = score_t(
@ -1208,23 +1146,16 @@ local function saveTime(player)
player.name,
player.mo.skin,
player.skincolor,
timeFinished,
TimeFinished,
splits,
Flags,
stat_t(player.HMRs or pskin.kartspeed, player.HMRw or pskin.kartweight)
)
-- 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
@ -1236,62 +1167,36 @@ 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
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
-- DEBUGGING
--local function saveLeaderboard(player, ...)
-- timeFinished = tonumber(... or player.realtime)
-- 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
if player.laps > prevLap and TimeFinished == 0 then
prevLap = player.laps
table.insert(splits, player.realtime)
showSplit = 5 * TICRATE
@ -1319,7 +1224,7 @@ local function think()
if not singleplayer() then
for p in players.iterate do
if p.valid and not p.spectator and not p.exiting and p.lives > 0 then
if p.cmd.buttons then
if p.cmd.buttons or p.cmd.driftturn then
p.afkTime = leveltime
end
@ -1364,13 +1269,7 @@ local function think()
end
-- Autospec
-- Encore
if leveltime == 1 then
Flags = $ & !F_ENCORE
if encoremode then
Flags = $ | F_ENCORE
end
if p then
for s in players.iterate do
if s.valid and s.spectator then
@ -1380,44 +1279,43 @@ local function think()
end
end
-- 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 leveltime > START_TIME - (3 * TICRATE) / 2 then
if clearcheats then
clearcheats = false
for q in players.iterate do
q.SPBAKARTBIG = false
q.SPBAjustice = false
q.SPBAshutup = false
end
end
if p then
if p.SPBAKARTBIG then
Flags = $ | F_SPBBIG
end
if p.SPBAjustice then
Flags = $ | F_SPBJUS
p.SPBAKARTBIG = false
p.SPBAjustice = false
p.SPBAshutup = false
end
end
Flags = checkFlags(p)
-- make sure the spb actually spawned
if server.SPBArunning and leveltime == START_TIME - 1 then
if not (server.SPBAbomb and server.SPBAbomb.valid) then
-- it didn't spawn, clear spb flags
Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS)
end
if not (Flags & F_SPBATK) then
end
else
hud.enable("freeplay")
end
end
scoreTable = getScoreTable(gamemap, Flags)
ScoreTable = MapRecords[ST_SEP & Flags]
if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange")
end
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
-- Spectators can't input buttons so let the gamer do it
if drawState == DS_SCROLL then
@ -1431,13 +1329,33 @@ local function think()
scrollAcc = 0
end
end
elseif drawState == DS_BROWSER then
if BrowserController(p) then
drawState = DS_DEFAULT
end
if p.lives == 0 then
-- prevent intermission while browsing
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
end
if p.cmd.buttons then
if p.cmd.buttons or p.cmd.driftturn then
p.afkTime = leveltime
end
@ -1453,10 +1371,6 @@ local function think()
end
end
if p.laps >= mapheaderinfo[gamemap].numlaps and timeFinished == 0 then
timeFinished = p.realtime
saveTime(p)
end
regLap(p)
elseif cv_teamchange.value == 0 then
allowJoin(true)
@ -1488,8 +1402,8 @@ local function netvars(net)
splits = net($)
prevLap = net($)
drawState = net($)
StatTrack = net($)
EncoreInitial = net($)
lb = net($)
MapRecords = net($)
TimeFinished = net($)
end
addHook("NetVars", netvars)