Files
esx_core-esx-framework/server/main.lua
T
Linden b7c521c8e7 feat(server): database optimisations
Limited testing, but works in a fairly clean ESX environment. Sets up foreign keys in the database to allow for easy and optimised character deletion.

Foreign keys will only be set where owner and identifier columns exist, the field cannot be NULL and uses VARCHAR.
2022-02-12 06:03:23 +11:00

246 lines
7.7 KiB
Lua

if not ESX then
error('\n^1Unable to start Multicharacter - you must be using ESX Legacy^0')
elseif ESX.GetConfig().Multichar == true then
local DATABASE do
local connectionString = GetConvar('mysql_connection_string', '');
if connectionString == '' then
error(connectionString..'\n^1Unable to start Multicharacter - unable to determine database from mysql_connection_string^0', 0)
elseif connectionString:find('mysql://') then
connectionString = connectionString:sub(9, -1)
DATABASE = connectionString:sub(connectionString:find('/')+1, -1):gsub('[%?]+[%w%p]*$', '')
else
connectionString = {string.strsplit(';', connectionString)}
for i = 1, #connectionString do
local v = connectionString[i]
if v:match('database') then
DATABASE = v:sub(10, #v)
end
end
end
end
local DB_TABLES = {users = 'identifier'}
local FETCH = nil
local SLOTS = Config.Slots or 4
local PREFIX = Config.Prefix or 'char'
local PRIMARY_IDENTIFIER = ESX.GetConfig().Identifier or GetConvar('sv_lan', '') == 'true' and 'ip' or Config.Identifier
local function GetIdentifier(source)
local identifier = PRIMARY_IDENTIFIER..':'
for _, v in pairs(GetPlayerIdentifiers(source)) do
if string.match(v, identifier) then
identifier = string.gsub(v, identifier, '')
return identifier
end
end
end
if next(ESX.Players) then
local players = table.clone(ESX.Players)
table.wipe(ESX.Players)
for _, v in pairs(players) do
ESX.Players[GetIdentifier(v.source)] = true
end
else ESX.Players = {} end
local function SetupCharacters(source)
while not FETCH do Citizen.Wait(100) end
local identifier = GetIdentifier(source)
ESX.Players[identifier] = true
local slots = MySQL.scalar('SELECT slots FROM multicharacter_slots WHERE identifier = ?', { identifier }) or SLOTS
identifier = PREFIX..'%:'..identifier
local result = MySQL.query.await(FETCH, {identifier, slots})
local characters
if result then
local characterCount = #result
characters = table.create(0, characterCount)
for i = 1, characterCount, 1 do
local v = result[i]
local job, grade = v.job or 'unemployed', tostring(v.job_grade)
if ESX.Jobs[job] and ESX.Jobs[job].grades[grade] then
if job ~= 'unemployed' then grade = ESX.Jobs[job].grades[grade].label else grade = '' end
job = ESX.Jobs[job].label
end
local accounts = json.decode(v.accounts)
local id = tonumber(string.sub(v.identifier, #PREFIX+1, string.find(v.identifier, ':')-1))
characters[id] = {
id = id,
bank = accounts.bank,
money = accounts.money,
job = job,
job_grade = grade,
firstname = v.firstname,
lastname = v.lastname,
dateofbirth = v.dateofbirth,
skin = v.skin and json.decode(v.skin) or {},
disabled = v.disabled,
sex = v.sex == 'm' and _('male') or _('female')
}
end
end
TriggerClientEvent('esx_multicharacter:SetupUI', source, characters, slots)
end
AddEventHandler('playerConnecting', function(playerName, setKickReason, deferrals)
deferrals.defer()
local identifier = GetIdentifier(source)
if identifier then
if ESX.Players[identifier] then
deferrals.done(('A player is already connected to the server with this identifier.\nYour identifier: %s:%s'):format(PRIMARY_IDENTIFIER, identifier))
else
deferrals.done()
end
else
deferrals.done(('Unable to retrieve player identifier.\nIdentifier type: %s'):format(PRIMARY_IDENTIFIER))
end
end)
local function DeleteCharacter(source, charid)
local identifier = ('%s%s:%s'):format(PREFIX, charid, GetIdentifier(source))
local query = 'DELETE FROM %s WHERE %s = ?'
local queries = {}
local count = 0
for table, column in pairs(DB_TABLES) do
count += 1
queries[count] = {query = query:format(table, column), values = {identifier}}
end
MySQL.transaction(queries, function(result)
if result then
print(('[^2INFO^7] Player [%s] %s has deleted a character (%s)'):format(GetPlayerName(source), source, identifier))
Citizen.Wait(50)
SetupCharacters(source)
else
error('\n^1Transaction failed while trying to delete '..identifier..'^0')
end
end)
end
MySQL.ready(function()
local length = 42 + #PREFIX
local DB_COLUMNS = MySQL.query.await(('SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "%s" AND DATA_TYPE = "varchar" AND COLUMN_NAME IN (?)'):format(DATABASE, length), {
{'identifier', 'owner'}
})
if DB_COLUMNS then
local FK = MySQL.query.await(('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = "FOREIGN KEY" AND TABLE_SCHEMA = "%s"'):format(DATABASE))
local FOREIGN_KEY = {}
for i = 1, #FK do
FOREIGN_KEY[FK[i].TABLE_NAME] = true
end
local columns = {}
local count = 0
for i = 1, #DB_COLUMNS do
local v = DB_COLUMNS[i]
if v?.CHARACTER_MAXIMUM_LENGTH ~= length then
count += 1
columns[v.TABLE_NAME] = v.COLUMN_NAME
end
end
if next(columns) then
local query = 'ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(%s)'
local queries = table.create(count, 0)
for k, v in pairs(columns) do
queries[#queries+1] = {query = query:format(k, v, length)}
end
if MySQL.transaction.await(queries) then
print(('[^2INFO^7] Updated ^5%s^7 columns to use VARCHAR(%s)'):format(count, length))
else
print(('[^2INFO^7] Unable to update ^5%s^7 columns to use VARCHAR(%s)'):format(count, length))
end
end
for i = 1, #DB_COLUMNS do
local v = DB_COLUMNS[i]
if not FOREIGN_KEY[v.TABLE_NAME] then
DB_TABLES[v.TABLE_NAME] = v.COLUMN_NAME
if v.TABLE_NAME ~= 'users' and v.IS_NULLABLE == 'NO' then
MySQL.update(([[ALTER TABLE `%s`
ADD CONSTRAINT `FK_%s_users` FOREIGN KEY (`%s`) REFERENCES `users` (`identifier`) ON UPDATE CASCADE ON DELETE CASCADE]]):format(v.TABLE_NAME, v.TABLE_NAME, v.COLUMN_NAME), function(result)
if result then
print(('[^2INFO^7] Updated ^5"%s.%s"^7 to use FOREIGN_KEY referencing ^5"users.identifier"^7'):format(v.TABLE_NAME, v.COLUMN_NAME))
end
end)
end
end
end
ESX.Jobs = {}
local function GetJobs()
if ESX.GetJobs then
return ESX.GetJobs()
end
return exports.es_extended:getSharedObject().Jobs
end
repeat
ESX.Jobs = GetJobs()
Citizen.Wait(50)
until next(ESX.Jobs)
FETCH = 'SELECT identifier, accounts, job, job_grade, firstname, lastname, dateofbirth, sex, skin, disabled FROM users WHERE identifier LIKE ? LIMIT ?'
end
end)
RegisterNetEvent('esx_multicharacter:SetupCharacters', function()
SetupCharacters(source)
end)
local awaitingRegistration = {}
RegisterNetEvent('esx_multicharacter:CharacterChosen', function(charid, isNew)
if type(charid) == 'number' and string.len(charid) <= 2 and type(isNew) == 'boolean' then
if isNew then
awaitingRegistration[source] = charid
else
TriggerEvent('esx:onPlayerJoined', source, PREFIX..charid)
ESX.Players[GetIdentifier(source)] = true
end
end
end)
AddEventHandler('esx_identity:completedRegistration', function(source, data)
TriggerEvent('esx:onPlayerJoined', source, PREFIX..awaitingRegistration[source], data)
awaitingRegistration[source] = nil
ESX.Players[GetIdentifier(source)] = true
end)
AddEventHandler('playerDropped', function(reason)
awaitingRegistration[source] = nil
ESX.Players[GetIdentifier(source)] = nil
end)
RegisterNetEvent('esx_multicharacter:DeleteCharacter', function(charid)
if Config.CanDelete and type(charid) == 'number' and string.len(charid) <= 2 then
DeleteCharacter(source, charid)
end
end)
RegisterNetEvent('esx_multicharacter:relog', function()
TriggerEvent('esx:playerLogout', source)
end)
else
assert(nil, '^3WARNING: Multicharacter is disabled - please check your ESX configuration^0')
end