|
翻译一下
if not lib then return end
local Inventory = {}
---@class OxInventoryProperties
---@field id any trust me it's less annoying this way
---@field dbId string|number
---@field label string
---@field type string
---@field slots number
---@field weight number
---@field maxWeight number
---@field open? number|false
---@field items table<number, SlotWithItem?>
---@field set function
---@field get function
---@field minimal function
---@field time number
---@field owner? string|number|boolean
---@field groups? table<string, number>
---@field coords? vector3
---@field datastore? boolean
---@field changed? boolean
---@field weapon? number
---@field containerSlot? number
---@field player? { source: number, ped: number, groups: table, name?: string, sex?: string, dateofbirth?: string }
---@field netid? number
---@field distance? number
---@field openedBy { [number]: true }
---@field currentShop? string
---@alias inventory OxInventory | table | string | number
---@class OxInventory : OxInventoryProperties
local OxInventory = {}
OxInventory.__index = OxInventory
---Open a player's inventory, optionally with a secondary inventory.
---@param inv? inventory
function OxInventory:openInventory(inv)
if not self?.player then return end
inv = Inventory(inv)
if not inv then return end
inv:set('open', true)
inv.openedBy[self.id] = true
self.open = inv.id
TriggerEvent('ox_inventory:openedInventory', self.id, inv.id)
end
---Close a player's inventory.
---@param noEvent? boolean
function OxInventory:closeInventory(noEvent)
if not self.player then return end
local inv = self.open and Inventory(self.open)
if not inv then return end
inv.openedBy[self.id] = nil
inv:set('open', false)
self.open = false
self.currentShop = nil
self.containerSlot = nil
if not noEvent then
TriggerClientEvent('ox_inventory:closeInventory', self.id, true)
end
TriggerEvent('ox_inventory:closedInventory', self.id, inv.id)
end
---@alias updateSlot { item: SlotWithItem | { slot: number }, inventory: string|number }
---Sync a player's inventory state.
---@param slots updateSlot[]
---@param weight { left?: number, right?: number } | number
function OxInventory:syncSlotsWithPlayer(slots, weight)
TriggerClientEvent('ox_inventory:updateSlots', self.id, slots, weight)
end
---Sync an inventory's state with all player's accessing it.
---@param slots updateSlot[]
---@param weight { left?: number, right?: number }
---@param syncOwner? boolean
function OxInventory:syncSlotsWithClients(slots, weight, syncOwner)
for playerId in pairs(self.openedBy) do
if self.id ~= playerId then
TriggerClientEvent('ox_inventory:updateSlots', playerId, slots, weight)
end
end
if syncOwner and self.player then
TriggerClientEvent('ox_inventory:updateSlots', self.id, slots, weight)
end
end
---@type table<any, OxInventory>
local Inventories = {}
local Vehicles = data 'vehicles'
local RegisteredStashes = {}
for _, stash in pairs(data 'stashes') do
RegisteredStashes[stash.name] = {
name = stash.name,
label = stash.label,
owner = stash.owner,
slots = stash.slots,
maxWeight = stash.weight,
groups = stash.groups or stash.jobs,
coords = shared.target and stash.target?.loc or stash.coords
}
end
local GetVehicleNumberPlateText = GetVehicleNumberPlateText
---Atempts to lazily load inventory data from the database or create a new player-owned instance for "personal" stashes
---@param data table
---@param player table
---@return OxInventory | false | nil
local function loadInventoryData(data, player)
local source = source
local inventory
if not data.type and type(data.id) == 'string' then
if data.id:find('^glove') then
data.type = 'glovebox'
elseif data.id:find('^trunk') then
data.type = 'trunk'
elseif data.id:find('^evidence-') then
data.type = 'policeevidence'
end
end
if data.type == 'trunk' or data.type == 'glovebox' then
local plate = data.id:sub(6)
if server.trimplate then
plate = string.strtrim(plate)
data.id = ('%s%s'):format(data.id:sub(1, 5), plate)
end
inventory = Inventories[data.id]
if not inventory then
local entity
if data.netid then
entity = NetworkGetEntityFromNetworkId(data.netid)
if not entity then
return shared.info('Failed to load vehicle inventory data (no entity exists with given netid).')
end
else
local vehicles = GetAllVehicles()
for i = 1, #vehicles do
local vehicle = vehicles
local _plate = GetVehicleNumberPlateText(vehicle)
if _plate:find(plate) then
entity = vehicle
data.netid = NetworkGetNetworkIdFromEntity(entity)
break
end
end
if not entity then
return shared.info('Failed to load vehicle inventory data (no entity exists with given plate).')
end
end
if not source then
source = NetworkGetEntityOwner(entity)
if not source then
return shared.info('Failed to load vehicle inventory data (entity is unowned).')
end
end
local model, class = lib.callback.await('ox_inventory:getVehicleData', source, data.netid)
local storage = Vehicles[data.type].models[model] or Vehicles[data.type][class]
if Ox then
local vehicle = Ox.GetVehicle(entity)
if vehicle then
inventory = Inventory.Create(vehicle.id or vehicle.plate, plate, data.type, storage[1], 0, storage[2], false)
end
end
if not inventory then
inventory = Inventory.Create(data.id, plate, data.type, storage[1], 0, storage[2], false)
end
end
elseif data.type == 'policeevidence' then
inventory = Inventory.Create(data.id, locale('police_evidence'), data.type, 100, 0, 100000, false)
else
local stash = RegisteredStashes[data.id]
if stash then
if stash.jobs then stash.groups = stash.jobs end
if player and stash.groups and not server.hasGroup(player, stash.groups) then return end
local owner
if stash.owner then
if stash.owner == true then
owner = data.owner or player?.owner
else
owner = stash.owner
end
end
inventory = Inventories[owner and ('%s:%s'):format(stash.name, owner) or stash.name]
if not inventory then
inventory = Inventory.Create(stash.name, stash.label or stash.name, 'stash', stash.slots, 0, stash.maxWeight, owner, nil, stash.groups)
end
end
end
if data.netid then
inventory.netid = data.netid
end
return inventory or false
end
setmetatable(Inventory, {
__call = function(self, inv, player)
if not inv then
return self
elseif type(inv) == 'table' then
if inv.items then return inv end
return not inv.owner and Inventories[inv.id] or loadInventoryData(inv, player)
end
return Inventories[inv] or loadInventoryData({ id = inv }, player)
end
})
---@cast Inventory +fun(inv: inventory, player?: inventory): OxInventory|false|nil
---@param inv inventory
---@param owner? string | number
local function getInventory(inv, owner)
if not inv then return Inventory end
local type = type(inv)
if type == 'table' or type == 'number' then
return Inventory(inv)
else
return Inventory({ id = inv, owner = owner })
end
end
exports('Inventory', getInventory)
exports('GetInventory', getInventory)
---@param inv inventory
---@param owner? string | number
---@return table?
exports('GetInventoryItems', function(inv, owner)
return getInventory(inv, owner)?.items
end)
---@param inv inventory
---@param slotId number
---@return OxInventory?
function Inventory.GetContainerFromSlot(inv, slotId)
local inventory = Inventory(inv)
local slotData = inventory and inventory.items[slotId]
if not slotData then return end
local container = Inventory(slotData.metadata.container)
if not container then
container = Inventory.Create(slotData.metadata.container, slotData.label, 'container', slotData.metadata.size[1], 0, slotData.metadata.size[2], false)
end
return container
end
exports('GetContainerFromSlot', Inventory.GetContainerFromSlot)
---@param inv? inventory
---@param ignoreId? number|false
function Inventory.CloseAll(inv, ignoreId)
if not inv then
for _, data in pairs(Inventories) do
for playerId in pairs(data.openedBy) do
local playerInv = Inventory(playerId)
if playerInv then playerInv:closeInventory(true) end
end
end
return TriggerClientEvent('ox_inventory:closeInventory', -1, true)
end
inv = Inventory(inv) --[[@as OxInventory?]]
if not inv then return end
for playerId in pairs(inv.openedBy) do
local playerInv = Inventory(playerId)
if playerInv and (not ignoreId or playerId ~= ignoreId) then playerInv:closeInventory() end
end
end
---@param inv inventory
---@param k string
---@param v any
function Inventory.Set(inv, k, v)
inv = Inventory(inv) --[[@as OxInventory]]
if inv then
if type(v) == 'number' then math.floor(v + 0.5) end
if k == 'open' and v == false then
if inv.type ~= 'player' then
if inv.player then
inv.type = 'player'
elseif inv.type == 'drop' and not next(inv.items) and not next(inv.openedBy) then
return Inventory.Remove(inv)
else
inv.time = os.time()
end
end
if inv.player then
inv.containerSlot = nil
end
elseif k == 'maxWeight' and v < 10000 then
v *= 10000
end
inv[k] = v
end
end
---@param inv inventory
---@param key string
function Inventory.Get(inv, key)
inv = Inventory(inv) --[[@as OxInventory]]
if inv then
return inv[key]
end
end
---@param inv inventory
---@return table items table containing minimal inventory data
local function minimal(inv)
inv = Inventory(inv) --[[@as OxInventory]]
local inventory, count = {}, 0
for k, v in pairs(inv.items) do
if v.name and v.count > 0 then
count += 1
inventory[count] = {
name = v.name,
count = v.count,
slot = k,
metadata = next(v.metadata) and v.metadata or nil
}
end
end
return inventory
end
---@param inv inventory
---@param item table
---@param count number
---@param metadata any
---@param slot any
function Inventory.SetSlot(inv, item, count, metadata, slot)
inv = Inventory(inv) --[[@as OxInventory]]
if not inv then return end
local currentSlot = inv.items[slot]
local newCount = currentSlot and currentSlot.count + count or count
local newWeight = currentSlot and inv.weight - currentSlot.weight or inv.weight
if currentSlot and newCount < 1 then
TriggerClientEvent('ox_inventory:itemNotify', inv.id, { currentSlot, 'ui_removed', currentSlot.count })
currentSlot = nil
else
currentSlot = {name = item.name, label = item.label, weight = item.weight, slot = slot, count = newCount, description = item.description, metadata = metadata, stack = item.stack, close = item.close}
local slotWeight = Inventory.SlotWeight(item, currentSlot)
currentSlot.weight = slotWeight
newWeight += slotWeight
TriggerClientEvent('ox_inventory:itemNotify', inv.id, { currentSlot, count < 0 and 'ui_removed' or 'ui_added', math.abs(count) })
end
inv.weight = newWeight
inv.items[slot] = currentSlot
inv.changed = true
return currentSlot
end
local Items = require 'modules.items.server'
CreateThread(function()
Inventory.accounts = server.accounts
TriggerEvent('ox_inventory:loadInventory', Inventory)
end)
function Inventory.GetAccountItemCounts(inv)
inv = Inventory(inv)
if not inv then return end
local accounts = table.clone(server.accounts)
for _, v in pairs(inv.items) do
if accounts[v.name] then
accounts[v.name] += v.count
end
end
return accounts
end
---@param item table
---@param slot table
function Inventory.SlotWeight(item, slot, ignoreCount)
local weight = ignoreCount and item.weight or item.weight * (slot.count or 1)
if not slot.metadata then slot.metadata = {} end
if item.ammoname and slot.metadata.ammo then
local ammoWeight = Items(item.ammoname)?.weight
if ammoWeight then
weight += (ammoWeight * slot.metadata.ammo)
end
end
if slot.metadata.components then
for i = #slot.metadata.components, 1, -1 do
local componentWeight = Items(slot.metadata.components)?.weight
if componentWeight then
weight += componentWeight
end
end
end
if slot.metadata.weight then
weight += ignoreCount and slot.metadata.weight or (slot.metadata.weight * (slot.count or 1))
end
return weight
end
---@param items table
function Inventory.CalculateWeight(items)
local weight = 0
for _, v in pairs(items) do
local item = Items(v.name)
if item then
weight = weight + Inventory.SlotWeight(item, v)
end
end
return weight
end
-- This should be handled by frameworks, but sometimes isn't or is exploitable in some way.
local activeIdentifiers = {}
local function hasActiveInventory(playerId, owner)
local activePlayer = activeIdentifiers[owner]
if activePlayer then
local inventory = Inventory(activePlayer)
if inventory then
local endpoint = GetPlayerEndpoint(activePlayer)
if endpoint then
DropPlayer(playerId, ("Character identifier '%s' is already active."):format(owner))
-- Supposedly still getting stuck? Print info and hope somebody reports back (lol)
print(('kicked player.%s (charid is already in use)'):format(playerId), json.encode({
oldId = activePlayer,
newId = playerId,
charid = owner,
endpoint = endpoint,
playerName = GetPlayerName(activePlayer),
fivem = GetPlayerIdentifierByType(activePlayer, 'fivem'),
license = GetPlayerIdentifierByType(activePlayer, 'license2') or GetPlayerIdentifierByType(activePlayer, 'license'),
}, {
indent = true,
sort_keys = true
}))
return true
end
Inventory.CloseAll(inventory)
db.savePlayer(owner, json.encode(inventory:minimal()))
Inventory.Remove(inventory)
Wait(1000)
end
end
activeIdentifiers[owner] = playerId
end
---Manually clear an inventory state tied to the given identifier.
---Temporary workaround until somebody actually gives me info.
RegisterCommand('clearActiveIdentifier', function(source, args)
---Server console only.
if source ~= 0 then return end
local activePlayer = activeIdentifiers[args[1]] or activeIdentifiers[tonumber(args[1])]
local inventory = activePlayer and Inventory(activePlayer)
if not inventory then return end
local endpoint = GetPlayerEndpoint(activePlayer)
if endpoint then
DropPlayer(activePlayer, 'Kicked')
-- Supposedly still getting stuck? Print info and hope somebody reports back (lol)
print(('kicked player.%s (clearActiveIdentifier)'):format(activePlayer), json.encode({
oldId = activePlayer,
charid = inventory.owner,
endpoint = endpoint,
playerName = GetPlayerName(activePlayer),
fivem = GetPlayerIdentifierByType(activePlayer, 'fivem'),
license = GetPlayerIdentifierByType(activePlayer, 'license2') or GetPlayerIdentifierByType(activePlayer, 'license'),
}, {
indent = true,
sort_keys = true
}))
end
Inventory.CloseAll(inventory)
db.savePlayer(inventory.owner, json.encode(inventory:minimal()))
Inventory.Remove(inventory)
end, true)
---@param id string|number
---@param label string|nil
---@param invType string
---@param slots number
---@param weight number
---@param maxWeight number
---@param owner string | number | boolean
---@param items? table
---@return OxInventory?
--- This should only be utilised internally!
--- To create a stash, please use `exports.ox_inventory:RegisterStash` instead.
function Inventory.Create(id, label, invType, slots, weight, maxWeight, owner, items, groups)
if invType == 'player' and hasActiveInventory(id, owner) then return end
local self = {
id = id,
label = label or id,
type = invType,
slots = slots,
weight = weight,
maxWeight = maxWeight or shared.playerweight,
owner = owner,
items = type(items) == 'table' and items,
open = false,
set = Inventory.Set,
get = Inventory.Get,
minimal = minimal,
time = os.time(),
groups = groups,
openedBy = {},
}
if invType == 'drop' or invType == 'temp' then
self.datastore = true
else
self.changed = false
if invType ~= 'glovebox' and invType ~= 'trunk' then
self.dbId = id
if invType ~= 'player' and owner and type(owner) ~= 'boolean' then
self.id = ('%s:%s'):format(self.id, owner)
end
else
if Ox then
self.dbId = id
self.id = (invType == 'glovebox' and 'glove' or invType)..label
else
self.dbId = label
end
end
end
if not items then
self.items, self.weight, self.datastore = Inventory.Load(self.dbId, invType, owner)
elseif weight == 0 and next(items) then
self.weight = Inventory.CalculateWeight(items)
end
Inventories[self.id] = setmetatable(self, OxInventory)
return Inventories[self.id]
end
---@param inv inventory
function Inventory.Remove(inv)
inv = Inventory(inv) --[[@as OxInventory]]
if inv then
if inv.type == 'drop' then
TriggerClientEvent('ox_inventory:removeDrop', -1, inv.id)
Inventory.Drops[inv.id] = nil
elseif inv.player then
activeIdentifiers[inv.owner] = nil
end
Inventories[inv.id] = nil
end
end
---Update the internal reference to vehicle stashes. Does not trigger a save or update the database.
---@param oldPlate string
---@param newPlate string
function Inventory.UpdateVehicle(oldPlate, newPlate)
oldPlate = oldPlate:upper()
newPlate = newPlate:upper()
if server.trimplate then
oldPlate = string.strtrim(oldPlate)
newPlate = string.strtrim(newPlate)
end
local trunk = Inventory(('trunk%s'):format(oldPlate))
local glove = Inventory(('glove%s'):format(oldPlate))
if trunk then
Inventory.CloseAll(trunk)
Inventories[trunk.id] = nil
trunk.label = newPlate
trunk.dbId = type(trunk.id) == 'number' and trunk.dbId or newPlate
trunk.id = ('trunk%s'):format(newPlate)
Inventories[trunk.id] = trunk
end
if glove then
Inventory.CloseAll(glove)
Inventories[glove.id] = nil
glove.label = newPlate
glove.dbId = type(glove.id) == 'number' and glove.dbId or newPlate
glove.id = ('glove%s'):format(newPlate)
Inventories[glove.id] = glove
end
end
exports('UpdateVehicle', Inventory.UpdateVehicle)
function Inventory.Save(inv)
inv = Inventory(inv) --[[@as OxInventory]]
if inv then
local items = json.encode(minimal(inv))
inv.changed = false
if inv.player then
db.savePlayer(inv.owner, items)
else
if inv.type == 'trunk' then
db.saveTrunk(inv.dbId, items)
elseif inv.type == 'glovebox' then
db.saveGlovebox(inv.dbId, items)
else
db.saveStash(inv.owner, inv.dbId, items)
end
end
end
end
local function randomItem(loot, items, size)
local item = loot[math.random(1, size)]
for i = 1, #items do
if items[1] == item[1] then
return randomItem(loot, items, size)
end
end
return item
end
local function randomLoot(loot)
local items = {}
if loot then
local size = #loot
for i = 1, math.random(0, 3) do
if i > size then return items end
local item = randomItem(loot, items, size)
if math.random(1, 100) <= (item[4] or 80) then
local count = math.random(item[2], item[3])
if count > 0 then
items[#items+1] = {item[1], count}
end
end
end
end
return items
end
---@param inv inventory
---@param invType string
---@param items? table
---@return table returnData, number totalWeight, boolean true
local function generateItems(inv, invType, items)
if items == nil then
if invType == 'dumpster' then
items = randomLoot(server.dumpsterloot)
elseif invType == 'vehicle' then
items = randomLoot(server.vehicleloot)
end
end
if not items then
items = {}
end
local returnData, totalWeight = table.create(#items, 0), 0
for i = 1, #items do
local v = items
local item = Items(v[1])
if not item then
warn('unable to generate', v[1], 'item does not exist')
else
local metadata, count = Items.Metadata(inv, item, v[3] or {}, v[2])
local weight = Inventory.SlotWeight(item, {count=count, metadata=metadata})
totalWeight = totalWeight + weight
returnData = {name = item.name, label = item.label, weight = weight, slot = i, count = count, description = item.description, metadata = metadata, stack = item.stack, close = item.close}
end
end
return returnData, totalWeight, true
end
---@param id string|number
---@param invType string
---@param owner string | number | boolean
function Inventory.Load(id, invType, owner)
local datastore, result
if id and invType then
if invType == 'dumpster' then
if server.randomloot then
return generateItems(id, invType)
else
datastore = true
end
elseif invType == 'trunk' or invType == 'glovebox' then
result = invType == 'trunk' and db.loadTrunk(id) or db.loadGlovebox(id)
if not result then
if server.randomloot then
return generateItems(id, 'vehicle')
else
datastore = true
end
else result = result[invType] end
else
result = db.loadStash(owner or '', id)
end
end
local returnData, weight = {}, 0
if result and type(result) == 'string' then
result = json.decode(result)
end
if result then
local ostime = os.time()
for _, v in pairs(result) do
local item = Items(v.name)
if item then
v.metadata = Items.CheckMetadata(v.metadata or {}, item, v.name, ostime)
local slotWeight = Inventory.SlotWeight(item, v)
weight += slotWeight
returnData[v.slot] = {name = item.name, label = item.label, weight = slotWeight, slot = v.slot, count = v.count, description = item.description, metadata = v.metadata, stack = item.stack, close = item.close}
end
end
end
return returnData, weight, datastore
end
local table = lib.table
local function assertMetadata(metadata)
if metadata and type(metadata) ~= 'table' then
metadata = metadata and { type = metadata or nil }
end
return metadata
end
---@param inv inventory
---@param item table | string
---@param metadata? any
---@param returnsCount? boolean
---@return table | number | nil
function Inventory.GetItem(inv, item, metadata, returnsCount)
if type(item) ~= 'table' then item = Items(item) end
if item then
item = returnsCount and item or table.clone(item)
inv = Inventory(inv) --[[@as OxInventory]]
local count = 0
if inv then
local ostime = os.time()
metadata = assertMetadata(metadata)
for _, v in pairs(inv.items) do
if v.name == item.name and (not metadata or table.contains(v.metadata, metadata)) and not Items.UpdateDurability(inv, v, item, nil, ostime) then
count += v.count
end
end
end
if returnsCount then return count else
item.count = count
return item
end
end
end
exports('GetItem', Inventory.GetItem)
---@param fromInventory any
---@param toInventory any
---@param slot1 number
---@param slot2 number
function Inventory.SwapSlots(fromInventory, toInventory, slot1, slot2)
local fromSlot = fromInventory.items[slot1] and table.clone(fromInventory.items[slot1]) or nil
local toSlot = toInventory.items[slot2] and table.clone(toInventory.items[slot2]) or nil
if fromSlot then fromSlot.slot = slot2 end
if toSlot then toSlot.slot = slot1 end
fromInventory.items[slot1], toInventory.items[slot2] = toSlot, fromSlot
fromInventory.changed, toInventory.changed = true, true
return fromSlot, toSlot
end
exports('SwapSlots', Inventory.SwapSlots)
function Inventory.ContainerWeight(container, metaWeight, playerInventory)
playerInventory.weight -= container.weight
container.weight = Items(container.name).weight
container.weight += metaWeight
container.metadata.weight = metaWeight
playerInventory.weight += container.weight
end
---@param inv inventory
---@param item table | string
---@param count number
---@param metadata? table
function Inventory.SetItem(inv, item, count, metadata)
if type(item) ~= 'table' then item = Items(item) end
if item and count >= 0 then
inv = Inventory(inv) --[[@as OxInventory]]
if inv then
inv.changed = true
local itemCount = Inventory.GetItem(inv, item.name, metadata, true) --[[@as number]]
if count > itemCount then
count -= itemCount
return Inventory.AddItem(inv, item.name, count, metadata)
elseif count <= itemCount then
itemCount -= count
return Inventory.RemoveItem(inv, item.name, itemCount, metadata)
end
end
end
end
exports('SetItem', Inventory.SetItem)
---@param inv inventory
function Inventory.GetCurrentWeapon(inv)
inv = Inventory(inv) --[[@as OxInventory]]
if inv?.player then
local weapon = inv.items[inv.weapon]
if weapon and Items(weapon.name).weapon then
return weapon
end
inv.weapon = nil
end
end
exports('GetCurrentWeapon', Inventory.GetCurrentWeapon)
---@param inv inventory
---@param slotId number
---@return table? item
function Inventory.GetSlot(inv, slotId)
inv = Inventory(inv) --[[@as OxInventory]]
local slot = inv and inv.items[slotId]
if slot and not Items.UpdateDurability(inv, slot, Items(slot.name), nil, os.time()) then
return slot
end
end
exports('GetSlot', Inventory.GetSlot)
---@param inv inventory
---@param slotId number
function Inventory.SetDurability(inv, slotId, durability)
inv = Inventory(inv) --[[@as OxInventory]]
local slot = inv and inv.items[slotId]
if not slot then return end
Items.UpdateDurability(inv, slot, Items(slot.name), durability)
if inv.player and server.syncInventory then
server.syncInventory(inv)
end
end
exports('SetDurability', Inventory.SetDurability)
local Utils = require 'modules.utils.server'
---@param inv inventory
---@param slotId number
---@param metadata { [string]: any }
function Inventory.SetMetadata(inv, slotId, metadata)
inv = Inventory(inv) --[[@as OxInventory]]
local slot = inv and inv.items[slotId]
if not slot then return end
local item = Items(slot.name)
local imageurl = slot.metadata.imageurl
slot.metadata = type(metadata) == 'table' and metadata or { type = metadata or nil }
inv.changed = true
if metadata.weight then
inv.weight -= slot.weight
slot.weight = Inventory.SlotWeight(item, slot)
inv.weight += slot.weight
end
if metadata.durability ~= slot.metadata.durability then
Items.UpdateDurability(inv, slot, item, metadata.durability)
else
inv:syncSlotsWithClients({
{
item = slot,
inventory = inv.id
}
}, { left = inv.weight }, true)
end
if inv.player and server.syncInventory then
server.syncInventory(inv)
end
if metadata.imageurl ~= imageurl and Utils.IsValidImageUrl then
if Utils.IsValidImageUrl(metadata.imageurl) then
Utils.DiscordEmbed('Valid image URL', ('Updated item "%s" (%s) with valid url in "%s".\n%s\nid: %s\nowner: %s'):format(metadata.label or slot.label, slot.name, inv.label, metadata.imageurl, inv.id, inv.owner, metadata.imageurl), metadata.imageurl, 65280)
else
Utils.DiscordEmbed('Invalid image URL', ('Updated item "%s" (%s) with invalid url in "%s".\n%s\nid: %s\nowner: %s'):format(metadata.label or slot.label, slot.name, inv.label, metadata.imageurl, inv.id, inv.owner, metadata.imageurl), metadata.imageurl, 16711680)
metadata.imageurl = nil
end
end
end
exports('SetMetadata', Inventory.SetMetadata)
---@param inv inventory
---@param slots number
function Inventory.SetSlotCount(inv, slots)
inv = Inventory(inv) --[[@as OxInventory]]
if not inv then return end
if type(slots) ~= 'number' then return end
inv.changed = true
inv.slots = slots
end
exports('SetSlotCount', Inventory.SetSlotCount)
---@param inv inventory
---@param maxWeight number
function Inventory.SetMaxWeight(inv, maxWeight)
inv = Inventory(inv) --[[@as OxInventory]]
if not inv then return end
if type(maxWeight) ~= 'number' then return end
inv.maxWeight = maxWeight
if inv.player then
TriggerClientEvent('ox_inventory:refreshMaxWeight', inv.id, {inventoryId = inv.id, maxWeight = inv.maxWeight})
end
for playerId in pairs(inv.openedBy) do
if playerId ~= inv.id then
TriggerClientEvent('ox_inventory:refreshMaxWeight', playerId, {inventoryId = inv.id, maxWeight = inv.maxWeight})
end
end
end
exports('SetMaxWeight', Inventory.SetMaxWeight)
---@param inv inventory
---@param item table | string
---@param count number
---@param metadata? table | string
---@param slot? number
---@param cb? fun(success?: boolean, response: string|SlotWithItem|nil)
---@return boolean? success, string|SlotWithItem|nil response
function Inventory.AddItem(inv, item, count, metadata, slot, cb)
if type(item) ~= 'table' then item = Items(item) end
if not item then return false, 'invalid_item' end
inv = Inventory(inv) --[[@as OxInventory]]
if not inv?.slots then return false, 'invalid_inventory' end
local toSlot, slotMetadata, slotCount
local success, response = false
count = math.floor(count + 0.5)
metadata = assertMetadata(metadata)
if slot then
local slotData = inv.items[slot]
slotMetadata, slotCount = Items.Metadata(inv.id, item, metadata and table.clone(metadata) or {}, count)
if not slotData or (item.stack and slotData.name == item.name and table.matches(slotData.metadata, slotMetadata)) then
toSlot = slot
end
end
if not toSlot then
local items = inv.items
slotMetadata, slotCount = Items.Metadata(inv.id, item, metadata and table.clone(metadata) or {}, count)
for i = 1, inv.slots do
local slotData = items
if item.stack and slotData ~= nil and slotData.name == item.name and table.matches(slotData.metadata, slotMetadata) then
toSlot = i
break
elseif not item.stack and not slotData then
if not toSlot then toSlot = {} end
toSlot[#toSlot + 1] = { slot = i, count = slotCount, metadata = slotMetadata }
if count == slotCount then
break
end
count -= 1
slotMetadata, slotCount = Items.Metadata(inv.id, item, metadata and table.clone(metadata) or {}, count)
elseif not toSlot and not slotData then
toSlot = i
end
end
end
if not toSlot then return false, 'inventory_full' end
inv.changed = true
local invokingResource = server.loglevel > 1 and GetInvokingResource()
local toSlotType = type(toSlot)
if toSlotType == 'number' then
Inventory.SetSlot(inv, item, slotCount, slotMetadata, toSlot)
if inv.player and server.syncInventory then
server.syncInventory(inv)
end
inv:syncSlotsWithClients({
{
item = inv.items[toSlot],
inventory = inv.id
}
},
{
left = inv.weight,
right = inv.open and Inventories[inv.open]?.weight or nil
}, true)
if invokingResource then
lib.logger(inv.owner, 'addItem', ('"%s" added %sx %s to "%s"'):format(invokingResource, count, item.name, inv.label))
end
success = true
response = inv.items[toSlot]
elseif toSlotType == 'table' then
local added = 0
for i = 1, #toSlot do
local data = toSlot
added += data.count
Inventory.SetSlot(inv, item, data.count, data.metadata, data.slot)
toSlot = { item = inv.items[data.slot], inventory = inv.id }
end
if inv.player and server.syncInventory then
server.syncInventory(inv)
end
inv:syncSlotsWithClients(toSlot, {
left = inv.weight,
right = inv.open and Inventories[inv.open]?.weight or nil
}, true)
if invokingResource then
lib.logger(inv.owner, 'addItem', ('"%s" added %sx %s to "%s"'):format(invokingResource, added, item.name, inv.label))
end
for i = 1, #toSlot do
toSlot = toSlot.item
end
success = true
response = toSlot
end
if cb then
return cb(success, response)
end
return success, response
end
exports('AddItem', Inventory.AddItem)
---@param inv inventory
---@param search string|number slots|1, count|2
---@param items table | string
---@param metadata? table | string
function Inventory.Search(inv, search, items, metadata)
if items then
inv = Inventory(inv) --[[@as OxInventory]]
if inv then
inv = inv.items
if search == 'slots' then search = 1 elseif search == 'count' then search = 2 end
if type(items) == 'string' then items = {items} end
metadata = assertMetadata(metadata)
local itemCount = #items
local returnData = {}
for i = 1, itemCount do
local item = string.lower(items)
if item:sub(0, 7) == 'weapon_' then item = string.upper(item) end
if search == 1 then
returnData[item] = {}
elseif search == 2 then
returnData[item] = 0
end
for _, v in pairs(inv) do
if v.name == item then
if not v.metadata then v.metadata = {} end
if not metadata or table.contains(v.metadata, metadata) then
if search == 1 then
returnData[item][#returnData[item]+1] = inv[v.slot]
elseif search == 2 then
returnData[item] += v.count
end
end
end
end
end
if next(returnData) then return itemCount == 1 and returnData[items[1]] or returnData end
end
end
return false
end
exports('Search', Inventory.Search)
---@param inv inventory
---@param item table | string
---@param metadata? table
function Inventory.GetItemSlots(inv, item, metadata)
inv = Inventory(inv) --[[@as OxInventory]]
if not inv?.slots then return end
local totalCount, slots, emptySlots = 0, {}, inv.slots
for k, v in pairs(inv.items) do
emptySlots -= 1
if v.name == item.name then
if metadata and v.metadata == nil then
v.metadata = {}
end
if not metadata or table.matches(v.metadata, metadata) then
totalCount = totalCount + v.count
slots[k] = v.count
end
end
end
return slots, totalCount, emptySlots
end
exports('GetItemSlots', Inventory.GetItemSlots)
---@param inv inventory
---@param item table | string
---@param count integer
---@param metadata? table | string
---@param slot? number
---@param ignoreTotal? boolean
---@return boolean? success, string? response
function Inventory.RemoveItem(inv, item, count, metadata, slot, ignoreTotal)
if type(item) ~= 'table' then item = Items(item) end
if not item then return false, 'invalid_item' end
count = math.floor(count + 0.5)
if count > 0 then
inv = Inventory(inv) --[[@as OxInventory]]
if not inv?.slots then return false, 'invalid_inventory' end
metadata = assertMetadata(metadata)
local itemSlots, totalCount = Inventory.GetItemSlots(inv, item, metadata)
if not itemSlots then return false end
if totalCount and count > totalCount then
if not ignoreTotal then return false, 'not_enough_items' end
count = totalCount
end
local removed, total, slots = 0, count, {}
if slot and itemSlots[slot] then
removed = count
Inventory.SetSlot(inv, item, -count, inv.items[slot].metadata, slot)
slots[#slots+1] = inv.items[slot] or slot
elseif itemSlots and totalCount > 0 then
for k, v in pairs(itemSlots) do
if removed < total then
if v == count then
TriggerClientEvent('ox_inventory:itemNotify', inv.id, { inv.items[k], 'ui_removed', v })
removed = total
inv.weight -= inv.items[k].weight
inv.items[k] = nil
slots[#slots+1] = inv.items[k] or k
elseif v > count then
Inventory.SetSlot(inv, item, -count, inv.items[k].metadata, k)
slots[#slots+1] = inv.items[k] or k
removed = total
count = v - count
else
TriggerClientEvent('ox_inventory:itemNotify', inv.id, { inv.items[k], 'ui_removed', v })
removed = removed + v
count = count - v
inv.weight -= inv.items[k].weight
inv.items[k] = nil
slots[#slots+1] = k
end
else break end
end
end
if removed > 0 then
inv.changed = true
if inv.player and server.syncInventory then
server.syncInventory(inv)
end
local array = table.create(#slots, 0)
for k, v in pairs(slots) do
array[k] = {item = type(v) == 'number' and { slot = v } or v, inventory = inv.id}
end
inv:syncSlotsWithClients(array, {
left = inv.weight,
right = inv.open and Inventories[inv.open]?.weight or nil
}, true)
local invokingResource = server.loglevel > 1 and GetInvokingResource()
if invokingResource then
lib.logger(inv.owner, 'removeItem', ('"%s" removed %sx %s from "%s"'):format(invokingResource, removed, item.name, inv.label))
end
return true
end
end
return false, 'not_enough_items'
end
exports('RemoveItem', Inventory.RemoveItem)
---@param inv inventory
---@param item table | string
---@param count number
---@param metadata? table | string
function Inventory.CanCarryItem(inv, item, count, metadata)
if type(item) ~= 'table' then item = Items(item) end
if item then
inv = Inventory(inv) --[[@as OxInventory]]
if inv then
local itemSlots, _, emptySlots = Inventory.GetItemSlots(inv, item, type(metadata) == 'table' and metadata or { type = metadata or nil })
if not itemSlots then return end
local weight = metadata and metadata.weight or item.weight
if next(itemSlots) or emptySlots > 0 then
if not count then count = 1 end
if not item.stack and emptySlots < count then return false end
if weight == 0 then return true end
local newWeight = inv.weight + (weight * count)
if newWeight > inv.maxWeight then
TriggerClientEvent('ox_lib:notify', inv.id, { type = 'error', description = locale('cannot_carry') })
return false
end
return true
end
end
end
end
exports('CanCarryItem', Inventory.CanCarryItem)
---@param inv inventory
---@param item table | string
function Inventory.CanCarryAmount(inv, item)
if type(item) ~= 'table' then item = Items(item) end
inv = Inventory(inv) --[[@as OxInventory]]
if inv and item then
local availableWeight = inv.maxWeight - inv.weight
return math.floor(availableWeight / item.weight)
end
end
exports('CanCarryAmount', Inventory.CanCarryAmount)
---@param inv inventory
---@param weight number
function Inventory.CanCarryWeight(inv, weight)
inv = Inventory(inv) --[[@as OxInventory]]
if not inv then return end
local availableWeight = inv.maxWeight - inv.weight
local canHold = availableWeight >= weight
return canHold, availableWeight
end
exports('CanCarryWeight', Inventory.CanCarryWeight)
---@param inv inventory
---@param firstItem string
---@param firstItemCount number
---@param testItem string
---@param testItemCount number
function Inventory.CanSwapItem(inv, firstItem, firstItemCount, testItem, testItemCount)
inv = Inventory(inv) --[[@as OxInventory]]
if not inv then return end
local firstItemData = Inventory.GetItem(inv, firstItem)
local testItemData = Inventory.GetItem(inv, testItem)
if firstItemData and testItemData and firstItemData.count >= firstItemCount then
local weightWithoutFirst = inv.weight - (firstItemData.weight * firstItemCount)
local weightWithTest = weightWithoutFirst + (testItemData.weight * testItemCount)
return weightWithTest <= inv.maxWeight
end
end
exports('CanSwapItem', Inventory.CanSwapItem)
---Mostly for internal use, but deprecated.
---@param name string
---@param count number
---@param metadata { [string]: any }
---@param slot number
RegisterServerEvent('ox_inventory:removeItem', function(name, count, metadata, slot)
Inventory.RemoveItem(source, name, count, metadata, slot)
end)
Inventory.Drops = {}
---@param prefix string?
---@return string
local function generateInvId(prefix)
while true do
local invId = ('%s-%s'):format(prefix or 'drop', math.random(100000, 999999))
if not Inventories[invId] then return invId end
Wait(0)
end
end
local function CustomDrop(prefix, items, coords, slots, maxWeight, instance, model)
local dropId = generateInvId()
local inventory = Inventory.Create(dropId, ('%s %s'):format(prefix, dropId:gsub('%D', '')), 'drop', slots or shared.playerslots, 0, maxWeight or shared.playerweight, false)
if not inventory then return end
local items, weight = generateItems(inventory, 'drop', items)
inventory.items = items
inventory.weight = weight
inventory.coords = coords
Inventory.Drops[dropId] = {
coords = inventory.coords,
instance = instance,
model = model,
}
TriggerClientEvent('ox_inventory:createDrop', -1, dropId, Inventory.Drops[dropId])
end
AddEventHandler('ox_inventory:customDrop', CustomDrop)
exports('CustomDrop', CustomDrop)
exports('CreateDropFromPlayer', function(playerId)
local playerInventory = Inventories[playerId]
if not playerInventory or not next(playerInventory.items) then return end
local dropId = generateInvId()
local inventory = Inventory.Create(dropId, ('Drop %s'):format(dropId:gsub('%D', '')), 'drop', playerInventory.slots, playerInventory.weight, playerInventory.maxWeight, false, table.clone(playerInventory.items))
if not inventory then return end
local coords = GetEntityCoords(GetPlayerPed(playerId))
inventory.coords = vec3(coords.x, coords.y, coords.z-0.2)
Inventory.Drops[dropId] = {
coords = inventory.coords,
instance = Player(playerId).state.instance
}
Inventory.Clear(playerInventory)
TriggerClientEvent('ox_inventory:createDrop', -1, dropId, Inventory.Drops[dropId])
return dropId
end)
local TriggerEventHooks = require 'modules.hooks.server'
---@class SwapSlotData
---@field count number
---@field fromSlot number
---@field toSlot number
---@field instance any
---@field fromType string
---@field toType string
---@field coords? vector3
---@param source number
---@param playerInventory OxInventory
---@param fromData SlotWithItem?
---@param data SwapSlotData
local function dropItem(source, playerInventory, fromData, data)
if not fromData then return end
local toData = table.clone(fromData)
toData.slot = data.toSlot
toData.count = data.count
toData.weight = Inventory.SlotWeight(Items(toData.name), toData)
if toData.weight > shared.playerweight then return end
if not TriggerEventHooks('swapItems', {
source = source,
fromInventory = playerInventory.id,
fromSlot = fromData,
fromType = playerInventory.type,
toInventory = 'newdrop',
toSlot = data.toSlot,
toType = 'drop',
count = data.count,
action = 'move',
}) then return end
fromData.count -= data.count
fromData.weight = Inventory.SlotWeight(Items(fromData.name), fromData)
if fromData.count < 1 then
fromData = nil
else
toData.metadata = table.clone(toData.metadata)
end
local slot = data.fromSlot
playerInventory.weight -= toData.weight
playerInventory.items[slot] = fromData
if slot == playerInventory.weapon then
playerInventory.weapon = nil
end
local dropId = generateInvId('drop')
local inventory = Inventory.Create(dropId, ('Drop %s'):format(dropId:gsub('%D', '')), 'drop', shared.playerslots, toData.weight, shared.playerweight, false, {[data.toSlot] = toData})
if not inventory then return end
inventory.coords = data.coords
Inventory.Drops[dropId] = {coords = inventory.coords, instance = data.instance}
playerInventory.changed = true
TriggerClientEvent('ox_inventory:createDrop', -1, dropId, Inventory.Drops[dropId], playerInventory.open and source, slot)
if server.loglevel > 0 then
lib.logger(playerInventory.owner, 'swapSlots', ('%sx %s transferred from "%s" to "%s"'):format(data.count, toData.name, playerInventory.label, dropId))
end
if server.syncInventory then server.syncInventory(playerInventory) end
return true, {
weight = playerInventory.weight,
items = {
{
item = fromData or { slot = data.fromSlot },
inventory = playerInventory.id
}
}
}
end
local activeSlots = {}
---@param source number
---@param data SwapSlotData
lib.callback.register('ox_inventory:swapItems', function(source, data)
if data.count < 1 then return end
local playerInventory = Inventory(source)
if not playerInventory then return end
local toInventory = (data.toType == 'player' and playerInventory) or Inventory(playerInventory.open)
local fromInventory = (data.fromType == 'player' and playerInventory) or Inventory(playerInventory.open)
if not fromInventory or not toInventory then
playerInventory:closeInventory()
return
end
local fromRef = ('%s:%s'):format(fromInventory.id, data.fromSlot)
local toRef = ('%s:%s'):format(toInventory.id, data.toSlot)
if activeSlots[fromRef] or activeSlots[toRef] then
return false, {
{
item = toInventory.items[data.toSlot] or { slot = data.toSlot },
inventory = toInventory.id
},
{
item = fromInventory.items[data.fromSlot] or { slot = data.fromSlot },
inventory = fromInventory.id
}
}
end
local sameInventory = fromInventory.id == toInventory.id
local fromOtherPlayer = fromInventory.player and fromInventory ~= playerInventory
local toOtherPlayer = toInventory.player and toInventory ~= playerInventory
local toData = toInventory.items[data.toSlot]
if not sameInventory and (fromInventory.type == 'policeevidence' or (toInventory.type == 'policeevidence' and toData)) then
local group, rank = server.hasGroup(playerInventory, shared.police)
if not group or server.evidencegrade > rank then
return false, 'evidence_cannot_take'
end
end
activeSlots[fromRef] = true
activeSlots[toRef] = true
local _ <close> = defer(function()
activeSlots[fromRef] = nil
activeSlots[toRef] = nil
end)
if toInventory and (data.toType == 'newdrop' or fromInventory ~= toInventory or data.fromSlot ~= data.toSlot) then
local fromData = fromInventory.items[data.fromSlot]
if not fromData then
return false, {
{
item = { slot = data.fromSlot },
inventory = fromInventory.id
},
{
item = toData or { slot = data.toSlot },
inventory = toInventory.id
}
}
end
if data.count > fromData.count then
data.count = fromData.count
end
if data.toType == 'newdrop' then
return dropItem(source, playerInventory, fromData, data)
end
if fromData and (not fromData.metadata.container or fromData.metadata.container and toInventory.type ~= 'container') then
local container, containerItem = (not sameInventory and playerInventory.containerSlot) and (fromInventory.type == 'container' and fromInventory or toInventory)
if container then
containerItem = playerInventory.items[playerInventory.containerSlot]
end
local hookPayload = {
source = source,
fromInventory = fromInventory.id,
fromSlot = fromData,
fromType = fromInventory.type,
toInventory = toInventory.id,
toSlot = toData or data.toSlot,
toType = toInventory.type,
count = data.count,
}
if toData and ((toData.name ~= fromData.name) or not toData.stack or (not table.matches(toData.metadata, fromData.metadata))) then
-- Swap items
local toWeight = not sameInventory and (toInventory.weight - toData.weight + fromData.weight) or 0
local fromWeight = not sameInventory and (fromInventory.weight + toData.weight - fromData.weight) or 0
hookPayload.action = 'swap'
if not sameInventory then
if (toWeight <= toInventory.maxWeight and fromWeight <= fromInventory.maxWeight) then
if not TriggerEventHooks('swapItems', hookPayload) then return end
if containerItem then
local toContainer = toInventory.type == 'container'
local whitelist = Items.containers[containerItem.name]?.whitelist
local blacklist = Items.containers[containerItem.name]?.blacklist
local checkItem = toContainer and fromData.name or toData.name
if (whitelist and not whitelist[checkItem]) or (blacklist and blacklist[checkItem]) then
return
end
Inventory.ContainerWeight(containerItem, toContainer and fromWeight or toWeight, playerInventory)
end
if fromOtherPlayer then
TriggerClientEvent('ox_inventory:itemNotify', fromInventory.id, { fromData, 'ui_removed', fromData.count })
TriggerClientEvent('ox_inventory:itemNotify', fromInventory.id, { toData, 'ui_added', toData.count })
elseif toOtherPlayer then
TriggerClientEvent('ox_inventory:itemNotify', toInventory.id, { fromData, 'ui_added', fromData.count })
TriggerClientEvent('ox_inventory:itemNotify', toInventory.id, { toData, 'ui_removed', toData.count })
end
fromInventory.weight = fromWeight
toInventory.weight = toWeight
toData, fromData = Inventory.SwapSlots(fromInventory, toInventory, data.fromSlot, data.toSlot) --[[@as table]]
if server.loglevel > 0 then
lib.logger(playerInventory.owner, 'swapSlots', ('%sx %s transferred from "%s" to "%s" for %sx %s'):format(fromData.count, fromData.name, fromInventory.owner and fromInventory.label or fromInventory.id, toInventory.owner and toInventory.label or toInventory.id, toData.count, toData.name))
end
else return false, 'cannot_carry' end
else
if not TriggerEventHooks('swapItems', hookPayload) then return end
toData, fromData = Inventory.SwapSlots(fromInventory, toInventory, data.fromSlot, data.toSlot)
end
elseif toData and toData.name == fromData.name and table.matches(toData.metadata, fromData.metadata) then
-- Stack items
toData.count += data.count
fromData.count -= data.count
local toSlotWeight = Inventory.SlotWeight(Items(toData.name), toData)
local totalWeight = toInventory.weight - toData.weight + toSlotWeight
if fromInventory.type == 'container' or sameInventory or totalWeight <= toInventory.maxWeight then
hookPayload.action = 'stack'
if not TriggerEventHooks('swapItems', hookPayload) then
toData.count -= data.count
fromData.count += data.count
return
end
local fromSlotWeight = Inventory.SlotWeight(Items(fromData.name), fromData)
toData.weight = toSlotWeight
if not sameInventory then
fromInventory.weight = fromInventory.weight - fromData.weight + fromSlotWeight
toInventory.weight = totalWeight
if container then
Inventory.ContainerWeight(containerItem, toInventory.type == 'container' and toInventory.weight or fromInventory.weight, playerInventory)
end
if fromOtherPlayer then
TriggerClientEvent('ox_inventory:itemNotify', fromInventory.id, { fromData, 'ui_removed', data.count })
elseif toOtherPlayer then
TriggerClientEvent('ox_inventory:itemNotify', toInventory.id, { toData, 'ui_added', data.count })
end
if server.loglevel > 0 then
lib.logger(playerInventory.owner, 'swapSlots', ('%sx %s transferred from "%s" to "%s"'):format(data.count, fromData.name, fromInventory.owner and fromInventory.label or fromInventory.id, toInventory.owner and toInventory.label or toInventory.id))
end
end
fromData.weight = fromSlotWeight
else
toData.count -= data.count
fromData.count += data.count
return false, 'cannot_carry'
end
elseif data.count <= fromData.count then
-- Move item to an empty slot
toData = table.clone(fromData)
toData.count = data.count
toData.slot = data.toSlot
toData.weight = Inventory.SlotWeight(Items(toData.name), toData)
if fromInventory.type == 'container' or sameInventory or (toInventory.weight + toData.weight <= toInventory.maxWeight) then
hookPayload.action = 'move'
if not TriggerEventHooks('swapItems', hookPayload) then return end
if not sameInventory then
local toContainer = toInventory.type == 'container'
if container then
if toContainer and containerItem then
local whitelist = Items.containers[containerItem.name]?.whitelist
local blacklist = Items.containers[containerItem.name]?.blacklist
if (whitelist and not whitelist[fromData.name]) or (blacklist and blacklist[fromData.name]) then
return
end
end
end
fromInventory.weight -= toData.weight
toInventory.weight += toData.weight
if container then
Inventory.ContainerWeight(containerItem, toContainer and toInventory.weight or fromInventory.weight, playerInventory)
end
if fromOtherPlayer then
TriggerClientEvent('ox_inventory:itemNotify', fromInventory.id, { fromData, 'ui_removed', data.count })
elseif toOtherPlayer then
TriggerClientEvent('ox_inventory:itemNotify', toInventory.id, { fromData, 'ui_added', data.count })
end
if server.loglevel > 0 then
lib.logger(playerInventory.owner, 'swapSlots', ('%sx %s transferred from "%s" to "%s"'):format(data.count, fromData.name, fromInventory.owner and fromInventory.label or fromInventory.id, toInventory.owner and toInventory.label or toInventory.id))
end
end
fromData.count -= data.count
fromData.weight = Inventory.SlotWeight(Items(fromData.name), fromData)
if fromData.count > 0 then
toData.metadata = table.clone(toData.metadata)
end
else return false, 'cannot_carry_other' end
end
if fromData and fromData.count < 1 then fromData = nil end
---@type updateSlot[]
local items = {}
if fromInventory.player and not fromOtherPlayer then
if toInventory.type == 'container' and containerItem then
items[#items + 1] = {
item = containerItem,
inventory = playerInventory.id
}
end
end
if toInventory.player and not toOtherPlayer then
if fromInventory.type == 'container' and containerItem then
items[#items + 1] = {
item = containerItem,
inventory = playerInventory.id
}
end
end
fromInventory.items[data.fromSlot] = fromData
toInventory.items[data.toSlot] = toData
if fromInventory.changed ~= nil then fromInventory.changed = true end
if toInventory.changed ~= nil then toInventory.changed = true end
if sameInventory then
fromInventory:syncSlotsWithClients({
{
item = fromInventory.items[data.toSlot] or { slot = data.toSlot },
inventory = fromInventory.id
},
{
item = fromInventory.items[data.fromSlot] or { slot = data.fromSlot },
inventory = fromInventory.id
}
}, { left = fromInventory.weight }, true)
else
toInventory:syncSlotsWithClients({
{
item = toInventory.items[data.toSlot] or { slot = data.toSlot },
inventory = toInventory.id
}
}, { left = toInventory.weight }, true)
fromInventory:syncSlotsWithClients({
{
item = fromInventory.items[data.fromSlot] or { slot = data.fromSlot },
inventory = fromInventory.id
}
}, { left = fromInventory.weight }, true)
end
local resp
if next(items) then
resp = { weight = playerInventory.weight, items = items }
end
if server.syncInventory then
if fromInventory.player then
server.syncInventory(fromInventory)
end
if toInventory.player and not sameInventory then
server.syncInventory(toInventory)
end
end
local weaponSlot
if toInventory.weapon == data.toSlot then
if not sameInventory then
toInventory.weapon = nil
TriggerClientEvent('ox_inventory:disarm', toInventory.id)
else
weaponSlot = data.fromSlot
toInventory.weapon = weaponSlot
end
end
if fromInventory.weapon == data.fromSlot then
if not sameInventory then
fromInventory.weapon = nil
TriggerClientEvent('ox_inventory:disarm', fromInventory.id)
elseif not weaponSlot then
weaponSlot = data.toSlot
fromInventory.weapon = weaponSlot
end
end
return containerItem and containerItem.weight or true, resp, weaponSlot
end
end
end)
function Inventory.Confiscate(source)
local inv = Inventories[source]
if inv?.player then
db.saveStash(inv.owner, inv.owner, json.encode(minimal(inv)))
table.wipe(inv.items)
inv.weight = 0
inv.changed = true
TriggerClientEvent('ox_inventory:inventoryConfiscated', inv.id)
if server.syncInventory then server.syncInventory(inv) end
end
end
exports('ConfiscateInventory', Inventory.Confiscate)
function Inventory.Return(source)
local inv = Inventories[source]
if inv?.player then
MySQL.scalar('SELECT data FROM ox_inventory WHERE name = ?', { inv.owner }, function(data)
if data then
MySQL.query('DELETE FROM ox_inventory WHERE name = ?', { inv.owner })
data = json.decode(data)
local inventory, totalWeight = {}, 0
if data and next(data) then
for i = 1, #data do
local i = data
if type(i) == 'number' then break end
local item = Items(i.name)
if item then
local weight = Inventory.SlotWeight(item, i)
totalWeight = totalWeight + weight
inventory[i.slot] = {name = i.name, label = item.label, weight = weight, slot = i.slot, count = i.count, description = item.description, metadata = i.metadata, stack = item.stack, close = item.close}
end
end
end
inv.changed = true
inv.weight = totalWeight
inv.items = inventory
TriggerClientEvent('ox_inventory:inventoryReturned', source, {inventory, totalWeight})
if server.syncInventory then server.syncInventory(inv) end
end
end)
end
end
exports('ReturnInventory', Inventory.Return)
---@param inv inventory
---@param keep? string | string[] an item or list of items to ignore while clearing items
function Inventory.Clear(inv, keep)
inv = Inventory(inv) --[[@as OxInventory]]
if not inv or not next(inv.items) then return end
local updateSlots = {}
local newWeight = 0
local inc = 0
if keep then
local keptItems = {}
local keepType = type(keep)
if keepType == 'string' then
for slot, v in pairs(inv.items) do
if v.name == keep then
keptItems[v.slot] = v
newWeight += v.weight
elseif updateSlots then
inc += 1
updateSlots[inc] = { item = { slot = slot }, inventory = inv.id }
end
end
elseif keepType == 'table' and table.type(keep) == 'array' then
for slot, v in pairs(inv.items) do
for i = 1, #keep do
if v.name == keep then
keptItems[v.slot] = v
newWeight += v.weight
goto foundItem
end
end
if updateSlots then
inc += 1
updateSlots[inc] = { item = { slot = slot }, inventory = inv.id }
end
::foundItem::
end
end
table.wipe(inv.items)
inv.items = keptItems
else
if updateSlots then
for slot in pairs(inv.items) do
inc += 1
updateSlots[inc] = { item = { slot = slot }, inventory = inv.id }
end
end
table.wipe(inv.items)
end
inv.weight = newWeight
inv.changed = true
inv:syncSlotsWithClients(updateSlots, {
left = inv.weight,
right = inv.open and Inventories[inv.open]?.weight or nil
}, true)
if not inv.player then
if inv.open then
local playerInv = Inventory(inv.open)
if not playerInv then return end
playerInv:closeInventory()
end
inv:openInventory(inv)
return
end
if server.syncInventory then server.syncInventory(inv) end
inv.weapon = nil
end
exports('ClearInventory', Inventory.Clear)
---@param inv inventory
---@return integer?
function Inventory.GetEmptySlot(inv)
local inventory = Inventory(inv)
if not inventory then return end
local items = inventory.items
for i = 1, inventory.slots do
if not items then
return i
end
end
end
exports('GetEmptySlot', Inventory.GetEmptySlot)
---@param inv inventory
---@param itemName string
---@param metadata any
function Inventory.GetSlotForItem(inv, itemName, metadata)
local inventory = Inventory(inv)
local item = Items(itemName) --[[@as OxServerItem?]]
if not inventory or not item then return end
metadata = assertMetadata(metadata)
local items = inventory.items
local emptySlot
for i = 1, inventory.slots do
local slotData = items
if item.stack and slotData and slotData.name == item.name and table.matches(slotData.metadata, metadata) then
return i
elseif not item.stack and not slotData and not emptySlot then
emptySlot = i
end
end
return emptySlot
end
exports('GetSlotForItem', Inventory.GetSlotForItem)
---@param inv inventory
---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return SlotWithItem?
function Inventory.GetSlotWithItem(inv, itemName, metadata, strict)
local inventory = Inventory(inv)
local item = Items(itemName) --[[@as OxServerItem?]]
if not inventory or not item then return end
metadata = assertMetadata(metadata)
local tablematch = strict and table.matches or table.contains
for _, slotData in pairs(inventory.items) do
if slotData and slotData.name == item.name and (not metadata or tablematch(slotData.metadata, metadata)) then
if not Items.UpdateDurability(inventory, slotData, item, nil, os.time()) then
return slotData
end
end
end
end
exports('GetSlotWithItem', Inventory.GetSlotWithItem)
---@param inv inventory
---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return number?
function Inventory.GetSlotIdWithItem(inv, itemName, metadata, strict)
return Inventory.GetSlotWithItem(inv, itemName, metadata, strict)?.slot
end
exports('GetSlotIdWithItem', Inventory.GetSlotIdWithItem)
---@param inv inventory
---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return SlotWithItem[]?
function Inventory.GetSlotsWithItem(inv, itemName, metadata, strict)
local inventory = Inventory(inv)
local item = Items(itemName) --[[@as OxServerItem?]]
if not inventory or not item then return end
metadata = assertMetadata(metadata)
local response = {}
local n = 0
local tablematch = strict and table.matches or table.contains
local ostime = os.time()
for _, slotData in pairs(inventory.items) do
if slotData and slotData.name == item.name and (not metadata or tablematch(slotData.metadata, metadata)) then
if not Items.UpdateDurability(inventory, slotData, item, nil, os.time()) then
n += 1
response[n] = slotData
end
end
end
return response
end
exports('GetSlotsWithItem', Inventory.GetSlotsWithItem)
---@param inv inventory
---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return number[]?
function Inventory.GetSlotIdsWithItem(inv, itemName, metadata, strict)
local items = Inventory.GetSlotsWithItem(inv, itemName, metadata, strict)
if items then
---@cast items +number[]
for i = 1, #items do
items = items.slot
end
return items
end
end
---@param inv inventory
---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return number
function Inventory.GetItemCount(inv, itemName, metadata, strict)
local inventory = Inventory(inv)
local item = Items(itemName) --[[@as OxServerItem?]]
if not inventory or not item then return 0 end
metadata = assertMetadata(metadata)
local count = 0
local tablematch = strict and table.matches or table.contains
for _, slotData in pairs(inventory.items) do
if slotData and slotData.name == item.name and (not metadata or tablematch(slotData.metadata, metadata)) then
count += slotData.count
end
end
return count
end
exports('GetItemCount', Inventory.GetItemCount)
---@param inv OxInventory
---@param buffer table
---@param time integer
---@return integer | false | nil
---@return table | nil
local function prepareInventorySave(inv, buffer, time)
local shouldSave = not inv.datastore and inv.changed
local n = 0
for k, v in pairs(inv.items) do
if not Items.UpdateDurability(inv, v, Items(v.name), nil, time) and shouldSave then
n += 1
buffer[n] = {
name = v.name,
count = v.count,
slot = k,
metadata = next(v.metadata) and v.metadata or nil
}
end
end
if not shouldSave then return end
local data = next(buffer) and json.encode(buffer) or nil
inv.changed = false
table.wipe(buffer)
if inv.player then
if shared.framework == 'esx' then return end
return 1, { data, inv.owner }
end
if inv.type == 'trunk' then
return 2, { data, inv.dbId }
end
if inv.type == 'glovebox' then
return 3, { data, inv.dbId }
end
return 4, { data, inv.owner and tostring(inv.owner) or '', inv.dbId }
end
local function saveInventories(manual)
local time = os.time()
local parameters = { {}, {}, {}, {} }
local size = { 0, 0, 0, 0 }
local buffer = {}
for _, inv in pairs(Inventories) do
local index, data = prepareInventorySave(inv, buffer, time)
if index then
size[index] += 1
parameters[index][size[index]] = data
end
end
db.saveInventories(parameters[1], parameters[2], parameters[3], parameters[4])
if not manual then return end
for _, inv in pairs(Inventories) do
if not inv.open then
if inv.datastore and inv.netid and (inv.type == 'trunk' or inv.type == 'glovebox') then
if NetworkGetEntityFromNetworkId(inv.netid) == 0 then
Inventory.Remove(inv)
end
elseif not inv.player and (inv.datastore or inv.owner) and time - inv.time >= 1200 then
-- inv.time is a timestamp for when the inventory was last closed
-- if unopened for n seconds, the inventory is unloaded (datastore/temp stash is deleted)
Inventory.Remove(inv)
end
end
end
end
lib.cron.new('*/5 * * * *', function()
saveInventories()
end)
function Inventory.SaveInventories(lock)
Inventory.Lock = lock or nil
Inventory.CloseAll()
saveInventories(lock)
end
AddEventHandler('playerDropped', function()
if GetNumPlayerIndices() == 0 then
Inventory.SaveInventories()
end
end)
AddEventHandler('txAdmin:events:serverShuttingDown', function()
Inventory.SaveInventories(true)
end)
AddEventHandler('onResourceStop', function(resource)
if resource == shared.resource then
Inventory.SaveInventories(true)
end
end)
RegisterServerEvent('ox_inventory:closeInventory', function()
local inventory = Inventories[source]
if inventory?.open then
local secondary = Inventories[inventory.open]
if secondary then
secondary:closeInventory()
end
inventory:closeInventory(true)
end
end)
RegisterServerEvent('ox_inventory:giveItem', function(slot, target, count)
local fromInventory = Inventories[source]
local toInventory = Inventories[target]
if count <= 0 then count = 1 end
if toInventory?.player then
local data = fromInventory.items[slot]
if not data then return end
local item = Items(data.name)
if not item or data.count < count or not Inventory.CanCarryItem(toInventory, item, count, data.metadata) then
return TriggerClientEvent('ox_lib:notify', fromInventory.id, { type = 'error', description = locale('cannot_give', count, data.label) })
end
local toSlot = Inventory.GetSlotForItem(toInventory, data.name, data.metadata)
local fromRef = ('%s:%s'):format(fromInventory.id, slot)
local toRef = ('%s:%s'):format(toInventory.id, toSlot)
if activeSlots[fromRef] or activeSlots[toRef] then
return TriggerClientEvent('ox_lib:notify', fromInventory.id, { type = 'error', description = locale('cannot_give', count, data.label) })
end
activeSlots[fromRef] = true
activeSlots[toRef] = true
local _ <close> = defer(function()
activeSlots[fromRef] = nil
activeSlots[toRef] = nil
end)
if TriggerEventHooks('swapItems', {
source = fromInventory.id,
fromInventory = fromInventory.id,
fromType = fromInventory.type,
toInventory = toInventory.id,
toType = toInventory.type,
count = data.count,
action = 'give',
fromSlot = data,
}) then
---@todo manually call swapItems or something?
if Inventory.AddItem(toInventory, item, count, data.metadata, toSlot) then
if Inventory.RemoveItem(fromInventory, item, count, data.metadata, slot) then
if server.loglevel > 0 then
lib.logger(fromInventory.owner, 'giveItem', ('"%s" gave %sx %s to "%s"'):format(fromInventory.label, count, data.name, toInventory.label))
end
return
end
end
end
return TriggerClientEvent('ox_lib:notify', fromInventory.id, { type = 'error', description = locale('cannot_give', count, data.label) })
end
end)
local function updateWeapon(source, action, value, slot, specialAmmo)
local inventory = Inventories[source]
if not inventory then return end
if not action then
inventory.weapon = nil
return
end
local type = type(value)
if type == 'table' and action == 'component' then
local item = inventory.items[value.slot]
if item then
if item.metadata.components then
for k, v in pairs(item.metadata.components) do
if v == value.component then
if not Inventory.AddItem(inventory, value.component, 1) then return end
table.remove(item.metadata.components, k)
inventory:syncSlotsWithPlayer({
{ item = item }
}, inventory.weight)
return true
end
end
end
end
else
if not slot then slot = inventory.weapon end
local weapon = inventory.items[slot]
if weapon and weapon.metadata then
local item = Items(weapon.name)
if not item.weapon then
inventory.weapon = nil
return
end
if action == 'load' and weapon.metadata.durability > 0 then
local ammo = Items(weapon.name).ammoname
local diff = value - (weapon.metadata.ammo or 0)
if not Inventory.RemoveItem(inventory, ammo, diff, specialAmmo) then return end
weapon.metadata.ammo = value
weapon.metadata.specialAmmo = specialAmmo
weapon.weight = Inventory.SlotWeight(item, weapon)
elseif action == 'throw' then
if not Inventory.RemoveItem(inventory, weapon.name, 1, weapon.metadata, weapon.slot) then return end
elseif action == 'component' then
if type == 'number' then
if not Inventory.AddItem(inventory, weapon.metadata.components[value], 1) then return false end
table.remove(weapon.metadata.components, value)
weapon.weight = Inventory.SlotWeight(item, weapon)
elseif type == 'string' then
local component = inventory.items[tonumber(value)]
if not Inventory.RemoveItem(inventory, component.name, 1) then return false end
table.insert(weapon.metadata.components, component.name)
weapon.weight = Inventory.SlotWeight(item, weapon)
end
elseif action == 'ammo' then
if item.hash == `WEAPON_FIREEXTINGUISHER` or item.hash == `WEAPON_PETROLCAN` or item.hash == `WEAPON_HAZARDCAN` or item.hash == `WEAPON_FERTILIZERCAN` then
weapon.metadata.durability = math.floor(value)
weapon.metadata.ammo = weapon.metadata.durability
elseif value < weapon.metadata.ammo then
local durability = Items(weapon.name).durability * math.abs((weapon.metadata.ammo or 0.1) - value)
weapon.metadata.ammo = value
weapon.metadata.durability = weapon.metadata.durability - durability
weapon.weight = Inventory.SlotWeight(item, weapon)
end
elseif action == 'melee' and value > 0 then
weapon.metadata.durability = weapon.metadata.durability - ((Items(weapon.name).durability or 1) * value)
end
if action ~= 'throw' then
inventory:syncSlotsWithPlayer({
{ item = weapon }
}, inventory.weight)
end
if server.syncInventory then server.syncInventory(inventory) end
return true
end
end
end
lib.callback.register('ox_inventory:updateWeapon', updateWeapon)
RegisterNetEvent('ox_inventory:updateWeapon', function(action, value, slot, specialAmmo)
updateWeapon(source, action, value, slot, specialAmmo)
end)
lib.callback.register('ox_inventory:removeAmmoFromWeapon', function(source, slot)
local inventory = Inventory(source)
if not inventory then return end
local slotData = inventory.items[slot]
if not slotData or not slotData.metadata.ammo or slotData.metadata.ammo < 1 then return end
local item = Items(slotData.name)
if not item or not item.ammoname then return end
if Inventory.AddItem(inventory, item.ammoname, slotData.metadata.ammo, { type = slotData.metadata.specialAmmo or nil }) then
slotData.metadata.ammo = 0
slotData.weight = Inventory.SlotWeight(item, slotData)
inventory:syncSlotsWithPlayer({
{ item = slotData }
}, inventory.weight)
if server.syncInventory then server.syncInventory(inventory) end
return true
end
end)
local function checkStashProperties(properties)
local name, slots, maxWeight, coords in properties
if type(name) ~= 'string' then
error(('received %s for stash name (expected string)'):format(type(name)))
end
if type(slots) ~= 'number' then
error(('received %s for stash slots (expected number)'):format(type(slots)))
end
if type(maxWeight) ~= 'number' then
error(('received %s for stash maxWeight (expected number)'):format(type(maxWeight)))
end
if coords then
local typeof = type(coords)
if typeof ~= 'vector3' then
if typeof == 'table' and table.type(coords) ~= 'array' then
coords = vec3(coords.x or coords[1], coords.y or coords[2], coords.z or coords[3])
else
if table.type(coords) == 'array' then
for i = 1, #coords do
coords = vec3(coords.x, coords.y, coords.z)
end
else
error(('received %s for stash coords (expected vector3 or array of vector3)'):format(typeof))
end
end
end
end
return name, slots, maxWeight, coords
end
---@param name string stash identifier when loading from the database
---@param label string display name when inventory is open
---@param slots number
---@param maxWeight number
---@param owner? string|number|boolean
---@param groups? table<string, number>
---@param coords? vector3|table<vector3>
--- For simple integration with other resources that want to create valid stashes.
--- This needs to be triggered before a player can open a stash.
--- ```
--- Owner sets the stash permissions.
--- string: can only access the stash linked to the owner (usually player identifier)
--- true: each player has a unique stash, but can request other player's stashes
--- nil: always shared
---
--- groups: { ['police'] = 0 }
--- ```
local function registerStash(name, label, slots, maxWeight, owner, groups, coords)
name, slots, maxWeight, coords = checkStashProperties({
name = name,
slots = slots,
maxWeight = maxWeight,
coords = coords,
})
local curStash = RegisteredStashes[name]
if curStash then
---@todo creating proper stash classes with inheritence would simplify updating data
---i.e. all stashes with the same type share groups, maxweight, slots, dbid, etc.
---only label, owner, weight, coords, and items really need to vary
for _, stash in pairs(Inventories) do
if stash.type == 'stash' and stash.dbId == name then
stash.label = label or stash.label
stash.owner = (owner and owner ~= true) and stash.owner or owner
stash.slots = slots or stash.slots
stash.maxWeight = maxWeight or stash.maxWeight
stash.groups = groups or stash.groups
stash.coords = coords or stash.coords
end
end
end
RegisteredStashes[name] = {
name = name,
label = label,
owner = owner,
slots = slots,
maxWeight = maxWeight,
groups = groups,
coords = coords
}
end
exports('RegisterStash', registerStash)
---@class TemporaryStashProperties
---@field label string
---@field slots number
---@field maxWeight number
---@field owner? string|number|boolean
---@field groups? table<string, number>
---@field coords? vector3
---@field items? { [number]: string, [number]: number, [number]: table | string }[]
---@param properties TemporaryStashProperties
function Inventory.CreateTemporaryStash(properties)
properties.name = generateInvId('temp')
local name, slots, maxWeight, coords = checkStashProperties(properties)
local inventory = Inventory.Create(name, properties.label, 'temp', slots, 0, maxWeight, properties.owner, {}, properties.groups)
if not inventory then return end
inventory.items, inventory.weight = generateItems(inventory, 'drop', properties.items)
inventory.coords = coords
return inventory.id
end
exports('CreateTemporaryStash', Inventory.CreateTemporaryStash)
return Inventory
|
|