This repository has been archived on 2025-12-11. You can view files and clone it, but cannot push or open issues or pull requests.
2025-02-02 10:40:42 +01:00

623 lines
20 KiB
Lua

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)