mirror of
https://github.com/esx-framework/esx_core.git
synced 2026-08-29 11:01:25 +00:00
b7c521c8e7
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.
246 lines
7.7 KiB
Lua
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
|