--[[--------------------------------------------------------------------------- Functions and variables ---------------------------------------------------------------------------]] local setUpNonOwnableDoors, setUpTeamOwnableDoors, setUpGroupDoors, migrateDB --[[--------------------------------------------------------- Database initialize ---------------------------------------------------------]] function DarkRP.initDatabase() MySQLite.begin() -- Gotta love the difference between SQLite and MySQL local is_mysql = MySQLite.isMySQL() local AUTOINCREMENT = is_mysql and "AUTO_INCREMENT" or "AUTOINCREMENT" -- in MySQL, the engine is set to InnoDB. InnoDB has been the default -- for a while, but people might be running old versions of MySQL. -- SQLite has no database engine, so it is not explicitly set. local ENGINE_INNODB = is_mysql and "ENGINE=InnoDB" or "" -- Table that holds all position data (jail, spawns etc.) -- Queue these queries because other queries depend on the existence of the darkrp_position table -- Race conditions could occur if the queries are executed simultaneously MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS darkrp_position( id INTEGER NOT NULL PRIMARY KEY ]] .. AUTOINCREMENT .. [[, map VARCHAR(45) NOT NULL, type CHAR(1) NOT NULL, x INTEGER NOT NULL, y INTEGER NOT NULL, z INTEGER NOT NULL ) ]] .. ENGINE_INNODB .. [[; ]]) -- team spawns require extra data MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS darkrp_jobspawn( id INTEGER NOT NULL PRIMARY KEY REFERENCES darkrp_position(id) ON UPDATE CASCADE ON DELETE CASCADE, teamcmd VARCHAR(255) NOT NULL ) ]] .. ENGINE_INNODB .. [[; ]]) -- This table is kept for compatibility with older addons and websites -- See https://github.com/FPtje/DarkRP/issues/819 MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS playerinformation( uid BIGINT NOT NULL, steamID VARCHAR(50) NOT NULL PRIMARY KEY ) ]] .. ENGINE_INNODB .. [[ ]]) -- Player information MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS darkrp_player( uid BIGINT NOT NULL PRIMARY KEY, rpname VARCHAR(45), salary INTEGER NOT NULL DEFAULT 45, wallet BIGINT NOT NULL ) ]] .. ENGINE_INNODB .. [[; ]]) -- Door data MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS darkrp_door( idx INTEGER NOT NULL, map VARCHAR(45) NOT NULL, title VARCHAR(25), isLocked BOOLEAN, isDisabled BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY(idx, map) ) ]] .. ENGINE_INNODB .. [[; ]]) -- Some doors are owned by certain teams MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS darkrp_doorjobs( idx INTEGER NOT NULL, map VARCHAR(45) NOT NULL, job VARCHAR(255) NOT NULL, PRIMARY KEY(idx, map, job) ) ]] .. ENGINE_INNODB .. [[; ]]) -- Door groups MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS darkrp_doorgroups( idx INTEGER NOT NULL, map VARCHAR(45) NOT NULL, doorgroup VARCHAR(100) NOT NULL, PRIMARY KEY(idx, map) ) ]] .. ENGINE_INNODB .. [[ ]]) MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS darkrp_dbversion(version INTEGER NOT NULL PRIMARY KEY) ]] .. ENGINE_INNODB .. [[ ]]) -- Load the last DBVersion into DarkRP.DBVersion, to allow checks to see whether migration is needed. MySQLite.queueQuery([[ SELECT MAX(version) AS version FROM darkrp_dbversion ]], function(data) -- The database is created with the schema of the latest version. On -- initialization the version is not set yet. Set it to the latest -- version. if not data or not data[1] or not tonumber(data[1].version) then DarkRP.DBVersion = 20211228 MySQLite.query([[ REPLACE INTO darkrp_dbversion VALUES(20211228) ]]) else DarkRP.DBVersion = tonumber(data[1].version) end end) MySQLite.commit(fp{migrateDB, -- Migrate the database function() -- Initialize the data after all the tables have been created setUpNonOwnableDoors() setUpTeamOwnableDoors() setUpGroupDoors() if MySQLite.isMySQL() then -- In a listen server, the connection with the external database is often made AFTER the listen server host has joined, --so he walks around with the settings from the SQLite database for _, v in ipairs(player.GetAll()) do DarkRP.offlinePlayerData(v:SteamID(), function(data) local Data = data and data[1] if not IsValid(v) or not Data then return end v:setDarkRPVar("rpname", Data.rpname) v:setSelfDarkRPVar("salary", Data.salary) v:setDarkRPVar("money", Data.wallet) end) end end hook.Call("DarkRPDBInitialized") end}) end --[[--------------------------------------------------------------------------- Database migration backwards compatibility with older versions of DarkRP ---------------------------------------------------------------------------]] function migrateDB(callback) -- Simple function that checks the database version, migrates if -- necessary, and recurses to perform the next migration, until the last -- migration has been performed. -- Calls callback when the migration is finished or not necessary. local function migrate(version) if version < 20160610 then MySQLite.begin() if MySQLite.isMySQL() then -- if only SQLite were this easy MySQLite.queueQuery([[DROP INDEX rpname ON darkrp_player]]) else -- darkrp_player used to have a UNIQUE rpname field. -- This sucks, get rid of it MySQLite.queueQuery([[PRAGMA foreign_keys=OFF]]) MySQLite.queueQuery([[ CREATE TABLE IF NOT EXISTS new_darkrp_player( uid BIGINT NOT NULL PRIMARY KEY, rpname VARCHAR(45), salary INTEGER NOT NULL DEFAULT 45, wallet INTEGER NOT NULL ); ]]) MySQLite.queueQuery([[INSERT INTO new_darkrp_player SELECT * FROM darkrp_player]]) MySQLite.queueQuery([[DROP TABLE darkrp_player]]) MySQLite.queueQuery([[ALTER TABLE new_darkrp_player RENAME TO darkrp_player]]) MySQLite.queueQuery([[PRAGMA foreign_keys=ON]]) end MySQLite.queueQuery([[REPLACE INTO darkrp_dbversion VALUES(20160610)]]) MySQLite.commit(fp{migrate, 20160610}) return end if version < 20181013 then -- migrate from darkrp_jobown to darkrp_doorjobs MySQLite.tableExists("darkrp_jobown", function(exists) if not exists then migrate(20181013) return end MySQLite.begin() -- Create a temporary table that links job IDs to job commands MySQLite.queueQuery("CREATE TABLE IF NOT EXISTS TempJobCommands(id INT NOT NULL PRIMARY KEY, cmd VARCHAR(255) NOT NULL);") if MySQLite.isMySQL() then local jobCommands = {} for k, v in pairs(RPExtraTeams) do table.insert(jobCommands, "(" .. k .. "," .. MySQLite.SQLStr(v.command) .. ")") end -- This WOULD work with SQLite if the implementation in GMod wasn't out of date. MySQLite.queueQuery("INSERT IGNORE INTO TempJobCommands VALUES " .. table.concat(jobCommands, ",") .. ";") else for k, v in pairs(RPExtraTeams) do MySQLite.queueQuery("INSERT INTO TempJobCommands VALUES(" .. k .. ", " .. MySQLite.SQLStr(v.command) .. ");") end end MySQLite.queueQuery("REPLACE INTO darkrp_doorjobs SELECT darkrp_jobown.idx AS idx, darkrp_jobown.map AS map, TempJobCommands.cmd AS job FROM darkrp_jobown JOIN TempJobCommands ON darkrp_jobown.job = TempJobCommands.id;") -- Clean up the transition table and the old table MySQLite.queueQuery("DROP TABLE TempJobCommands;") MySQLite.queueQuery("DROP TABLE darkrp_jobown;") MySQLite.queueQuery([[REPLACE INTO darkrp_dbversion VALUES(20181013)]]) MySQLite.commit(fp{migrate, 20181013}) end) return end if version < 20181014 then MySQLite.query([[SELECT * FROM darkrp_jobspawn]], function(oldData) oldData = oldData or {} MySQLite.begin() MySQLite.queueQuery([[DROP TABLE darkrp_jobspawn]]) MySQLite.queueQuery([[ CREATE TABLE darkrp_jobspawn( id INTEGER NOT NULL PRIMARY KEY REFERENCES darkrp_position(id) ON UPDATE CASCADE ON DELETE CASCADE, teamcmd VARCHAR(255) NOT NULL ); ]]) for i, row in pairs(oldData) do local teamcmd = (RPExtraTeams[tonumber(row.team)] or {}).command if not teamcmd then continue end MySQLite.queueQuery(string.format([[INSERT INTO darkrp_jobspawn(id, teamcmd) VALUES(%s, %s)]], row.id, MySQLite.SQLStr(teamcmd))) end MySQLite.queueQuery([[REPLACE INTO darkrp_dbversion VALUES(20181014)]]) MySQLite.commit(fp{migrate, 20181014}) end) return end if version < 20190914 then MySQLite.begin() -- Migration not necessary for SQLite, since BIGINT and -- INTEGER are considered the same in SQLite -- https://www.sqlite.org/datatype3.html if MySQLite.isMySQL() then MySQLite.queueQuery([[DROP TRIGGER IF EXISTS JobPositionFKDelete]]) MySQLite.queueQuery([[ALTER TABLE darkrp_player MODIFY wallet BIGINT;]]) end MySQLite.queueQuery([[REPLACE INTO darkrp_dbversion VALUES(20190914)]]) MySQLite.commit(fp{migrate, 20190914}) return end if version < 20211228 then MySQLite.begin() -- Migrate all tables to InnoDB if they weren't already. -- See https://github.com/FPtje/DarkRP/issues/3157 if MySQLite.isMySQL() then MySQLite.queueQuery([[ALTER TABLE darkrp_dbversion ENGINE = InnoDB;]]) MySQLite.queueQuery([[ALTER TABLE darkrp_door ENGINE = InnoDB;]]) MySQLite.queueQuery([[ALTER TABLE darkrp_doorgroups ENGINE = InnoDB;]]) MySQLite.queueQuery([[ALTER TABLE darkrp_doorjobs ENGINE = InnoDB;]]) MySQLite.queueQuery([[ALTER TABLE darkrp_jobspawn ENGINE = InnoDB;]]) MySQLite.queueQuery([[ALTER TABLE darkrp_player ENGINE = InnoDB;]]) MySQLite.queueQuery([[ALTER TABLE darkrp_position ENGINE = InnoDB;]]) MySQLite.queueQuery([[ALTER TABLE playerinformation ENGINE = InnoDB;]]) end MySQLite.queueQuery([[REPLACE INTO darkrp_dbversion VALUES(20211228)]]) MySQLite.commit(fp{migrate, 20211228}) return end -- All migrations finished callback() end migrate(DarkRP.DBVersion) end --[[--------------------------------------------------------- Players ---------------------------------------------------------]] function DarkRP.storeRPName(ply, name) if not name or string.len(name) < 2 then return end hook.Call("onPlayerChangedName", nil, ply, ply:getDarkRPVar("rpname"), name) ply:setDarkRPVar("rpname", name) MySQLite.query([[UPDATE darkrp_player SET rpname = ]] .. MySQLite.SQLStr(name) .. [[ WHERE UID = ]] .. ply:SteamID64() .. ";") MySQLite.query([[UPDATE darkrp_player SET rpname = ]] .. MySQLite.SQLStr(name) .. [[ WHERE UID = ]] .. ply:UniqueID() .. ";") end function DarkRP.retrieveRPNames(name, callback) MySQLite.query("SELECT COUNT(*) AS count FROM darkrp_player WHERE rpname = " .. MySQLite.SQLStr(name) .. ";", function(r) callback(tonumber(r[1].count) > 0) end) end function DarkRP.offlinePlayerData(steamid, callback, failed) local sid64 = util.SteamIDTo64(steamid) local uniqueid = util.CRC("gm_" .. string.upper(steamid) .. "_gm") MySQLite.query(string.format([[REPLACE INTO playerinformation VALUES(%s, %s);]], MySQLite.SQLStr(sid64), MySQLite.SQLStr(steamid)), nil, failed) local query = [[ SELECT rpname, wallet, salary, "SID64" AS kind FROM darkrp_player where uid = %s UNION SELECT rpname, wallet, salary, "UniqueID" AS kind FROM darkrp_player where uid = %s ; ]] MySQLite.query( query:format(sid64, uniqueid), function(data, ...) -- The database has no record of the player data in SteamID64 form -- Otherwise the first row would have kind SID64 if data and data[1] and data[1].kind == "UniqueID" then -- The rpname must be unique -- adding a new row with uid = SteamID64, but the same rpname will remove the uid=UniqueID row local replquery = [[ REPLACE INTO darkrp_player(uid, rpname, wallet, salary) VALUES (%s, %s, %s, %s) ]] MySQLite.begin() MySQLite.queueQuery( replquery:format( sid64, data[1].rpname == "NULL" and "NULL" or MySQLite.SQLStr(data[1].rpname), data[1].wallet, data[1].salary ), nil, failed ) MySQLite.commit() end return callback and callback(data, ...) end , failed ) end function DarkRP.retrievePlayerData(ply, callback, failed, attempts, err) attempts = attempts or 0 if attempts > 3 then return failed(err) end DarkRP.offlinePlayerData(ply:SteamID(), callback, function(sqlErr) if not IsValid(ply) then return end DarkRP.retrievePlayerData(ply, callback, failed, attempts + 1, sqlErr) end) end function DarkRP.createPlayerData(ply, name, wallet, salary, onError) MySQLite.query([[REPLACE INTO darkrp_player VALUES(]] .. ply:SteamID64() .. [[, ]] .. MySQLite.SQLStr(utf8.force(name)) .. [[, ]] .. salary .. [[, ]] .. wallet .. ");", nil, onError) -- Backwards compatibility MySQLite.query([[REPLACE INTO darkrp_player VALUES(]] .. ply:UniqueID() .. [[, ]] .. MySQLite.SQLStr(utf8.force(name)) .. [[, ]] .. salary .. [[, ]] .. wallet .. ");", nil, onError) end function DarkRP.storeMoney(ply, amount) if not isnumber(amount) or amount < 0 or amount >= 1 / 0 then DarkRP.errorNoHalt("Some addon attempted to store a invalid money amount " .. tostring(amount) .. " for Player " .. ply:Nick() .. " (" .. ply:SteamID() .. ")", 1, { "This money amount will not be stored in the database, but it may be set in the game.", "The database simply stores the last valid, non-negative amount of money.", "Please try to find the very first time this error happened for this player. Then look at the files mentioned in this error.", "That will tell you which addon is causing this.", "IMPORTANT: This is NOT a DarkRP bug!", "Note: The player can simply rejoin to fix their negative money, until whatever causes this happens again." }) return end -- Also keep deprecated UniqueID data at least somewhat up to date MySQLite.query([[UPDATE darkrp_player SET wallet = ]] .. amount .. [[ WHERE uid = ]] .. ply:UniqueID() .. [[ OR uid = ]] .. ply:SteamID64()) end function DarkRP.storeOfflineMoney(sid64, amount) if isnumber(sid64) or isstring(sid64) and string.len(sid64) < 17 then -- smaller than 76561197960265728 is not a SteamID64 DarkRP.errorNoHalt([[Some addon is giving DarkRP.storeOfflineMoney a UniqueID as its first argument, but this function now expects a SteamID64]], 1, { "The function used to take UniqueIDs, but it does not anymore.", "If you are a server owner, please look closely to the files mentioned in this error", "After all, these files will tell you WHICH addon is doing it", "This is NOT a DarkRP bug!", "Your server will continue working normally", "But whichever addon just tried to store an offline player's money", "Will NOT take effect!" }) end if not isnumber(amount) or amount < 0 or amount >= 1 / 0 then DarkRP.errorNoHalt("Some addon attempted to store a invalid money amount " .. tostring(amount) .. " for an offline player with steamID64 " .. sid64, 1, { "This money amount will not be stored in the database.", "Please try to find the very first time this error happened for this player. Then look at the files mentioned in this error.", "That will tell you which addon is causing this.", "IMPORTANT: This is NOT a DarkRP bug!" }) return end -- Also store on deprecated UniqueID local uniqueid = util.CRC("gm_" .. string.upper(util.SteamIDFrom64(sid64)) .. "_gm") MySQLite.query([[UPDATE darkrp_player SET wallet = ]] .. amount .. [[ WHERE uid = ]] .. uniqueid .. [[ OR uid = ]] .. sid64) end local function resetAllMoney(ply, cmd, args) if ply:EntIndex() ~= 0 and not ply:IsSuperAdmin() then return end MySQLite.query("UPDATE darkrp_player SET wallet = " .. GAMEMODE.Config.startingmoney .. " ;") for _, v in ipairs(player.GetAll()) do v:setDarkRPVar("money", GAMEMODE.Config.startingmoney) end if ply:IsPlayer() then DarkRP.notifyAll(0, 4, DarkRP.getPhrase("reset_money", ply:Nick())) else DarkRP.notifyAll(0, 4, DarkRP.getPhrase("reset_money", "Console")) end end concommand.Add("rp_resetallmoney", resetAllMoney) function DarkRP.storeSalary(ply, amount) ply:setSelfDarkRPVar("salary", math.floor(amount)) return amount end function DarkRP.retrieveSalary(ply, callback) local val = ply:getJobTable() and ply:getJobTable().salary or RPExtraTeams[GAMEMODE.DefaultTeam].salary or (GM or GAMEMODE).Config.normalsalary if callback then callback(val) end return val end --[[--------------------------------------------------------------------------- Players ---------------------------------------------------------------------------]] local meta = FindMetaTable("Player") function meta:restorePlayerData() self.DarkRPUnInitialized = true local function onError(err) if not IsValid(self) then return end self.DarkRPUnInitialized = true -- no information should be saved from here, or the playerdata might be reset self:setDarkRPVar("money", GAMEMODE.Config.startingmoney) self:setSelfDarkRPVar("salary", DarkRP.retrieveSalary(self)) local name = string.sub(string.gsub(self:SteamName(), "\\\"", "\""), 1, 30) self:setDarkRPVar("rpname", name) self.DarkRPDataRetrievalFailed = true -- marker on the player that says shit is fucked DarkRP.error("Failed to retrieve player information from the database. ", nil, {"This means your database or the connection to your database is fucked.", "This is the error given by the database:\n\t\t" .. tostring(err)}) end DarkRP.retrievePlayerData(self, function(data) if not IsValid(self) then return end self.DarkRPUnInitialized = nil local info = data and data[1] or {} if not info.rpname or info.rpname == "NULL" then info.rpname = string.sub(string.gsub(self:SteamName(), "\\\"", "\""), 1, 30) end info.wallet = info.wallet or GAMEMODE.Config.startingmoney info.salary = DarkRP.retrieveSalary(self) self:setDarkRPVar("money", tonumber(info.wallet)) self:setSelfDarkRPVar("salary", tonumber(info.salary)) self:setDarkRPVar("rpname", info.rpname) if not data then info = hook.Call("onPlayerFirstJoined", nil, self, info) or info DarkRP.createPlayerData(self, info.rpname, info.wallet, info.salary, onError) end end, onError) end --[[--------------------------------------------------------- Doors ---------------------------------------------------------]] function DarkRP.storeDoorData(ent) if not ent:CreatedByMap() then return end local map = string.lower(game.GetMap()) local nonOwnable = ent:getKeysNonOwnable() local title = ent:getKeysTitle() MySQLite.query([[REPLACE INTO darkrp_door VALUES(]] .. ent:doorIndex() .. [[, ]] .. MySQLite.SQLStr(map) .. [[, ]] .. (title and MySQLite.SQLStr(title) or "NULL") .. [[, ]] .. "NULL" .. [[, ]] .. (nonOwnable and 1 or 0) .. [[);]]) end function setUpNonOwnableDoors() MySQLite.query("SELECT idx, title, isLocked, isDisabled FROM darkrp_door WHERE map = " .. MySQLite.SQLStr(string.lower(game.GetMap())) .. ";", function(r) if not r then return end for _, row in pairs(r) do local e = DarkRP.doorIndexToEnt(tonumber(row.idx)) if not IsValid(e) then continue end if e:isKeysOwnable() then if tobool(row.isDisabled) then e:setKeysNonOwnable(tobool(row.isDisabled)) end if row.isLocked and row.isLocked ~= "NULL" then e:Fire((tobool(row.isLocked) and "" or "un") .. "lock", "", 0) end e:setKeysTitle(row.title ~= "NULL" and row.title or nil) end end end) end local keyValueActions = { ["DarkRPNonOwnable"] = function(ent, val) ent:setKeysNonOwnable(tobool(val)) end, ["DarkRPTitle"] = function(ent, val) ent:setKeysTitle(val) end, ["DarkRPDoorGroup"] = function(ent, val) if RPExtraTeamDoors[val] then ent:setDoorGroup(val) end end, ["DarkRPCanLockpick"] = function(ent, val) ent.DarkRPCanLockpick = tobool(val) end } local function onKeyValue(ent, key, value) if not ent:isDoor() then return end if keyValueActions[key] then keyValueActions[key](ent, value) end end hook.Add("EntityKeyValue", "darkrp_doors", onKeyValue) function DarkRP.storeTeamDoorOwnability(ent) if not ent:CreatedByMap() then return end local map = string.lower(game.GetMap()) MySQLite.query("DELETE FROM darkrp_doorjobs WHERE idx = " .. ent:doorIndex() .. " AND map = " .. MySQLite.SQLStr(map) .. ";") for k in pairs(ent:getKeysDoorTeams() or {}) do MySQLite.query("INSERT INTO darkrp_doorjobs VALUES(" .. ent:doorIndex() .. ", " .. MySQLite.SQLStr(map) .. ", " .. MySQLite.SQLStr(RPExtraTeams[k].command) .. ");") end end function setUpTeamOwnableDoors() MySQLite.query("SELECT idx, job FROM darkrp_doorjobs WHERE map = " .. MySQLite.SQLStr(string.lower(game.GetMap())) .. ";", function(r) if not r then return end local map = string.lower(game.GetMap()) for _, row in pairs(r) do row.idx = tonumber(row.idx) local e = DarkRP.doorIndexToEnt(row.idx) if not IsValid(e) then continue end local _, job = DarkRP.getJobByCommand(row.job) if job then e:addKeysDoorTeam(job) else print(("can't find job %s for door %d, removing from database"):format(row.job, row.idx)) MySQLite.query(("DELETE FROM darkrp_doorjobs WHERE idx = %d AND map = %s AND job = %s;"):format(row.idx, MySQLite.SQLStr(map), MySQLite.SQLStr(row.job))) end end end) end function DarkRP.storeDoorGroup(ent, group) if not ent:CreatedByMap() then return end local map = MySQLite.SQLStr(string.lower(game.GetMap())) local index = ent:doorIndex() if group == "" or not group then MySQLite.query("DELETE FROM darkrp_doorgroups WHERE map = " .. map .. " AND idx = " .. index .. ";") return end MySQLite.query("REPLACE INTO darkrp_doorgroups VALUES(" .. index .. ", " .. map .. ", " .. MySQLite.SQLStr(group) .. ");"); end function setUpGroupDoors() local map = MySQLite.SQLStr(string.lower(game.GetMap())) MySQLite.query("SELECT idx, doorgroup FROM darkrp_doorgroups WHERE map = " .. map, function(data) if not data then return end for _, row in pairs(data) do local ent = DarkRP.doorIndexToEnt(tonumber(row.idx)) if not IsValid(ent) or not ent:isKeysOwnable() then continue end if not RPExtraTeamDoorIDs[row.doorgroup] then continue end ent:setDoorGroup(row.doorgroup) end end) end hook.Add("PostCleanupMap", "DarkRP.hooks", function() setUpNonOwnableDoors() setUpTeamOwnableDoors() setUpGroupDoors() end)