Compare commits

..

6 Commits

Author SHA1 Message Date
e25b417fbd fix function name 2022-05-15 22:31:51 +02:00
3d54350dae Use strings as the default cvar value 2022-05-14 08:12:58 +02:00
89da89fbfe Simplify allowJoin() 2022-05-14 08:04:28 +02:00
906444ae08 change ingame() to only check if there is 2 players in game
as ingame() is only used to check if there is more than 1 player then it is changed to not look for more than 2 since its not needed. name changed to reflect that.
2022-05-14 08:02:05 +02:00
405a0e50f2 save player iteration on thinker
we know that there are no more than one player as `disable` is false and you know there is 1 if `p` exist
2022-05-14 07:41:40 +02:00
6ea3341b06 better init logic + make sure we are not in battle 2022-05-14 07:33:03 +02:00
5 changed files with 390 additions and 1603 deletions

View File

@ -1,498 +0,0 @@
local MapRecords
local maps
local mapIndex = 1
local scrollPos = 1
local modes = nil
local mode = 1
local prefMode = nil
local ModeSep
---- Imported functions ----
-- lb_common.lua
local ZoneAct = lb_ZoneAct
local TicsToTime = lb_TicsToTime
local mapChecksum = lb_map_checksum
-- 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], mapChecksum(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, mapChecksum(gamemap), ModeSep)
scrollPos = 1
updateModes()
end
rawset(_G, "InitBrowser", initBrowser)
-- initialize maps with racemaps only
local function loadMaps()
maps = {}
local hell = {}
for i = 0, #mapheaderinfo do
local map = mapheaderinfo[i]
if map and map.typeoflevel & TOL_RACE then
if map.menuflags & LF2_HIDEINMENU then
table.insert(hell, i)
else
table.insert(maps, i)
end
end
end
-- append hell maps
for _, map in ipairs(hell) do
table.insert(maps, map)
end
end
addHook("MapLoad", loadMaps)
local repeatCount = 0
local keyRepeat = 0
local function updateKeyRepeat()
S_StartSound(nil, 143)
if repeatCount < 1 then
keyRepeat = TICRATE / 4
else
keyRepeat = TICRATE / 15
end
repeatCount = $ + 1
end
local function resetKeyRepeat()
keyRepeat = 0
repeatCount = 0
end
local ValidButtons = BT_ACCELERATE | BT_BRAKE | BT_FORWARD | BT_BACKWARD | BT_DRIFT | BT_ATTACK
-- return value indicates we want to exit the browser
local function controller(player)
keyRepeat = max(0, $ - 1)
if not (player.cmd.driftturn or player.cmd.buttons) then
resetKeyRepeat()
end
local cmd = player.cmd
if not keyRepeat then
if not (cmd.buttons & ValidButtons or cmd.driftturn) then
return
end
updateKeyRepeat()
if cmd.buttons & BT_BRAKE then
S_StartSound(nil, 115)
return true
elseif cmd.buttons & BT_ACCELERATE then
COM_BufInsertText(player, "changelevel "..G_BuildMapName(maps[mapIndex]))
return true
elseif cmd.buttons & BT_ATTACK then
COM_BufInsertText(player, "encore")
elseif cmd.driftturn then
local dir = cmd.driftturn > 0 and -1 or 1
if encoremode then
updateMapIndex(-dir)
else
updateMapIndex(dir)
end
elseif cmd.buttons & BT_FORWARD then
scrollPos = $ - 1
elseif cmd.buttons & BT_BACKWARD then
scrollPos = $ + 1
elseif cmd.buttons & BT_DRIFT then
scrollPos = 1
if modes and #modes then
mode = $ % #modes + 1
prefMode = modes[mode]
end
end
end
end
rawset(_G, "BrowserController", controller)
local function netvars(net)
maps = net($)
mapIndex = net($)
modes = net($)
mode = net($)
prefMode = net($)
scrollPos = net($)
MapRecords = net($)
ModeSep = net($)
end
addHook("NetVars", netvars)

View File

