This commit is contained in:
2026-07-07 21:08:52 +02:00
commit 4c20cfc716
2613 changed files with 318021 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
ESX.RegisterCommand(
"setslots",
"admin",
function(xPlayer, args)
Database:SetSlots(args.identifier, args.slots)
xPlayer.triggerEvent("esx:showNotification", TranslateCap("slotsadd", args.slots, args.identifier))
end,
true,
{
help = TranslateCap("command_setslots"),
validate = true,
arguments = {
{ name = "identifier", help = TranslateCap("command_identifier"), type = "string" },
{ name = "slots", help = TranslateCap("command_slots"), type = "number" },
},
}
)
ESX.RegisterCommand(
"remslots",
"admin",
function(xPlayer, args)
local removed = Database:RemoveSlots(args.identifier)
if removed then
xPlayer.triggerEvent("esx:showNotification", TranslateCap("slotsrem", args.identifier))
end
end,
true,
{
help = TranslateCap("command_remslots"),
validate = true,
arguments = {
{ name = "identifier", help = TranslateCap("command_identifier"), type = "string" },
},
}
)
ESX.RegisterCommand(
"enablechar",
"admin",
function(xPlayer, args)
local enabled = Database:EnableSlot(args.identifier, args.charslot)
if enabled then
xPlayer.triggerEvent("esx:showNotification", TranslateCap("charenabled", args.charslot, args.identifier))
else
xPlayer.triggerEvent("esx:showNotification", TranslateCap("charnotfound", args.charslot, args.identifier))
end
end,
true,
{
help = TranslateCap("command_enablechar"),
validate = true,
arguments = {
{ name = "identifier", help = TranslateCap("command_identifier"), type = "string" },
{ name = "charslot", help = TranslateCap("command_charslot"), type = "number" },
},
}
)
ESX.RegisterCommand(
"disablechar",
"admin",
function(xPlayer, args)
local disabled = Database:DisableSlot(args.identifier, args.charslot)
if disabled then
xPlayer.triggerEvent("esx:showNotification", TranslateCap("chardisabled", args.charslot, args.identifier))
else
xPlayer.triggerEvent("esx:showNotification", TranslateCap("charnotfound", args.charslot, args.identifier))
end
end,
true,
{
help = TranslateCap("command_disablechar"),
validate = true,
arguments = {
{ name = "identifier", help = TranslateCap("command_identifier"), type = "string" },
{ name = "charslot", help = TranslateCap("command_charslot"), type = "number" },
},
}
)
RegisterCommand("forcelog", function(source)
TriggerEvent("esx:playerLogout", source)
end, true)

View File

