87 Commits

Author SHA1 Message Date
Not
68e44e534b esacpe special characters 2022-11-16 16:10:14 +01:00
Not
0a21799e60 add coldstore string loading 2022-11-16 16:09:53 +01:00
Not
34da7e2929 reject records missing checksum 2022-11-14 22:18:01 +01:00
Not
08c1af3073 insert checksums 2022-11-14 22:17:30 +01:00
Not
52316347da add coldstore tool 2022-11-14 17:53:40 +01:00
Not
4a1e42c5cb remove print 2022-11-14 17:53:21 +01:00
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
Not
27e596967c console record browser 2022-08-21 04:38:12 +02:00
Not
d7955feaf8 flash score on finish 2022-07-19 20:30:30 +02:00
Not
5a74a428b1 disable on battle maps 2022-07-08 19:58:34 +02:00
Not
4f138797a9 fix encore ruby icon 2022-07-08 19:22:10 +02:00
Not
22aa4ecd2f include zone and act in findmap 2022-05-27 15:07:57 +02:00
Not
1305fa2979 check only for max 2 ingame players 2022-05-25 19:00:55 +02:00
Not
13aeceeedd use TOL_SP and TOL_COOP as indicators of nuked race, battle maps 2022-05-20 19:49:49 +02:00
Not
7b28cd0777 verify the map in changelevel isn't a battle map 2022-05-20 19:32:32 +02:00
Not
ec982d10c3 display more map info on findmap 2022-05-20 19:09:27 +02:00
Not
f3ec32384a use ascii arithmetic to find letter values 2022-05-20 17:35:04 +02:00
Not
07dca46c92 force change level, enforce race mode 2022-05-20 17:17:12 +02:00
Not
14645dbc90 Merge branch 'lonsfor-patch-1' 2022-05-12 12:17:36 +02:00
Not
6c7cdf34b9 set players afkTime before enabling antiAFK 2022-05-12 12:14:48 +02:00
23a8373230 only look for scoreTable once 2022-04-30 23:11:52 +02:00
14f8769d93 dont do an iteration when the player is already known 2022-04-30 22:56:19 +02:00
5080672f7a Update 'leaderboard.lua' 2022-04-30 22:49:28 +02:00
5790a3c020 Dont iterate players if afk is disabled 2022-04-30 21:30:34 +02:00
e3c870eefd Prevent overflow of minutes 2022-04-30 21:00:55 +02:00
41b152ccf6 add VoteThinker hook 2022-04-30 20:55:38 +02:00
de63c4b2be Mmove local cv_teamchange to the top scope
No need to constantly look up the cvar when it can be saved
2022-04-30 20:52:42 +02:00
Not
8a6161a1e1 prevent netvar changes in replays 2022-04-30 15:50:23 +02:00
Not
5cecda05f1 rename cvar lb_spb_separate to lb_spb_combined, display spb flags on rival command 2022-04-27 13:55:41 +02:00
Not
775886f045 use correct function 2022-04-27 13:11:07 +02:00
Not
1a72fba49d draw spb enabled modes in bottom left 2022-04-22 17:10:30 +02:00
Not
8de3423ef8 warn afk players 10 sec ahead 2022-04-19 13:02:01 +02:00
Not
45b785c654 fix nil skin 2022-04-19 11:36:31 +02:00
Not
a1989d319b restore kartencore to initial value 2022-04-18 20:51:18 +02:00
Not
1ab596ebeb save stats everytime 2022-04-18 20:39:58 +02:00
Not
14d2e889b5 fix gamemode exploit 2022-04-18 19:39:22 +02:00
Not
f31c68aa52 add flags to netvar 2022-04-18 19:34:06 +02:00
Not
c46f40001c add cvar for separation of SPB, KARTBIG, and EXP 2022-04-18 19:32:05 +02:00
Not
553afc606d add encore to help message 2022-04-05 12:12:32 +02:00
Not
c792dd1079 allow join on disable 2022-04-04 23:07:31 +02:00
Not
ee251b7a44 Add encore support 2022-04-04 20:58:35 +02:00
Not
0bde2a7fea add encore support 2022-04-04 20:56:03 +02:00
Not
c49702c33b create README.md 2022-03-28 13:27:11 +02:00
Not
6244847b81 fix ranking numbers x offset for 10, 100 2022-03-28 13:19:50 +02:00
Not
f53a6ba91f v1.2.17_hf 2022-03-28 13:16:00 +02:00
Not
647e4a0299 v1.2.17 2022-03-28 13:15:29 +02:00
Not
c0a296df69 v1.2.16 2022-03-28 13:15:01 +02:00
Not
b040fdd933 v1.2.15 2022-03-28 13:14:30 +02:00
6 changed files with 1993 additions and 311 deletions

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# Leaderboard plugin for srb2kart
https://mb.srb2.org/addons/time-attack-leaderboard.3742/

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)

274
lb_store.lua Normal file
View File