@ -1,109 +0,0 @@
rawset(_G, "lb_score_t", function(map, name, skin, color, time, splits, flags, stat, checksum)
return {
["map"] = map,
["name"] = name,
["skin"] = skin,
["color"] = color,
["time"] = time,
["splits"] = splits,
["flags"] = flags,
["stat"] = stat,
["checksum"] = checksum
}
end)
rawset(_G, "lb_TicsToTime", function(tics, pure)
if tics == 0 and pure then
return "-:--:--"
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)
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
rawset(_G, "lb_map_checksum", function(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)
rawset(_G, "lb_mapnum_from_extended", function(map)
local p, q = map:upper():match("MAP(%w)(%w)$", 1)
if not (p and q) then
return nil
end
local mapnum = 0
local A = string.byte("A")
if tonumber(p) != nil then
-- Non extended map numbers
if tonumber(q) == nil then
return nil
end
mapnum = tonumber(p) * 10 + tonumber(q)
else
--Extended map numbers
p = string.byte(p) - A
local qn = tonumber(q)
if qn == nil then
qn = string.byte(q) - A + 10
end
mapnum = 36 * p + qn + 100
end
return mapnum
end)

View File

@ -1,394 +0,0 @@
-- This file handles the storage and related netvars of the leaderboard
---- Imported functions ----
-- lb_common.lua
local stat_t = lb_stat_t
local lbComp = lb_comp
local score_t = lb_score_t
local mapChecksum = lb_map_checksum
local mapnumFromExtended = lb_mapnum_from_extended
----------------------------
local LEADERBOARD_FILE = "leaderboard.txt"
local COLDSTORE_FILE = "leaderboard.coldstore.txt"
-- ColdStore are records loaded from lua addons
-- this table should never be modified outside of the AddColdStore function
local ColdStore = {}
-- Livestore are new records nad records loaded from leaderboard.txt file
local LiveStore = {}
-- parse score function
local parseScore
local MSK_SPEED = 0xF0
local MSK_WEIGHT = 0xF
local function stat_str(stat)
if stat then
return string.format("%d%d", (stat & MSK_SPEED) >> 4, stat & MSK_WEIGHT)
end
return "0"
end
local function isSameRecord(a, b, modeSep)
return a.name == b.name and (a.flags & modeSep) == (b.flags & modeSep)
end
-- insert or replace the score in dest
local function insertOrReplace(dest, score, modeSep)
for i, record in ipairs(dest) do
if isSameRecord(record, score, modeSep) then
if lbComp(score, record) then
dest[i] = score
end
return
end
end
table.insert(dest, score)
end
local function dumpStoreToFile(filename, store)
local f = assert(
io.open(filename, "w"),
"Failed to open file for writing: "..filename
)
f:setvbuf("line")
for mapid, checksums in pairs(store) do
for checksum, records in pairs(checksums) do
for _, record in ipairs(records) do
if not record.checksum or record.checksum == "" then
record.checksum = mapChecksum(record.map) or ""
end
f:write(
mapid, "\t",
record.name, "\t",
record.skin, "\t",
record.color, "\t",
record.time, "\t",
table.concat(record.splits, " "), "\t",
record.flags, "\t",
stat_str(record.stat), "\t",
record.checksum, "\n"
)
end
end
end
f:close()
end
-- GLOBAL
-- Returns a list of all maps with records
local function MapList()
local maps = {}
for mapid, checksums in pairs(ColdStore) do
maps[mapid] = $ or {}
for checksum in pairs(checksums) do
maps[mapid][checksum] = true
end
end
for mapid, checksums in pairs(LiveStore) do
maps[mapid] = $ or {}
for checksum in pairs(checksums) do
maps[mapid][checksum] = true
end
end
local maplist = {}
for mapid, checksums in pairs(maps) do
for checksum in pairs(checksums) do
table.insert(maplist, {["id"] = mapid, ["checksum"] = checksum})
end
end
table.sort(maplist, function(a, b) return a.id < b.id end)
return maplist
end
rawset(_G, "lb_map_list", MapList)
-- GLOBAL
-- Function for adding a single record from lua
local function AddColdStore(record)
ColdStore[record.map] = $ or {}
ColdStore[record.map][record.checksum] = $ or {}
table.insert(ColdStore[record.map][record.checksum], record)
end
rawset(_G, "lb_add_coldstore_record", AddColdStore)
-- GLOBAL
-- Function for adding a single record in string form from lua
local function AddColdStoreString(record)
AddColdStore(parseScore(record))
end
rawset(_G, "lb_add_coldstore_record_string", AddColdStoreString)
-- Insert mode separated records from the flat sourceTable into dest
local function insertRecords(dest, sourceTable, checksum, modeSep)
if not sourceTable then return end
if not sourceTable[checksum] then return end
local mode = nil
for _, record in ipairs(sourceTable[checksum]) do
mode = record.flags & modeSep
dest[mode] = $ or {}
table.insert(dest[mode], record)
end
end
-- GLOBAL
-- Construct the leaderboard table of the supplied mapid
-- combines the ColdStore and LiveStore records
local function GetMapRecords(map, checksum, modeSep)
local mapRecords = {}
-- Insert ColdStore records
insertRecords(mapRecords, ColdStore[map], checksum, modeSep)
-- Insert LiveStore records
insertRecords(mapRecords, LiveStore[map], checksum, modeSep)
-- Sort records
for _, records in pairs(mapRecords) do
table.sort(records, lbComp)
end
-- Remove duplicate entries
for _, records in pairs(mapRecords) do
local players = {}
local i = 1
while i <= #records do
if players[records[i].name] then
table.remove(records, i)
else
players[records[i].name] = true
i = i + 1
end
end
end
return mapRecords
end
rawset(_G, "lb_get_map_records", GetMapRecords)
-- GLOBAL
-- Save a record to the LiveStore and write to disk
-- SaveRecord will replace the record holders previous record
local function SaveRecord(score, map, modeSep)
local checksum = mapChecksum(map)
LiveStore[map] = $ or {}
LiveStore[map][checksum] = $ or {}
insertOrReplace(LiveStore[map][checksum], score, modeSep)
print("Saving score")
if isserver then
dumpStoreToFile(LEADERBOARD_FILE, LiveStore)
end
end
rawset(_G, "lb_save_record", SaveRecord)
local function netvars(net)
LiveStore = net($)
end
addHook("NetVars", netvars)
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] or ""
return score_t(
tonumber(t[1]), -- Map
t[2], -- Name
t[3], -- Skin
t[4], -- Color
tonumber(t[5]), -- Time
splits,
flags,
stats,
checksum:lower()
)
end
rawset(_G, "lb_parse_score", parseScore)
-- Read and parse a store file
local function loadStoreFile(filename)
local f = assert(
io.open(filename, "r"),
"Failed to open file for reading: "..filename
)
local store = {}
for l in f:lines() do
local score = parseScore(l)
store[score.map] = $ or {}
store[score.map][score.checksum] = $ or {}
table.insert(store[score.map][score.checksum], score)
end
f:close()
return store
end
-- GLOBAL
-- Command for moving records from one map to another
local function moveRecords(from, to, modeSep)
local function moveRecordsInStore(store)
if not (store[from.id] and store[from.id][from.checksum]) then
return 0
end
store[to.id] = $ or {}
store[to.id][to.checksum] = $ or {}
for i, score in ipairs(store[from.id][from.checksum]) do
score.map = to.id
score.checksum = to.checksum
insertOrReplace(store[to.id][to.checksum], score, modeSep)
end
-- Destroy the original table
store[from.id][from.checksum] = nil
end
-- move livestore records and write to disk
moveRecordsInStore(LiveStore)
if isserver then
dumpStoreToFile(LEADERBOARD_FILE, LiveStore)
-- move coldstore records
local ok, coldstore = pcall(loadStoreFile, COLDSTORE_FILE)
if ok and coldstore then
moveRecordsInStore(coldstore)
dumpStoreToFile(COLDSTORE_FILE, coldstore)
end
end
end
rawset(_G, "lb_move_records", moveRecords)
-- Helper function for those upgrading from 1.2 to 1.3
COM_AddCommand("lb_write_checksums", function(player)
local count = 0
local moved = {}
-- Gather movable records (no checksum, map loaded)
for map, checksums in pairs(LiveStore) do
for checksum, records in pairs(checksums) do
if checksum == "" then
local sum = mapChecksum(map)
if not sum then continue end
moved[map] = {}
moved[map][sum] = {}
for i, record in ipairs(records) do
record.checksum = sum
table.insert(moved[map][sum], record)
end
end
end
end
-- Write moved to livestore
for map, checksums in pairs(moved) do
LiveStore[map] = $ or {}
for checksum, records in pairs(checksums) do
LiveStore[map][checksum] = $ or {}
for i, score in ipairs(records) do
table.insert(LiveStore[map][checksum], score)
end
count = $ + #records
end
LiveStore[map][""] = nil
end
if isserver then
dumpStoreToFile(LEADERBOARD_FILE, LiveStore)
end
CONS_Printf(player, string.format("Successful operation on %d records", count))
end, COM_ADMIN)
COM_AddCommand("lb_known_maps", function(player, map)
local mapnum = gamemap
if map then
mapnum = mapnumFromExtended(map)
if not mapnum then
CONS_Printf(player, string.format("invalid map '%s'", map))
return
end
end
local known = {}
if LiveStore[mapnum] then
for checksum, records in pairs(LiveStore[mapnum]) do
known[checksum] = #records
end
end
if ColdStore[mapnum] then
for checksum, records in pairs(ColdStore[mapnum]) do
known[checksum] = $ or 0 + #records
end
end
CONS_Printf(player, "Map Chck Records")
for checksum, count in pairs(known) do
CONS_Printf(player, string.format("%s %s %d", G_BuildMapName(mapnum), checksum, count))
end
end)
COM_AddCommand("lb_download_live_records", function(player, filename)
if not filename then
CONS_Printf(player, "Usage: lb_download_live_records <filename>")
return
end
if filename:sub(#filename-3) != ".txt" then
filename = $..".txt"
end
dumpStoreToFile(filename, LiveStore)
end, COM_LOCAL)
-- Load the livestore
if isserver then
LiveStore = loadStoreFile(LEADERBOARD_FILE)
end

View File

@ -1,57 +1,25 @@
-- Leaderboards written by Not -- Leaderboards written by Not
-- Reusable -- Reusable
---------- Imported functions ------------- -- Leaderboard Table
-- lb_common.lua -- [mode][mapnum][scoreTable]
local ticsToTime = lb_TicsToTime local lb = {}
local zoneAct = lb_ZoneAct
local stat_t = lb_stat_t
local lbComp = lb_comp
local mapChecksum = lb_map_checksum
local score_t = lb_score_t
local mapnumFromExtended = lb_mapnum_from_extended
-- browser.lua local timeFinished = 0
local InitBrowser = InitBrowser
local DrawBrowser = DrawBrowser
local BrowserController = BrowserController
-- lb_store.lua
local GetMapRecords = lb_get_map_records
local SaveRecord = lb_save_record
local MapList = lb_map_list
local MoveRecords = lb_move_records
--------------------------------------------
-- Holds the current maps records table including all modes
local MapRecords = {}
local TimeFinished = 0
local disable = false local disable = false
local prevLap = 0 local prevLap = 0
local splits = {} local splits = {}
local PATCH = nil local PATCH = nil
local help = true local help = true
local EncoreInitial = nil local EncoreInitial = nil
local ScoreTable local cv_teamchange
local scoreTable
-- Tracks if stats have been written or not
-- Text flash on finish local StatTrack = false
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 UNCLAIMED = "Unclaimed Record"
local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records levelselect" local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore"
local FILENAME = "leaderboard.txt" local FILENAME = "leaderboard.txt"
-- Retry / changelevel map -- Retry / changelevel map
@ -73,7 +41,6 @@ local clearcheats = false
local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1 local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1
local AFK_TIMEOUT = TICRATE * 5 local AFK_TIMEOUT = TICRATE * 5
local AFK_BROWSER = TICRATE * 15
local AFK_BALANCE = TICRATE * 60 local AFK_BALANCE = TICRATE * 60
local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10 local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10
local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5 local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5
@ -87,7 +54,6 @@ local DS_DEFAULT = 0x0
local DS_SCROLL = 0x1 local DS_SCROLL = 0x1
local DS_AUTO = 0x2 local DS_AUTO = 0x2
local DS_SCRLTO = 0x4 local DS_SCRLTO = 0x4
local DS_BROWSER = 0x8
local drawState = DS_DEFAULT local drawState = DS_DEFAULT
@ -105,16 +71,14 @@ local clamp
local scroll_to local scroll_to
local ticsToTime
local allowJoin local allowJoin
---------------
-- cvars
local cv_teamchange
local cv_spbatk
local cv_gui = CV_RegisterVar({ local cv_gui = CV_RegisterVar({
name = "lb_gui", name = "lb_gui",
defaultvalue = GUI_ON, defaultvalue = "On",
flags = 0, flags = 0,
PossibleValue = {Off = GUI_OFF, Splits = GUI_SPLITS, On = GUI_ON} PossibleValue = {Off = GUI_OFF, Splits = GUI_SPLITS, On = GUI_ON}
}) })
@ -122,7 +86,7 @@ local cv_gui = CV_RegisterVar({
local AntiAFK = true local AntiAFK = true
CV_RegisterVar({ CV_RegisterVar({
name = "lb_afk", name = "lb_afk",
defaultvalue = 1, defaultvalue = "On",
flags = CV_NETVAR | CV_CALL, flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_OnOff, PossibleValue = CV_OnOff,
func = function(v) func = function(v)
@ -141,7 +105,7 @@ CV_RegisterVar({
local cv_enable = CV_RegisterVar({ local cv_enable = CV_RegisterVar({
name = "lb_enable", name = "lb_enable",
defaultvalue = 1, defaultvalue = "On",
flags = CV_NETVAR | CV_CALL, flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_OnOff, PossibleValue = CV_OnOff,
func = function(v) func = function(v)
@ -161,7 +125,7 @@ local cv_saves = CV_RegisterVar({
local cv_interrupt = CV_RegisterVar({ local cv_interrupt = CV_RegisterVar({
name = "lb_interrupt", name = "lb_interrupt",
defaultvalue = 0, defaultvalue = "Off",
flags = CV_NETVAR | CV_CALL, flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_OnOff, PossibleValue = CV_OnOff,
func = function(v) func = function(v)
@ -171,10 +135,66 @@ local cv_interrupt = CV_RegisterVar({
end 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({ local cv_spb_separate = CV_RegisterVar({
name = "lb_spb_combined", name = "lb_spb_combined",
defaultvalue = 1, defaultvalue = "On",
flags = CV_NETVAR | CV_CALL | CV_NOINIT, flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_YesNo, PossibleValue = CV_YesNo,
func = function(v) func = function(v)
if v.value then if v.value then
@ -182,48 +202,134 @@ local cv_spb_separate = CV_RegisterVar({
else else
ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP
end end
reinit_lb()
end end
}) })
local MSK_SPEED = 0xF0 local function score_t(map, name, skin, color, time, splits, flags, stat)
local MSK_WEIGHT = 0xF return {
["map"] = map,
["name"] = name,
["skin"] = skin,
["color"] = color,
["time"] = time,
["splits"] = splits,
["flags"] = flags,
["stat"] = 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
function allowJoin(v) function allowJoin(v)
if not cv_interrupt.value then if not cv_interrupt.value then
local y
if v then if v then
y = "yes" COM_BufInsertText(server, "allowteamchange Yes")
hud.enable("freeplay") hud.enable("freeplay")
else else
y = "no" COM_BufInsertText(server, "allowteamchange No")
hud.disable("freeplay") hud.disable("freeplay")
end end
COM_BufInsertText(server, "allowteamchange " + y)
end end
end end
-- Returns true if there is a single player ingame local function TwoPlusInGame()
local function singleplayer()
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 if n == 2 then
return false
end
end
end
return true return true
end end
end
end
return false
end
local function initLeaderboard(player) local function initLeaderboard(player)
if cv_enable.value and G_RaceGametype() then
if disable and leveltime < START_TIME then if disable and leveltime < START_TIME then
disable = not singleplayer() disable = TwoPlusInGame()
else else
disable = disable or not singleplayer() disable = $ or TwoPlusInGame()
end
else
disable = true
end end
disable = $ or not cv_enable.value or not (maptol & (TOL_SP | TOL_RACE))
-- Restore encore mode to initial value -- Restore encore mode to initial value
if disable and EncoreInitial != nil then if disable and EncoreInitial != nil then
@ -236,7 +342,7 @@ end
addHook("PlayerSpawn", initLeaderboard) addHook("PlayerSpawn", initLeaderboard)
local function doyoudare(player) local function doyoudare(player)
if not singleplayer() or player.spectator then if TwoPlusInGame() or player.spectator then
CONS_Printf(player, "How dare you") CONS_Printf(player, "How dare you")
return false return false
end end
@ -245,12 +351,6 @@ end
local function retry(player, ...) local function retry(player, ...)
if doyoudare(player) then if doyoudare(player) then
-- Verify valid race level
if not (mapheaderinfo[gamemap].typeoflevel & (TOL_SP | TOL_RACE)) then
CONS_Printf(player, "Battle maps are not supported")
return
end
-- Prevents bind crash -- Prevents bind crash
if leveltime < 20 then if leveltime < 20 then
return return
@ -267,65 +367,26 @@ local function exitlevel(player, ...)
end end
COM_AddCommand("exit", exitlevel) COM_AddCommand("exit", exitlevel)
local function initBrowser(player)
if not doyoudare(player) then return end
-- TODO: allow in battle
if mapheaderinfo[gamemap].typeoflevel & TOL_MATCH then
CONS_Printf(player, "Please exit battle first")
return
end
if not InitBrowser then
print("Browser is not loaded")
return
end
InitBrowser(ST_SEP)
drawState = DS_BROWSER
player.afkTime = leveltime
end
COM_AddCommand("levelselect", initBrowser)
local function findMap(player, ...) local function findMap(player, ...)
local search = ... local search = ...
if search == nil then
local hell = "\x85HELL" return
local tol = { end
[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 for i = 1, #mapheaderinfo do
map = mapheaderinfo[i] local map = mapheaderinfo[i]
if map == nil then if map == nil then
continue continue
end end
lvlttl = map.lvlttl + zoneAct(map) if map.lvlttl:lower():find(search:lower()) then
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( CONS_Printf(
player, player,
string.format( string.format(
"%s %-9s %-30s - %s\t%s", "%s - %s",
G_BuildMapName(i), G_BuildMapName(i),
lvltype, map.lvlttl
lvlttl,
map.subttl,
(map.menuflags & LF2_HIDEINMENU and hell) or ""
) )
) )
end end
@ -333,100 +394,11 @@ local function findMap(player, ...)
end end
COM_AddCommand("findmap", findMap) COM_AddCommand("findmap", findMap)
local SPBModeSym = { local function mapNotExists(player, map)
[F_SPBEXP] = "X", CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper()))
[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 end
return modestr local ALPH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
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, mapChecksum(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
@ -442,19 +414,32 @@ local function changelevel(player, ...)
return return
end end
local mapnum = mapnumFromExtended(map) local p, q = map:upper():match("MAP(%w)(%w)$", 1)
if not mapnum then if not (p and q) then
CONS_Printf(player, string.format("Invalid map name: %s", map)) CONS_Printf(player, string.format("Invalid map name: %s", map))
end
if mapheaderinfo[mapnum] == nil then
CONS_Printf(player, string.format("Map doesn't exist: %s", map:upper()))
return return
end end
-- Verify valid race level local mapnum = 0
if not (mapheaderinfo[mapnum].typeoflevel & (TOL_SP | TOL_RACE)) then if tonumber(p) != nil then
CONS_Printf(player, "Battle maps are not supported") -- Non extended map numbers
if tonumber(q) == nil then
mapNotExists(player, map)
return
end
mapnum = tonumber(p) * 10 + tonumber(q)
else
--Extended map numbers
p = ALPH:find(p) - 1
local qn = tonumber(q)
if qn == nil then
qn = ALPH:find(q) + 9
end
mapnum = 36 * p + qn + 100
end
if mapheaderinfo[mapnum] == nil then
mapNotExists(player, map)
return return
end end
@ -523,26 +508,21 @@ local function findRival(player, ...)
local totalScores = 0 local totalScores = 0
local totalDiff = 0 local totalDiff = 0
CONS_Printf(player, string.format("\x89%s's times:", rival)) CONS_Printf(player, string.format("\x89%s's times:", rival))
CONS_Printf(player, "MAP CHCK Time Diff Mode") CONS_Printf(player, "MAP Time Diff Mode")
local maplist = MapList() for mode, tbl in pairs(lb) do
local mapRecords scores[mode] = {}
local rivalScore
local yourScore
for i = 1, #maplist do
mapRecords = GetMapRecords(maplist[i].id, maplist[i].checksum, ST_SEP)
for mode, records in pairs(mapRecords) do for map, scoreTable in pairs(tbl) do
scores[mode] = $ or {} local rivalScore = nil
local yourScore = nil
rivalScore = nil for _, score in pairs(scoreTable) do
yourScore = nil if score["name"] == player.name then
for _, score in ipairs(records) do
if score.name == player.name then
yourScore = score yourScore = score
elseif score.name == rival then elseif score["name"] == rival then
rivalScore = score rivalScore = score
end end
@ -552,7 +532,7 @@ local function findRival(player, ...)
end end
if rivalScore and yourScore then if rivalScore and yourScore then
totalDiff = totalDiff + yourScore.time - rivalScore.time totalDiff = totalDiff + yourScore["time"] - rivalScore["time"]
end end
if rivalScore then if rivalScore then
@ -560,8 +540,8 @@ local function findRival(player, ...)
table.insert( table.insert(
scores[mode], scores[mode],
{ {
rival = rivalScore, ["rival"] = rivalScore,
your = yourScore ["your"] = yourScore
} }
) )
end end
@ -576,6 +556,12 @@ local function findRival(player, ...)
return a["rival"]["map"] < b["rival"]["map"] return a["rival"]["map"] < b["rival"]["map"]
end end
local modestrings = {
[F_SPBEXP] = "X",
[F_SPBBIG] = "B",
[F_SPBJUS] = "J",
}
for mode, tbl in pairs(scores) do for mode, tbl in pairs(scores) do
if i >= stop then break end if i >= stop then break end
@ -589,7 +575,15 @@ local function findRival(player, ...)
if i >= stop then break end if i >= stop then break end
i = i + 1 i = i + 1
local modestr = modeToString(score["rival"]["flags"]) local modestr = "TA"
if score["rival"]["flags"] & F_SPBATK then
modestr = "SPB"
for k, v in pairs(modestrings) do
if score["rival"]["flags"] & k then
modestr = $ + v
end
end
end
if score["your"] then if score["your"] then
local diff = score["your"]["time"] - score["rival"]["time"] local diff = score["your"]["time"] - score["rival"]["time"]
@ -598,10 +592,9 @@ local function findRival(player, ...)
CONS_Printf( CONS_Printf(
player, player,
string.format( string.format(
"%s %4s %8s %s%9s \x80%s", "%s %8s %s%9s \x80%s",
G_BuildMapName(score.rival.map), G_BuildMapName(score["rival"]["map"]),
score.rival.checksum, ticsToTime(score["rival"]["time"]),
ticsToTime(score.rival.time),
color, color,
sym[diff<0] + ticsToTime(abs(diff)), sym[diff<0] + ticsToTime(abs(diff)),
modestr modestr
@ -611,10 +604,9 @@ local function findRival(player, ...)
CONS_Printf( CONS_Printf(
player, player,
string.format( string.format(
"%s %4s %8s %9s %s", "%s %8s %9s %s",
G_BuildMapName(score.rival.map), G_BuildMapName(score["rival"]["map"]),
score.rival.checksum, ticsToTime(score["rival"]["time"]),
ticsToTime(score.rival.time),
ticsToTime(0, true), ticsToTime(0, true),
modestr modestr
) )
@ -644,64 +636,6 @@ local function findRival(player, ...)
end end
COM_AddCommand("rival", findRival) COM_AddCommand("rival", findRival)
local function moveRecords(player, from_map, from_checksum, to_map, to_checksum)
if not(from_map and from_checksum and to_map) then
CONS_Printf(player, "Usage: lb_move_records <from_map> <from_checksum> <to_map> [<to_checksum>]")
CONS_Printf(
player,
string.format(
"Summary: Move records from one map to another.\n"..
"If no <to_checksum> is supplied then the checksum of the current loaded map %s is used.\n"..
"Hint: Use lb_known_maps to find checksums",
to_map or "<to_map>"
)
)
return
end
local from = {
["id"] = mapnumFromExtended(from_map),
["checksum"] = from_checksum:lower()
}
local to = {
["id"] = mapnumFromExtended(to_map),
}
to.checksum = to_checksum or mapChecksum(to.id)
if not to.checksum then
CONS_Printf(player, string.format("error: %s is not loaded; provide to_checksum to continue", to_map:upper()))
return
end
if #to.checksum != 4 or to.checksum:match("[^a-f0-9]") then
CONS_Printf(player, string.format("error: %s is an invalid checksum; checksums are of length 4 and can contain only 0-9a-f", to.checksum))
return
end
to.checksum = $:lower()
local mapRecords = GetMapRecords(from.id, from.checksum, F_SPBATK | F_SPBBIG | F_SPBEXP)
local recordCount = 0
for mode, records in pairs(mapRecords) do
recordCount = $ + #records
end
MoveRecords(from, to, ST_SEP)
CONS_Printf(
player,
string.format(
"%d records have been moved from\x82 %s %s\x80 to\x88 %s %s",
recordCount,
from_map, from.checksum,
to_map, to.checksum
)
)
CONS_Printf(player, "Please repack coldstore and restart the server for changes to take effect.")
end
COM_AddCommand("lb_move_records", moveRecords, COM_ADMIN)
--DEBUGGING --DEBUGGING
--local function printTable(tb) --local function printTable(tb)
-- for mode, tbl in pairs(tb) do -- for mode, tbl in pairs(tb) do
@ -725,21 +659,31 @@ COM_AddCommand("lb_move_records", moveRecords, COM_ADMIN)
--end --end
addHook("MapLoad", function() addHook("MapLoad", function()
TimeFinished = 0 timeFinished = 0
splits = {} splits = {}
prevLap = 0 prevLap = 0
drawState = DS_DEFAULT drawState = DS_DEFAULT
scrollY = 50 * FRACUNIT scrollY = 50 * FRACUNIT
scrollAcc = 0 scrollAcc = 0
FlashTics = 0
allowJoin(true) allowJoin(true)
--printTable(lb) --printTable(lb)
MapRecords = GetMapRecords(gamemap, mapChecksum(gamemap), ST_SEP)
end end
) )
function ticsToTime(tics, pure)
if tics == 0 and pure then
return "-:--:--"
end
return string.format(
"%d:%02d:%02d",
G_TicsToMinutes(tics, true),
G_TicsToSeconds(tics),
G_TicsToCentiseconds(tics)
)
end
-- Item patches have the amazing property of being displaced 12x 13y pixels -- Item patches have the amazing property of being displaced 12x 13y pixels
local iXoffset = 13 * FRACUNIT local iXoffset = 13 * FRACUNIT
local iYoffset = 12 * FRACUNIT local iYoffset = 12 * FRACUNIT
@ -762,7 +706,7 @@ local modePatches = {
local function modePatch(flag) local function modePatch(flag)
if flag == F_SPBEXP then if flag == F_SPBEXP then
return PATCH[modePatches[flag]][(leveltime / 3) % 6] return PATCH[modePatches[flag]][(leveltime / 4) % 6]
end end
return PATCH[modePatches[flag]] return PATCH[modePatches[flag]]
end end
@ -813,11 +757,10 @@ local FACERANK_DIM = 16
local FACERANK_SPC = FACERANK_DIM + 4 local FACERANK_SPC = FACERANK_DIM + 4
local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, textVFlags) local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, textVFlags)
textVFlags = textVFlags or V_HUDTRANSHALF textVFlags = textVFlags or V_HUDTRANSHALF
local me = player.name == score["name"]
--draw Patch/chili --draw Patch/chili
v.draw(x, y, faceRank, V_HUDTRANS | VFLAGS, v.getColormap("sonic", score["color"])) v.draw(x, y, faceRank, V_HUDTRANS | VFLAGS, v.getColormap("sonic", score["color"]))
if me then if player.name == score["name"] then
v.draw(x, y, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS) v.draw(x, y, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS | VFLAGS)
end end
@ -829,7 +772,7 @@ local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, te
bob + (y + FACERANK_DIM / 2) * FRACUNIT, bob + (y + FACERANK_DIM / 2) * FRACUNIT,
FRACUNIT / 6, FRACUNIT / 6,
PATCH["RUBY"], PATCH["RUBY"],
V_HUDTRANS | VFLAGS V_HUDTRANS
) )
end end
@ -886,11 +829,11 @@ local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, te
local pskin = score["skin"] and skins[score["skin"]] local pskin = score["skin"] and skins[score["skin"]]
if stat and not ( if stat and not (
pskin pskin
and pskin.kartweight == stat & MSK_WEIGHT and pskin.kartweight == stat["weight"]
and pskin.kartspeed == (stat & MSK_SPEED) >> 4 and pskin.kartspeed == stat["speed"]
) then ) then
v.drawString(x + FACERANK_DIM - 2, y + 4, (stat & MSK_SPEED) >> 4, V_HUDTRANS | VFLAGS, "small") v.drawString(x + FACERANK_DIM - 2, y + 4, stat["speed"], V_HUDTRANS | VFLAGS, "small")
v.drawString(x + FACERANK_DIM - 2, y + 8, stat & MSK_WEIGHT, V_HUDTRANS | VFLAGS, "small") v.drawString(x + FACERANK_DIM - 2, y + 8, stat["weight"], V_HUDTRANS | VFLAGS, "small")
end end
if gui == GUI_ON or (gui == GUI_SPLITS and showSplit) then if gui == GUI_ON or (gui == GUI_SPLITS and showSplit) then
@ -913,16 +856,11 @@ local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, te
end end
end end
local flashV = 0
if me and FlashTics > leveltime then
flashV = FlashVFlags[leveltime / FlashRate % (#FlashVFlags + 1)]
end
v.drawString( v.drawString(
x + FACERANK_DIM + px, x + FACERANK_DIM + px,
y + py, y + py,
name, name,
textVFlags | V_ALLOWLOWERCASE | VFLAGS | flashV, textVFlags | V_ALLOWLOWERCASE | VFLAGS,
stralign stralign
) )
@ -940,7 +878,7 @@ local function drawScore(v, player, pos, x, y, gui, faceRank, score, drawPos, te
x + px + FACERANK_DIM, x + px + FACERANK_DIM,
y + 8, y + 8,
ticsToTime(score["time"], true), ticsToTime(score["time"], true),
textVFlags | bodium[min(pos, 4)] | VFLAGS | flashV textVFlags | bodium[min(pos, 4)] | VFLAGS
) )
end end
end end
@ -1020,16 +958,11 @@ local function drawScrollTo(v, player, scoreTable, gui)
drawScroll(v, player, scoreTable, gui) drawScroll(v, player, scoreTable, gui)
end end
local function drawBrowser(v, player)
DrawBrowser(v, player)
end
local stateFunctions = { local stateFunctions = {
[DS_DEFAULT] = drawDefault, [DS_DEFAULT] = drawDefault,
[DS_SCROLL] = drawScroll, [DS_SCROLL] = drawScroll,
[DS_AUTO] = drawAuto, [DS_AUTO] = drawAuto,
[DS_SCRLTO] = drawScrollTo, [DS_SCRLTO] = drawScrollTo
[DS_BROWSER] = drawBrowser
} }
-- Draw mode and return pos + 1 if success -- Draw mode and return pos + 1 if success
@ -1046,15 +979,14 @@ local function drawScoreboard(v, player)
cachePatches(v) cachePatches(v)
local gui = cv_gui.value or drawState == DS_BROWSER local gui = cv_gui.value
-- Force enable gui at start and end of the race
if leveltime < START_TIME or player.exiting or player.lives == 0 then if leveltime < START_TIME or player.exiting or player.lives == 0 then
gui = GUI_ON gui = GUI_ON
end end
if gui then if gui then
stateFunctions[drawState](v, player, ScoreTable, gui) stateFunctions[drawState](v, player, scoreTable, gui)
end
local pos = 0 local pos = 0
-- Draw current active modes bottom left -- Draw current active modes bottom left
@ -1062,7 +994,6 @@ local function drawScoreboard(v, player)
pos = drawMode(v, pos, F_SPBBIG) pos = drawMode(v, pos, F_SPBBIG)
pos = drawMode(v, pos, F_SPBEXP) pos = drawMode(v, pos, F_SPBEXP)
end end
end
hud.add(drawScoreboard, "game") hud.add(drawScoreboard, "game")
function cachePatches(v) function cachePatches(v)
@ -1094,7 +1025,7 @@ end
-- Find location of player and scroll to it -- Find location of player and scroll to it
function scroll_to(player) function scroll_to(player)
local m = ScoreTable or {} local m = scoreTable or {}
scrollToPos = 2 scrollToPos = 2
for pos, score in ipairs(m) do for pos, score in ipairs(m) do
@ -1108,59 +1039,23 @@ function scroll_to(player)
end end
-- Write skin stats to each score where there are none -- Write skin stats to each score where there are none
--local function writeStats() local function writeStats()
-- for _, t in pairs(lb) do for _, t in pairs(lb) do
-- for _, scoreTable in pairs(t) do for _, scoreTable in pairs(t) do
-- for _, score in ipairs(scoreTable) do for _, score in ipairs(scoreTable) do
-- local skin = skins[score["skin"]] local skin = skins[score["skin"]]
-- if skin and not score["stat"] then if skin and not score["stat"] then
-- local stats = stat_t(skin.kartspeed, skin.kartweight) local stats = stat_t(skin.kartspeed, skin.kartweight)
-- score["stat"] = stats score["stat"] = stats
-- end end
-- end end
-- end
-- end
--end
local function checkFlags(p)
local flags = 0
-- Encore
if encoremode then
flags = $ | F_ENCORE
end
if not cv_spbatk then
cv_spbatk = CV_FindVar("spbatk")
end
-- SPBAttack
if server.SPBArunning and cv_spbatk.value then
flags = $ | F_SPBATK
if server.SPBAexpert then
flags = $ | F_SPBEXP
end
if p.SPBAKARTBIG then
flags = $ | F_SPBBIG
end
if p.SPBAjustice then
flags = $ | F_SPBJUS
end end
end end
return flags
end end
local function saveTime(player) local function saveTime(player)
-- Disqualify if the flags changed mid trial.
if checkFlags(player) != Flags then
print("Game mode change detected! Time has been disqualified.")
S_StartSound(nil, 110)
return
end
ScoreTable = $ or {} scoreTable = $ or {}
local pskin = skins[player.mo.skin] local pskin = skins[player.mo.skin]
local newscore = score_t( local newscore = score_t(
@ -1168,58 +1063,84 @@ local function saveTime(player)
player.name, player.name,
player.mo.skin, player.mo.skin,
player.skincolor, player.skincolor,
TimeFinished, timeFinished,
splits, splits,
Flags, Flags,
stat_t(player.HMRs or pskin.kartspeed, player.HMRw or pskin.kartweight), stat_t(player.HMRs or pskin.kartspeed, player.HMRw or pskin.kartweight)
mapChecksum(gamemap)
) )
-- Check if you beat your previous best -- Check if you beat your previous best
for i = 1, #ScoreTable do for i = 1, #scoreTable do
if ScoreTable[i].name == player.name then if scoreTable[i]["name"] == player.name then
if not lbComp(newscore, ScoreTable[i]) then if lbComp(newscore, scoreTable[i]) then
table.remove(scoreTable, i)
S_StartSound(nil, 130)
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) scroll_to(player)
return return
end end
end end
end end
print("Saving score")
table.insert(
scoreTable,
newscore
)
-- Save the record table.sort(scoreTable, lbComp)
SaveRecord(newscore, gamemap, ST_SEP) while #scoreTable > cv_saves.value do
table.remove(scoreTable)
end
-- Set players text flash and play chime sfx
S_StartSound(nil, 130)
FlashTics = leveltime + TICRATE * 3
FlashRate = 1
FlashVFlags = YellowFlash
-- Reload the MapRecords
MapRecords = GetMapRecords(gamemap, mapChecksum(gamemap), ST_SEP)
-- Set the updated ScoreTable
ScoreTable = MapRecords[Flags]
-- Scroll the gui to the player entry
scroll_to(player) scroll_to(player)
setScoreTable(gamemap, Flags, scoreTable)
if not StatTrack then
writeStats()
StatTrack = true
end
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 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
@ -1234,20 +1155,18 @@ local function getGamer()
end end
end end
local function changeMap() local function think()
COM_BufInsertText(server, "map " + nextMap + " -force -gametype race") if nextMap then
COM_BufInsertText(server, "map " + nextMap)
nextMap = nil nextMap = nil
end end
local function think()
if nextMap then changeMap() end
if disable then if disable then
if AntiAFK then if AntiAFK then
if not singleplayer() then if TwoPlusInGame() then
for p in players.iterate do for p in players.iterate do
if p.valid and not p.spectator and not p.exiting and p.lives > 0 then if p.valid and not p.spectator and not p.exiting and p.lives > 0 then
if p.cmd.buttons or p.cmd.driftturn then if p.cmd.buttons then
p.afkTime = leveltime p.afkTime = leveltime
end end
@ -1281,7 +1200,7 @@ local function think()
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 singleplayer() then if p then
if help then if help then
help = false help = false
chatprint(HELP_MESSAGE, true) chatprint(HELP_MESSAGE, true)
@ -1292,7 +1211,13 @@ local function think()
end end
-- Autospec -- Autospec
-- Encore
if leveltime == 1 then if leveltime == 1 then
Flags = $ & !F_ENCORE
if encoremode then
Flags = $ | F_ENCORE
end
if p then if p then
for s in players.iterate do for s in players.iterate do
if s.valid and s.spectator then if s.valid and s.spectator then
@ -1302,43 +1227,44 @@ local function think()
end end
end end
if leveltime > START_TIME - (3 * TICRATE) / 2 then -- Gamemode flags
Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS)
if leveltime > START_TIME - (3 * TICRATE) / 2 and server.SPBArunning then
Flags = $ | F_SPBATK
if server.SPBAexpert then
Flags = $ | F_SPBEXP
end
if clearcheats then if clearcheats then
clearcheats = false clearcheats = false
for q in players.iterate do
q.SPBAKARTBIG = false
q.SPBAjustice = false
q.SPBAshutup = false
end
end
if p then if p then
p.SPBAKARTBIG = false if p.SPBAKARTBIG then
p.SPBAjustice = false Flags = $ | F_SPBBIG
p.SPBAshutup = false end
if p.SPBAjustice then
Flags = $ | F_SPBJUS
end end
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 end
end if not (Flags & F_SPBATK) then
else
hud.enable("freeplay") hud.enable("freeplay")
end end
end end
ScoreTable = MapRecords[ST_SEP & Flags] scoreTable = getScoreTable(gamemap, Flags)
if not cv_teamchange then if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange") cv_teamchange = CV_FindVar("allowteamchange")
end end
if p then if p then
-- must be done before browser control
if p.laps >= mapheaderinfo[gamemap].numlaps and TimeFinished == 0 then
TimeFinished = p.realtime
saveTime(p)
end
-- Scroll controller -- Scroll controller
-- Spectators can't input buttons so let the gamer do it -- Spectators can't input buttons so let the gamer do it
if drawState == DS_SCROLL then if drawState == DS_SCROLL then
@ -1352,33 +1278,13 @@ local function think()
scrollAcc = 0 scrollAcc = 0
end end
end end
elseif drawState == DS_BROWSER then
if BrowserController(p) then
drawState = DS_DEFAULT
end end
-- prevent intermission while browsing if p.lives == 0 then
if p.exiting then
p.exiting = $ + 1
end
-- disable spba hud
if server.SPBArunning and server.SPBAdone then
server.SPBArunning = false
p.pflags = $ & !(PF_TIMEOVER)
p.exiting = 100
end
-- prevent softlocking the server
if p.afkTime + AFK_BROWSER < leveltime then
drawState = DS_DEFAULT
S_StartSound(nil, 100)
end
elseif p.lives == 0 then
drawState = DS_SCROLL drawState = DS_SCROLL
end end
if p.cmd.buttons or p.cmd.driftturn then if p.cmd.buttons then
p.afkTime = leveltime p.afkTime = leveltime
end end
@ -1394,6 +1300,10 @@ local function think()
end end
end end
if p.laps >= mapheaderinfo[gamemap].numlaps and timeFinished == 0 then
timeFinished = p.realtime
saveTime(p)
end
regLap(p) regLap(p)
elseif cv_teamchange.value == 0 then elseif cv_teamchange.value == 0 then
allowJoin(true) allowJoin(true)
@ -1402,7 +1312,10 @@ end
addHook("ThinkFrame", think) addHook("ThinkFrame", think)
local function interThink() local function interThink()
if nextMap then changeMap() end if nextMap then
COM_BufInsertText(server, "map " + nextMap)
nextMap = nil
end
if not cv_teamchange then if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange") cv_teamchange = CV_FindVar("allowteamchange")
@ -1425,8 +1338,8 @@ local function netvars(net)
splits = net($) splits = net($)
prevLap = net($) prevLap = net($)
drawState = net($) drawState = net($)
StatTrack = net($)
EncoreInitial = net($) EncoreInitial = net($)
MapRecords = net($) lb = net($)
TimeFinished = net($)
end end
addHook("NetVars", netvars) addHook("NetVars", netvars)

View File

@ -1,125 +0,0 @@
#!/usr/bin/env python3
import sys
from os import path
linesep = "\n"
if len(sys.argv) != 3 or not sys.argv[1] or not sys.argv[2]:
print("Usage: coldstore.py <game_directory> <leaderboard_records.lua>")
print("\t<game_directory>\t\tthe game directory where wads and luafiles reside. Usually at '$HOME/.srb2kart'.")
print("\t<leaderboard_records.lua>\tthe output name for the records packed lua file. It will be saved within <game_directory>.")
quit()
if not sys.argv[2].endswith(".lua"):
print("{} must end with .lua".format(sys.argv[2]))
quit()
game_dir = sys.argv[1]
leaderboard_txt = path.join(game_dir, "luafiles", "leaderboard.txt")
coldstore_txt = path.join(game_dir, "luafiles", "leaderboard.coldstore.txt")
records_lua = path.join(game_dir, sys.argv[2])
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["checksum"] == b["checksum"] and (a["flags"] & F_SEP) == (b["flags"] & F_SEP)
def LoadRecordsFromFile(path):
records = []
try:
with open(path, "r") as f:
for line in f.readlines():
line = line.strip()
if line != "":
records.append(ParseScore(line))
except FileNotFoundError:
pass
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)