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