@ -0,0 +1,274 @@
-- 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,18 +1,42 @@
-- Leaderboards written by Not -- Leaderboards written by Not
-- Reusable
local FILENAME = "leaderboard.txt" -- Holds the current maps records table including all modes
local lb = {} local MapRecords = {}
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 ScoreTable
-- Text flash on finish
local FlashTics = 0
local FlashRate
local FlashVFlags
local YellowFlash = {
[0] = V_YELLOWMAP,
[1] = V_ORANGEMAP,
[2] = 0
}
local RedFlash = {
[0] = V_REDMAP,
[1] = 0
}
local UNCLAIMED = "Unclaimed Record"
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 -- Retry / changelevel map
local nextMap = nil local nextMap = nil
local Flags = 0 local Flags = 0
local F_ENCORE = 0x80
-- SPB flags with the least significance first -- SPB flags with the least significance first
local F_SPBATK = 0x1 local F_SPBATK = 0x1
@ -20,26 +44,144 @@ local F_SPBJUS = 0x2
local F_SPBBIG = 0x4 local F_SPBBIG = 0x4
local F_SPBEXP = 0x8 local F_SPBEXP = 0x8
-- Score table separator
local ST_SEP = F_SPBATK
local clearcheats = false local clearcheats = false
local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) 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 PREVENT_JOIN_TIME = START_TIME + TICRATE * 5 local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5
-- patch caching function local GUI_OFF = 0x0
local GUI_SPLITS = 0x1
local GUI_ON = 0x2
-- Draw states
local DS_DEFAULT = 0x0
local DS_SCROLL = 0x1
local DS_AUTO = 0x2
local DS_SCRLTO = 0x4
local DS_BROWSER = 0x8
local drawState = DS_DEFAULT
-- fixed_t scroll position
local scrollY = 50 * FRACUNIT
local scrollAcc = 0
-- functions --
-- patch caching
local cachePatches local cachePatches
local function lbID(map, flags) -- clamp(min, v, max)
local id = tostring(map) local clamp
if flags & F_SPBATK then
id = id + "S" 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
-- 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,
flags = 0,
PossibleValue = {Off = GUI_OFF, Splits = GUI_SPLITS, On = GUI_ON}
})
local AntiAFK = true
CV_RegisterVar({
name = "lb_afk",
defaultvalue = 1,
flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_OnOff,
func = function(v)
-- Set players afkTime and toggle AntiAFK
if v.value then
for p in players.iterate do
p.afkTime = leveltime
end
AntiAFK = true
else
AntiAFK = false
end
end end
})
return id local cv_enable = CV_RegisterVar({
end name = "lb_enable",
defaultvalue = 1,
flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_OnOff,
func = function(v)
disable = $ or not v.value
if disable then
allowJoin(true)
end
end
})
local function score_t(map, name, skin, color, time, splits, flags) local cv_saves = CV_RegisterVar({
name = "lb_save_count",
defaultvalue = 20,
flags = CV_NETVAR,
PossibleValue = CV_Natural
})
local cv_interrupt = CV_RegisterVar({
name = "lb_interrupt",
defaultvalue = 0,
flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_OnOff,
func = function(v)
if v.value then
COM_BufInsertText(server, "allowteamchange yes")
end
end
})
local cv_spb_separate = CV_RegisterVar({
name = "lb_spb_combined",
defaultvalue = 1,
flags = CV_NETVAR | CV_CALL | CV_NOINIT,
PossibleValue = CV_YesNo,
func = function(v)
if v.value then
ST_SEP = F_SPBATK
else
ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP
end
end
})
local function score_t(map, name, skin, color, time, splits, flags, stat)
return { return {
["map"] = map, ["map"] = map,
["name"] = name, ["name"] = name,
@ -47,92 +189,63 @@ local function score_t(map, name, skin, color, time, splits, flags)
["color"] = color, ["color"] = color,
["time"] = time, ["time"] = time,
["splits"] = splits, ["splits"] = splits,
["flags"] = flags ["flags"] = flags,
["stat"] = stat
} }
end end
-- Read the leaderboard local MSK_SPEED = 0xF0
local f = io.open(FILENAME, "r") local MSK_WEIGHT = 0xF
if f then
for l in f:lines() do function allowJoin(v)
-- Leaderboard is stored in the following tab separated format if not cv_interrupt.value then
-- name, skin, color, time, splits, flags local y
local t = {} if v then
for word in (l+"\t"):gmatch("(.-)\t") do y = "yes"
table.insert(t, word) hud.enable("freeplay")
else
y = "no"
hud.disable("freeplay")
end end
local flags = 0 COM_BufInsertText(server, "allowteamchange " + y)
if t[7] != nil then
flags = tonumber(t[7])
end
local lbt = lb[lbID(t[1], flags)]
if lbt == nil then
lbt = {}
end
local spl = {}
if t[6] != nil then
for str in t[6]:gmatch("([^ ]+)") do
table.insert(spl, tonumber(str))
end
end
table.insert(
lbt,
score_t(
t[1],
t[2],
t[3],
t[4],
tonumber(t[5]),
spl,
flags
)
)
lb[lbID(t[1], flags)] = lbt
end end
f:close()
else
print("Failed to open file: ", FILENAME)
end end
local function allowJoin(v) -- Returns true if there is a single player ingame
local y local function singleplayer()
if v then
y = "yes"
hud.enable("freeplay")
else
y = "no"
hud.disable("freeplay")
end
COM_BufInsertText(server, "allowteamchange " + y)
end
local function ingame()
local n = 0 local n = 0
for p in players.iterate do for p in players.iterate do
if p.valid and not p.spectator then if p.valid and not p.spectator then
n = $ + 1 n = $ + 1
if n > 1 then
return false
end
end end
end end
return n return true
end end
local function initLeaderboard(player) local function initLeaderboard(player)
if disable and leveltime < START_TIME then if disable and leveltime < START_TIME then
disable = ingame() > 1 disable = not singleplayer()
else else
disable = disable or ingame() > 1 disable = disable or not singleplayer()
end end
player.afkTimeout = leveltime disable = $ or not cv_enable.value or not (maptol & (TOL_SP | TOL_RACE))
-- Restore encore mode to initial value
if disable and EncoreInitial != nil then
COM_BufInsertText(server, string.format("kartencore %d", EncoreInitial))
EncoreInitial = nil
end
player.afkTime = leveltime
end end
addHook("PlayerSpawn", initLeaderboard) addHook("PlayerSpawn", initLeaderboard)
local function doyoudare(player) local function doyoudare(player)
if ingame() > 1 or player.spectator then if not singleplayer() or player.spectator then
CONS_Printf(player, "How dare you") CONS_Printf(player, "How dare you")
return false return false
end end
@ -141,6 +254,12 @@ 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
@ -157,11 +276,195 @@ local function exitlevel(player, ...)
end end
COM_AddCommand("exit", exitlevel) COM_AddCommand("exit", exitlevel)
local function mapNotExists(player, map) local function initBrowser(player)
CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper())) 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 search = ...
local hell = "\x85HELL"
local tol = {
[TOL_SP] = "\x81Race\x80", -- Nuked race maps
[TOL_COOP] = "\x8D\Battle\x80", -- Nuked battle maps
[TOL_RACE] = "\x88Race\x80",
[TOL_MATCH] = "\x87\Battle\x80"
}
local lvltype, map, lvlttl
for i = 1, #mapheaderinfo do
map = mapheaderinfo[i]
if map == nil then
continue
end
lvlttl = map.lvlttl + zoneAct(map)
if not search or lvlttl:lower():find(search:lower()) then
-- Only care for up to TOL_MATCH (0x10)
lvltype = tol[map.typeoflevel & 0x1F] or map.typeoflevel
-- If not battle print numlaps
lvltype = (map.typeoflevel & (TOL_MATCH | TOL_COOP) and lvltype)
or string.format("%s \x82%-2d\x80", lvltype, map.numlaps)
CONS_Printf(
player,
string.format(
"%s %-9s %-30s - %s\t%s",
G_BuildMapName(i),
lvltype,
lvlttl,
map.subttl,
(map.menuflags & LF2_HIDEINMENU and hell) or ""
)
)
end
end
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 end
local ALPH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 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
@ -177,29 +480,19 @@ local function changelevel(player, ...)
return return
end end
local p, q = map:upper():match("MAP(%w)(%w)$", 1) local mapnum = mapnumFromExtended(map)
if not (p and q) then if not mapnum then
CONS_Printf(player, string.format("Invalid map name: %s", map)) CONS_Printf(player, string.format("Invalid map name: %s", map))
end
if mapheaderinfo[mapnum] == nil then
CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper()))
return return
end end
local mapnum = 0 -- Verify valid race level
if tonumber(p) != nil then if not (mapheaderinfo[mapnum].typeoflevel & (TOL_SP | TOL_RACE)) then
-- Non extended map numbers CONS_Printf(player, "Battle maps are not supported")
if tonumber(q) == nil then
mapNotExists(player, map)
return
end
mapnum = tonumber(p) * 10 + tonumber(q)
else
--Extended map numbers
p = ALPH:find(p) - 1
q = (tonumber(q) or ALPH:find(q) + 9)
mapnum = 36 * p + q + 100
end
if mapheaderinfo[mapnum] == nil then
mapNotExists(player, map)
return return
end end
@ -207,6 +500,24 @@ local function changelevel(player, ...)
end end
COM_AddCommand("changelevel", changelevel) COM_AddCommand("changelevel", changelevel)
local function toggleEncore(player)
if not doyoudare(player) then
return
end
local enc = CV_FindVar("kartencore")
if EncoreInitial == nil then
EncoreInitial = enc.value
end
if enc.value then
COM_BufInsertText(server, "kartencore off")
else
COM_BufInsertText(server, "kartencore on")
end
end
COM_AddCommand("encore", toggleEncore)
local function clearcheats(player) local function clearcheats(player)
if not player.spectator then if not player.spectator then
clearcheats = true clearcheats = true
@ -215,46 +526,198 @@ local function clearcheats(player)
end end
COM_AddCommand("spba_clearcheats", clearcheats) COM_AddCommand("spba_clearcheats", clearcheats)
local function scrollGUI(player, ...)
if not doyoudare(player) then return end
if drawState == DS_DEFAULT then
scroll_to(player)
else
drawState = DS_DEFAULT
end
end
COM_AddCommand("scroll", scrollGUI)
local function findRival(player, ...)
local rival, page = ...
page = (tonumber(page) or 1) - 1
if rival == nil then
CONS_Printf(player, "Print the times of your rival.\nUsage: rival <playername> <page>")
return
end
local colors = {
[1] = "\x85",
[0] = "\x89",
[-1] = "\x88"
}
local sym = {
[true] = "-",
[false] = "",
}
local scores = {}
local totalScores = 0
local totalDiff = 0
CONS_Printf(player, string.format("\x89%s's times:", rival))
CONS_Printf(player, "MAP Time Diff Mode")
local maplist = MapList()
local mapRecords
local rivalScore
local yourScore
for i = 1, #maplist do
mapRecords = GetMapRecords(maplist[i], ST_SEP)
for mode, records in pairs(mapRecords) do
scores[mode] = $ or {}
rivalScore = nil
yourScore = nil
for _, score in ipairs(records) do
if score.name == player.name then
yourScore = score
elseif score.name == rival then
rivalScore = score
end
if rivalScore and yourScore then
break
end
end
if rivalScore and yourScore then
totalDiff = totalDiff + yourScore.time - rivalScore.time
end
if rivalScore then
totalScores = totalScores + 1
table.insert(
scores[mode],
{
rival = rivalScore,
your = yourScore
}
)
end
end
end
local i = 0
local stop = 19
local o = page * stop
local function sortf(a, b)
return a["rival"]["map"] < b["rival"]["map"]
end
for mode, tbl in pairs(scores) do
if i >= stop then break end
table.sort(tbl, sortf)
for _, score in ipairs(tbl) do
if o then
o = o - 1
continue
end
if i >= stop then break end
i = i + 1
local modestr = modeToString(score["rival"]["flags"])
if score["your"] then
local diff = score["your"]["time"] - score["rival"]["time"]
local color = colors[clamp(-1, diff, 1)]
CONS_Printf(
player,
string.format(
"%s %8s %s%9s \x80%s",
G_BuildMapName(score["rival"]["map"]),
ticsToTime(score["rival"]["time"]),
color,
sym[diff<0] + ticsToTime(abs(diff)),
modestr
)
)
else
CONS_Printf(
player,
string.format(
"%s %8s %9s %s",
G_BuildMapName(score["rival"]["map"]),
ticsToTime(score["rival"]["time"]),
ticsToTime(0, true),
modestr
)
)
end
end
end
CONS_Printf(
player,
string.format(
"Your score = %s%s%s",
colors[clamp(-1, totalDiff, 1)],
sym[totalDiff<0],
ticsToTime(abs(totalDiff))
)
)
CONS_Printf(
player,
string.format(
"Page %d out of %d",
page + 1,
totalScores / stop + 1
)
)
end
COM_AddCommand("rival", findRival)
--DEBUGGING --DEBUGGING
--local function printTable(tb) --local function printTable(tb)
-- for k, v in pairs(tb) do -- for mode, tbl in pairs(tb) do
-- for i = 1, #v do -- for map, scoreTable in pairs(tbl) do
-- print("TABLE: " + k, tb[k]) -- print(string.format("[%d][%d] #%d", mode, map, #scoreTable))
-- if v[i] != nil then --
-- print( -- --if scoreTable != nil then
-- v[i]["name"], -- -- print(
-- v[i]["skin"], -- -- v[i]["name"],
-- v[i]["color"], -- -- v[i]["skin"],
-- v[i]["time"], -- -- v[i]["color"],
-- table.concat(v[i]["splits"]), -- -- v[i]["time"],
-- v[i]["flags"], -- -- table.concat(v[i]["splits"]),
-- "," -- -- 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
scrollY = 50 * FRACUNIT
scrollAcc = 0
FlashTics = 0
allowJoin(true) allowJoin(true)
--printTable(lb) --printTable(lb)
MapRecords = GetMapRecords(gamemap, ST_SEP)
end end
) )
local function ticsToTime(tics)
return string.format(
"%d:%02d:%02d",
G_TicsToMinutes(tics),
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
@ -268,97 +731,153 @@ local function drawitem(v, x, y, scale, itempatch, vflags)
) )
end end
local modePatches = {
[F_SPBATK] = "SPB",
[F_SPBJUS] = "HYUD",
[F_SPBBIG] = "BIG",
[F_SPBEXP] = "INV"
}
local function modePatch(flag)
if flag == F_SPBEXP then
return PATCH[modePatches[flag]][(leveltime / 3) % 6]
end
return PATCH[modePatches[flag]]
end
local cursors = {
[1] = ". ",
[2] = " ."
}
local function marquee(text, maxwidth) 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
local pos = ((leveltime / 16) % (#text - maxwidth + shift * 2)) + 1 - shift local pos = ((leveltime / 16) % (#text - maxwidth + shift * 2)) + 1 - shift
local cursor = ""
if pos < #text - maxwidth + 1 then
cursor = cursors[((leveltime / 11) % #cursors) + 1]
end
-- The pos is the index going from -shift to #text + shift
-- It's clamped within the text boundaries ie.
-- 0 < pos < #text - maxwidth
pos = min(max(pos, 1), #text - maxwidth + 1) pos = min(max(pos, 1), #text - maxwidth + 1)
return text:sub(pos, pos + maxwidth - 1) return text:sub(pos, pos + maxwidth - 1) + cursor
end end
-- Bats on ...
local bodium = {V_YELLOWMAP, V_GRAYMAP, V_BROWNMAP, 0} local bodium = {V_YELLOWMAP, V_GRAYMAP, V_BROWNMAP, 0}
local splitColor = {[true]=V_SKYMAP, [false]=V_REDMAP}
local splitSymbol = {[true]="-", [false]="+"} local splitColor = {
[-1] = V_SKYMAP,
[0] = V_PURPLEMAP,
[1] = V_REDMAP
}
local splitSymbol = {
[-1] = "-",
[0] = "",
[1] = "+"
}
local showSplit = 0 local showSplit = 0
local VFLAGS = V_SNAPTOLEFT local VFLAGS = V_SNAPTOLEFT
local function drawScoreboard(v, player) local FACERANK_DIM = 16
if disable then return end local FACERANK_SPC = FACERANK_DIM + 4
if player != displayplayers[0] then return end local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, textVFlags)
textVFlags = textVFlags or V_HUDTRANSHALF
local me = player.name == score["name"]
cachePatches(v) --draw Patch/chili
v.draw(x, y, faceRank, V_HUDTRANS | VFLAGS, v.getColormap("sonic", score["color"]))
local m = lb[lbID(gamemap, Flags)] if me then
if m == nil then v.draw(x, y, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS)
return
end end
for i, score in ipairs(m) do -- Encore
local name = score["name"] if score["flags"] & F_ENCORE then
local skin = skins[score["skin"]] local bob = sin((leveltime + i * 5) * (ANG10))
if skin == nil then v.drawScaled(
skin = skins["sonic"] x * FRACUNIT,
end bob + (y + FACERANK_DIM / 2) * FRACUNIT,
local skinPatch = PATCH["FACERANK"][skin.name] FRACUNIT / 6,
PATCH["RUBY"],
V_HUDTRANS | VFLAGS
)
end
-- | OFFSET | + | PADDING | * |INDEX| -- SPB
local h = ((200 / 4) + 4) + (skinPatch.height + 4) * (i - 1) if score["flags"] & F_SPBATK then
local scale = FRACUNIT / 4
v.draw(4, h, skinPatch, V_HUDTRANS | VFLAGS, v.getColormap("sonic", score["color"])) drawitem(
if player.name == name then v,
v.draw(4, h, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS) x - 2,
end y - 2,
scale,
-- SPB modePatch(F_SPBATK),
if score["flags"] & F_SPBATK then V_HUDTRANS | VFLAGS
local scale = FRACUNIT / 4 )
if score["flags"] & F_SPBEXP then
drawitem( drawitem(
v, v,
4 - 2, x + FACERANK_DIM - 4,
h - 2, y - 2,
scale, scale,
PATCH["SPB"], modePatch(F_SPBEXP),
V_HUDTRANS | VFLAGS V_HUDTRANS | VFLAGS
) )
if score["flags"] & F_SPBEXP then
drawitem(
v,
skinPatch.width,
h - 2,
scale,
PATCH["INV"][(leveltime / 4) % 6],
V_HUDTRANS | VFLAGS
)
end
if score["flags"] & F_SPBBIG then
drawitem(
v,
4 - 2,
h + skinPatch.height - 4,
scale,
PATCH["BIG"],
V_HUDTRANS | VFLAGS
)
end
if score["flags"] & F_SPBJUS then
drawitem(
v,
skinPatch.width,
h + skinPatch.height - 4,
scale,
PATCH["HYUD"],
V_HUDTRANS | VFLAGS
)
end
end end
if score["flags"] & F_SPBBIG then
drawitem(
v,
x - 2,
y + FACERANK_DIM - 4,
scale,
modePatch(F_SPBBIG),
V_HUDTRANS | VFLAGS
)
end
if score["flags"] & F_SPBJUS then
drawitem(
v,
x + FACERANK_DIM - 4,
y + FACERANK_DIM - 4,
scale,
modePatch(F_SPBJUS),
V_HUDTRANS | VFLAGS
)
end
end
-- Position
if drawPos then
v.drawNum(x, y + 3, pos, textVFlags | VFLAGS)
end
-- Stats
local stat = score["stat"]
local pskin = score["skin"] and skins[score["skin"]]
if stat and not (
pskin
and pskin.kartweight == stat & MSK_WEIGHT
and pskin.kartspeed == (stat & MSK_SPEED) >> 4
) then
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
local name = score["name"]
-- Shorten long names -- Shorten long names
local stralign = "left" local stralign = "left"
local MAXWIDTH = 70 local MAXWIDTH = 70
local px = 6 local px = 2
local py = 0 local py = 0
if v.stringWidth(name) > MAXWIDTH then if v.stringWidth(name) > MAXWIDTH then
stralign = "thin" stralign = "thin"
@ -372,33 +891,156 @@ local function drawScoreboard(v, player)
end end
end end
local flashV = 0
if me and FlashTics > leveltime then
flashV = FlashVFlags[leveltime / FlashRate % (#FlashVFlags + 1)]
end
v.drawString( v.drawString(
px + skinPatch.width, x + FACERANK_DIM + px,
h + py, y + py,
name, name,
V_HUDTRANSHALF | V_ALLOWLOWERCASE | VFLAGS, textVFlags | V_ALLOWLOWERCASE | VFLAGS | flashV,
stralign stralign
) )
-- Draw splits -- Draw splits
if showSplit > 0 and score["splits"][prevLap] != nil then if showSplit and score["splits"] and score["splits"][prevLap] != nil then
local split = splits[prevLap] - score["splits"][prevLap] local split = splits[prevLap] - score["splits"][prevLap]
v.drawString( v.drawString(
px + skinPatch.width, x + px + FACERANK_DIM,
h + 8, y + 8,
splitSymbol[split < 0] + ticsToTime(abs(split)), splitSymbol[clamp(-1, split, 1)] + ticsToTime(abs(split)),
V_HUDTRANSHALF | splitColor[split < 0] | VFLAGS textVFlags | splitColor[clamp(-1, split, 1)] | VFLAGS
) )
else else
v.drawString( v.drawString(
px + skinPatch.width, x + px + FACERANK_DIM,
h + 8, y + 8,
ticsToTime(score["time"]), ticsToTime(score["time"], true),
V_HUDTRANSHALF | bodium[min(i, 4)] | VFLAGS textVFlags | bodium[min(pos, 4)] | VFLAGS | flashV
) )
end end
end end
end end
local function drawDefault(v, player, scoreTable, gui)
local yoffset = (200 / 4) + 4
local x = 4
-- Draw placeholder score
if scoreTable == nil then
drawScore(v, player, 1, x, y, gui, PATCH["NORANK"], {["name"] = UNCLAIMED, ["time"] = 0, ["flags"] = 0})
else
for pos, score in ipairs(scoreTable) do
if pos > 5 then break end
local faceRank = PATCH["FACERANK"][score.skin] or PATCH["NORANK"]
local y = yoffset + (FACERANK_SPC) * (pos - 1)
drawScore(
v, player, pos,
x, y,
gui, faceRank,
score
)
end
end
end
local function drawScroll(v, player, scoreTable, gui)
if scoreTable then
scrollY = scrollY + FixedMul(1 * FRACUNIT, scrollAcc)
local minim = -((#scoreTable - 1) * FACERANK_SPC * FRACUNIT)
local maxim = (200 - FACERANK_DIM) * FRACUNIT
scrollY = clamp(minim, scrollY, maxim)
-- Bounceback
if scrollY == minim or scrollY == maxim then
scrollAcc = -FixedMul(scrollAcc, FRACUNIT / 3)
end
local x = 10
if #scoreTable >= 10 then
x = x + 8
if #scoreTable >= 100 then
x = x + 8
end
end
local y = FixedInt(scrollY)
for pos, score in ipairs(scoreTable) do
local faceRank = PATCH["FACERANK"][score.skin] or PATCH["NORANK"]
drawScore(
v, player, pos,
x, y + ((pos - 1) * FACERANK_SPC),
gui, faceRank,
score,
true,
V_HUDTRANS
)
end
end
end
local function drawAuto(v, player, scoreTable, gui)
end
local scrollToPos = nil
local function drawScrollTo(v, player, scoreTable, gui)
drawState = DS_SCROLL
if scrollToPos == nil then return end
scrollY = (-(scrollToPos * FACERANK_SPC) + (100 - FACERANK_SPC / 2)) * FRACUNIT
scrollToPos = nil
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_BROWSER] = drawBrowser
}
-- Draw mode and return pos + 1 if success
local function drawMode(v, pos, flag)
if not (Flags & flag) then return pos end
drawitem(v, pos * 6 + 1, 194, FRACUNIT / 4, modePatch(flag), V_SNAPTOBOTTOM | V_SNAPTOLEFT)
return pos + 1
end
local function drawScoreboard(v, player)
if disable then return end
if player != displayplayers[0] then return end
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)
end
local pos = 0
-- Draw current active modes bottom left
pos = drawMode(v, pos, F_SPBJUS)
pos = drawMode(v, pos, F_SPBBIG)
pos = drawMode(v, pos, F_SPBEXP)
end
hud.add(drawScoreboard, "game") hud.add(drawScoreboard, "game")
function cachePatches(v) function cachePatches(v)
@ -410,6 +1052,8 @@ function cachePatches(v)
PATCH["CHILI"][i-1] = v.cachePatch("K_CHILI" + i) PATCH["CHILI"][i-1] = v.cachePatch("K_CHILI" + i)
end end
PATCH["NORANK"] = v.cachePatch("M_NORANK")
PATCH["FACERANK"] = {} PATCH["FACERANK"] = {}
for skin in skins.iterate do for skin in skins.iterate do
PATCH["FACERANK"][skin.name] = v.cachePatch(skin.facerank) PATCH["FACERANK"][skin.name] = v.cachePatch(skin.facerank)
@ -422,96 +1066,137 @@ function cachePatches(v)
end end
PATCH["BIG"] = v.cachePatch("K_ISGROW") PATCH["BIG"] = v.cachePatch("K_ISGROW")
PATCH["HYUD"] = v.cachePatch("K_ISHYUD") PATCH["HYUD"] = v.cachePatch("K_ISHYUD")
PATCH["RUBY"] = v.cachePatch("RUBYICON")
end end
end end
-- True if a is better than b -- Find location of player and scroll to it
local function lbComp(a, b) function scroll_to(player)
-- Calculates the difficulty, harder has higher priority local m = ScoreTable or {}
-- if s is positive then a is harder
-- if s is negative then b is harder scrollToPos = 2
-- if s is 0 then compare time for pos, score in ipairs(m) do
local s = (a["flags"] & (F_SPBEXP | F_SPBBIG)) - (b["flags"] & (F_SPBEXP | F_SPBBIG)) if player.name == score["name"] then
return s > 0 or not(s < 0 or a["time"] >= b["time"]) scrollToPos = max(2, pos - 1)
break
end
end
drawState = DS_SCRLTO
end
-- Write skin stats to each score where there are none
--local function writeStats()
-- for _, t in pairs(lb) do
-- for _, scoreTable in pairs(t) do
-- for _, score in ipairs(scoreTable) do
-- local skin = skins[score["skin"]]
-- if skin and not score["stat"] then
-- local stats = stat_t(skin.kartspeed, skin.kartweight)
-- score["stat"] = stats
-- end
-- end
-- end
-- end
--end
local function 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 end
local function saveTime(player) local function saveTime(player)
local m = lb[lbID(gamemap, Flags)] -- Disqualify if the flags changed mid trial.
if m == nil then if checkFlags(player) != Flags then
m = {} print("Game mode change detected! Time has been disqualified.")
S_StartSound(nil, 110)
return
end end
ScoreTable = $ or {}
local pskin = skins[player.mo.skin]
local newscore = score_t( local newscore = score_t(
gamemap, gamemap,
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)
) )
-- Check if you beat your previous best -- Check if you beat your previous best
for i = 1, #m do for i = 1, #ScoreTable do
if m[i]["name"] == player.name then if ScoreTable[i].name == player.name then
if lbComp(newscore, m[i]) then if not lbComp(newscore, ScoreTable[i]) then
table.remove(m, i)
S_StartSound(nil, 130)
break
else
-- You suck lol -- You suck lol
S_StartSound(nil, 201) S_StartSound(nil, 201)
FlashTics = leveltime + TICRATE * 3
FlashRate = 3
FlashVFlags = RedFlash
scroll_to(player)
return return
end end
end end
end end
print("Saving score")
table.insert(
m,
newscore
)
table.sort(m, lbComp) -- Save the record
while #m > 5 do SaveRecord(newscore, gamemap, ST_SEP)
table.remove(m)
end
lb[lbID(gamemap, Flags)] = m -- Set players text flash and play chime sfx
S_StartSound(nil, 130)
FlashTics = leveltime + TICRATE * 3
FlashRate = 1
FlashVFlags = YellowFlash
local f = assert(io.open(FILENAME, "w")) -- Reload the MapRecords
if f == nil then MapRecords = GetMapRecords(gamemap, ST_SEP)
print("Failed to open file for writing: " + FILENAME)
return
end
for k, v in pairs(lb) do -- Set the updated ScoreTable
for i = 1, #v do ScoreTable = MapRecords[Flags]
local s = v[i]
f:write(
s["map"], "\t",
s["name"], "\t",
s["skin"], "\t",
s["color"], "\t",
s["time"], "\t",
table.concat(s["splits"], " "), "\t",
s["flags"], "\n"
)
end
end
f:close() -- Scroll the gui to the player entry
scroll_to(player)
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
@ -526,29 +1211,39 @@ local function getGamer()
end end
end end
local function changeMap()
COM_BufInsertText(server, "map " + nextMap + " -force -gametype race")
nextMap = nil
end
local function think() local function think()
if nextMap then if nextMap then changeMap() end
COM_BufInsertText(server, "map " + nextMap)
nextMap = nil
end
if disable then if disable then
if ingame() > 1 then if AntiAFK then
for p in players.iterate do if not singleplayer() then
if p.valid and not p.spectator and not p.exiting then for p in players.iterate do
if p.cmd.buttons then if p.valid and not p.spectator and not p.exiting and p.lives > 0 then
p.afkTimeout = leveltime if p.cmd.buttons or p.cmd.driftturn then
end p.afkTime = leveltime
if p.afkTimeout + AFK_BALANCE < leveltime then end
p.spectator = true
chatprint("\x89" + p.name + " was moved to the other team for game balance", true) --Away from kart
if p.afkTime + AFK_BALANCE_WARN == leveltime then
chatprintf(p, "[AFK] \x89You will be moved to spectator in 10 seconds!", false)
S_StartSound(nil, 26, p)
end
if p.afkTime + AFK_BALANCE < leveltime then
p.spectator = true
chatprint("\x89" + p.name + " was moved to the other team for game balance", true)
end
end end
end end
end else
else for p in players.iterate do
for p in players.iterate do if p.valid and not p.spectator then
if p.valid and not p.spectator then p.afkTime = leveltime
p.afkTimeout = leveltime end
end end
end end
end end
@ -556,18 +1251,17 @@ local function think()
help = true help = true
return return
end end
if showSplit > 0 then
showSplit = showSplit - 1 showSplit = max(0, showSplit - 1)
end
local p = getGamer() local p = getGamer()
if leveltime < START_TIME then if leveltime < START_TIME then
-- Help message -- Help message
if leveltime == START_TIME - TICRATE * 3 then if leveltime == START_TIME - TICRATE * 3 then
if ingame() == 1 then if singleplayer() then
if help then if help then
help = false help = false
chatprint("\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats", true) chatprint(HELP_MESSAGE, true)
end end
else else
help = true help = true
@ -584,55 +1278,99 @@ local function think()
end end
end end
end end
-- Gamemode flags if leveltime > START_TIME - (3 * TICRATE) / 2 then
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
for p in players.iterate do if p then
p.SPBAKARTBIG = false p.SPBAKARTBIG = false
p.SPBAjustice = false p.SPBAjustice = false
p.SPBAshutup = false p.SPBAshutup = false
end end
end end
for p in players.iterate do
if not p.spectator then Flags = checkFlags(p)
if p.SPBAKARTBIG then
Flags = $ | F_SPBBIG -- make sure the spb actually spawned
end if server.SPBArunning and leveltime == START_TIME - 1 then
if p.SPBAjustice then if not (server.SPBAbomb and server.SPBAbomb.valid) then
Flags = $ | F_SPBJUS -- it didn't spawn, clear spb flags
end Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS)
end end
end end
else
hud.enable("freeplay")
end end
end end
local cv_teamchange = CV_FindVar("allowteamchange") ScoreTable = MapRecords[ST_SEP & Flags]
if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange")
end
if p then if p then
if p.cmd.buttons then -- must be done before browser control
p.afkTimeout = leveltime if p.laps >= mapheaderinfo[gamemap].numlaps and TimeFinished == 0 then
end TimeFinished = p.realtime
if leveltime > PREVENT_JOIN_TIME and p.afkTimeout + AFK_TIMEOUT > leveltime then
if cv_teamchange.value then
allowJoin(false)
end
elseif p.afkTimeout + AFK_TIMEOUT < leveltime then
if not cv_teamchange.value then
allowJoin(true)
end
end
if p.laps >= mapheaderinfo[gamemap].numlaps and timeFinished == 0 then
timeFinished = p.realtime
saveTime(p) saveTime(p)
end end
-- Scroll controller
-- Spectators can't input buttons so let the gamer do it
if drawState == DS_SCROLL then
if p.cmd.buttons & BT_BACKWARD then
scrollAcc = scrollAcc - FRACUNIT / 3
elseif p.cmd.buttons & BT_FORWARD then
scrollAcc = scrollAcc + FRACUNIT / 3
else
scrollAcc = FixedMul(scrollAcc, (FRACUNIT * 90) / 100)
if scrollAcc < FRACUNIT and scrollAcc > -FRACUNIT then
scrollAcc = 0
end
end
elseif drawState == DS_BROWSER then
if BrowserController(p) then
drawState = DS_DEFAULT
end
-- 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 or p.cmd.driftturn then
p.afkTime = leveltime
end
if not replayplayback then
if leveltime > PREVENT_JOIN_TIME and p.afkTime + AFK_TIMEOUT > leveltime then
if cv_teamchange.value then
allowJoin(false)
end
elseif p.afkTime + AFK_TIMEOUT < leveltime then
if not cv_teamchange.value then
allowJoin(true)
end
end
end
regLap(p) regLap(p)
elseif cv_teamchange.value == 0 then elseif cv_teamchange.value == 0 then
allowJoin(true) allowJoin(true)
@ -641,17 +1379,31 @@ end
addHook("ThinkFrame", think) addHook("ThinkFrame", think)
local function interThink() local function interThink()
if nextMap then if nextMap then changeMap() end
COM_BufInsertText(server, "map " + nextMap)
nextMap = nil if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange")
end end
if not CV_FindVar("allowteamchange").value then
if not cv_teamchange.value then
allowJoin(true) allowJoin(true)
end end
end end
addHook("IntermissionThinker", interThink) addHook("IntermissionThinker", interThink)
addHook("VoteThinker", interThink)
-- Returns the values clamed between min, max
function clamp(min_v, v, max_v)
return max(min_v, min(v, max_v))
end
local function netvars(net) local function netvars(net)
lb = net($) Flags = net($)
splits = net($)
prevLap = net($)
drawState = net($)
EncoreInitial = net($)
MapRecords = net($)
TimeFinished = net($)
end end
addHook("NetVars", netvars) addHook("NetVars", netvars)

111
tools/coldstore.py Executable file
View File

@ -0,0 +1,111 @@
#!/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)