2016-09-11 19:24:21 +00:00
|
|
|
--[[
|
|
|
|
Author: [Spyhawk]
|
|
|
|
License: ISC
|
|
|
|
Website: http://www.etlegacy.com
|
|
|
|
Mod: compatible with Legacy mod only
|
|
|
|
|
|
|
|
Description: Skill Rating (aka TrueSkill) data management
|
|
|
|
Use in conjunction with g_skillRating cvar
|
|
|
|
]]--
|
|
|
|
|
|
|
|
--[[
|
|
|
|
TODO:
|
|
|
|
* 0.1: handle basic rating update functionality
|
2016-09-20 21:42:22 +00:00
|
|
|
* 0.2: handle disconnected then reconnected clients
|
|
|
|
* 0.3: handle clients that left
|
|
|
|
* 0.4: handle map bias parameter
|
|
|
|
* 0.5: add useful commands
|
2016-09-11 19:24:21 +00:00
|
|
|
]]--
|
|
|
|
|
|
|
|
-- Lua module version
|
2016-09-20 21:42:22 +00:00
|
|
|
local version = "0.2"
|
2016-09-11 19:24:21 +00:00
|
|
|
|
|
|
|
-- load sqlite driver (or mysql..)
|
|
|
|
local luasql = require "luasql.sqlite3"
|
|
|
|
|
|
|
|
local env -- environment object
|
|
|
|
local con -- database connection
|
|
|
|
local cur -- cursor
|
|
|
|
|
|
|
|
-- check feature
|
|
|
|
local g_skillRating = tonumber(et.trap_Cvar_Get("g_skillRating"))
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Functions
|
|
|
|
]]--
|
|
|
|
|
|
|
|
-- database helper function
|
|
|
|
-- returns database rows matching sql_statement
|
|
|
|
function rows(connection, sql_statement)
|
|
|
|
local cursor = assert(connection:execute (sql_statement))
|
|
|
|
return function ()
|
|
|
|
return cursor:fetch()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- con:prepare with bind_names should be used to prevent SQL injections
|
|
|
|
-- but it isn't currently implemented in LuaSQL
|
|
|
|
function validateGUID(clientNum, guid)
|
|
|
|
-- allow only alphanumeric characters in guid
|
|
|
|
if(string.match(guid, "%W")) then
|
|
|
|
-- Invalid characters detected. We should probably drop this client
|
|
|
|
et.G_Print("^1[Skill Rating]:^7 User with ID " .. clientNum .. " has an invalid GUID: " .. guid .. "\n")
|
|
|
|
et.trap_SendServerCommand (clientNum, "cpm \"^2Your Skill Rating won't be saved because you have an invalid GUID!\n\"")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
-- saves SR values of a player with id 'clientNum' into database
|
|
|
|
function saveSR(clientNum)
|
|
|
|
local name = et.Info_ValueForKey(et.trap_GetUserinfo(clientNum), "name")
|
|
|
|
local guid = et.Info_ValueForKey(et.trap_GetUserinfo(clientNum), "cl_guid")
|
|
|
|
|
|
|
|
if not validateGUID(clientNum, guid) then return end
|
|
|
|
|
|
|
|
cur = assert(con:execute(string.format("SELECT * FROM users WHERE guid='%s' LIMIT 1", guid)))
|
|
|
|
local player = cur:fetch({}, 'a')
|
|
|
|
|
|
|
|
if not player then
|
|
|
|
-- should not happen
|
|
|
|
et.G_Print("^1[Skill Rating]:^7 User not found in database!\n")
|
|
|
|
-- cur:close()
|
|
|
|
return
|
|
|
|
else
|
|
|
|
-- save data
|
|
|
|
cur = assert(con:execute(string.format([[UPDATE users SET
|
|
|
|
last_seen='%s',
|
2016-09-21 16:06:56 +00:00
|
|
|
mu='%f',
|
|
|
|
sigma='%f'
|
2016-09-11 19:24:21 +00:00
|
|
|
WHERE guid='%s']],
|
|
|
|
os.date("%Y-%m-%d %H:%M:%S"),
|
|
|
|
et.gentity_get(clientNum, "sess.mu"),
|
|
|
|
et.gentity_get(clientNum, "sess.sigma"),
|
|
|
|
guid
|
|
|
|
)))
|
|
|
|
end
|
|
|
|
-- cur:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Callbacks
|
|
|
|
]]--
|
|
|
|
|
|
|
|
-- called when game initializes
|
|
|
|
function et_InitGame(levelTime, randomSeed, restart)
|
|
|
|
-- register name of this module
|
|
|
|
et.RegisterModname("Skill Rating " .. version)
|
|
|
|
|
|
|
|
-- check status
|
|
|
|
if tonumber(et.trap_Cvar_Get("g_skillRating")) < 1 then return end
|
|
|
|
|
|
|
|
-- create environement object
|
|
|
|
env = assert(luasql.sqlite3())
|
|
|
|
|
|
|
|
-- connect to database
|
|
|
|
con = assert(env:connect("rating.sqlite"))
|
|
|
|
|
|
|
|
-- drop database
|
|
|
|
-- cur = assert(con:execute("DROP TABLE users"))
|
2016-09-21 16:06:56 +00:00
|
|
|
cur = assert(con:execute("DROP TABLE IF EXISTS match"))
|
2016-09-11 19:24:21 +00:00
|
|
|
|
|
|
|
-- create database
|
|
|
|
cur = assert(con:execute[[
|
|
|
|
CREATE TABLE IF NOT EXISTS users(
|
|
|
|
guid VARCHAR(64),
|
|
|
|
last_seen VARCHAR(64),
|
|
|
|
mu REAL,
|
|
|
|
sigma REAL,
|
|
|
|
UNIQUE (guid)
|
|
|
|
)
|
|
|
|
]])
|
2016-09-20 21:42:22 +00:00
|
|
|
|
|
|
|
cur = assert(con:execute[[
|
2016-09-21 16:06:56 +00:00
|
|
|
CREATE TABLE match(
|
2016-09-20 21:42:22 +00:00
|
|
|
guid VARCHAR(64),
|
|
|
|
time_axis INT,
|
|
|
|
time_allies INT,
|
|
|
|
UNIQUE (guid)
|
|
|
|
)
|
|
|
|
]])
|
2016-09-11 19:24:21 +00:00
|
|
|
--cur:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- called when game shuts down
|
|
|
|
function et_ShutdownGame(restart)
|
|
|
|
-- check status
|
|
|
|
if g_skillRating < 1 then return end
|
|
|
|
-- clean up
|
|
|
|
cur:close()
|
|
|
|
con:close()
|
|
|
|
env:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- called every server frame
|
|
|
|
function et_RunFrame(levelTime)
|
|
|
|
-- check status
|
|
|
|
if g_skillRating < 1 then return end
|
|
|
|
|
|
|
|
-- check gamestate changes
|
|
|
|
gamestate = tonumber(et.trap_Cvar_Get("gamestate"))
|
|
|
|
|
|
|
|
if oldgamestate ~= gamestate then
|
|
|
|
oldgamestate = tonumber(et.trap_Cvar_Get("gamestate"))
|
|
|
|
|
|
|
|
-- GS_WARMUP
|
|
|
|
-- if gamestate == 1 then
|
|
|
|
-- et.G_Print("^1[Skill Rating]:^7 GS_WARMUP\n")
|
|
|
|
-- end
|
|
|
|
|
|
|
|
-- GS_PLAYING
|
|
|
|
-- if gamestate == 0 then
|
|
|
|
-- et.G_Print("^1[Skill Rating]:^7 GS_PLAYING\n")
|
|
|
|
-- end
|
|
|
|
|
|
|
|
-- GS_INTERMISSION
|
|
|
|
if gamestate == 3 then
|
|
|
|
-- et.G_Print("^1[Skill Rating]:^7 GS_INTERMISSION\n")
|
|
|
|
|
|
|
|
local clientNum = 0
|
|
|
|
local maxclients = tonumber(et.trap_Cvar_Get("sv_maxclients"))
|
|
|
|
|
|
|
|
-- iterate through clients
|
|
|
|
while clientNum < maxclients do
|
|
|
|
local cs = et.trap_GetConfigstring(tonumber(et.CS_PLAYERS) + clientNum)
|
|
|
|
-- save new ratings
|
|
|
|
if cs ~= nil and cs ~= "" then
|
|
|
|
saveSR(clientNum)
|
|
|
|
end
|
|
|
|
clientNum = clientNum + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- called for every ClientConnect
|
|
|
|
function et_ClientConnect(clientNum, firstTime, isBot)
|
|
|
|
-- check status
|
|
|
|
if g_skillRating < 1 then return end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- called for every ClientDisconnect
|
|
|
|
function et_ClientDisconnect(clientNum)
|
|
|
|
-- check status
|
|
|
|
if g_skillRating < 1 then return end
|
|
|
|
|
|
|
|
local guid = et.Info_ValueForKey(et.trap_GetUserinfo(clientNum), "cl_guid")
|
|
|
|
local name = et.Info_ValueForKey(et.trap_GetUserinfo(clientNum), "name")
|
|
|
|
|
|
|
|
if not validateGUID(clientNum, guid) then return end
|
|
|
|
|
|
|
|
cur = assert(con:execute(string.format("SELECT * FROM users WHERE guid='%s' LIMIT 1", guid)))
|
2016-09-20 21:42:22 +00:00
|
|
|
local user = cur:fetch({}, 'a')
|
2016-09-11 19:24:21 +00:00
|
|
|
|
2016-09-20 21:42:22 +00:00
|
|
|
if not user then
|
2016-09-11 19:24:21 +00:00
|
|
|
-- should not happen
|
|
|
|
et.G_Print("^1[Skill Rating]:^7 User not found in database!\n")
|
|
|
|
-- cur:close()
|
|
|
|
return
|
|
|
|
else
|
|
|
|
cur = assert(con:execute(string.format([[UPDATE users SET
|
|
|
|
last_seen='%s'
|
|
|
|
WHERE guid='%s']],
|
|
|
|
os.date("%Y-%m-%d %H:%M:%S"),
|
|
|
|
guid
|
|
|
|
)))
|
2016-09-20 21:42:22 +00:00
|
|
|
|
|
|
|
cur = assert(con:execute(string.format([[UPDATE match SET
|
|
|
|
time_axis='%i',
|
|
|
|
time_allies='%i'
|
|
|
|
WHERE guid='%s']],
|
|
|
|
et.gentity_get(clientNum, "sess.time_axis"),
|
|
|
|
et.gentity_get(clientNum, "sess.time_allies"),
|
|
|
|
guid
|
|
|
|
)))
|
2016-09-11 19:24:21 +00:00
|
|
|
-- cur:close()
|
|
|
|
end
|
|
|
|
-- cur:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- called for every ClientBegin
|
|
|
|
function et_ClientBegin(clientNum)
|
|
|
|
-- check status
|
|
|
|
if g_skillRating < 1 then return end
|
|
|
|
|
|
|
|
local guid = et.Info_ValueForKey(et.trap_GetUserinfo(clientNum), "cl_guid")
|
|
|
|
local name = et.Info_ValueForKey(et.trap_GetUserinfo(clientNum), "name")
|
|
|
|
|
|
|
|
if not validateGUID(clientNum, guid) then return end
|
|
|
|
|
|
|
|
cur = assert(con:execute(string.format("SELECT * FROM users WHERE guid='%s'", guid)))
|
2016-09-20 21:42:22 +00:00
|
|
|
local user = cur:fetch({}, 'a')
|
2016-09-11 19:24:21 +00:00
|
|
|
|
2016-09-20 21:42:22 +00:00
|
|
|
if not user then
|
|
|
|
-- first time this user is seen
|
2016-09-11 19:24:21 +00:00
|
|
|
et.trap_SendServerCommand(clientNum, "cpm \"^2[Skill Rating]:^7 Welcome, " .. name .. "^7! You are playing on an Skill Rating enabled server\n\"")
|
|
|
|
|
|
|
|
-- use default values
|
2016-09-21 16:06:56 +00:00
|
|
|
cur = assert(con:execute(string.format("INSERT INTO users VALUES ('%s', '%s', '%f', '%f')",
|
2016-09-11 19:24:21 +00:00
|
|
|
guid,
|
|
|
|
os.date("%Y-%m-%d %H:%M:%S"),
|
|
|
|
25,
|
|
|
|
25/3
|
|
|
|
)))
|
2016-09-20 21:42:22 +00:00
|
|
|
|
|
|
|
cur = assert(con:execute(string.format("INSERT INTO match VALUES ('%s', '%i', '%i')",
|
|
|
|
guid,
|
|
|
|
0,
|
|
|
|
0
|
|
|
|
)))
|
2016-09-11 19:24:21 +00:00
|
|
|
-- cur:close()
|
|
|
|
else
|
|
|
|
-- load current rating
|
2016-09-20 21:42:22 +00:00
|
|
|
et.gentity_set(clientNum, "sess.mu", tonumber(user.mu))
|
|
|
|
et.gentity_set(clientNum, "sess.sigma", tonumber(user.sigma))
|
2016-09-11 19:24:21 +00:00
|
|
|
-- create copy for delta rating
|
2016-09-20 21:42:22 +00:00
|
|
|
et.gentity_set(clientNum, "sess.oldmu", tonumber(user.mu))
|
|
|
|
et.gentity_set(clientNum, "sess.oldsigma", tonumber(user.sigma))
|
2016-09-11 19:24:21 +00:00
|
|
|
|
2016-09-21 16:06:56 +00:00
|
|
|
et.trap_SendServerCommand(clientNum, string.format("cpm \"^2[Skill Rating]:^7 Welcome back, %s^7! Your rating is ^3%.2f\n\"",
|
|
|
|
name, math.max(user.mu - 3 * user.sigma, 0))
|
|
|
|
)
|
2016-09-20 21:42:22 +00:00
|
|
|
|
|
|
|
-- load current play time, if reconnecting to the same match
|
|
|
|
cur = assert(con:execute(string.format("SELECT * FROM match WHERE guid='%s'", guid)))
|
|
|
|
local player = cur:fetch({}, 'a')
|
|
|
|
|
|
|
|
if player then
|
|
|
|
et.gentity_set(clientNum, "sess.time_axis", tonumber(player.time_axis))
|
|
|
|
et.gentity_set(clientNum, "sess.time_allies", tonumber(player.time_allies))
|
|
|
|
end
|
|
|
|
|
2016-09-11 19:24:21 +00:00
|
|
|
end
|
|
|
|
-- cur:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- called for every client command
|
|
|
|
-- return 1 if intercepted, 0 if passthrough
|
|
|
|
function et_ClientCommand(clientNum, cmd)
|
|
|
|
-- check status
|
|
|
|
if g_skillRating < 1 then return 0 end
|
|
|
|
|
|
|
|
-- local cmd = et.trap_Argv(0)
|
|
|
|
cmd = string.lower(cmd)
|
|
|
|
|
|
|
|
-- display current rating
|
|
|
|
if cmd == "!sr" then
|
|
|
|
local mu = et.gentity_get(clientNum, "sess.mu")
|
|
|
|
local sigma = et.gentity_get(clientNum, "sess.sigma")
|
|
|
|
|
2016-09-21 16:06:56 +00:00
|
|
|
et.trap_SendServerCommand(clientNum, string.format("cpm \"^2[Skill Rating]:^7 Your rating is ^3%.2f\n\"",
|
|
|
|
math.max(mu - 3 * sigma, 0))
|
|
|
|
)
|
2016-09-11 19:24:21 +00:00
|
|
|
return 1
|
|
|
|
end
|
|
|
|
|
|
|
|
return 0
|
|
|
|
end
|
|
|
|
|
|
|
|
-- called for every console command
|
|
|
|
-- return 1 if intercepted, 0 if passthrough
|
|
|
|
function et_ConsoleCommand()
|
|
|
|
-- check status
|
|
|
|
if g_skillRating < 1 then return 0 end
|
|
|
|
|
|
|
|
local cmd = et.trap_Argv(0)
|
|
|
|
cmd = string.lower(cmd)
|
|
|
|
|
|
|
|
-- drop users
|
|
|
|
if cmd == "!srdbdrop" then
|
|
|
|
-- FIXME: LuaSQL: database table is locked
|
|
|
|
cur = assert(con:execute("DROP TABLE users"))
|
|
|
|
et.G_Print("^2[Skill Rating]:^7 Dropped users table\n")
|
|
|
|
-- cur:close()
|
|
|
|
return 1
|
|
|
|
end
|
|
|
|
|
2016-09-20 21:42:22 +00:00
|
|
|
-- drop match players time
|
|
|
|
if cmd == "!srmatchdrop" then
|
|
|
|
-- FIXME: LuaSQL: database table is locked
|
|
|
|
cur = assert(con:execute("DROP TABLE match"))
|
|
|
|
et.G_Print("^2[Skill Rating]:^7 Dropped match table\n")
|
|
|
|
-- cur:close()
|
|
|
|
return 1
|
|
|
|
end
|
|
|
|
|
2016-09-11 19:24:21 +00:00
|
|
|
-- list all users
|
|
|
|
if cmd == "!srdblist" then
|
|
|
|
cur = assert(con:execute("SELECT COUNT(*) FROM users"))
|
|
|
|
et.G_Print("^2[Skill Rating]:^3 " .. tonumber(cur:fetch(row, 'a')) .. "^7 users in database\n")
|
|
|
|
local guid, lastseen, mu, sigma
|
|
|
|
for guid, lastseen, mu, sigma in rows(con, "SELECT * FROM users") do
|
2016-09-21 16:06:56 +00:00
|
|
|
et.G_Print(string.format("\tGUID %s\tLast seen: %s mu: ^1%.2f^: sigma: ^4%.2f^: Rating: ^3%.2f\n",
|
|
|
|
guid, lastseen, mu, sigma, math.max(mu - 3 * sigma, 0))
|
|
|
|
)
|
2016-09-11 19:24:21 +00:00
|
|
|
end
|
|
|
|
-- cur:close()
|
|
|
|
return 1
|
|
|
|
end
|
|
|
|
|
2016-09-20 21:42:22 +00:00
|
|
|
-- list all match players
|
|
|
|
if cmd == "!srmatchlist" then
|
|
|
|
cur = assert(con:execute("SELECT COUNT(*) FROM match"))
|
|
|
|
et.G_Print("^2[Skill Rating]:^3 " .. tonumber(cur:fetch(row, 'a')) .. "^7 players in match\n")
|
|
|
|
local guid, time_axis, time_allies
|
|
|
|
for guid, time_axis, time_allies in rows(con, "SELECT * FROM match") do
|
|
|
|
et.G_Print(string.format("\tGUID %s\tTime Axis: %i\tTime Allies: %i\n",
|
2016-09-21 16:06:56 +00:00
|
|
|
guid, time_axis, time_allies)
|
|
|
|
)
|
2016-09-20 21:42:22 +00:00
|
|
|
end
|
|
|
|
-- cur:close()
|
|
|
|
return 1
|
|
|
|
end
|
|
|
|
|
2016-09-11 19:24:21 +00:00
|
|
|
return 0
|
|
|
|
end
|