@@ -0,0 +1,144 @@
Database = {}
Database.connected = false
Database.found = false
Database.tables = { users = "identifier" }
function Database:GetConnection()
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)
self.name = connectionString:sub(connectionString:find("/") + 1, -1):gsub("[%?]+[%w%p]*$", "")
self.found = true
else
local confPairs = { string.strsplit(";", connectionString) }
for i = 1, #confPairs do
local confPair = confPairs[i]
local key, value = confPair:match("^%s*(.-)%s*=%s*(.-)%s*$")
if key == "database" then
self.name = value
self.found = true
break
end
end
end
end
MySQL.ready(function()
local length = 42 + #Server.prefix
local DB_COLUMNS = MySQL.query.await(('SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "%s" AND DATA_TYPE = "varchar" AND COLUMN_NAME IN (?)'):format(Database.name, length), {
{ "identifier", "owner" },
})
if DB_COLUMNS then
local columns = {}
local count = 0
for i = 1, #DB_COLUMNS do
local column = DB_COLUMNS[i]
Database.tables[column.TABLE_NAME] = column.COLUMN_NAME
if column?.CHARACTER_MAXIMUM_LENGTH < length then
count = count + 1
columns[column.TABLE_NAME] = column.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 ^5VARCHAR(%s)^7"):format(count, length))
else
print(("[^2INFO^7] Unable to update ^5%s^7 columns to use ^5VARCHAR(%s)^7"):format(count, length))
end
end
Database.connected = true
ESX.Jobs = ESX.GetJobs()
while not next(ESX.Jobs) do
Wait(500)
ESX.Jobs = ESX.GetJobs()
end
end
end)
function Database:DeleteCharacter(source, charid)
local identifier = ("%s%s:%s"):format(Server.prefix, charid, ESX.GetIdentifier(source))
local query = "DELETE FROM `%s` WHERE %s = ?"
local queries = {}
local count = 0
for table, column in pairs(self.tables) do
count = count + 1
queries[count] = { query = query:format(table, column), values = { identifier } }
end
MySQL.transaction(queries, function(result)
if result then
local name = GetPlayerName(source)
print(("[^2INFO^7] Player ^5%s %s^7 has deleted a character ^5(%s)^7"):format(name, source, identifier))
Wait(50)
Multicharacter:SetupCharacters(source)
else
error("\n^1Transaction failed while trying to delete " .. identifier .. "^0")
end
end)
end
function Database:GetPlayerSlots(identifier)
return MySQL.scalar.await("SELECT slots FROM multicharacter_slots WHERE identifier = ?", { identifier }) or
Server.slots
end
function Database:GetPlayerInfo(identifier, slots)
return MySQL.query.await(
"SELECT identifier, accounts, job, job_grade, firstname, lastname, dateofbirth, sex, skin, disabled FROM users WHERE identifier LIKE ? LIMIT ?",
{ identifier, slots })
end
function Database:SetSlots(identifier, slots)
MySQL.insert("INSERT INTO `multicharacter_slots` (`identifier`, `slots`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `slots` = VALUES(`slots`)", {
identifier,
slots,
})
end
function Database:RemoveSlots(identifier)
local slots = MySQL.scalar.await("SELECT `slots` FROM `multicharacter_slots` WHERE identifier = ?", {
identifier,
})
if slots then
MySQL.update("DELETE FROM `multicharacter_slots` WHERE `identifier` = ?", {
identifier,
})
return true
end
return false
end
function Database:EnableSlot(identifier, slot)
local selectedCharacter = ("char%s:%s"):format(slot, identifier)
local updated = MySQL.update.await("UPDATE `users` SET `disabled` = 0 WHERE identifier = ?", {selectedCharacter})
return updated > 0
end
function Database:DisableSlot(identifier, slot)
local selectedCharacter = ("char%s:%s"):format(slot, identifier)
local updated = MySQL.update.await("UPDATE `users` SET `disabled` = 1 WHERE identifier = ?", {selectedCharacter})
return updated > 0
end
Database:GetConnection()

View File

@@ -0,0 +1,87 @@
ESX.Players = {}
function Server:ResetPlayers()
if next(ESX.Players) then
local players = table.clone(ESX.Players)
table.wipe(ESX.Players)
for _, v in pairs(players) do
ESX.Players[ESX.GetIdentifier(v.source)] = v.identifier
end
else
ESX.Players = {}
end
end
function Server:OnConnecting(source, deferrals)
deferrals.defer()
Wait(0) -- Required
local identifier
local correctLicense, _ = pcall(function()
identifier = ESX.GetIdentifier(source)
end)
-- luacheck: ignore
if not SetEntityOrphanMode then
return deferrals.done(("[ESX Multicharacter] ESX Requires a minimum Artifact version of 10188, Please update your server."))
end
if Server.oneSync == "off" or Server.oneSync == "legacy" then
return deferrals.done(("[ESX Multicharacter] ESX Requires Onesync Infinity to work. This server currently has Onesync set to: %s"):format(Server.oneSync))
end
if not Database.found then
deferrals.done("[ESX Multicharacter] Cannot Find the servers mysql_connection_string. Please make sure it is correctly configured in your server.cfg")
end
if not Database.connected then
deferrals.done("[ESX Multicharacter] OxMySQL Was Unable To Connect to your database. Please make sure it is turned on and correctly configured in your server.cfg")
end
if not identifier or not correctLicense then return deferrals.done(("[ESX Multicharacter] Unable to retrieve player identifier.\nIdentifier type: %s"):format(Server.identifierType)) end
if ESX.GetConfig().EnableDebug or not ESX.Players[identifier] then
ESX.Players[identifier] = source
return deferrals.done()
end
---@param staleSrc number
local function cleanupStalePlayer(staleSrc)
deferrals.update(("[ESX Multicharacter] Cleaning stale player entry..."):format(identifier))
TriggerEvent("esx:onPlayerDropped", staleSrc, "esx_stale_player_obj", function()
ESX.Players[identifier] = source
deferrals.done()
end)
end
local function reject()
return deferrals.done(
("[ESX Multicharacter] There was an error loading your character!\nError code: identifier-active\n\nThis error is caused by a player on this server who has the same identifier as you have. Make sure you are not playing on the same account.\n\nYour identifier: %s"):format(
identifier)
)
end
local plyRef = ESX.Players[identifier] ---@type number|string If player has not chosen character yet, plyRef = source, otherwise plyRef = identifier prefix ("char1", "char2", etc.)
if type(plyRef) == "number" then
if GetPlayerPing(plyRef --[[@as string]]) > 0 then
return reject()
end
return cleanupStalePlayer(plyRef)
end
local xPlayer = ESX.GetPlayerFromIdentifier(("%s:%s"):format(plyRef, identifier))
if not xPlayer then
ESX.Players[identifier] = source
return deferrals.done()
end
if GetPlayerPing(xPlayer.source --[[@as string]]) > 0 then
return reject()
end
return cleanupStalePlayer(xPlayer.source)
end
Server:ResetPlayers()

