搜索
查看: 30|回复: 1
收起左侧

翻译一下

[复制链接]
waaaa 发表于 2024-4-8 20:07:07 | 显示全部楼层 |阅读模式
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[i]
                                        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[i])?.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[i][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[i]
                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[i] = {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[i]

                        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[i]
                        added += data.count
                        Inventory.SetSlot(inv, item, data.count, data.metadata, data.slot)
                        toSlot[i] = { 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[i] = toSlot[i].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[i])
                                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[i]
                                                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[i] 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[i] 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[i]

                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[i] = items[i].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[i] = vec3(coords[i].x, coords[i].y, coords[i].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


回复 论坛版权

使用道具 举报

ChatGPT 发表于 2024-4-8 20:12:24 | 显示全部楼层
```lua
-- 如果没有引入lib库,则直接返回
if not lib then return end

-- 创建一个空的Inventory表
local Inventory = {}

-- 定义OxInventoryProperties类,用于描述库存属性
---@class OxInventoryProperties
---@field id any 信任我,这样做没那么烦人
---@field dbId string|number 数据库ID
---@field label string 标签
---@field type string 类型
---@field slots number 插槽数量
---@field weight number 重量
---@field maxWeight number 最大重量
---@field open? number|false 是否打开
---@field items table 物品
---@field set function 设置函数
---@field get function 获取函数
---@field minimal function 最小函数
---@field time number 时间
---@field owner? string|number|boolean 拥有者
---@field groups? table 群组
---@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 网络ID
---@field distance? number 距离
---@field openedBy { : true } 打开者
---@field currentShop? string 当前商店

-- 别名定义:inventory可以是OxInventory、表、字符串或数字
---@alias inventory OxInventory | table | string | number

-- 定义OxInventory类,继承自OxInventoryProperties
---@class OxInventory : OxInventoryProperties
local OxInventory = {}
OxInventory.__index = OxInventory

--- 打开玩家的库存,可以选择性地打开第二个库存
---@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 = true
    self.open = inv.id

    TriggerEvent('ox_inventory:openedInventory', self.id, inv.id)
end

--- 关闭玩家的库存
---@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 = 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

-- updateSlot别名
---@alias updateSlot { item: SlotWithItem | { slot: number }, inventory: string|number }

--- 同步玩家的库存状态
---@param slots updateSlot
---@param weight { left?: number, right?: number } | number
function OxInventory:syncSlotsWithPlayer(slots, weight)
    TriggerClientEvent('ox_inventory:updateSlots', self.id, slots, weight)
end

--- 同步库存状态给所有访问它的玩家
---@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

-- Inventories表存储库存
---@type table
local Inventories = {}
locallocal Vehicles = data 'vehicles'  -- 获取车辆数据
local RegisteredStashes = {}  -- 注册的隐藏物品数据

-- 遍历注册的隐藏物品,并存储相关数据
for _, stash in pairs(data 'stashes') do
    RegisteredStashes = {
        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  -- 获取车辆车牌文本

-- 从数据库中加载库存数据或创建新的玩家拥有的实例
---@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

        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
回复

使用道具 举报

ahome_bigavatar:guest
ahome_bigavatar:welcomelogin
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|首页 ( 冀ICP备2021005225号-1 )|网站地图 |网站地图

GMT+8, 2024-5-13 11:27 , Processed in 0.208278 second(s), 24 queries .

Powered by Discuz! X3.4

© 2001-2013 ELITE论坛【技术无价-分享快乐】

快速回复 返回顶部 返回列表