diff --git a/[core]/es_extended/client/imports/point.lua b/[core]/es_extended/client/imports/point.lua index afe429556..5c8f48eca 100644 --- a/[core]/es_extended/client/imports/point.lua +++ b/[core]/es_extended/client/imports/point.lua @@ -32,7 +32,7 @@ function Point:constructor(properties) if self.leave then self:leave() end - if #nearby == 0 then + if next(nearby) == nil then loop = false end end) diff --git a/[core]/es_extended/client/modules/actions.lua b/[core]/es_extended/client/modules/actions.lua index de174910e..0d3649257 100644 --- a/[core]/es_extended/client/modules/actions.lua +++ b/[core]/es_extended/client/modules/actions.lua @@ -208,6 +208,10 @@ end function Actions:SlowLoop() CreateThread(function() while ESX.PlayerLoaded do + -- TrackPed first so ESX.PlayerData.ped is fresh before the vehicle/weapon + -- trackers read it. The ped handle rarely changes (spawn/death set it via their + -- own events), so folding it here at 500ms removes a dedicated per-frame thread. + self:TrackPed() self:TrackPauseMenu() self:TrackVehicle() self:TrackWeapon() @@ -216,18 +220,8 @@ function Actions:SlowLoop() end) end -function Actions:PedLoop() - CreateThread(function() - while ESX.PlayerLoaded do - self:TrackPed() - Wait(0) - end - end) -end - function Actions:Init() self:SlowLoop() - self:PedLoop() self:TrackPedCoordsOnce() end diff --git a/[core]/es_extended/client/modules/adjustments.lua b/[core]/es_extended/client/modules/adjustments.lua index 9b79a4dec..f333da55a 100644 --- a/[core]/es_extended/client/modules/adjustments.lua +++ b/[core]/es_extended/client/modules/adjustments.lua @@ -42,6 +42,12 @@ function Adjustments:HealthRegeneration() end function Adjustments:AmmoAndVehicleRewards() + -- Both natives only affect the current frame, so the per-frame loop is required when + -- either feature is on. When both are off, don't spawn an idle Wait(0) thread at all. + if not Config.DisableDisplayAmmo and not Config.DisableVehicleRewards then + return + end + CreateThread(function() while true do if Config.DisableDisplayAmmo then @@ -222,14 +228,15 @@ function Adjustments:DisableRadio() end function Adjustments:Multipliers() + local multipliers = Config.Multipliers CreateThread(function() while true do - SetPedDensityMultiplierThisFrame(Config.Multipliers.pedDensity) - SetScenarioPedDensityMultiplierThisFrame(Config.Multipliers.scenarioPedDensityInterior, Config.Multipliers.scenarioPedDensityExterior) - SetAmbientVehicleRangeMultiplierThisFrame(Config.Multipliers.ambientVehicleRange) - SetParkedVehicleDensityMultiplierThisFrame(Config.Multipliers.parkedVehicleDensity) - SetRandomVehicleDensityMultiplierThisFrame(Config.Multipliers.randomVehicleDensity) - SetVehicleDensityMultiplierThisFrame(Config.Multipliers.vehicleDensity) + SetPedDensityMultiplierThisFrame(multipliers.pedDensity) + SetScenarioPedDensityMultiplierThisFrame(multipliers.scenarioPedDensityInterior, multipliers.scenarioPedDensityExterior) + SetAmbientVehicleRangeMultiplierThisFrame(multipliers.ambientVehicleRange) + SetParkedVehicleDensityMultiplierThisFrame(multipliers.parkedVehicleDensity) + SetRandomVehicleDensityMultiplierThisFrame(multipliers.randomVehicleDensity) + SetVehicleDensityMultiplierThisFrame(multipliers.vehicleDensity) Wait(0) end end) diff --git a/[core]/es_extended/client/modules/events.lua b/[core]/es_extended/client/modules/events.lua index f59cef91c..2ef59e79e 100644 --- a/[core]/es_extended/client/modules/events.lua +++ b/[core]/es_extended/client/modules/events.lua @@ -371,41 +371,48 @@ if not Config.CustomInventory then CreateThread(function() while true do local Sleep = 1500 - local playerCoords = GetEntityCoords(ESX.PlayerData.ped) - local _, closestDistance = ESX.Game.GetClosestPlayer(playerCoords) - for pickupId, pickup in pairs(pickups) do - local distance = #(playerCoords - pickup.coords) - - if distance < 5 then - Sleep = 0 - local label = pickup.label - - if distance < 1 then - if IsControlJustReleased(0, 38) then - if IsPedOnFoot(ESX.PlayerData.ped) and (closestDistance == -1 or closestDistance > 3) and not pickup.inRange then - pickup.inRange = true - - local dict, anim = "weapons@first_person@aim_rng@generic@projectile@sticky_bomb@", "plant_floor" - ESX.Streaming.RequestAnimDict(dict) - TaskPlayAnim(ESX.PlayerData.ped, dict, anim, 8.0, 1.0, 1000, 16, 0.0, false, false, false) - RemoveAnimDict(dict) - Wait(1000) - - TriggerServerEvent("esx:onPickup", pickupId) - PlaySoundFrontend(-1, "PICK_UP", "HUD_FRONTEND_DEFAULT_SOUNDSET", false) + -- Skip everything when there are no pickups, and only resolve the closest player + -- at the moment of an actual pickup attempt instead of enumerating every player + -- on every frame while standing near a pickup. + if next(pickups) then + local playerCoords = GetEntityCoords(ESX.PlayerData.ped) + + for pickupId, pickup in pairs(pickups) do + local distance = #(playerCoords - pickup.coords) + + if distance < 5 then + Sleep = 0 + local label = pickup.label + + if distance < 1 then + if IsControlJustReleased(0, 38) then + local _, closestDistance = ESX.Game.GetClosestPlayer(playerCoords) + if IsPedOnFoot(ESX.PlayerData.ped) and (closestDistance == -1 or closestDistance > 3) and not pickup.inRange then + pickup.inRange = true + + local dict, anim = "weapons@first_person@aim_rng@generic@projectile@sticky_bomb@", "plant_floor" + ESX.Streaming.RequestAnimDict(dict) + TaskPlayAnim(ESX.PlayerData.ped, dict, anim, 8.0, 1.0, 1000, 16, 0.0, false, false, false) + RemoveAnimDict(dict) + Wait(1000) + + TriggerServerEvent("esx:onPickup", pickupId) + PlaySoundFrontend(-1, "PICK_UP", "HUD_FRONTEND_DEFAULT_SOUNDSET", false) + end end + + label = ("%s~n~%s"):format(label, TranslateCap("threw_pickup_prompt")) end - label = ("%s~n~%s"):format(label, TranslateCap("threw_pickup_prompt")) + 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 - - 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 end + Wait(Sleep) end end) diff --git a/[core]/es_extended/client/modules/points.lua b/[core]/es_extended/client/modules/points.lua index bbcaf8a82..fc70519de 100644 --- a/[core]/es_extended/client/modules/points.lua +++ b/[core]/es_extended/client/modules/points.lua @@ -27,16 +27,18 @@ end function StartPointsLoop() CreateThread(function() while true do - local coords = GetEntityCoords(ESX.PlayerData.ped) - for handle, point in pairs(points) do - if not point.hidden and #(coords - point.coords) <= point.distance then - if not point.nearby then - points[handle].nearby = true - points[handle].enter() + if next(points) ~= nil then + local coords = GetEntityCoords(ESX.PlayerData.ped) + for handle, point in pairs(points) do + if not point.hidden and #(coords - point.coords) <= point.distance then + if not point.nearby then + points[handle].nearby = true + points[handle].enter() + end + elseif point.nearby then + points[handle].nearby = false + points[handle].leave() end - elseif point.nearby then - points[handle].nearby = false - points[handle].leave() end end Wait(500) diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index 46696f93a..ae14ba6db 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -198,9 +198,12 @@ end local function updateHealthAndArmorInMetadata(xPlayer) local ped = GetPlayerPed(xPlayer.source) - xPlayer.setMeta("health", GetEntityHealth(ped)) - xPlayer.setMeta("armor", GetPedArmour(ped)) - xPlayer.setMeta("lastPlaytime", xPlayer.getPlayTime()) + -- Write directly to metadata instead of setMeta: this only runs on save, and the + -- client is the source of truth for health/armor (lastPlaytime is server-only), so + -- echoing the full metadata table back to the owning client here is pure waste. + xPlayer.metadata.health = GetEntityHealth(ped) + xPlayer.metadata.armor = GetPedArmour(ped) + xPlayer.metadata.lastPlaytime = xPlayer.getPlayTime() end ---@param xPlayer table diff --git a/[core]/es_extended/server/main.lua b/[core]/es_extended/server/main.lua index f9f272c2d..e44ff0aee 100644 --- a/[core]/es_extended/server/main.lua +++ b/[core]/es_extended/server/main.lua @@ -19,6 +19,9 @@ end loadPlayer = loadPlayer .. " FROM `users` WHERE identifier = ?" +-- Tracks players whose load is in-flight so a re-triggered join event cannot start a second load. +local playersLoading = {} + local function createESXPlayer(identifier, playerId, data) local accounts = {} @@ -72,6 +75,8 @@ end ---@param reason string ---@param cb function? local function onPlayerDropped(playerId, reason, cb) + playersLoading[playerId] = nil + local p = not cb and promise:new() local function resolve() if cb then @@ -126,6 +131,11 @@ if Config.Multichar then else RegisterNetEvent("esx:onPlayerJoined", function() local _source = source + if ESX.Players[_source] or playersLoading[_source] then + return + end + playersLoading[_source] = true + while not next(ESX.Jobs) do Wait(50) end @@ -133,6 +143,8 @@ else if not ESX.Players[_source] then onPlayerJoined(_source) end + -- Flag is cleared by loadESXPlayer on success (incl. the async new-player path) or by + -- onPlayerDropped on failure; clearing it here would reopen the race before the async load lands. end) end @@ -318,6 +330,7 @@ function loadESXPlayer(identifier, playerId, isNew) GlobalState["playerCount"] = GlobalState["playerCount"] + 1 ESX.Players[playerId] = xPlayer Core.playersByIdentifier[identifier] = xPlayer + playersLoading[playerId] = nil -- Identity if result.firstname and result.firstname ~= "" then @@ -423,6 +436,18 @@ if not Config.CustomInventory then RegisterNetEvent("esx:updateWeaponAmmo", function(weaponName, ammoCount) local xPlayer = ESX.GetPlayerFromId(source) + ammoCount = tonumber(ammoCount) + if not ammoCount then + return + end + + ammoCount = math.floor(ammoCount) + if ammoCount < 0 then + ammoCount = 0 + elseif ammoCount > 9999 then + ammoCount = 9999 + end + if xPlayer then xPlayer.updateWeaponAmmo(weaponName, ammoCount) end @@ -438,6 +463,15 @@ if not Config.CustomInventory then return end + -- itemCount is client-controlled; weapon transfers derive their own count from the loadout. + if itemType ~= "item_weapon" then + itemCount = tonumber(itemCount) + if not itemCount or itemCount < 1 then + return sourceXPlayer.showNotification(TranslateCap("imp_invalid_quantity")) + end + itemCount = math.floor(itemCount) + end + if itemType == "item_standard" then local sourceItem = sourceXPlayer.getInventoryItem(itemName) @@ -459,7 +493,12 @@ if not Config.CustomInventory then sourceXPlayer.showNotification(TranslateCap("gave_item", itemCount, sourceItem.label, targetXPlayer.name)) targetXPlayer.showNotification(TranslateCap("received_item", itemCount, sourceItem.label, sourceXPlayer.name)) elseif itemType == "item_account" then - if itemCount < 1 or sourceXPlayer.getAccount(itemName).money < itemCount then + local sourceAccount = itemName and sourceXPlayer.getAccount(itemName) + if not sourceAccount or not Config.Accounts[itemName] then + return + end + + if itemCount < 1 or sourceAccount.money < itemCount then return sourceXPlayer.showNotification(TranslateCap("imp_invalid_amount")) end @@ -623,9 +662,12 @@ if not Config.CustomInventory then return end - local count = xPlayer.getInventoryItem(itemName).count + local item = xPlayer.getInventoryItem(itemName) + if not item then + return + end - if count < 1 then + if item.count < 1 then return xPlayer.showNotification(TranslateCap("act_imp")) end @@ -705,22 +747,44 @@ ESX.RegisterServerCallback("esx:getGameBuild", function(_, cb) cb(tonumber(GetConvar("sv_enforceGameBuild", "1604"))) end) -ESX.RegisterServerCallback("esx:getOtherPlayerData", function(_, cb, target) +ESX.RegisterServerCallback("esx:getOtherPlayerData", function(source, cb, target) local xPlayer = ESX.GetPlayerFromId(target) if not xPlayer then - return + return cb(nil) + end + + -- Admins (server-side ACE) get the full record at any distance, for admin tooling. + if Core.IsPlayerAdmin(source) then + return cb({ + identifier = xPlayer.identifier, + accounts = xPlayer.getAccounts(), + inventory = xPlayer.getInventory(), + job = xPlayer.getJob(), + loadout = xPlayer.getLoadout(), + money = xPlayer.getMoney(), + position = xPlayer.getCoords(true), + metadata = xPlayer.getMeta(), + }) + end + + -- Non-admins: target is client-supplied and untrusted. Expose only a reduced, non-sensitive + -- record (no identifier/economy/inventory/metadata) and only when actually near the target, + -- bounded by Config.DistanceGetOtherPlayerData. + local sourcePed = GetPlayerPed(source) + local targetPed = GetPlayerPed(target) + if sourcePed == 0 or targetPed == 0 then + return cb(nil) + end + + if #(GetEntityCoords(sourcePed) - GetEntityCoords(targetPed)) > (Config.DistanceGetOtherPlayerData or 10.0) then + return cb(nil) end cb({ - identifier = xPlayer.identifier, - accounts = xPlayer.getAccounts(), - inventory = xPlayer.getInventory(), + playerId = target, + name = xPlayer.getName(), job = xPlayer.getJob(), - loadout = xPlayer.getLoadout(), - money = xPlayer.getMoney(), - position = xPlayer.getCoords(true), - metadata = xPlayer.getMeta(), }) end) @@ -741,9 +805,19 @@ ESX.RegisterServerCallback("esx:getPlayerNames", function(source, cb, players) end) ESX.RegisterServerCallback("esx:spawnVehicle", function(source, cb, vehData) + vehData = type(vehData) == "table" and vehData or {} + local ped = GetPlayerPed(source) - ESX.OneSync.SpawnVehicle(vehData.model or `ADDER`, vehData.coords or GetEntityCoords(ped), vehData.coords.w or 0.0, vehData.props or {}, function(id) - if vehData.warp then + local model = (type(vehData.model) == "string" or type(vehData.model) == "number") and vehData.model or `ADDER` + local coords = vehData.coords + if type(coords) ~= "table" and type(coords) ~= "vector3" and type(coords) ~= "vector4" then + coords = GetEntityCoords(ped) + end + local heading = coords.w or 0.0 + local props = type(vehData.props) == "table" and vehData.props or {} + + ESX.OneSync.SpawnVehicle(model, coords, heading, props, function(id) + if vehData.warp and id then local vehicle = NetworkGetEntityFromNetworkId(id) local timeout = 0 while GetVehiclePedIsIn(ped, false) ~= vehicle and timeout <= 15 do diff --git a/[core]/es_extended/server/modules/callback.lua b/[core]/es_extended/server/modules/callback.lua index c0fa8f731..3efbbd1b7 100644 --- a/[core]/es_extended/server/modules/callback.lua +++ b/[core]/es_extended/server/modules/callback.lua @@ -37,15 +37,16 @@ end function Callbacks:Trigger(player, event, cb, invoker, ...) self.requests[self.id] = { await = type(cb) == "boolean", - cb = cb or promise:new() + cb = cb or promise:new(), + player = player } - local table = self.requests[self.id] + local request = self.requests[self.id] TriggerClientEvent("esx:triggerClientCallback", player, event, self.id, invoker, ...) self.id += 1 - return table.cb + return request.cb end function Callbacks:ServerRecieve(player, event, requestId, invoker, ...) @@ -63,20 +64,27 @@ function Callbacks:ServerRecieve(player, event, requestId, invoker, ...) self:Execute(callback, player, returnCb, ...) end -function Callbacks:RecieveClient(requestId, invoker, ...) - self.currentId = requestId +function Callbacks:RecieveClient(player, requestId, invoker, ...) + local request = self.requests[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)) + if not request then + -- Benign (timeout/late response) or a forged id: drop quietly so a client cannot flood logs. + return end - local callback = self.requests[self.currentId] + -- A request is addressed to a single player; reject a response coming from anyone else so a + -- client cannot resolve another player's pending callback by guessing the sequential requestId. + if request.player ~= player then + return print(("[^3WARNING^7] Player ^5%s^7 tried to answer callback ^5%s^7 addressed to player ^5%s^7"):format(player, requestId, request.player)) + end + self.currentId = requestId self.requests[requestId] = nil - if callback.await then - callback.cb:resolve({ ... }) + + if request.await then + request.cb:resolve({ ... }) else - self:Execute(callback.cb, ...) + self:Execute(request.cb, ...) end end @@ -138,7 +146,7 @@ end -- ============================================= RegisterNetEvent("esx:clientCallback", function(requestId, invoker, ...) - Callbacks:RecieveClient(requestId, invoker, ...) + Callbacks:RecieveClient(source, requestId, invoker, ...) end) RegisterNetEvent("esx:triggerServerCallback", function(eventName, requestId, invoker, ...) diff --git a/[core]/esx_skin/server/main.lua b/[core]/esx_skin/server/main.lua index ff5e87b25..7a8a23073 100644 --- a/[core]/esx_skin/server/main.lua +++ b/[core]/esx_skin/server/main.lua @@ -1,12 +1,30 @@ +-- Highest configured backpack modifier: a client-supplied bags_1 can never raise max weight past it. +local MaxBackpackModifier = 0 +for _, modifier in pairs(Config.BackpackWeight) do + if type(modifier) == "number" and modifier > MaxBackpackModifier then + MaxBackpackModifier = modifier + end +end + +-- skin (and bags_1) come from the client: only accept a numeric key mapping to a configured backpack +-- and clamp the modifier to the highest configured value. +local function resolveBackpackModifier(skin) + local modifier = type(skin.bags_1) == "number" and Config.BackpackWeight[skin.bags_1] or nil + return modifier and math.min(modifier, MaxBackpackModifier) or nil +end + RegisterNetEvent("esx_skin:save", function(skin) if not skin or type(skin) ~= "table" then return end local xPlayer = ESX.Player(source) + if not xPlayer then + return + end if not ESX.GetConfig().CustomInventory then local defaultMaxWeight = ESX.GetConfig().MaxWeight - local backpackModifier = Config.BackpackWeight[skin.bags_1] + local backpackModifier = resolveBackpackModifier(skin) if backpackModifier then xPlayer.setMaxWeight(defaultMaxWeight + backpackModifier) @@ -22,11 +40,17 @@ RegisterNetEvent("esx_skin:save", function(skin) end) RegisterNetEvent("esx_skin:setWeight", function(skin) + if type(skin) ~= "table" then + return + end local xPlayer = ESX.Player(source) + if not xPlayer then + return + end if not ESX.GetConfig().CustomInventory then local defaultMaxWeight = ESX.GetConfig().MaxWeight - local backpackModifier = Config.BackpackWeight[skin.bags_1] + local backpackModifier = resolveBackpackModifier(skin) if backpackModifier then xPlayer.setMaxWeight(defaultMaxWeight + backpackModifier)