Compare commits

...

55 Commits

Author SHA1 Message Date
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
Not
70cd579079 v1.2.14 2022-03-28 13:14:08 +02:00
Not
e68ab38262 v1.2.13 2022-03-28 13:13:42 +02:00
Not
f1a886182c v1.2.12 2022-03-28 13:13:21 +02:00
Not
017794c554 v1.2.11 2022-03-28 13:13:00 +02:00
Not
2e5c209a65 v1.2.10 2022-03-28 13:12:43 +02:00
Not
3101d29dcc v1.2.9 2022-03-28 13:12:16 +02:00
Not
36134572af v1.2.8 2022-03-28 13:11:51 +02:00
Not
bc002667c8 v1.2.7 2022-03-28 13:11:21 +02:00
Not
b0d5b5abeb v1.2.6 2022-03-28 13:11:00 +02:00
Not
18a81fab6d v1.2.5 2022-03-28 13:10:43 +02:00
Not
9a1fb0677a v1.2.4 2022-03-28 13:10:13 +02:00
2 changed files with 1262 additions and 216 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/

View File

@ -1,14 +1,46 @@
-- Leaderboards written by Not -- Leaderboards written by Not
-- Reusable
local FILENAME = "leaderboard.txt" -- Leaderboard Table
-- [mode][mapnum][scoreTable]
local lb = {} local lb = {}
local timeFinished = 0 local timeFinished = 0
local disable = true local disable = false
local prevLap = 0 local prevLap = 0
local splits = {} local splits = {}
local PATCH = nil local PATCH = nil
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
}
-- Tracks if stats have been written or not
local StatTrack = false
local UNCLAIMED = "Unclaimed Record"
local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records"
local FILENAME = "leaderboard.txt"
-- Retry / changelevel map
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
@ -16,23 +48,184 @@ 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 startTime = 6 * TICRATE + (3 * TICRATE / 4) local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1
local AFK_TIMEOUT = TICRATE * 5
local AFK_BALANCE = TICRATE * 60
local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10
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 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 ticsToTime
local allowJoin
---------------
-- 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 end
return id AntiAFK = true
else
AntiAFK = false
end
end
})
local cv_enable = CV_RegisterVar({
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 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 function setST(t, map, flags, scoreTable)
local mode = flags & ST_SEP
t[mode] = t[mode] or {}
t[mode][map] = scoreTable
end end
local function score_t(map, name, skin, color, time, splits, flags) local function getST(t, map, flags)
local mode = flags & ST_SEP
return t[mode] and t[mode][map] or nil
end
local function setScoreTable(map, flags, scoreTable)
setST(lb, map, flags, scoreTable)
end
local function getScoreTable(map, flags)
return getST(lb, map, flags)
end
-- True if a is better than b
local function lbComp(a, b)
-- Calculates the difficulty, harder has higher priority
-- if s is positive then a is harder
-- if s is negative then b is harder
-- if s is 0 then compare time
local s = (a["flags"] & (F_SPBEXP | F_SPBBIG)) - (b["flags"] & (F_SPBEXP | F_SPBBIG))
return s > 0 or not(s < 0 or a["time"] >= b["time"])
end
local function sortScores()
for mode, t in pairs(lb) do
for map, scoreTable in pairs(t) do
table.sort(scoreTable, lbComp)
setScoreTable(map, mode, scoreTable)
end
end
end
-- Reinitialize lb based on new ST_SEP value
local function reinit_lb()
local nlb = {}
for mode, t in pairs(lb) do
for map, scoreTable in pairs(t) do
for i, score in ipairs(scoreTable) do
local st = getST(nlb, map, score["flags"]) or {}
table.insert(st, score)
setST(nlb, map, score["flags"], st)
end
end
end
lb = nlb
sortScores()
end
local cv_spb_separate = CV_RegisterVar({
name = "lb_spb_combined",
defaultvalue = 1,
flags = CV_NETVAR | CV_CALL,
PossibleValue = CV_YesNo,
func = function(v)
if v.value then
ST_SEP = F_SPBATK
else
ST_SEP = F_SPBATK | F_SPBBIG | F_SPBEXP
end
reinit_lb()
end
})
local function score_t(map, name, skin, color, time, splits, flags, stat)
return { return {
["map"] = map, ["map"] = map,
["name"] = name, ["name"] = name,
@ -40,16 +233,36 @@ 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
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 -- Read the leaderboard
local f = io.open(FILENAME, "r") if isserver then
if f then local f = io.open(FILENAME, "r")
if f then
for l in f:lines() do for l in f:lines() do
-- Leaderboard is stored in the following tab separated format -- Leaderboard is stored in the following tab separated format
-- name, skin, color, time, splits, flags -- mapnum, name, skin, color, time, splits, flags, stat
local t = {} local t = {}
for word in (l+"\t"):gmatch("(.-)\t") do for word in (l+"\t"):gmatch("(.-)\t") do
table.insert(t, word) table.insert(t, word)
@ -60,10 +273,7 @@ if f then
flags = tonumber(t[7]) flags = tonumber(t[7])
end end
local lbt = lb[lbID(t[1], flags)] scoreTable = getScoreTable(tonumber(t[1]), flags) or {}
if lbt == nil then
lbt = {}
end
local spl = {} local spl = {}
if t[6] != nil then if t[6] != nil then
@ -72,61 +282,346 @@ if f then
end end
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( table.insert(
lbt, scoreTable,
score_t( score_t(
t[1], tonumber(t[1]),
t[2], t[2],
t[3], t[3],
t[4], t[4],
tonumber(t[5]), tonumber(t[5]),
spl, spl,
flags flags,
stats
) )
) )
lb[lbID(t[1], flags)] = lbt
setScoreTable(tonumber(t[1]), flags, scoreTable)
end end
sortScores()
f:close() f:close()
else else
print("Failed to open file: ", FILENAME) print("Failed to open file: ", FILENAME)
end
end
function allowJoin(v)
if not cv_interrupt.value then
local y
if v then
y = "yes"
hud.enable("freeplay")
else
y = "no"
hud.disable("freeplay")
end
COM_BufInsertText(server, "allowteamchange " + y)
end
end
-- Returns true if there is a single player ingame
local function singleplayer()
local n = 0
for p in players.iterate do
if p.valid and not p.spectator then
n = $ + 1
if n > 1 then
return false
end
end
end
return true
end end
local function initLeaderboard(player) local function initLeaderboard(player)
local ingame = 0 if disable and leveltime < START_TIME then
for p in players.iterate do disable = not singleplayer()
if p.valid and not p.spectator then else
ingame = ingame + 1 disable = disable or not singleplayer()
end end
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 end
disable = ingame > 1 player.afkTime = leveltime
if disable then
--print("To many players in game, leaderboard has been disabled")
return
end
end end
addHook("PlayerSpawn", initLeaderboard) addHook("PlayerSpawn", initLeaderboard)
local function retry(player, ...) local function doyoudare(player)
if disable 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
end
return true
end
local function retry(player, ...)
if doyoudare(player) then
-- Prevents bind crash
if leveltime < 20 then
return return
end end
nextMap = G_BuildMapName(gamemap)
COM_BufInsertText(server, "map " + G_BuildMapName(gamemap)) end
end end
COM_AddCommand("retry", retry) COM_AddCommand("retry", retry)
local function exitlevel(player, ...) local function exitlevel(player, ...)
if disable or player.spectator then if doyoudare(player) then
CONS_Printf(player, "How dare you")
return
end
G_ExitLevel() G_ExitLevel()
end
end end
COM_AddCommand("exit", exitlevel) COM_AddCommand("exit", exitlevel)
local function zoneAct(map)
local z = ""
if map.zonttl != "" then
z = " " + map.zonttl
elseif not(map.levelflags & LF_NOZONE) then
z = " Zone"
end
if map.actnum != "" then
z = $ + " " + map.actnum
end
return z
end
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
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
if mapid then
mapnum = mapnumFromExtended(mapid)
if not mapnum then
CONS_Printf(player, string.format("Invalid map name: %s", mapid))
return
end
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, maps in pairs(lb) do
local maptbl = maps[mapnum]
if not maptbl then continue end
CONS_Printf(player, "")
CONS_Printf(player, modeToString(mode))
-- don't print flags for time attack
if mode then
for i, tbl in ipairs(maptbl) do
CONS_Printf(
player,
string.format(
"%2d %-21s \x89%8s \x80%s",
i,
tbl["name"],
ticsToTime(tbl["time"]),
modeToString(tbl["flags"])
)
)
end
else
for i, tbl in ipairs(maptbl) do
CONS_Printf(
player,
string.format(
"%2d %-21s \x89%8s",
i,
tbl["name"],
ticsToTime(tbl["time"])
)
)
end
end
end
end
COM_AddCommand("records", records)
local function changelevel(player, ...)
if not doyoudare(player) then
return
end
if leveltime < 20 then
return
end
local map = ...
if map == nil then
CONS_Printf(player, "Usage: changelevel MAPXX")
return
end
local mapnum = mapnumFromExtended(map)
if not mapnum then
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
end
-- Verify valid race level
if not (mapheaderinfo[mapnum].typeoflevel & (TOL_SP | TOL_RACE)) then
CONS_Printf(player, "Battle maps are not supported")
return
end
nextMap = G_BuildMapName(mapnum)
end
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
@ -135,23 +630,173 @@ 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")
for mode, tbl in pairs(lb) do
scores[mode] = {}
for map, scoreTable in pairs(tbl) do
local rivalScore = nil
local yourScore = nil
for _, score in pairs(scoreTable) 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(
-- v[i]["name"],
-- v[i]["skin"],
-- v[i]["color"],
-- v[i]["time"],
-- table.concat(v[i]["splits"]),
-- v[i]["flags"],
-- ","
-- )
-- --
-- end -- --if scoreTable != nil then
-- -- print(
-- -- v[i]["name"],
-- -- v[i]["skin"],
-- -- v[i]["color"],
-- -- v[i]["time"],
-- -- table.concat(v[i]["splits"]),
-- -- v[i]["flags"],
-- -- ","
-- -- )
-- --
-- --end
-- end -- end
-- end -- end
--end --end
@ -160,14 +805,24 @@ 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)
--printTable(lb) --printTable(lb)
end end
) )
local function ticsToTime(tics) function ticsToTime(tics, pure)
if tics == 0 and pure then
return "-:--:--"
end
return string.format( return string.format(
"%d:%02d:%02d", "%d:%02d:%02d",
G_TicsToMinutes(tics), G_TicsToMinutes(tics, true),
G_TicsToSeconds(tics), G_TicsToSeconds(tics),
G_TicsToCentiseconds(tics) G_TicsToCentiseconds(tics)
) )
@ -186,47 +841,84 @@ 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 / 4) % 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 function drawScoreboard(v, player) local VFLAGS = V_SNAPTOLEFT
if disable then return end local FACERANK_DIM = 16
if player != displayplayers[0] then return end local FACERANK_SPC = FACERANK_DIM + 4
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"],
-- | OFFSET | + | PADDING | * |INDEX| V_HUDTRANS | VFLAGS
local h = ((200 / 4) + 4) + (skinPatch.height + 4) * (i - 1) )
v.draw(4, h, skinPatch, V_HUDTRANS, v.getColormap("sonic", score["color"]))
if player.name == name then
v.draw(4, h, PATCH["CHILI"][(leveltime / 4) % 8], V_HUDTRANS)
end end
-- SPB -- SPB
@ -234,48 +926,68 @@ local function drawScoreboard(v, player)
local scale = FRACUNIT / 4 local scale = FRACUNIT / 4
drawitem( drawitem(
v, v,
4 - 2, x - 2,
h - 2, y - 2,
scale, scale,
PATCH["SPB"], modePatch(F_SPBATK),
V_HUDTRANS V_HUDTRANS | VFLAGS
) )
if score["flags"] & F_SPBEXP then if score["flags"] & F_SPBEXP then
drawitem( drawitem(
v, v,
skinPatch.width, x + FACERANK_DIM - 4,
h - 2, y - 2,
scale, scale,
PATCH["INV"][(leveltime / 4) % 6], modePatch(F_SPBEXP),
V_HUDTRANS V_HUDTRANS | VFLAGS
) )
end end
if score["flags"] & F_SPBBIG then if score["flags"] & F_SPBBIG then
drawitem( drawitem(
v, v,
4 - 2, x - 2,
h + skinPatch.height - 4, y + FACERANK_DIM - 4,
scale, scale,
PATCH["BIG"], modePatch(F_SPBBIG),
V_HUDTRANS V_HUDTRANS | VFLAGS
) )
end end
if score["flags"] & F_SPBJUS then if score["flags"] & F_SPBJUS then
drawitem( drawitem(
v, v,
skinPatch.width, x + FACERANK_DIM - 4,
h + skinPatch.height - 4, y + FACERANK_DIM - 4,
scale, scale,
PATCH["HYUD"], modePatch(F_SPBJUS),
V_HUDTRANS V_HUDTRANS | VFLAGS
) )
end end
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["weight"]
and pskin.kartspeed == stat["speed"]
) then
v.drawString(x + FACERANK_DIM - 2, y + 4, stat["speed"], V_HUDTRANS | VFLAGS, "small")
v.drawString(x + FACERANK_DIM - 2, y + 8, stat["weight"], V_HUDTRANS | VFLAGS, "small")
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"
@ -289,17 +1001,149 @@ local function drawScoreboard(v, player)
end end
end end
v.drawString(px + skinPatch.width, h + py, name, V_HUDTRANSHALF | V_ALLOWLOWERCASE, stralign) local flashV = 0
if me and FlashTics > leveltime then
flashV = FlashVFlags[leveltime / FlashRate % (#FlashVFlags + 1)]
end
v.drawString(
x + FACERANK_DIM + px,
y + py,
name,
textVFlags | V_ALLOWLOWERCASE | VFLAGS | flashV,
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(px + skinPatch.width, h + 8, splitSymbol[split < 0] + ticsToTime(abs(split)), V_HUDTRANSHALF | splitColor[split < 0]) v.drawString(
x + px + FACERANK_DIM,
y + 8,
splitSymbol[clamp(-1, split, 1)] + ticsToTime(abs(split)),
textVFlags | splitColor[clamp(-1, split, 1)] | VFLAGS
)
else else
v.drawString(px + skinPatch.width, h + 8, ticsToTime(score["time"]), V_HUDTRANSHALF | bodium[min(i, 4)]) v.drawString(
x + px + FACERANK_DIM,
y + 8,
ticsToTime(score["time"], true),
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 stateFunctions = {
[DS_DEFAULT] = drawDefault,
[DS_SCROLL] = drawScroll,
[DS_AUTO] = drawAuto,
[DS_SCRLTO] = drawScrollTo
}
-- 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
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)
@ -311,6 +1155,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)
@ -323,24 +1169,45 @@ 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 end
local function saveTime(player) local function saveTime(player)
local m = lb[lbID(gamemap, Flags)]
if m == nil then scoreTable = $ or {}
m = {}
end local pskin = skins[player.mo.skin]
local newscore = score_t( local newscore = score_t(
gamemap, gamemap,
player.name, player.name,
@ -348,19 +1215,27 @@ local function saveTime(player)
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 lbComp(newscore, scoreTable[i]) then
table.remove(m, i) table.remove(scoreTable, i)
S_StartSound(nil, 130) S_StartSound(nil, 130)
FlashTics = leveltime + TICRATE * 3
FlashRate = 1
FlashVFlags = YellowFlash
break break
else 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
@ -368,44 +1243,55 @@ local function saveTime(player)
print("Saving score") print("Saving score")
table.insert( table.insert(
m, scoreTable,
newscore newscore
) )
table.sort(m, lbComp) table.sort(scoreTable, lbComp)
while #m > 5 do while #scoreTable > cv_saves.value do
table.remove(m) table.remove(scoreTable)
end end
lb[lbID(gamemap, Flags)] = m scroll_to(player)
setScoreTable(gamemap, Flags, scoreTable)
if not StatTrack then
writeStats()
StatTrack = true
end
if isserver then
local f = assert(io.open(FILENAME, "w")) local f = assert(io.open(FILENAME, "w"))
if f == nil then if f == nil then
print("Failed to open file for writing: " + FILENAME) print("Failed to open file for writing: " + FILENAME)
return return
end end
for k, v in pairs(lb) do for _, tbl in pairs(lb) do
for i = 1, #v do for _, scoreTable in pairs(tbl) do
local s = v[i] for _, score in ipairs(scoreTable) do
f:write( f:write(
s["map"], "\t", score["map"], "\t",
s["name"], "\t", score["name"], "\t",
s["skin"], "\t", score["skin"], "\t",
s["color"], "\t", score["color"], "\t",
s["time"], "\t", score["time"], "\t",
table.concat(s["splits"], " "), "\t", table.concat(score["splits"], " "), "\t",
s["flags"], "\n" score["flags"], "\t",
stat_str(score["stat"]), "\n"
) )
end end
end end
end
f:close() f:close()
end
end end
-- DEBUGGING -- DEBUGGING
--local function saveLeaderboard(player, ...) --local function saveLeaderboard(player, ...)
-- timeFinished = player.realtime -- timeFinished = tonumber(... or player.realtime)
-- splits = {1000, 2000, 3000} -- splits = {1000, 2000, 3000}
-- saveTime(player) -- saveTime(player)
--end --end
@ -419,31 +1305,110 @@ local function regLap(player)
end end
end end
local function think() local function getGamer()
if disable then for p in players.iterate do
return if p.valid and not p.spectator then
return p
end end
if showSplit > 0 then end
showSplit = showSplit - 1 end
local function changeMap()
COM_BufInsertText(server, "map " + nextMap + " -force -gametype race")
nextMap = nil
end
local function think()
if nextMap then changeMap() end
if disable then
if AntiAFK then
if not singleplayer() then
for p in players.iterate do
if p.valid and not p.spectator and not p.exiting and p.lives > 0 then
if p.cmd.buttons then
p.afkTime = leveltime
end end
if leveltime < startTime then --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
else
for p in players.iterate do
if p.valid and not p.spectator then
p.afkTime = leveltime
end
end
end
end
help = true
return
end
showSplit = max(0, showSplit - 1)
local p = getGamer()
if leveltime < START_TIME then
-- Help message
if leveltime == START_TIME - TICRATE * 3 then
if singleplayer() then
if help then
help = false
chatprint(HELP_MESSAGE, true)
end
else
help = true
end
end
-- Autospec
-- Encore
if leveltime == 1 then
Flags = $ & !F_ENCORE
if encoremode then
Flags = $ | F_ENCORE
end
if p then
for s in players.iterate do
if s.valid and s.spectator then
COM_BufInsertText(s, string.format("view \"%d\"", #p))
end
end
end
end
if not cv_spbatk then
cv_spbatk = CV_FindVar("spbatk")
end
-- Gamemode flags
Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS) Flags = $ & !(F_SPBATK | F_SPBEXP | F_SPBBIG | F_SPBJUS)
if leveltime > startTime - (3 * TICRATE) / 2 and server.SPBArunning then if server.SPBArunning
and cv_spbatk.value
and leveltime > START_TIME - (3 * TICRATE) / 2 then
Flags = $ | F_SPBATK Flags = $ | F_SPBATK
if server.SPBAexpert then if server.SPBAexpert then
Flags = $ | F_SPBEXP Flags = $ | F_SPBEXP
end end
if clearcheats then if clearcheats then
clearcheats = false clearcheats = false
for p in players.iterate do for q in players.iterate do
p.SPBAKARTBIG = false q.SPBAKARTBIG = false
p.SPBAjustice = false q.SPBAjustice = false
p.SPBAshutup = false q.SPBAshutup = false
end end
end end
for p in players.iterate do
if not p.spectator then if p then
if p.SPBAKARTBIG then if p.SPBAKARTBIG then
Flags = $ | F_SPBBIG Flags = $ | F_SPBBIG
end end
@ -451,21 +1416,100 @@ local function think()
Flags = $ | F_SPBJUS Flags = $ | F_SPBJUS
end end
end end
-- make sure the spb actually spawned
if 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
else
hud.enable("freeplay")
end
end
scoreTable = getScoreTable(gamemap, Flags)
if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange")
end
if p then
-- 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
end
if p.lives == 0 then
drawState = DS_SCROLL
end
if p.cmd.buttons 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 end
end end
for p in players.iterate do
if p.laps >= mapheaderinfo[gamemap].numlaps and timeFinished == 0 then if p.laps >= mapheaderinfo[gamemap].numlaps and timeFinished == 0 then
timeFinished = p.realtime timeFinished = p.realtime
saveTime(p) saveTime(p)
end end
regLap(p) regLap(p)
elseif cv_teamchange.value == 0 then
allowJoin(true)
end end
end end
addHook("ThinkFrame", think) addHook("ThinkFrame", think)
local function interThink()
if nextMap then changeMap() end
if not cv_teamchange then
cv_teamchange = CV_FindVar("allowteamchange")
end
if not cv_teamchange.value then
allowJoin(true)
end
end
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)
Flags = net($)
splits = net($)
prevLap = net($)
drawState = net($)
StatTrack = net($)
EncoreInitial = net($)
lb = net($) lb = net($)
end end
addHook("NetVars", netvars) addHook("NetVars", netvars)