if not lib then return end if GetConvar('inventory:versioncheck', 'true') == 'true' then lib.versionCheck('overextended/ox_inventory') end local TriggerEventHooks = require 'modules.hooks.server' local db = require 'modules.mysql.server' local Items = require 'modules.items.server' local Inventory = require 'modules.inventory.server' require 'modules.crafting.server' require 'modules.shops.server' require 'modules.pefcl.server' require 'modules.bridge.server' ---@param player table ---@param data table? --- player requires source, identifier, and name --- optionally, it should contain jobs/groups, sex, and dateofbirth function server.setPlayerInventory(player, data) while not shared.ready do Wait(0) end if not data then data = db.loadPlayer(player.identifier) end local inventory = {} local totalWeight = 0 if type(data) == 'table' then local ostime = os.time() for _, v in pairs(data) do if type(v) == 'number' or not v.count or not v.slot then if server.convertInventory then inventory, totalWeight = server.convertInventory(player.source, data) break else return error(('Inventory for player.%s (%s) contains invalid data. Ensure you have converted inventories to the correct format.'):format(player.source, GetPlayerName(player.source))) end else local item = Items(v.name) if item then v.metadata = Items.CheckMetadata(v.metadata or {}, item, v.name, ostime) local weight = Inventory.SlotWeight(item, v) totalWeight = totalWeight + weight inventory[v.slot] = {name = item.name, label = item.label, weight = weight, slot = v.slot, count = v.count, description = item.description, metadata = v.metadata, stack = item.stack, close = item.close} end end end end player.source = tonumber(player.source) local inv = Inventory.Create(player.source, player.name, 'player', shared.playerslots, totalWeight, shared.playerweight, player.identifier, inventory) if inv then inv.player = server.setPlayerData(player) inv.player.ped = GetPlayerPed(player.source) if server.syncInventory then server.syncInventory(inv) end TriggerClientEvent('ox_inventory:setPlayerInventory', player.source, Inventory.Drops, inventory, totalWeight, inv.player) end end exports('setPlayerInventory', server.setPlayerInventory) AddEventHandler('ox_inventory:setPlayerInventory', server.setPlayerInventory) ---@param playerPed number ---@param coordinates vector3|vector3[] ---@param distance? number ---@return vector3|false local function getClosestStashCoords(playerPed, coordinates, distance) local playerCoords = GetEntityCoords(playerPed) if not distance then distance = 10 end if type(coordinates) == 'table' then for i = 1, #coordinates do local coords = coordinates[i] --[[@as vector3]] if #(coords - playerCoords) < distance then return coords end end return false end return #(coordinates - playerCoords) < distance and coordinates end ---@param source number ---@param invType string ---@param data? string|number|table ---@param ignoreSecurityChecks boolean? ---@return table | false | nil, table | false | nil, string? local function openInventory(source, invType, data, ignoreSecurityChecks) if Inventory.Lock then return false end local left = Inventory(source) --[[@as OxInventory]] local right, closestCoords left:closeInventory(true) Inventory.CloseAll(left, source) if invType == 'player' and data == source then data = nil end if data then local isDataTable = type(data) == 'table' if invType == 'stash' then right = Inventory(data, left, ignoreSecurityChecks) if right == false then return false end elseif isDataTable then if data.netid then if invType == 'trunk' then local entity = NetworkGetEntityFromNetworkId(data.netid) local lockStatus = entity > 0 and GetVehicleDoorLockStatus(entity) -- 0: no lock; 1: unlocked; 8: boot unlocked if lockStatus > 1 and lockStatus ~= 8 then return false, false, 'vehicle_locked' end end data.type = invType right = Inventory(data) elseif invType == 'drop' then right = Inventory(data.id) else return end elseif invType == 'policeevidence' then if ignoreSecurityChecks or server.hasGroup(left, shared.police) then right = Inventory(('evidence-%s'):format(data)) end elseif invType == 'dumpster' then ---@cast data string right = Inventory(data) if not right then local netid = tonumber(data:sub(9)) -- dumpsters do not work with entity lockdown. need to rewrite, but having to do -- distance checks to some ~7000 dumpsters and freeze the entities isn't ideal if netid and NetworkGetEntityFromNetworkId(netid) > 0 then right = Inventory.Create(data, locale('dumpster'), invType, 15, 0, 100000, false) end end elseif invType == 'container' then left.containerSlot = data --[[@as number]] data = left.items[data] if data then right = Inventory(data.metadata.container) if not right then right = Inventory.Create(data.metadata.container, data.label, invType, data.metadata.size[1], 0, data.metadata.size[2], false) end else left.containerSlot = nil end else right = Inventory(data) end if not right then return end if not ignoreSecurityChecks and right.groups and not server.hasGroup(left, right.groups) then return end local hookPayload = { source = source, inventoryId = right.id, inventoryType = right.type, } if invType == 'container' then hookPayload.slot = left.containerSlot end if isDataTable and data.netid then hookPayload.netId = data.netid end if not TriggerEventHooks('openInventory', hookPayload) then return end if left == right then return end if right.player then if right.open then return end right.coords = not ignoreSecurityChecks and GetEntityCoords(right.player.ped) or nil end if not ignoreSecurityChecks and right.coords then closestCoords = getClosestStashCoords(left.player.ped, right.coords) if not closestCoords then return end end left:openInventory(right) else left:openInventory(left) end return { id = left.id, label = left.label, type = left.type, slots = left.slots, weight = left.weight, maxWeight = left.maxWeight }, right and { id = right.id, label = right.player and '' or right.label, type = right.player and 'otherplayer' or right.type, slots = right.slots, weight = right.weight, maxWeight = right.maxWeight, items = right.items, coords = closestCoords or right.coords, distance = right.distance } end ---@param source number ---@param invType string ---@param data string|number|table lib.callback.register('ox_inventory:openInventory', function(source, invType, data) return openInventory(source, invType, data) end) ---@param netId number lib.callback.register('ox_inventory:isVehicleATrailer', function(source, netId) local entity = NetworkGetEntityFromNetworkId(netId) local retval = GetVehicleType(entity) return retval == 'trailer' end) ---@param playerId number ---@param invType string ---@param data string|number|table function server.forceOpenInventory(playerId, invType, data) local left, right = openInventory(playerId, invType, data, true) if left and right then TriggerClientEvent('ox_inventory:forceOpenInventory', playerId, left, right) return right.id end end exports('forceOpenInventory', server.forceOpenInventory) local Licenses = lib.load('data.licenses') lib.callback.register('ox_inventory:buyLicense', function(source, id) local license = Licenses[id] if not license then return end local inventory = Inventory(source) if not inventory then return end return server.buyLicense(inventory, license) end) lib.callback.register('ox_inventory:getItemCount', function(source, item, metadata, target) local inventory = target and Inventory(target) or Inventory(source) return (inventory and Inventory.GetItemCount(inventory, item, metadata, true)) end) lib.callback.register('ox_inventory:getInventory', function(source, id) local inventory = Inventory(id or source) return inventory and { id = inventory.id, label = inventory.label, type = inventory.type, slots = inventory.slots, weight = inventory.weight, maxWeight = inventory.maxWeight, owned = inventory.owner and true or false, items = inventory.items } end) RegisterNetEvent('ox_inventory:usedItemInternal', function(slot) local inventory = Inventory(source) if not inventory then return end local item = inventory.usingItem if not item or item.slot ~= slot then ---@todo DropPlayer(inventory.id, 'sussy') return end TriggerEvent('ox_inventory:usedItem', inventory.id, item.name, item.slot, next(item.metadata) and item.metadata) inventory.usingItem = nil end) ---@param source number ---@param itemName string ---@param slot number? ---@param metadata { [string]: any }? ---@return table | boolean | nil lib.callback.register('ox_inventory:useItem', function(source, itemName, slot, metadata, noAnim) local inventory = Inventory(source) --[[@as OxInventory]] if inventory.player then local item = Items(itemName) local data = item and (slot and inventory.items[slot] or Inventory.GetSlotWithItem(inventory, item.name, metadata, true)) if not data then return end slot = data.slot local durability = data.metadata.durability --[[@as number|boolean|nil]] local consume = item.consume local label = data.metadata.label or item.label if durability and consume then if durability > 100 then local ostime = os.time() if ostime > durability then Items.UpdateDurability(inventory, data, item, 0) return TriggerClientEvent('ox_lib:notify', source, { type = 'error', description = locale('no_durability', label) }) elseif consume ~= 0 and consume < 1 then local degrade = (data.metadata.degrade or item.degrade) * 60 local percentage = ((durability - ostime) * 100) / degrade if percentage < consume * 100 then return TriggerClientEvent('ox_lib:notify', source, { type = 'error', description = locale('not_enough_durability', label) }) end end elseif durability <= 0 then return TriggerClientEvent('ox_lib:notify', source, { type = 'error', description = locale('no_durability', label) }) elseif consume ~= 0 and consume < 1 and durability < consume * 100 then return TriggerClientEvent('ox_lib:notify', source, { type = 'error', description = locale('not_enough_durability', label) }) end if data.count > 1 and consume < 1 and consume > 0 and not Inventory.GetEmptySlot(inventory) then return TriggerClientEvent('ox_lib:notify', source, { type = 'error', description = locale('cannot_use', label) }) end end if item and data and data.count > 0 and data.name == item.name then data = {name=data.name, label=label, count=data.count, slot=slot, metadata=data.metadata, weight=data.weight} if item.ammo then if inventory.weapon then local weapon = inventory.items[inventory.weapon] if weapon and weapon?.metadata.durability > 0 then consume = nil end else return false end elseif item.component or item.tint then consume = 1 data.component = true elseif consume then if data.count >= consume then local result = item.cb and item.cb('usingItem', item, inventory, slot) if result == false then return end if result ~= nil then data.server = result end else return TriggerClientEvent('ox_lib:notify', source, { type = 'error', description = locale('item_not_enough', item.name) }) end elseif not item.weapon and server.UseItem then inventory.usingItem = data -- This is used to call an external useItem function, i.e. ESX.UseItem -- If an error is being thrown on item use there is no internal solution. We previously kept a list -- of usable items which led to issues when restarting resources (for obvious reasons), but config -- developers complained the inventory broke their items. Safely invoking registered item callbacks -- should resolve issues, i.e. https://github.com/esx-framework/esx-legacy/commit/9fc382bbe0f5b96ff102dace73c424a53458c96e return pcall(server.UseItem, source, data.name, data) end data.consume = consume ---@type boolean local success = lib.callback.await('ox_inventory:usingItem', source, data, noAnim) if item.weapon then inventory.weapon = success and slot or nil end if not success then return end inventory.usingItem = data if consume and consume ~= 0 and not data.component then data = inventory.items[data.slot] if not data then return end durability = consume ~= 0 and consume < 1 and data.metadata.durability --[[@as number | false]] if durability then if durability > 100 then local degrade = (data.metadata.degrade or item.degrade) * 60 durability -= degrade * consume else durability -= consume * 100 end if data.count > 1 then local emptySlot = Inventory.GetEmptySlot(inventory) if emptySlot then local newItem = Inventory.SetSlot(inventory, item, 1, table.deepclone(data.metadata), emptySlot) if newItem then Items.UpdateDurability(inventory, newItem, item, durability) end end durability = 0 else Items.UpdateDurability(inventory, data, item, durability) end if durability <= 0 then durability = false end end if not durability then Inventory.RemoveItem(inventory.id, data.name, consume < 1 and 1 or consume, nil, data.slot) else inventory.changed = true if server.syncInventory then server.syncInventory(inventory) end end if item?.cb then item.cb('usedItem', item, inventory, data.slot) end end return true end end end) local function conversionScript() shared.ready = false local file = 'setup/convert.lua' local import = LoadResourceFile(shared.resource, file) local func = load(import, ('@@%s/%s'):format(shared.resource, file)) --[[@as function]] conversionScript = func() end RegisterCommand('convertinventory', function(source, args) if source ~= 0 then return warn('This command can only be executed with the server console.') end if type(conversionScript) == 'function' then conversionScript() end local arg = args[1] local convert = arg and conversionScript[arg] if not convert then return warn('Invalid conversion argument. Valid options: esx, esxproperty') end CreateThread(convert) end, true) lib.addCommand({'additem', 'giveitem'}, { help = 'Gives an item to a player with the given id', params = { { name = 'target', type = 'playerId', help = 'The player to receive the item' }, { name = 'item', type = 'string', help = 'The name of the item' }, { name = 'count', type = 'number', help = 'The amount of the item to give', optional = true }, { name = 'type', help = 'Sets the "type" metadata to the value', optional = true }, }, restricted = 'group.admin', }, function(source, args) local item = Items(args.item) if item then local inventory = Inventory(args.target) --[[@as OxInventory]] local count = args.count or 1 local success, response = Inventory.AddItem(inventory, item.name, count, args.type and { type = tonumber(args.type) or args.type }) if not success then return Citizen.Trace(('Failed to give %sx %s to player %s (%s)'):format(count, item.name, args.target, response)) end source = Inventory(source) or { label = 'console', owner = 'console' } if server.loglevel > 0 then lib.logger(source.owner, 'admin', ('"%s" gave %sx %s to "%s"'):format(source.label, count, item.name, inventory.label)) end end end) lib.addCommand('removeitem', { help = 'Removes an item to a player with the given id', params = { { name = 'target', type = 'playerId', help = 'The player to remove the item from' }, { name = 'item', type = 'string', help = 'The name of the item' }, { name = 'count', type = 'number', help = 'The amount of the item to take' }, { name = 'type', help = 'Only remove items with a matching metadata "type"', optional = true }, }, restricted = 'group.admin', }, function(source, args) local item = Items(args.item) if item and args.count > 0 then local inventory = Inventory(args.target) --[[@as OxInventory]] local success, response = Inventory.RemoveItem(inventory, item.name, args.count, args.type and { type = tonumber(args.type) or args.type }, nil, true) if not success then return Citizen.Trace(('Failed to remove %sx %s from player %s (%s)'):format(args.count, item.name, args.target, response)) end source = Inventory(source) or {label = 'console', owner = 'console'} if server.loglevel > 0 then lib.logger(source.owner, 'admin', ('"%s" removed %sx %s from "%s"'):format(source.label, args.count, item.name, inventory.label)) end end end) lib.addCommand('setitem', { help = 'Sets the item count for a player, removing or adding as needed', params = { { name = 'target', type = 'playerId', help = 'The player to set the items for' }, { name = 'item', type = 'string', help = 'The name of the item' }, { name = 'count', type = 'number', help = 'The amount of items to set', optional = true }, { name = 'type', help = 'Add or remove items with the metadata "type"', optional = true }, }, restricted = 'group.admin', }, function(source, args) local item = Items(args.item) if item then local inventory = Inventory(args.target) --[[@as OxInventory]] local success, response = Inventory.SetItem(inventory, item.name, args.count or 0, args.type and { type = tonumber(args.type) or args.type }) if not success then return Citizen.Trace(('Failed to set %s count to %sx for player %s (%s)'):format(item.name, args.count, args.target, response)) end source = Inventory(source) or {label = 'console', owner = 'console'} if server.loglevel > 0 then lib.logger(source.owner, 'admin', ('"%s" set "%s" %s count to %sx'):format(source.label, inventory.label, item.name, args.count)) end end end) lib.addCommand('clearevidence', { help = 'Clears a police evidence locker with the given id', params = { { name = 'locker', type = 'number', help = 'The locker id to clear' }, }, }, function(source, args) if not server.isPlayerBoss then return end local inventory = Inventory(source) local group, grade = server.hasGroup(inventory, shared.police) local hasPermission = group and server.isPlayerBoss(source, group, grade) if hasPermission then MySQL.query('DELETE FROM ox_inventory WHERE name = ?', {('evidence-%s'):format(args.locker)}) end end) lib.addCommand('takeinv', { help = 'Confiscates the target inventory, to restore with /restoreinv', params = { { name = 'target', type = 'playerId', help = 'The player to confiscate items from' }, }, restricted = 'group.admin', }, function(source, args) Inventory.Confiscate(args.target) end) lib.addCommand({'restoreinv', 'returninv'}, { help = 'Restores a previously confiscated inventory for the target', params = { { name = 'target', type = 'playerId', help = 'The player to restore items to' }, }, restricted = 'group.admin', }, function(source, args) Inventory.Return(args.target) end) lib.addCommand('clearinv', { help = 'Wipes all items from the target inventory', params = { { name = 'invId', help = 'The inventory to wipe items from' }, }, restricted = 'group.admin', }, function(source, args) Inventory.Clear(tonumber(args.invId) or args.invId == 'me' and source or args.invId) end) lib.addCommand('saveinv', { help = 'Save all pending inventory changes to the database', params = { { name = 'lock', help = 'Lock inventory access, until restart or saved without a lock', optional = true }, }, restricted = 'group.admin', }, function(source, args) Inventory.SaveInventories(args.lock == 'true', false) end) lib.addCommand('viewinv', { help = 'Inspect the target inventory without allowing interactions', params = { { name = 'invId', help = 'The inventory to inspect' }, }, restricted = 'group.admin', }, function(source, args) Inventory.InspectInventory(source, tonumber(args.invId) or args.invId) end)