mirror of
https://github.com/esx-framework/esx_core.git
synced 2026-08-29 02:01:31 +00:00
refactor(esx_multicharacter): Complete Rewrite
This commit is contained in:
@@ -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
|
||||
@@ -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' }
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+15
-33
@@ -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
|
||||
Reference in New Issue
Block a user