Files
2026-03-15 14:54:49 +03:00

628 lines
26 KiB
Lua

--[[---------------------------------------------------------------------------
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)