diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..0ae80640 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "Lua.diagnostics.globals": [ + "MySQL", + "GetPlayerTimeOnline" + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 3a3fd663..7fee73dd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -78,17 +78,3 @@ This code of conduct and its related procedures also applies to unacceptable beh ## 10. Contact info esxframework@gmail.com - -## 11. License and attribution - -The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). - -Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). - -_Revision 2.3. Posted 6 March 2017._ - -_Revision 2.2. Posted 4 February 2016._ - -_Revision 2.1. Posted 23 June 2014._ - -_Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ diff --git a/[core]/es_extended/client/common.lua b/[core]/es_extended/client/common.lua index e86221c3..69625133 100644 --- a/[core]/es_extended/client/common.lua +++ b/[core]/es_extended/client/common.lua @@ -1,8 +1,32 @@ -exports("getSharedObject", function() - return ESX -end) +Core = {} +ESX.PlayerData = {} +ESX.playerId = PlayerId() +ESX.serverId = GetPlayerServerId(ESX.playerId) +ESX.PlayerLoaded = false +Core.Input = {} +ESX.UI = {} +ESX.UI.Menu = {} +ESX.UI.Menu.RegisteredTypes = {} +ESX.UI.Menu.Opened = {} -AddEventHandler("esx:getSharedObject", function() - local Invoke = GetInvokingResource() - error(("Resource ^5%s^7 Used the ^5getSharedObject^7 Event, this event ^1no longer exists!^7 Visit https://documentation.esx-framework.org/tutorials/tutorials-esx/sharedevent for how to fix!"):format(Invoke)) +ESX.Game = {} +ESX.Game.Utils = {} + +ESX.Scaleform = {} +ESX.Scaleform.Utils = {} + +ESX.Streaming = {} + +CreateThread(function() + while not Config.Multichar do + Wait(100) + + if NetworkIsPlayerActive(ESX.playerId) then + ESX.DisableSpawnManager() + DoScreenFadeOut(0) + Wait(500) + TriggerServerEvent("esx:onPlayerJoined") + break + end + end end) diff --git a/[core]/es_extended/client/functions.lua b/[core]/es_extended/client/functions.lua index ee192243..b387c4cd 100644 --- a/[core]/es_extended/client/functions.lua +++ b/[core]/es_extended/client/functions.lua @@ -1,21 +1,3 @@ -ESX = {} -Core = {} -ESX.PlayerData = {} -ESX.PlayerLoaded = false -Core.Input = {} -ESX.UI = {} -ESX.UI.Menu = {} -ESX.UI.Menu.RegisteredTypes = {} -ESX.UI.Menu.Opened = {} - -ESX.Game = {} -ESX.Game.Utils = {} - -ESX.Scaleform = {} -ESX.Scaleform.Utils = {} - -ESX.Streaming = {} - function ESX.IsPlayerLoaded() return ESX.PlayerLoaded end @@ -28,13 +10,22 @@ local addonResourcesState = { ['esx_progressbar'] = GetResourceState('esx_progressbar') ~= 'missing', ['esx_notify'] = GetResourceState('esx_notify') ~= 'missing', ['esx_textui'] = GetResourceState('esx_textui') ~= 'missing', - ['esx_context'] = GetResourceState('esx_context') ~= 'missing' + ['esx_context'] = GetResourceState('esx_context') ~= 'missing', } local function IsResourceFound(resource) - return addonResourcesState[resource] or print(('[^1ERROR^7] ^5%s^7 is Missing!'):format(resource)) + return addonResourcesState[resource] or error(('Resource [^5%s^1] is Missing!'):format(resource)) end +function ESX.DisableSpawnManager() + if GetResourceState("spawnmanager") == "started" then + exports.spawnmanager:setAutoSpawn(false) + end +end + +---@param items string | table The item(s) to search for +---@param count? boolean Whether to return the count of the item as well +---@return table | number function ESX.SearchInventory(items, count) local item if type(items) == 'string' then @@ -58,6 +49,9 @@ function ESX.SearchInventory(items, count) return not item and data or data[item] end +---@param key string Table key to set +---@param val any Value to set +---@return nil function ESX.SetPlayerData(key, val) local current = ESX.PlayerData[key] ESX.PlayerData[key] = val @@ -68,10 +62,67 @@ function ESX.SetPlayerData(key, val) end end -function ESX.Progressbar(...) - return IsResourceFound('esx_progressbar') and exports['esx_progressbar']:Progressbar(...) +---@param freeze boolean Whether to freeze the player +---@return nil +function Core.FreezePlayer(freeze) + local ped = PlayerPedId() + SetPlayerControl(ESX.playerId, not freeze, 0) + + if freeze then + SetEntityCollision(ped, false, false) + FreezeEntityPosition(ped, true) + else + SetEntityCollision(ped, true, true) + FreezeEntityPosition(ped, false) + end end +---@param skin table Skin data to set +---@param coords table Coords to spawn the player at +---@param cb function Callback function +---@return nil +function ESX.SpawnPlayer(skin, coords, cb) + local p = promise.new() + TriggerEvent("skinchanger:loadSkin", skin, function() + p:resolve() + end) + Citizen.Await(p) + + RequestCollisionAtCoord(coords.x, coords.y, coords.z) + + local playerPed = PlayerPedId() + local timer = GetGameTimer() + + Core.FreezePlayer(true) + SetEntityCoordsNoOffset(playerPed, coords.x, coords.y, coords.z, false, false, true) + SetEntityHeading(playerPed, coords.heading) + + RequestCollisionAtCoord(coords.x, coords.y, coords.z) + while not HasCollisionLoadedAroundEntity(playerPed) and (GetGameTimer() - timer) < 5000 do + Wait(0) + end + + NetworkResurrectLocalPlayer(coords.x, coords.y, coords.z, coords.heading, 0, true) + TriggerEvent('playerSpawned', coords) + cb() +end + +---@param message string +---@param length? number Timeout in milliseconds +---@param options? ProgressBarOptions +---@return boolean Success Whether the progress bar was successfully created or not +function ESX.Progressbar(message, length, options) + return IsResourceFound('esx_progressbar') and exports['esx_progressbar']:Progressbar(message, length, options) +end + +function ESX.CancelProgressbar() + return IsResourceFound('esx_progressbar') and exports['esx_progressbar']:CancelProgressbar() +end + +---@param message string The message to show +---@param notifyType? string The type of notification to show +---@param length? number The length of the notification +---@return nil function ESX.ShowNotification(message, notifyType, length) return IsResourceFound('esx_notify') and exports['esx_notify']:Notify(notifyType, length, message) end @@ -80,10 +131,20 @@ function ESX.TextUI(...) return IsResourceFound('esx_textui') and exports['esx_textui']:TextUI(...) end +---@return nil function ESX.HideUI() return IsResourceFound('esx_textui') and exports['esx_textui']:HideUI() end +---@param sender string +---@param subject string +---@param msg string +---@param textureDict string +---@param iconType integer +---@param flash boolean +---@param saveToBrief? boolean +---@param hudColorIndex? integer +---@return nil function ESX.ShowAdvancedNotification(sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex) AddTextEntry("esxAdvancedNotification", msg) BeginTextCommandThefeedPost("esxAdvancedNotification") @@ -94,24 +155,35 @@ function ESX.ShowAdvancedNotification(sender, subject, msg, textureDict, iconTyp EndTextCommandThefeedPostTicker(flash, saveToBrief == nil or saveToBrief) end +---@param msg string The message to show +---@param thisFrame? boolean Whether to show the message this frame +---@param beep? boolean Whether to beep +---@param duration? number The duration of the message +---@return nil function ESX.ShowHelpNotification(msg, thisFrame, beep, duration) AddTextEntry("esxHelpNotification", msg) if thisFrame then - DisplayHelpTextThisFrame("esxHelpNotification") + DisplayHelpTextThisFrame("esxHelpNotification", false) else BeginTextCommandDisplayHelp("esxHelpNotification") EndTextCommandDisplayHelp(0, false, beep == nil or beep, duration or -1) end end +---@param msg string The message to show +---@param coords table The coords to show the message at +---@return nil function ESX.ShowFloatingHelpNotification(msg, coords) AddTextEntry("esxFloatingHelpNotification", msg) - SetFloatingHelpTextWorldPosition(1, coords) + SetFloatingHelpTextWorldPosition(1, coords.x, coords.y, coords.z) SetFloatingHelpTextStyle(1, 1, 2, -1, 3, 0) BeginTextCommandDisplayHelp("esxFloatingHelpNotification") EndTextCommandDisplayHelp(2, false, false, -1) end +---@param msg string The message to show +---@param time number The duration of the message +---@return nil function ESX.DrawMissionText(msg, time) ClearPrints() BeginTextCommandPrint('STRING') @@ -119,6 +191,8 @@ function ESX.DrawMissionText(msg, time) EndTextCommandPrint(time, true) end +---@param str string The string to hash +---@return string The hashed string function ESX.HashString(str) return ('~INPUT_%s~'):format(('%x'):format(joaat(str) & 0x7fffffff + 2 ^ 31):upper()) end @@ -139,16 +213,25 @@ function ESX.RefreshContext(...) return IsResourceFound('esx_context') and exports['esx_context']:Refresh(...) end +---@param command_name string The command name +---@param label string The label to show +---@param input_group string The input group +---@param key string The key to bind +---@param on_press function The function to call on press +---@param on_release? function The function to call on release function ESX.RegisterInput(command_name, label, input_group, key, on_press, on_release) local command = on_release and '+' .. command_name or command_name - RegisterCommand(command, on_press) + RegisterCommand(command, on_press, false) Core.Input[command_name] = ESX.HashString(command) if on_release then - RegisterCommand('-' .. command_name, on_release) + RegisterCommand('-' .. command_name, on_release, false) end RegisterKeyMapping(command, label or '', input_group or 'keyboard', key or '') end +---@param menuType string +---@param open function The function to call on open +---@param close function The function to call on close function ESX.UI.Menu.RegisterType(menuType, open, close) ESX.UI.Menu.RegisteredTypes[menuType] = { open = open, @@ -156,7 +239,34 @@ function ESX.UI.Menu.RegisterType(menuType, open, close) } end +---@class ESXMenu +---@field type string +---@field namespace string +---@field resourceName string +---@field name string +---@field data table +---@field submit? function +---@field cancel? function +---@field change? function +---@field close function +---@field update function +---@field refresh function +---@field setElement function +---@field setElements function +---@field setTitle function +---@field removeElement function + +---@param menuType string +---@param namespace string +---@param name string +---@param data table +---@param submit? function +---@param cancel? function +---@param change? function +---@param close? function +---@return ESXMenu function ESX.UI.Menu.Open(menuType, namespace, name, data, submit, cancel, change, close) + ---@class ESXMenu local menu = {} menu.type = menuType @@ -237,6 +347,11 @@ function ESX.UI.Menu.Open(menuType, namespace, name, data, submit, cancel, chang return menu end +---@param menuType string +---@param namespace string +---@param name string +---@param cancel? boolean Should the close be classified as a cancel +---@return nil function ESX.UI.Menu.Close(menuType, namespace, name, cancel) for i = 1, #ESX.UI.Menu.Opened, 1 do if ESX.UI.Menu.Opened[i] then @@ -252,6 +367,8 @@ function ESX.UI.Menu.Close(menuType, namespace, name, cancel) end end +---@param cancel? boolean Should the close be classified as a cancel +---@return nil function ESX.UI.Menu.CloseAll(cancel) for i = 1, #ESX.UI.Menu.Opened, 1 do if ESX.UI.Menu.Opened[i] then @@ -265,6 +382,10 @@ function ESX.UI.Menu.CloseAll(cancel) end end +---@param menuType string +---@param namespace string +---@param name string +---@return ESXMenu | nil function ESX.UI.Menu.GetOpened(menuType, namespace, name) for i = 1, #ESX.UI.Menu.Opened, 1 do if ESX.UI.Menu.Opened[i] then @@ -275,12 +396,17 @@ function ESX.UI.Menu.GetOpened(menuType, namespace, name) end end +---@return ESXMenu[] function ESX.UI.Menu.GetOpenedMenus() return ESX.UI.Menu.Opened end ESX.UI.Menu.IsOpen = ESX.UI.Menu.GetOpened +---@param add boolean Whether the item is being added or removed +---@param item string The item to show +---@param count number How many of the item to show +---@return nil function ESX.UI.ShowInventoryItemNotification(add, item, count) SendNUIMessage({ action = "inventoryNotification", @@ -290,6 +416,8 @@ function ESX.UI.ShowInventoryItemNotification(add, item, count) }) end +---@param ped integer The ped to get the mugshot of +---@param transparent? boolean Whether the mugshot should be transparent function ESX.Game.GetPedMugshot(ped, transparent) if not DoesEntityExist(ped) then return @@ -303,17 +431,19 @@ function ESX.Game.GetPedMugshot(ped, transparent) return mugshot, GetPedheadshotTxdString(mugshot) end +---@param entity integer The entity to get the coords of +---@param coords table | vector3 | vector4 The coords to teleport the entity to +---@param cb? function The callback function function ESX.Game.Teleport(entity, coords, cb) - local vector = type(coords) == "vector4" and coords or type(coords) == "vector3" and vector4(coords, 0.0) or vec(coords.x, coords.y, coords.z, coords.heading or 0.0) if DoesEntityExist(entity) then - RequestCollisionAtCoord(vector.xyz) + RequestCollisionAtCoord(coords.x, coords.y, coords.z) while not HasCollisionLoadedAroundEntity(entity) do Wait(0) end - SetEntityCoords(entity, vector.xyz, false, false, false, false) - SetEntityHeading(entity, vector.w) + SetEntityCoords(entity, coords.x, coords.y, coords.z, false, false, false, false) + SetEntityHeading(entity, coords.w or coords.heading or 0.0) end if cb then @@ -321,29 +451,52 @@ function ESX.Game.Teleport(entity, coords, cb) end end +---@param object integer | string The object to spawn +---@param coords table | vector3 The coords to spawn the object at +---@param cb? function The callback function +---@param networked? boolean Whether the object should be networked +---@return integer | nil function ESX.Game.SpawnObject(object, coords, cb, networked) - local obj = CreateObject(ESX.Streaming.RequestModel(object), coords.x, coords.y, coords.z, networked == nil or networked, false, true) + local model = type(object) == "number" and object or joaat(object) + + ESX.Streaming.RequestModel(model) + + local obj = CreateObject(model, coords.x, coords.y, coords.z, networked == nil or networked, false, true) return cb and cb(obj) or obj end +---@param object integer | string The object to spawn +---@param coords table | vector3 The coords to spawn the object at +---@param cb? function The callback function +---@return nil function ESX.Game.SpawnLocalObject(object, coords, cb) ESX.Game.SpawnObject(object, coords, cb, false) end +---@param vehicle integer The vehicle to delete +---@return nil function ESX.Game.DeleteVehicle(vehicle) SetEntityAsMissionEntity(vehicle, true, true) DeleteVehicle(vehicle) end +---@param object integer The object to delete +---@return nil function ESX.Game.DeleteObject(object) SetEntityAsMissionEntity(object, false, true) DeleteObject(object) end +---@param vehicleModel integer | string The vehicle to spawn +---@param coords table | vector3 The coords to spawn the vehicle at +---@param heading number The heading of the vehicle +---@param cb? function The callback function +---@param networked? boolean Whether the vehicle should be networked +---@return nil function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) local model = type(vehicleModel) == "number" and vehicleModel or joaat(vehicleModel) local vector = type(coords) == "vector3" and coords or vec(coords.x, coords.y, coords.z) - networked = networked == nil and true or networked + local isNetworked = networked == nil or networked local playerCoords = GetEntityCoords(ESX.PlayerData.ped) if not vector or not playerCoords then @@ -353,13 +506,13 @@ function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) local dist = #(playerCoords - vector) if dist > 424 then -- Onesync infinity Range (https://docs.fivem.net/docs/scripting-reference/onesync/) local executingResource = GetInvokingResource() or "Unknown" - return error(("Resource ^5%s^7 Tried to spawn vehicle on the client but the position is too far away (Out of onesync range)."):format(executingResource)) + return error(("Resource ^5%s^1 Tried to spawn vehicle on the client but the position is too far away (Out of onesync range)."):format(executingResource)) end CreateThread(function() ESX.Streaming.RequestModel(model) - local vehicle = CreateVehicle(model, vector.xyz, heading, networked, true) + local vehicle = CreateVehicle(model, vector.x, vector.y, vector.z, heading, isNetworked, true) if networked then local id = NetworkGetNetworkIdFromEntity(vehicle) @@ -371,7 +524,7 @@ function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) SetModelAsNoLongerNeeded(model) SetVehRadioStation(vehicle, "OFF") - RequestCollisionAtCoord(vector.xyz) + RequestCollisionAtCoord(vector.x, vector.y, vector.z) while not HasCollisionLoadedAroundEntity(vehicle) do Wait(0) end @@ -382,18 +535,28 @@ function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) end) end +---@param vehicle integer The vehicle to spawn +---@param coords table | vector3 The coords to spawn the vehicle at +---@param heading number The heading of the vehicle +---@param cb? function The callback function +---@return nil function ESX.Game.SpawnLocalVehicle(vehicle, coords, heading, cb) ESX.Game.SpawnVehicle(vehicle, coords, heading, cb, false) end +---@param vehicle integer The vehicle to check +---@return boolean function ESX.Game.IsVehicleEmpty(vehicle) return GetVehicleNumberOfPassengers(vehicle) == 0 and IsVehicleSeatFree(vehicle, -1) end +---@return table function ESX.Game.GetObjects() -- Leave the function for compatibility return GetGamePool("CObject") end +---@param onlyOtherPeds? boolean Whether to exlude the player ped +---@return table function ESX.Game.GetPeds(onlyOtherPeds) local pool = GetGamePool("CPed") @@ -410,19 +573,24 @@ function ESX.Game.GetPeds(onlyOtherPeds) return pool end +---@return table function ESX.Game.GetVehicles() -- Leave the function for compatibility return GetGamePool("CVehicle") end +---@param onlyOtherPlayers? boolean Whether to exclude the player +---@param returnKeyValue? boolean Whether to return the key value pair +---@param returnPeds? boolean Whether to return the peds +---@return table function ESX.Game.GetPlayers(onlyOtherPlayers, returnKeyValue, returnPeds) - local players, myPlayer = {}, PlayerId() + local players = {} local active = GetActivePlayers() for i = 1, #active do local currentPlayer = active[i] local ped = GetPlayerPed(currentPlayer) - if DoesEntityExist(ped) and ((onlyOtherPlayers and currentPlayer ~= myPlayer) or not onlyOtherPlayers) then + if DoesEntityExist(ped) and ((onlyOtherPlayers and currentPlayer ~= ESX.playerId) or not onlyOtherPlayers) then if returnKeyValue then players[currentPlayer] = ped else @@ -434,22 +602,38 @@ function ESX.Game.GetPlayers(onlyOtherPlayers, returnKeyValue, returnPeds) return players end +---@param coords? table | vector3 The coords to get the closest object to +---@param modelFilter? table The model filter +---@return integer, integer function ESX.Game.GetClosestObject(coords, modelFilter) return ESX.Game.GetClosestEntity(ESX.Game.GetObjects(), false, coords, modelFilter) end +---@param coords? table | vector3 The coords to get the closest ped to +---@param modelFilter? table The model filter +---@return integer, integer function ESX.Game.GetClosestPed(coords, modelFilter) return ESX.Game.GetClosestEntity(ESX.Game.GetPeds(true), false, coords, modelFilter) end +---@param coords? table | vector3 The coords to get the closest player to +---@return integer, integer function ESX.Game.GetClosestPlayer(coords) return ESX.Game.GetClosestEntity(ESX.Game.GetPlayers(true, true), true, coords, nil) end +---@param coords? table | vector3 The coords to get the closest vehicle to +---@param modelFilter? table The model filter +---@return integer, integer function ESX.Game.GetClosestVehicle(coords, modelFilter) return ESX.Game.GetClosestEntity(ESX.Game.GetVehicles(), false, coords, modelFilter) end +---@param entities table The entities to search through +---@param isPlayerEntities boolean Whether the entities are players +---@param coords table | vector3 The coords to search from +---@param maxDistance number The max distance to search within +---@return table local function EnumerateEntitiesWithinDistance(entities, isPlayerEntities, coords, maxDistance) local nearbyEntities = {} @@ -471,25 +655,41 @@ local function EnumerateEntitiesWithinDistance(entities, isPlayerEntities, coord return nearbyEntities end +---@param coords table | vector3 The coords to search from +---@param maxDistance number The max distance to search within +---@return table function ESX.Game.GetPlayersInArea(coords, maxDistance) return EnumerateEntitiesWithinDistance(ESX.Game.GetPlayers(true, true), true, coords, maxDistance) end +---@param coords table | vector3 The coords to search from +---@param maxDistance number The max distance to search within +---@return table function ESX.Game.GetVehiclesInArea(coords, maxDistance) return EnumerateEntitiesWithinDistance(ESX.Game.GetVehicles(), false, coords, maxDistance) end +---@param coords table | vector3 The coords to search from +---@param maxDistance number The max distance to search within +---@return boolean function ESX.Game.IsSpawnPointClear(coords, maxDistance) return #ESX.Game.GetVehiclesInArea(coords, maxDistance) == 0 end +---@param shape integer The shape to get the test result from +---@return boolean, table, table, integer, integer function ESX.Game.GetShapeTestResultSync(shape) local handle, hit, coords, normal, material, entity - repeat handle, hit, coords, normal, material, entity = GetShapeTestResultIncludingMaterial(shape) - until handle ~= 1 or Wait() + repeat + handle, hit, coords, normal, material, entity = GetShapeTestResultIncludingMaterial(shape) + Wait(0) + until handle ~= 1 return hit, coords, normal, material, entity end +---@param depth number The depth to raycast +---@vararg any The arguments to pass to the shape test +---@return table, boolean, table, table, integer, integer function ESX.Game.RaycastScreen(depth, ...) local world, normal = GetWorldCoordFromScreenCoord(.5, .5) local origin = world + normal @@ -497,6 +697,11 @@ function ESX.Game.RaycastScreen(depth, ...) return target, ESX.Game.GetShapeTestResultSync(StartShapeTestLosProbe(origin.x, origin.y, origin.z, target.x, target.y, target.z, ...)) end +---@param entities table The entities to search through +---@param isPlayerEntities boolean Whether the entities are players +---@param coords? table | vector3 The coords to search from +---@param modelFilter? table The model filter +---@return integer, integer function ESX.Game.GetClosestEntity(entities, isPlayerEntities, coords, modelFilter) local closestEntity, closestEntityDistance, filteredEntities = -1, -1, nil @@ -528,6 +733,7 @@ function ESX.Game.GetClosestEntity(entities, isPlayerEntities, coords, modelFilt return closestEntity, closestEntityDistance end +---@return integer | nil, vector3 | nil function ESX.Game.GetVehicleInDirection() local _, hit, coords, _, _, entity = ESX.Game.RaycastScreen(5, 10, ESX.PlayerData.ped) if hit and IsEntityAVehicle(entity) then @@ -535,6 +741,8 @@ function ESX.Game.GetVehicleInDirection() end end +---@param vehicle integer The vehicle to get the properties of +---@return table | nil function ESX.Game.GetVehicleProperties(vehicle) if not DoesEntityExist(vehicle) then return @@ -690,6 +898,9 @@ function ESX.Game.GetVehicleProperties(vehicle) } end +---@param vehicle integer The vehicle to set the properties of +---@param props table The properties to set +---@return nil function ESX.Game.SetVehicleProperties(vehicle, props) if not DoesEntityExist(vehicle) then return @@ -766,7 +977,10 @@ function ESX.Game.SetVehicleProperties(vehicle, props) if props.extras ~= nil then for extraId, enabled in pairs(props.extras) do - SetVehicleExtra(vehicle, tonumber(extraId), enabled and 0 or 1) + extraId = tonumber(extraId) + if extraId then + SetVehicleExtra(vehicle, extraId, enabled) + end end end @@ -928,7 +1142,10 @@ function ESX.Game.SetVehicleProperties(vehicle, props) if props.windowsBroken ~= nil then for k, v in pairs(props.windowsBroken) do if v then - RemoveVehicleWindow(vehicle, tonumber(k)) + k = tonumber(k) + if k then + RemoveVehicleWindow(vehicle, k) + end end end end @@ -936,7 +1153,10 @@ function ESX.Game.SetVehicleProperties(vehicle, props) if props.doorsBroken ~= nil then for k, v in pairs(props.doorsBroken) do if v then - SetVehicleDoorBroken(vehicle, tonumber(k), true) + k = tonumber(k) + if k then + SetVehicleDoorBroken(vehicle, k, true) + end end end end @@ -944,12 +1164,20 @@ function ESX.Game.SetVehicleProperties(vehicle, props) if props.tyreBurst ~= nil then for k, v in pairs(props.tyreBurst) do if v then - SetVehicleTyreBurst(vehicle, tonumber(k), true, 1000.0) + k = tonumber(k) + if k then + SetVehicleTyreBurst(vehicle, k, true, 1000.0) + end end end end end +---@param coords vector3 | table coords to get the closest pickup to +---@param text string The text to display +---@param size? number The size of the text +---@param font? number The font of the text +---@return nil function ESX.Game.Utils.DrawText3D(coords, text, size, font) local vector = type(coords) == "vector3" and coords or vec(coords.x, coords.y, coords.z) @@ -965,12 +1193,12 @@ function ESX.Game.Utils.DrawText3D(coords, text, size, font) SetTextScale(0.0, 0.55 * scale) SetTextFont(font) - SetTextProportional(1) + SetTextProportional(true) SetTextColour(255, 255, 255, 215) BeginTextCommandDisplayText("STRING") SetTextCentre(true) AddTextComponentSubstringPlayerName(text) - SetDrawOrigin(vector.xyz, 0) + SetDrawOrigin(vector.x, vector.y, vector.z, 0) EndTextCommandDisplayText(0.0, 0.0) ClearDrawOrigin() end @@ -986,6 +1214,7 @@ function ESX.GetAccount(account) return nil end +---@return nil function ESX.ShowInventory() if not Config.EnableDefaultInventory then return @@ -1310,11 +1539,11 @@ local mismatchedTypes = { } ---@param model number|string ----@return string -function ESX.GetVehicleType(model) +---@return string | boolean +function ESX.GetVehicleTypeClient(model) model = type(model) == "string" and joaat(model) or model if not IsModelInCdimage(model) then - return + return false end if mismatchedTypes[model] then return mismatchedTypes[model] @@ -1333,3 +1562,5 @@ function ESX.GetVehicleType(model) return types[vehicleType] or "automobile" end + +ESX.GetVehicleType = ESX.GetVehicleTypeClient diff --git a/[core]/es_extended/client/main.lua b/[core]/es_extended/client/main.lua index bbb8993c..5192cd88 100644 --- a/[core]/es_extended/client/main.lua +++ b/[core]/es_extended/client/main.lua @@ -1,71 +1,17 @@ local pickups = {} -CreateThread(function() - while not Config.Multichar do - Wait(100) - - if NetworkIsPlayerActive(PlayerId()) then - exports.spawnmanager:setAutoSpawn(false) - DoScreenFadeOut(0) - Wait(500) - TriggerServerEvent("esx:onPlayerJoined") - break - end - end -end) - RegisterNetEvent("esx:requestModel", function(model) ESX.Streaming.RequestModel(model) end) -function ESX.SpawnPlayer(skin, coords, cb) - local p = promise.new() - TriggerEvent("skinchanger:loadSkin", skin, function() - p:resolve() - end) - Citizen.Await(p) - - RequestCollisionAtCoord(coords.x, coords.y, coords.z) - - local playerPed = PlayerPedId() - local timer = GetGameTimer() - - FreezeEntityPosition(playerPed, true) - SetEntityCoordsNoOffset(playerPed, coords.x, coords.y, coords.z, false, false, false, true) - SetEntityHeading(playerPed, coords.heading) - - while not HasCollisionLoadedAroundEntity(playerPed) and (GetGameTimer() - timer) < 5000 do - Wait(0) +local function EnablePvP() + if Config.EnablePVP then + SetCanAttackFriendly(ESX.PlayerData.ped, true, false) + NetworkSetFriendlyFireOption(true) end - - NetworkResurrectLocalPlayer(coords.x, coords.y, coords.z, coords.heading, true, true, false) - TriggerEvent('playerSpawned', coords) - cb() end -RegisterNetEvent("esx:playerLoaded") -AddEventHandler("esx:playerLoaded", function(xPlayer, _, skin) - ESX.PlayerData = xPlayer - - if not Config.Multichar then - ESX.SpawnPlayer(skin, ESX.PlayerData.coords, function() - TriggerEvent("esx:onPlayerSpawn") - TriggerEvent("esx:restoreLoadout") - TriggerServerEvent("esx:onPlayerSpawn") - TriggerEvent("esx:loadingScreenOff") - ShutdownLoadingScreen() - ShutdownLoadingScreenNui() - end) - end - - while not DoesEntityExist(ESX.PlayerData.ped) do - Wait(20) - end - - ESX.PlayerLoaded = true - - local metadata = ESX.PlayerData.metadata - +local function ApplyMetadata(metadata) if metadata.health then SetEntityHealth(ESX.PlayerData.ped, metadata.health) end @@ -73,74 +19,10 @@ AddEventHandler("esx:playerLoaded", function(xPlayer, _, skin) if metadata.armor and metadata.armor > 0 then SetPedArmour(ESX.PlayerData.ped, metadata.armor) end +end - local timer = GetGameTimer() - while not HaveAllStreamingRequestsCompleted(ESX.PlayerData.ped) and (GetGameTimer() - timer) < 2000 do - Wait(0) - end - - if Config.EnablePVP then - SetCanAttackFriendly(ESX.PlayerData.ped, true, false) - NetworkSetFriendlyFireOption(true) - end - - local playerId = PlayerId() - -- RemoveHudComponents - for i = 1, #Config.RemoveHudComponents do - if Config.RemoveHudComponents[i] then - SetHudComponentPosition(i, 999999.0, 999999.0) - end - end - - -- DisableNPCDrops - if Config.DisableNPCDrops then - local weaponPickups = { `PICKUP_WEAPON_CARBINERIFLE`, `PICKUP_WEAPON_PISTOL`, `PICKUP_WEAPON_PUMPSHOTGUN` } - for i = 1, #weaponPickups do - ToggleUsePickupsForPlayer(playerId, weaponPickups[i], false) - end - end - - if Config.DisableVehicleSeatShuff then - AddEventHandler("esx:enteredVehicle", function(vehicle, _, seat) - if seat == 0 then - SetPedIntoVehicle(ESX.PlayerData.ped, vehicle, 0) - SetPedConfigFlag(ESX.PlayerData.ped, 184, true) - end - end) - end - - if Config.DisableHealthRegeneration then - SetPlayerHealthRechargeMultiplier(playerId, 0.0) - end - - if Config.DisableWeaponWheel or Config.DisableAimAssist or Config.DisableVehicleRewards then - CreateThread(function() - while true do - if Config.DisableDisplayAmmo then - DisplayAmmoThisFrame(false) - end - - if Config.DisableWeaponWheel then - BlockWeaponWheelThisFrame() - DisableControlAction(0, 37, true) - end - - if Config.DisableAimAssist then - if IsPedArmed(ESX.PlayerData.ped, 4) then - SetPlayerLockonRangeOverride(playerId, 2.0) - end - end - - if Config.DisableVehicleRewards then - DisablePlayerVehicleRewards(playerId) - end - - Wait(0) - end - end) - end - - -- Disable Dispatch services +local function DisableNPCs() + -- Disable Dispatch services if Config.DisableDispatchServices then for i = 1, 15 do EnableDispatchService(i, false) @@ -148,7 +30,6 @@ AddEventHandler("esx:playerLoaded", function(xPlayer, _, skin) SetAudioFlag('PoliceScannerDisabled', true) end - -- Disable Scenarios if Config.DisableScenarios then local scenarios = { "WORLD_VEHICLE_ATTRACTOR", @@ -208,15 +89,106 @@ AddEventHandler("esx:playerLoaded", function(xPlayer, _, skin) SetScenarioTypeEnabled(v, false) end end +end + +local function DisableAmmoOrVehicleRewards(playerId) + CreateThread(function() + while true do + if Config.DisableDisplayAmmo then + DisplayAmmoThisFrame(false) + end + + if Config.DisableVehicleRewards then + DisablePlayerVehicleRewards(playerId) + end + + Wait(0) + end + end) +end + +local function ApplyConfig() + -- RemoveHudComponents + for i = 1, #Config.RemoveHudComponents do + if Config.RemoveHudComponents[i] then + SetHudComponentSize(i, 0.0, 0.0) + end + end + + if Config.DisableAimAssist then + SetPlayerLockon(ESX.playerId, false) + end + + -- DisableNPCDrops + if Config.DisableNPCDrops then + local weaponPickups = { `PICKUP_WEAPON_CARBINERIFLE`, `PICKUP_WEAPON_PISTOL`, `PICKUP_WEAPON_PUMPSHOTGUN` } + for i = 1, #weaponPickups do + ToggleUsePickupsForPlayer(ESX.playerId, weaponPickups[i], false) + end + end + + if Config.DisableVehicleSeatShuff then + AddEventHandler("esx:enteredVehicle", function(vehicle, _, seat) + if seat > -1 then + SetPedIntoVehicle(ESX.PlayerData.ped, vehicle, seat) + SetPedConfigFlag(ESX.PlayerData.ped, 184, true) + end + end) + end + + if Config.DisableHealthRegeneration then + SetPlayerHealthRechargeMultiplier(ESX.playerId, 0.0) + end + + if Config.DisableDisplayAmmo or Config.DisableVehicleRewards then + DisableAmmoOrVehicleRewards(ESX.playerId) + end + + EnablePvP() + DisableNPCs() +end + +RegisterNetEvent("esx:playerLoaded") +AddEventHandler("esx:playerLoaded", function(xPlayer, _, skin) + ESX.PlayerData = xPlayer if not Config.Multichar then - FreezeEntityPosition(ESX.PlayerData.ped, false) + ESX.SpawnPlayer(skin, ESX.PlayerData.coords, function() + TriggerEvent("esx:onPlayerSpawn") + TriggerEvent("esx:restoreLoadout") + TriggerServerEvent("esx:onPlayerSpawn") + TriggerEvent("esx:loadingScreenOff") + ShutdownLoadingScreen() + ShutdownLoadingScreenNui() + end) + end + + while not DoesEntityExist(ESX.PlayerData.ped) do + Wait(20) + end + + ESX.PlayerLoaded = true + + ApplyMetadata(ESX.PlayerData.metadata) + + local timer = GetGameTimer() + while not HaveAllStreamingRequestsCompleted(ESX.PlayerData.ped) and (GetGameTimer() - timer) < 2000 do + Wait(0) + end + + ApplyConfig() + + ClearPedTasksImmediately(ESX.PlayerData.ped) + + if not Config.Multichar then + Core.FreezePlayer(false) end if IsScreenFadedOut() then DoScreenFadeIn(500) end + Actions:Init() SetDefaultVehicleNumberPlateTextPattern(-1, Config.CustomAIPlates) StartServerSyncLoops() end) @@ -281,13 +253,26 @@ AddEventHandler("esx:restoreLoadout", function() end end) +---@diagnostic disable-next-line: param-type-mismatch AddStateBagChangeHandler("VehicleProperties", nil, function(bagName, _, value) if not value then return end - local netId = bagName:gsub("entity:", "") - local vehicle = NetToVeh(tonumber(netId)) + local netId = tonumber(bagName:gsub("entity:", "")) + if not netId then + error("Tried to set vehicle properties with invalid netId") + return + end + local vehicle = NetToVeh(netId) + if not DoesEntityExist(vehicle) then + error("Tried to set vehicle properties with invalid vehicle") + return + end + + if not NetworkGetEntityOwner(vehicle) == ESX.playerId then + return + end ESX.Game.SetVehicleProperties(vehicle, value) end) @@ -337,17 +322,17 @@ if not Config.OxInventory then RegisterNetEvent("esx:addWeapon") AddEventHandler("esx:addWeapon", function() - error("event ^5'esx:addWeapon'^7 Has Been Removed. Please use ^5xPlayer.addWeapon^7 Instead!") + error("event ^5'esx:addWeapon'^1 Has Been Removed. Please use ^5xPlayer.addWeapon^1 Instead!") end) RegisterNetEvent("esx:addWeaponComponent") AddEventHandler("esx:addWeaponComponent", function() - error("event ^5'esx:addWeaponComponent'^7 Has Been Removed. Please use ^5xPlayer.addWeaponComponent^7 Instead!") + error("event ^5'esx:addWeaponComponent'^1 Has Been Removed. Please use ^5xPlayer.addWeaponComponent^1 Instead!") end) RegisterNetEvent("esx:setWeaponAmmo") AddEventHandler("esx:setWeaponAmmo", function() - error("event ^5'esx:setWeaponAmmo'^7 Has Been Removed. Please use ^5xPlayer.addWeaponAmmo^7 Instead!") + error("event ^5'esx:setWeaponAmmo'^1 Has Been Removed. Please use ^5xPlayer.addWeaponAmmo^1 Instead!") end) RegisterNetEvent("esx:setWeaponTint") @@ -357,7 +342,7 @@ if not Config.OxInventory then RegisterNetEvent("esx:removeWeapon") AddEventHandler("esx:removeWeapon", function() - error("event ^5'esx:removeWeapon'^7 Has Been Removed. Please use ^5xPlayer.removeWeapon^7 Instead!") + error("event ^5'esx:removeWeapon'^1 Has Been Removed. Please use ^5xPlayer.removeWeapon^1 Instead!") end) RegisterNetEvent("esx:removeWeaponComponent") @@ -402,7 +387,9 @@ if not Config.OxInventory then for _, v in ipairs(components) do local component = ESX.GetWeaponComponent(name, v) - GiveWeaponComponentToWeaponObject(pickupObject, component.hash) + if component then + GiveWeaponComponentToWeaponObject(pickupObject, component.hash) + end end setObjectProperties(pickupObject) @@ -479,7 +466,7 @@ end -- disable wanted level if not Config.EnableWantedLevel then - ClearPlayerWantedLevel(PlayerId()) + ClearPlayerWantedLevel(ESX.playerId) SetMaxWantedLevel(0) end @@ -516,11 +503,8 @@ if not Config.OxInventory then label = ("%s~n~%s"):format(label, TranslateCap("threw_pickup_prompt")) end - ESX.Game.Utils.DrawText3D({ - x = pickup.coords.x, - y = pickup.coords.y, - z = pickup.coords.z + 0.25, - }, label, 1.2, 1) + local textCoords = pickup.coords + vector3(0.0, 0.0, 0.25) + ESX.Game.Utils.DrawText3D(textCoords, label, 1.2, 1) elseif pickup.inRange then pickup.inRange = false end @@ -548,7 +532,7 @@ AddEventHandler("esx:tpm", function() end local blipMarker = GetFirstBlipInfoId(8) if not DoesBlipExist(blipMarker) then - ESX.ShowNotification(TranslateCap("tpm_nowaypoint"), true, false, 140) + ESX.ShowNotification(TranslateCap("tpm_nowaypoint"), "error") return "marker" end @@ -611,12 +595,12 @@ AddEventHandler("esx:tpm", function() -- If we can't find the coords, set the coords to the old ones. -- We don't unpack them before since they aren't in a loop and only called once. SetPedCoordsKeepVehicle(ped, oldCoords["x"], oldCoords["y"], oldCoords["z"] - 1.0) - ESX.ShowNotification(TranslateCap("tpm_success"), true, false, 140) + ESX.ShowNotification(TranslateCap("tpm_success"), "success") end -- If Z coord was found, set coords in found coords. SetPedCoordsKeepVehicle(ped, x, y, groundZ) - ESX.ShowNotification(TranslateCap("tpm_success"), true, false, 140) + ESX.ShowNotification(TranslateCap("tpm_success"), "success") end) end) @@ -626,7 +610,7 @@ local heading = 0 local function noclipThread() while noclip do - SetEntityCoordsNoOffset(ESX.PlayerData.ped, noclip_pos.x, noclip_pos.y, noclip_pos.z, 0, 0, 0) + SetEntityCoordsNoOffset(ESX.PlayerData.ped, noclip_pos.x, noclip_pos.y, noclip_pos.z, false, false, true) if IsControlPressed(1, 34) then heading = heading + 1.5 @@ -682,7 +666,11 @@ AddEventHandler("esx:noclip", function() CreateThread(noclipThread) end - ESX.ShowNotification(TranslateCap("noclip_message", noclip and Translate("enabled") or Translate("disabled")), true, false, 140) + if noclip then + ESX.ShowNotification(TranslateCap("noclip_message", Translate("enabled")), "success") + else + ESX.ShowNotification(TranslateCap("noclip_message", Translate("disabled")), "error") + end end) end) @@ -696,27 +684,26 @@ AddEventHandler("esx:repairPedVehicle", function() local ped = ESX.PlayerData.ped local vehicle = GetVehiclePedIsIn(ped, false) SetVehicleEngineHealth(vehicle, 1000) - SetVehicleEngineOn(vehicle, true, true) + SetVehicleEngineOn(vehicle, true, true, false) SetVehicleFixed(vehicle) SetVehicleDirtLevel(vehicle, 0) end) RegisterNetEvent("esx:freezePlayer") AddEventHandler("esx:freezePlayer", function(input) - local player = PlayerId() if input == "freeze" then - SetEntityCollision(ESX.PlayerData.ped, false) + SetEntityCollision(ESX.PlayerData.ped, false, false) FreezeEntityPosition(ESX.PlayerData.ped, true) - SetPlayerInvincible(player, true) + SetPlayerInvincible(ESX.playerId, true) elseif input == "unfreeze" then - SetEntityCollision(ESX.PlayerData.ped, true) + SetEntityCollision(ESX.PlayerData.ped, true, true) FreezeEntityPosition(ESX.PlayerData.ped, false) - SetPlayerInvincible(player, false) + SetPlayerInvincible(ESX.playerId, false) end end) ESX.RegisterClientCallback("esx:GetVehicleType", function(cb, model) - cb(ESX.GetVehicleType(model)) + cb(ESX.GetVehicleTypeClient(model)) end) RegisterNetEvent('esx:updatePlayerData', function(key, val) diff --git a/[core]/es_extended/client/modules/actions.lua b/[core]/es_extended/client/modules/actions.lua index 7af8db25..455dcff0 100644 --- a/[core]/es_extended/client/modules/actions.lua +++ b/[core]/es_extended/client/modules/actions.lua @@ -1,129 +1,152 @@ -local isInVehicle, isEnteringVehicle, isJumping, inPauseMenu = false, false, false, false -local playerPed = PlayerPedId() -local current = {} +Actions = {} +Actions._index = Actions -local function GetPedVehicleSeat(ped, vehicle) +Actions.inVehicle = false +Actions.enteringVehicle = false +Actions.inPauseMenu = false + +function Actions:GetSeatPedIsIn() for i = -1, 16 do - if GetPedInVehicleSeat(vehicle, i) == ped then + if GetPedInVehicleSeat(self.vehicle, i) == ESX.PlayerData.ped then return i end end return -1 end -local function GetData(vehicle) - if not DoesEntityExist(vehicle) then +function Actions:GetVehicleData() + if not DoesEntityExist(self.vehicle) then return end - local model = GetEntityModel(vehicle) - local displayName = GetDisplayNameFromVehicleModel(model) - local netId = vehicle - if NetworkGetEntityIsNetworked(vehicle) then - netId = VehToNet(vehicle) + + local vehicleModel = GetEntityModel(self.vehicle) + local displayName = GetDisplayNameFromVehicleModel(vehicleModel) + local netId = NetworkGetEntityIsNetworked(self.vehicle) and VehToNet(self.vehicle) or self.vehicle + local plate = GetVehicleNumberPlateText(self.vehicle) + + return displayName, netId, plate +end + +function Actions:SetVehicleStatus() + ESX.SetPlayerData("vehicle", self.vehicle) + ESX.SetPlayerData("seat", self.seat) +end + +function Actions:TrackPedCoords() + local playerPed = ESX.PlayerData.ped + local coords = GetEntityCoords(playerPed) + + ESX.SetPlayerData("coords", coords) +end + +function Actions:TrackPed() + local playerPed = ESX.PlayerData.ped + local newPed = PlayerPedId() + + if playerPed ~= newPed then + ESX.PlayerData.ped = newPed + ESX.SetPlayerData("ped", playerPed) + + TriggerEvent("esx:playerPedChanged", playerPed) + TriggerServerEvent("esx:playerPedChanged", PedToNet(playerPed)) end - return displayName, netId end -local function ToggleVehicleStatus(inVehicle, seat) - ESX.SetPlayerData("vehicle", inVehicle) - ESX.SetPlayerData("seat", seat) -end +function Actions:TrackPauseMenu() + local isActive = IsPauseMenuActive() -CreateThread(function() - while not ESX.PlayerLoaded do Wait(200) end - while true do - ESX.SetPlayerData("coords", GetEntityCoords(playerPed)) - if playerPed ~= PlayerPedId() then - playerPed = PlayerPedId() - ESX.SetPlayerData("ped", playerPed) - TriggerEvent("esx:playerPedChanged", playerPed) - TriggerServerEvent("esx:playerPedChanged", PedToNet(playerPed)) - if Config.DisableHealthRegeneration then - SetPlayerHealthRechargeMultiplier(PlayerId(), 0.0) - end - end - - if IsPedJumping(playerPed) and not isJumping then - isJumping = true - TriggerEvent("esx:playerJumping") - TriggerServerEvent("esx:playerJumping") - elseif not IsPedJumping(playerPed) and isJumping then - isJumping = false - end - - if IsPauseMenuActive() and not inPauseMenu then - inPauseMenu = true - TriggerEvent("esx:pauseMenuActive", inPauseMenu) - elseif not IsPauseMenuActive() and inPauseMenu then - inPauseMenu = false - TriggerEvent("esx:pauseMenuActive", inPauseMenu) - end - - if not isInVehicle and not IsPlayerDead(PlayerId()) then - if DoesEntityExist(GetVehiclePedIsTryingToEnter(playerPed)) and not isEnteringVehicle then - -- trying to enter a vehicle! - local vehicle = GetVehiclePedIsTryingToEnter(playerPed) - local plate = GetVehicleNumberPlateText(vehicle) - local seat = GetSeatPedIsTryingToEnter(playerPed) - local _, netId = GetData(vehicle) - isEnteringVehicle = true - TriggerEvent("esx:enteringVehicle", vehicle, plate, seat, netId) - TriggerServerEvent("esx:enteringVehicle", plate, seat, netId) - ToggleVehicleStatus(vehicle, seat) - elseif not DoesEntityExist(GetVehiclePedIsTryingToEnter(playerPed)) and not IsPedInAnyVehicle(playerPed, true) and isEnteringVehicle then - -- vehicle entering aborted - TriggerEvent("esx:enteringVehicleAborted") - TriggerServerEvent("esx:enteringVehicleAborted") - isEnteringVehicle = false - ToggleVehicleStatus(false, false) - elseif IsPedInAnyVehicle(playerPed, false) then - -- suddenly appeared in a vehicle, possible teleport - isEnteringVehicle = false - isInVehicle = true - current.vehicle = GetVehiclePedIsUsing(playerPed) - current.seat = GetPedVehicleSeat(playerPed, current.vehicle) - current.plate = GetVehicleNumberPlateText(current.vehicle) - current.displayName, current.netId = GetData(current.vehicle) - TriggerEvent("esx:enteredVehicle", current.vehicle, current.plate, current.seat, current.displayName, current.netId) - TriggerServerEvent("esx:enteredVehicle", current.plate, current.seat, current.displayName, current.netId) - ToggleVehicleStatus(current.vehicle, current.seat) - end - elseif isInVehicle then - if (current.vehicle ~= GetVehiclePedIsUsing(playerPed)) or IsPlayerDead(PlayerId()) then - -- bye, vehicle - TriggerEvent("esx:exitedVehicle", current.vehicle, current.plate, current.seat, current.displayName, current.netId) - TriggerServerEvent("esx:exitedVehicle", current.plate, current.seat, current.displayName, current.netId) - isInVehicle = false - current = {} - ToggleVehicleStatus(false,false) - end - end - Wait(200) + if isActive ~= self.inPauseMenu then + self.inPauseMenu = isActive + TriggerEvent("esx:pauseMenuActive", isActive) end -end) +end -if Config.EnableDebug then - AddEventHandler("esx:playerPedChanged", function(netId) - print("esx:playerPedChanged", netId) - end) +function Actions:EnterVehicle() + self.seat = GetSeatPedIsTryingToEnter(ESX.PlayerData.ped) - AddEventHandler("esx:playerJumping", function() - print("esx:playerJumping") - end) + local _, netId, plate = self:GetVehicleData() - AddEventHandler("esx:enteringVehicle", function(vehicle, plate, seat, netId) - print("esx:enteringVehicle", "vehicle", vehicle, "plate", plate, "seat", seat, "netId", netId) - end) + self.isEnteringVehicle = true + TriggerEvent("esx:enteringVehicle", self.vehicle, plate, self.seat, netId) + TriggerServerEvent("esx:enteringVehicle", plate, self.seat, netId) - AddEventHandler("esx:enteringVehicleAborted", function() - print("esx:enteringVehicleAborted") - end) + self:SetVehicleStatus() +end - AddEventHandler("esx:enteredVehicle", function(vehicle, plate, seat, displayName, netId) - print("esx:enteredVehicle", "vehicle", vehicle, "plate", plate, "seat", seat, "displayName", displayName, "netId", netId) - end) +function Actions:ResetVehicleData() + self.enteringVehicle = false + self.vehicle = false + self.seat = false + self.inVehicle = false - AddEventHandler("esx:exitedVehicle", function(vehicle, plate, seat, displayName, netId) - print("esx:exitedVehicle", "vehicle", vehicle, "plate", plate, "seat", seat, "displayName", displayName, "netId", netId) + self:SetVehicleStatus() +end + +function Actions:EnterAborted() + self:ResetVehicleData() + + TriggerEvent("esx:enteringVehicleAborted") + TriggerServerEvent("esx:enteringVehicleAborted") +end + +function Actions:WarpEnter() + self.enteringVehicle = false + self.inVehicle = true + + self.seat = self:GetSeatPedIsIn() + + local displayName, netId, plate = self:GetVehicleData() + + self:SetVehicleStatus() + TriggerEvent("esx:enteredVehicle", self.vehicle, plate, self.seat, displayName, netId) + TriggerServerEvent("esx:enteredVehicle", plate, self.seat, displayName, netId) +end + +function Actions:ExitVehicle() + local currentVehicle = GetVehiclePedIsIn(ESX.PlayerData.ped, false) + + if currentVehicle ~= self.vehicle or ESX.PlayerData.dead then + local displayName, netId, plate = self:GetVehicleData() + + TriggerEvent("esx:exitedVehicle", self.vehicle, plate, self.seat, displayName, netId) + TriggerServerEvent("esx:exitedVehicle", plate, self.seat, displayName, netId) + + self:ResetVehicleData() + end +end + +function Actions:TrackVehicle() + if not self.inVehicle and not ESX.PlayerData.dead then + local tempVehicle = GetVehiclePedIsTryingToEnter(ESX.PlayerData.ped) + + if DoesEntityExist(tempVehicle) and not self.enteringVehicle then + self.vehicle = tempVehicle + self:EnterVehicle() + elseif not DoesEntityExist(tempVehicle) and not IsPedInAnyVehicle(ESX.PlayerData.ped, true) and self.enteringVehicle then + self:EnterAborted() + elseif IsPedInAnyVehicle(ESX.PlayerData.ped, false) then + self.vehicle = GetVehiclePedIsIn(ESX.PlayerData.ped, false) + self:WarpEnter() + end + elseif self.inVehicle then + self:ExitVehicle() + end +end + +function Actions:SlowLoop() + CreateThread(function() + while ESX.PlayerLoaded do + self:TrackPed() + self:TrackPedCoords() + self:TrackPauseMenu() + self:TrackVehicle() + Wait(500) + end end) end + +function Actions:Init() + self:SlowLoop() +end + +Actions:Init() diff --git a/[core]/es_extended/client/modules/callback.lua b/[core]/es_extended/client/modules/callback.lua index 337b26b7..b418878a 100644 --- a/[core]/es_extended/client/modules/callback.lua +++ b/[core]/es_extended/client/modules/callback.lua @@ -1,40 +1,82 @@ -local RequestId = 0 -local serverRequests = {} +---@diagnostic disable: duplicate-set-field -local clientCallbacks = {} +Callbacks = {} + +Callbacks.requests = {} +Callbacks.storage = {} +Callbacks.id = 0 + +function Callbacks:Trigger(event, cb, invoker, ...) + self.requests[self.id] = cb + TriggerServerEvent("esx:triggerServerCallback", event, self.id, invoker, ...) + + self.id += 1 +end + +function Callbacks:Execute(cb, ...) + local success, errorString = pcall(cb, ...) + + if not success then + print(("[^1ERROR^7] Failed to execute Callback with RequestId: ^5%s^7"):format(self.currentId)) + error(errorString) + return + end + self.currentId = nil +end + +function Callbacks:ServerRecieve(requestId, invoker, ...) + self.currentId = requestId + if not self.requests[self.currentId] then + return error(("Server Callback with requestId ^5%s^1 Was Called by ^5%s^1 but does not exist."):format(self.currentId, invoker)) + end + + local callback = self.requests[self.currentId] + + Callbacks:Execute(callback, ...) + self.requests[requestId] = nil +end + +function Callbacks:Register(name, cb) + self.storage[name] = cb +end + +function Callbacks:ClientRecieve(eventName, requestId, invoker, ...) + self.currentId = requestId + + if not self.storage[eventName] then + return error(("Client Callback with requestId ^5%s^1 Was Called by ^5%s^1 but does not exist."):format(eventName, invoker)) + end + + local returnCb = function(...) + TriggerServerEvent("esx:clientCallback", requestId, invoker, ...) + end + local callback = self.storage[eventName] + + Callbacks:Execute(callback, returnCb, ...) +end ---@param eventName string ---@param callback function ---@param ... any +---@return nil ESX.TriggerServerCallback = function(eventName, callback, ...) - serverRequests[RequestId] = callback + local invokingResource = GetInvokingResource() + local invoker = (invokingResource and invokingResource ~= "unknown") and invokingResource or "es_extended" - TriggerServerEvent("esx:triggerServerCallback", eventName, RequestId, GetInvokingResource() or "unknown", ...) - - RequestId = RequestId + 1 + Callbacks:Trigger(eventName, callback, invoker, ...) end -RegisterNetEvent("esx:serverCallback", function(requestId, invoker, ...) - if not serverRequests[requestId] then - return error(("Server Callback with requestId ^5%s^7 Was Called by ^5%s^7 but does not exist."):format(requestId, invoker)) - end - - serverRequests[requestId](...) - serverRequests[requestId] = nil +RegisterNetEvent("esx:serverCallback", function(...) + Callbacks:ServerRecieve(...) end) ---@param eventName string ---@param callback function +---@return nil ESX.RegisterClientCallback = function(eventName, callback) - clientCallbacks[eventName] = callback + Callbacks:Register(eventName, callback) end -RegisterNetEvent("esx:triggerClientCallback", function(eventName, requestId, invoker, ...) - if not clientCallbacks[eventName] then - return error(("Client Callback not registered, name: ^5%s^7, invoker resource: ^5%s^7"):format(eventName, invoker)) - end - - clientCallbacks[eventName](function(...) - TriggerServerEvent("esx:clientCallback", requestId, invoker, ...) - end, ...) +RegisterNetEvent("esx:triggerClientCallback", function(...) + Callbacks:ClientRecieve(...) end) diff --git a/[core]/es_extended/client/modules/death.lua b/[core]/es_extended/client/modules/death.lua index ed3347f0..d82d1a66 100644 --- a/[core]/es_extended/client/modules/death.lua +++ b/[core]/es_extended/client/modules/death.lua @@ -1,27 +1,16 @@ -AddEventHandler("gameEventTriggered", function(event, data) - if event ~= "CEventNetworkEntityDamage" then - return - end - local victim, victimDied = data[1], data[4] - if not IsPedAPlayer(victim) then - return - end - local player = PlayerId() - local playerPed = PlayerPedId() - if victimDied and NetworkGetPlayerIndexFromPed(victim) == player and (IsPedDeadOrDying(victim, true) or IsPedFatallyInjured(victim)) then - local killerEntity, deathCause = GetPedSourceOfDeath(playerPed), GetPedCauseOfDeath(playerPed) - local killerClientId = NetworkGetPlayerIndexFromPed(killerEntity) - if killerEntity ~= playerPed and killerClientId and NetworkIsPlayerActive(killerClientId) then - PlayerKilledByPlayer(GetPlayerServerId(killerClientId), killerClientId, deathCause) - else - PlayerKilled(deathCause) - end - end -end) +Death = {} +Death._index = Death -function PlayerKilledByPlayer(killerServerId, killerClientId, deathCause) - local victimCoords = GetEntityCoords(PlayerPedId()) - local killerCoords = GetEntityCoords(GetPlayerPed(killerClientId)) +function Death:ResetValues() + self.killerEntity = nil + self.deathCause = nil + self.killerId = nil + self.killerServerId = nil +end + +function Death:ByPlayer() + local victimCoords = GetEntityCoords(ESX.PlayerData.ped) + local killerCoords = GetEntityCoords(self.killerEntity) local distance = #(victimCoords - killerCoords) local data = { @@ -29,28 +18,65 @@ function PlayerKilledByPlayer(killerServerId, killerClientId, deathCause) killerCoords = { x = ESX.Math.Round(killerCoords.x, 1), y = ESX.Math.Round(killerCoords.y, 1), z = ESX.Math.Round(killerCoords.z, 1) }, killedByPlayer = true, - deathCause = deathCause, + deathCause = self.deathCause, distance = ESX.Math.Round(distance, 1), - killerServerId = killerServerId, - killerClientId = killerClientId, + killerServerId = self.killerServerId, + killerClientId = self.killerId, } + self:ResetValues() TriggerEvent("esx:onPlayerDeath", data) TriggerServerEvent("esx:onPlayerDeath", data) end -function PlayerKilled(deathCause) - local playerPed = PlayerPedId() - local victimCoords = GetEntityCoords(playerPed) +function Death:Natual() + local coords = GetEntityCoords(ESX.PlayerData.ped) local data = { - victimCoords = { x = ESX.Math.Round(victimCoords.x, 1), y = ESX.Math.Round(victimCoords.y, 1), z = ESX.Math.Round(victimCoords.z, 1) }, + victimCoords = { x = ESX.Math.Round(coords.x, 1), y = ESX.Math.Round(coords.y, 1), z = ESX.Math.Round(coords.z, 1) }, killedByPlayer = false, - deathCause = deathCause, + deathCause = self.deathCause, } + self:ResetValues() TriggerEvent("esx:onPlayerDeath", data) TriggerServerEvent("esx:onPlayerDeath", data) end + +function Death:Damaged(victim, victimDied) + if not IsEntityAPed(victim) then + return + end + + if IsPedAPlayer(victim) then + return + end + + local victimId = NetworkGetPlayerIndexFromPed(victim) + local isDead = IsPedDeadOrDying(victim, true) or IsPedFatallyInjured(victim) + if not victimDied or victimId ~= ESX.playerId or not isDead then + return + end + + self.killerEntity = GetPedSourceOfDeath(ESX.PlayerData.ped) + self.deathCause = GetPedCauseOfDeath(ESX.PlayerData.ped) + self.killerId = NetworkGetPlayerIndexFromPed(self.killerEntity) + self.killerServerId = GetPlayerServerId(self.killerId) + + local isActive = NetworkIsPlayerActive(self.killerId) + + if self.killerEntity ~= ESX.PlayerData.ped and self.killerId and isActive then + self:ByPlayer() + else + self:Natual() + end +end + +AddEventHandler("gameEventTriggered", function(event, data) + if event ~= "CEventNetworkEntityDamage" then + return + end + Death:Damaged(data[1], data[4]) +end) diff --git a/[core]/es_extended/client/modules/interactions.lua b/[core]/es_extended/client/modules/interactions.lua new file mode 100644 index 00000000..0a1a31fe --- /dev/null +++ b/[core]/es_extended/client/modules/interactions.lua @@ -0,0 +1,38 @@ +local interactions = {} +local pressedInteractions = {} + +function ESX.RemoveInteraction(name) + if not interactions[name] then return end + interactions[name] = nil +end + +ESX.RegisterInteraction = function(name, onPress, condition) + interactions[name] = { + condition = condition or function() return true end, + onPress = onPress, + creator = GetInvokingResource() or "es_extended" + } +end + +function ESX.GetInteractKey() + local hash = joaat('esx_interact') | 0x80000000 + return GetControlInstructionalButton(0, hash, true):sub(3) +end + +ESX.RegisterInput("esx_interact", "Interact", "keyboard", "e", function() + for _, interaction in pairs(interactions) do + local success, result = pcall(interaction.condition) + if success and result then + pressedInteractions[#pressedInteractions+1] = interaction + interaction.onPress() + end + end +end) + +AddEventHandler("onResourceStop", function(resource) + for name, interaction in pairs(interactions) do + if interaction.creator == resource then + interactions[name] = nil + end + end +end) \ No newline at end of file diff --git a/[core]/es_extended/client/modules/scaleform.lua b/[core]/es_extended/client/modules/scaleform.lua index 46e20e12..b8a5359e 100644 --- a/[core]/es_extended/client/modules/scaleform.lua +++ b/[core]/es_extended/client/modules/scaleform.lua @@ -10,7 +10,7 @@ function ESX.Scaleform.ShowFreemodeMessage(title, msg, sec) Wait(0) sec = sec - 0.01 - DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255) + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) end SetScaleformMovieAsNoLongerNeeded(scaleform) @@ -41,7 +41,7 @@ function ESX.Scaleform.ShowBreakingNews(title, msg, bottom, sec) Wait(0) sec = sec - 0.01 - DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255) + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) end SetScaleformMovieAsNoLongerNeeded(scaleform) @@ -64,7 +64,7 @@ function ESX.Scaleform.ShowPopupWarning(title, msg, bottom, sec) Wait(0) sec = sec - 0.01 - DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255) + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) end SetScaleformMovieAsNoLongerNeeded(scaleform) @@ -81,7 +81,7 @@ function ESX.Scaleform.ShowTrafficMovie(sec) Wait(0) sec = sec - 0.01 - DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255) + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) end SetScaleformMovieAsNoLongerNeeded(scaleform) diff --git a/[core]/es_extended/client/modules/streaming.lua b/[core]/es_extended/client/modules/streaming.lua index 46cf16bd..b583bbcb 100644 --- a/[core]/es_extended/client/modules/streaming.lua +++ b/[core]/es_extended/client/modules/streaming.lua @@ -1,37 +1,68 @@ +---@param modelHash number | string +---@param cb? function +---@return number | nil function ESX.Streaming.RequestModel(modelHash, cb) modelHash = type(modelHash) == "number" and modelHash or joaat(modelHash) + if not IsModelInCdimage(modelHash) then return end + RequestModel(modelHash) - while not HasModelLoaded(modelHash) do Wait() end + while not HasModelLoaded(modelHash) do Wait(500) end + return cb and cb(modelHash) or modelHash end +---@param textureDict string +---@param cb? function +---@return string | nil function ESX.Streaming.RequestStreamedTextureDict(textureDict, cb) - RequestStreamedTextureDict(textureDict) - while not HasStreamedTextureDictLoaded(textureDict) do Wait() end + RequestStreamedTextureDict(textureDict, false) + + while not HasStreamedTextureDictLoaded(textureDict) do Wait(500) end + return cb and cb(textureDict) or textureDict end +---@param assetName string +---@param cb? function +---@return string | nil function ESX.Streaming.RequestNamedPtfxAsset(assetName, cb) RequestNamedPtfxAsset(assetName) - while not HasNamedPtfxAssetLoaded(assetName) do Wait() end + + while not HasNamedPtfxAssetLoaded(assetName) do Wait(500) end + return cb and cb(assetName) or assetName end +---@param animSet string +---@param cb? function +---@return string | nil function ESX.Streaming.RequestAnimSet(animSet, cb) RequestAnimSet(animSet) - while not HasAnimSetLoaded(animSet) do Wait() end + + while not HasAnimSetLoaded(animSet) do Wait(500) end + return cb and cb(animSet) or animSet end +---@param animDict string +---@param cb? function +---@return string | nil function ESX.Streaming.RequestAnimDict(animDict, cb) RequestAnimDict(animDict) - while not HasAnimDictLoaded(animDict) do Wait() end + + while not HasAnimDictLoaded(animDict) do Wait(500) end + return cb and cb(animDict) or animDict end +---@param weaponHash number | string +---@param cb? function +---@return string | number | nil function ESX.Streaming.RequestWeaponAsset(weaponHash, cb) - RequestWeaponAsset(weaponHash) - while not HasWeaponAssetLoaded(weaponHash) do Wait() end + RequestWeaponAsset(weaponHash, 31, 0) + + while not HasWeaponAssetLoaded(weaponHash) do Wait(500) end + return cb and cb(weaponHash) or weaponHash end diff --git a/[core]/es_extended/common/functions.lua b/[core]/es_extended/common/functions.lua index 0e9adb08..fc342bce 100644 --- a/[core]/es_extended/common/functions.lua +++ b/[core]/es_extended/common/functions.lua @@ -20,16 +20,21 @@ CreateThread(function() end end) +---@param length number +---@return string function ESX.GetRandomString(length) math.randomseed(GetGameTimer()) return length > 0 and ESX.GetRandomString(length - 1) .. Charset[math.random(1, #Charset)] or "" end +---@return table function ESX.GetConfig() return Config end +---@param weaponName string +---@return number, table function ESX.GetWeapon(weaponName) weaponName = string.upper(weaponName) @@ -39,16 +44,22 @@ function ESX.GetWeapon(weaponName) return index, Config.Weapons[index] end +---@param weaponHash number +---@return table function ESX.GetWeaponFromHash(weaponHash) weaponHash = type(weaponHash) == "string" and joaat(weaponHash) or weaponHash return weaponsByHash[weaponHash] end +---@param byHash boolean +---@return table function ESX.GetWeaponList(byHash) return byHash and weaponsByHash or Config.Weapons end +---@param weaponName string +---@return string function ESX.GetWeaponLabel(weaponName) weaponName = string.upper(weaponName) @@ -58,6 +69,9 @@ function ESX.GetWeaponLabel(weaponName) return Config.Weapons[index].label or "" end +---@param weaponName string +---@param weaponComponent string +---@return table | nil function ESX.GetWeaponComponent(weaponName, weaponComponent) weaponName = string.upper(weaponName) @@ -71,6 +85,9 @@ function ESX.GetWeaponComponent(weaponName, weaponComponent) end end +---@param table table +---@param nb? number +---@return string function ESX.DumpTable(table, nb) if nb == nil then nb = 0 @@ -103,10 +120,16 @@ function ESX.DumpTable(table, nb) end end +---@param value any +---@param numDecimalPlaces? number +---@return number function ESX.Round(value, numDecimalPlaces) return ESX.Math.Round(value, numDecimalPlaces) end +---@param value string +---@param ... any +---@return boolean, string? function ESX.ValidateType(value, ...) local types = { ... } if #types == 0 then return true end @@ -132,6 +155,8 @@ function ESX.ValidateType(value, ...) return true end +---@param ... any +---@return boolean function ESX.AssertType(...) local matches, errorMessage = ESX.ValidateType(...) diff --git a/[core]/es_extended/common/main.lua b/[core]/es_extended/common/main.lua new file mode 100644 index 00000000..0f2c2e0f --- /dev/null +++ b/[core]/es_extended/common/main.lua @@ -0,0 +1,10 @@ +ESX = {} + +exports("getSharedObject", function() + return ESX +end) + +AddEventHandler("esx:getSharedObject", function() + local Invoke = GetInvokingResource() + error(("Resource ^5%s^1 Used the ^5getSharedObject^1 Event, this event ^1no longer exists!^1 Visit https://documentation.esx-framework.org/tutorials/tutorials-esx/sharedevent for how to fix!"):format(Invoke)) +end) diff --git a/[core]/es_extended/common/modules/math.lua b/[core]/es_extended/common/modules/math.lua index 868773b6..acd3fdc9 100644 --- a/[core]/es_extended/common/modules/math.lua +++ b/[core]/es_extended/common/modules/math.lua @@ -1,5 +1,8 @@ ESX.Math = {} +---@param value number +---@param numDecimalPlaces? number +---@return number function ESX.Math.Round(value, numDecimalPlaces) if numDecimalPlaces then local power = 10 ^ numDecimalPlaces @@ -10,20 +13,24 @@ function ESX.Math.Round(value, numDecimalPlaces) end -- credit http://richard.warburton.it +---@param value number +---@return string function ESX.Math.GroupDigits(value) local left, num, right = string.match(value, "^([^%d]*%d)(%d*)(.-)$") return left .. (num:reverse():gsub("(%d%d%d)", "%1" .. TranslateCap("locale_digit_grouping_symbol")):reverse()) .. right end +---@param value string | number +---@return string | nil function ESX.Math.Trim(value) - if value then - return (string.gsub(value, "^%s*(.-)%s*$", "%1")) - else - return nil - end + value = tostring(value) + return (string.gsub(value, "^%s*(.-)%s*$", "%1")) end +---@param minRange number +---@param maxRange number +---@return number function ESX.Math.Random(minRange, maxRange) math.randomseed(GetGameTimer()) return math.random(minRange or 1, maxRange or 10) diff --git a/[core]/es_extended/common/modules/table.lua b/[core]/es_extended/common/modules/table.lua index 860f68c7..fc345200 100644 --- a/[core]/es_extended/common/modules/table.lua +++ b/[core]/es_extended/common/modules/table.lua @@ -1,6 +1,8 @@ ESX.Table = {} -- nil proof alternative to #table +---@param t table +---@return number function ESX.Table.SizeOf(t) local count = 0 @@ -11,6 +13,8 @@ function ESX.Table.SizeOf(t) return count end +---@param t table +---@return table function ESX.Table.Set(t) local set = {} for _, v in ipairs(t) do @@ -19,6 +23,9 @@ function ESX.Table.Set(t) return set end +---@param t table +---@param value any +---@return number function ESX.Table.IndexOf(t, value) for i = 1, #t, 1 do if t[i] == value then @@ -29,6 +36,9 @@ function ESX.Table.IndexOf(t, value) return -1 end +---@param t table +---@param value any +---@return number function ESX.Table.LastIndexOf(t, value) for i = #t, 1, -1 do if t[i] == value then @@ -39,6 +49,9 @@ function ESX.Table.LastIndexOf(t, value) return -1 end +---@param t table +---@param cb function +---@return any function ESX.Table.Find(t, cb) for i = 1, #t, 1 do if cb(t[i]) then @@ -49,6 +62,9 @@ function ESX.Table.Find(t, cb) return nil end +---@param t table +---@param cb function +---@return number function ESX.Table.FindIndex(t, cb) for i = 1, #t, 1 do if cb(t[i]) then @@ -59,18 +75,24 @@ function ESX.Table.FindIndex(t, cb) return -1 end +---@param t table +---@param cb function +---@return table function ESX.Table.Filter(t, cb) local newTable = {} for i = 1, #t, 1 do if cb(t[i]) then - table.insert(newTable, t[i]) + newTable[#newTable + 1] = t[i] end end return newTable end +---@param t table +---@param cb function +---@return table function ESX.Table.Map(t, cb) local newTable = {} @@ -81,6 +103,8 @@ function ESX.Table.Map(t, cb) return newTable end +---@param t table +---@return table function ESX.Table.Reverse(t) local newTable = {} @@ -91,6 +115,8 @@ function ESX.Table.Reverse(t) return newTable end +---@param t table +---@return table function ESX.Table.Clone(t) if type(t) ~= "table" then return t @@ -112,6 +138,9 @@ function ESX.Table.Clone(t) return target end +---@param t1 table +---@param t2 table +---@return table function ESX.Table.Concat(t1, t2) local t3 = ESX.Table.Clone(t1) @@ -122,6 +151,9 @@ function ESX.Table.Concat(t1, t2) return t3 end +---@param t table +---@param sep string +---@return string function ESX.Table.Join(t, sep) local str = "" @@ -137,6 +169,9 @@ function ESX.Table.Join(t, sep) end -- Credits: https://github.com/JonasDev99/qb-garages/blob/b0335d67cb72a6b9ac60f62a87fb3946f5c2f33d/server/main.lua#L5 +---@param tab table +---@param val any +---@return boolean function ESX.Table.TableContains(tab, val) if type(val) == "table" then for _, value in pairs(tab) do @@ -157,6 +192,9 @@ end -- Credit: https://stackoverflow.com/a/15706820 -- Description: sort function for pairs +---@param t table +---@param order function +---@return function function ESX.Table.Sort(t, order) -- collect the keys local keys = {} diff --git a/[core]/es_extended/common/modules/timeout.lua b/[core]/es_extended/common/modules/timeout.lua index 381cbbcc..b0590d5a 100644 --- a/[core]/es_extended/common/modules/timeout.lua +++ b/[core]/es_extended/common/modules/timeout.lua @@ -1,6 +1,9 @@ local TimeoutCount = 0 local CancelledTimeouts = {} +---@param msec number +---@param cb function +---@return number ESX.SetTimeout = function(msec, cb) local id = TimeoutCount + 1 @@ -18,6 +21,8 @@ ESX.SetTimeout = function(msec, cb) return id end +---@param id number +---@return nil ESX.ClearTimeout = function(id) CancelledTimeouts[id] = true end diff --git a/[core]/es_extended/config.lua b/[core]/es_extended/config.lua index 274f121c..8731941a 100644 --- a/[core]/es_extended/config.lua +++ b/[core]/es_extended/config.lua @@ -59,8 +59,8 @@ Config.AdminLogging = false -- Logs the usage of certain commands by those with Config.DisableHealthRegeneration = false -- Player will no longer regenerate health Config.DisableVehicleRewards = false -- Disables Player Receiving weapons from vehicles Config.DisableNPCDrops = false -- stops NPCs from dropping weapons on death -Config.DisableDispatchServices = false -- Disable Dispatch services -Config.DisableScenarios = false -- Disable Scenarios +Config.DisableDispatchServices = true -- Disable Dispatch services +Config.DisableScenarios = true -- Disable Scenarios Config.DisableWeaponWheel = false -- Disables default weapon wheel Config.DisableAimAssist = false -- disables AIM assist (mainly on controllers) Config.DisableVehicleSeatShuff = false -- Disables vehicle seat shuff @@ -71,10 +71,10 @@ Config.RemoveHudComponents = { [3] = false, --CASH [4] = false, --MP_CASH [5] = false, --MP_MESSAGE - [6] = false, --VEHICLE_NAME - [7] = false, -- AREA_NAME - [8] = false, -- VEHICLE_CLASS - [9] = false, --STREET_NAME + [6] = true, --VEHICLE_NAME + [7] = true, -- AREA_NAME + [8] = true, -- VEHICLE_CLASS + [9] = true, --STREET_NAME [10] = false, --HELP_TEXT [11] = false, --FLOATING_HELP_TEXT_1 [12] = false, --FLOATING_HELP_TEXT_2 diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index 32a89b19..48afd7e3 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -11,6 +11,10 @@ shared_scripts { 'config.lua', 'config.weapons.lua', + + 'common/main.lua', + 'common/functions.lua', + 'common/modules/*.lua', } server_scripts { @@ -27,27 +31,23 @@ server_scripts { 'server/main.lua', 'server/commands.lua', - 'common/modules/*.lua', - 'common/functions.lua', 'server/modules/actions.lua', 'server/modules/npwd.lua', 'server/modules/createJob.lua' } client_scripts { - 'client/common.lua', + 'client/common.lua', 'client/functions.lua', 'client/wrapper.lua', 'client/modules/callback.lua', 'client/main.lua', - 'common/modules/*.lua', - 'common/functions.lua', - 'client/modules/actions.lua', 'client/modules/death.lua', 'client/modules/npwd.lua', + 'client/modules/interactions.lua', 'client/modules/scaleform.lua', 'client/modules/streaming.lua', } @@ -75,5 +75,4 @@ dependencies { '/native:0x6AE51D4B', '/native:0xA61C8FC6', 'oxmysql', - 'spawnmanager', } diff --git a/[core]/es_extended/locale.lua b/[core]/es_extended/locale.lua index cb857f3d..db77eba7 100644 --- a/[core]/es_extended/locale.lua +++ b/[core]/es_extended/locale.lua @@ -2,7 +2,7 @@ Locales = {} function Translate(str, ...) -- Translate string if not str then - error(("Resource ^5%s^7 You did not specify a parameter for the Translate function or the value is nil!"):format(GetInvokingResource() or GetCurrentResourceName())) + error(("Resource ^5%s^1 You did not specify a parameter for the Translate function or the value is nil!"):format(GetInvokingResource() or GetCurrentResourceName())) return "Given translate function parameter is nil!" end if Locales[Config.Locale] then diff --git a/[core]/es_extended/server/classes/player.lua b/[core]/es_extended/server/classes/player.lua index 4df069f4..581f0ef6 100644 --- a/[core]/es_extended/server/classes/player.lua +++ b/[core]/es_extended/server/classes/player.lua @@ -1,16 +1,21 @@ -local _GetPlayerPed = GetPlayerPed -local _GetEntityCoords = GetEntityCoords -local _GetEntityHeading = GetEntityHeading -local _ExecuteCommand = ExecuteCommand -local _SetEntityCoords = SetEntityCoords -local _SetEntityHeading = SetEntityHeading -local _TriggerClientEvent = TriggerClientEvent -local _DropPlayer = DropPlayer -local _TriggerEvent = TriggerEvent -local _GiveWeaponToPed = GiveWeaponToPed -local _SetPedAmmo = SetPedAmmo -local _RemoveWeaponFromPed = RemoveWeaponFromPed -local _assert = assert +---@class xPlayer +---@field accounts table +---@field coords table +---@field group string +---@field identifier string +---@field inventory table +---@field job table +---@field loadout table +---@field name string +---@field playerId number +---@field source number +---@field variables table +---@field weight number +---@field maxWeight number +---@field metadata table +---@field lastPlaytime number +---@field admin boolean +---@field license string ---@param playerId number ---@param identifier string @@ -26,6 +31,7 @@ local _assert = assert function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, weight, job, loadout, name, coords, metadata) local targetOverrides = Config.PlayerFunctionOverride and Core.PlayerFunctionOverrides[Config.PlayerFunctionOverride] or {} + ---@class xPlayer local self = {} self.accounts = accounts @@ -42,11 +48,16 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, self.weight = weight self.maxWeight = Config.MaxWeight self.metadata = metadata + self.lastPlaytime = self.metadata.lastPlaytime or 0 + self.paycheckEnabled = true self.admin = Core.IsPlayerAdmin(playerId) if Config.Multichar then - self.license = "license" .. identifier:sub(identifier:find(":"), identifier:len()) + local startIndex = identifier:find(":", 1) + if startIndex then + self.license = ("license%s"):format(identifier:sub(startIndex, identifier:len())) + end else - self.license = "license:" .. identifier + self.license = ("license:%s"):format(identifier) end if type(metadata.jobduty) ~= "boolean" then @@ -55,7 +66,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, self.job.duty = metadata.jobduty end - _ExecuteCommand(("add_principal identifier.%s group.%s"):format(self.license, self.group)) + ExecuteCommand(("add_principal identifier.%s group.%s"):format(self.license, self.group)) local stateBag = Player(self.source).state stateBag:set("identifier", self.identifier, false) @@ -66,33 +77,44 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param eventName string ---@param ... any - ---@return void + ---@return nil function self.triggerEvent(eventName, ...) - _assert(type(eventName) == "string", "eventName should be string!") - _TriggerClientEvent(eventName, self.source, ...) + assert(type(eventName) == "string", "eventName should be string!") + TriggerClientEvent(eventName, self.source, ...) + end + + ---@param toggle boolean + ---@return nil + function self.togglePaycheck(toggle) + self.paycheckEnabled = toggle + end + + ---@return boolean + function self.isPaycheckEnabled() + return self.paycheckEnabled end ---@param coordinates vector4 | vector3 | table - ---@return void + ---@return nil function self.setCoords(coordinates) - local ped = _GetPlayerPed(self.source) - local vector = type(coordinates) == "vector4" and coordinates or type(coordinates) == "vector3" and vector4(coordinates, 0.0) or vec(coordinates.x, coordinates.y, coordinates.z, coordinates.heading or 0.0) - _SetEntityCoords(ped, vector.xyz, false, false, false, false) - _SetEntityHeading(ped, vector.w) + local ped = GetPlayerPed(self.source) + + SetEntityCoords(ped, coordinates.x, coordinates.y, coordinates.z, false, false, false, false) + SetEntityHeading(ped, coordinates.w or coordinates.heading or 0.0) end ---@param vector boolean ---@param heading boolean ---@return vector3 | vector4 | table function self.getCoords(vector, heading) - local ped = _GetPlayerPed(self.source) - local entityCoords = _GetEntityCoords(ped) - local entityHeading = _GetEntityHeading(ped) + local ped = GetPlayerPed(self.source) + local entityCoords = GetEntityCoords(ped) + local entityHeading = GetEntityHeading(ped) local coordinates = { x = entityCoords.x, y = entityCoords.y, z = entityCoords.z } if vector then - coordinates = (heading and vector4(entityCoords.xyz, entityHeading) or entityCoords) + coordinates = (heading and vector4(entityCoords.x, entityCoords.y, entityCoords.z, entityHeading) or entityCoords) else if heading then coordinates.heading = entityHeading @@ -103,15 +125,21 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end ---@param reason string - ---@return void + ---@return nil function self.kick(reason) - _DropPlayer(self.source, reason) + local source = tostring(self.source) + DropPlayer(source, reason) + end + + ---@return number + function self.getPlayTime() + return self.lastPlaytime + GetPlayerTimeOnline(self.source) end ---@param money number - ---@return void + ---@return nil function self.setMoney(money) - _assert(type(money) == "number", "money should be number!") + assert(type(money) == "number", "money should be number!") money = ESX.Math.Round(money) self.setAccountMoney("money", money) end @@ -123,7 +151,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param money number ---@param reason string - ---@return void + ---@return nil function self.addMoney(money, reason) money = ESX.Math.Round(money) self.addAccountMoney("money", money, reason) @@ -131,7 +159,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param money number ---@param reason string - ---@return void + ---@return nil function self.removeMoney(money, reason) money = ESX.Math.Round(money) self.removeAccountMoney("money", money, reason) @@ -143,19 +171,19 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end ---@param newGroup string - ---@return void + ---@return nil function self.setGroup(newGroup) local lastGroup = self.group - _ExecuteCommand(("remove_principal identifier.%s group.%s"):format(self.license, self.group)) + ExecuteCommand(("remove_principal identifier.%s group.%s"):format(self.license, self.group)) self.group = newGroup - _TriggerEvent("esx:setGroup", self.source, self.group, lastGroup) + TriggerEvent("esx:setGroup", self.source, self.group, lastGroup) self.triggerEvent("esx:setGroup", self.group, lastGroup) Player(self.source).state:set("group", self.group, true) - _ExecuteCommand(("add_principal identifier.%s group.%s"):format(self.license, self.group)) + ExecuteCommand(("add_principal identifier.%s group.%s"):format(self.license, self.group)) end ---@return string @@ -165,7 +193,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param k string ---@param v any - ---@return void + ---@return nil function self.set(k, v) self.variables[k] = v end @@ -266,7 +294,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end ---@param newName string - ---@return void + ---@return nil function self.setName(newName) self.name = newName Player(self.source).state:set("name", self.name, true) @@ -274,12 +302,12 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param accountName string ---@param money number - ---@param reason string - ---@return void + ---@param reason string | nil + ---@return nil function self.setAccountMoney(accountName, money, reason) reason = reason or "unknown" if not tonumber(money) then - error(("Tried To Set Account ^5%s^0 For Player ^5%s^0 To An Invalid Number -> ^5%s^7"):format(accountName, self.playerId, money)) + error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) return end if money >= 0 then @@ -290,23 +318,23 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, self.accounts[account.index].money = money self.triggerEvent("esx:setAccountMoney", account) - _TriggerEvent("esx:setAccountMoney", self.source, accountName, money, reason) + TriggerEvent("esx:setAccountMoney", self.source, accountName, money, reason) else - error(("Tried To Set Invalid Account ^5%s^0 For Player ^5%s^0!"):format(accountName, self.playerId)) + error(("Tried To Set Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) end else - error(("Tried To Set Account ^5%s^0 For Player ^5%s^0 To An Invalid Number -> ^5%s^7"):format(accountName, self.playerId, money)) + error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) end end ---@param accountName string ---@param money number - ---@param reason string - ---@return void + ---@param reason string | nil + ---@return nil function self.addAccountMoney(accountName, money, reason) reason = reason or "Unknown" if not tonumber(money) then - error(("Tried To Set Account ^5%s^0 For Player ^5%s^0 To An Invalid Number -> ^5%s^7"):format(accountName, self.playerId, money)) + error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) return end if money > 0 then @@ -316,23 +344,23 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, self.accounts[account.index].money = self.accounts[account.index].money + money self.triggerEvent("esx:setAccountMoney", account) - _TriggerEvent("esx:addAccountMoney", self.source, accountName, money, reason) + TriggerEvent("esx:addAccountMoney", self.source, accountName, money, reason) else - error(("Tried To Set Add To Invalid Account ^5%s^0 For Player ^5%s^0!"):format(accountName, self.playerId)) + error(("Tried To Set Add To Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) end else - error(("Tried To Set Account ^5%s^0 For Player ^5%s^0 To An Invalid Number -> ^5%s^7"):format(accountName, self.playerId, money)) + error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) end end ---@param accountName string ---@param money number - ---@param reason string - ---@return void + ---@param reason string | nil + ---@return nil function self.removeAccountMoney(accountName, money, reason) reason = reason or "Unknown" if not tonumber(money) then - error(("Tried To Set Account ^5%s^0 For Player ^5%s^0 To An Invalid Number -> ^5%s^7"):format(accountName, self.playerId, money)) + error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) return end if money > 0 then @@ -341,18 +369,18 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, if account then money = account.round and ESX.Math.Round(money) or money if self.accounts[account.index].money - money > self.accounts[account.index].money then - error(("Tried To Underflow Account ^5%s^0 For Player ^5%s^0!"):format(accountName, self.playerId)) + error(("Tried To Underflow Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) return end self.accounts[account.index].money = self.accounts[account.index].money - money self.triggerEvent("esx:setAccountMoney", account) - _TriggerEvent("esx:removeAccountMoney", self.source, accountName, money, reason) + TriggerEvent("esx:removeAccountMoney", self.source, accountName, money, reason) else - error(("Tried To Set Add To Invalid Account ^5%s^0 For Player ^5%s^0!"):format(accountName, self.playerId)) + error(("Tried To Set Add To Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId)) end else - error(("Tried To Set Account ^5%s^0 For Player ^5%s^0 To An Invalid Number -> ^5%s^7"):format(accountName, self.playerId, money)) + error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money)) end end @@ -369,7 +397,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param itemName string ---@param count number - ---@return void + ---@return nil function self.addInventoryItem(itemName, count) local item = self.getInventoryItem(itemName) @@ -378,14 +406,14 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, item.count = item.count + count self.weight = self.weight + (item.weight * count) - _TriggerEvent("esx:onAddInventoryItem", self.source, item.name, item.count) + TriggerEvent("esx:onAddInventoryItem", self.source, item.name, item.count) self.triggerEvent("esx:addInventoryItem", item.name, item.count) end end ---@param itemName string ---@param count number - ---@return void + ---@return nil function self.removeInventoryItem(itemName, count) local item = self.getInventoryItem(itemName) @@ -398,7 +426,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, item.count = newCount self.weight = self.weight - (item.weight * count) - _TriggerEvent("esx:onRemoveInventoryItem", self.source, item.name, item.count) + TriggerEvent("esx:onRemoveInventoryItem", self.source, item.name, item.count) self.triggerEvent("esx:removeInventoryItem", item.name, item.count) end else @@ -409,7 +437,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param itemName string ---@param count number - ---@return void + ---@return nil function self.setInventoryItem(itemName, count) local item = self.getInventoryItem(itemName) @@ -445,6 +473,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, return newWeight <= self.maxWeight else print(('[^3WARNING^7] Item ^5"%s"^7 was used but does not exist!'):format(itemName)) + return false end end @@ -455,7 +484,13 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@return boolean function self.canSwapItem(firstItem, firstItemCount, testItem, testItemCount) local firstItemObject = self.getInventoryItem(firstItem) + if not firstItemObject then + return false + end local testItemObject = self.getInventoryItem(testItem) + if not testItemObject then + return false + end if firstItemObject.count >= firstItemCount then local weightWithoutFirstItem = ESX.Math.Round(self.weight - (firstItemObject.weight * firstItemCount)) @@ -468,7 +503,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end ---@param newWeight number - ---@return void + ---@return nil function self.setMaxWeight(newWeight) self.maxWeight = newWeight self.triggerEvent("esx:setMaxWeight", self.maxWeight) @@ -476,13 +511,13 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param newJob string ---@param grade string - ---@return void + ---@return nil function self.setJob(newJob, grade) grade = tostring(grade) local lastJob = self.job if not ESX.DoesJobExist(newJob, grade) then - return print(("[es_extended] [^3WARNING^7] Ignoring invalid ^5.setJob()^7 usage for ID: ^5%s^7, Job: ^5%s^7"):format(self.source, newJob)) + return print(("[ESX] [^3WARNING^7] Ignoring invalid ^5.setJob()^7 usage for ID: ^5%s^7, Job: ^5%s^7"):format(self.source, newJob)) end local jobObject, gradeObject = ESX.Jobs[newJob], ESX.Jobs[newJob].grades[grade] @@ -504,7 +539,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, self.metadata["jobduty"] = self.job.duty - _TriggerEvent("esx:setJob", self.source, self.job, lastJob) + TriggerEvent("esx:setJob", self.source, self.job, lastJob) self.triggerEvent("esx:setJob", self.job, lastJob) Player(self.source).state:set("job", self.job, true) end @@ -531,7 +566,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param weaponName string ---@param ammo number - ---@return void + ---@return nil function self.addWeapon(weaponName, ammo) if not self.hasWeapon(weaponName) then local weaponLabel = ESX.GetWeaponLabel(weaponName) @@ -544,14 +579,14 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, tintIndex = 0, }) - _GiveWeaponToPed(_GetPlayerPed(self.source), joaat(weaponName), ammo, false, false) + GiveWeaponToPed(GetPlayerPed(self.source), joaat(weaponName), ammo, false, false) self.triggerEvent("esx:addInventoryItem", weaponLabel, false, true) end end ---@param weaponName string ---@param weaponComponent string - ---@return void + ---@return nil function self.addWeaponComponent(weaponName, weaponComponent) local loadoutNum , weapon = self.getWeapon(weaponName) @@ -571,19 +606,19 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param weaponName string ---@param ammoCount number - ---@return void + ---@return nil function self.addWeaponAmmo(weaponName, ammoCount) local _, weapon = self.getWeapon(weaponName) if weapon then weapon.ammo = weapon.ammo + ammoCount - _SetPedAmmo(GetPlayerPed(self.source), joaat(weaponName), weapon.ammo) + SetPedAmmo(GetPlayerPed(self.source), joaat(weaponName), weapon.ammo) end end ---@param weaponName string ---@param ammoCount number - ---@return void + ---@return nil function self.updateWeaponAmmo(weaponName, ammoCount) local _, weapon = self.getWeapon(weaponName) @@ -594,7 +629,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param weaponName string ---@param weaponTintIndex number - ---@return void + ---@return nil function self.setWeaponTint(weaponName, weaponTintIndex) local loadoutNum , weapon = self.getWeapon(weaponName) @@ -622,12 +657,12 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end ---@param weaponName string - ---@return void + ---@return nil function self.removeWeapon(weaponName) - local weaponLabel, playerPed = nil, _GetPlayerPed(self.source) + local weaponLabel, playerPed = nil, GetPlayerPed(self.source) if not playerPed then - return error("xPlayer.removeWeapon ^5invalid^7 player ped!") + return error("xPlayer.removeWeapon ^5invalid^1 player ped!") end for k, v in ipairs(self.loadout) do @@ -639,8 +674,8 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end local weaponHash = joaat(v.name) - _RemoveWeaponFromPed(playerPed, weaponHash) - _SetPedAmmo(playerPed, weaponHash, 0) + RemoveWeaponFromPed(playerPed, weaponHash) + SetPedAmmo(playerPed, weaponHash, 0) table.remove(self.loadout, k) break @@ -654,12 +689,11 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param weaponName string ---@param weaponComponent string - ---@return void + ---@return nil function self.removeWeaponComponent(weaponName, weaponComponent) local loadoutNum , weapon = self.getWeapon(weaponName) if weapon then - ---@type table local component = ESX.GetWeaponComponent(weaponName, weaponComponent) if component then @@ -680,13 +714,13 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param weaponName string ---@param ammoCount number - ---@return void + ---@return nil function self.removeWeaponAmmo(weaponName, ammoCount) local _, weapon = self.getWeapon(weaponName) if weapon then weapon.ammo = weapon.ammo - ammoCount - _SetPedAmmo(GetPlayerPed(self.source), joaat(weaponName), weapon.ammo) + SetPedAmmo(GetPlayerPed(self.source), joaat(weaponName), weapon.ammo) end end @@ -722,7 +756,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end ---@param item string - ---@return table, number | false + ---@return table | false, number | nil function self.hasItem(item) for _, v in ipairs(self.inventory) do if v.name == item and v.count >= 1 then @@ -734,7 +768,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end ---@param weaponName string - ---@return number, table | nil + ---@return number | nil, table | nil function self.getWeapon(weaponName) for k, v in ipairs(self.loadout) do if v.name == weaponName then @@ -742,13 +776,13 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, end end - return nil + return nil, nil end ---@param msg string - ---@param type string + ---@param notifyType string ---@param length number - ---@return void + ---@return nil function self.showNotification(msg, notifyType, length) self.triggerEvent("esx:showNotification", msg, notifyType, length) end @@ -761,7 +795,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param flash boolean ---@param saveToBrief boolean ---@param hudColorIndex number - ---@return void + ---@return nil function self.showAdvancedNotification(sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex) self.triggerEvent("esx:showAdvancedNotification", sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex) end @@ -770,26 +804,27 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param thisFrame boolean ---@param beep boolean ---@param duration number - ---@return void + ---@return nil function self.showHelpNotification(msg, thisFrame, beep, duration) self.triggerEvent("esx:showHelpNotification", msg, thisFrame, beep, duration) end ---@param index any ---@param subIndex any - ---@return table + ---@return table | nil function self.getMeta(index, subIndex) if not index then return self.metadata end if type(index) ~= "string" then - return error("xPlayer.getMeta ^5index^7 should be ^5string^7!") + error("xPlayer.getMeta ^5index^1 should be ^5string^1!") + return end local metaData = self.metadata[index] if metaData == nil then - return Config.EnableDebug and error(("xPlayer.getMeta ^5%s^7 not exist!"):format(index)) or nil + return Config.EnableDebug and error(("xPlayer.getMeta ^5%s^1 not exist!"):format(index)) or nil end if subIndex and type(metaData) == "table" then @@ -808,14 +843,15 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, if type(key) == "string" then returnValues[key] = self.getMeta(index, key) else - error(("xPlayer.getMeta subIndex should be ^5string^7 or ^5table^7! that contains ^5string^7, received ^5%s^7!, skipping..."):format(type(key))) + error(("xPlayer.getMeta subIndex should be ^5string^1 or ^5table^1! that contains ^5string^1, received ^5%s^1!, skipping..."):format(type(key))) end end return returnValues end - return error(("xPlayer.getMeta subIndex should be ^5string^7 or ^5table^7!, received ^5%s^7!"):format(_type)) + error(("xPlayer.getMeta subIndex should be ^5string^1 or ^5table^1!, received ^5%s^1!"):format(_type)) + return end return metaData @@ -824,14 +860,14 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, ---@param index any ---@param value any ---@param subValue any - ---@return void + ---@return nil function self.setMeta(index, value, subValue) if not index then - return error("xPlayer.setMeta ^5index^7 is Missing!") + return error("xPlayer.setMeta ^5index^1 is Missing!") end if type(index) ~= "string" then - return error("xPlayer.setMeta ^5index^7 should be ^5string^7!") + return error("xPlayer.setMeta ^5index^1 should be ^5string^1!") end if value == nil then @@ -842,13 +878,13 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, if not subValue then if _type ~= "number" and _type ~= "string" and _type ~= "table" then - return error(("xPlayer.setMeta ^5%s^7 should be ^5number^7 or ^5string^7 or ^5table^7!"):format(value)) + return error(("xPlayer.setMeta ^5%s^1 should be ^5number^1 or ^5string^1 or ^5table^1!"):format(value)) end self.metadata[index] = value else if _type ~= "string" then - return error(("xPlayer.setMeta ^5value^7 should be ^5string^7 as a subIndex!"):format(value)) + return error(("xPlayer.setMeta ^5value^1 should be ^5string^1 as a subIndex!"):format(value)) end if not self.metadata[index] or type(self.metadata[index]) ~= "table" then @@ -863,16 +899,16 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, function self.clearMeta(index, subValues) if not index then - return error("xPlayer.clearMeta ^5index^7 is Missing!") + return error("xPlayer.clearMeta ^5index^1 is Missing!") end if type(index) ~= "string" then - return error("xPlayer.clearMeta ^5index^7 should be ^5string^7!") + return error("xPlayer.clearMeta ^5index^1 should be ^5string^1!") end local metaData = self.metadata[index] if metaData == nil then - return Config.EnableDebug and error(("xPlayer.clearMeta ^5%s^7 does not exist!"):format(index)) or nil + return Config.EnableDebug and error(("xPlayer.clearMeta ^5%s^1 does not exist!"):format(index)) or nil end if not subValues then @@ -883,7 +919,7 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, if type(metaData) == "table" then metaData[subValues] = nil else - return error(("xPlayer.clearMeta ^5%s^7 is not a table! Cannot clear subValue ^5%s^7."):format(index, subValues)) + return error(("xPlayer.clearMeta ^5%s^1 is not a table! Cannot clear subValue ^5%s^1."):format(index, subValues)) end elseif type(subValues) == "table" then -- If subValues is a table, we will clear multiple subValues within the table @@ -893,14 +929,14 @@ function CreateExtendedPlayer(playerId, identifier, group, accounts, inventory, if type(metaData) == "table" then metaData[subValue] = nil else - error(("xPlayer.clearMeta ^5%s^7 is not a table! Cannot clear subValue ^5%s^7."):format(index, subValue)) + error(("xPlayer.clearMeta ^5%s^1 is not a table! Cannot clear subValue ^5%s^1."):format(index, subValue)) end else - error(("xPlayer.clearMeta subValues should contain ^5string^7, received ^5%s^7, skipping..."):format(type(subValue))) + error(("xPlayer.clearMeta subValues should contain ^5string^1, received ^5%s^1, skipping..."):format(type(subValue))) end end else - return error(("xPlayer.clearMeta ^5subValues^7 should be ^5string^7 or ^5table^7, received ^5%s^7!"):format(type(subValues))) + return error(("xPlayer.clearMeta ^5subValues^1 should be ^5string^1 or ^5table^1, received ^5%s^1!"):format(type(subValues))) end self.triggerEvent('esx:updatePlayerData', 'metadata', self.metadata) end diff --git a/[core]/es_extended/server/commands.lua b/[core]/es_extended/server/commands.lua index 36dd3744..e951e111 100644 --- a/[core]/es_extended/server/commands.lua +++ b/[core]/es_extended/server/commands.lua @@ -77,7 +77,7 @@ ESX.RegisterCommand( local playerPed = GetPlayerPed(xPlayer.source) local playerCoords = GetEntityCoords(playerPed) local playerHeading = GetEntityHeading(playerPed) - local playerVehicle = GetVehiclePedIsIn(playerPed) + local playerVehicle = GetVehiclePedIsIn(playerPed, false) if not args.car or type(args.car) ~= "string" then args.car = "adder" @@ -126,11 +126,15 @@ ESX.RegisterCommand( { "cardel", "dv" }, "admin", function(xPlayer, args) - local PedVehicle = GetVehiclePedIsIn(GetPlayerPed(xPlayer.source), false) - if DoesEntityExist(PedVehicle) then - DeleteEntity(PedVehicle) + local ped = GetPlayerPed(xPlayer.source) + local pedVehicle = GetVehiclePedIsIn(ped, false) + + if DoesEntityExist(pedVehicle) then + DeleteEntity(pedVehicle) end - local Vehicles = ESX.OneSync.GetVehiclesInArea(GetEntityCoords(GetPlayerPed(xPlayer.source)), tonumber(args.radius) or 5.0) + + local coords = GetEntityCoords(ped) + local Vehicles = ESX.OneSync.GetVehiclesInArea(coords, tonumber(args.radius) or 5.0) for i = 1, #Vehicles do local Vehicle = NetworkGetEntityFromNetworkId(Vehicles[i]) if DoesEntityExist(Vehicle) then @@ -549,6 +553,14 @@ ESX.RegisterCommand("info", { "user", "admin" }, function(xPlayer) print(("^2ID: ^5%s^0 | ^2Name: ^5%s^0 | ^2Group: ^5%s^0 | ^2Job: ^5%s^0"):format(xPlayer.source, xPlayer.getName(), xPlayer.getGroup(), job)) end, false) +ESX.RegisterCommand("playtime", { "user", "admin" }, function(xPlayer) + local playtime = xPlayer.getPlayTime() + local days = math.floor(playtime / 86400) + local hours = math.floor((playtime % 86400) / 3600) + local minutes = math.floor((playtime % 3600) / 60) + print(("Playtime: ^5%s^0 Days | ^5%s^0 Hours | ^5%s^0 Minutes"):format(days, hours, minutes)) +end, false) + ESX.RegisterCommand("coords", "admin", function(xPlayer) local ped = GetPlayerPed(xPlayer.source) local coords = GetEntityCoords(ped, false) diff --git a/[core]/es_extended/server/common.lua b/[core]/es_extended/server/common.lua index 58326b42..7d460947 100644 --- a/[core]/es_extended/server/common.lua +++ b/[core]/es_extended/server/common.lua @@ -1,4 +1,3 @@ -ESX = {} ESX.Players = {} ESX.Jobs = {} ESX.JobsPlayerCount = {} @@ -18,19 +17,10 @@ RegisterNetEvent("esx:onPlayerSpawn", function() ESX.Players[source].spawned = true end) -AddEventHandler("esx:getSharedObject", function() - local Invoke = GetInvokingResource() - error(("Resource ^5%s^7 Used the ^5getSharedObject^7 Event, this event ^1no longer exists!^7 Visit https://documentation.esx-framework.org/tutorials/tutorials-esx/sharedevent for how to fix!"):format(Invoke)) -end) - -exports("getSharedObject", function() - return ESX -end) - if Config.OxInventory then Config.PlayerFunctionOverride = "OxInventory" SetConvarReplicated("inventory:framework", "esx") - SetConvarReplicated("inventory:weight", Config.MaxWeight * 1000) + SetConvarReplicated("inventory:weight", tostring(Config.MaxWeight * 1000)) end local function StartDBSync() diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index 4ed4b39a..79de4a91 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -1,3 +1,5 @@ +---@param msg string +---@return nil function ESX.Trace(msg) if Config.EnableDebug then print(("[^2TRACE^7] %s^7"):format(msg)) @@ -22,12 +24,16 @@ function ESX.TriggerClientEvent(eventName, playerIds, ...) end end +---@param name string | table +---@param group string | table +---@param cb function +---@param allowConsole? boolean +---@param suggestion? table function ESX.RegisterCommand(name, group, cb, allowConsole, suggestion) if type(name) == "table" then for _, v in ipairs(name) do ESX.RegisterCommand(v, group, cb, allowConsole, suggestion) end - return end @@ -186,8 +192,12 @@ local function updateHealthAndArmorInMetadata(xPlayer) local ped = GetPlayerPed(xPlayer.source) xPlayer.setMeta("health", GetEntityHealth(ped)) xPlayer.setMeta("armor", GetPedArmour(ped)) + xPlayer.setMeta("lastPlaytime", xPlayer.getPlayTime()) end +---@param xPlayer table +---@param cb? function +---@return nil function Core.SavePlayer(xPlayer, cb) if not xPlayer.spawned then return cb and cb() @@ -221,6 +231,8 @@ function Core.SavePlayer(xPlayer, cb) ) end +---@param cb? function +---@return nil function Core.SavePlayers(cb) local xPlayers = ESX.Players if not next(xPlayers) then @@ -277,6 +289,9 @@ local function checkTable(key, val, player, xPlayers) end end +---@param key? string +---@param val? string|table +---@return table function ESX.GetExtendedPlayers(key, val) local xPlayers = {} if type(val) == "table" then @@ -298,6 +313,9 @@ function ESX.GetExtendedPlayers(key, val) return xPlayers end +---@param key? string +---@param val? string|table +---@return number | table function ESX.GetNumPlayers(key, val) if not key then return #GetPlayers() @@ -326,28 +344,36 @@ function ESX.GetNumPlayers(key, val) return #ESX.GetExtendedPlayers(key, val) end +---@param source number +---@return table function ESX.GetPlayerFromId(source) return ESX.Players[tonumber(source)] end +---@param identifier string +---@return table function ESX.GetPlayerFromIdentifier(identifier) return Core.playersByIdentifier[identifier] end +---@param playerId number | string +---@return string function ESX.GetIdentifier(playerId) local fxDk = GetConvarInt("sv_fxdkMode", 0) if fxDk == 1 then return "ESX-DEBUG-LICENCE" end + playerId = tostring(playerId) + local identifier = GetPlayerIdentifierByType(playerId, "license") return identifier and identifier:gsub("license:", "") end ---@param model string|number ----@param player number playerId +---@param player number ---@param cb function - +---@diagnostic disable-next-line: duplicate-set-field function ESX.GetVehicleType(model, player, cb) model = type(model) == "string" and joaat(model) or model @@ -361,6 +387,11 @@ function ESX.GetVehicleType(model, player, cb) end, model) end +---@param name string +---@param title string +---@param color string +---@param message string +---@return nil function ESX.DiscordLog(name, title, color, message) local webHook = Config.DiscordLogs.Webhooks[name] or Config.DiscordLogs.Webhooks.default local embedData = { @@ -380,7 +411,9 @@ function ESX.DiscordLog(name, title, color, message) } PerformHttpRequest( webHook, - nil, + function () + return + end, "POST", json.encode({ username = "Logs", @@ -392,6 +425,11 @@ function ESX.DiscordLog(name, title, color, message) ) end +---@param name string +---@param title string +---@param color string +---@param fields table +---@return nil function ESX.DiscordLogFields(name, title, color, fields) local webHook = Config.DiscordLogs.Webhooks[name] or Config.DiscordLogs.Webhooks.default local embedData = { @@ -412,7 +450,9 @@ function ESX.DiscordLogFields(name, title, color, fields) } PerformHttpRequest( webHook, - nil, + function () + return + end, "POST", json.encode({ username = "Logs", @@ -424,6 +464,7 @@ function ESX.DiscordLogFields(name, title, color, fields) ) end +---@return nil function ESX.RefreshJobs() local Jobs = {} local jobs = MySQL.query.await("SELECT * FROM jobs") @@ -458,10 +499,17 @@ function ESX.RefreshJobs() end end +---@param item string +---@param cb function +---@return nil function ESX.RegisterUsableItem(item, cb) Core.UsableItemsCallbacks[item] = cb end +---@param source number +---@param item string +---@param ... any +---@return nil function ESX.UseItem(source, item, ...) if ESX.Items[item] then local itemCallback = Core.UsableItemsCallbacks[item] @@ -478,10 +526,15 @@ function ESX.UseItem(source, item, ...) end end +---@param index string +---@param overrides table +---@return nil function ESX.RegisterPlayerFunctionOverrides(index, overrides) Core.PlayerFunctionOverrides[index] = overrides end +---@param index string +---@return nil function ESX.SetPlayerFunctionOverride(index) if not index or not Core.PlayerFunctionOverrides[index] then return print("[^3WARNING^7] No valid index provided.") @@ -490,6 +543,8 @@ function ESX.SetPlayerFunctionOverride(index) Config.PlayerFunctionOverride = index end +---@param item string +---@return string? function ESX.GetItemLabel(item) if Config.OxInventory then item = exports.ox_inventory:Items(item) @@ -505,10 +560,12 @@ function ESX.GetItemLabel(item) end end +---@return table function ESX.GetJobs() return ESX.Jobs end +---@return table function ESX.GetUsableItems() local Usables = {} for k in pairs(Core.UsableItemsCallbacks) do @@ -518,6 +575,15 @@ function ESX.GetUsableItems() end if not Config.OxInventory then + ---@param itemType string + ---@param name string + ---@param count integer + ---@param label string + ---@param playerId number + ---@param components? string | table + ---@param tintIndex? integer + ---@param coords? table | vector3 + ---@return nil function ESX.CreatePickup(itemType, name, count, label, playerId, components, tintIndex, coords) local pickupId = (Core.PickupId == 65635 and 0 or Core.PickupId + 1) local xPlayer = ESX.Players[playerId] @@ -535,11 +601,17 @@ if not Config.OxInventory then end end +---@param job string +---@param grade string +---@return boolean function ESX.DoesJobExist(job, grade) return (ESX.Jobs[job] and ESX.Jobs[job].grades[tostring(grade)] ~= nil) or false end +---@param playerId string | number +---@return boolean function Core.IsPlayerAdmin(playerId) + playerId = tostring(playerId) if (IsPlayerAceAllowed(playerId, "command") or GetConvar("sv_lan", "") == "true") and true or false then return true end diff --git a/[core]/es_extended/server/main.lua b/[core]/es_extended/server/main.lua index 26cab119..d59c4adc 100644 --- a/[core]/es_extended/server/main.lua +++ b/[core]/es_extended/server/main.lua @@ -19,6 +19,54 @@ end loadPlayer = loadPlayer .. " FROM `users` WHERE identifier = ?" +local function createESXPlayer(identifier, playerId, data) + local accounts = {} + + for account, money in pairs(Config.StartingAccountMoney) do + accounts[account] = money + end + + local defaultGroup = "user" + if Core.IsPlayerAdmin(playerId) then + print(("[^2INFO^0] Player ^5%s^0 Has been granted admin permissions via ^5Ace Perms^7."):format(playerId)) + defaultGroup = "admin" + end + + local parameters = Config.Multichar and { json.encode(accounts), identifier, defaultGroup, data.firstname, data.lastname, data.dateofbirth, data.sex, data.height } or { json.encode(accounts), identifier, defaultGroup } + + if Config.StartingInventoryItems then + table.insert(parameters, json.encode(Config.StartingInventoryItems)) + end + + MySQL.prepare(newPlayer, parameters, function() + loadESXPlayer(identifier, playerId, true) + end) +end + + +local function onPlayerJoined(playerId) + local identifier = ESX.GetIdentifier(playerId) + if identifier then + if ESX.GetPlayerFromIdentifier(identifier) then + DropPlayer( + playerId, + ("there was an error loading your character!\nError code: identifier-active-ingame\n\nThis error is caused by a player on this server who has the same identifier as you have. Make sure you are not playing on the same Rockstar account.\n\nYour Rockstar identifier: %s"):format( + identifier + ) + ) + else + local result = MySQL.scalar.await("SELECT 1 FROM users WHERE identifier = ?", { identifier }) + if result then + loadESXPlayer(identifier, playerId, false) + else + createESXPlayer(identifier, playerId) + end + end + else + DropPlayer(playerId, "there was an error loading your character!\nError code: identifier-missing-ingame\n\nThe cause of this error is not known, your identifier could not be found. Please come back later or report this problem to the server administration team.") + end +end + if Config.Multichar then AddEventHandler("esx:onPlayerJoined", function(src, char, data) while not next(ESX.Jobs) do @@ -48,59 +96,17 @@ else end) end -function onPlayerJoined(playerId) - local identifier = ESX.GetIdentifier(playerId) - if identifier then - if ESX.GetPlayerFromIdentifier(identifier) then - DropPlayer( - playerId, - ("there was an error loading your character!\nError code: identifier-active-ingame\n\nThis error is caused by a player on this server who has the same identifier as you have. Make sure you are not playing on the same Rockstar account.\n\nYour Rockstar identifier: %s"):format( - identifier - ) - ) - else - local result = MySQL.scalar.await("SELECT 1 FROM users WHERE identifier = ?", { identifier }) - if result then - loadESXPlayer(identifier, playerId, false) - else - createESXPlayer(identifier, playerId) - end - end - else - DropPlayer(playerId, "there was an error loading your character!\nError code: identifier-missing-ingame\n\nThe cause of this error is not known, your identifier could not be found. Please come back later or report this problem to the server administration team.") - end -end - -function createESXPlayer(identifier, playerId, data) - local accounts = {} - - for account, money in pairs(Config.StartingAccountMoney) do - accounts[account] = money - end - - local defaultGroup = "user" - if Core.IsPlayerAdmin(playerId) then - print(("[^2INFO^0] Player ^5%s^0 Has been granted admin permissions via ^5Ace Perms^7."):format(playerId)) - defaultGroup = "admin" - end - - local parameters = Config.Multichar and { json.encode(accounts), identifier, defaultGroup, data.firstname, data.lastname, data.dateofbirth, data.sex, data.height } or { json.encode(accounts), identifier, defaultGroup } - - if Config.StartingInventoryItems then - table.insert(parameters, json.encode(Config.StartingInventoryItems)) - end - - MySQL.prepare(newPlayer, parameters, function() - loadESXPlayer(identifier, playerId, true) - end) -end - if not Config.Multichar then AddEventHandler("playerConnecting", function(_, _, deferrals) deferrals.defer() + Wait(0) -- Required local playerId = source local identifier = ESX.GetIdentifier(playerId) + if not GetPlayerTimeOnline then + return deferrals.done(("[ESX] ESX Requires a minimum Artifact version of 9515, Please update your server."):format(oneSyncState)) + end + 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 @@ -608,7 +614,7 @@ ESX.RegisterServerCallback("esx:isUserAdmin", function(source, cb) end) ESX.RegisterServerCallback("esx:getGameBuild", function(_, cb) - cb(tonumber(GetConvar("sv_enforceGameBuild", 1604))) + cb(tonumber(GetConvar("sv_enforceGameBuild", "1604"))) end) ESX.RegisterServerCallback("esx:getOtherPlayerData", function(_, cb, target) @@ -648,7 +654,7 @@ ESX.RegisterServerCallback("esx:spawnVehicle", function(source, cb, vehData) if vehData.warp then local vehicle = NetworkGetEntityFromNetworkId(id) local timeout = 0 - while GetVehiclePedIsIn(ped) ~= vehicle and timeout <= 15 do + while GetVehiclePedIsIn(ped, false) ~= vehicle and timeout <= 15 do Wait(0) TaskWarpPedIntoVehicle(ped, vehicle, -1) timeout += 1 @@ -689,13 +695,13 @@ AddEventHandler("onResourceStart", function(key) end StopResource(key) - error(("WE STOPPED A RESOURCE THAT WILL BREAK ^1ESX^7, PLEASE REMOVE ^5%s^7"):format(key)) + error(("WE STOPPED A RESOURCE THAT WILL BREAK ^1ESX^1, PLEASE REMOVE ^5%s^1"):format(key)) end end) for key in pairs(DoNotUse) do if GetResourceState(key) == "started" or GetResourceState(key) == "starting" then StopResource(key) - error(("WE STOPPED A RESOURCE THAT WILL BREAK ^1ESX^7, PLEASE REMOVE ^5%s^7"):format(key)) + error(("WE STOPPED A RESOURCE THAT WILL BREAK ^1ESX^1, PLEASE REMOVE ^5%s^1"):format(key)) end end diff --git a/[core]/es_extended/server/modules/actions.lua b/[core]/es_extended/server/modules/actions.lua index 2c96a3a0..52eeeb5b 100644 --- a/[core]/es_extended/server/modules/actions.lua +++ b/[core]/es_extended/server/modules/actions.lua @@ -10,10 +10,6 @@ if Config.EnableDebug then print("esx:playerPedChanged", source, netId) end) - AddEventHandler("esx:playerJumping", function() - print("esx:playerJumping", source) - end) - AddEventHandler("esx:enteringVehicle", function(plate, seat, netId) print("esx:enteringVehicle", "source", source, "plate", plate, "seat", seat, "netId", netId) end) diff --git a/[core]/es_extended/server/modules/callback.lua b/[core]/es_extended/server/modules/callback.lua index 9fad6bfc..245b0987 100644 --- a/[core]/es_extended/server/modules/callback.lua +++ b/[core]/es_extended/server/modules/callback.lua @@ -1,43 +1,86 @@ -local serverCallbacks = {} +---@diagnostic disable: duplicate-set-field -local clientRequests = {} -local RequestId = 0 +Callbacks = {} ----@param eventName string ----@param callback function -ESX.RegisterServerCallback = function(eventName, callback) - serverCallbacks[eventName] = callback +Callbacks.requests = {} +Callbacks.storage = {} +Callbacks.id = 0 + +function Callbacks:Register(name, cb) + self.storage[name] = cb end -RegisterNetEvent("esx:triggerServerCallback", function(eventName, requestId, invoker, ...) - if not serverCallbacks[eventName] then - return error(("Server Callback not registered, name: ^5%s^7, invoker resource: ^5%s^7"):format(eventName, invoker)) +function Callbacks:Execute(cb, ...) + local success, errorString = pcall(cb, ...) + + if not success then + print(("[^1ERROR^7] Failed to execute Callback with RequestId: ^5%s^7"):format(self.currentId)) + error(errorString) + return + end + self.currentId = nil +end + +function Callbacks:Trigger(player, event, cb, invoker, ...) + self.requests[self.id] = cb + + TriggerClientEvent("esx:triggerClientCallback", player, event, self.id, invoker, ...) + + self.id += 1 +end + +function Callbacks:ServerRecieve(player, event, requestId, invoker, ...) + self.currentId = requestId + + if not self.storage[event] then + return error(("Server Callback with requestId ^5%s^1 Was Called by ^5%s^1 but does not exist."):format(event, invoker)) end - local source = source + local returnCb = function(...) + TriggerClientEvent("esx:serverCallback", player, requestId, invoker, ...) + end + local callback = self.storage[event] - serverCallbacks[eventName](source, function(...) - TriggerClientEvent("esx:serverCallback", source, requestId, invoker, ...) - end, ...) + self:Execute(callback, player, returnCb, ...) +end + +function Callbacks:RecieveClient(requestId, invoker, ...) + self.currentId = requestId + + if not self.requests[self.currentId] then + return error(("Client Callback with requestId ^5%s^1 Was Called by ^5%s^1 but does not exist."):format(self.currentId, invoker)) + end + + local callback = self.requests[self.currentId] + + self:Execute(callback, ...) + self.requests[requestId] = nil +end + +RegisterNetEvent("esx:clientCallback", function(requestId, invoker, ...) + Callbacks:RecieveClient(requestId, invoker, ...) end) +RegisterNetEvent("esx:triggerServerCallback", function(eventName, requestId, invoker, ...) + local source = source + Callbacks:ServerRecieve(source, eventName, requestId, invoker, ...) +end) + + ---@param player number playerId ---@param eventName string ---@param callback function ---@param ... any -ESX.TriggerClientCallback = function(player, eventName, callback, ...) - clientRequests[RequestId] = callback +function ESX.TriggerClientCallback(player, eventName, callback, ...) + local invokingResource = GetInvokingResource() + local invoker = (invokingResource and invokingResource ~= "unknown") and invokingResource or "es_extended" - TriggerClientEvent("esx:triggerClientCallback", player, eventName, RequestId, GetInvokingResource() or "unknown", ...) - - RequestId = RequestId + 1 + Callbacks:Trigger(player, eventName, callback, invoker, ...) end -RegisterNetEvent("esx:clientCallback", function(requestId, invoker, ...) - if not clientRequests[requestId] then - return error(("Client Callback with requestId ^5%s^7 Was Called by ^5%s^7 but does not exist."):format(requestId, invoker)) - end +---@param eventName string +---@param callback function +ESX.RegisterServerCallback = function(eventName, callback) + Callbacks:Register(eventName, callback) +end - clientRequests[requestId](...) - clientRequests[requestId] = nil -end) diff --git a/[core]/es_extended/server/modules/createJob.lua b/[core]/es_extended/server/modules/createJob.lua index 432d0f1b..c4c09eff 100644 --- a/[core]/es_extended/server/modules/createJob.lua +++ b/[core]/es_extended/server/modules/createJob.lua @@ -22,7 +22,7 @@ end local function generateNewJobTable(name, label, grades) local job = { name = name, label = label, grades = {} } for _, v in pairs(grades) do - job.grades[tostring(v.grade)] = { job_name = name, grade = v.grade, name = v.name, label = v.label, salary = v.salary, skin_male = {}, skin_female = {} } + job.grades[tostring(v.grade)] = { job_name = name, grade = v.grade, name = v.name, label = v.label, salary = v.salary, skin_male = v.skin_male or '{}', skin_female = v.skin_female or '{}' } end return job diff --git a/[core]/es_extended/server/modules/npwd.lua b/[core]/es_extended/server/modules/npwd.lua index 35cdfd76..78aa863d 100644 --- a/[core]/es_extended/server/modules/npwd.lua +++ b/[core]/es_extended/server/modules/npwd.lua @@ -7,6 +7,9 @@ AddEventHandler("onServerResourceStart", function(resource) npwd = GetResourceState("npwd"):find("start") and exports.npwd or nil + if not npwd then + return + end for _, xPlayer in pairs(ESX.Players) do npwd:newPlayer({ source = xPlayer.source, diff --git a/[core]/es_extended/server/onesync.lua b/[core]/es_extended/server/onesync.lua index cb4ad40a..1beff597 100644 --- a/[core]/es_extended/server/onesync.lua +++ b/[core]/es_extended/server/onesync.lua @@ -91,16 +91,21 @@ function ESX.OneSync.SpawnVehicle(model, coords, heading, properties, cb) local xPlayer = ESX.OneSync.GetClosestPlayer(coords, 300) ESX.GetVehicleType(vehicleModel, xPlayer.id, function(vehicleType) if vehicleType then - local createdVehicle = CreateVehicleServerSetter(vehicleModel, vehicleType, coords, heading) - if not DoesEntityExist(createdVehicle) then - return error("Unfortunately, this vehicle has not spawned") + local createdVehicle = CreateVehicleServerSetter(vehicleModel, vehicleType, coords.x, coords.y, coords.z, heading) + local tries = 0 + while not DoesEntityExist(createdVehicle) do + Wait(200) + tries = tries + 1 + if tries > 10 then + return print("[^1ERROR^7] Unfortunately, this vehicle has not spawned") + end end local networkId = NetworkGetNetworkIdFromEntity(createdVehicle) Entity(createdVehicle).state:set("VehicleProperties", vehicleProperties, true) cb(networkId) else - error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) + print(("[^1ERROR^7] Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) end end) end) @@ -116,7 +121,7 @@ function ESX.OneSync.SpawnObject(model, coords, heading, cb) end local objectCoords = type(coords) == "vector3" and coords or vector3(coords.x, coords.y, coords.z) CreateThread(function() - local entity = CreateObject(model, objectCoords, true, true) + local entity = CreateObject(model, objectCoords.x, objectCoords.y, objectCoords.z, true, true, false) while not DoesEntityExist(entity) do Wait(50) end @@ -194,14 +199,14 @@ end ---@param coords vector3 ---@param maxDistance number ----@param modelFilter table models to ignore, where the key is the model hash and the value is true +---@param modelFilter table | nil models to ignore, where the key is the model hash and the value is true ---@return table function ESX.OneSync.GetVehiclesInArea(coords, maxDistance, modelFilter) return getNearbyEntities(GetAllVehicles(), coords, modelFilter, maxDistance) end local function getClosestEntity(entities, coords, modelFilter, isPed) - local distance, closestEntity, closestCoords = 100, nil, nil + local distance, closestEntity, closestCoords = 100, 0, vector3(0, 0, 0) coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z) for _, entity in pairs(entities) do @@ -215,6 +220,7 @@ local function getClosestEntity(entities, coords, modelFilter, isPed) end end end + return NetworkGetNetworkIdFromEntity(closestEntity), distance, closestCoords end diff --git a/[core]/es_extended/server/paycheck.lua b/[core]/es_extended/server/paycheck.lua index ef317e5d..f80c7f12 100644 --- a/[core]/es_extended/server/paycheck.lua +++ b/[core]/es_extended/server/paycheck.lua @@ -7,59 +7,61 @@ function StartPayCheck() local job = xPlayer.job.grade_name local salary = xPlayer.job.grade_salary - if salary > 0 then - if job == "unemployed" then -- unemployed - xPlayer.addAccountMoney("bank", salary, "Welfare Check") - TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_help", salary), "CHAR_BANK_MAZE", 9) - if Config.LogPaycheck then - ESX.DiscordLogFields("Paycheck", "Paycheck - Unemployment Benefits", "green", { - { name = "Player", value = xPlayer.name, inline = true }, - { name = "ID", value = xPlayer.source, inline = true }, - { name = "Amount", value = salary, inline = true }, - }) - end - elseif Config.EnableSocietyPayouts then -- possibly a society - TriggerEvent("esx_society:getSociety", xPlayer.job.name, function(society) - if society ~= nil then -- verified society - TriggerEvent("esx_addonaccount:getSharedAccount", society.account, function(account) - if account.money >= salary then -- does the society money to pay its employees? - xPlayer.addAccountMoney("bank", salary, "Paycheck") - account.removeMoney(salary) - if Config.LogPaycheck then - ESX.DiscordLogFields("Paycheck", "Paycheck - " .. jobLabel, "green", { - { name = "Player", value = xPlayer.name, inline = true }, - { name = "ID", value = xPlayer.source, inline = true }, - { name = "Amount", value = salary, inline = true }, - }) - end - - TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9) - else - TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), "", TranslateCap("company_nomoney"), "CHAR_BANK_MAZE", 1) - end - end) - else -- not a society - xPlayer.addAccountMoney("bank", salary, "Paycheck") - if Config.LogPaycheck then - ESX.DiscordLogFields("Paycheck", "Paycheck - " .. jobLabel, "green", { - { name = "Player", value = xPlayer.name, inline = true }, - { name = "ID", value = xPlayer.source, inline = true }, - { name = "Amount", value = salary, inline = true }, - }) - end - TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9) + if xPlayer.paycheckEnabled then + if salary > 0 then + if job == "unemployed" then -- unemployed + xPlayer.addAccountMoney("bank", salary, "Welfare Check") + TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_help", salary), "CHAR_BANK_MAZE", 9) + if Config.LogPaycheck then + ESX.DiscordLogFields("Paycheck", "Paycheck - Unemployment Benefits", "green", { + { name = "Player", value = xPlayer.name, inline = true }, + { name = "ID", value = xPlayer.source, inline = true }, + { name = "Amount", value = salary, inline = true }, + }) end - end) - else -- generic job - xPlayer.addAccountMoney("bank", salary, "Paycheck") - if Config.LogPaycheck then - ESX.DiscordLogFields("Paycheck", "Paycheck - Generic", "green", { - { name = "Player", value = xPlayer.name, inline = true }, - { name = "ID", value = xPlayer.source, inline = true }, - { name = "Amount", value = salary, inline = true }, - }) + elseif Config.EnableSocietyPayouts then -- possibly a society + TriggerEvent("esx_society:getSociety", xPlayer.job.name, function(society) + if society ~= nil then -- verified society + TriggerEvent("esx_addonaccount:getSharedAccount", society.account, function(account) + if account.money >= salary then -- does the society money to pay its employees? + xPlayer.addAccountMoney("bank", salary, "Paycheck") + account.removeMoney(salary) + if Config.LogPaycheck then + ESX.DiscordLogFields("Paycheck", "Paycheck - " .. jobLabel, "green", { + { name = "Player", value = xPlayer.name, inline = true }, + { name = "ID", value = xPlayer.source, inline = true }, + { name = "Amount", value = salary, inline = true }, + }) + end + + TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9) + else + TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), "", TranslateCap("company_nomoney"), "CHAR_BANK_MAZE", 1) + end + end) + else -- not a society + xPlayer.addAccountMoney("bank", salary, "Paycheck") + if Config.LogPaycheck then + ESX.DiscordLogFields("Paycheck", "Paycheck - " .. jobLabel, "green", { + { name = "Player", value = xPlayer.name, inline = true }, + { name = "ID", value = xPlayer.source, inline = true }, + { name = "Amount", value = salary, inline = true }, + }) + end + TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9) + end + end) + else -- generic job + xPlayer.addAccountMoney("bank", salary, "Paycheck") + if Config.LogPaycheck then + ESX.DiscordLogFields("Paycheck", "Paycheck - Generic", "green", { + { name = "Player", value = xPlayer.name, inline = true }, + { name = "ID", value = xPlayer.source, inline = true }, + { name = "Amount", value = salary, inline = true }, + }) + end + TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9) end - TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9) end end end diff --git a/[core]/esx_chat_theme/LICENSE b/[core]/esx_chat_theme/LICENSE new file mode 100644 index 00000000..2aa46e29 --- /dev/null +++ b/[core]/esx_chat_theme/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + esx_clotheshop + Copyright (C) 2015-2023 Jérémie N'gadi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + esx_clotheshop Copyright (C) 2015-2023 Jérémie N'gadi + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/[core]/esx_chat_theme/README.md b/[core]/esx_chat_theme/README.md new file mode 100644 index 00000000..c6c88f0e --- /dev/null +++ b/[core]/esx_chat_theme/README.md @@ -0,0 +1,15 @@ +

[ESX] Chat Theme

Discord - Documentation + +A ESX-Based Chat-theme for your server + +# License + +esx_chat_theme - ESX Chat Theme + +Copyright (C) 2024 Jérémie N'gadi + +This program Is free software: you can redistribute it And/Or modify it under the terms Of the GNU General Public License As published by the Free Software Foundation, either version 3 Of the License, Or (at your option) any later version. + +This program Is distributed In the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty Of MERCHANTABILITY Or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License For more details. + +You should have received a copy Of the GNU General Public License along with this program. If Not, see http://www.gnu.org/licenses/. diff --git a/[core]/esx_chat_theme/fxmanifest.lua b/[core]/esx_chat_theme/fxmanifest.lua new file mode 100644 index 00000000..b5bb6785 --- /dev/null +++ b/[core]/esx_chat_theme/fxmanifest.lua @@ -0,0 +1,17 @@ +version '1.10.10' +author 'ESX-Framework' +description 'A ESX Stylised theme for the chat resource.' + +file 'style.css' +file 'shadow.js' + +chat_theme 'esx' { + styleSheet = 'style.css', + script = 'shadow.js', + msgTemplates = { + default = '{0}{1}' + } +} + +game 'common' +fx_version 'adamant' diff --git a/[core]/esx_chat_theme/shadow.js b/[core]/esx_chat_theme/shadow.js new file mode 100644 index 00000000..6393b2dc --- /dev/null +++ b/[core]/esx_chat_theme/shadow.js @@ -0,0 +1,74 @@ +(function() { +var Filters = {} + +var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); +svg.setAttribute("style", "display:block;width:0px;height:0px"); +var defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); + +var blurFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter"); +blurFilter.setAttribute("id", "svgBlurFilter"); +var feGaussianFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur"); +feGaussianFilter.setAttribute("stdDeviation", "0 0"); +blurFilter.appendChild(feGaussianFilter); +defs.appendChild(blurFilter); +Filters._svgBlurFilter = feGaussianFilter; + +// Drop Shadow Filter +var dropShadowFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter"); +dropShadowFilter.setAttribute("id", "svgDropShadowFilter"); +var feGaussianFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur"); +feGaussianFilter.setAttribute("in", "SourceAlpha"); +feGaussianFilter.setAttribute("stdDeviation", "3"); +dropShadowFilter.appendChild(feGaussianFilter); +Filters._svgDropshadowFilterBlur = feGaussianFilter; + +var feOffset = document.createElementNS("http://www.w3.org/2000/svg", "feOffset"); +feOffset.setAttribute("dx", "0"); +feOffset.setAttribute("dy", "0"); +feOffset.setAttribute("result", "offsetblur"); +dropShadowFilter.appendChild(feOffset); +Filters._svgDropshadowFilterOffset = feOffset; + +var feFlood = document.createElementNS("http://www.w3.org/2000/svg", "feFlood"); +feFlood.setAttribute("flood-color", "rgba(0,0,0,1)"); +dropShadowFilter.appendChild(feFlood); +Filters._svgDropshadowFilterFlood = feFlood; + +var feComposite = document.createElementNS("http://www.w3.org/2000/svg", "feComposite"); +feComposite.setAttribute("in2", "offsetblur"); +feComposite.setAttribute("operator", "in"); +dropShadowFilter.appendChild(feComposite); +var feComposite = document.createElementNS("http://www.w3.org/2000/svg", "feComposite"); +feComposite.setAttribute("in2", "SourceAlpha"); +feComposite.setAttribute("operator", "out"); +feComposite.setAttribute("result", "outer"); +dropShadowFilter.appendChild(feComposite); + +var feMerge = document.createElementNS("http://www.w3.org/2000/svg", "feMerge"); +var feMergeNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode"); +feMerge.appendChild(feMergeNode); +var feMergeNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode"); +feMerge.appendChild(feMergeNode); +Filters._svgDropshadowMergeNode = feMergeNode; +dropShadowFilter.appendChild(feMerge); +defs.appendChild(dropShadowFilter); +svg.appendChild(defs); +document.documentElement.appendChild(svg); + +const blurScale = 4; +const scale = (document.body.clientWidth / 1280); + +Filters._svgDropshadowFilterBlur.setAttribute("stdDeviation", + 1 * blurScale + " " + + 1 * blurScale +); +Filters._svgDropshadowFilterOffset.setAttribute("dx", + String(Math.cos(45 * Math.PI / 180) * 1 * scale)); +Filters._svgDropshadowFilterOffset.setAttribute("dy", + String(Math.sin(45 * Math.PI / 180) * 1 * scale)); +Filters._svgDropshadowFilterFlood.setAttribute("flood-color", + 'rgba(0, 0, 0, 1)'); +Filters._svgDropshadowMergeNode.setAttribute("in", + "SourceGraphic"); + +})(); \ No newline at end of file diff --git a/[core]/esx_chat_theme/style.css b/[core]/esx_chat_theme/style.css new file mode 100644 index 00000000..844d720a --- /dev/null +++ b/[core]/esx_chat_theme/style.css @@ -0,0 +1,103 @@ +* { + font-family: inherit; +} + +.chat-window { + background: inherit !important; +} + +@font-face { + font-family: 'Font2'; + src: url(https://runtime.fivem.net/temp/ChaletLondonNineteenSixty.otf?a); +} + +@font-face { + font-family: 'Font2_cond'; + src: url(https://runtime.fivem.net/temp/chaletcomprime-colognesixty-webfont.ttf?a); +} + +.msg { + font-family: Font2, sans-serif; + color: #ffffff; + + font-size: calc(1.7vh); /* 13px in 720p, calc'd by width */ + filter: url(#svgDropShadowFilter); + + line-height: calc(2.7vh * 1.2); + + margin-bottom: 0; +} + +.chat-messages { + margin: 0; + height: 100%; +} + +.msg > span > span > b { + font-family: Font2_cond, sans-serif; + font-weight: normal; + + vertical-align: baseline; + padding-right: 11px; + + line-height: calc(2.7vh * 1.2); + + font-size: calc(2.4vh); +} + +.msg > span > span > span { + vertical-align: baseline; +} + +.msg i:first-of-type { + font-style: normal; + color: #c0c0c0; +} + +.chat-input { + background: inherit !important; + height: auto; + + font-family: Font2, sans-serif; +} + +.chat-input > div { + background-color: rgba(30, 30, 30, 0.7) !important; + border: calc(0.24vh / 2) solid #fb9c04ce; + outline: calc(0.24vh / 2) solid #fb9c04ce; /* to replace margin-background */ + padding: calc(0.2vh / 2); + border-radius: 5px; + filter: url(#svgDropShadowFilter); +} + +.chat-input .prefix { + margin: 0; + margin-left: 0.7%; + margin-right: 0.7%; + margin-top: -0.1%; + line-height: 2.8vh; +} + +.chat-input .prefix.any { + opacity: 0.8; +} + +.chat-input .prefix.any:before { + content: '['; + color: #fb9b04; +} + +.chat-input .prefix.any:after { + content: ']'; + color: #fb9b04; + margin-right: 1%; +} + +.suggestions { + background: transparent; +} + +textarea { + background: transparent; + padding: 0.5vh; +} \ No newline at end of file diff --git a/[core]/esx_context/main.lua b/[core]/esx_context/main.lua index 16a6b446..4b392607 100644 --- a/[core]/esx_context/main.lua +++ b/[core]/esx_context/main.lua @@ -144,7 +144,7 @@ local function focusPreview() end if PREVIEW_KEYBIND then - RegisterCommand("previewContext", focusPreview) + RegisterCommand("previewContext", focusPreview, false) RegisterKeyMapping("previewContext", "Preview Active Context", "keyboard", PREVIEW_KEYBIND) end @@ -208,15 +208,15 @@ if Debug then RegisterCommand("ctx:preview", function() exports["esx_context"]:Preview(position, eles) - end) + end, false) RegisterCommand("ctx:open", function() exports["esx_context"]:Open(position, eles, onSelect, onClose) - end) + end, false) RegisterCommand("ctx:close", function() exports["esx_context"]:Close() - end) + end, false) RegisterCommand("ctx:form", function() local formMenu = { @@ -260,5 +260,5 @@ if Debug then } exports["esx_context"]:Open(position, formMenu, onSelect, onClose) - end) + end, false) end diff --git a/[core]/esx_menu_default/client/main.lua b/[core]/esx_menu_default/client/main.lua index 522d1a55..95aa4933 100644 --- a/[core]/esx_menu_default/client/main.lua +++ b/[core]/esx_menu_default/client/main.lua @@ -37,6 +37,11 @@ ESX.UI.Menu.RegisterType(MenuType, openMenu, closeMenu) RegisterNUICallback("menu_submit", function(data, cb) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) + + if not menu then + return + end + if menu.submit ~= nil then menu.submit(data, menu) end @@ -46,6 +51,10 @@ end) RegisterNUICallback("menu_cancel", function(data, cb) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) + if not menu then + return + end + if menu.cancel ~= nil then menu.cancel(data, menu) end @@ -55,6 +64,10 @@ end) RegisterNUICallback("menu_change", function(data, cb) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) + if not menu then + return + end + for i = 1, #data.elements, 1 do menu.setElement(i, "value", data.elements[i].value) diff --git a/[core]/esx_menu_dialog/client/main.lua b/[core]/esx_menu_dialog/client/main.lua index aee38864..82588182 100644 --- a/[core]/esx_menu_dialog/client/main.lua +++ b/[core]/esx_menu_dialog/client/main.lua @@ -31,7 +31,7 @@ local function closeMenu(namespace, name) }) if not next(OpenedMenus) then - SetNuiFocus(false) + SetNuiFocus(false, false) end end @@ -41,10 +41,15 @@ AddEventHandler("esx_menu_dialog:message:menu_submit", function(data) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) local cancel = false + if not menu then + return + end + if menu.submit then -- is the submitted data a number? - if tonumber(data.value) then - data.value = ESX.Math.Round(tonumber(data.value)) + local value = tonumber(data.value) + if value then + data.value = ESX.Math.Round(value) -- check for negative value if tonumber(data.value) <= 0 then @@ -66,6 +71,10 @@ end) AddEventHandler("esx_menu_dialog:message:menu_cancel", function(data) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) + if not menu then + return + end + if menu.cancel ~= nil then menu.cancel(data, menu) end @@ -74,6 +83,10 @@ end) AddEventHandler("esx_menu_dialog:message:menu_change", function(data) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) + if not menu then + return + end + if menu.change ~= nil then menu.change(data, menu) end diff --git a/[core]/esx_menu_list/client/main.lua b/[core]/esx_menu_list/client/main.lua index ba2b3793..690c8ef6 100644 --- a/[core]/esx_menu_list/client/main.lua +++ b/[core]/esx_menu_list/client/main.lua @@ -33,7 +33,7 @@ CreateThread(function() end if OpenedMenuCount == 0 then - SetNuiFocus(false) + SetNuiFocus(false, false) end end @@ -41,6 +41,11 @@ CreateThread(function() RegisterNUICallback("menu_submit", function(data, cb) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) + + if not menu then + return + end + if menu.submit then menu.submit(data, menu) end @@ -50,6 +55,10 @@ CreateThread(function() RegisterNUICallback("menu_cancel", function(data, cb) local menu = ESX.UI.Menu.GetOpened(MenuType, data._namespace, data._name) + if not menu then + return + end + if menu.cancel ~= nil then menu.cancel(data, menu) end diff --git a/[core]/esx_multicharacter/client/main.lua b/[core]/esx_multicharacter/client/main.lua index 7771876d..04b9a88e 100644 --- a/[core]/esx_multicharacter/client/main.lua +++ b/[core]/esx_multicharacter/client/main.lua @@ -1,364 +1,64 @@ -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") +CreateThread(function() + + while not ESX.PlayerLoaded do + Wait(100) + + if NetworkIsPlayerActive(ESX.playerId) then + ESX.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(), 0, 0) - SetLocalPlayerVisibleLocally(1) - SetPlayerInvincible(PlayerId(), 1) - 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, 1, 0) - SetPlayerInvincible(playerId, 0) - 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) - 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) - 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) - 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) - - SetCamActive(cam, false) - RenderScriptCams(false, false, 0, true, true) - cam = nil - local playerPed = PlayerPedId() - FreezeEntityPosition(playerPed, true) - SetEntityCoordsNoOffset(playerPed, spawn.x, spawn.y, spawn.z, false, false, false, true) - 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,false) end 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..486de36b --- /dev/null +++ b/[core]/esx_multicharacter/client/modules/multicharacter.lua @@ -0,0 +1,292 @@ +---@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 HiddenCompents = {} + +local function HideComponents(hide) + local components = {11, 12, 21} + for i = 1, #components do + if hide then + local size = GetHudComponentSize(components[i]) + if size.x > 0 or size.y > 0 then + HiddenCompents[components[i]] = size + SetHudComponentSize(components[i], 0.0, 0.0) + end + else + if HiddenCompents[components[i]] then + local size = HiddenCompents[components[i]] + SetHudComponentSize(components[i], size.x, size.z) + HiddenCompents[components[i]] = nil + end + end + end + DisplayRadar(not hide) +end + +function Multicharacter:HideHud(hide) + self.hidePlayers = true + + MumbleSetVolumeOverride(ESX.PlayerId, 0.0) + HideComponents(hide) +end + +function Multicharacter:SetupCharacters() + ESX.PlayerLoaded = false + ESX.PlayerData = {} + + 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) + + SetPlayerControl(ESX.PlayerId, false, 0) + self:SetupCamera() + self:HideHud(true) + + 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.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.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 + + SetTimeout(10000, function() + self.canRelog = true + end) +end + +function Multicharacter:Spawn(spawn) + + 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 or spawn.w or 0.0, 0, true) +end + +function Multicharacter:PlayerLoaded(playerData, isNew, skin) + DoScreenFadeOut(750) + self:AwaitFadeOut() + + local esxSpawns = ESX.GetConfig().DefaultSpawns + local spawn = esxSpawns[math.random(1, #esxSpawns)] + + 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(spawn) + Wait(500) + + self:HideHud(false) + SetPlayerControl(ESX.playerId, true, 0) + FreezeEntityPosition(self.playerPed, false) + + DoScreenFadeIn(750) + self:AwaitFadeIn() + + TriggerServerEvent("esx:onPlayerSpawn") + TriggerEvent("esx:onPlayerSpawn") + TriggerEvent("playerSpawned") + TriggerEvent("esx:restoreLoadout") + + self:Reset() +end diff --git a/[core]/esx_multicharacter/config.lua b/[core]/esx_multicharacter/config.lua index 9f6e0577..9c80d374 100644 --- a/[core]/esx_multicharacter/config.lua +++ b/[core]/esx_multicharacter/config.lua @@ -30,8 +30,10 @@ else ["m"] = { mom = 43, dad = 29, + grandparents = 0, face_md_weight = 61, skin_md_weight = 27, + face_g_weight = 0, nose_1 = -5, nose_2 = 6, nose_3 = 5, @@ -127,8 +129,10 @@ else ["f"] = { mom = 28, dad = 6, + grandparents = 0, face_md_weight = 63, skin_md_weight = 60, + face_g_weight = 0, nose_1 = -10, nose_2 = 4, nose_3 = 5, 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 eec85eb6..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^0", 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 .. "^0") - end - end) -end - -MySQL.ready(function() - local length = 42 + #PREFIX - local DB_COLUMNS = MySQL.query.await(('SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH 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..923dd55e --- /dev/null +++ b/[core]/esx_multicharacter/server/modules/multicharacter.lua @@ -0,0 +1,98 @@ +---@diagnostic disable: duplicate-set-field + +Multicharacter = {} +Multicharacter._index = Multicharacter +Multicharacter.awaitingRegistration = {} + +function Multicharacter:SetupCharacters(source) + SetPlayerRoutingBucket(source, source) + while not Database.connected do + Wait(100) + end + + local identifier = 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 + SetPlayerRoutingBucket(source, 0) + 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 + + SetPlayerRoutingBucket(source, 0) + 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 diff --git a/[core]/esx_notify/Notify.lua b/[core]/esx_notify/Notify.lua index 5bddf2cc..2d8e5900 100644 --- a/[core]/esx_notify/Notify.lua +++ b/[core]/esx_notify/Notify.lua @@ -38,21 +38,21 @@ if Debug then RegisterCommand("oldnotify", function() ---@diagnostic disable-next-line ESX.ShowNotification("No Waypoint Set.", true, false, 140) - end) + end, false) RegisterCommand("notify", function() ESX.ShowNotification("You Recived ~br~ 1x ball~s~!", "success", 3000) - end) + end, false) RegisterCommand("notify1", function() ESX.ShowNotification("Well ~g~Done~s~!", "success", 3000) - end) + end, false) RegisterCommand("notify2", function() ESX.ShowNotification("Information Recived", "info", 3000) - end) + end, false) RegisterCommand("notify3", function() ESX.ShowNotification("You Did something ~r~WRONG~s~!", "error", 3000) - end) + end, false) end diff --git a/[core]/esx_progressbar/Progress.lua b/[core]/esx_progressbar/Progress.lua index 0f018f5f..d412f581 100644 --- a/[core]/esx_progressbar/Progress.lua +++ b/[core]/esx_progressbar/Progress.lua @@ -1,5 +1,37 @@ +---@class ProgressBarAnimation +---@field public type "anim" | "Scenario" +---@field public dict? string +---@field public lib? string + +---@class ProgressBarOptions +---@field public animation ProgressBarAnimation +---@field public FreezePlayer? boolean +---@field public onFinish? function + local CurrentProgress = nil +local function startProcessing() + while (CurrentProgress ~= nil) do + if CurrentProgress.length > 0 then + CurrentProgress.length = CurrentProgress.length - 1000 + else + ClearPedTasks(ESX.PlayerData.ped) + if CurrentProgress.FreezePlayer then + FreezeEntityPosition(ESX.PlayerData.ped, false) + end + if CurrentProgress.onFinish then + CurrentProgress.onFinish() + end + CurrentProgress = nil + end + Wait(1000) + end +end + +---@param message string +---@param length? number Timeout in milliseconds +---@param Options? ProgressBarOptions +---@return boolean Success local function Progressbar(message, length, Options) if CurrentProgress then return false @@ -24,21 +56,8 @@ local function Progressbar(message, length, Options) message = message or "ESX-Framework", }) CurrentProgress.length = length or 3000 - while CurrentProgress ~= nil do - if CurrentProgress.length > 0 then - CurrentProgress.length = CurrentProgress.length - 1000 - else - ClearPedTasks(ESX.PlayerData.ped) - if CurrentProgress.FreezePlayer then - FreezeEntityPosition(ESX.PlayerData.ped, false) - end - if CurrentProgress.onFinish then - CurrentProgress.onFinish() - end - CurrentProgress = nil - end - Wait(1000) - end + CreateThread(startProcessing); + return true; end local function CancelProgressbar() diff --git a/[core]/esx_skin/client/main.lua b/[core]/esx_skin/client/main.lua index 243d30ec..33b611a5 100644 --- a/[core]/esx_skin/client/main.lua +++ b/[core]/esx_skin/client/main.lua @@ -1,260 +1,51 @@ -local lastSkin, cam, isCameraActive -local firstSpawn, zoomOffset, camOffset, heading = true, 0.0, 0.0, 90.0 +Skin = {} +Skin._index = Skin -RegisterNetEvent("esx:playerLoaded") -AddEventHandler("esx:playerLoaded", function(_, _, skin) - TriggerServerEvent("esx_skin:setWeight", skin) -end) +Skin.firstSpawn = true +Skin.zoomOffset = 0.0 +Skin.camOffset = 0.0 +Skin.heading = 90.0 +Skin.customPI = math.pi / 180.0 -function OpenMenu(submitCb, cancelCb, restrict) - local playerPed = PlayerPedId() - - TriggerEvent("skinchanger:getSkin", function(skin) - lastSkin = skin - end) - TriggerEvent("skinchanger:getData", function(components, maxVals) - local elements = {} - local _components = {} - - -- Restrict menu - if restrict == nil then - for i = 1, #components, 1 do - _components[i] = components[i] - end - else - for i = 1, #components, 1 do - local found = false - - for j = 1, #restrict, 1 do - if components[i].name == restrict[j] then - found = true - end - end - - if found then - table.insert(_components, components[i]) - end - end - end - -- Insert elements - for i = 1, #_components, 1 do - local value = _components[i].value - local componentId = _components[i].componentId - - if componentId == 0 then - value = GetPedPropIndex(playerPed, _components[i].componentId) - end - - local data = { - label = _components[i].label, - name = _components[i].name, - value = value, - min = _components[i].min, - textureof = _components[i].textureof, - zoomOffset = _components[i].zoomOffset, - camOffset = _components[i].camOffset, - type = "slider", - } - - for k, v in pairs(maxVals) do - if k == _components[i].name then - data.max = v - break - end - end - - table.insert(elements, data) - end - - CreateSkinCam() - zoomOffset = _components[1].zoomOffset - camOffset = _components[1].camOffset - - ESX.UI.Menu.Open("default", GetCurrentResourceName(), "skin", { - title = TranslateCap("skin_menu"), - align = "bottom-left", - elements = elements, - }, function(data, menu) - TriggerEvent("skinchanger:getSkin", function(skin) - lastSkin = skin - end) - - submitCb(data, menu) - DeleteSkinCam() - end, function(data, menu) - menu.close() - DeleteSkinCam() - TriggerEvent("skinchanger:loadSkin", lastSkin) - - if cancelCb ~= nil then - cancelCb(data, menu) - end - end, function(data, menu) - local skin, tempMaxVals - - TriggerEvent("skinchanger:getSkin", function(getSkin) - skin = getSkin - end) - - zoomOffset = data.current.zoomOffset - camOffset = data.current.camOffset - - if skin[data.current.name] ~= data.current.value then - -- Change skin element - TriggerEvent("skinchanger:change", data.current.name, data.current.value) - - -- Update max values - TriggerEvent("skinchanger:getData", function(comp, max) - tempMaxVals = max - end) - - local newData = {} - - for i = 1, #elements, 1 do - newData.max = tempMaxVals[elements[i].name] - - if elements[i].textureof ~= nil and data.current.name == elements[i].textureof then - newData.value = 0 - end - - menu.update({ name = elements[i].name }, newData) - end - - menu.refresh() - end - end, function() - DeleteSkinCam() - end) - end) +function Skin:CalcualteHeading(angle) + if angle > 360 then + angle = angle - 360 + elseif angle < 0 then + angle = angle + 360 + end + angle = angle * self.customPI + return angle end -function CreateSkinCam() - if not DoesCamExist(cam) then - cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) - end +function Skin:CalcuatePosition(coords) - local playerPed = PlayerPedId() + local angle = self.heading * self.customPI + local theta = { + x = math.cos(angle), + y = math.sin(angle), + } - SetCamActive(cam, true) - RenderScriptCams(true, true, 500, true, true) + local pos = { + x = coords.x + (self.zoomOffset * theta.x), + y = coords.y + (self.zoomOffset * theta.y), + } - isCameraActive = true - SetCamCoord(cam, GetEntityCoords(playerPed)) - SetCamRot(cam, 0.0, 0.0, 270.0, true) - SetEntityHeading(playerPed, 0.0) -end + local angleToLook = self:CalcualteHeading(self.heading - 140.0) + + local thetaToLook = { + x = math.cos(angleToLook), + y = math.sin(angleToLook), + } -function DeleteSkinCam() - isCameraActive = false - SetCamActive(cam, false) - RenderScriptCams(false, true, 500, true, true) - cam = nil -end - -CreateThread(function() - local customPI = math.pi / 180.0 - while true do - local sleep = 1500 - - if isCameraActive then - sleep = 0 - DisableControlAction(2, 30, true) - DisableControlAction(2, 31, true) - DisableControlAction(2, 32, true) - DisableControlAction(2, 33, true) - DisableControlAction(2, 34, true) - DisableControlAction(2, 35, true) - DisableControlAction(0, 25, true) -- Input Aim - DisableControlAction(0, 24, true) -- Input Attack - - local playerPed = PlayerPedId() - local coords = GetEntityCoords(playerPed) - - local angle = heading * customPI - local theta = { - x = math.cos(angle), - y = math.sin(angle), - } - - local pos = { - x = coords.x + (zoomOffset * theta.x), - y = coords.y + (zoomOffset * theta.y), - } - - local angleToLook = heading - 140.0 - if angleToLook > 360 then - angleToLook = angleToLook - 360 - elseif angleToLook < 0 then - angleToLook = angleToLook + 360 - end - - angleToLook = angleToLook * customPI - local thetaToLook = { - x = math.cos(angleToLook), - y = math.sin(angleToLook), - } - - local posToLook = { - x = coords.x + (zoomOffset * thetaToLook.x), - y = coords.y + (zoomOffset * thetaToLook.y), - } - - SetCamCoord(cam, pos.x, pos.y, coords.z + camOffset) - PointCamAtCoord(cam, posToLook.x, posToLook.y, coords.z + camOffset) - - ESX.ShowHelpNotification(TranslateCap("use_rotate_view")) - end - Wait(sleep) - end -end) - -CreateThread(function() - local angle = 90 - - while true do - local sleep = 1500 - - if isCameraActive then - sleep = 0 - if IsControlPressed(0, 209) then - angle = angle - 1 - elseif IsControlPressed(0, 19) then - angle = angle + 1 - end - - if angle > 360 then - angle = angle - 360 - elseif angle < 0 then - angle = angle + 360 - end - - heading = angle + 0.0 - end - Wait(sleep) - end -end) - -function OpenSaveableMenu(submitCb, cancelCb, restrict) - TriggerEvent("skinchanger:getSkin", function(skin) - lastSkin = skin - end) - - OpenMenu(function(data, menu) - menu.close() - DeleteSkinCam() - - TriggerEvent("skinchanger:getSkin", function(skin) - TriggerServerEvent("esx_skin:save", skin) - - if submitCb ~= nil then - submitCb(data, menu) - end - end) - end, cancelCb, restrict) + local posToLook = { + x = coords.x + (self.zoomOffset * thetaToLook.x), + y = coords.y + (self.zoomOffset * thetaToLook.y), + } + return pos, posToLook end AddEventHandler("esx_skin:resetFirstSpawn", function() - firstSpawn = true + Skin.firstSpawn = true ESX.PlayerLoaded = false end) @@ -264,57 +55,46 @@ AddEventHandler("esx_skin:playerRegistered", function() Wait(100) end - if firstSpawn then + if Skin.firstSpawn then ESX.TriggerServerCallback("esx_skin:getPlayerSkin", function(skin) if skin == nil then - TriggerEvent("skinchanger:loadSkin", { sex = 0 }, OpenSaveableMenu) - Wait(100) + exports["skinchanger"]:loadSkin({ sex = 0 }) + Menu:Saveable() else - TriggerEvent("skinchanger:loadSkin", skin) - Wait(100) + exports["skinchanger"]:loadSkin(skin) end end) - firstSpawn = false + Skin.firstSpawn = false end end) end) -RegisterNetEvent("esx:playerLoaded") -AddEventHandler("esx:playerLoaded", function() +RegisterNetEvent("esx:playerLoaded", function(_, _, skin) ESX.PlayerLoaded = true + TriggerServerEvent("esx_skin:setWeight", skin) +end) + +RegisterNetEvent("esx_skin:openMenu", function(submitCb, cancelCb) + Menu:Open(submitCb, cancelCb, nil) +end) + +RegisterNetEvent("esx_skin:openRestrictedMenu", function(submitCb, cancelCb, restrict) + Menu:Open(submitCb, cancelCb, nil) +end) + +RegisterNetEvent("esx_skin:openSaveableMenu", function(submitCb, cancelCb) + Menu:Saveable(submitCb, cancelCb, nil) +end) + +RegisterNetEvent("esx_skin:openSaveableRestrictedMenu", function(submitCb, cancelCb, restrict) + Menu:Saveable(submitCb, cancelCb, restrict) end) AddEventHandler("esx_skin:getLastSkin", function(cb) - cb(lastSkin) + cb(Skin.Last) end) + AddEventHandler("esx_skin:setLastSkin", function(skin) - lastSkin = skin -end) - -RegisterNetEvent("esx_skin:openMenu") -AddEventHandler("esx_skin:openMenu", function(submitCb, cancelCb) - OpenMenu(submitCb, cancelCb, nil) -end) - -RegisterNetEvent("esx_skin:openRestrictedMenu") -AddEventHandler("esx_skin:openRestrictedMenu", function(submitCb, cancelCb, restrict) - OpenMenu(submitCb, cancelCb, restrict) -end) - -RegisterNetEvent("esx_skin:openSaveableMenu") -AddEventHandler("esx_skin:openSaveableMenu", function(submitCb, cancelCb) - OpenSaveableMenu(submitCb, cancelCb, nil) -end) - -RegisterNetEvent("esx_skin:openSaveableRestrictedMenu") -AddEventHandler("esx_skin:openSaveableRestrictedMenu", function(submitCb, cancelCb, restrict) - OpenSaveableMenu(submitCb, cancelCb, restrict) -end) - -RegisterNetEvent("esx_skin:requestSaveSkin") -AddEventHandler("esx_skin:requestSaveSkin", function() - TriggerEvent("skinchanger:getSkin", function(skin) - TriggerServerEvent("esx_skin:responseSaveSkin", skin) - end) -end) + Skin.Last = skin +end) \ No newline at end of file diff --git a/[core]/esx_skin/client/modules/camera.lua b/[core]/esx_skin/client/modules/camera.lua new file mode 100644 index 00000000..f1b10636 --- /dev/null +++ b/[core]/esx_skin/client/modules/camera.lua @@ -0,0 +1,84 @@ +Camera = {} +Camera._index = Camera + +function Camera:AngleLoop() + CreateThread(function() + local angle = 90 + + while self.cam do + if IsDisabledControlPressed(0, 44) then + angle = angle - 1 + elseif IsDisabledControlPressed(0, 38) then + angle = angle + 1 + end + + if angle > 360 then + angle = angle - 360 + elseif angle < 0 then + angle = angle + 360 + end + + Skin.heading = angle + 0.0 + Wait(0) + end + end) +end + +function Camera:DisableContols() + local controls = {30, 31, 32, 33, 34, 35, 25, 24} + for i=1 , #controls do + DisableControlAction(2, controls[i], true) + end +end + +function Camera:PositionLoop() + CreateThread(function() + while self.cam do + self:DisableContols() + + local ped = ESX.PlayerData.ped or PlayerPedId() + local coords = GetEntityCoords(ped) + + local pos, posToLook = Skin:CalcuatePosition(coords) + + SetCamCoord(self.cam, pos.x, pos.y, coords.z + Skin.camOffset) + PointCamAtCoord(self.cam, posToLook.x, posToLook.y, coords.z + Skin.camOffset) + Wait(0) + end + end) +end + +function Camera:StartLoops() + ESX.TextUI(Translate('use_rotate_view', "Q", "E")) + self:AngleLoop() + self:PositionLoop() +end + +function Camera:Create() + if self.cam then + return + end + + local playerPed = ESX.PlayerData.ped or PlayerPedId() + local playerCoords = GetEntityCoords(playerPed) + + self.cam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) + SetCamCoord(self.cam, playerCoords.x, playerCoords.y, playerCoords.z + 1.0) + SetCamRot(self.cam, 0.0, 0.0, 270.0, 2) + SetEntityHeading(playerPed, 0.0) + + SetCamActive(self.cam, true) + RenderScriptCams(true, true, 500, true, true) + self:StartLoops() +end + +function Camera:Destroy() + if not self.cam then + return + end + + ESX.HideUI() + RenderScriptCams(false, true, 500, true, true) + DestroyCam(self.cam, true) + self.cam = nil +end diff --git a/[core]/esx_skin/client/modules/menu.lua b/[core]/esx_skin/client/modules/menu.lua new file mode 100644 index 00000000..df606b2b --- /dev/null +++ b/[core]/esx_skin/client/modules/menu.lua @@ -0,0 +1,144 @@ +Menu = {} +Menu._index = Menu + +function Menu:ESXMenu() + ESX.UI.Menu.Open("default", GetCurrentResourceName(), "skin", { + title = TranslateCap("skin_menu"), + align = "bottom-left", + elements = self.elements, + }, function(data, menu) + self:Submit(data, menu) + end, function(data, menu) + self:Cancel(data, menu) + end, function(data, menu) + self:Change(data, menu) + end, function() + self:Close() + end) +end + +function Menu:Restrict() + local _components = {} + for i = 1, #self.components, 1 do + local found = false + + for j = 1, #self.restricted, 1 do + if self.components[i].name == self.restricted[j] then + found = true + end + end + + if found then + _components[#_components + 1] = self.components[i] + end + end +end + +function Menu:InsertElements() + local playerPed = ESX.PlayerData.ped or PlayerPedId() + + for i = 1, #self.components, 1 do + local value = self.components[i].value + local componentId = self.components[i].componentId + + if componentId == 0 then + value = GetPedPropIndex(playerPed, self.components[i].componentId) + end + + local data = self.components[i] + data.value = value + data.type = "slider" + data.max = type(data.max) == "function" and data.max() or data.max + + if not self.elements then + self.elements = {} + end + self.elements[#self.elements + 1] = data + end +end + +function Menu:Submit(data, menu) + Skin.last = exports["skinchanger"]:GetSkin() + self.submitCb(data, menu) + Camera:Destroy() +end + +function Menu:Cancel(data, menu) + menu.close() + Camera:Destroy() + TriggerEvent("skinchanger:loadSkin", Skin.last) + + if self.cancelCb then + self.cancelCb(data, menu) + end +end + +function Menu:Change(data, menu) + local skin = exports["skinchanger"]:GetSkin() + + Skin.zoomOffset = data.current.zoomOffset + Skin.camOffset = data.current.camOffset + + if skin[data.current.name] ~= data.current.value then + -- Change skin element + TriggerEvent("skinchanger:change", data.current.name, data.current.value) + + local newData = {} + + for i = 1, #self.elements, 1 do + local component = self.components[i] + newData.max = type(component.max) == "function" and component.max() or component.max + + if self.elements[i].textureof ~= nil and data.current.name == self.elements[i].textureof then + newData.value = 0 + end + + menu.update({ name = self.elements[i].name }, newData) + end + + self.elements = newData + menu.refresh() + end +end + +---@diagnostic disable-next-line: duplicate-set-field +function Menu:Close() + Camera:Destroy() +end + +function Menu:Open(submit, cancel, restrict) + self.submitCb = submit + self.cancelCb = cancel + self.restricted = restrict + Skin.last = exports["skinchanger"]:GetSkin() + + self.components, self.maxValues = exports["skinchanger"]:GetData() + + if restrict then + self.components = self:Restrict() + end + + self:InsertElements() + + self.zoomOffset = self.components[1].zoomOffset + self.camOffset = self.components[1].camOffset + Camera:Create() + + self:ESXMenu() +end + +function Menu:Saveable(submitCb, cancelCb, restrict) + Skin.last = exports["skinchanger"]:GetSkin() + + self:Open(function(data, menu) + menu.close() + Camera:Destroy() + + local skin = exports["skinchanger"]:GetSkin() + TriggerServerEvent("esx_skin:save", skin) + + if submitCb ~= nil then + submitCb(data, menu) + end + end, cancelCb, restrict) +end \ No newline at end of file diff --git a/[core]/esx_skin/fxmanifest.lua b/[core]/esx_skin/fxmanifest.lua index 0f14b54c..7e7f6e2b 100644 --- a/[core]/esx_skin/fxmanifest.lua +++ b/[core]/esx_skin/fxmanifest.lua @@ -5,21 +5,21 @@ description 'Allows players to customise their character\'s appearance' version '1.10.10' lua54 'yes' -shared_script '@es_extended/imports.lua' +shared_scripts { + '@es_extended/locale.lua', + 'locales/*.lua', + '@es_extended/imports.lua', + 'config.lua', +} server_scripts { '@oxmysql/lib/MySQL.lua', - '@es_extended/locale.lua', - 'locales/*.lua', - 'config.lua', 'server/main.lua' } client_scripts { - '@es_extended/locale.lua', - 'locales/*.lua', - 'config.lua', - 'client/main.lua' + 'client/main.lua', + 'client/modules/*.lua' } dependencies { diff --git a/[core]/esx_skin/locales/da.lua b/[core]/esx_skin/locales/da.lua index e6d8b8f8..4aee2f79 100644 --- a/[core]/esx_skin/locales/da.lua +++ b/[core]/esx_skin/locales/da.lua @@ -1,6 +1,6 @@ Locales["da"] = { ["skin_menu"] = "Udseende Menu", - ["use_rotate_view"] = "brug ~INPUT_FRONTEND_LS~ og ~INPUT_CHARACTER_WHEEL~ for at dreje kameraet.", + ["use_rotate_view"] = "brug Q og E for at dreje kameraet.", ["skin"] = "skift udseende", ["saveskin"] = "gem udseende til en fil", } diff --git a/[core]/esx_skin/locales/de.lua b/[core]/esx_skin/locales/de.lua index b40caa1d..d92041a1 100644 --- a/[core]/esx_skin/locales/de.lua +++ b/[core]/esx_skin/locales/de.lua @@ -1,6 +1,6 @@ Locales["de"] = { ["skin_menu"] = "Skin Menü", - ["use_rotate_view"] = "Drücke ~INPUT_FRONTEND_LS~ oder ~INPUT_CHARACTER_WHEEL~ um deine Ansicht zu ändern.", + ["use_rotate_view"] = "Drücke Q oder E um deine Ansicht zu ändern.", ["skin"] = "Aussehen ändern", ["saveskin"] = "Aussehen abspeichern", - } \ No newline at end of file + } diff --git a/[core]/esx_skin/locales/en.lua b/[core]/esx_skin/locales/en.lua index 5372bb93..afc9581a 100644 --- a/[core]/esx_skin/locales/en.lua +++ b/[core]/esx_skin/locales/en.lua @@ -1,6 +1,6 @@ Locales["en"] = { ["skin_menu"] = "Skin Menu", - ["use_rotate_view"] = "use ~INPUT_FRONTEND_LS~ and ~INPUT_CHARACTER_WHEEL~ to rotate the view.", + ["use_rotate_view"] = "use Q and E to rotate the view.", ["skin"] = "change skin", ["saveskin"] = "save skin to a file", } diff --git a/[core]/esx_skin/locales/es.lua b/[core]/esx_skin/locales/es.lua index 2cda4f34..735c78fe 100644 --- a/[core]/esx_skin/locales/es.lua +++ b/[core]/esx_skin/locales/es.lua @@ -1,6 +1,6 @@ Locales["es"] = { ["skin_menu"] = "Menú de apariencia", - ["use_rotate_view"] = "Utiliza ~INPUT_FRONTEND_LS~ y ~INPUT_CHARACTER_WHEEL~ para rotar la vista.", + ["use_rotate_view"] = "Utiliza Q y E para rotar la vista.", ["skin"] = "Cambiar aspecto", ["saveskin"] = "Guarda tu aspecto actual", } diff --git a/[core]/esx_skin/locales/fi.lua b/[core]/esx_skin/locales/fi.lua index b725260d..7747dcc0 100644 --- a/[core]/esx_skin/locales/fi.lua +++ b/[core]/esx_skin/locales/fi.lua @@ -1,6 +1,6 @@ Locales["fi"] = { ["skin_menu"] = "Ulkonäkö", - ["use_rotate_view"] = "Paina ~INPUT_FRONTEND_LS~ tai ~INPUT_CHARACTER_WHEEL~ liikutaaksesi kameraa.", + ["use_rotate_view"] = "Paina Q tai E liikutaaksesi kameraa.", ["skin"] = "Muokkaa ulkonäköä", ["saveskin"] = "Tallenna", } diff --git a/[core]/esx_skin/locales/fr.lua b/[core]/esx_skin/locales/fr.lua index cce0db0a..44d3d4b2 100644 --- a/[core]/esx_skin/locales/fr.lua +++ b/[core]/esx_skin/locales/fr.lua @@ -1,6 +1,6 @@ Locales["fr"] = { ["skin_menu"] = "menu de Skin", - ["use_rotate_view"] = "utilisez ~INPUT_FRONTEND_LS~ et ~INPUT_CHARACTER_WHEEL~ pour tourner la vue.", + ["use_rotate_view"] = "utilisez Q et E pour tourner la vue.", ["skin"] = "changer de skin", ["saveskin"] = "sauvegarder skin dans un fichier", } diff --git a/[core]/esx_skin/locales/he.lua b/[core]/esx_skin/locales/he.lua index 0e9f9379..6f750e07 100644 --- a/[core]/esx_skin/locales/he.lua +++ b/[core]/esx_skin/locales/he.lua @@ -1,6 +1,6 @@ Locales["he"] = { ["skin_menu"] = "תפריט עור", - ["use_rotate_view"] = "השתמש ~INPUT_FRONTEND_LS~ ו ~INPUT_CHARACTER_WHEEL~ כדי לסובב את התצוגה.", + ["use_rotate_view"] = "השתמש Q ו E כדי לסובב את התצוגה.", ["skin"] = "שנה עור", ["saveskin"] = "שמור עור לקובץ", } diff --git a/[core]/esx_skin/locales/hu.lua b/[core]/esx_skin/locales/hu.lua index c7ba9a12..a83ea999 100644 --- a/[core]/esx_skin/locales/hu.lua +++ b/[core]/esx_skin/locales/hu.lua @@ -1,6 +1,6 @@ Locales["hu"] = { ["skin_menu"] = "Kinézet Menü", - ["use_rotate_view"] = "Használd ~INPUT_FRONTEND_LS~ vagy ~INPUT_CHARACTER_WHEEL~ gombokat a forgatáshoz", + ["use_rotate_view"] = "Használd Q vagy E gombokat a forgatáshoz", ["skin"] = "kinézet változtatása", ["saveskin"] = "kinézet mentése fájlba", } diff --git a/[core]/esx_skin/locales/it.lua b/[core]/esx_skin/locales/it.lua index 4f434d42..c6605e97 100644 --- a/[core]/esx_skin/locales/it.lua +++ b/[core]/esx_skin/locales/it.lua @@ -1,6 +1,6 @@ Locales["it"] = { ["skin_menu"] = "Menu Skin", - ["use_rotate_view"] = "usa ~INPUT_FRONTEND_LS~ e ~INPUT_CHARACTER_WHEEL~ per ruotare la visuale.", + ["use_rotate_view"] = "usa Q e E per ruotare la visuale.", ["skin"] = "cambia skin", ["saveskin"] = "salvare la skin", } diff --git a/[core]/esx_skin/locales/nl.lua b/[core]/esx_skin/locales/nl.lua index 7c0a10bb..1ec2b1d2 100644 --- a/[core]/esx_skin/locales/nl.lua +++ b/[core]/esx_skin/locales/nl.lua @@ -1,6 +1,6 @@ Locales["nl"] = { ["skin_menu"] = "Kleding Menu", - ["use_rotate_view"] = "gebruik ~INPUT_FRONTEND_LS~ en ~INPUT_CHARACTER_WHEEL~ om de camera te draaien.", + ["use_rotate_view"] = "gebruik Q en E om de camera te draaien.", ["skin"] = "verander outfit", ["saveskin"] = "sla outfit op in je kledingkast.", } diff --git a/[core]/esx_skin/locales/pl.lua b/[core]/esx_skin/locales/pl.lua index a47859b4..c12fe139 100644 --- a/[core]/esx_skin/locales/pl.lua +++ b/[core]/esx_skin/locales/pl.lua @@ -1,6 +1,6 @@ Locales["pl"] = { ["skin_menu"] = "menu wyglądu", - ["use_rotate_view"] = "użyj ~INPUT_FRONTEND_LS~ i ~INPUT_CHARACTER_WHEEL~ aby obrócić ekran.", + ["use_rotate_view"] = "użyj Q i E aby obrócić ekran.", ["skin"] = "zmień wygląd", ["saveskin"] = "zapisz wygląd do pliku", } diff --git a/[core]/esx_skin/locales/pt.lua b/[core]/esx_skin/locales/pt.lua index f7916886..0109a66a 100644 --- a/[core]/esx_skin/locales/pt.lua +++ b/[core]/esx_skin/locales/pt.lua @@ -1,6 +1,6 @@ Locales["pt"] = { ["skin_menu"] = "Menu de Skin", - ["use_rotate_view"] = "Usa ~INPUT_FRONTEND_LS~ e ~INPUT_CHARACTER_WHEEL~ para rodar a câmara.", + ["use_rotate_view"] = "Usa Q e E para rodar a câmara.", ["skin"] = "alterar skin", ["saveskin"] = "salvar a skin num ficheiro", } diff --git a/[core]/esx_skin/locales/sl.lua b/[core]/esx_skin/locales/sl.lua index 3ae559f3..d2f3571c 100644 --- a/[core]/esx_skin/locales/sl.lua +++ b/[core]/esx_skin/locales/sl.lua @@ -1,6 +1,6 @@ Locales["sl"] = { ["skin_menu"] = "Oblacilni menu.", - ["use_rotate_view"] = "Pritisni ~INPUT_FRONTEND_LS~ in ~INPUT_CHARACTER_WHEEL~ da se obracas s pogledom.", + ["use_rotate_view"] = "Pritisni Q in E da se obracas s pogledom.", ["skin"] = "Zamenjaj Skin.", ["saveskin"] = "Shrani skin.", } diff --git a/[core]/esx_skin/locales/sr.lua b/[core]/esx_skin/locales/sr.lua index 5ec9324c..9d6ee01d 100644 --- a/[core]/esx_skin/locales/sr.lua +++ b/[core]/esx_skin/locales/sr.lua @@ -1,6 +1,6 @@ Locales["sr"] = { ["skin_menu"] = "Skin Meni", - ["use_rotate_view"] = "koristi ~INPUT_FRONTEND_LS~ i ~INPUT_CHARACTER_WHEEL~ da rotiras kameru.", + ["use_rotate_view"] = "koristi Q i E da rotiras kameru.", ["skin"] = "promeni skin", ["saveskin"] = "sacuvaj skin u file", } diff --git a/[core]/esx_skin/locales/sv.lua b/[core]/esx_skin/locales/sv.lua index 19519594..1e14369a 100644 --- a/[core]/esx_skin/locales/sv.lua +++ b/[core]/esx_skin/locales/sv.lua @@ -1,6 +1,6 @@ Locales["sv"] = { ["skin_menu"] = "Skin Meny", - ["use_rotate_view"] = "Använd ~INPUT_FRONTEND_LS~ och ~INPUT_CHARACTER_WHEEL~ för att rotera.", + ["use_rotate_view"] = "Använd Q och E för att rotera.", ["skin"] = "Ända utseende", ["saveskin"] = "Spara", } diff --git a/[core]/esx_skin/locales/zh-cn.lua b/[core]/esx_skin/locales/zh-cn.lua index 6eeafdaf..c0402d43 100644 --- a/[core]/esx_skin/locales/zh-cn.lua +++ b/[core]/esx_skin/locales/zh-cn.lua @@ -1,6 +1,6 @@ Locales["zh-cn"] = { ["skin_menu"] = "皮肤选单", - ["use_rotate_view"] = "使用 ~INPUT_FRONTEND_LS~ 和 ~INPUT_CHARACTER_WHEEL~ 旋转镜头视角.", + ["use_rotate_view"] = "使用 Q 和 E 旋转镜头视角.", ["skin"] = "更换皮肤数据", ["saveskin"] = "保存皮肤数据", } diff --git a/[core]/esx_skin/server/main.lua b/[core]/esx_skin/server/main.lua index 1804130e..c917d3db 100644 --- a/[core]/esx_skin/server/main.lua +++ b/[core]/esx_skin/server/main.lua @@ -1,5 +1,4 @@ -RegisterServerEvent("esx_skin:save") -AddEventHandler("esx_skin:save", function(skin) +RegisterNetEvent("esx_skin:save", function(skin) local xPlayer = ESX.GetPlayerFromId(source) if not ESX.GetConfig().OxInventory then @@ -19,8 +18,7 @@ AddEventHandler("esx_skin:save", function(skin) }) end) -RegisterServerEvent("esx_skin:setWeight") -AddEventHandler("esx_skin:setWeight", function(skin) +RegisterServerEvent("esx_skin:setWeight", function(skin) local xPlayer = ESX.GetPlayerFromId(source) if not ESX.GetConfig().OxInventory then @@ -35,28 +33,13 @@ AddEventHandler("esx_skin:setWeight", function(skin) end end) -RegisterServerEvent("esx_skin:responseSaveSkin") -AddEventHandler("esx_skin:responseSaveSkin", function(skin) - local xPlayer = ESX.GetPlayerFromId(source) - - if xPlayer.getGroup() == "admin" then - local file = io.open("resources/[esx]/esx_skin/skins.txt", "a") - - file:write(json.encode(skin) .. "\n\n") - file:flush() - file:close() - else - print(("[^2INFO^7] ^5%s^7 attempted saving skin to file"):format(xPlayer.getIdentifier())) - end -end) - ESX.RegisterServerCallback("esx_skin:getPlayerSkin", function(source, cb) local xPlayer = ESX.GetPlayerFromId(source) MySQL.query("SELECT skin FROM users WHERE identifier = @identifier", { ["@identifier"] = xPlayer.identifier, }, function(users) - local user, skin = users[1] + local user, skin = users[1], nil local jobSkin = { skin_male = xPlayer.job.skin_male, @@ -74,7 +57,3 @@ end) ESX.RegisterCommand("skin", "admin", function(xPlayer) xPlayer.triggerEvent("esx_skin:openSaveableMenu") end, false, { help = TranslateCap("skin") }) - -ESX.RegisterCommand("skinsave", "admin", function(xPlayer) - xPlayer.triggerEvent("esx_skin:requestSaveSkin") -end, false, { help = TranslateCap("saveskin") }) diff --git a/[core]/esx_textui/TextUI.lua b/[core]/esx_textui/TextUI.lua index de603ebe..756d8f24 100644 --- a/[core]/esx_textui/TextUI.lua +++ b/[core]/esx_textui/TextUI.lua @@ -29,17 +29,17 @@ RegisterNetEvent("ESX:HideUI", HideUI) if Debug then RegisterCommand("textui:error", function() ESX.TextUI("i ~r~love~s~ donuts", "error") - end) + end, false) RegisterCommand("textui:success", function() ESX.TextUI("i ~g~love~s~ donuts", "success") - end) + end, false) RegisterCommand("textui:info", function() ESX.TextUI("i ~b~love~s~ donuts", "info") - end) + end, false) RegisterCommand("textui:hide", function() ESX.HideUI() - end) + end, false) end diff --git a/[core]/skinchanger/client/main.lua b/[core]/skinchanger/client/main.lua index 276750d1..08dc2a45 100644 --- a/[core]/skinchanger/client/main.lua +++ b/[core]/skinchanger/client/main.lua @@ -1,342 +1,299 @@ -local LastSex = -1 -local LoadSkin = nil -local LoadClothes = nil -local Character = {} -local Components = Config.Components -for i = 1, #Components, 1 do - Character[Components[i].name] = Components[i].value -end +SkinChanger = {} +SkinChanger._index = SkinChanger -function LoadDefaultModel(malePed, cb) - local playerPed = PlayerPedId() - local characterModel +SkinChanger.lastSex = -1 +SkinChanger.character = {} +SkinChanger.components = Config.Components - if malePed then - characterModel = `mp_m_freemode_01` - else - characterModel = `mp_f_freemode_01` +function SkinChanger:RequestModel(model) + if type(model) == "string" then + model = joaat(model) end - RequestModel(characterModel) + RequestModel(model) - CreateThread(function() - while not HasModelLoaded(characterModel) do - RequestModel(characterModel) - Wait(0) - end + while not HasModelLoaded(model) do + Wait(200) + end - if IsModelInCdimage(characterModel) and IsModelValid(characterModel) then - SetPlayerModel(PlayerId(), characterModel) - SetPedDefaultComponentVariation(playerPed) - end - - SetModelAsNoLongerNeeded(characterModel) - - if cb ~= nil then - cb() - end - - TriggerEvent("skinchanger:modelLoaded") - end) + return model end -function GetMaxVals() - local playerPed = PlayerPedId() +function SkinChanger:ModelLoaded() + ClearPedProp(PlayerPedId(), 0) - local data = { - sex = 1, - mom = 45, -- numbers 21-41 and 45 are female (22 total) - dad = 44, -- numbers 0-20 and 42-44 are male (24 total) - face_md_weight = 100, - skin_md_weight = 100, - nose_1 = 10, - nose_2 = 10, - nose_3 = 10, - nose_4 = 10, - nose_5 = 10, - nose_6 = 10, - cheeks_1 = 10, - cheeks_2 = 10, - cheeks_3 = 10, - lip_thickness = 10, - jaw_1 = 10, - jaw_2 = 10, - chin_1 = 10, - chin_2 = 10, - chin_3 = 10, - chin_4 = 10, - neck_thickness = 10, - age_1 = GetPedHeadOverlayNum(3) - 1, - age_2 = 10, - beard_1 = GetPedHeadOverlayNum(1) - 1, - beard_2 = 10, - beard_3 = GetNumHairColors() - 1, - beard_4 = GetNumHairColors() - 1, - hair_1 = GetNumberOfPedDrawableVariations(playerPed, 2) - 1, - hair_2 = GetNumberOfPedTextureVariations(playerPed, 2, Character["hair_1"]) - 1, - hair_color_1 = GetNumHairColors() - 1, - hair_color_2 = GetNumHairColors() - 1, - eye_color = 31, - eye_squint = 10, - eyebrows_1 = GetPedHeadOverlayNum(2) - 1, - eyebrows_2 = 10, - eyebrows_3 = GetNumHairColors() - 1, - eyebrows_4 = GetNumHairColors() - 1, - eyebrows_5 = 10, - eyebrows_6 = 10, - makeup_1 = GetPedHeadOverlayNum(4) - 1, - makeup_2 = 10, - makeup_3 = GetNumHairColors() - 1, - makeup_4 = GetNumHairColors() - 1, - lipstick_1 = GetPedHeadOverlayNum(8) - 1, - lipstick_2 = 10, - lipstick_3 = GetNumHairColors() - 1, - lipstick_4 = GetNumHairColors() - 1, - blemishes_1 = GetPedHeadOverlayNum(0) - 1, - blemishes_2 = 10, - blush_1 = GetPedHeadOverlayNum(5) - 1, - blush_2 = 10, - blush_3 = GetNumHairColors() - 1, - complexion_1 = GetPedHeadOverlayNum(6) - 1, - complexion_2 = 10, - sun_1 = GetPedHeadOverlayNum(7) - 1, - sun_2 = 10, - moles_1 = GetPedHeadOverlayNum(9) - 1, - moles_2 = 10, - chest_1 = GetPedHeadOverlayNum(10) - 1, - chest_2 = 10, - chest_3 = GetNumHairColors() - 1, - bodyb_1 = GetPedHeadOverlayNum(11) - 1, - bodyb_2 = 10, - bodyb_3 = GetPedHeadOverlayNum(12) - 1, - bodyb_4 = 10, - ears_1 = GetNumberOfPedPropDrawableVariations(playerPed, 2) - 1, - ears_2 = GetNumberOfPedPropTextureVariations(playerPed, 2, Character["ears_1"] - 1), - tshirt_1 = GetNumberOfPedDrawableVariations(playerPed, 8) - 1, - tshirt_2 = GetNumberOfPedTextureVariations(playerPed, 8, Character["tshirt_1"]) - 1, - torso_1 = GetNumberOfPedDrawableVariations(playerPed, 11) - 1, - torso_2 = GetNumberOfPedTextureVariations(playerPed, 11, Character["torso_1"]) - 1, - decals_1 = GetNumberOfPedDrawableVariations(playerPed, 10) - 1, - decals_2 = GetNumberOfPedTextureVariations(playerPed, 10, Character["decals_1"]) - 1, - arms = GetNumberOfPedDrawableVariations(playerPed, 3) - 1, - arms_2 = 10, - pants_1 = GetNumberOfPedDrawableVariations(playerPed, 4) - 1, - pants_2 = GetNumberOfPedTextureVariations(playerPed, 4, Character["pants_1"]) - 1, - shoes_1 = GetNumberOfPedDrawableVariations(playerPed, 6) - 1, - shoes_2 = GetNumberOfPedTextureVariations(playerPed, 6, Character["shoes_1"]) - 1, - mask_1 = GetNumberOfPedDrawableVariations(playerPed, 1) - 1, - mask_2 = GetNumberOfPedTextureVariations(playerPed, 1, Character["mask_1"]) - 1, - bproof_1 = GetNumberOfPedDrawableVariations(playerPed, 9) - 1, - bproof_2 = GetNumberOfPedTextureVariations(playerPed, 9, Character["bproof_1"]) - 1, - chain_1 = GetNumberOfPedDrawableVariations(playerPed, 7) - 1, - chain_2 = GetNumberOfPedTextureVariations(playerPed, 7, Character["chain_1"]) - 1, - bags_1 = GetNumberOfPedDrawableVariations(playerPed, 5) - 1, - bags_2 = GetNumberOfPedTextureVariations(playerPed, 5, Character["bags_1"]) - 1, - helmet_1 = GetNumberOfPedPropDrawableVariations(playerPed, 0) - 1, - helmet_2 = GetNumberOfPedPropTextureVariations(playerPed, 0, Character["helmet_1"]) - 1, - glasses_1 = GetNumberOfPedPropDrawableVariations(playerPed, 1) - 1, - glasses_2 = GetNumberOfPedPropTextureVariations(playerPed, 1, Character["glasses_1"] - 1), - watches_1 = GetNumberOfPedPropDrawableVariations(playerPed, 6) - 1, - watches_2 = GetNumberOfPedPropTextureVariations(playerPed, 6, Character["watches_1"]) - 1, - bracelets_1 = GetNumberOfPedPropDrawableVariations(playerPed, 7) - 1, - bracelets_2 = GetNumberOfPedPropTextureVariations(playerPed, 7, Character["bracelets_1"] - 1), - } + if self.loadSkin ~= nil then + self:ApplySkin(self.loadSkin) + self.loadSkin = nil + end + + if self.loadClothes ~= nil then + self:ApplySkin(self.loadClothes.playerSkin, self.loadClothes.clothesSkin) + self.loadClothes = nil + end +end + +function SkinChanger:DefaultModel(male, cb) + local playerPed = PlayerPedId() + local characterModel = male and `mp_m_freemode_01` or `mp_f_freemode_01` + + if not IsModelInCdimage(characterModel) or not IsModelValid(characterModel) then + return + end + local model = self:RequestModel(characterModel) + + SetPlayerModel(PlayerId(), model) + SetPedDefaultComponentVariation(playerPed) + + SetModelAsNoLongerNeeded(model) + + if cb then + cb() + end + + self:ModelLoaded() +end + +function SkinChanger:MaxValues() + local playerPed = PlayerPedId() + local data = {} + + for i = 1, #self.components, 1 do + local component = self.components[i] + local max = type(component.max) == "function" and component.max(playerPed, self.character) or component.max + data[component.name] = max + end return data end -function ApplySkin(skin, clothes) - local playerPed = PlayerPedId() +function SkinChanger:DefaultFromSex(sex, cb) + if sex == 0 then + self:DefaultModel(true, cb) + else + self:DefaultModel(false, cb) + end +end + +function SkinChanger:LoadSkin(skin, cb) + local sex = skin["sex"] + if sex ~= self.lastSex then + self.loadSkin = skin + self:DefaultFromSex(sex, cb) + + self.lastSex = sex + else + self:ApplySkin(skin) + + if cb ~= nil then + cb() + else + return + end + end +end + +function SkinChanger:ValidClothes(key) + local keys = {["sex"] = true, ["mom"] = true, ["dad"] = true, ["face_md_weight"] = true, ["skin_md_weight"] = true, ["nose_1"] = true, ["nose_2"] = true, ["nose_3"] = true, ["nose_4"] = true, ["nose_5"] = true, ["nose_6"] = true, ["cheeks_1"] = true, ["cheeks_2"] = true, ["cheeks_3"] = true, ["lip_thickness"] = true, ["jaw_1"] = true, ["jaw_2"] = true, ["chin_1"] = true, ["chin_2"] = true, ["chin_3"] = true, ["chin_4"] = true, ["neck_thickness"] = true, ["age_1"] = true, ["age_2"] = true, ["eye_color"] = true, ["eye_squint"] = true, ["beard_1"] = true, ["beard_2"] = true, ["beard_3"] = true, ["beard_4"] = true, ["hair_1"] = true, ["hair_2"] = true, ["hair_color_1"] = true, ["hair_color_2"] = true, ["eyebrows_1"] = true, ["eyebrows_2"] = true, ["eyebrows_3"] = true, ["eyebrows_4"] = true, ["eyebrows_5"] = true, ["eyebrows_6"] = true, ["makeup_1"] = true, ["makeup_2"] = true, ["makeup_3"] = true, ["makeup_4"] = true, ["lipstick_1"] = true, ["lipstick_2"] = true, ["lipstick_3"] = true, ["lipstick_4"] = true, ["blemishes_1"] = true, ["blemishes_2"] = true, ["blemishes_3"] = true, ["blush_1"] = true, ["blush_2"] = true, ["blush_3"] = true, ["complexion_1"] = true, ["complexion_2"] = true, ["sun_1"] = true, ["sun_2"] = true, ["moles_1"] = true, ["moles_2"] = true, ["chest_1"] = true, ["chest_2"] = true, ["chest_3"] = true, ["bodyb_1"] = true, ["bodyb_2"] = true, ["bodyb_3"] = true, ["bodyb_4"] = true} + return keys[key] ~= nil +end + +local function Normalise(weight, divison) + return (weight / divison) + 0.0 +end + +function SkinChanger:SetHead() + local face_weight = Normalise(self.character["face_md_weight"], 100) + local skin_weight = Normalise(self.character["skin_md_weight"], 100) + + if not self.character["face_g_weight"] then + self.character["face_g_weight"] = 0 + end + + local third_weight = Normalise(self.character["face_g_weight"], 100) + + if not self.character["grandparents"] then + self.character["grandparents"] = 0 + end + + SetPedHeadBlendData(self.playerPed, + self.character["mom"], self.character["dad"], self.character["grandparents"] , self.character["mom"], + self.character["dad"], self.character["grandparents"], face_weight, skin_weight, third_weight, false) +end + +function SkinChanger:SetFace() + local features = {"nose_1", "nose_2", "nose_3", "nose_4", "nose_5", "nose_6", "eyebrows_5", "eyebrows_6", "cheeks_1", "cheeks_2", "cheeks_3", "eye_squint", "lip_thickness", "jaw_1", "jaw_2", "chin_1", "chin_2", "chin_3", "chin_4", "neck_thickness"} + for i = 1, #features, 1 do + local feature = features[i] + SetPedFaceFeature(self.playerPed, i - 1, Normalise(self.character[feature], 10)) + end + SetPedEyeColor(self.playerPed, self.character["eye_color"]) -- Eyes color +end + +function SkinChanger:SetHeadOverlay() + local features = {{"age_1", "age_2"}, {"blemishes_1", "blemishes_2"}, {"beard_1", "beard_2"}, {"eyebrows_1", "eyebrows_2"}, {"makeup_1", "makeup_2"}, {"lipstick_1", "lipstick_2"}, {"blush_1", "blush_2"}, {"complexion_1", "complexion_2"}, {"sun_1", "sun_2"}, {"moles_1", "moles_2"}, {"chest_1", "chest_2"}} + for i = 1, #features, 1 do + local feature = features[i] + SetPedHeadOverlay(self.playerPed, i - 1, self.character[feature[1]], Normalise(self.character[feature[2]], 10)) + end +end + +function SkinChanger:SetHeadOverlayColour() + local features = {{"beard_3", "beard_4"}, {"eyebrows_3", "eyebrows_4"}, {"makeup_3", "makeup_4"}, {"lipstick_3", "lipstick_4"}, {"blush_3", 0}, {"chest_3", 0}} + for i = 1, #features, 1 do + local feature = features[i] + SetPedHeadOverlayColor(self.playerPed, i - 1, 1, self.character[feature[1]], self.character[feature[2]]) + end + if self.character["bodyb_1"] == -1 then + SetPedHeadOverlay(self.playerPed, 11, 255, (self.character["bodyb_2"] / 10) + 0.0) -- Body Blemishes + opacity + else + SetPedHeadOverlay(self.playerPed, 11, self.character["bodyb_1"], (self.character["bodyb_2"] / 10) + 0.0) + end + + if self.character["bodyb_3"] == -1 then + SetPedHeadOverlay(self.playerPed, 12, 255, (self.character["bodyb_4"] / 10) + 0.0) + else + SetPedHeadOverlay(self.playerPed, 12, self.character["bodyb_3"], (self.character["bodyb_4"] / 10) + 0.0) -- Blemishes 'added body effect' + opacity + end +end + +function SkinChanger:SetHair() + SetPedComponentVariation(self.playerPed, 2, self.character["hair_1"], self.character["hair_2"], 2) -- Hair + SetPedHairColor(self.playerPed, self.character["hair_color_1"], self.character["hair_color_2"]) -- Hair Color +end + +function SkinChanger:SetComponents() + local components = {{"tshirt_1", "tshirt_2", 8}, {"torso_1", "torso_2", 11}, {"decals_1", "decals_2", 10}, {"arms", "arms_2", 3}, {"pants_1", "pants_2", 4}, {"shoes_1", "shoes_2", 6}, {"mask_1", "mask_2", 1}, {"bproof_1", "bproof_2", 9}, {"chain_1", "chain_2", 7}, {"bags_1", "bags_2", 5}} + for i = 1, #components, 1 do + local component = components[i] + SetPedComponentVariation(self.playerPed, component[3], self.character[component[1]], self.character[component[2]], 2) + end +end + +function SkinChanger:SetProps() + local props = {{"helmet_1", "helmet_2", 0}, {"glasses_1", "glasses_2", 1}, {"ears_1", "ears_2", 2}, {"watches_1", "watches_2", 6}, {"bracelets_1", "bracelets_2", 7}} + for i = 1, #props, 1 do + local prop = props[i] + local propVal = self.character[prop[1]] + if propVal == -1 then + ClearPedProp(self.playerPed, prop[3]) + else + SetPedPropIndex(self.playerPed, prop[3], propVal, self.character[prop[2]], true) + end + end +end + +function SkinChanger:ApplySkin(skin, clothes) + self.playerPed = PlayerPedId() for k, v in pairs(skin) do - Character[k] = v + self.character[k] = v end if clothes ~= nil then for k, v in pairs(clothes) do - if - k ~= "sex" - and k ~= "mom" - and k ~= "dad" - and k ~= "face_md_weight" - and k ~= "skin_md_weight" - and k ~= "nose_1" - and k ~= "nose_2" - and k ~= "nose_3" - and k ~= "nose_4" - and k ~= "nose_5" - and k ~= "nose_6" - and k ~= "cheeks_1" - and k ~= "cheeks_2" - and k ~= "cheeks_3" - and k ~= "lip_thickness" - and k ~= "jaw_1" - and k ~= "jaw_2" - and k ~= "chin_1" - and k ~= "chin_2" - and k ~= "chin_3" - and k ~= "chin_4" - and k ~= "neck_thickness" - and k ~= "age_1" - and k ~= "age_2" - and k ~= "eye_color" - and k ~= "eye_squint" - and k ~= "beard_1" - and k ~= "beard_2" - and k ~= "beard_3" - and k ~= "beard_4" - and k ~= "hair_1" - and k ~= "hair_2" - and k ~= "hair_color_1" - and k ~= "hair_color_2" - and k ~= "eyebrows_1" - and k ~= "eyebrows_2" - and k ~= "eyebrows_3" - and k ~= "eyebrows_4" - and k ~= "eyebrows_5" - and k ~= "eyebrows_6" - and k ~= "makeup_1" - and k ~= "makeup_2" - and k ~= "makeup_3" - and k ~= "makeup_4" - and k ~= "lipstick_1" - and k ~= "lipstick_2" - and k ~= "lipstick_3" - and k ~= "lipstick_4" - and k ~= "blemishes_1" - and k ~= "blemishes_2" - and k ~= "blemishes_3" - and k ~= "blush_1" - and k ~= "blush_2" - and k ~= "blush_3" - and k ~= "complexion_1" - and k ~= "complexion_2" - and k ~= "sun_1" - and k ~= "sun_2" - and k ~= "moles_1" - and k ~= "moles_2" - and k ~= "chest_1" - and k ~= "chest_2" - and k ~= "chest_3" - and k ~= "bodyb_1" - and k ~= "bodyb_2" - and k ~= "bodyb_3" - and k ~= "bodyb_4" - then - Character[k] = v + if self:ValidClothes(k) then + self.character[k] = v end end end - local face_weight = (Character["face_md_weight"] / 100) + 0.0 - local skin_weight = (Character["skin_md_weight"] / 100) + 0.0 - SetPedHeadBlendData(playerPed, Character["mom"], Character["dad"], 0, Character["mom"], Character["dad"], 0, face_weight, skin_weight, 0.0, false) + self:SetHead() + self:SetHair() + self:SetFace() + self:SetHeadOverlay() + self:SetHeadOverlayColour() + self:SetComponents() + self:SetProps() + return true +end - SetPedFaceFeature(playerPed, 0, (Character["nose_1"] / 10) + 0.0) -- Nose Width - SetPedFaceFeature(playerPed, 1, (Character["nose_2"] / 10) + 0.0) -- Nose Peak Height - SetPedFaceFeature(playerPed, 2, (Character["nose_3"] / 10) + 0.0) -- Nose Peak Length - SetPedFaceFeature(playerPed, 3, (Character["nose_4"] / 10) + 0.0) -- Nose Bone Height - SetPedFaceFeature(playerPed, 4, (Character["nose_5"] / 10) + 0.0) -- Nose Peak Lowering - SetPedFaceFeature(playerPed, 5, (Character["nose_6"] / 10) + 0.0) -- Nose Bone Twist - SetPedFaceFeature(playerPed, 6, (Character["eyebrows_5"] / 10) + 0.0) -- Eyebrow height - SetPedFaceFeature(playerPed, 7, (Character["eyebrows_6"] / 10) + 0.0) -- Eyebrow depth - SetPedFaceFeature(playerPed, 8, (Character["cheeks_1"] / 10) + 0.0) -- Cheekbones Height - SetPedFaceFeature(playerPed, 9, (Character["cheeks_2"] / 10) + 0.0) -- Cheekbones Width - SetPedFaceFeature(playerPed, 10, (Character["cheeks_3"] / 10) + 0.0) -- Cheeks Width - SetPedFaceFeature(playerPed, 11, (Character["eye_squint"] / 10) + 0.0) -- Eyes squint - SetPedFaceFeature(playerPed, 12, (Character["lip_thickness"] / 10) + 0.0) -- Lip Fullness - SetPedFaceFeature(playerPed, 13, (Character["jaw_1"] / 10) + 0.0) -- Jaw Bone Width - SetPedFaceFeature(playerPed, 14, (Character["jaw_2"] / 10) + 0.0) -- Jaw Bone Length - SetPedFaceFeature(playerPed, 15, (Character["chin_1"] / 10) + 0.0) -- Chin Height - SetPedFaceFeature(playerPed, 16, (Character["chin_2"] / 10) + 0.0) -- Chin Length - SetPedFaceFeature(playerPed, 17, (Character["chin_3"] / 10) + 0.0) -- Chin Width - SetPedFaceFeature(playerPed, 18, (Character["chin_4"] / 10) + 0.0) -- Chin Hole Size - SetPedFaceFeature(playerPed, 19, (Character["neck_thickness"] / 10) + 0.0) -- Neck Thickness +function SkinChanger:LoadClothes(playerSkin, clothesSkin) + local sex = playerSkin["sex"] + if sex ~= self.lastSex then + self.loadClothes = { + playerSkin = playerSkin, + clothesSkin = clothesSkin, + } - SetPedHairColor(playerPed, Character["hair_color_1"], Character["hair_color_2"]) -- Hair Color - SetPedHeadOverlay(playerPed, 3, Character["age_1"], (Character["age_2"] / 10) + 0.0) -- Age + opacity - SetPedHeadOverlay(playerPed, 0, Character["blemishes_1"], (Character["blemishes_2"] / 10) + 0.0) -- Blemishes + opacity - SetPedHeadOverlay(playerPed, 1, Character["beard_1"], (Character["beard_2"] / 10) + 0.0) -- Beard + opacity - SetPedEyeColor(playerPed, Character["eye_color"]) -- Eyes color - SetPedHeadOverlay(playerPed, 2, Character["eyebrows_1"], (Character["eyebrows_2"] / 10) + 0.0) -- Eyebrows + opacity - SetPedHeadOverlay(playerPed, 4, Character["makeup_1"], (Character["makeup_2"] / 10) + 0.0) -- Makeup + opacity - SetPedHeadOverlay(playerPed, 8, Character["lipstick_1"], (Character["lipstick_2"] / 10) + 0.0) -- Lipstick + opacity - SetPedComponentVariation(playerPed, 2, Character["hair_1"], Character["hair_2"], 2) -- Hair - SetPedHeadOverlayColor(playerPed, 1, 1, Character["beard_3"], Character["beard_4"]) -- Beard Color - SetPedHeadOverlayColor(playerPed, 2, 1, Character["eyebrows_3"], Character["eyebrows_4"]) -- Eyebrows Color - SetPedHeadOverlayColor(playerPed, 4, 2, Character["makeup_3"], Character["makeup_4"]) -- Makeup Color - SetPedHeadOverlayColor(playerPed, 8, 1, Character["lipstick_3"], Character["lipstick_4"]) -- Lipstick Color - SetPedHeadOverlay(playerPed, 5, Character["blush_1"], (Character["blush_2"] / 10) + 0.0) -- Blush + opacity - SetPedHeadOverlayColor(playerPed, 5, 2, Character["blush_3"]) -- Blush Color - SetPedHeadOverlay(playerPed, 6, Character["complexion_1"], (Character["complexion_2"] / 10) + 0.0) -- Complexion + opacity - SetPedHeadOverlay(playerPed, 7, Character["sun_1"], (Character["sun_2"] / 10) + 0.0) -- Sun Damage + opacity - SetPedHeadOverlay(playerPed, 9, Character["moles_1"], (Character["moles_2"] / 10) + 0.0) -- Moles/Freckles + opacity - SetPedHeadOverlay(playerPed, 10, Character["chest_1"], (Character["chest_2"] / 10) + 0.0) -- Chest Hair + opacity - SetPedHeadOverlayColor(playerPed, 10, 1, Character["chest_3"]) -- Torso Color + self:DefaultFromSex(sex) - if Character["bodyb_1"] == -1 then - SetPedHeadOverlay(playerPed, 11, 255, (Character["bodyb_2"] / 10) + 0.0) -- Body Blemishes + opacity + self.lastSex = sex + return true else - SetPedHeadOverlay(playerPed, 11, Character["bodyb_1"], (Character["bodyb_2"] / 10) + 0.0) - end - - if Character["bodyb_3"] == -1 then - SetPedHeadOverlay(playerPed, 12, 255, (Character["bodyb_4"] / 10) + 0.0) - else - SetPedHeadOverlay(playerPed, 12, Character["bodyb_3"], (Character["bodyb_4"] / 10) + 0.0) -- Blemishes 'added body effect' + opacity - end - - if Character["ears_1"] == -1 then - ClearPedProp(playerPed, 2) - else - SetPedPropIndex(playerPed, 2, Character["ears_1"], Character["ears_2"], 2) -- Ears Accessories - end - - SetPedComponentVariation(playerPed, 8, Character["tshirt_1"], Character["tshirt_2"], 2) -- Tshirt - SetPedComponentVariation(playerPed, 11, Character["torso_1"], Character["torso_2"], 2) -- torso parts - SetPedComponentVariation(playerPed, 3, Character["arms"], Character["arms_2"], 2) -- Arms - SetPedComponentVariation(playerPed, 10, Character["decals_1"], Character["decals_2"], 2) -- decals - SetPedComponentVariation(playerPed, 4, Character["pants_1"], Character["pants_2"], 2) -- pants - SetPedComponentVariation(playerPed, 6, Character["shoes_1"], Character["shoes_2"], 2) -- shoes - SetPedComponentVariation(playerPed, 1, Character["mask_1"], Character["mask_2"], 2) -- mask - SetPedComponentVariation(playerPed, 9, Character["bproof_1"], Character["bproof_2"], 2) -- bulletproof - SetPedComponentVariation(playerPed, 7, Character["chain_1"], Character["chain_2"], 2) -- chain - SetPedComponentVariation(playerPed, 5, Character["bags_1"], Character["bags_2"], 2) -- Bag - - if Character["helmet_1"] == -1 then - ClearPedProp(playerPed, 0) - else - SetPedPropIndex(playerPed, 0, Character["helmet_1"], Character["helmet_2"], 2) -- Helmet - end - - if Character["glasses_1"] == -1 then - ClearPedProp(playerPed, 1) - else - SetPedPropIndex(playerPed, 1, Character["glasses_1"], Character["glasses_2"], 2) -- Glasses - end - - if Character["watches_1"] == -1 then - ClearPedProp(playerPed, 6) - else - SetPedPropIndex(playerPed, 6, Character["watches_1"], Character["watches_2"], 2) -- Watches - end - - if Character["bracelets_1"] == -1 then - ClearPedProp(playerPed, 7) - else - SetPedPropIndex(playerPed, 7, Character["bracelets_1"], Character["bracelets_2"], 2) -- Bracelets + return self:ApplySkin(playerSkin, clothesSkin) end end +function SkinChanger:Change(key, val) + self.character[key] = val + + if key == "sex" then + self:LoadSkin(self.character) + else + self:ApplySkin(self.character) + end +end + +function SkinChanger:Init() + for i = 1, #self.components, 1 do + local component = self.components[i] + self.character[component.name] = component.value + end +end + +--- exports + +exports("GetSkin", function() + return SkinChanger.character +end) + +exports("GetMaxVals", function() + return SkinChanger:MaxValues() +end) + +exports("LoadSkin", function(skin) + return SkinChanger:LoadSkin(skin) +end) + +exports("LoadClothes", function(playerSkin, clothesSkin) + return SkinChanger:LoadClothes(playerSkin, clothesSkin) +end) + +exports("Change", function(key, val) + return SkinChanger:Change(key, val) +end) + +--- Events + +RegisterNetEvent("skinchanger:loadSkin", function(skin, cb) + SkinChanger:LoadSkin(skin, cb) +end) + +RegisterNetEvent("skinchanger:loadClothes", function(playerSkin, clothesSkin) + SkinChanger:LoadClothes(playerSkin, clothesSkin) +end) + +AddEventHandler("skinchanger:getSkin", function(cb) + cb(SkinChanger.character) +end) + AddEventHandler("skinchanger:loadDefaultModel", function(loadMale, cb) - LoadDefaultModel(loadMale, cb) + SkinChanger:DefaultModel(loadMale, cb) +end) + +AddEventHandler("skinchanger:change", function(key, val) + SkinChanger:Change(key, val) +end) + +AddEventHandler("skinchanger:modelLoaded", function() + SkinChanger:ModelLoaded() end) AddEventHandler("skinchanger:getData", function(cb) - local components = json.decode(json.encode(Components)) - - for k, v in pairs(Character) do + local components = SkinChanger.components + for k, v in pairs(SkinChanger.character) do for i = 1, #components, 1 do if k == components[i].name then components[i].value = v @@ -344,74 +301,7 @@ AddEventHandler("skinchanger:getData", function(cb) end end - cb(components, GetMaxVals()) + cb(components, SkinChanger:MaxValues()) end) -AddEventHandler("skinchanger:change", function(key, val) - Character[key] = val - - if key == "sex" then - TriggerEvent("skinchanger:loadSkin", Character) - else - ApplySkin(Character) - end -end) - -AddEventHandler("skinchanger:getSkin", function(cb) - cb(Character) -end) - -AddEventHandler("skinchanger:modelLoaded", function() - ClearPedProp(PlayerPedId(), 0) - - if LoadSkin ~= nil then - ApplySkin(LoadSkin) - LoadSkin = nil - end - - if LoadClothes ~= nil then - ApplySkin(LoadClothes.playerSkin, LoadClothes.clothesSkin) - LoadClothes = nil - end -end) - -RegisterNetEvent("skinchanger:loadSkin") -AddEventHandler("skinchanger:loadSkin", function(skin, cb) - if skin["sex"] ~= LastSex then - LoadSkin = skin - - if skin["sex"] == 0 then - TriggerEvent("skinchanger:loadDefaultModel", true, cb) - else - TriggerEvent("skinchanger:loadDefaultModel", false, cb) - end - else - ApplySkin(skin) - - if cb ~= nil then - cb() - end - end - - LastSex = skin["sex"] -end) - -RegisterNetEvent("skinchanger:loadClothes") -AddEventHandler("skinchanger:loadClothes", function(playerSkin, clothesSkin) - if playerSkin["sex"] ~= LastSex then - LoadClothes = { - playerSkin = playerSkin, - clothesSkin = clothesSkin, - } - - if playerSkin["sex"] == 0 then - TriggerEvent("skinchanger:loadDefaultModel", true) - else - TriggerEvent("skinchanger:loadDefaultModel", false) - end - else - ApplySkin(playerSkin, clothesSkin) - end - - LastSex = playerSkin["sex"] -end) +SkinChanger:Init() \ No newline at end of file diff --git a/[core]/skinchanger/config.lua b/[core]/skinchanger/config.lua index fa6eac7a..2401b9c7 100644 --- a/[core]/skinchanger/config.lua +++ b/[core]/skinchanger/config.lua @@ -10,14 +10,16 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = 1, }, { label = TranslateCap("mom"), name = "mom", - value = 21, - min = 21, + value = 0, + min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = 45, }, { label = TranslateCap("dad"), @@ -26,6 +28,16 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = 44, + }, + { + label = TranslateCap("grandparents"), + name = "grandparents", + value = 0, + min = 0, + zoomOffset = 0.6, + camOffset = 0.65, + max = 45, }, { label = TranslateCap("resemblance"), @@ -34,6 +46,16 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = 100, + }, + { + label = TranslateCap("resemblance_g"), + name = "face_g_weight", + value = 0, + min = 0, + zoomOffset = 0.6, + camOffset = 0.65, + max = 100, }, { label = TranslateCap("skin_tone"), @@ -42,6 +64,7 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = 100, }, { label = TranslateCap("nose_1"), @@ -50,6 +73,7 @@ Config.Components = { min = -10, zoomOffset = 0.6, camOffset = 0.65, + max = 10, }, { label = TranslateCap("nose_2"), @@ -58,6 +82,7 @@ Config.Components = { min = -10, zoomOffset = 0.6, camOffset = 0.65, + max = 10, }, { label = TranslateCap("nose_3"), @@ -66,6 +91,7 @@ Config.Components = { min = -10, zoomOffset = 0.6, camOffset = 0.65, + max = 10, }, { label = TranslateCap("nose_4"), @@ -74,6 +100,7 @@ Config.Components = { min = -10, zoomOffset = 0.6, camOffset = 0.65, + max = 10, }, { label = TranslateCap("nose_5"), @@ -82,6 +109,7 @@ Config.Components = { min = -10, zoomOffset = 0.6, camOffset = 0.65, + max = 10, }, { label = TranslateCap("nose_6"), @@ -90,6 +118,7 @@ Config.Components = { min = -10, zoomOffset = 0.6, camOffset = 0.65, + max = 10, }, { label = TranslateCap("cheeks_1"), @@ -98,6 +127,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("cheeks_2"), @@ -106,6 +136,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("cheeks_3"), @@ -114,6 +145,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("lip_fullness"), @@ -122,6 +154,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("jaw_bone_width"), @@ -130,6 +163,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("jaw_bone_length"), @@ -138,6 +172,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("chin_height"), @@ -146,6 +181,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("chin_length"), @@ -154,6 +190,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("chin_width"), @@ -162,6 +199,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("chin_hole"), @@ -170,6 +208,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("neck_thickness"), @@ -178,6 +217,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("hair_1"), @@ -186,6 +226,9 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 2) - 1 + end }, { label = TranslateCap("hair_2"), @@ -194,6 +237,9 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 2, Character["hair_1"]) - 1 + end }, { label = TranslateCap("hair_color_1"), @@ -202,6 +248,9 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("hair_color_2"), @@ -210,6 +259,9 @@ Config.Components = { min = 0, zoomOffset = 0.6, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("tshirt_1"), @@ -219,6 +271,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, componentId = 8, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 8) - 1 + end }, { label = TranslateCap("tshirt_2"), @@ -228,6 +283,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, textureof = "tshirt_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 8, Character["tshirt_1"]) - 1 + end }, { label = TranslateCap("torso_1"), @@ -237,6 +295,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, componentId = 11, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 11) - 1 + end }, { label = TranslateCap("torso_2"), @@ -246,6 +307,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, textureof = "torso_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 11, Character["torso_1"]) - 1 + end }, { label = TranslateCap("decals_1"), @@ -255,6 +319,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, componentId = 10, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 10) - 1 + end }, { label = TranslateCap("decals_2"), @@ -264,6 +331,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, textureof = "decals_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 10, Character["decals_1"]) - 1 + end }, { label = TranslateCap("arms"), @@ -272,6 +342,9 @@ Config.Components = { min = 0, zoomOffset = 0.75, camOffset = 0.15, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 3) - 1 + end }, { label = TranslateCap("arms_2"), @@ -280,6 +353,7 @@ Config.Components = { min = 0, zoomOffset = 0.75, camOffset = 0.15, + max = 10, }, { label = TranslateCap("pants_1"), @@ -289,6 +363,9 @@ Config.Components = { zoomOffset = 0.8, camOffset = -0.5, componentId = 4, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 4) - 1 + end }, { label = TranslateCap("pants_2"), @@ -298,6 +375,9 @@ Config.Components = { zoomOffset = 0.8, camOffset = -0.5, textureof = "pants_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 4, Character["pants_1"]) - 1 + end }, { label = TranslateCap("shoes_1"), @@ -307,6 +387,9 @@ Config.Components = { zoomOffset = 0.8, camOffset = -0.8, componentId = 6, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 6) - 1 + end }, { label = TranslateCap("shoes_2"), @@ -316,6 +399,9 @@ Config.Components = { zoomOffset = 0.8, camOffset = -0.8, textureof = "shoes_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 6, Character["shoes_1"]) - 1 + end }, { label = TranslateCap("mask_1"), @@ -325,6 +411,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, componentId = 1, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 1) - 1 + end }, { label = TranslateCap("mask_2"), @@ -334,6 +423,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, textureof = "mask_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 1, Character["mask_1"]) - 1 + end }, { label = TranslateCap("bproof_1"), @@ -343,6 +435,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, componentId = 9, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 9) - 1 + end }, { label = TranslateCap("bproof_2"), @@ -352,6 +447,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, textureof = "bproof_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 9, Character["bproof_1"]) - 1 + end }, { label = TranslateCap("chain_1"), @@ -361,6 +459,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, componentId = 7, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 7) - 1 + end }, { label = TranslateCap("chain_2"), @@ -370,6 +471,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, textureof = "chain_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 7, Character["chain_1"]) - 1 + end }, { label = TranslateCap("helmet_1"), @@ -379,6 +483,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, componentId = 0, + max = function(playerPed) + return GetNumberOfPedPropDrawableVariations(playerPed, 0) - 1 + end }, { label = TranslateCap("helmet_2"), @@ -388,6 +495,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, textureof = "helmet_1", + max = function(playerPed, Character) + return GetNumberOfPedPropTextureVariations(playerPed, 0, Character["helmet_1"]) - 1 + end }, { label = TranslateCap("glasses_1"), @@ -397,6 +507,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, componentId = 1, + max = function(playerPed) + return GetNumberOfPedPropDrawableVariations(playerPed, 1) - 1 + end }, { label = TranslateCap("glasses_2"), @@ -406,6 +519,9 @@ Config.Components = { zoomOffset = 0.6, camOffset = 0.65, textureof = "glasses_1", + max = function(playerPed, Character) + return GetNumberOfPedPropTextureVariations(playerPed, 1, Character["glasses_1"]) - 1 + end }, { label = TranslateCap("watches_1"), @@ -415,6 +531,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, componentId = 6, + max = function(playerPed) + return GetNumberOfPedPropDrawableVariations(playerPed, 6) - 1 + end }, { label = TranslateCap("watches_2"), @@ -424,6 +543,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, textureof = "watches_1", + max = function(playerPed, Character) + return GetNumberOfPedPropTextureVariations(playerPed, 6, Character["watches_1"]) - 1 + end }, { label = TranslateCap("bracelets_1"), @@ -433,6 +555,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, componentId = 7, + max = function(playerPed) + return GetNumberOfPedPropDrawableVariations(playerPed, 7) - 1 + end }, { label = TranslateCap("bracelets_2"), @@ -442,6 +567,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, textureof = "bracelets_1", + max = function(playerPed, Character) + return GetNumberOfPedPropTextureVariations(playerPed, 7, Character["bracelets_1"]) - 1 + end }, { label = TranslateCap("bag"), @@ -451,6 +579,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, componentId = 5, + max = function(playerPed) + return GetNumberOfPedDrawableVariations(playerPed, 5) - 1 + end }, { label = TranslateCap("bag_color"), @@ -460,6 +591,9 @@ Config.Components = { zoomOffset = 0.75, camOffset = 0.15, textureof = "bags_1", + max = function(playerPed, Character) + return GetNumberOfPedTextureVariations(playerPed, 5, Character["bags_1"]) - 1 + end }, { label = TranslateCap("eye_color"), @@ -468,6 +602,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 31, }, { label = TranslateCap("eye_squint"), @@ -476,6 +611,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("eyebrow_size"), @@ -484,6 +620,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("eyebrow_type"), @@ -492,6 +629,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(2) - 1 + end }, { label = TranslateCap("eyebrow_color_1"), @@ -500,6 +640,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("eyebrow_color_2"), @@ -508,6 +651,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("eyebrow_height"), @@ -516,6 +662,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("eyebrow_depth"), @@ -524,6 +671,7 @@ Config.Components = { min = -10, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("makeup_type"), @@ -532,6 +680,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(4) - 1 + end }, { label = TranslateCap("makeup_thickness"), @@ -540,6 +691,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("makeup_color_1"), @@ -548,6 +700,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("makeup_color_2"), @@ -556,6 +711,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("lipstick_type"), @@ -564,6 +722,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(8) - 1 + end }, { label = TranslateCap("lipstick_thickness"), @@ -572,6 +733,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("lipstick_color_1"), @@ -580,6 +742,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("lipstick_color_2"), @@ -588,6 +753,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("ear_accessories"), @@ -597,6 +765,9 @@ Config.Components = { zoomOffset = 0.4, camOffset = 0.65, componentId = 2, + max = function(playerPed) + return GetNumberOfPedPropDrawableVariations(playerPed, 2) - 1 + end }, { label = TranslateCap("ear_accessories_color"), @@ -606,6 +777,9 @@ Config.Components = { zoomOffset = 0.4, camOffset = 0.65, textureof = "ears_1", + max = function(playerPed, Character) + return GetNumberOfPedPropTextureVariations(playerPed, 2, Character["ears_1"]) - 1 + end }, { label = TranslateCap("chest_hair"), @@ -614,6 +788,9 @@ Config.Components = { min = 0, zoomOffset = 0.75, camOffset = 0.15, + max = function() + return GetPedHeadOverlayNum(10) - 1 + end }, { label = TranslateCap("chest_hair_1"), @@ -622,6 +799,7 @@ Config.Components = { min = 0, zoomOffset = 0.75, camOffset = 0.15, + max = 10, }, { label = TranslateCap("chest_color"), @@ -630,6 +808,9 @@ Config.Components = { min = 0, zoomOffset = 0.75, camOffset = 0.15, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("bodyb"), @@ -638,6 +819,9 @@ Config.Components = { min = -1, zoomOffset = 0.75, camOffset = 0.15, + max = function() + return GetPedHeadOverlayNum(11) - 1 + end }, { label = TranslateCap("bodyb_size"), @@ -646,6 +830,7 @@ Config.Components = { min = 0, zoomOffset = 0.75, camOffset = 0.15, + max = 10, }, { label = TranslateCap("bodyb_extra"), @@ -654,6 +839,9 @@ Config.Components = { min = -1, zoomOffset = 0.4, camOffset = 0.15, + max = function() + return GetPedHeadOverlayNum(12) - 1 + end }, { label = TranslateCap("bodyb_extra_thickness"), @@ -662,6 +850,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.15, + max = 10, }, { label = TranslateCap("wrinkles"), @@ -670,6 +859,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(3) - 1 + end }, { label = TranslateCap("wrinkle_thickness"), @@ -678,6 +870,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("blemishes"), @@ -686,6 +879,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(0) - 1 + end }, { label = TranslateCap("blemishes_size"), @@ -694,6 +890,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("blush"), @@ -702,6 +899,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(5) - 1 + end }, { label = TranslateCap("blush_1"), @@ -710,6 +910,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("blush_color"), @@ -718,6 +919,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("complexion"), @@ -726,6 +930,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(6) - 1 + end }, { label = TranslateCap("complexion_1"), @@ -734,6 +941,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("sun"), @@ -742,6 +950,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(7) - 1 + end }, { label = TranslateCap("sun_1"), @@ -750,6 +961,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("freckles"), @@ -758,6 +970,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(9) - 1 + end }, { label = TranslateCap("freckles_1"), @@ -766,6 +981,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("beard_type"), @@ -774,6 +990,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetPedHeadOverlayNum(1) - 1 + end }, { label = TranslateCap("beard_size"), @@ -782,6 +1001,7 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = 10, }, { label = TranslateCap("beard_color_1"), @@ -790,6 +1010,9 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, { label = TranslateCap("beard_color_2"), @@ -798,5 +1021,8 @@ Config.Components = { min = 0, zoomOffset = 0.4, camOffset = 0.65, + max = function() + return GetNumHairColors() - 1 + end }, } diff --git a/server.cfg b/server.cfg index 5eb2a389..afac7a91 100644 --- a/server.cfg +++ b/server.cfg @@ -23,7 +23,9 @@ sv_maxclients 10 # Allow access to features usually locked behind a FiveM patr sets sv_projectName "Unconfigured | ESX Legacy Server" sets sv_projectDesc "The official recipe of the most popular FiveM RP framework, containing Jobs, Housing, Vehicles & more!" sets tags "default, esx, esx legacy, legacy, official, roleplay" -sv_scriptHookAllowed 0 + +sv_enforceGameBuild 3095 #mp2023_02 - The Chop Shop +set resources_useSystemChat true set onesync on # Enable OneSync [REQUIRED FOR LEGACY 1.7.5 +] set mysql_connection_string "mysql://user:password@localhost/es_extended?waitForConnections=true&charset=utf8mb4" @@ -36,7 +38,6 @@ set mysql_ui true ## These resources will start by default. ensure chat ensure spawnmanager -ensure sessionmanager ensure hardcap ## Add system admins @@ -49,7 +50,6 @@ add_ace resource.es_extended command.remove_principal allow add_ace resource.es_extended command.stop allow ## Default & Standalone resources -ensure chat-theme-gtao # Cool Chat theme ensure oxmysql ## ESX Legacy