diff --git a/[core]/esx_multicharacter/client/main.lua b/[core]/esx_multicharacter/client/main.lua index 3388d521..50161b31 100644 --- a/[core]/esx_multicharacter/client/main.lua +++ b/[core]/esx_multicharacter/client/main.lua @@ -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 \ No newline at end of file diff --git a/[core]/esx_multicharacter/client/modules/menu.lua b/[core]/esx_multicharacter/client/modules/menu.lua new file mode 100644 index 00000000..97d2c43f --- /dev/null +++ b/[core]/esx_multicharacter/client/modules/menu.lua @@ -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 \ No newline at end of file diff --git a/[core]/esx_multicharacter/client/modules/multicharacter.lua b/[core]/esx_multicharacter/client/modules/multicharacter.lua new file mode 100644 index 00000000..a50dd76e --- /dev/null +++ b/[core]/esx_multicharacter/client/modules/multicharacter.lua @@ -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 \ No newline at end of file diff --git a/[core]/esx_multicharacter/fxmanifest.lua b/[core]/esx_multicharacter/fxmanifest.lua index 367db711..f847337a 100644 --- a/[core]/esx_multicharacter/fxmanifest.lua +++ b/[core]/esx_multicharacter/fxmanifest.lua @@ -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' } diff --git a/[core]/esx_multicharacter/server/main.lua b/[core]/esx_multicharacter/server/main.lua index 0b2d0330..0418e173 100644 --- a/[core]/esx_multicharacter/server/main.lua +++ b/[core]/esx_multicharacter/server/main.lua @@ -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() diff --git a/[core]/esx_multicharacter/server/commands.lua b/[core]/esx_multicharacter/server/modules/commands.lua similarity index 53% rename from [core]/esx_multicharacter/server/commands.lua rename to [core]/esx_multicharacter/server/modules/commands.lua index 51e657e8..df5d8a72 100644 --- a/[core]/esx_multicharacter/server/commands.lua +++ b/[core]/esx_multicharacter/server/modules/commands.lua @@ -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, { diff --git a/[core]/esx_multicharacter/server/modules/database.lua b/[core]/esx_multicharacter/server/modules/database.lua new file mode 100644 index 00000000..96294523 --- /dev/null +++ b/[core]/esx_multicharacter/server/modules/database.lua @@ -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() \ No newline at end of file diff --git a/[core]/esx_multicharacter/server/modules/functions.lua b/[core]/esx_multicharacter/server/modules/functions.lua new file mode 100644 index 00000000..0ab35888 --- /dev/null +++ b/[core]/esx_multicharacter/server/modules/functions.lua @@ -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() \ No newline at end of file diff --git a/[core]/esx_multicharacter/server/modules/multicharacter.lua b/[core]/esx_multicharacter/server/modules/multicharacter.lua new file mode 100644 index 00000000..590d9b75 --- /dev/null +++ b/[core]/esx_multicharacter/server/modules/multicharacter.lua @@ -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