refactor(esx_multicharacter): Complete Rewrite

This commit is contained in:
Kasey FItton
2024-10-24 22:49:03 +01:00
parent ca3905f169
commit 7c72acda58
9 changed files with 929 additions and 616 deletions
+60 -357
View File
@@ -1,367 +1,70 @@
local mp_m_freemode_01 = `mp_m_freemode_01`
local mp_f_freemode_01 = `mp_f_freemode_01`
local SpawnCoords = Config.Spawn[ESX.Math.Random(1,#Config.Spawn)]
-- Connection Logic
if ESX.GetConfig().Multichar then
CreateThread(function()
while not ESX.PlayerLoaded do
Wait(100)
local function AwaitContext()
while GetResourceState("esx_context") ~= "started" do
Wait(100)
end
return true
end
if NetworkIsPlayerActive(PlayerId()) then
exports.spawnmanager:setAutoSpawn(false)
DoScreenFadeOut(0)
while GetResourceState("esx_context") ~= "started" do
Wait(100)
end
TriggerEvent("esx_multicharacter:SetupCharacters")
local function DisableSpawnManager()
if GetResourceState("spawnmanager") == "started" then
exports.spawnmanager:setAutoSpawn(false)
end
end
CreateThread(function()
while not ESX.PlayerLoaded do
Wait(100)
if NetworkIsPlayerActive(ESX.PlayerData.playerId) then
DisableSpawnManager()
DoScreenFadeOut(0)
local ready = AwaitContext()
if ready then
Multicharacter:SetupCharacters()
break
end
end
end)
local canRelog, cam, spawned = true, nil, nil
local Characters , hidePlayers = {} , false
RegisterNetEvent("esx_multicharacter:SetupCharacters")
AddEventHandler("esx_multicharacter:SetupCharacters", function()
ESX.PlayerLoaded = false
ESX.PlayerData = {}
spawned = false
cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
local playerPed = PlayerPedId()
SetEntityCoords(playerPed, SpawnCoords.x, SpawnCoords.y, SpawnCoords.z, true, false, false, false)
SetEntityHeading(playerPed, SpawnCoords.w)
local offset = GetOffsetFromEntityInWorldCoords(playerPed, 0, 1.7, 0.4)
DoScreenFadeOut(0)
SetCamActive(cam, true)
RenderScriptCams(true, false, 1, true, true)
SetCamCoord(cam, offset.x, offset.y, offset.z)
PointCamAtCoord(cam, SpawnCoords.x, SpawnCoords.y, SpawnCoords.z + 1.3)
StartLoop()
ShutdownLoadingScreen()
ShutdownLoadingScreenNui()
TriggerEvent("esx:loadingScreenOff")
Wait(200)
TriggerServerEvent("esx_multicharacter:SetupCharacters")
end)
StartLoop = function()
hidePlayers = true
MumbleSetVolumeOverride(PlayerId(), 0.0)
CreateThread(function()
local keys = { 18, 27, 172, 173, 174, 175, 176, 177, 187, 188, 191, 201, 108, 109, 209, 19 }
while hidePlayers do
DisableAllControlActions(0)
for i = 1, #keys do
EnableControlAction(0, keys[i], true)
end
SetEntityVisible(PlayerPedId(), false, false)
SetLocalPlayerVisibleLocally(true)
SetPlayerInvincible(PlayerId(), true)
ThefeedHideThisFrame()
HideHudComponentThisFrame(11)
HideHudComponentThisFrame(12)
HideHudComponentThisFrame(21)
HideHudAndRadarThisFrame()
Wait(0)
local vehicles = GetGamePool("CVehicle")
for i = 1, #vehicles do
SetEntityLocallyInvisible(vehicles[i])
end
end
local playerId, playerPed = PlayerId(), PlayerPedId()
MumbleSetVolumeOverride(playerId, -1.0)
SetEntityVisible(playerPed, true, false)
SetPlayerInvincible(playerId, true)
FreezeEntityPosition(playerPed, false)
Wait(10000)
canRelog = true
end)
CreateThread(function()
local playerPool = {}
while hidePlayers do
local players = GetActivePlayers()
for i = 1, #players do
local player = players[i]
if player ~= PlayerId() and not playerPool[player] then
playerPool[player] = true
NetworkConcealPlayer(player, true, true)
end
end
Wait(500)
end
for k in pairs(playerPool) do
NetworkConcealPlayer(k, false, false)
end
end)
end
end)
SetupCharacter = function(index)
if not spawned then
exports.spawnmanager:spawnPlayer({
x = SpawnCoords.x,
y = SpawnCoords.y,
z = SpawnCoords.z,
heading = SpawnCoords.w,
model = Characters[index].model or mp_m_freemode_01,
skipFade = true,
}, function()
canRelog = false
if Characters[index] then
local skin = Characters[index].skin or Config.Default
if not Characters[index].model then
if Characters[index].sex == TranslateCap("female") then
skin.sex = 1
else
skin.sex = 0
end
end
TriggerEvent("skinchanger:loadSkin", skin)
end
DoScreenFadeIn(600)
-- Events
RegisterNetEvent("esx_multicharacter:SetupUI", function(data, slots)
Multicharacter:SetupUI(data, slots)
end)
RegisterNetEvent('esx:playerLoaded', function(playerData, isNew, skin)
Multicharacter:PlayerLoaded(playerData, isNew, skin)
end)
RegisterNetEvent('esx:onPlayerLogout', function()
DoScreenFadeOut(500)
Wait(5000)
Multicharacter.spawned = false
Multicharacter:SetupCharacters()
TriggerEvent("esx_skin:resetFirstSpawn")
end)
-- Relog
if Config.Relog then
RegisterCommand("relog", function()
if Multicharacter.canRelog then
Multicharacter.canRelog = false
TriggerServerEvent("esx_multicharacter:relog")
ESX.SetTimeout(10000, function()
Multicharacter.canRelog = true
end)
repeat
Wait(200)
until not IsScreenFadedOut()
elseif Characters[index] and Characters[index].skin then
if Characters[spawned] and Characters[spawned].model then
RequestModel(Characters[index].model)
while not HasModelLoaded(Characters[index].model) do
RequestModel(Characters[index].model)
Wait(0)
end
SetPlayerModel(PlayerId(), Characters[index].model)
SetModelAsNoLongerNeeded(Characters[index].model)
end
TriggerEvent("skinchanger:loadSkin", Characters[index].skin)
end
spawned = index
local playerPed = PlayerPedId()
FreezeEntityPosition(PlayerPedId(), true)
SetPedAoBlobRendering(playerPed, true)
SetEntityAlpha(playerPed, 255, false)
SendNUIMessage({
action = "openui",
character = Characters[spawned],
})
end
function CharacterDeleteConfirmation(characters, slots, SelectedCharacter, value)
local elements = {
{ title = TranslateCap("char_delete_confirmation"), icon = "fa-solid fa-users", description = TranslateCap("char_delete_confirmation_description"), unselectable = true },
{ title = TranslateCap("char_delete"), icon = "fa-solid fa-xmark", description = TranslateCap("char_delete_yes_description"), action = "delete", value = value },
{ title = TranslateCap("return"), unselectable = false, icon = "fa-solid fa-arrow-left", description = TranslateCap("char_delete_no_description"), action = "return" },
}
ESX.OpenContext("left", elements, function(_, Action)
if Action.action == "delete" then
ESX.CloseContext()
TriggerServerEvent("esx_multicharacter:DeleteCharacter", Action.value)
spawned = false
elseif Action.action == "return" then
CharacterOptions(characters, slots, SelectedCharacter)
end
end, nil, false)
end
function CharacterOptions(characters, slots, SelectedCharacter)
local elements = {
{ title = TranslateCap("character", characters[SelectedCharacter.value].firstname .. " " .. characters[SelectedCharacter.value].lastname), icon = "fa-regular fa-user", unselectable = true },
{ title = TranslateCap("return"), unselectable = false, icon = "fa-solid fa-arrow-left", description = TranslateCap("return_description"), action = "return" },
}
if not characters[SelectedCharacter.value].disabled then
elements[3] = { title = TranslateCap("char_play"), description = TranslateCap("char_play_description"), icon = "fa-solid fa-play", action = "play", value = SelectedCharacter.value }
else
elements[3] = { title = TranslateCap("char_disabled"), value = SelectedCharacter.value, icon = "fa-solid fa-xmark", description = TranslateCap("char_disabled_description") }
end
if Config.CanDelete then
elements[4] = { title = TranslateCap("char_delete"), icon = "fa-solid fa-xmark", description = TranslateCap("char_delete_description"), action = "delete", value = SelectedCharacter.value }
end
ESX.OpenContext("left", elements, function(_, Action)
if Action.action == "play" then
SendNUIMessage({
action = "closeui",
})
ESX.CloseContext()
TriggerServerEvent("esx_multicharacter:CharacterChosen", Action.value, false)
elseif Action.action == "delete" then
CharacterDeleteConfirmation(characters, slots, SelectedCharacter, Action.value)
elseif Action.action == "return" then
SelectCharacterMenu(characters, slots)
end
end, nil, false)
end
function SelectCharacterMenu(characters, slots)
local Character = next(characters)
local elements = { { title = TranslateCap("select_char"), icon = "fa-solid fa-users", description = TranslateCap("select_char_description"), unselectable = true } }
for k, v in pairs(characters) do
if not v.model and v.skin then
if v.skin.model then
v.model = v.skin.model
elseif v.skin.sex == 1 then
v.model = mp_f_freemode_01
else
v.model = mp_m_freemode_01
end
end
if not spawned then
SetupCharacter(Character)
end
local label = v.firstname .. " " .. v.lastname
if characters[k].disabled then
elements[#elements + 1] = { title = label, icon = "fa-regular fa-user", value = v.id }
else
elements[#elements + 1] = { title = label, icon = "fa-regular fa-user", value = v.id }
end
end
if #elements - 1 < slots then
elements[#elements + 1] = { title = TranslateCap("create_char"), icon = "fa-solid fa-plus", value = (#elements + 1), new = true }
end
ESX.OpenContext("left", elements, function(_, SelectedCharacter)
if SelectedCharacter.new then
ESX.CloseContext()
local GetSlot = function()
for i = 1, slots do
if not characters[i] then
return i
end
end
end
local slot = GetSlot()
TriggerServerEvent("esx_multicharacter:CharacterChosen", slot, true)
TriggerEvent("esx_identity:showRegisterIdentity")
local playerPed = PlayerPedId()
SetPedAoBlobRendering(playerPed, false)
SetEntityAlpha(playerPed, 0, false)
SendNUIMessage({
action = "closeui",
})
else
CharacterOptions(characters, slots, SelectedCharacter)
SetupCharacter(SelectedCharacter.value)
local playerPed = PlayerPedId()
SetPedAoBlobRendering(playerPed, true)
ResetEntityAlpha(playerPed)
end
end, nil, false)
end
RegisterNetEvent("esx_multicharacter:SetupUI")
AddEventHandler("esx_multicharacter:SetupUI", function(data, slots)
DoScreenFadeOut(0)
Characters = data
slots = slots
local Character = next(Characters)
exports.spawnmanager:forceRespawn()
if not Character then
SendNUIMessage({
action = "closeui",
})
exports.spawnmanager:spawnPlayer({
x = SpawnCoords.x,
y = SpawnCoords.y,
z = SpawnCoords.z,
heading = SpawnCoords.w,
model = mp_m_freemode_01,
skipFade = true,
}, function()
canRelog = false
DoScreenFadeIn(400)
Wait(400)
local playerPed = PlayerPedId()
SetPedAoBlobRendering(playerPed, false)
SetEntityAlpha(playerPed, 0, false)
TriggerServerEvent("esx_multicharacter:CharacterChosen", 1, true)
TriggerEvent("esx_identity:showRegisterIdentity")
end)
else
SelectCharacterMenu(Characters, slots)
end
end)
RegisterNetEvent("esx:playerLoaded")
AddEventHandler("esx:playerLoaded", function(playerData, isNew, skin)
local spawn = playerData.coords or Config.Spawn
if isNew or not skin or #skin == 1 then
local finished = false
skin = Config.Default[playerData.sex]
skin.sex = playerData.sex == "m" and 0 or 1
local model = skin.sex == 0 and mp_m_freemode_01 or mp_f_freemode_01
RequestModel(model)
while not HasModelLoaded(model) do
RequestModel(model)
Wait(0)
end
SetPlayerModel(PlayerId(), model)
SetModelAsNoLongerNeeded(model)
TriggerEvent("skinchanger:loadSkin", skin, function()
local playerPed = PlayerPedId()
SetPedAoBlobRendering(playerPed, true)
ResetEntityAlpha(playerPed)
TriggerEvent("esx_skin:openSaveableMenu", function()
finished = true
end, function()
finished = true
end)
end)
repeat
Wait(200)
until finished
end
DoScreenFadeOut(750)
Wait(750)
if cam then
SetCamActive(cam, false)
RenderScriptCams(false, false, 0, true, true)
cam = nil
end
local playerPed = PlayerPedId()
FreezeEntityPosition(playerPed, true)
SetEntityCoordsNoOffset(playerPed, spawn.x, spawn.y, spawn.z, false, false, false)
SetEntityHeading(playerPed, spawn.heading)
if not isNew then
TriggerEvent("skinchanger:loadSkin", skin or Characters[spawned].skin)
end
Wait(500)
DoScreenFadeIn(750)
Wait(750)
repeat
Wait(200)
until not IsScreenFadedOut()
TriggerServerEvent("esx:onPlayerSpawn")
TriggerEvent("esx:onPlayerSpawn")
TriggerEvent("playerSpawned")
TriggerEvent("esx:restoreLoadout")
Characters, hidePlayers = {}, false
end)
RegisterNetEvent("esx:onPlayerLogout")
AddEventHandler("esx:onPlayerLogout", function()
DoScreenFadeOut(500)
Wait(1000)
spawned = false
TriggerEvent("esx_multicharacter:SetupCharacters")
TriggerEvent("esx_skin:resetFirstSpawn")
end)
if Config.Relog then
RegisterCommand("relog", function()
if canRelog then
canRelog = false
TriggerServerEvent("esx_multicharacter:relog")
ESX.SetTimeout(10000, function()
canRelog = true
end)
end
end,false)
end
end
end,false)
end
@@ -0,0 +1,191 @@
Menu = {}
Menu._index = Menu
Menu.currentElements = {}
function Menu:OpenMenu()
ESX.OpenContext("left", self.currentElements, self.onUse, nil, false)
end
function Menu:Close()
self.currentElements = {}
ESX.CloseContext()
end
function Menu:CheckModel(character)
if not character.model and character.skin then
if character.skin.model then
character.model = character.skin.model
elseif character.skin.sex == 1 then
character.model = `mp_f_freemode_01`
else
character.model = `mp_m_freemode_01`
end
end
end
function Menu:AddCharacters()
for _, v in pairs(Multicharacter.Characters) do
self:CheckModel(v)
local label = ("%s %s"):format(v.firstname, v.lastname)
self.currentElements[#self.currentElements + 1] = { title = label, icon = "fa-regular fa-user", value = v.id}
end
if #self.currentElements - 1 < Multicharacter.slots then
self.currentElements[#self.currentElements + 1] = { title = TranslateCap("create_char"), icon = "fa-solid fa-plus", value = (#self.currentElements + 1), new = true }
end
end
local GetSlot = function()
for i = 1, Multicharacter.slots do
if not Multicharacter.Characters[i] then
return i
end
end
end
function Menu:NewCharacter()
self:Close()
local slot = GetSlot()
TriggerServerEvent("esx_multicharacter:CharacterChosen", slot, true)
TriggerEvent("esx_identity:showRegisterIdentity")
local playerPed = PlayerPedId()
SetPedAoBlobRendering(playerPed, false)
SetEntityAlpha(playerPed, 0, false)
Multicharacter:CloseUI()
end
function Menu:SelectCharacter()
local Characters = Multicharacter.Characters
local Character = next(Characters)
self:CheckModel(Characters[Character])
if not Multicharacter.spawned then
Multicharacter:SetupCharacter(Character)
end
self.currentElements = {
{
title = TranslateCap("select_char"),
icon = "fa-solid fa-users",
description = TranslateCap("select_char_description"),
unselectable = true
}
}
self:AddCharacters()
self.onUse = function(_, SelectedCharacter)
if SelectedCharacter.new then
self:NewCharacter()
else
if SelectedCharacter.value ~= Multicharacter.spawned then
Multicharacter:SetupCharacter(SelectedCharacter.value)
local playerPed = PlayerPedId()
SetPedAoBlobRendering(playerPed, true)
ResetEntityAlpha(playerPed)
end
self:CharacterOptions()
end
end
self:OpenMenu()
end
function Menu:CharacterOptions()
local currentCharacter = Multicharacter.Characters[Multicharacter.spawned]
local elements = {
{
title = TranslateCap("character", currentCharacter.firstname .. " " .. currentCharacter.lastname),
icon = "fa-regular fa-user",
unselectable = true
},
{
title = TranslateCap("return"),
unselectable = false,
icon = "fa-solid fa-arrow-left",
description = TranslateCap("return_description"),
action = "return"
},
}
if not currentCharacter.disabled then
elements[3] = {
title = TranslateCap("char_play"),
description = TranslateCap("char_play_description"),
icon = "fa-solid fa-play",
action = "play",
}
else
elements[3] = {
title = TranslateCap("char_disabled"),
icon = "fa-solid fa-xmark",
description = TranslateCap("char_disabled_description")
}
end
if Config.CanDelete then
elements[4] = {
title = TranslateCap("char_delete"),
icon = "fa-solid fa-xmark",
description = TranslateCap("char_delete_description"),
action = "delete",
}
end
self.currentElements = elements
self.onUse = function(_, Action)
if Action.action == "play" then
Multicharacter:CloseUI()
self:Close()
TriggerServerEvent("esx_multicharacter:CharacterChosen", Multicharacter.spawned, false)
elseif Action.action == "delete" then
self:ConfirmDeletion()
elseif Action.action == "return" then
self:SelectCharacter()
end
end
self:OpenMenu()
end
function Menu:ConfirmDeletion()
self.currentElements = {
{
title = TranslateCap("char_delete_confirmation"),
icon = "fa-solid fa-users",
description = TranslateCap("char_delete_confirmation_description"),
unselectable = true
},
{
title = TranslateCap("char_delete"),
icon = "fa-solid fa-xmark",
description = TranslateCap("char_delete_yes_description"),
action = "delete",
},
{
title = TranslateCap("return"),
unselectable = false,
icon = "fa-solid fa-arrow-left",
description = TranslateCap("char_delete_no_description"),
action = "return"
},
}
self.onUse = function(_, Action)
if Action.action == "delete" then
self:Close()
TriggerServerEvent("esx_multicharacter:DeleteCharacter", Multicharacter.spawned)
Multicharacter.spawned = false
elseif Action.action == "return" then
self:CharacterOptions()
end
end
self:OpenMenu()
end
@@ -0,0 +1,336 @@
---@diagnostic disable: duplicate-set-field
Multicharacter = {}
Multicharacter._index = Multicharacter
Multicharacter.canRelog = true
Multicharacter.Characters = {}
Multicharacter.hidePlayers = false
function Multicharacter:SetupCamera()
self.cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
SetCamActive(self.cam, true)
RenderScriptCams(true, false, 1, true, true)
local offset = GetOffsetFromEntityInWorldCoords(self.playerPed, 0, 1.7, 0.4)
SetCamCoord(self.cam, offset.x, offset.y, offset.z)
PointCamAtCoord(self.cam, self.spawnCoords.x, self.spawnCoords.y, self.spawnCoords.z + 1.3)
end
function Multicharacter:AwaitFadeIn()
while IsScreenFadingIn() do
Wait(200)
end
end
function Multicharacter:AwaitFadeOut()
while IsScreenFadingOut() do
Wait(200)
end
end
function Multicharacter:DestoryCamera()
if self.cam then
SetCamActive(self.cam, false)
RenderScriptCams(false, false, 0, true, true)
self.cam = nil
end
end
local function HideComponents()
local components = {11, 12, 21}
for i = 1, #components do
HideHudComponentThisFrame(components[i])
end
ThefeedHideThisFrame()
HideHudAndRadarThisFrame()
end
function Multicharacter:ResetHideActivePlayer()
self.playerPed = PlayerPedId()
MumbleSetVolumeOverride(ESX.PlayerData.playerId, -1.0)
SetEntityVisible(self.playerPed, true, false)
SetPlayerInvincible(self.playerPed, true)
FreezeEntityPosition(self.playerPed, false)
end
function Multicharacter:HideActivePlayerLoop()
CreateThread(function()
local keys = { 18, 27, 172, 173, 174, 175, 176, 177, 187, 188, 191, 201, 108, 109, 209, 19 }
while self.hidePlayers do
DisableAllControlActions(0)
for i = 1, #keys do
EnableControlAction(0, keys[i], true)
end
SetEntityVisible(self.playerPed, false, false)
SetLocalPlayerVisibleLocally(true)
SetPlayerInvincible(ESX.PlayerData.playerId, true)
HideComponents()
local vehicles = GetGamePool("CVehicle")
for i = 1, #vehicles do
SetEntityLocallyInvisible(vehicles[i])
end
Wait(0)
end
self:ResetHideActivePlayer()
SetTimeout(10000, function()
self.canRelog = true
end)
end)
end
function Multicharacter:ConcealLoop()
CreateThread(function()
local playerPool = {}
while self.hidePlayers do
local players = GetActivePlayers()
for i = 1, #players do
local player = players[i]
if player ~= ESX.PlayerData.playerId and not playerPool[player] then
playerPool[player] = true
NetworkConcealPlayer(player, true, true)
end
end
Wait(500)
end
for k in pairs(playerPool) do
NetworkConcealPlayer(k, false, false)
end
end)
end
function Multicharacter:StartLoops()
self.hidePlayers = true
MumbleSetVolumeOverride(ESX.PlayerData.playerId, 0.0)
self:HideActivePlayerLoop()
self:ConcealLoop()
end
function Multicharacter:SetupCharacters()
ESX.PlayerLoaded = false
ESX.PlayerData = {}
ESX.PlayerData.playerId = PlayerId()
ESX.PlayerData.serverId = GetPlayerServerId(ESX.PlayerData.playerId)
self.spawned = false
self.cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
self.playerPed = PlayerPedId()
self.spawnCoords = Config.Spawn[ESX.Math.Random(1,#Config.Spawn)]
SetEntityCoords(self.playerPed, self.spawnCoords.x, self.spawnCoords.y, self.spawnCoords.z, true, false, false, false)
SetEntityHeading(self.playerPed, self.spawnCoords.w)
self:SetupCamera()
self:StartLoops()
ShutdownLoadingScreen()
ShutdownLoadingScreenNui()
TriggerEvent("esx:loadingScreenOff")
SetTimeout(200, function()
TriggerServerEvent("esx_multicharacter:SetupCharacters")
end)
end
function Multicharacter:GetSkin()
local character = self.Characters[self.tempIndex]
local skin = character and character.skin or Config.Default
if not character.model then
if character.sex == TranslateCap("female") then
skin.sex = 1
else
skin.sex = 0
end
end
return skin
end
function Multicharacter:SpawnTempPed()
self.canRelog = false
local skin = self:GetSkin()
ESX.SpawnPlayer(skin, self.spawnCoords, function()
DoScreenFadeIn(600)
self.playerPed = PlayerPedId()
end)
end
function Multicharacter:ChangeExistingPed()
local newCharacter = self.Characters[self.tempIndex]
local spawnedCharacter = self.Characters[self.spawned]
if spawnedCharacter and spawnedCharacter.model then
local model = ESX.Streaming.RequestModel(newCharacter.model)
if model then
SetPlayerModel(ESX.PlayerData.playerId, newCharacter.model)
SetModelAsNoLongerNeeded(newCharacter.model)
end
end
TriggerEvent("skinchanger:loadSkin", newCharacter.skin)
end
function Multicharacter:PrepForUI()
FreezeEntityPosition(self.playerPed, true)
SetPedAoBlobRendering(self.playerPed, true)
SetEntityAlpha(self.playerPed, 255, false)
end
function Multicharacter:CloseUI()
SendNUIMessage({
action = "closeui",
})
end
function Multicharacter:SetupCharacter(index)
local character = self.Characters[index]
self.tempIndex = index
if not self.spawned then
self:SpawnTempPed()
elseif character and character.skin then
self:ChangeExistingPed()
end
self.spawned = index
self.playerPed = PlayerPedId()
self:PrepForUI()
SendNUIMessage({
action = "openui",
character = character,
})
end
function Multicharacter:SetupUI(characters, slots)
DoScreenFadeOut(0)
self.Characters = characters
self.slots = slots
local Character = next(self.Characters)
if not Character then
self.canRelog = false
ESX.SpawnPlayer(Config.Default, self.spawnCoords, function()
DoScreenFadeIn(400)
self:AwaitFadeIn()
self.playerPed = PlayerPedId()
SetPedAoBlobRendering(self.playerPed, false)
SetEntityAlpha(self.playerPed, 0, false)
TriggerServerEvent("esx_multicharacter:CharacterChosen", 1, true)
TriggerEvent("esx_identity:showRegisterIdentity")
end)
else
Menu:SelectCharacter()
end
end
function Multicharacter:LoadSkinCreator(skin)
TriggerEvent("skinchanger:loadSkin", skin, function()
DoScreenFadeIn(600)
SetPedAoBlobRendering(self.playerPed, true)
ResetEntityAlpha(self.playerPed)
TriggerEvent("esx_skin:openSaveableMenu", function()
Multicharacter.finishedCreation = true
end, function()
Multicharacter.finishedCreation = true
end)
end)
end
function Multicharacter:SetDefaultSkin(playerData)
local skin = Config.Default[playerData.sex]
skin.sex = playerData.sex == "m" and 0 or 1
local model = skin.sex == 0 and `mp_m_freemode_01` or `mp_f_freemode_01`
---@diagnostic disable-next-line: cast-local-type
model = ESX.Streaming.RequestModel(model)
if not model then
return
end
SetPlayerModel(ESX.PlayerData.playerId, model)
SetModelAsNoLongerNeeded(model)
self.playerPed = PlayerPedId()
self:LoadSkinCreator(skin)
end
function Multicharacter:Reset()
self.Characters = {}
self.tempIndex = nil
self.playerPed = PlayerPedId()
self.hidePlayers = false
self.slots = nil
end
function Multicharacter:Spawn()
local esxSpawns = ESX.GetConfig().DefaultSpawns
local spawn = esxSpawns[math.random(1, #esxSpawns)]
RequestCollisionAtCoord(spawn.x, spawn.y, spawn.z)
FreezeEntityPosition(self.playerPed, true)
SetEntityCoordsNoOffset(self.playerPed, spawn.x, spawn.y, spawn.z, false, false, true)
SetEntityHeading(self.playerPed, spawn.heading or spawn.w or 0.0)
while not HasCollisionLoadedAroundEntity(self.playerPed) do
Wait(0)
end
NetworkResurrectLocalPlayer(spawn.x, spawn.y, spawn.z, spawn.heading, 0, true)
end
function Multicharacter:PlayerLoaded(playerData, isNew, skin)
DoScreenFadeOut(750)
self:AwaitFadeOut()
if not isNew and playerData.coords then
spawn = playerData.coords
end
if isNew or not skin or #skin == 1 then
self.finishedCreation = false
self:SetDefaultSkin(playerData)
while not self.finishedCreation do
Wait(200)
end
DoScreenFadeOut(500)
self:AwaitFadeOut()
elseif not isNew then
TriggerEvent("skinchanger:loadSkin", skin or self.Characters[self.spawned].skin)
end
self:DestoryCamera()
self:Spawn()
Wait(500)
DoScreenFadeIn(750)
self:AwaitFadeIn()
TriggerServerEvent("esx:onPlayerSpawn")
TriggerEvent("esx:onPlayerSpawn")
TriggerEvent("playerSpawned")
TriggerEvent("esx:restoreLoadout")
self:Reset()
end
+10 -3
View File
@@ -3,16 +3,23 @@ fx_version 'cerulean'
game 'gta5'
author 'ESX-Framework - Linden - KASH'
description 'Allows players to have multiple characters on the same account.'
version '1.10.10'
version '1.10.9'
lua54 'yes'
dependencies { 'es_extended', 'esx_context', 'esx_identity', 'esx_skin' }
shared_scripts { '@es_extended/imports.lua', '@es_extended/locale.lua', 'locales/*.lua', 'config.lua' }
server_scripts { '@oxmysql/lib/MySQL.lua', 'server/*.lua' }
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/*.lua',
'server/modules/*.lua'
}
client_scripts { 'client/*.lua' }
client_scripts {
"client/modules/*.lua",
'client/*.lua'
}
ui_page { 'html/ui.html' }
+19 -223
View File
@@ -1,245 +1,41 @@
local databaseConnected = false
local databaseFound = false
local oneSyncState = GetConvar("onesync", "off")
Server = {}
Server._index = Server
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^1", 0)
elseif connectionString:find("mysql://") then
connectionString = connectionString:sub(9, -1)
DATABASE = connectionString:sub(connectionString:find("/") + 1, -1):gsub("[%?]+[%w%p]*$", "")
databaseFound = true
else
connectionString = { string.strsplit(";", connectionString) }
for i = 1, #connectionString do
local v = connectionString[i]
if v:match("database") then
DATABASE = v:sub(10, #v)
databaseFound = true
break
end
end
end
end
local DB_TABLES = { users = "identifier" }
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 "license"
local function GetIdentifier(source)
local fxDk = GetConvarInt("sv_fxdkMode", 0)
if fxDk == 1 then
return "ESX-DEBUG-LICENCE"
end
local identifier = GetPlayerIdentifierByType(source, PRIMARY_IDENTIFIER)
return identifier and identifier:gsub(PRIMARY_IDENTIFIER .. ":", "")
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 databaseConnected do
Wait(100)
end
local identifier = GetIdentifier(source)
ESX.Players[identifier] = true
local slots = MySQL.scalar.await("SELECT slots FROM multicharacter_slots WHERE identifier = ?", { identifier }) or SLOTS
identifier = PREFIX .. "%:" .. identifier
local result = MySQL.query.await("SELECT identifier, accounts, job, job_grade, firstname, lastname, dateofbirth, sex, skin, disabled FROM users WHERE identifier LIKE ? LIMIT ?", { 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 TranslateCap("male") or TranslateCap("female"),
}
end
end
TriggerClientEvent("esx_multicharacter:SetupUI", source, characters, slots)
end
Server.oneSync = GetConvar("onesync", "off")
Server.slots = Config.Slots or 4
Server.prefix = Config.Prefix or "char"
Server.identifierType = ESX.GetConfig().Identifier or GetConvar("sv_lan", "") == "true" and "ip" or "license"
AddEventHandler("playerConnecting", function(_, _, deferrals)
deferrals.defer()
local identifier = GetIdentifier(source)
if oneSyncState == "off" or oneSyncState == "legacy" then
return deferrals.done(("[ESX] ESX Requires Onesync Infinity to work. This server currently has Onesync set to: %s"):format(oneSyncState))
end
if not databaseFound then
deferrals.done("[ESX] Cannot Find the servers mysql_connection_string. Please make sure it is correctly configured in your server.cfg")
end
if not databaseConnected then
deferrals.done("[ESX] OxMySQL Was Unable To Connect to your database. Please make sure it is turned on and correctly configured in your server.cfg")
end
if identifier then
if not ESX.GetConfig().EnableDebug then
if ESX.Players[identifier] then
deferrals.done(("[ESX Multicharacter] 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()
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 = count + 1
queries[count] = { query = query:format(table, column), values = { identifier } }
end
MySQL.transaction(queries, function(result)
if result then
print(("[^2INFO^7] Player ^5%s %s^7 has deleted a character ^5(%s)^7"):format(GetPlayerName(source), source, identifier))
Wait(50)
SetupCharacters(source)
else
error("\n^1Transaction failed while trying to delete " .. identifier .. "^1")
end
end)
end
MySQL.ready(function()
local length = 42 + #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, length), {
{ "identifier", "owner" },
})
if DB_COLUMNS then
local columns = {}
local count = 0
for i = 1, #DB_COLUMNS do
local column = DB_COLUMNS[i]
DB_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
databaseConnected = true
while not next(ESX.Jobs) do
Wait(500)
ESX.Jobs = ESX.GetJobs()
end
end
local source = source
Server:OnConnecting(source, deferrals)
end)
RegisterNetEvent("esx_multicharacter:SetupCharacters", function()
SetupCharacters(source)
local source = source
Multicharacter: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
if not ESX.GetConfig().EnableDebug then
local identifier = PREFIX .. charid .. ":" .. GetIdentifier(source)
if ESX.GetPlayerFromIdentifier(identifier) then
DropPlayer(source, "Your identifier " .. identifier .. " is already on the server!")
return
end
end
TriggerEvent("esx:onPlayerJoined", source, PREFIX .. charid)
ESX.Players[GetIdentifier(source)] = true
end
end
local source = source
Multicharacter:CharacterChosen(source, charid, isNew)
end)
AddEventHandler("esx_identity:completedRegistration", function(source, data)
TriggerEvent("esx:onPlayerJoined", source, PREFIX .. awaitingRegistration[source], data)
awaitingRegistration[source] = nil
ESX.Players[GetIdentifier(source)] = true
Multicharacter:RegistrationComplete(source, data)
end)
AddEventHandler("playerDropped", function()
awaitingRegistration[source] = nil
ESX.Players[GetIdentifier(source)] = nil
local source = source
Multicharacter:PlayerDropped(source)
end)
RegisterNetEvent("esx_multicharacter:DeleteCharacter", function(charid)
if Config.CanDelete and type(charid) == "number" and string.len(charid) <= 2 then
DeleteCharacter(source, charid)
if not Config.CanDelete or type(charid) ~= "number" or string.len(charid) > 2 then
return
end
local source = source
Database:DeleteCharacter(source, charid)
end)
RegisterNetEvent("esx_multicharacter:relog", function()
@@ -2,10 +2,7 @@ ESX.RegisterCommand(
"setslots",
"admin",
function(xPlayer, args)
MySQL.insert("INSERT INTO `multicharacter_slots` (`identifier`, `slots`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `slots` = VALUES(`slots`)", {
args.identifier,
args.slots,
})
Database:SetSlots(args.identifier, args.slots)
xPlayer.triggerEvent("esx:showNotification", TranslateCap("slotsadd", args.slots, args.identifier))
end,
true,
@@ -23,14 +20,9 @@ ESX.RegisterCommand(
"remslots",
"admin",
function(xPlayer, args)
local slots = MySQL.scalar.await("SELECT `slots` FROM `multicharacter_slots` WHERE identifier = ?", {
args.identifier,
})
local removed = Database:RemoveSlots(args.identifier)
if slots then
MySQL.update("DELETE FROM `multicharacter_slots` WHERE `identifier` = ?", {
args.identifier,
})
if removed then
xPlayer.triggerEvent("esx:showNotification", TranslateCap("slotsrem", args.identifier))
end
end,
@@ -48,17 +40,12 @@ ESX.RegisterCommand(
"enablechar",
"admin",
function(xPlayer, args)
local selectedCharacter = "char" .. args.charslot .. ":" .. args.identifier
MySQL.update("UPDATE `users` SET `disabled` = 0 WHERE identifier = ?", {
selectedCharacter,
}, function(result)
if result > 0 then
xPlayer.triggerEvent("esx:showNotification", TranslateCap("charenabled", args.charslot, args.identifier))
else
xPlayer.triggerEvent("esx:showNotification", TranslateCap("charnotfound", args.charslot, args.identifier))
end
end)
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,
{
@@ -75,17 +62,12 @@ ESX.RegisterCommand(
"disablechar",
"admin",
function(xPlayer, args)
local selectedCharacter = "char" .. args.charslot .. ":" .. args.identifier
MySQL.update("UPDATE `users` SET `disabled` = 1 WHERE identifier = ?", {
selectedCharacter,
}, function(result)
if result > 0 then
xPlayer.triggerEvent("esx:showNotification", TranslateCap("chardisabled", args.charslot, args.identifier))
else
xPlayer.triggerEvent("esx:showNotification", TranslateCap("charnotfound", args.charslot, args.identifier))
end
end)
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,
{
@@ -0,0 +1,143 @@
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 connectionExtracted = { string.strsplit(";", connectionString) }
for i = 1, #connectionExtracted do
local v = connectionExtracted[i]
if v:match("database") then
self.name = connectionString:sub(connectionString:find("/") + 1, -1):gsub("[%?]+[%w%p]*$", "")
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
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, Server: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()
@@ -0,0 +1,60 @@
function Server:GetIdentifier(source)
local fxDk = GetConvarInt("sv_fxdkMode", 0)
if fxDk == 1 then
return "ESX-DEBUG-LICENCE"
end
local identifier = GetPlayerIdentifierByType(source, self.identifierType)
return identifier and identifier:gsub(self.identifierType .. ":", "")
end
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[self:GetIdentifier(v.source)] = true
end
else
ESX.Players = {}
end
end
function Server:OnConnecting(source, deferrals)
deferrals.defer()
Wait(0) -- Required
local identifier = self:GetIdentifier(source)
if not GetPlayerTimeOnline then
return deferrals.done(("[ESX Multicharacter] ESX Requires a minimum Artifact version of 9515, 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 identifier then
if not ESX.GetConfig().EnableDebug then
if ESX.Players[identifier] then
deferrals.done(("[ESX Multicharacter] A player is already connected to the server with this identifier.\nYour identifier: %s:%s"):format(Server.identifierType, identifier))
else
deferrals.done()
end
else
deferrals.done()
end
else
deferrals.done(("[ESX Multicharacter] Unable to retrieve player identifier.\nIdentifier type: %s"):format(Server.identifierType))
end
end
Server:ResetPlayers()
@@ -0,0 +1,95 @@
---@diagnostic disable: duplicate-set-field
Multicharacter = {}
Multicharacter._index = Multicharacter
Multicharacter.awaitingRegistration = {}
function Multicharacter:SetupCharacters(source)
while not Database.connected do
Wait(100)
end
local identifier = Server:GetIdentifier(source)
ESX.Players[identifier] = true
local slots = Database:GetPlayerSlots()
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
if not ESX.GetConfig().EnableDebug then
local identifier = ("%s%s:%s"):format(Server.prefix, charid, Server:GetIdentifier(source))
if ESX.GetPlayerFromIdentifier(identifier) then
DropPlayer(source, "[ESX Multicharacter] Your identifier " .. identifier .. " is already on the server!")
return
end
end
TriggerEvent("esx:onPlayerJoined", source, ("%s%s"):format(Server.prefix, charid))
ESX.Players[Server:GetIdentifier(source)] = true
end
end
function Multicharacter:RegistrationComplete(source, data)
local charId = self.awaitingRegistration[source]
self.awaitingRegistration[source] = nil
ESX.Players[Server:GetIdentifier(source)] = true
TriggerEvent("esx:onPlayerJoined", source, ("%s%s"):format(Server.prefix, charId), data)
end
function Multicharacter:PlayerDropped(player)
self.awaitingRegistration[player] = nil
ESX.Players[Server:GetIdentifier(player)] = nil
end