etlegacy-lua-scripts/rating/rating.lua

322 lines
8.6 KiB
Lua
Raw Normal View History

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
* 0.2: handle disconnected clients
* 0.3: handle map bias parameter
* 0.4: add useful commands
]]--
-- Lua module version
local version = "0.1"
-- 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',
mu='%s',
sigma='%s'
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"))
-- 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)
)
]])
--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)))
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
cur = assert(con:execute(string.format([[UPDATE users SET
last_seen='%s'
WHERE guid='%s']],
os.date("%Y-%m-%d %H:%M:%S"),
guid
)))
-- 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)))
local player = cur:fetch({}, 'a')
if not player then
-- first time this player is seen
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
cur = assert(con:execute(string.format("INSERT INTO users VALUES ('%s', '%s', '%s', '%s')",
guid,
os.date("%Y-%m-%d %H:%M:%S"),
25,
25/3
)))
-- cur:close()
else
-- load current rating
et.gentity_set(clientNum, "sess.mu", tonumber(player.mu))
et.gentity_set(clientNum, "sess.sigma", tonumber(player.sigma))
-- create copy for delta rating
et.gentity_set(clientNum, "sess.oldmu", tonumber(player.mu))
et.gentity_set(clientNum, "sess.oldsigma", tonumber(player.sigma))
et.trap_SendServerCommand(clientNum, string.format("cpm \"^2[Skill Rating]:^7 Welcome back, %s^7! Your rating is ^3%s\n\"",
name, string.format("%.2f", math.max(player.mu - 3 * player.sigma, 0))
))
-- et.trap_SendServerCommand(clientNum, string.format("cpm \"^2[Skill Rating]:^7 Welcome back, %s^7! Your rating is ^3%s ^7(^1%s^7,^4%s^7)\n\"",
-- name,
-- string.format("%.2f", math.max(player.mu - 3 * player.sigma, 0)),
-- string.format("%.2f", player.mu),
-- string.format("%.2f", player.sigma)
-- ))
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")
et.trap_SendServerCommand(clientNum, string.format("cpm \"^2[Skill Rating]:^7 Your rating is ^3%s\n\"",
string.format("%.2f", math.max(mu - 3 * sigma, 0))
))
-- et.trap_SendServerCommand(clientNum, string.format("cpm \"^2[Skill Rating]:^7 Your rating is ^3%s^7 (^1%s^7, ^4%s^7)\n\"",
-- string.format("%.2f", math.max(mu - 3 * sigma, 0)),
-- string.format("%.2f", mu),
-- string.format("%.2f", sigma)
-- ))
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
-- 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
et.G_Print(string.format("\tGUID %s\tLast seen: %s mu: ^1%s^: sigma: ^4%s^: Rating: ^3%s\n",
guid,
lastseen,
string.format("%.2f", mu),
string.format("%.2f", sigma),
string.format("%.2f", math.max(mu - 3 * sigma, 0))
))
end
-- cur:close()
return 1
end
return 0
end