View File

@@ -0,0 +1,100 @@
---@diagnostic disable: duplicate-set-field
Multicharacter = {}
Multicharacter._index = Multicharacter
Multicharacter.awaitingRegistration = {}
function Multicharacter:SetupCharacters(source)
SetPlayerRoutingBucket(source, source)
while not Database.connected do
Wait(100)
end
local identifier = ESX.GetIdentifier(source)
ESX.Players[identifier] = source
local slots = Database:GetPlayerSlots(identifier)
identifier = Server.prefix .. "%:" .. identifier
local rawCharacters = Database:GetPlayerInfo(identifier, slots)
local characters
if rawCharacters then
local characterCount = #rawCharacters
characters = table.create(0, characterCount)
for i = 1, characterCount, 1 do
local v = rawCharacters[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 idString = string.sub(v.identifier, #Server.prefix + 1, string.find(v.identifier, ":") - 1)
local id = tonumber(idString)
if id then
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 TranslateCap("male") or TranslateCap("female"),
}
end
end
end
TriggerClientEvent("esx_multicharacter:SetupUI", source, characters, slots)
end
function Multicharacter:CharacterChosen(source, charid, isNew)
if type(charid) ~= "number" or string.len(charid) > 2 or type(isNew) ~= "boolean" then
return
end
if isNew then
self.awaitingRegistration[source] = charid
else
SetPlayerRoutingBucket(source, 0)
if not ESX.GetConfig().EnableDebug then
local identifier = ("%s%s:%s"):format(Server.prefix, charid, ESX.GetIdentifier(source))
if ESX.GetPlayerFromIdentifier(identifier) then
DropPlayer(source, "[ESX Multicharacter] Your identifier " .. identifier .. " is already on the server!")
return
end
end
local charIdentifier = ("%s%s"):format(Server.prefix, charid)
TriggerEvent("esx:onPlayerJoined", source, charIdentifier)
ESX.Players[ESX.GetIdentifier(source)] = charIdentifier
end
end
function Multicharacter:RegistrationComplete(source, data)
local charId = self.awaitingRegistration[source]
local charIdentifier = ("%s%s"):format(Server.prefix, charId)
self.awaitingRegistration[source] = nil
ESX.Players[ESX.GetIdentifier(source)] = charIdentifier
SetPlayerRoutingBucket(source, 0)
TriggerEvent("esx:onPlayerJoined", source, charIdentifier, data)
end
function Multicharacter:PlayerDropped(player)
self.awaitingRegistration[player] = nil
ESX.Players[ESX.GetIdentifier(player)] = nil
end