Initial commit
This commit is contained in:
90
gamemodes/darkrp/gamemode/libraries/disjointset.lua
Normal file
90
gamemodes/darkrp/gamemode/libraries/disjointset.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------
|
||||
Disjoint-set forest implementation
|
||||
Inspired by the book Introduction To Algorithms (third edition)
|
||||
|
||||
by FPtje Atheos
|
||||
|
||||
Running time per operation (Union/FindSet): O(a(n)) where a is the inverse of the Ackermann function.
|
||||
---------------------------------------------------------------------------*/
|
||||
|
||||
local ipairs = ipairs
|
||||
local setmetatable = setmetatable
|
||||
local string = string
|
||||
local table = table
|
||||
local tostring = tostring
|
||||
|
||||
module("disjoint")
|
||||
|
||||
local metatable
|
||||
|
||||
-- Make a singleton set. Parent parameter is optional, must be a disjoint-set as well.
|
||||
function MakeSet(x, parent)
|
||||
local set = {}
|
||||
set.value = x
|
||||
set.rank = 0
|
||||
set.parent = parent or set
|
||||
|
||||
setmetatable(set, metatable)
|
||||
|
||||
return set
|
||||
end
|
||||
|
||||
local function Link(x, y)
|
||||
if x == y then return x end
|
||||
|
||||
-- Union by rank
|
||||
if x.rank > y.rank then
|
||||
y.parent = x
|
||||
return x
|
||||
end
|
||||
|
||||
x.parent = y
|
||||
|
||||
if x.rank == y.rank then
|
||||
y.rank = y.rank + 1
|
||||
end
|
||||
|
||||
return y
|
||||
end
|
||||
|
||||
-- Apply the union operation between two sets.
|
||||
function Union(x, y)
|
||||
return Link(FindSet(x), FindSet(y))
|
||||
end
|
||||
|
||||
function FindSet(x)
|
||||
local parent = x
|
||||
local listParents
|
||||
|
||||
-- Go up the tree to find the parent
|
||||
while parent ~= parent.parent do
|
||||
parent = parent.parent
|
||||
|
||||
listParents = listParents or {}
|
||||
table.insert(listParents, parent)
|
||||
end
|
||||
|
||||
-- Path compression, update all parents to refer to the top parent
|
||||
if listParents then
|
||||
for _, v in ipairs(listParents) do
|
||||
v.parent = parent
|
||||
end
|
||||
end
|
||||
|
||||
return parent
|
||||
end
|
||||
|
||||
function Disconnect(x)
|
||||
x.parent = x
|
||||
|
||||
return x
|
||||
end
|
||||
|
||||
|
||||
metatable = {
|
||||
__tostring = function(self)
|
||||
return string.format("Disjoint-Set [value: %s][Rank: %s][Parent: %s]", tostring(self.value), self.rank, tostring(self.parent.value))
|
||||
end,
|
||||
__metatable = true, -- restrict access to metatable
|
||||
__add = Union
|
||||
}
|
||||
337
gamemodes/darkrp/gamemode/libraries/fn.lua
Normal file
337
gamemodes/darkrp/gamemode/libraries/fn.lua
Normal file
@@ -0,0 +1,337 @@
|
||||
/*---------------------------------------------------------------------------
|
||||
Functional library
|
||||
|
||||
by FPtje Atheos
|
||||
---------------------------------------------------------------------------*/
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Function currying
|
||||
Take a function with n parameters.
|
||||
Currying is the procedure of storing k < n parameters "in the function"
|
||||
in such a way that the remaining function can be called with n - k parameters
|
||||
|
||||
Example:
|
||||
DebugPrint = fp{print, "[DEBUG]"}
|
||||
DebugPrint("TEST")
|
||||
> [DEBUG] TEST
|
||||
---------------------------------------------------------------------------*/
|
||||
function fp(tbl)
|
||||
local func = tbl[1]
|
||||
|
||||
return function(...)
|
||||
local fnArgs = {}
|
||||
local arg = {...}
|
||||
local tblN = table.maxn(tbl)
|
||||
|
||||
for i = 2, tblN do fnArgs[i - 1] = tbl[i] end
|
||||
for i = 1, table.maxn(arg) do fnArgs[tblN + i - 1] = arg[i] end
|
||||
|
||||
return func(unpack(fnArgs, 1, table.maxn(fnArgs)))
|
||||
end
|
||||
end
|
||||
|
||||
local unpack = unpack
|
||||
local table = table
|
||||
local pairs = pairs
|
||||
local ipairs = ipairs
|
||||
local error = error
|
||||
local math = math
|
||||
local select = select
|
||||
local type = type
|
||||
local _G = _G
|
||||
local fp = fp
|
||||
|
||||
|
||||
module("fn")
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Parameter manipulation
|
||||
---------------------------------------------------------------------------*/
|
||||
Id = function(...) return ... end
|
||||
|
||||
Flip = function(f)
|
||||
if not f then error("not a function") end
|
||||
return function(b, a, ...)
|
||||
return f(a, b, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- Definition from http://lua-users.org/wiki/CurriedLua
|
||||
ReverseArgs = function(...)
|
||||
|
||||
--reverse args by building a function to do it, similar to the unpack() example
|
||||
local function reverse_h(acc, v, ...)
|
||||
if select('#', ...) == 0 then
|
||||
return v, acc()
|
||||
else
|
||||
return reverse_h(function () return v, acc() end, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- initial acc is the end of the list
|
||||
return reverse_h(function () return end, ...)
|
||||
end
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Misc functions
|
||||
---------------------------------------------------------------------------*/
|
||||
-- function composition
|
||||
do
|
||||
local function comp_h(a, b, ...)
|
||||
if b == nil then return a end
|
||||
b = comp_h(b, ...)
|
||||
return function(...)
|
||||
return a(b(...))
|
||||
end
|
||||
end
|
||||
Compose = function(funcs, ...)
|
||||
if type(funcs) == "table" then
|
||||
return comp_h(unpack(funcs))
|
||||
else
|
||||
return comp_h(funcs, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
_G.fc = Compose
|
||||
|
||||
-- Definition from http://lua-users.org/wiki/CurriedLua
|
||||
Curry = function(func, num_args)
|
||||
if not num_args then error("Missing argument #2: num_args") end
|
||||
if not func then error("Function does not exist!", 2) end
|
||||
-- helper
|
||||
local function curry_h(argtrace, n)
|
||||
if n == 0 then
|
||||
-- reverse argument list and call function
|
||||
return func(ReverseArgs(argtrace()))
|
||||
else
|
||||
-- "push" argument (by building a wrapper function) and decrement n
|
||||
return function(x)
|
||||
return curry_h(function() return x, argtrace() end, n - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- no sense currying for 1 arg or less
|
||||
if num_args > 1 then
|
||||
return curry_h(function() return end, num_args)
|
||||
else
|
||||
return func
|
||||
end
|
||||
end
|
||||
|
||||
-- Thanks Lexic!
|
||||
Partial = function(func, ...)
|
||||
local args = {...}
|
||||
return function(...)
|
||||
return func(unpack(table.Add( args, {...})))
|
||||
end
|
||||
end
|
||||
|
||||
Apply = function(f, ...) return f(...) end
|
||||
|
||||
Const = function(a, b) return a end
|
||||
Until = function(cmp, fn, val)
|
||||
if cmp(val) then
|
||||
return val
|
||||
end
|
||||
return Until(cmp, fn, fn(val))
|
||||
end
|
||||
|
||||
Seq = function(f, x) f(x) return x end
|
||||
|
||||
GetGlobalVar = function(key) return _G[key] end
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Mathematical operators and functions
|
||||
---------------------------------------------------------------------------*/
|
||||
Add = function(a, b) return a + b end
|
||||
Sub = function(a, b) return a - b end
|
||||
Mul = function(a, b) return a * b end
|
||||
Div = function(a, b) return a / b end
|
||||
Mod = function(a, b) return a % b end
|
||||
Neg = function(a) return -a end
|
||||
|
||||
Eq = function(a, b) return a == b end
|
||||
Neq = function(a, b) return a ~= b end
|
||||
Gt = function(a, b) return a > b end
|
||||
Lt = function(a, b) return a < b end
|
||||
Gte = function(a, b) return a >= b end
|
||||
Lte = function(a, b) return a <= b end
|
||||
|
||||
Succ = Compose{Add, 1}
|
||||
Pred = Compose{Flip(Sub), 1}
|
||||
Even = Compose{fp{Eq, 0}, fp{Flip(Mod), 2}}
|
||||
Odd = Compose{Not, Even}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Functional logical operators and conditions
|
||||
---------------------------------------------------------------------------*/
|
||||
FAnd = function(fns)
|
||||
return function(...)
|
||||
local val
|
||||
for _, f in pairs(fns) do
|
||||
val = {f(...)}
|
||||
if not val[1] then return unpack(val) end
|
||||
end
|
||||
if val then return unpack(val) end
|
||||
end
|
||||
end
|
||||
|
||||
FOr = function(fns)
|
||||
return function(...)
|
||||
local val
|
||||
for _, f in pairs(fns) do
|
||||
val = {f(...)}
|
||||
if val[1] then return unpack(val) end
|
||||
end
|
||||
return false, unpack(val, 2)
|
||||
end
|
||||
end
|
||||
|
||||
Not = function(x) return not x end
|
||||
|
||||
If = function(f, Then, Else)
|
||||
return function(x)
|
||||
if f(x) then
|
||||
return Then
|
||||
else
|
||||
return Else
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
List operations
|
||||
---------------------------------------------------------------------------*/
|
||||
Map = function(f, xs)
|
||||
for k, v in pairs(xs) do
|
||||
xs[k] = f(v)
|
||||
end
|
||||
return xs
|
||||
end
|
||||
|
||||
Append = function(xs, ys)
|
||||
return table.Add(xs, ys)
|
||||
end
|
||||
|
||||
Filter = function(f, xs)
|
||||
local res = {}
|
||||
for k,v in pairs(xs) do
|
||||
if f(v) then res[k] = v end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
ForEach = function(f, xs)
|
||||
for k,v in pairs(xs) do
|
||||
local val = f(k, v)
|
||||
if val ~= nil then return val end
|
||||
end
|
||||
end
|
||||
|
||||
Head = function(xs)
|
||||
return table.GetFirstValue(xs)
|
||||
end
|
||||
|
||||
Last = function(xs)
|
||||
return xs[#xs] or table.GetLastValue(xs)
|
||||
end
|
||||
|
||||
Tail = function(xs)
|
||||
table.remove(xs, 1)
|
||||
return xs
|
||||
end
|
||||
|
||||
Init = function(xs)
|
||||
xs[#xs] = nil
|
||||
return xs
|
||||
end
|
||||
|
||||
GetValue = function(i, xs)
|
||||
return xs[i]
|
||||
end
|
||||
|
||||
Null = function(xs)
|
||||
for k, v in pairs(xs) do
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
Length = function(xs)
|
||||
return #xs
|
||||
end
|
||||
|
||||
Index = function(xs, i)
|
||||
return xs[i]
|
||||
end
|
||||
|
||||
Reverse = function(xs)
|
||||
local res = {}
|
||||
for i = #xs, 1, -1 do
|
||||
res[#xs - i + 1] = xs[i]
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Folds
|
||||
---------------------------------------------------------------------------*/
|
||||
Foldr = function(func, val, xs)
|
||||
for i = #xs, 1, -1 do
|
||||
val = func(xs[i], val)
|
||||
end
|
||||
|
||||
return val
|
||||
end
|
||||
|
||||
Foldl = function(func, val, xs)
|
||||
for k, v in ipairs(xs) do
|
||||
val = func(val, v)
|
||||
end
|
||||
|
||||
return val
|
||||
end
|
||||
|
||||
And = function(xs)
|
||||
for k, v in pairs(xs) do
|
||||
if v ~= true then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
Or = function(xs)
|
||||
for k, v in pairs(xs) do
|
||||
if v == true then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
Any = function(func, xs)
|
||||
for k, v in pairs(xs) do
|
||||
if func(v) == true then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
All = function(func, xs)
|
||||
for k, v in pairs(xs) do
|
||||
if func(v) ~= true then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
Sum = _G.fp{Foldr, Add, 0}
|
||||
|
||||
Product = _G.fp{Foldr, Mul, 1}
|
||||
|
||||
Concat = _G.fp{Foldr, Append, {}}
|
||||
|
||||
Maximum = _G.fp{Foldl, math.Max, -math.huge}
|
||||
|
||||
Minimum = _G.fp{Foldl, math.Min, math.huge}
|
||||
|
||||
Snd = _G.fp{select, 2}
|
||||
|
||||
Thrd = _G.fp{select, 3}
|
||||
210
gamemodes/darkrp/gamemode/libraries/interfaceloader.lua
Normal file
210
gamemodes/darkrp/gamemode/libraries/interfaceloader.lua
Normal file
@@ -0,0 +1,210 @@
|
||||
module("DarkRP", package.seeall)
|
||||
|
||||
MetaName = "DarkRP"
|
||||
|
||||
-- Variables that maintain the existing stubs and hooks
|
||||
local stubs = {}
|
||||
local hookStubs = {}
|
||||
|
||||
-- Contains the functions that the hooks call by default
|
||||
hooks = {}
|
||||
|
||||
-- Delay the calling of methods until the functions are implemented
|
||||
local delayedCalls = {}
|
||||
|
||||
local returnsLayout, isreturns
|
||||
local parameterLayout, isparameters
|
||||
local isdeprecated
|
||||
local checkStub
|
||||
|
||||
local hookLayout
|
||||
|
||||
local realm -- State variable to manage the realm of the stubs
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Methods that check whether certain fields are valid
|
||||
---------------------------------------------------------------------------]]
|
||||
isreturns = function(tbl)
|
||||
if not istable(tbl) then return false end
|
||||
for _, v in pairs(tbl) do
|
||||
if not checkStub(v, returnsLayout) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
isparameters = function(tbl)
|
||||
if not istable(tbl) then return false end
|
||||
for _, v in pairs(tbl) do
|
||||
if not checkStub(v, parameterLayout) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
isdeprecated = function(val)
|
||||
return val == nil or isstring(val)
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
The layouts of stubs
|
||||
---------------------------------------------------------------------------]]
|
||||
local stubLayout = {
|
||||
name = isstring,
|
||||
description = isstring,
|
||||
deprecated = isdeprecated,
|
||||
parameters = isparameters, -- the parameters of a method
|
||||
returns = isreturns, -- the return values of a method
|
||||
metatable = istable -- DarkRP, Player, Entity, Vector, ...
|
||||
}
|
||||
|
||||
hookLayout = {
|
||||
name = isstring,
|
||||
description = isstring,
|
||||
deprecated = isdeprecated,
|
||||
parameters = isreturns, -- doesn't have the 'optional' field
|
||||
returns = isreturns,
|
||||
}
|
||||
|
||||
returnsLayout = {
|
||||
name = isstring,
|
||||
description = isstring,
|
||||
type = isstring
|
||||
}
|
||||
|
||||
parameterLayout = {
|
||||
name = isstring,
|
||||
description = isstring,
|
||||
type = isstring,
|
||||
optional = isbool
|
||||
}
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Check the validity of a stub
|
||||
---------------------------------------------------------------------------]]
|
||||
checkStub = function(tbl, stub)
|
||||
if not istable(tbl) then return false, "table" end
|
||||
|
||||
for name, check in pairs(stub) do
|
||||
if not check(tbl[name]) then
|
||||
return false, name
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
When a stub is called, the calling of the method is delayed
|
||||
---------------------------------------------------------------------------]]
|
||||
local function notImplemented(name, args, thisFunc)
|
||||
if stubs[name] and stubs[name].metatable[name] ~= thisFunc then -- when calling the not implemented function after the function was implemented
|
||||
return stubs[name].metatable[name](unpack(args))
|
||||
end
|
||||
table.insert(delayedCalls, {name = name, args = args})
|
||||
|
||||
return nil -- no return value because the method is not implemented
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Generate a stub
|
||||
---------------------------------------------------------------------------]]
|
||||
function stub(tbl)
|
||||
local isStub, field = checkStub(tbl, stubLayout)
|
||||
if not isStub then
|
||||
error("Invalid DarkRP method stub! Field \"" .. field .. "\" is invalid!", 2)
|
||||
end
|
||||
|
||||
tbl.realm = tbl.realm or realm
|
||||
stubs[tbl.name] = tbl
|
||||
|
||||
local function retNotImpl(...)
|
||||
return notImplemented(tbl.name, {...}, retNotImpl)
|
||||
end
|
||||
|
||||
return retNotImpl
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Generate a hook stub
|
||||
---------------------------------------------------------------------------]]
|
||||
function hookStub(tbl)
|
||||
local isStub, field = checkStub(tbl, hookLayout)
|
||||
if not isStub then
|
||||
error("Invalid DarkRP hook! Field \"" .. field .. "\" is invalid!", 2)
|
||||
end
|
||||
|
||||
tbl.realm = tbl.realm or realm
|
||||
hookStubs[tbl.name] = tbl
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Retrieve the stubs
|
||||
---------------------------------------------------------------------------]]
|
||||
function getStubs()
|
||||
return table.Copy(stubs)
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Retrieve the hooks
|
||||
---------------------------------------------------------------------------]]
|
||||
function getHooks()
|
||||
return table.Copy(hookStubs)
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Call the cached methods
|
||||
---------------------------------------------------------------------------]]
|
||||
function finish()
|
||||
local calls = table.Copy(delayedCalls) -- Loop through a copy, so the notImplemented function doesn't get called again
|
||||
for _, tbl in ipairs(calls) do
|
||||
local name = tbl.name
|
||||
|
||||
if not stubs[name] then ErrorNoHalt("Calling non-existing stub \"" .. name .. "\"") continue end
|
||||
|
||||
stubs[name].metatable[name](unpack(tbl.args))
|
||||
end
|
||||
|
||||
delayedCalls = {}
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Load the interface files
|
||||
---------------------------------------------------------------------------]]
|
||||
local function loadInterfaces()
|
||||
local root = GM.FolderName .. "/gamemode/modules"
|
||||
|
||||
local _, folders = file.Find(root .. "/*", "LUA")
|
||||
|
||||
ENTITY = FindMetaTable("Entity")
|
||||
PLAYER = FindMetaTable("Player")
|
||||
VECTOR = FindMetaTable("Vector")
|
||||
|
||||
for _, folder in SortedPairs(folders, true) do
|
||||
local interfacefile = string.format("%s/%s/%s_interface.lua", root, folder, "%s")
|
||||
local client = string.format(interfacefile, "cl")
|
||||
local shared = string.format(interfacefile, "sh")
|
||||
local server = string.format(interfacefile, "sv")
|
||||
|
||||
if file.Exists(shared, "LUA") then
|
||||
if SERVER then AddCSLuaFile(shared) end
|
||||
realm = "Shared"
|
||||
include(shared)
|
||||
end
|
||||
|
||||
if SERVER and file.Exists(client, "LUA") then
|
||||
AddCSLuaFile(client)
|
||||
end
|
||||
|
||||
if SERVER and file.Exists(server, "LUA") then
|
||||
realm = "Server"
|
||||
include(server)
|
||||
end
|
||||
|
||||
if CLIENT and file.Exists(client, "LUA") then
|
||||
realm = "Client"
|
||||
include(client)
|
||||
end
|
||||
end
|
||||
|
||||
ENTITY, PLAYER, VECTOR = nil, nil, nil
|
||||
end
|
||||
loadInterfaces()
|
||||
152
gamemodes/darkrp/gamemode/libraries/modificationloader.lua
Normal file
152
gamemodes/darkrp/gamemode/libraries/modificationloader.lua
Normal file
@@ -0,0 +1,152 @@
|
||||
-- Modification loader.
|
||||
-- Dependencies:
|
||||
-- - fn
|
||||
-- - simplerr
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Disabled defaults
|
||||
---------------------------------------------------------------------------]]
|
||||
DarkRP.disabledDefaults = {}
|
||||
DarkRP.disabledDefaults["modules"] = {
|
||||
["afk"] = true,
|
||||
["chatsounds"] = false,
|
||||
["events"] = false,
|
||||
["fpp"] = false,
|
||||
["hitmenu"] = false,
|
||||
["hud"] = false,
|
||||
["hungermod"] = true,
|
||||
["playerscale"] = false,
|
||||
["sleep"] = false,
|
||||
}
|
||||
|
||||
DarkRP.disabledDefaults["agendas"] = {}
|
||||
DarkRP.disabledDefaults["ammo"] = {}
|
||||
DarkRP.disabledDefaults["demotegroups"] = {}
|
||||
DarkRP.disabledDefaults["doorgroups"] = {}
|
||||
DarkRP.disabledDefaults["entities"] = {}
|
||||
DarkRP.disabledDefaults["food"] = {}
|
||||
DarkRP.disabledDefaults["groupchat"] = {}
|
||||
DarkRP.disabledDefaults["hitmen"] = {}
|
||||
DarkRP.disabledDefaults["jobs"] = {}
|
||||
DarkRP.disabledDefaults["shipments"] = {}
|
||||
DarkRP.disabledDefaults["vehicles"] = {}
|
||||
DarkRP.disabledDefaults["workarounds"] = {}
|
||||
|
||||
-- The client cannot use simplerr.runLuaFile because of restrictions in GMod.
|
||||
local doInclude = CLIENT and include or fc{simplerr.wrapError, simplerr.wrapLog, simplerr.runFile}
|
||||
|
||||
if file.Exists("darkrp_config/disabled_defaults.lua", "LUA") then
|
||||
if SERVER then AddCSLuaFile("darkrp_config/disabled_defaults.lua") end
|
||||
doInclude("darkrp_config/disabled_defaults.lua")
|
||||
end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Config
|
||||
---------------------------------------------------------------------------]]
|
||||
local configFiles = {
|
||||
"darkrp_config/settings.lua",
|
||||
"darkrp_config/licenseweapons.lua",
|
||||
}
|
||||
|
||||
for _, File in ipairs(configFiles) do
|
||||
if not file.Exists(File, "LUA") then continue end
|
||||
|
||||
if SERVER then AddCSLuaFile(File) end
|
||||
doInclude(File)
|
||||
end
|
||||
if SERVER and file.Exists("darkrp_config/mysql.lua", "LUA") then doInclude("darkrp_config/mysql.lua") end
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Modules
|
||||
---------------------------------------------------------------------------]]
|
||||
local function loadModules()
|
||||
local fol = "darkrp_modules/"
|
||||
|
||||
local _, folders = file.Find(fol .. "*", "LUA")
|
||||
|
||||
for _, folder in SortedPairs(folders, true) do
|
||||
if folder == "." or folder == ".." or GAMEMODE.Config.DisabledCustomModules[folder] then continue end
|
||||
-- Sound but incomplete way of detecting the error of putting addons in the darkrpmod folder
|
||||
if file.Exists(fol .. folder .. "/addon.txt", "LUA") or file.Exists(fol .. folder .. "/addon.json", "LUA") then
|
||||
DarkRP.errorNoHalt("Addon detected in the darkrp_modules folder.", 2, {
|
||||
"This addon is not supposed to be in the darkrp_modules folder.",
|
||||
"It is supposed to be in garrysmod/addons/ instead.",
|
||||
"Whether a mod is to be installed in darkrp_modules or addons is the author's decision.",
|
||||
"Please read the readme of the addons you're installing next time."
|
||||
},
|
||||
"<darkrpmod addon>/lua/darkrp_modules/" .. folder, -1)
|
||||
continue
|
||||
end
|
||||
|
||||
for _, File in SortedPairs(file.Find(fol .. folder .. "/sh_*.lua", "LUA"), true) do
|
||||
if SERVER then
|
||||
AddCSLuaFile(fol .. folder .. "/" .. File)
|
||||
end
|
||||
|
||||
if File == "sh_interface.lua" then continue end
|
||||
doInclude(fol .. folder .. "/" .. File)
|
||||
end
|
||||
|
||||
if SERVER then
|
||||
for _, File in SortedPairs(file.Find(fol .. folder .. "/sv_*.lua", "LUA"), true) do
|
||||
if File == "sv_interface.lua" then continue end
|
||||
doInclude(fol .. folder .. "/" .. File)
|
||||
end
|
||||
end
|
||||
|
||||
for _, File in SortedPairs(file.Find(fol .. folder .. "/cl_*.lua", "LUA"), true) do
|
||||
if File == "cl_interface.lua" then continue end
|
||||
|
||||
if SERVER then
|
||||
AddCSLuaFile(fol .. folder .. "/" .. File)
|
||||
else
|
||||
doInclude(fol .. folder .. "/" .. File)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function loadLanguages()
|
||||
local fol = "darkrp_language/"
|
||||
|
||||
local files, _ = file.Find(fol .. "*", "LUA")
|
||||
for _, File in ipairs(files) do
|
||||
if SERVER then AddCSLuaFile(fol .. File) end
|
||||
doInclude(fol .. File)
|
||||
end
|
||||
end
|
||||
|
||||
local customFiles = {
|
||||
"darkrp_customthings/jobs.lua",
|
||||
"darkrp_customthings/shipments.lua",
|
||||
"darkrp_customthings/entities.lua",
|
||||
"darkrp_customthings/vehicles.lua",
|
||||
"darkrp_customthings/food.lua",
|
||||
"darkrp_customthings/ammo.lua",
|
||||
"darkrp_customthings/groupchats.lua",
|
||||
"darkrp_customthings/categories.lua",
|
||||
"darkrp_customthings/agendas.lua", -- has to be run after jobs.lua
|
||||
"darkrp_customthings/doorgroups.lua", -- has to be run after jobs.lua
|
||||
"darkrp_customthings/demotegroups.lua", -- has to be run after jobs.lua
|
||||
}
|
||||
local function loadCustomDarkRPItems()
|
||||
for _, File in ipairs(customFiles) do
|
||||
if not file.Exists(File, "LUA") then continue end
|
||||
if File == "darkrp_customthings/food.lua" and DarkRP.disabledDefaults["modules"]["hungermod"] then continue end
|
||||
|
||||
if SERVER then AddCSLuaFile(File) end
|
||||
doInclude(File)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function GM:DarkRPFinishedLoading()
|
||||
-- GAMEMODE gets set after the last statement in the gamemode files is run. That is not the case in this hook
|
||||
GAMEMODE = GAMEMODE or GM
|
||||
|
||||
loadLanguages()
|
||||
loadModules()
|
||||
loadCustomDarkRPItems()
|
||||
hook.Call("loadCustomDarkRPItems", self)
|
||||
hook.Call("postLoadCustomDarkRPItems", self)
|
||||
end
|
||||
519
gamemodes/darkrp/gamemode/libraries/mysqlite/mysqlite.lua
Normal file
519
gamemodes/darkrp/gamemode/libraries/mysqlite/mysqlite.lua
Normal file
@@ -0,0 +1,519 @@
|
||||
--[[
|
||||
MySQLite - Abstraction mechanism for SQLite and MySQL
|
||||
|
||||
Why use this?
|
||||
- Easy to use interface for MySQL
|
||||
- No need to modify code when switching between SQLite and MySQL
|
||||
- Queued queries: execute a bunch of queries in order an run the callback when all queries are done
|
||||
|
||||
License: LGPL V2.1 (read here: https://www.gnu.org/licenses/lgpl-2.1.html)
|
||||
|
||||
Supported MySQL modules:
|
||||
- MySQLOO
|
||||
- tmysql4
|
||||
|
||||
Note: When both MySQLOO and tmysql4 modules are installed, MySQLOO is used by default.
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Documentation
|
||||
---------------------------------------------------------------------------*/
|
||||
|
||||
MySQLite.initialize([config :: table]) :: No value
|
||||
Initialize MySQLite. Loads the config from either the config parameter OR the MySQLite_config global.
|
||||
This loads the module (if necessary) and connects to the MySQL database (if set up).
|
||||
The config must have this layout:
|
||||
{
|
||||
EnableMySQL :: Bool - set to true to use MySQL, false for SQLite
|
||||
Host :: String - database hostname
|
||||
Username :: String - database username
|
||||
Password :: String - database password (keep away from clients!)
|
||||
Database_name :: String - name of the database
|
||||
Database_port :: Number - connection port (3306 by default)
|
||||
Preferred_module :: String - Preferred module, case sensitive, must be either "mysqloo" or "tmysql4"
|
||||
MultiStatements :: Bool - Only available in tmysql4: allow multiple SQL statements per query
|
||||
}
|
||||
|
||||
----------------------------- Utility functions -----------------------------
|
||||
MySQLite.isMySQL() :: Bool
|
||||
Returns whether MySQLite is set up to use MySQL. True for MySQL, false for SQLite.
|
||||
Use this when the query syntax between SQLite and MySQL differs (example: AUTOINCREMENT vs AUTO_INCREMENT)
|
||||
|
||||
MySQLite.SQLStr(str :: String) :: String
|
||||
Escapes the string and puts it in quotes.
|
||||
It uses the escaping method of the module that is currently being used.
|
||||
|
||||
MySQLite.tableExists(tbl :: String, callback :: function, errorCallback :: function)
|
||||
Checks whether table tbl exists.
|
||||
|
||||
callback format: function(res :: Bool)
|
||||
res is a boolean indicating whether the table exists.
|
||||
|
||||
The errorCallback format is the same as in MySQLite.query.
|
||||
|
||||
----------------------------- Running queries -----------------------------
|
||||
MySQLite.query(sqlText :: String, callback :: function, errorCallback :: function) :: No value
|
||||
Runs a query. Calls the callback parameter when finished, calls errorCallback when an error occurs.
|
||||
|
||||
callback format:
|
||||
function(result :: table, lastInsert :: number)
|
||||
Result is the table with results (nil when there are no results or when the result list is empty)
|
||||
lastInsert is the row number of the last inserted value (use with AUTOINCREMENT)
|
||||
|
||||
Note: lastInsert is NOT supported when using SQLite.
|
||||
|
||||
errorCallback format:
|
||||
function(error :: String, query :: String) :: Bool
|
||||
error is the error given by the database module.
|
||||
query is the query that triggered the error.
|
||||
|
||||
Return true to suppress the error!
|
||||
|
||||
MySQLite.queryValue(sqlText :: String, callback :: function, errorCallback :: function) :: No value
|
||||
Runs a query and returns the first value it comes across.
|
||||
|
||||
callback format:
|
||||
function(result :: any)
|
||||
where the result is either a string or a number, depending on the requested database field.
|
||||
|
||||
The errorCallback format is the same as in MySQLite.query.
|
||||
|
||||
MySQLite.prepare(sqlText :: String, sqlParams :: Table, callback :: function, errorCallback :: function)
|
||||
Calls a prepared statement, faster for queries that are ran multiple times. Params do not need to be escaped.
|
||||
This does not work with SQLite, and will instead call a regular query
|
||||
|
||||
callback format:
|
||||
function(result :: table, lastInsert :: number, rowsChanged :: number)
|
||||
Result is the table with results (nil when there are no results or when the result list is empty)
|
||||
lastInsert is the row number of the last inserted value (use with AUTOINCREMENT)
|
||||
rowsChanged is the amount of rows that were changed with the query
|
||||
|
||||
Note: lastInsert is NOT supported when using SQLite.
|
||||
|
||||
The errorCallback format is the same as in MySQLite.query.
|
||||
|
||||
Example:
|
||||
MySQLite.prepare("UPDATE `darkrp_player` SET `rpname` = ? WHERE `rpname` = ?", { "players_old_name", "players_new_name"}, success_callback, error_callback)
|
||||
|
||||
----------------------------- Transactions -----------------------------
|
||||
MySQLite.begin() :: No value
|
||||
Starts a transaction. Use in combination with MySQLite.queueQuery and MySQLite.commit.
|
||||
|
||||
MySQLite.queueQuery(sqlText :: String, callback :: function, errorCallback :: function) :: No value
|
||||
Queues a query in the transaction. Note: a transaction must be started with MySQLite.begin() for this to work.
|
||||
The callback will be called when this specific query has been executed successfully.
|
||||
The errorCallback function will be called when an error occurs in this specific query.
|
||||
|
||||
See MySQLite.query for the callback and errorCallback format.
|
||||
|
||||
MySQLite.commit(onFinished)
|
||||
Commits a transaction and calls onFinished when EVERY queued query has finished.
|
||||
onFinished is NOT called when an error occurs in one of the queued queries.
|
||||
|
||||
onFinished is called without arguments.
|
||||
|
||||
----------------------------- Hooks -----------------------------
|
||||
DatabaseInitialized
|
||||
Called when a successful connection to the database has been made.
|
||||
]]
|
||||
|
||||
local debug = debug
|
||||
local error = error
|
||||
local ErrorNoHalt = ErrorNoHalt
|
||||
local hook = hook
|
||||
local pairs = pairs
|
||||
local require = require
|
||||
local sql = sql
|
||||
local string = string
|
||||
local table = table
|
||||
local timer = timer
|
||||
local tostring = tostring
|
||||
local mysqlOO
|
||||
local TMySQL
|
||||
local _G = _G
|
||||
local ipairs = ipairs
|
||||
local type = type
|
||||
local unpack = unpack
|
||||
|
||||
local multistatements
|
||||
|
||||
local MySQLite_config = MySQLite_config or RP_MySQLConfig or FPP_MySQLConfig
|
||||
local moduleLoaded
|
||||
|
||||
local function loadMySQLModule()
|
||||
if moduleLoaded or not MySQLite_config or not MySQLite_config.EnableMySQL then return end
|
||||
|
||||
local moo, tmsql = util.IsBinaryModuleInstalled("mysqloo"), util.IsBinaryModuleInstalled("tmysql4")
|
||||
|
||||
if not moo and not tmsql then
|
||||
error("Could not find a suitable MySQL module. Please either:\n - Install tmysql. It can be obtained from https://github.com/SuperiorServers/gm_tmysql4\n - Install MySQLOO. It can be obtained from https://github.com/FredyH/MySQLOO\nDue to this error, MySQL is disabled. This means that SQLite is used instead to store data.")
|
||||
end
|
||||
moduleLoaded = true
|
||||
|
||||
require(
|
||||
moo and tmsql and MySQLite_config.Preferred_module or
|
||||
moo and "mysqloo" or
|
||||
tmsql and "tmysql4"
|
||||
)
|
||||
|
||||
multistatements = CLIENT_MULTI_STATEMENTS
|
||||
|
||||
mysqlOO = mysqloo
|
||||
TMySQL = tmysql
|
||||
|
||||
if MySQLite_config.Preferred_module == "tmysql4" then
|
||||
if not tmsql then
|
||||
ErrorNoHalt("The preferred module for MySQL is selected to be tmysql4. However, tmysql4 does not appear to be installed. Please either:\n - Install tmysql. It can be obtained from https://github.com/SuperiorServers/gm_tmysql4\n - Select MySQLOO as the preferred module for MySQL. MySQLOO appears to be installed.")
|
||||
return
|
||||
end
|
||||
|
||||
if not tmysql.Version or tmysql.Version < 4.1 then
|
||||
MsgC(Color(255, 0, 0), "Using older tmysql version, please consider updating!\n")
|
||||
MsgC(Color(255, 0, 0), "Newer Version: https://github.com/SuperiorServers/gm_tmysql4\n")
|
||||
end
|
||||
|
||||
-- Turns tmysql.Connect into tmysql.Initialize if they're using an older version.
|
||||
TMySQL.Connect = tmysql.Version and tmysql.Version >= 4.1 and TMySQL.Connect or TMySQL.initialize
|
||||
TMySQL.SetOption = tmysql.Version and tmysql.Version >= 4.1 and TMySQL.SetOption or TMySQL.Option
|
||||
else
|
||||
if not moo then
|
||||
ErrorNoHalt("The preferred module for MySQL is selected to be MySQLOO. However, MySQLOO does not appear to be installed. Please either:\n - Install MySQLOO. It can be obtained from https://github.com/FredyH/MySQLOO\n - Select tmysql4 as the preferred module for MySQL. tmysql4 appears to be installed.")
|
||||
end
|
||||
end
|
||||
end
|
||||
loadMySQLModule()
|
||||
|
||||
module("MySQLite")
|
||||
|
||||
-- Helper function to return the first value found when iterating over a table.
|
||||
-- Replaces the now deprecated table.GetFirstValue
|
||||
local function arbitraryTableValue(tbl)
|
||||
for _, v in pairs(tbl) do return v end
|
||||
end
|
||||
|
||||
function initialize(config)
|
||||
MySQLite_config = config or MySQLite_config
|
||||
|
||||
if not MySQLite_config then
|
||||
ErrorNoHalt("Warning: No MySQL config!")
|
||||
end
|
||||
|
||||
loadMySQLModule()
|
||||
|
||||
if MySQLite_config.EnableMySQL then
|
||||
connectToMySQL(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
|
||||
else
|
||||
timer.Simple(0, function()
|
||||
_G.GAMEMODE.DatabaseInitialized = _G.GAMEMODE.DatabaseInitialized or function() end
|
||||
hook.Call("DatabaseInitialized", _G.GAMEMODE)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local CONNECTED_TO_MYSQL = false
|
||||
local msOOConnect
|
||||
databaseObject = nil
|
||||
|
||||
local queuedQueries
|
||||
local cachedQueries
|
||||
|
||||
function isMySQL()
|
||||
return CONNECTED_TO_MYSQL
|
||||
end
|
||||
|
||||
function begin()
|
||||
if not CONNECTED_TO_MYSQL then
|
||||
sql.Begin()
|
||||
else
|
||||
if queuedQueries then
|
||||
debug.Trace()
|
||||
error("Transaction ongoing!")
|
||||
end
|
||||
queuedQueries = {}
|
||||
end
|
||||
end
|
||||
|
||||
function commit(onFinished)
|
||||
if not CONNECTED_TO_MYSQL then
|
||||
sql.Commit()
|
||||
if onFinished then onFinished() end
|
||||
return
|
||||
end
|
||||
|
||||
if not queuedQueries then
|
||||
error("No queued queries! Call begin() first!")
|
||||
end
|
||||
|
||||
if #queuedQueries == 0 then
|
||||
queuedQueries = nil
|
||||
if onFinished then onFinished() end
|
||||
return
|
||||
end
|
||||
|
||||
-- Copy the table so other scripts can create their own queue
|
||||
local queue = table.Copy(queuedQueries)
|
||||
queuedQueries = nil
|
||||
|
||||
-- Handle queued queries in order
|
||||
local queuePos = 0
|
||||
local call
|
||||
|
||||
-- Recursion invariant: queuePos > 0 and queue[queuePos] <= #queue
|
||||
call = function(...)
|
||||
queuePos = queuePos + 1
|
||||
|
||||
if queue[queuePos].callback then
|
||||
queue[queuePos].callback(...)
|
||||
end
|
||||
|
||||
-- Base case, end of the queue
|
||||
if queuePos + 1 > #queue then
|
||||
if onFinished then onFinished() end -- All queries have finished
|
||||
return
|
||||
end
|
||||
|
||||
-- Recursion
|
||||
local nextQuery = queue[queuePos + 1]
|
||||
query(nextQuery.query, call, nextQuery.onError)
|
||||
end
|
||||
|
||||
query(queue[1].query, call, queue[1].onError)
|
||||
end
|
||||
|
||||
function queueQuery(sqlText, callback, errorCallback)
|
||||
if CONNECTED_TO_MYSQL then
|
||||
table.insert(queuedQueries, {query = sqlText, callback = callback, onError = errorCallback})
|
||||
return
|
||||
end
|
||||
-- SQLite is instantaneous, simply running the query is equal to queueing it
|
||||
query(sqlText, callback, errorCallback)
|
||||
end
|
||||
|
||||
local function msOOQuery(sqlText, callback, errorCallback, queryValue)
|
||||
local queryObject = databaseObject:query(sqlText)
|
||||
local data
|
||||
queryObject.onData = function(Q, D)
|
||||
data = data or {}
|
||||
data[#data + 1] = D
|
||||
end
|
||||
|
||||
queryObject.onError = function(Q, E)
|
||||
if databaseObject:status() == mysqlOO.DATABASE_NOT_CONNECTED then
|
||||
table.insert(cachedQueries, {sqlText, callback, queryValue})
|
||||
|
||||
-- Immediately try reconnecting
|
||||
msOOConnect(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
|
||||
return
|
||||
end
|
||||
|
||||
local supp = errorCallback and errorCallback(E, sqlText)
|
||||
if not supp then error(E .. " (" .. sqlText .. ")") end
|
||||
end
|
||||
|
||||
queryObject.onSuccess = function()
|
||||
local res = queryValue and data and data[1] and arbitraryTableValue(data[1]) or not queryValue and data or nil
|
||||
if callback then callback(res, queryObject:lastInsert()) end
|
||||
end
|
||||
queryObject:start()
|
||||
end
|
||||
|
||||
local preparedStatements = {}
|
||||
local paramTypes = {}
|
||||
paramTypes["number"] = function(queryObj, paramIndex, paramValue) return queryObj:setNumber(paramIndex, paramValue) end
|
||||
paramTypes["string"] = function(queryObj, paramIndex, paramValue) return queryObj:setString(paramIndex, paramValue) end
|
||||
paramTypes["boolean"] = function(queryObj, paramIndex, paramValue) return queryObj:setBoolean(paramIndex, paramValue) end
|
||||
|
||||
local function msOOPrepare(sqlText, sqlParams, callback, errorCallback)
|
||||
local queryObject
|
||||
|
||||
if preparedStatements[sqlText] then
|
||||
queryObject = preparedStatements[sqlText]
|
||||
else
|
||||
queryObject = databaseObject:prepare(sqlText)
|
||||
preparedStatements[sqlText] = queryObject
|
||||
end
|
||||
|
||||
for i, param in ipairs(sqlParams) do
|
||||
local paramType = type(param)
|
||||
|
||||
if paramTypes[paramType] then
|
||||
paramTypes[paramType](queryObject, i, param)
|
||||
else
|
||||
queryObject:setString(i, param)
|
||||
end
|
||||
end
|
||||
|
||||
queryObject.onError = function(_, E)
|
||||
local supp = errorCallback and errorCallback(E, sqlText)
|
||||
if not supp then error(E .. " (" .. sqlText .. ")") end
|
||||
end
|
||||
|
||||
queryObject.onSuccess = function(_, data)
|
||||
if callback then callback(data, queryObject:lastInsert(), queryObject:affectedRows()) end
|
||||
end
|
||||
|
||||
queryObject:start()
|
||||
end
|
||||
|
||||
local function tmsqlPrepare(sqlText, sqlParams, callback, errorCallback)
|
||||
local queryObject
|
||||
|
||||
if preparedStatements[sqlText] then
|
||||
queryObject = preparedStatements[sqlText]
|
||||
else
|
||||
queryObject = databaseObject:Prepare(sqlText)
|
||||
preparedStatements[sqlText] = queryObject
|
||||
end
|
||||
|
||||
for i, param in ipairs(sqlParams) do
|
||||
param = SQLStr(param, true)
|
||||
end
|
||||
|
||||
local varcount = queryObject:GetArgCount()
|
||||
|
||||
sqlParams[varcount + 1] = function(results)
|
||||
if results[1].error ~= nil then
|
||||
local supp = errorCallback and errorCallback(E, results[1].error)
|
||||
if not supp then error(E .. " (" .. results[1].error .. ")") end
|
||||
end
|
||||
|
||||
if callback then callback(data, results[1].lastid, results[1].affected) end
|
||||
end
|
||||
|
||||
queryObject:Run(unpack(sqlParams, 1, varcount + 2))
|
||||
end
|
||||
|
||||
local function tmsqlQuery(sqlText, callback, errorCallback, queryValue)
|
||||
local call = function(res)
|
||||
res = res[1] -- For now only support one result set
|
||||
if not res.status then
|
||||
local supp = errorCallback and errorCallback(res.error, sqlText)
|
||||
if not supp then error(res.error .. " (" .. sqlText .. ")") end
|
||||
return
|
||||
end
|
||||
|
||||
if not res.data or #res.data == 0 then res.data = nil end -- compatibility with other backends
|
||||
if queryValue and callback then return callback(res.data and res.data[1] and arbitraryTableValue(res.data[1]) or nil) end
|
||||
if callback then callback(res.data, res.lastid) end
|
||||
end
|
||||
|
||||
databaseObject:Query(sqlText, call)
|
||||
end
|
||||
|
||||
local function SQLiteQuery(sqlText, callback, errorCallback, queryValue)
|
||||
sql.m_strError = "" -- reset last error
|
||||
|
||||
local lastError = sql.LastError()
|
||||
local Result = queryValue and sql.QueryValue(sqlText) or sql.Query(sqlText)
|
||||
|
||||
if sql.LastError() and sql.LastError() ~= lastError then
|
||||
local err = sql.LastError()
|
||||
local supp = errorCallback and errorCallback(err, sqlText)
|
||||
if supp == false then error(err .. " (" .. sqlText .. ")", 2) end
|
||||
return
|
||||
end
|
||||
|
||||
if callback then callback(Result) end
|
||||
return Result
|
||||
end
|
||||
|
||||
-- SQLite doesn't support preparedStatements, so convert it to a regular query.
|
||||
local function SQLitePrepare(sqlText, sqlParams, callback, errorCallback)
|
||||
for _, param in ipairs(sqlParams) do
|
||||
sqlText = sqlText:gsub("%?", sql.SQLStr(param), 1)
|
||||
end
|
||||
|
||||
return SQLiteQuery(sqlText, callback, errorCallback, false)
|
||||
end
|
||||
|
||||
function query(sqlText, callback, errorCallback)
|
||||
local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOQuery) or (TMySQL and tmsqlQuery))) or SQLiteQuery
|
||||
return qFunc(sqlText, callback, errorCallback, false)
|
||||
end
|
||||
|
||||
function queryValue(sqlText, callback, errorCallback)
|
||||
local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOQuery) or (TMySQL and tmsqlQuery))) or SQLiteQuery
|
||||
return qFunc(sqlText, callback, errorCallback, true)
|
||||
end
|
||||
|
||||
function prepare(sqlText, sqlParams, callback, errorCallback)
|
||||
local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOPrepare) or (TMySQL and tmsqlPrepare))) or SQLitePrepare
|
||||
return qFunc(sqlText, sqlParams, callback, errorCallback, false)
|
||||
end
|
||||
|
||||
local function onConnected()
|
||||
CONNECTED_TO_MYSQL = true
|
||||
|
||||
-- Run the queries that were called before the connection was made
|
||||
for k, v in pairs(cachedQueries or {}) do
|
||||
cachedQueries[k] = nil
|
||||
if v[3] then
|
||||
queryValue(v[1], v[2])
|
||||
else
|
||||
query(v[1], v[2])
|
||||
end
|
||||
end
|
||||
cachedQueries = {}
|
||||
local GM = _G.GAMEMODE or _G.GM
|
||||
|
||||
hook.Call("DatabaseInitialized", GM.DatabaseInitialized and GM or nil)
|
||||
|
||||
end
|
||||
|
||||
msOOConnect = function(host, username, password, database_name, database_port)
|
||||
databaseObject = mysqlOO.connect(host, username, password, database_name, database_port)
|
||||
|
||||
if timer.Exists("darkrp_check_mysql_status") then timer.Remove("darkrp_check_mysql_status") end
|
||||
|
||||
databaseObject.onConnectionFailed = function(_, msg)
|
||||
timer.Simple(5, function()
|
||||
msOOConnect(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
|
||||
end)
|
||||
error("Connection failed! " .. tostring(msg) .. "\nTrying again in 5 seconds.")
|
||||
end
|
||||
|
||||
databaseObject.onConnected = onConnected
|
||||
|
||||
databaseObject:connect()
|
||||
end
|
||||
|
||||
local function tmsqlConnect(host, username, password, database_name, database_port)
|
||||
local db, err = TMySQL.Connect(host, username, password, database_name, database_port, nil, MySQLite_config.MultiStatements and multistatements or nil)
|
||||
if err then error("Connection failed! " .. err .. "\n") end
|
||||
|
||||
databaseObject = db
|
||||
onConnected()
|
||||
|
||||
if (TMySQL.Version and TMySQL.Version >= 4.1) then
|
||||
hook.Add("Think", "MySQLite:tmysqlPoll", function()
|
||||
db:Poll()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function connectToMySQL(host, username, password, database_name, database_port)
|
||||
database_port = database_port or 3306
|
||||
local func = mysqlOO and msOOConnect or TMySQL and tmsqlConnect or function() end
|
||||
func(host, username, password, database_name, database_port)
|
||||
end
|
||||
|
||||
function SQLStr(sqlStr)
|
||||
local escape =
|
||||
not CONNECTED_TO_MYSQL and sql.SQLStr or
|
||||
mysqlOO and function(str) return "\"" .. databaseObject:escape(tostring(str)) .. "\"" end or
|
||||
TMySQL and function(str) return "\"" .. databaseObject:Escape(tostring(str)) .. "\"" end
|
||||
|
||||
return escape(sqlStr)
|
||||
end
|
||||
|
||||
function tableExists(tbl, callback, errorCallback)
|
||||
if not CONNECTED_TO_MYSQL then
|
||||
local exists = sql.TableExists(tbl)
|
||||
callback(exists)
|
||||
|
||||
return exists
|
||||
end
|
||||
|
||||
queryValue(string.format("SHOW TABLES LIKE %s", SQLStr(tbl)), function(v)
|
||||
callback(v ~= nil)
|
||||
end, errorCallback)
|
||||
end
|
||||
362
gamemodes/darkrp/gamemode/libraries/sh_cami.lua
Normal file
362
gamemodes/darkrp/gamemode/libraries/sh_cami.lua
Normal file
@@ -0,0 +1,362 @@
|
||||
--[[
|
||||
CAMI - Common Admin Mod Interface.
|
||||
Copyright 2020 CAMI Contributors
|
||||
|
||||
Makes admin mods intercompatible and provides an abstract privilege interface
|
||||
for third party addons.
|
||||
|
||||
Follows the specification on this page:
|
||||
https://github.com/glua/CAMI/blob/master/README.md
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
|
||||
-- Version number in YearMonthDay format.
|
||||
local version = 20211019
|
||||
|
||||
if CAMI and CAMI.Version >= version then return end
|
||||
|
||||
CAMI = CAMI or {}
|
||||
CAMI.Version = version
|
||||
|
||||
|
||||
--- @class CAMI_USERGROUP
|
||||
--- defines the charactaristics of a usergroup
|
||||
--- @field Name string @The name of the usergroup
|
||||
--- @field Inherits string @The name of the usergroup this usergroup inherits from
|
||||
--- @field CAMI_Source string @The source specified by the admin mod which registered this usergroup (if any, converted to a string)
|
||||
|
||||
--- @class CAMI_PRIVILEGE
|
||||
--- defines the charactaristics of a privilege
|
||||
--- @field Name string @The name of the privilege
|
||||
--- @field MinAccess "'user'" | "'admin'" | "'superadmin'" @Default group that should have this privilege
|
||||
--- @field Description string | nil @Optional text describing the purpose of the privilege
|
||||
local CAMI_PRIVILEGE = {}
|
||||
--- Optional function to check if a player has access to this privilege
|
||||
--- (and optionally execute it on another player)
|
||||
---
|
||||
--- ⚠ **Warning**: This function may not be called by all admin mods
|
||||
--- @param actor GPlayer @The player
|
||||
--- @param target GPlayer | nil @Optional - the target
|
||||
--- @return boolean @If they can or not
|
||||
--- @return string | nil @Optional reason
|
||||
function CAMI_PRIVILEGE:HasAccess(actor, target)
|
||||
end
|
||||
|
||||
--- Contains the registered CAMI_USERGROUP usergroup structures.
|
||||
--- Indexed by usergroup name.
|
||||
--- @type CAMI_USERGROUP[]
|
||||
local usergroups = CAMI.GetUsergroups and CAMI.GetUsergroups() or {
|
||||
user = {
|
||||
Name = "user",
|
||||
Inherits = "user",
|
||||
CAMI_Source = "Garry's Mod",
|
||||
},
|
||||
admin = {
|
||||
Name = "admin",
|
||||
Inherits = "user",
|
||||
CAMI_Source = "Garry's Mod",
|
||||
},
|
||||
superadmin = {
|
||||
Name = "superadmin",
|
||||
Inherits = "admin",
|
||||
CAMI_Source = "Garry's Mod",
|
||||
}
|
||||
}
|
||||
|
||||
--- Contains the registered CAMI_PRIVILEGE privilege structures.
|
||||
--- Indexed by privilege name.
|
||||
--- @type CAMI_PRIVILEGE[]
|
||||
local privileges = CAMI.GetPrivileges and CAMI.GetPrivileges() or {}
|
||||
|
||||
--- Registers a usergroup with CAMI.
|
||||
---
|
||||
--- Use the source parameter to make sure CAMI.RegisterUsergroup function and
|
||||
--- the CAMI.OnUsergroupRegistered hook don't cause an infinite loop
|
||||
--- @param usergroup CAMI_USERGROUP @The structure for the usergroup you want to register
|
||||
--- @param source any @Identifier for your own admin mod. Can be anything.
|
||||
--- @return CAMI_USERGROUP @The usergroup given as an argument
|
||||
function CAMI.RegisterUsergroup(usergroup, source)
|
||||
if source then
|
||||
usergroup.CAMI_Source = tostring(source)
|
||||
end
|
||||
usergroups[usergroup.Name] = usergroup
|
||||
|
||||
hook.Call("CAMI.OnUsergroupRegistered", nil, usergroup, source)
|
||||
return usergroup
|
||||
end
|
||||
|
||||
--- Unregisters a usergroup from CAMI. This will call a hook that will notify
|
||||
--- all other admin mods of the removal.
|
||||
---
|
||||
--- ⚠ **Warning**: Call only when the usergroup is to be permanently removed.
|
||||
---
|
||||
--- Use the source parameter to make sure CAMI.UnregisterUsergroup function and
|
||||
--- the CAMI.OnUsergroupUnregistered hook don't cause an infinite loop
|
||||
--- @param usergroupName string @The name of the usergroup.
|
||||
--- @param source any @Identifier for your own admin mod. Can be anything.
|
||||
--- @return boolean @Whether the unregistering succeeded.
|
||||
function CAMI.UnregisterUsergroup(usergroupName, source)
|
||||
if not usergroups[usergroupName] then return false end
|
||||
|
||||
local usergroup = usergroups[usergroupName]
|
||||
usergroups[usergroupName] = nil
|
||||
|
||||
hook.Call("CAMI.OnUsergroupUnregistered", nil, usergroup, source)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Retrieves all registered usergroups.
|
||||
--- @return CAMI_USERGROUP[] @Usergroups indexed by their names.
|
||||
function CAMI.GetUsergroups()
|
||||
return usergroups
|
||||
end
|
||||
|
||||
--- Receives information about a usergroup.
|
||||
--- @param usergroupName string
|
||||
--- @return CAMI_USERGROUP | nil @Returns nil when the usergroup does not exist.
|
||||
function CAMI.GetUsergroup(usergroupName)
|
||||
return usergroups[usergroupName]
|
||||
end
|
||||
|
||||
--- Checks to see if potentialAncestor is an ancestor of usergroupName.
|
||||
--- All usergroups are ancestors of themselves.
|
||||
---
|
||||
--- Examples:
|
||||
--- * `user` is an ancestor of `admin` and also `superadmin`
|
||||
--- * `admin` is an ancestor of `superadmin`, but not `user`
|
||||
--- @param usergroupName string @The usergroup to query
|
||||
--- @param potentialAncestor string @The ancestor to query
|
||||
--- @return boolean @Whether usergroupName inherits potentialAncestor.
|
||||
function CAMI.UsergroupInherits(usergroupName, potentialAncestor)
|
||||
repeat
|
||||
if usergroupName == potentialAncestor then return true end
|
||||
|
||||
usergroupName = usergroups[usergroupName] and
|
||||
usergroups[usergroupName].Inherits or
|
||||
usergroupName
|
||||
until not usergroups[usergroupName] or
|
||||
usergroups[usergroupName].Inherits == usergroupName
|
||||
|
||||
-- One can only be sure the usergroup inherits from user if the
|
||||
-- usergroup isn't registered.
|
||||
return usergroupName == potentialAncestor or potentialAncestor == "user"
|
||||
end
|
||||
|
||||
--- Find the base group a usergroup inherits from.
|
||||
---
|
||||
--- This function traverses down the inheritence chain, so for example if you have
|
||||
--- `user` -> `group1` -> `group2`
|
||||
--- this function will return `user` if you pass it `group2`.
|
||||
---
|
||||
--- ℹ **NOTE**: All usergroups must eventually inherit either user, admin or superadmin.
|
||||
--- @param usergroupName string @The name of the usergroup
|
||||
--- @return "'user'" | "'admin'" | "'superadmin'" @The name of the root usergroup
|
||||
function CAMI.InheritanceRoot(usergroupName)
|
||||
if not usergroups[usergroupName] then return end
|
||||
|
||||
local inherits = usergroups[usergroupName].Inherits
|
||||
while inherits ~= usergroups[usergroupName].Inherits do
|
||||
usergroupName = usergroups[usergroupName].Inherits
|
||||
end
|
||||
|
||||
return usergroupName
|
||||
end
|
||||
|
||||
--- Registers an addon privilege with CAMI.
|
||||
---
|
||||
--- ⚠ **Warning**: This should only be used by addons. Admin mods must *NOT*
|
||||
--- register their privileges using this function.
|
||||
--- @param privilege CAMI_PRIVILEGE
|
||||
--- @return CAMI_PRIVILEGE @The privilege given as argument.
|
||||
function CAMI.RegisterPrivilege(privilege)
|
||||
privileges[privilege.Name] = privilege
|
||||
|
||||
hook.Call("CAMI.OnPrivilegeRegistered", nil, privilege)
|
||||
|
||||
return privilege
|
||||
end
|
||||
|
||||
--- Unregisters a privilege from CAMI.
|
||||
--- This will call a hook that will notify any admin mods of the removal.
|
||||
---
|
||||
--- ⚠ **Warning**: Call only when the privilege is to be permanently removed.
|
||||
--- @param privilegeName string @The name of the privilege.
|
||||
--- @return boolean @Whether the unregistering succeeded.
|
||||
function CAMI.UnregisterPrivilege(privilegeName)
|
||||
if not privileges[privilegeName] then return false end
|
||||
|
||||
local privilege = privileges[privilegeName]
|
||||
privileges[privilegeName] = nil
|
||||
|
||||
hook.Call("CAMI.OnPrivilegeUnregistered", nil, privilege)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Retrieves all registered privileges.
|
||||
--- @return CAMI_PRIVILEGE[] @All privileges indexed by their names.
|
||||
function CAMI.GetPrivileges()
|
||||
return privileges
|
||||
end
|
||||
|
||||
--- Receives information about a privilege.
|
||||
--- @param privilegeName string
|
||||
--- @return CAMI_PRIVILEGE | nil
|
||||
function CAMI.GetPrivilege(privilegeName)
|
||||
return privileges[privilegeName]
|
||||
end
|
||||
|
||||
-- Default access handler
|
||||
local defaultAccessHandler = {["CAMI.PlayerHasAccess"] =
|
||||
function(_, actorPly, privilegeName, callback, targetPly, extraInfoTbl)
|
||||
-- The server always has access in the fallback
|
||||
if not IsValid(actorPly) then return callback(true, "Fallback.") end
|
||||
|
||||
local priv = privileges[privilegeName]
|
||||
|
||||
local fallback = extraInfoTbl and (
|
||||
not extraInfoTbl.Fallback and actorPly:IsAdmin() or
|
||||
extraInfoTbl.Fallback == "user" and true or
|
||||
extraInfoTbl.Fallback == "admin" and actorPly:IsAdmin() or
|
||||
extraInfoTbl.Fallback == "superadmin" and actorPly:IsSuperAdmin())
|
||||
|
||||
|
||||
if not priv then return callback(fallback, "Fallback.") end
|
||||
|
||||
local hasAccess =
|
||||
priv.MinAccess == "user" or
|
||||
priv.MinAccess == "admin" and actorPly:IsAdmin() or
|
||||
priv.MinAccess == "superadmin" and actorPly:IsSuperAdmin()
|
||||
|
||||
if hasAccess and priv.HasAccess then
|
||||
hasAccess = priv:HasAccess(actorPly, targetPly)
|
||||
end
|
||||
|
||||
callback(hasAccess, "Fallback.")
|
||||
end,
|
||||
["CAMI.SteamIDHasAccess"] =
|
||||
function(_, _, _, callback)
|
||||
callback(false, "No information available.")
|
||||
end
|
||||
}
|
||||
|
||||
--- @class CAMI_ACCESS_EXTRA_INFO
|
||||
--- @field Fallback "'user'" | "'admin'" | "'superadmin'" @Fallback status for if the privilege doesn't exist. Defaults to `admin`.
|
||||
--- @field IgnoreImmunity boolean @Ignore any immunity mechanisms an admin mod might have.
|
||||
--- @field CommandArguments table @Extra arguments that were given to the privilege command.
|
||||
|
||||
--- Checks if a player has access to a privilege
|
||||
--- (and optionally can execute it on targetPly)
|
||||
---
|
||||
--- This function is designed to be asynchronous but will be invoked
|
||||
--- synchronously if no callback is passed.
|
||||
---
|
||||
--- ⚠ **Warning**: If the currently installed admin mod does not support
|
||||
--- synchronous queries, this function will throw an error!
|
||||
--- @param actorPly GPlayer @The player to query
|
||||
--- @param privilegeName string @The privilege to query
|
||||
--- @param callback fun(hasAccess: boolean, reason: string|nil) @Callback to receive the answer, or nil for synchronous
|
||||
--- @param targetPly GPlayer | nil @Optional - target for if the privilege effects another player (eg kick/ban)
|
||||
--- @param extraInfoTbl CAMI_ACCESS_EXTRA_INFO | nil @Table of extra information for the admin mod
|
||||
--- @return boolean | nil @Synchronous only - if the player has the privilege
|
||||
--- @return string | nil @Synchronous only - optional reason from admin mod
|
||||
function CAMI.PlayerHasAccess(actorPly, privilegeName, callback, targetPly,
|
||||
extraInfoTbl)
|
||||
local hasAccess, reason = nil, nil
|
||||
local callback_ = callback or function(hA, r) hasAccess, reason = hA, r end
|
||||
|
||||
hook.Call("CAMI.PlayerHasAccess", defaultAccessHandler, actorPly,
|
||||
privilegeName, callback_, targetPly, extraInfoTbl)
|
||||
|
||||
if callback ~= nil then return end
|
||||
|
||||
if hasAccess == nil then
|
||||
local err = [[The function CAMI.PlayerHasAccess was used to find out
|
||||
whether Player %s has privilege "%s", but an admin mod did not give an
|
||||
immediate answer!]]
|
||||
error(string.format(err,
|
||||
actorPly:IsPlayer() and actorPly:Nick() or tostring(actorPly),
|
||||
privilegeName))
|
||||
end
|
||||
|
||||
return hasAccess, reason
|
||||
end
|
||||
|
||||
--- Get all the players on the server with a certain privilege
|
||||
--- (and optionally who can execute it on targetPly)
|
||||
---
|
||||
--- ℹ **NOTE**: This is an asynchronous function!
|
||||
--- @param privilegeName string @The privilege to query
|
||||
--- @param callback fun(players: GPlayer[]) @Callback to receive the answer
|
||||
--- @param targetPly GPlayer | nil @Optional - target for if the privilege effects another player (eg kick/ban)
|
||||
--- @param extraInfoTbl CAMI_ACCESS_EXTRA_INFO | nil @Table of extra information for the admin mod
|
||||
function CAMI.GetPlayersWithAccess(privilegeName, callback, targetPly,
|
||||
extraInfoTbl)
|
||||
local allowedPlys = {}
|
||||
local allPlys = player.GetAll()
|
||||
local countdown = #allPlys
|
||||
|
||||
local function onResult(ply, hasAccess, _)
|
||||
countdown = countdown - 1
|
||||
|
||||
if hasAccess then table.insert(allowedPlys, ply) end
|
||||
if countdown == 0 then callback(allowedPlys) end
|
||||
end
|
||||
|
||||
for _, ply in ipairs(allPlys) do
|
||||
CAMI.PlayerHasAccess(ply, privilegeName,
|
||||
function(...) onResult(ply, ...) end,
|
||||
targetPly, extraInfoTbl)
|
||||
end
|
||||
end
|
||||
|
||||
--- @class CAMI_STEAM_ACCESS_EXTRA_INFO
|
||||
--- @field IgnoreImmunity boolean @Ignore any immunity mechanisms an admin mod might have.
|
||||
--- @field CommandArguments table @Extra arguments that were given to the privilege command.
|
||||
|
||||
--- Checks if a (potentially offline) SteamID has access to a privilege
|
||||
--- (and optionally if they can execute it on a target SteamID)
|
||||
---
|
||||
--- ℹ **NOTE**: This is an asynchronous function!
|
||||
--- @param actorSteam string | nil @The SteamID to query
|
||||
--- @param privilegeName string @The privilege to query
|
||||
--- @param callback fun(hasAccess: boolean, reason: string|nil) @Callback to receive the answer
|
||||
--- @param targetSteam string | nil @Optional - target SteamID for if the privilege effects another player (eg kick/ban)
|
||||
--- @param extraInfoTbl CAMI_STEAM_ACCESS_EXTRA_INFO | nil @Table of extra information for the admin mod
|
||||
function CAMI.SteamIDHasAccess(actorSteam, privilegeName, callback,
|
||||
targetSteam, extraInfoTbl)
|
||||
hook.Call("CAMI.SteamIDHasAccess", defaultAccessHandler, actorSteam,
|
||||
privilegeName, callback, targetSteam, extraInfoTbl)
|
||||
end
|
||||
|
||||
--- Signify that your admin mod has changed the usergroup of a player. This
|
||||
--- function communicates to other admin mods what it thinks the usergroup
|
||||
--- of a player should be.
|
||||
---
|
||||
--- Listen to the hook to receive the usergroup changes of other admin mods.
|
||||
--- @param ply GPlayer @The player for which the usergroup is changed
|
||||
--- @param old string @The previous usergroup of the player.
|
||||
--- @param new string @The new usergroup of the player.
|
||||
--- @param source any @Identifier for your own admin mod. Can be anything.
|
||||
function CAMI.SignalUserGroupChanged(ply, old, new, source)
|
||||
hook.Call("CAMI.PlayerUsergroupChanged", nil, ply, old, new, source)
|
||||
end
|
||||
|
||||
--- Signify that your admin mod has changed the usergroup of a disconnected
|
||||
--- player. This communicates to other admin mods what it thinks the usergroup
|
||||
--- of a player should be.
|
||||
---
|
||||
--- Listen to the hook to receive the usergroup changes of other admin mods.
|
||||
--- @param steamId string @The steam ID of the player for which the usergroup is changed
|
||||
--- @param old string @The previous usergroup of the player.
|
||||
--- @param new string @The new usergroup of the player.
|
||||
--- @param source any @Identifier for your own admin mod. Can be anything.
|
||||
function CAMI.SignalSteamIDUserGroupChanged(steamId, old, new, source)
|
||||
hook.Call("CAMI.SteamIDUsergroupChanged", nil, steamId, old, new, source)
|
||||
end
|
||||
557
gamemodes/darkrp/gamemode/libraries/simplerr.lua
Normal file
557
gamemodes/darkrp/gamemode/libraries/simplerr.lua
Normal file
@@ -0,0 +1,557 @@
|
||||
local CompileFile = CompileFile
|
||||
local CompileString = CompileString
|
||||
local debug = debug
|
||||
local error = error
|
||||
local file = file
|
||||
local hook = hook
|
||||
local include = include
|
||||
local isfunction = isfunction
|
||||
local isstring = isstring
|
||||
local math = math
|
||||
local os = os
|
||||
local string = string
|
||||
local table = table
|
||||
local tonumber = tonumber
|
||||
local unpack = unpack
|
||||
local xpcall = xpcall
|
||||
|
||||
-- Template for syntax errors
|
||||
-- The [ERROR] start of it cannot be removed, because that would make the
|
||||
-- error mechanism remove all square brackets. Only Garry can make that bullshit up.
|
||||
local synErrTranslation = [=[[ERROR] Lua is unable to understand file "%s" because its author made a mistake around line number %i.
|
||||
The best help I can give you is this:
|
||||
|
||||
%s
|
||||
|
||||
Hints:
|
||||
%s
|
||||
|
||||
------- End of Simplerr error -------
|
||||
]=] -- The end is a special string by which simplerr errors are internally recognised
|
||||
|
||||
-- Template for runtime errors
|
||||
local runErrTranslation = [=[[ERROR] A runtime error has occurred in "%s" on line %i.
|
||||
The best help I can give you is this:
|
||||
|
||||
%s
|
||||
|
||||
Hints:
|
||||
%s
|
||||
|
||||
The responsibility for the error above lies with (the authors of) one (or more) of these files:
|
||||
%s
|
||||
------- End of Simplerr error -------
|
||||
]=]
|
||||
|
||||
-- Structure that contains syntax errors and their translations. Catches only the most common errors.
|
||||
-- Order is important: the structure with the first match is taken.
|
||||
local synErrs = {
|
||||
{
|
||||
match = "'=' expected near '(.*)'",
|
||||
text = "Right before the '%s', Lua expected to read an '='-sign, but it didn't.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"Did you simply forget the '='-sign?",
|
||||
"Did you forget a comma?",
|
||||
"Is this supposed to be a local variable?"
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "'.' expected [(]to close '([{[(])' at line ([0-9-]+)[)] near '(.*)'",
|
||||
text = "There is an opening '%s' bracket at line %i, but this bracket is never closed or not closed in time. It was expected to be closed before the '%s' at line %i.",
|
||||
format = function(m, l) return m[1], m[2], m[3], l end,
|
||||
hints = {
|
||||
"Did you forget a comma?",
|
||||
"All open brackets ({, (, [) must have a matching closing bracket. Are you sure it's there?",
|
||||
"Brackets must be opened and closed in the right order. This will work: ({}), but this won't: ({)}."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "'end' expected [(]to close '(.*)' at line ([0-9-]+)[)] near '(.*)'",
|
||||
text = "An '%s' was started on line %i, but it was never ended or not ended in time. It was expected to be ended before the '%s' at line %i",
|
||||
format = function(m, l) return m[1], m[2], m[3], l end,
|
||||
hints = {
|
||||
"For every if/for/do/while/function there must be an 'end' that closes it."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "unfinished string near '(.*)'",
|
||||
text = "The string '%s' at line %i is opened, but not closed.",
|
||||
format = function(m, l) return m[1], l end,
|
||||
hints = {
|
||||
"A string is a different word for literal text.",
|
||||
"Strings must be in single or double quotation marks (e.g. 'example', \"example\")",
|
||||
"A third option for strings is for them to be in double square brackets.",
|
||||
"Whatever you use (quotations or square brackets), you must not forget that strings are enclosed within a pair of quotation marks/square brackets."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "unfinished long string near '(.*)'",
|
||||
text = "Lua expected to see the end of a multiline string somewhere before the '%s' at line %i.",
|
||||
format = function(m, l) return m[1], l end,
|
||||
hints = {
|
||||
"A string is a different word for literal text.",
|
||||
"Multiline strings are strings that span over multiple lines.",
|
||||
"Multiline strings must be enclosed by double square brackets.",
|
||||
"Whatever you use (quotations or square brackets), you must not forget that strings are enclosed within a pair of quotation marks/square brackets.",
|
||||
"If you used brackets, the source of the mistake may be somewhere above the reported line."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "unfinished long comment near '(.*)'",
|
||||
text = "Lua expected to see the end of a multiline comment somewhere before the '%s' at line %i.",
|
||||
format = function(m, l) return m[1], l end,
|
||||
hints = {
|
||||
"A comment is text ignored by Lua.",
|
||||
"Multiline comments are ones that span multiple lines.",
|
||||
"Multiline comments must be enclosed by either /* and */ or double square brackets.",
|
||||
"Whatever you use (/**/ or square brackets), you must not forget that once you start a comment, you must end it.",
|
||||
"The source of the mistake may be somewhere above the reported line."
|
||||
}
|
||||
},
|
||||
-- Generic error messages
|
||||
{
|
||||
match = "function arguments expected near '(.*)'",
|
||||
text = "A function is being called right before '%s', but its arguments are not given.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"Did you write 'something:otherthing'? Try changing it to 'something:otherthing()'"
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "unexpected symbol near '(.*)'",
|
||||
text = "Right before the '%s', Lua encountered something it could not make sense of.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {"Did you forget something here? (Perhaps a closing bracket)", "Is it a typo?"}
|
||||
},
|
||||
{
|
||||
match = "'(.*)' expected near '(.*)'",
|
||||
text = "Right before the '%s', Lua expected to read a '%s', but it didn't.",
|
||||
format = function(m) return m[2], m[1] end,
|
||||
hints = {"Did you forget a keyword?", "Did you forget a comma?"}
|
||||
},
|
||||
{
|
||||
match = "malformed number near '(.*)'",
|
||||
text = "Lua attempted to read '%s' as a number, but failed to do so.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"Numbers starting with '0x' are hexidecimal.",
|
||||
"Lua can get confused when doing '<number>..\"some text\"'. Try inserting a space between the number and the '..'."
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
-- Similar structure for runtime errors. Catches only the most common errors.
|
||||
-- Order is important: the structure with the first match is taken
|
||||
local runErrs = {
|
||||
{
|
||||
match = "table index is nil",
|
||||
text = "A table is being indexed by something that does not exist (table index is nil).", -- Requires improvement
|
||||
format = function() end,
|
||||
hints = {
|
||||
"The thing between square brackets does not exist (is nil)."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "table index is NaN",
|
||||
text = "A table is being indexed by something that is not really a number (table index is NaN).",
|
||||
format = function() end,
|
||||
hints = {
|
||||
"Did you divide zero by zero thinking it would be funny?"
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to index global '(.*)' [(]a nil value[)]",
|
||||
text = "'%s' is being indexed like it is a table, but in reality it does not exist (is nil).",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You either have 'something.somethingElse', 'something[somethingElse]' or 'something:somethingElse(more)'. The 'something' here does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to index global '(.*)' [(]a (.*) value[)]",
|
||||
text = "'%s' is being indexed like it is a table, but in reality it is a %s value.",
|
||||
format = function(m) return m[1], m[2] end,
|
||||
hints = {
|
||||
"You either have 'something.somethingElse' or 'something:somethingElse(more)'. The 'something' here is not a table."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to index a nil value",
|
||||
text = "Something is being indexed like it is a table, but in reality does not exist (is nil).",
|
||||
format = function() end,
|
||||
hints = {
|
||||
"You either have 'something.somethingElse', 'something[somethingElse]' or 'something:somethingElse(more)'. The 'something' here does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to index a (.*) value",
|
||||
text = "Something is being indexed like it is a table, but in reality it is a %s value.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You either have 'something.somethingElse', 'something[somethingElse]' or 'something:somethingElse(more)'. The 'something' here is not a table."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to call global '(.*)' [(]a nil value[)]",
|
||||
text = "'%s' is being called like it is a function, but in reality does not exist (is nil).",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You are doing something(<otherstuff>). The 'something' here does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to call a nil value",
|
||||
text = "Something is being called like it is a function, but in reality it does not exist (is nil).",
|
||||
format = function() end,
|
||||
hints = {
|
||||
"You are doing something(<otherstuff>). The 'something' here does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to call global '(.*)' [(]a (.*) value[)]",
|
||||
text = "'%s' is being called like it is a function, but in reality it is a %s.",
|
||||
format = function(m) return m[1], m[2] end,
|
||||
hints = {
|
||||
"You are doing something(<otherstuff>). The 'something' here is not a function."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to call a (.*) value",
|
||||
text = "Something is being called like it is a function, but in reality it is a %s.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You are doing something(<otherstuff>). The 'something' here is not a function."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to call field '(.*)' [(]a nil value[)]",
|
||||
text = "'%s' is being called like it is a function, but in reality it does not exist (is nil).",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You are doing either stuff.something(<otherstuff>) or stuff:something(<otherstuff>). The 'something' here does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to call field '(.*)' [(]a (.*) value[)]",
|
||||
text = "'%s' is being called like it is a function, but in reality it is a %s.",
|
||||
format = function(m) return m[1], m[2] end,
|
||||
hints = {
|
||||
"You are doing either stuff.something(<otherstuff>) or stuff:something(<otherstuff>). The 'something' here is not a function."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to concatenate global '(.*)' [(]a nil value[)]",
|
||||
text = "'%s' is being concatenated to something else, but '%s' does not exist (is nil).",
|
||||
format = function(m) return m[1], m[1] end,
|
||||
hints = {
|
||||
"Concatenation looks like this: something .. otherThing. Either something or otherThing does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to concatenate global '(.*)' [(]a (.*) value[)]",
|
||||
text = "'%s' is being concatenated to something else, but %s values cannot be concatenated.",
|
||||
format = function(m) return m[1], m[2] end,
|
||||
hints = {
|
||||
"Concatenation looks like this: something .. otherThing. Either something or otherThing is neither string nor number."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to concatenate a nil value",
|
||||
text = "Two (or more) things are being concatenated and one of them does not exist (is nil).",
|
||||
format = function() end,
|
||||
hints = {
|
||||
"Concatenation looks like this: something .. otherThing. Either something or otherThing does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to concatenate a (.*) value",
|
||||
text = "Two (or more) things are being concatenated and one of them is neither string nor number, but a %s.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"Concatenation looks like this: something .. otherThing. Either something or otherThing is neither string nor number."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "stack overflow",
|
||||
text = "The stack of function calls has overflowed",
|
||||
format = function() end,
|
||||
hints = {
|
||||
"Most likely infinite recursion.",
|
||||
"Do you have a function calling itself?"
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to compare two (.*) values",
|
||||
text = "A comparison is being made between two %s values. They cannot be compared.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"This error usually occurs when two incompatible things are being compared.",
|
||||
"'comparison' in this context means one of <, >, <=, >= (smaller than, greater than, etc.)"
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to compare (.*) with (.*)",
|
||||
text = "A comparison is being made between a %s and a %s. This is not possible.",
|
||||
format = function(m) return m[1], m[2] end,
|
||||
hints = {
|
||||
"This error usually occurs when two incompatible things are being compared.",
|
||||
"'Comparison' in this context means one of <, >, <=, >= (smaller than, greater than, etc.)"
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to perform arithmetic on a (.*) value",
|
||||
text = "Arithmetic operations are being performed on a %s. This is not possible.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"'Arithmetic' in this context means adding, multiplying, dividing, etc."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to get length of global '(.*)' [(]a nil value[)]",
|
||||
text = "The length of '%s' is requested as if it is a table, but in reality it does not exist (is nil).",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You are doing #something. The 'something' here is does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to get length of global '(.*)' [(]a (.*) value[)]",
|
||||
text = "The length of '%s' is requested as if it is a table, but in reality it is a %s.",
|
||||
format = function(m) return m[1], m[2] end,
|
||||
hints = {
|
||||
"You are doing #something. The 'something' here is not a table."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to get length of a nil value",
|
||||
text = "The length of something is requested as if it is a table, but in reality it does not exist (is nil).",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You are doing #something. The 'something' here is does not exist."
|
||||
}
|
||||
},
|
||||
{
|
||||
match = "attempt to get length of a (.*) value",
|
||||
text = "The length of something is requested as if it is a table, but in reality it is a %s.",
|
||||
format = function(m) return m[1] end,
|
||||
hints = {
|
||||
"You are doing #something. The 'something' here is not a table."
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
module("simplerr")
|
||||
|
||||
-- Get a nicely formatted stack trace. Start is where to start numbering
|
||||
-- stackMod allows the caller to modify the stack before it is numbered
|
||||
local function getStack(i, start, stackMod)
|
||||
i = i or 1
|
||||
start = start or 1
|
||||
local stack = {}
|
||||
|
||||
-- Invariant: stack level (i + count) >= 2 and <= last stack item
|
||||
for count = 1, math.huge do -- user visible count
|
||||
local info = debug.getinfo(i + count, "Sln")
|
||||
if not info then break end
|
||||
|
||||
local line = info.currentline or "unknown"
|
||||
if line == -1 and info.name then
|
||||
table.insert(stack, string.format("function '%s'", info.name))
|
||||
else
|
||||
table.insert(stack, string.format("%s on line %s", info.short_src, line))
|
||||
end
|
||||
end
|
||||
|
||||
-- Allow modification of the stack
|
||||
if stackMod then stack = stackMod(stack) end
|
||||
|
||||
-- add the numbering
|
||||
for count = 1, #stack do
|
||||
local stackLevel = start + count - 1
|
||||
stack[count] = string.format("\t%i. %s", stackLevel, stack[count])
|
||||
end
|
||||
|
||||
return table.concat(stack, "\n")
|
||||
end
|
||||
|
||||
-- Translate a runtime error to simplerr format.
|
||||
-- Decorate with e.g. wrapError to have it actually throw the error.
|
||||
function runError(msg, stackNr, hints, path, line, stack)
|
||||
stackNr = stackNr or 1
|
||||
hints = hints or {"No hints, sorry."}
|
||||
hints = "\t- " .. table.concat(hints, "\n\t- ")
|
||||
|
||||
if not path and not line then
|
||||
local info = debug.getinfo(stackNr + 1, "Sln") or debug.getinfo(stackNr, "Sln") or debug.getinfo(stackNr - 1, "Sln")
|
||||
path = info.short_src
|
||||
line = info.currentline
|
||||
end
|
||||
|
||||
return false, string.format(runErrTranslation, path, line, msg, hints, stack or getStack(stackNr + 1))
|
||||
end
|
||||
|
||||
-- Translate the message of an error
|
||||
local function translateMsg(msg, path, line, errs)
|
||||
local res
|
||||
local hints = {"No hints, sorry."}
|
||||
|
||||
for i = 1, #errs do
|
||||
local trans = errs[i]
|
||||
if not string.find(msg, trans.match) then continue end
|
||||
|
||||
-- translate <eof>
|
||||
msg = string.Replace(msg, "<eof>", "end of the file")
|
||||
|
||||
res = string.format(trans.text, trans.format({string.match(msg, trans.match)}, line, path))
|
||||
hints = trans.hints
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
return res or msg, "\t- " .. table.concat(hints, "\n\t- ")
|
||||
end
|
||||
|
||||
-- Translate an error into a language understandable by non-programmers
|
||||
local function translateError(path, line, err, translation, errs, stack)
|
||||
-- Using .* instead of path because path may be wrong when error is called
|
||||
local msg, hints = translateMsg(string.match(err, ".*:[0-9-]+: (.*)"), path, line, errs)
|
||||
local res = string.format(translation, path, line, msg, hints, stack)
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
-- Trims the [C] functions at the beginning of the stack
|
||||
local function trimStart(stack)
|
||||
while true do
|
||||
if string.StartWith(stack[1], "function ") then
|
||||
table.remove(stack, 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return stack
|
||||
end
|
||||
|
||||
-- safeCall uses xpcall, which has the downside that both xpcall and
|
||||
-- the safeCall function itself end up in the stack trace.
|
||||
-- This function removes them from the stack trace
|
||||
local function removeXpcall(stack)
|
||||
for i = #stack - 1, 1, -1 do
|
||||
if stack[i] == "function 'xpcall'" and string.find(stack[i + 1], "simplerr") then
|
||||
table.remove(stack, i)
|
||||
table.remove(stack, i) -- also remove the simplerr safeCall call
|
||||
|
||||
return stack
|
||||
end
|
||||
end
|
||||
|
||||
return stack
|
||||
end
|
||||
|
||||
-- Combines the two above functions
|
||||
local function stackModAggregate(stack)
|
||||
stack = trimStart(stack)
|
||||
return removeXpcall(stack)
|
||||
end
|
||||
|
||||
-- Used as the error handler in safeCall
|
||||
local function errorHandler(err, func)
|
||||
-- Investigate the stack. Not using err matching because calls to error can give a different path and line
|
||||
local stack = getStack(func and 1 or 2, 1, stackModAggregate) -- add called func to stack
|
||||
|
||||
-- Fetch the path and line number from the top of the stack
|
||||
local firstLine = string.sub(stack, 1, string.match(stack, "()\n") - 1)
|
||||
local path, line = string.match(firstLine, "\t[0-9-]+%. (.*) on line ([0-9-]+)")
|
||||
line = tonumber(line)
|
||||
|
||||
return {err, path, line, stack}
|
||||
end
|
||||
|
||||
-- Call a function and catch immediate runtime errors
|
||||
function safeCall(f, ...)
|
||||
-- Use xpcall so fetching of debug info is in the stack of the error rather than after it is unwound
|
||||
local res = {xpcall(f, errorHandler, ...)}
|
||||
|
||||
local succ, errInfo = res[1], res[2]
|
||||
|
||||
if succ then return unpack(res) end
|
||||
|
||||
-- This will only happen if the error is "not enough memory" or "error in error handling".
|
||||
-- The former tends to crash the game and the latter will mean it'll probably error in the next line.
|
||||
-- But we will try anyway.
|
||||
-- Note: stack trace will be less accurate.
|
||||
if isstring(errInfo) then errInfo = errorHandler(errInfo, f) end
|
||||
|
||||
-- Skip translation if the error is already a simplerr error
|
||||
-- This prevents nested simplerr errors when runError is called by a file loaded by runFile
|
||||
local mustTranslate = not string.find(errInfo[1], "------- End of Simplerr error -------")
|
||||
return false, mustTranslate and translateError(errInfo[2], errInfo[3], errInfo[1], runErrTranslation, runErrs, errInfo[4]) or errInfo[1]
|
||||
end
|
||||
|
||||
-- Run a file or explain its syntax errors in layman's terms
|
||||
-- Returns bool succeed, [string error]
|
||||
-- Do NOT use this on clientside files.
|
||||
-- Clientside files sent by the server cannot be read using file.Read unless you're the host of a listen server
|
||||
function runFile(path)
|
||||
if not file.Exists(path, "LUA") then error(string.format("Could not run file '%s' (file not found)", path)) end
|
||||
local contents = file.Read(path, "LUA")
|
||||
|
||||
-- Files can make a comment containing #NoSimplerr# to disable simplerr (and thus enable autorefresh)
|
||||
if string.find(contents, "#NoSimplerr#") then include(path) return true end
|
||||
|
||||
-- Catch syntax errors with CompileString
|
||||
local err = CompileString(contents, path, false)
|
||||
|
||||
-- CompileString returns the following string whenever a file is empty: Invalid script - or too short.
|
||||
-- It also prints: Not running script <path> - it's too short.
|
||||
-- If so, do nothing.
|
||||
if err == "Invalid script - or too short." then return true end
|
||||
|
||||
-- No syntax errors, check for immediate runtime errors using CompileFile
|
||||
-- Using the function CompileString returned leads to relative path trouble
|
||||
if isfunction(err) then return safeCall(CompileFile(path), path) end
|
||||
|
||||
-- Fetch the line number from the error
|
||||
local line = string.match(err, ".*:([0-9-]+): .*")
|
||||
line = tonumber(line)
|
||||
|
||||
return false, translateError(path, line, err, synErrTranslation, synErrs)
|
||||
end
|
||||
|
||||
-- Error wrapper: decorator for runFile and safeCall that throws an error on failure.
|
||||
-- Breaks execution. Must be the last decorator.
|
||||
function wrapError(succ, err, ...)
|
||||
if succ then return succ, err, ... end
|
||||
|
||||
error(err)
|
||||
end
|
||||
|
||||
-- Hook wrapper: Calls a hook on error
|
||||
function wrapHook(succ, err, ...)
|
||||
if not succ then hook.Call("onSimplerrError", nil, err) end
|
||||
|
||||
return succ, err, ...
|
||||
end
|
||||
|
||||
-- Logging wrapper: decorator for runFile and safeCall that logs failures.
|
||||
local log = {}
|
||||
function wrapLog(succ, err, ...)
|
||||
if succ then return succ, err, ... end
|
||||
|
||||
local data = {
|
||||
err = err,
|
||||
time = os.time()
|
||||
}
|
||||
|
||||
table.insert(log, data)
|
||||
|
||||
return succ, err, ...
|
||||
end
|
||||
|
||||
-- Retrieve the log
|
||||
function getLog() return log end
|
||||
|
||||
-- Clear the log
|
||||
function clearLog() log = {} end
|
||||
364
gamemodes/darkrp/gamemode/libraries/tablecheck.lua
Normal file
364
gamemodes/darkrp/gamemode/libraries/tablecheck.lua
Normal file
@@ -0,0 +1,364 @@
|
||||
--[[
|
||||
tablecheck
|
||||
|
||||
WIP
|
||||
|
||||
Author: FPtje Falco
|
||||
|
||||
Purpose:
|
||||
Allow validating tables by creating schemas of tables. Inspired by Joi (https://github.com/hapijs/joi)
|
||||
|
||||
Requires fn library (https://github.com/FPtje/GModFunctional),
|
||||
|
||||
Example:
|
||||
```lua
|
||||
local schema = tc.checkTable{
|
||||
name = tc.addHint(isstring, "The name must be a string!"),
|
||||
id = tc.addHint(isnumber, "The id must be a number!"),
|
||||
gender = tc.addHint(tc.oneOf{"male", "female", "carp"}, "Gender missing or not recognised!", {"Perhaps you are a carp?"}),
|
||||
}
|
||||
|
||||
local correct, err, hints = schema({name = "Dick", id = 3, gender = "carp"})
|
||||
print(correct) -- true
|
||||
|
||||
|
||||
local correct, err, hints = schema({name = "Dick", id = 3, gender = "crap"})
|
||||
print(correct) -- false
|
||||
print(err) -- Gender missing or not recognised!
|
||||
PrintTable(hints) -- {"Perhaps you are a carp?"}
|
||||
```
|
||||
|
||||
For further examples, including nesting and combining of schemas, please see the `unitTests` function for now.
|
||||
--]]
|
||||
|
||||
module("tc", package.seeall)
|
||||
|
||||
-- Helpers for quick access to metatables
|
||||
angle = FindMetaTable("Angle")
|
||||
convar = FindMetaTable("ConVar")
|
||||
effectdata = FindMetaTable("CEffectData")
|
||||
entity = FindMetaTable("Entity")
|
||||
file = FindMetaTable("File")
|
||||
imaterial = FindMetaTable("IMaterial")
|
||||
irestore = FindMetaTable("IRestore")
|
||||
isave = FindMetaTable("ISave")
|
||||
itexture = FindMetaTable("ITexture")
|
||||
lualocomotion = FindMetaTable("CLuaLocomotion")
|
||||
movedata = FindMetaTable("CMoveData")
|
||||
navarea = FindMetaTable("CNavArea")
|
||||
navladder = FindMetaTable("CNavLadder")
|
||||
nextbot = FindMetaTable("NextBot")
|
||||
npc = FindMetaTable("NPC")
|
||||
pathfollower = FindMetaTable("PathFollower")
|
||||
physobj = FindMetaTable("PhysObj")
|
||||
player = FindMetaTable("Player")
|
||||
recipientfilter = FindMetaTable("CRecipientFilter")
|
||||
soundpatch = FindMetaTable("CSoundPatch")
|
||||
takedamageinfo = FindMetaTable("CTakeDamageInfo")
|
||||
usercmd = FindMetaTable("CUserCmd")
|
||||
vector = FindMetaTable("Vector")
|
||||
vehicle = FindMetaTable("Vehicle")
|
||||
vmatrix = FindMetaTable("VMatrix")
|
||||
weapon = FindMetaTable("Weapon")
|
||||
|
||||
-- Assert function, asserts a property and returns the error if false.
|
||||
-- Allows f to override err and hints by simply returning them
|
||||
addHint = function(f, err, hints) return function(...)
|
||||
local res = {f(...)}
|
||||
res[2] = err
|
||||
res[3] = hints
|
||||
|
||||
return unpack(res)
|
||||
end end
|
||||
|
||||
--[[ Validates a table against a schema
|
||||
Capable of nesting
|
||||
--]]
|
||||
function checkTable(schema)
|
||||
return function(tbl)
|
||||
if not istable(tbl) then
|
||||
return false, "Not a table!"
|
||||
end
|
||||
|
||||
for k, v in pairs(schema or {}) do
|
||||
local correct, err, hints = tbl[v] ~= nil
|
||||
if isfunction(v) then correct, err, hints, replace, replaceWith = v(tbl[k], tbl) end
|
||||
|
||||
|
||||
if not correct then
|
||||
err = err or string.format("Element '%s' is corrupt!", k)
|
||||
return correct, err, hints
|
||||
end
|
||||
|
||||
-- Update the value
|
||||
if correct and replace == true and replaceWith then
|
||||
tbl[k] = replaceWith
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns whether a value is nil
|
||||
isnil = fn.Curry(fn.Eq, 2)(nil)
|
||||
|
||||
-- Returns whether a value is a color
|
||||
iscolor = IsColor
|
||||
|
||||
-- Returns true on the client
|
||||
client = function() return CLIENT end
|
||||
|
||||
-- returns true on the server
|
||||
server = function() return SERVER end
|
||||
|
||||
-- Optional value, when filled in it must meet the conditions
|
||||
optional = function(...) return fn.FOr{isnil, ...} end
|
||||
|
||||
-- Default value, implies optional. Only works in combination with tc.checkTable
|
||||
-- Note that the tc.addHint is to be the second parameter of default.
|
||||
-- tc.addHint(tc.default(x)) does NOT work, default(x, tc.addHint(...)) does.
|
||||
-- example: tc.checkTable{test = tc.default(3, tc.addHint(isnumber, "must be a number"))}
|
||||
-- example: tc.checkTable{test = tc.default(3)}
|
||||
default = function(def, f)
|
||||
return function(val, ...)
|
||||
if val == nil then
|
||||
-- second return value is the default value. Expects parent function to actually change the value
|
||||
return true, nil, nil, true, def
|
||||
end
|
||||
-- Return in if statement rather than "return f and f(val) or true" to allow multiple return values
|
||||
if f then return f(val, ...) else return true end
|
||||
end
|
||||
end
|
||||
|
||||
-- A table of which each element must meet condition f
|
||||
-- i.e. "this must be a table of xxx"
|
||||
-- example: tc.tableOf(isnumber) demands that the table contains only numbers
|
||||
tableOf = function(f) return function(tbl, parentTbl)
|
||||
if not istable(tbl) then return false end
|
||||
for _, v in pairs(tbl) do
|
||||
local res = {f(v, parentTbl)}
|
||||
if not res[1] then
|
||||
return unpack(res)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end end
|
||||
|
||||
-- Checks whether a value is amongst a given set of values
|
||||
-- exapmle: tc.oneOf{"jobs", "entities", "shipments", "weapons", "vehicles", "ammo"}
|
||||
oneOf = function(f) return fp{table.HasValue, f} end
|
||||
|
||||
-- A table that is non-empty, also useful for wrapping around tableOf
|
||||
-- example: tc.nonEmpty(tc.tableOf(isnumber))
|
||||
-- example: tc.nonEmpty() -- just checks that the table is non-empty
|
||||
nonEmpty = function(f) return function(tbl, parentTbl)
|
||||
if not istable(tbl) or table.IsEmpty(tbl) then return false end
|
||||
if not f then return true end
|
||||
return f(tbl, parentTbl)
|
||||
end end
|
||||
|
||||
-- Number check: minimum
|
||||
min = function(n) return fn.FAnd{isnumber, fp{fn.Lte, n}} end
|
||||
|
||||
-- Number check: maximum
|
||||
max = function(n) return fn.FAnd{isnumber, fp{fn.Gte, n}} end
|
||||
|
||||
-- Number check: positive
|
||||
positive = min(0)
|
||||
|
||||
-- Number check: negative
|
||||
negative = max(0)
|
||||
|
||||
|
||||
-- Whether the input matches regex
|
||||
-- Note: uses string.match, so it doesn't support full regex.
|
||||
-- May also allow numbers, since string.match also accepts numbers.
|
||||
-- Note, also matches on substrings. Use ^pattern$ for a full match.
|
||||
regex = function(pattern, startpos) return function(val)
|
||||
return (isstring(val) or isnumber(val)) and tobool(string.match(val, pattern, startpos))
|
||||
end end
|
||||
|
||||
-- Requires that the value only contains alphanumeric characters
|
||||
alphanum = regex("^[a-zA-Z0-9]+$")
|
||||
|
||||
|
||||
-- Test cases. Also serve as nice examples
|
||||
function unitTests()
|
||||
local id = 0
|
||||
|
||||
-- unit test helper functions
|
||||
local function checkCorrect(correct, err, hints)
|
||||
id = id + 1
|
||||
|
||||
if correct ~= true then
|
||||
print(id, "Incorrect value that should be correct!", correct, err, hints)
|
||||
if hints then PrintTable(hints) end
|
||||
return
|
||||
end
|
||||
|
||||
print(id, "Correct")
|
||||
end
|
||||
|
||||
local function checkIncorrect(correct, err, hints)
|
||||
id = id + 1
|
||||
|
||||
if correct then
|
||||
print(id, "Correct value that should be incorrect!", correct, err, hints)
|
||||
if hints then PrintTable(hints) end
|
||||
return
|
||||
end
|
||||
|
||||
print(id, "Correct")
|
||||
end
|
||||
|
||||
--[[
|
||||
Simple value schema. Checks whether the input is a number.
|
||||
]]
|
||||
local simpleSchema = tc.addHint(isnumber, "Must be a number!")
|
||||
|
||||
-- This is how a schema is to be used. Just call it with the value you want to check.
|
||||
-- In further unit tests, the schema function is immediately called inside the checkCorrect/checIncorrect call for brevity
|
||||
local correct, err, hints = simpleSchema(3)
|
||||
|
||||
checkCorrect(correct, err, hints)
|
||||
|
||||
|
||||
--[[
|
||||
Simple table schema
|
||||
]]
|
||||
local simpleTableSchema = tc.checkTable{
|
||||
name = tc.addHint(isstring, "The name must be a string!"),
|
||||
id = tc.addHint(isnumber, "The id must be a number!"),
|
||||
gender = tc.addHint(tc.oneOf{"male", "female", "carp"}, "Gender missing or not recognised!", {"Perhaps you are a carp?"}),
|
||||
nilthing = tc.addHint(tc.isnil, "nilthing must be nil"),
|
||||
nonEmpty = tc.addHint(tc.nonEmpty(tc.tableOf(isnumber)), "nonEmpty not table of numbers"),
|
||||
optnum = tc.addHint(tc.optional(isnumber), "optnum given, but not a number"),
|
||||
strnum = tc.addHint(fn.FOr{isstring, isnumber}, "strnum must either be a string or a number"),
|
||||
minmax = tc.addHint(fn.FAnd{tc.min(5), tc.max(10)}),
|
||||
pos = tc.addHint(tc.optional(tc.positive)),
|
||||
regx = tc.addHint(tc.optional(tc.regex("[a-z]+"))),
|
||||
letters = tc.addHint(tc.optional(tc.alphanum)),
|
||||
}
|
||||
|
||||
checkCorrect(simpleTableSchema({name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 5, regx = "asdf", letters = "asdfj", pos = 3}))
|
||||
|
||||
-- Counterexamples, should throw errors
|
||||
local badTables = {
|
||||
{},
|
||||
{name = 1, id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 7},
|
||||
{name = "Dick", id = "3", gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 7},
|
||||
{name = "Dick", id = 3, gender = "other", nonEmpty = {1,2,3}, strnum = "str", minmax = 7},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {}, strnum = "str", minmax = 7},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = {}, minmax = 7},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", optnum = "nope", minmax = 7},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 4},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 11},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str"},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 7, regx = "666"},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 7, regx = "asdf", letters = ">:D"},
|
||||
{name = "Dick", id = 3, gender = "carp", nonEmpty = {1,2,3}, strnum = "str", minmax = 7, regx = "asdf", letters = ">:D", pos = -1},
|
||||
}
|
||||
|
||||
for _, tbl in pairs(badTables) do
|
||||
checkIncorrect(simpleTableSchema(tbl))
|
||||
end
|
||||
|
||||
--[[
|
||||
Table Schema with no explicit keys
|
||||
]]
|
||||
local nokeysSchema = tc.checkTable{
|
||||
tc.addHint(isstring, "The first value must be a string."),
|
||||
tc.addHint(isnumber, "The second value must be a number!"),
|
||||
}
|
||||
checkCorrect(nokeysSchema({"string", 3}))
|
||||
|
||||
--[[
|
||||
Nested table schema
|
||||
]]
|
||||
local nestedSchema = tc.checkTable{
|
||||
nested = tc.checkTable{
|
||||
val = tc.addHint(isnumber, "'val' must be a number!")
|
||||
}
|
||||
}
|
||||
|
||||
checkCorrect(nestedSchema({nested = {val = 3}}))
|
||||
checkIncorrect(nestedSchema({}))
|
||||
|
||||
--[[
|
||||
Combining schemas using the fn library
|
||||
]]
|
||||
local andSchema = fn.FAnd{
|
||||
tc.checkTable{
|
||||
num = tc.addHint(isnumber, "num is not a number")
|
||||
},
|
||||
tc.checkTable{
|
||||
str = tc.addHint(isstring, "str is not a string")
|
||||
}
|
||||
}
|
||||
|
||||
checkCorrect(andSchema({num = 1, str = "string!"}))
|
||||
checkIncorrect(andSchema({num = 1}))
|
||||
checkIncorrect(andSchema({str = "string!"}))
|
||||
|
||||
local orSchema = fn.FOr{
|
||||
tc.checkTable{
|
||||
num = tc.addHint(isnumber, "num is not a number")
|
||||
},
|
||||
tc.checkTable{
|
||||
str = tc.addHint(isstring, "str is not a string")
|
||||
}
|
||||
}
|
||||
checkCorrect(orSchema({num = 1}))
|
||||
checkCorrect(orSchema({str = "string!"}))
|
||||
|
||||
--[[
|
||||
Default value with a check
|
||||
]]
|
||||
local withDefaultSchema = tc.checkTable{
|
||||
value = tc.default(10, tc.addHint(isnumber, "must be a number!"))
|
||||
}
|
||||
checkCorrect(withDefaultSchema({value = 30}))
|
||||
checkIncorrect(withDefaultSchema({value = "string"}))
|
||||
|
||||
local empty = {}
|
||||
checkCorrect(withDefaultSchema(empty))
|
||||
if empty.value ~= 10 then
|
||||
print("Default did NOT set the value to 10!")
|
||||
else
|
||||
print("Default test OK!")
|
||||
end
|
||||
|
||||
--[[
|
||||
Default value with no checks
|
||||
]]
|
||||
local withDefaultNoCheck = tc.checkTable{
|
||||
value = tc.default(10)
|
||||
}
|
||||
checkCorrect(withDefaultNoCheck({}))
|
||||
checkCorrect(withDefaultNoCheck({value = "string"}))
|
||||
|
||||
--[[
|
||||
Creating your own checker function that returns an error message
|
||||
When both the function and the tc.addHint define error messages, there's a conflict
|
||||
]]
|
||||
local function customCheck(val)
|
||||
return false, "function error message", {"function hint"}
|
||||
end
|
||||
|
||||
local customCheckSchema = tc.checkTable{
|
||||
value = tc.addHint(customCheck, "added error message", {"added hint"})
|
||||
}
|
||||
checkIncorrect(customCheckSchema{value = 1})
|
||||
checkIncorrect(customCheckSchema{})
|
||||
|
||||
_, err, hints = customCheckSchema{value = 2}
|
||||
if err ~= "added error message" or hints[1] ~= "added hint" then
|
||||
print("Wrong conflict solution", err, hints[1])
|
||||
else
|
||||
print("Conflict solution OK!")
|
||||
end
|
||||
|
||||
print("finished")
|
||||
end
|
||||
Reference in New Issue
Block a user