if not lib then return end require 'modules.bridge.client' require 'modules.interface.client' local Utils = require 'modules.utils.client' local Weapon = require 'modules.weapon.client' local currentWeapon exports('getCurrentWeapon', function() return currentWeapon end) RegisterNetEvent('ox_inventory:disarm', function(noAnim) currentWeapon = Weapon.Disarm(currentWeapon, noAnim) end) RegisterNetEvent('ox_inventory:clearWeapons', function() Weapon.ClearAll(currentWeapon) end) local StashTarget exports('setStashTarget', function(id, owner) StashTarget = id and {id=id, owner=owner} end) ---@type boolean | number local invBusy = true ---@type boolean? local invOpen = false local plyState = LocalPlayer.state local IsPedCuffed = IsPedCuffed local playerPed = cache.ped lib.onCache('ped', function(ped) playerPed = ped Utils.WeaponWheel() end) plyState:set('invBusy', true, true) plyState:set('invHotkeys', false, false) plyState:set('canUseWeapons', false, false) local function canOpenInventory() if not PlayerData.loaded then return shared.info('cannot open inventory', '(player inventory has not loaded)') end if IsPauseMenuActive() then return end if invBusy or invOpen == nil or (currentWeapon?.timer or 0) > 0 then return shared.info('cannot open inventory', '(is busy)') end if PlayerData.dead or IsPedFatallyInjured(playerPed) then return shared.info('cannot open inventory', '(fatal injury)') end if PlayerData.cuffed or IsPedCuffed(playerPed) then return shared.info('cannot open inventory', '(cuffed)') end return true end ---@param ped number ---@return boolean local function canOpenTarget(ped) return IsPedFatallyInjured(ped) or IsEntityPlayingAnim(ped, 'dead', 'dead_a', 3) or IsPedCuffed(ped) or IsEntityPlayingAnim(ped, 'mp_arresting', 'idle', 3) or IsEntityPlayingAnim(ped, 'missminuteman_1ig_2', 'handsup_base', 3) or IsEntityPlayingAnim(ped, 'missminuteman_1ig_2', 'handsup_enter', 3) or IsEntityPlayingAnim(ped, 'random@mugging3', 'handsup_standing_base', 3) end local defaultInventory = { type = 'newdrop', slots = shared.playerslots, weight = 0, maxWeight = shared.playerweight, items = {} } local currentInventory = defaultInventory local function closeTrunk() if currentInventory?.type == 'trunk' then local coords = GetEntityCoords(playerPed, true) ---@todo animation for vans? Utils.PlayAnimAdvanced(0, 'anim@heists@fleeca_bank@scope_out@return_case', 'trevor_action', coords.x, coords.y, coords.z, 0.0, 0.0, GetEntityHeading(playerPed), 2.0, 2.0, 1000, 49, 0.25) CreateThread(function() local entity = currentInventory.entity local door = currentInventory.door Wait(900) if type(door) == 'table' then for i = 1, #door do SetVehicleDoorShut(entity, door[i], false) end else SetVehicleDoorShut(entity, door, false) end end) end end local CraftingBenches = require 'modules.crafting.client' local Vehicles = lib.load('data.vehicles') local Inventory = require 'modules.inventory.client' ---@param inv string? ---@param data any? ---@return boolean? function client.openInventory(inv, data) if invOpen then if not inv and currentInventory.type == 'newdrop' then return client.closeInventory() end if IsNuiFocused() then if inv == 'container' and currentInventory.id == PlayerData.inventory[data].metadata.container then return client.closeInventory() end if currentInventory.type == 'drop' and (not data or currentInventory.id == (type(data) == 'table' and data.id or data)) then return client.closeInventory() end if inv ~= 'drop' and inv ~= 'container' then if (data?.id or data) == currentInventory?.id then -- Triggering exports.ox_inventory:openInventory('stash', 'mystash') twice in rapid succession is weird behaviour return warn(("script tried to open inventory, but it is already open\n%s"):format(Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()))) else return client.closeInventory() end end end elseif IsNuiFocused() then -- If triggering from another nui, may need to wait for focus to end. Wait(100) -- People still complain about this being an "error" and ask "how fix" despite being a warning -- for people with above room-temperature iqs to look into resource conflicts on their own. -- if IsNuiFocused() then -- warn('other scripts have nui focus and may cause issues (e.g. disable focus, prevent input, overlap inventory window)') -- end end if inv == 'dumpster' and cache.vehicle then return lib.notify({ id = 'inventory_right_access', type = 'error', description = locale('inventory_right_access') }) end if not canOpenInventory() then return lib.notify({ id = 'inventory_player_access', type = 'error', description = locale('inventory_player_access') }) end local left, right, accessError if inv == 'player' and data ~= cache.serverId then local targetId, targetPed if not data then targetId, targetPed = Utils.GetClosestPlayer() data = targetId and GetPlayerServerId(targetId) else local serverId = type(data) == 'table' and data.id or data if serverId == cache.serverId then return end targetId = serverId and GetPlayerFromServerId(serverId) targetPed = targetId and GetPlayerPed(targetId) end local targetCoords = targetPed and GetEntityCoords(targetPed) if not targetCoords or #(targetCoords - GetEntityCoords(playerPed)) > 1.8 or not (client.hasGroup(shared.police) or canOpenTarget(targetPed)) then return lib.notify({ id = 'inventory_right_access', type = 'error', description = locale('inventory_right_access') }) end end if inv == 'shop' and invOpen == false then if cache.vehicle then return lib.notify({ id = 'cannot_perform', type = 'error', description = locale('cannot_perform') }) end left, right, accessError = lib.callback.await('ox_inventory:openShop', 200, data) elseif inv == 'crafting' then if cache.vehicle then return lib.notify({ id = 'cannot_perform', type = 'error', description = locale('cannot_perform') }) end left, right, accessError = lib.callback.await('ox_inventory:openCraftingBench', 200, data.id, data.index) if left then right = CraftingBenches[data.id] if not right?.items then return end local coords, distance if not right.zones and not right.points then coords = GetEntityCoords(cache.ped) distance = 2 else coords = shared.target and right.zones and right.zones[data.index].coords or right.points and right.points[data.index] distance = coords and shared.target and right.zones[data.index].distance or 2 end right = { type = 'crafting', id = data.id, label = right.label or locale('crafting_bench'), index = data.index, slots = right.slots, items = right.items, coords = coords, distance = distance } end elseif invOpen ~= nil then if inv == 'policeevidence' then if not data then local input = lib.inputDialog(locale('police_evidence'), { { label = locale('locker_number'), type = 'number', required = true, icon = 'calculator' } }) --[[@as number[]? ]] if not input then return end data = input[1] end end left, right, accessError = lib.callback.await('ox_inventory:openInventory', false, inv, data) end if accessError then return lib.notify({ id = accessError, type = 'error', description = locale(accessError) }) end -- Stash does not exist if not left then if left == false then return false end if invOpen == false then return lib.notify({ id = 'inventory_right_access', type = 'error', description = locale('inventory_right_access') }) end if invOpen then return client.closeInventory() end end if not cache.vehicle then if inv == 'player' then Utils.PlayAnim(0, 'mp_common', 'givetake1_a', 8.0, 1.0, 2000, 50, 0.0, 0, 0, 0) elseif inv ~= 'trunk' then Utils.PlayAnim(0, 'pickup_object', 'putdown_low', 5.0, 1.5, 1000, 48, 0.0, 0, 0, 0) end end plyState.invOpen = true SetInterval(client.interval, 100) SetNuiFocus(true, true) SetNuiFocusKeepInput(true) closeTrunk() if client.screenblur then TriggerScreenblurFadeIn(0) end currentInventory = right or defaultInventory left.items = PlayerData.inventory left.groups = PlayerData.groups SendNUIMessage({ action = 'setupInventory', data = { leftInventory = left, rightInventory = currentInventory } }) if not currentInventory.coords and not inv == 'container' then currentInventory.coords = GetEntityCoords(playerPed) end if inv == 'trunk' then SetTimeout(200, function() ---@todo animation for vans? Utils.PlayAnim(0, 'anim@heists@prison_heiststation@cop_reactions', 'cop_b_idle', 3.0, 3.0, -1, 49, 0.0, 0, 0, 0) local entity = data.entity or NetworkGetEntityFromNetworkId(data.netid) currentInventory.entity = entity currentInventory.door = data.door if not currentInventory.door then local vehicleHash = GetEntityModel(entity) local vehicleClass = GetVehicleClass(entity) currentInventory.door = vehicleClass == 12 and { 2, 3 } or Vehicles.Storage[vehicleHash] and 4 or 5 end while currentInventory?.entity == entity and invOpen and DoesEntityExist(entity) and Inventory.CanAccessTrunk(entity) do Wait(100) end if invOpen then client.closeInventory() end end) end return true end RegisterNetEvent('ox_inventory:openInventory', client.openInventory) exports('openInventory', client.openInventory) RegisterNetEvent('ox_inventory:forceOpenInventory', function(left, right) if source == '' then return end plyState.invOpen = true SetInterval(client.interval, 100) SetNuiFocus(true, true) SetNuiFocusKeepInput(true) closeTrunk() if client.screenblur then TriggerScreenblurFadeIn(0) end currentInventory = right or defaultInventory currentInventory.ignoreSecurityChecks = true left.items = PlayerData.inventory left.groups = PlayerData.groups SendNUIMessage({ action = 'setupInventory', data = { leftInventory = left, rightInventory = currentInventory } }) end) local Animations = lib.load('data.animations') local Items = require 'modules.items.client' local usingItem = false ---@param data { name: string, label: string, count: number, slot: number, metadata: table, weight: number } lib.callback.register('ox_inventory:usingItem', function(data, noAnim) local item = Items[data.name] if item and usingItem then if not item.client then return true end ---@cast item +OxClientProps item = item.client if type(item.anim) == 'string' then item.anim = Animations.anim[item.anim] end if item.prop then if item.prop[1] then for i = 1, #item.prop do if type(item.prop) == 'string' then item.prop = Animations.prop[item.prop[i]] end end elseif type(item.prop) == 'string' then item.prop = Animations.prop[item.prop] end end if not item.disable then item.disable = { combat = true } elseif item.disable.combat == nil then -- Backwards compatibility; you probably don't want people shooting while eating and bandaging anyway item.disable.combat = true end local success = (not item.usetime or noAnim or lib.progressBar({ duration = item.usetime, label = item.label or locale('using', data.metadata.label or data.label), useWhileDead = item.useWhileDead, canCancel = item.cancel, disable = item.disable, anim = item.anim or item.scenario, prop = item.prop --[[@as ProgressProps]] })) and not PlayerData.dead if success then if item.notification then lib.notify({ description = item.notification }) end if item.status then if client.setPlayerStatus then client.setPlayerStatus(item.status) end end return true end end end) local function canUseItem(isAmmo) local ped = cache.ped return not usingItem and (not isAmmo or currentWeapon) and PlayerData.loaded and not PlayerData.dead and not invBusy and not lib.progressActive() and not IsPedRagdoll(ped) and not IsPedFalling(ped) and not IsPedShooting(playerPed) end ---@param data table ---@param cb fun(response: SlotWithItem | false)? ---@param noAnim? boolean local function useItem(data, cb, noAnim) local slotData, result = PlayerData.inventory[data.slot] if not slotData or not canUseItem(data.ammo and true) then if currentWeapon then return lib.notify({ id = 'cannot_perform', type = 'error', description = locale('cannot_perform') }) end return end if currentWeapon and currentWeapon.timer ~= 0 then if not currentWeapon.timer or currentWeapon.timer - GetGameTimer() > 100 then return end DisablePlayerFiring(cache.playerId, true) end if invOpen and data.close then client.closeInventory() end usingItem = true ---@type boolean? result = lib.callback.await('ox_inventory:useItem', 200, data.name, data.slot, slotData.metadata, noAnim) if result and cb then local success, response = pcall(cb, result and slotData) if not success and response then warn(('^1An error occurred while calling item "%s" callback!\n^1SCRIPT ERROR: %s^0'):format(slotData.name, response)) end end if result then TriggerEvent('ox_inventory:usedItem', slotData.name, slotData.slot, next(slotData.metadata) and slotData.metadata) end Wait(500) usingItem = false end AddEventHandler('ox_inventory:usedItem', function(name, slot, metadata) TriggerServerEvent('ox_inventory:usedItemInternal', slot) end) AddEventHandler('ox_inventory:item', useItem) exports('useItem', useItem) ---@param slot number ---@return boolean? local function useSlot(slot, noAnim) local item = PlayerData.inventory[slot] if not item then return end local data = Items[item.name] if not data then return end if canUseItem(data.ammo and true) then if data.component and not currentWeapon then return lib.notify({ id = 'weapon_hand_required', type = 'error', description = locale('weapon_hand_required') }) end local durability = item.metadata.durability --[[@as number?]] local consume = data.consume --[[@as number?]] local label = item.metadata.label or item.label --[[@as string]] -- Naive durability check to get an early exit -- People often don't call the 'useItem' export and then complain about "broken" items being usable -- This won't work with degradation since we need access to os.time on the server if durability and durability <= 100 and consume then if durability <= 0 then return lib.notify({ type = 'error', description = locale('no_durability', label) }) elseif consume ~= 0 and consume < 1 and durability < consume * 100 then return lib.notify({ type = 'error', description = locale('not_enough_durability', label) }) end end data.slot = slot if item.metadata.container then return client.openInventory('container', item.slot) elseif data.client then if invOpen and data.close then client.closeInventory() end if data.export then return data.export(data, {name = item.name, slot = item.slot, metadata = item.metadata}) elseif data.client.event then -- re-add it, so I don't need to deal with morons taking screenshots of errors when using trigger event return TriggerEvent(data.client.event, data, {name = item.name, slot = item.slot, metadata = item.metadata}) end end if data.effect then data:effect({name = item.name, slot = item.slot, metadata = item.metadata}) elseif data.weapon then if EnableWeaponWheel or not plyState.canUseWeapons then return end if IsCinematicCamRendering() then SetCinematicModeActive(false) end if currentWeapon then if not currentWeapon.timer or currentWeapon.timer ~= 0 then return end local weaponSlot = currentWeapon.slot currentWeapon = Weapon.Disarm(currentWeapon) if weaponSlot == data.slot then return end end GiveWeaponToPed(playerPed, data.hash, 0, false, true) SetCurrentPedWeapon(playerPed, data.hash, false) if data.hash ~= GetSelectedPedWeapon(playerPed) then lib.print.info(('%s cannot be used in current context (default game behaviour)'):format(item.name)) return lib.notify({ type = 'error', description = locale('cannot_use', data.label) }) end RemoveWeaponFromPed(cache.ped, data.hash) useItem(data, function(result) if result then local sleep currentWeapon, sleep = Weapon.Equip(item, data, noAnim) if sleep then Wait(sleep) end end end, noAnim) elseif currentWeapon then if data.ammo then if EnableWeaponWheel or currentWeapon.metadata.durability <= 0 then return end local clipSize = GetMaxAmmoInClip(playerPed, currentWeapon.hash, true) local currentAmmo = GetAmmoInPedWeapon(playerPed, currentWeapon.hash) local _, maxAmmo = GetMaxAmmo(playerPed, currentWeapon.hash) if maxAmmo < clipSize then clipSize = maxAmmo end if currentAmmo == clipSize then return end useItem(data, function(resp) if not resp or resp.name ~= currentWeapon?.ammo then return end if currentWeapon.metadata.specialAmmo ~= resp.metadata.type and type(currentWeapon.metadata.specialAmmo) == 'string' then local clipComponentKey = ('%s_CLIP'):format(Items[currentWeapon.name].model:gsub('WEAPON_', 'COMPONENT_')) local specialClip = ('%s_%s'):format(clipComponentKey, (resp.metadata.type or currentWeapon.metadata.specialAmmo):upper()) if type(resp.metadata.type) == 'string' then if not HasPedGotWeaponComponent(playerPed, currentWeapon.hash, specialClip) then if not DoesWeaponTakeWeaponComponent(currentWeapon.hash, specialClip) then warn('cannot use clip with this weapon') return end local defaultClip = ('%s_01'):format(clipComponentKey) if not HasPedGotWeaponComponent(playerPed, currentWeapon.hash, defaultClip) then warn('cannot use clip with currently equipped clip') return end if currentAmmo > 0 then warn('cannot mix special ammo with base ammo') return end currentWeapon.metadata.specialAmmo = resp.metadata.type GiveWeaponComponentToPed(playerPed, currentWeapon.hash, specialClip) end elseif HasPedGotWeaponComponent(playerPed, currentWeapon.hash, specialClip) then if currentAmmo > 0 then warn('cannot mix special ammo with base ammo') return end currentWeapon.metadata.specialAmmo = nil RemoveWeaponComponentFromPed(playerPed, currentWeapon.hash, specialClip) end end if maxAmmo > clipSize then clipSize = GetMaxAmmoInClip(playerPed, currentWeapon.hash, true) end currentAmmo = GetAmmoInPedWeapon(playerPed, currentWeapon.hash) local missingAmmo = clipSize - currentAmmo local addAmmo = resp.count > missingAmmo and missingAmmo or resp.count local newAmmo = currentAmmo + addAmmo if newAmmo == currentAmmo then return end AddAmmoToPed(playerPed, currentWeapon.hash, addAmmo) if cache.vehicle then if cache.seat > -1 or IsVehicleStopped(cache.vehicle) then TaskReloadWeapon(playerPed, true) else -- This is a hacky solution for forcing ammo to properly load into the -- weapon clip while driving; without it, ammo will be added but won't -- load until the player stops doing anything. i.e. if you keep shooting, -- the weapon will not reload until the clip empties. -- And yes - for some reason RefillAmmoInstantly needs to run in a loop. lib.waitFor(function() RefillAmmoInstantly(playerPed) local _, ammo = GetAmmoInClip(playerPed, currentWeapon.hash) return ammo == newAmmo or nil end) end else Wait(100) MakePedReload(playerPed) SetTimeout(100, function() while IsPedReloading(playerPed) do DisableControlAction(0, 22, true) Wait(0) end end) end lib.callback.await('ox_inventory:updateWeapon', false, 'load', newAmmo, false, currentWeapon.metadata.specialAmmo) end) elseif data.component then local components = data.client.component if not components then return end local componentType = data.type local weaponComponents = PlayerData.inventory[currentWeapon.slot].metadata.components -- Checks if the weapon already has the same component type attached for componentIndex = 1, #weaponComponents do if componentType == Items[weaponComponents[componentIndex]].type then return lib.notify({ id = 'component_slot_occupied', type = 'error', description = locale('component_slot_occupied', componentType) }) end end for i = 1, #components do local component = components[i] if DoesWeaponTakeWeaponComponent(currentWeapon.hash, component) then if HasPedGotWeaponComponent(playerPed, currentWeapon.hash, component) then lib.notify({ id = 'component_has', type = 'error', description = locale('component_has', label) }) else useItem(data, function(data) if data then local success = lib.callback.await('ox_inventory:updateWeapon', false, 'component', tostring(data.slot), currentWeapon.slot) if success then GiveWeaponComponentToPed(playerPed, currentWeapon.hash, component) TriggerEvent('ox_inventory:updateWeaponComponent', 'added', component, data.name) end end end) end return end end lib.notify({ id = 'component_invalid', type = 'error', description = locale('component_invalid', label) }) elseif data.allowArmed then useItem(data) else return lib.notify({ id = 'cannot_perform', type = 'error', description = locale('cannot_perform') }) end elseif not data.ammo and not data.component then useItem(data) end end end exports('useSlot', useSlot) ---@param id number ---@param slot number local function useButton(id, slot) if PlayerData.loaded and not invBusy and not lib.progressActive() then local item = PlayerData.inventory[slot] if not item then return end local data = Items[item.name] local buttons = data?.buttons if buttons and buttons[id]?.action then buttons[id].action(slot) end end end local function openNearbyInventory() client.openInventory('player') end exports('openNearbyInventory', openNearbyInventory) local currentInstance local playerCoords local Shops = require 'modules.shops.client' ---@todo remove or replace when the bridge module gets restructured function OnPlayerData(key, val) if key ~= 'groups' and key ~= 'ped' and key ~= 'dead' then return end if key == 'groups' then Inventory.Stashes() Inventory.Evidence() Shops.refreshShops() elseif key == 'dead' and val then currentWeapon = Weapon.Disarm(currentWeapon) client.closeInventory() end Utils.WeaponWheel() end -- People consistently ignore errors when one of the "modules" failed to load if not Utils or not Weapon or not Items or not Inventory then return end local invHotkeys = false ---@type function? local function registerCommands() RegisterCommand('steal', openNearbyInventory, false) local function openGlovebox(vehicle) if not IsPedInAnyVehicle(playerPed, false) or not NetworkGetEntityIsNetworked(vehicle) then return end local vehicleHash = GetEntityModel(vehicle) local vehicleClass = GetVehicleClass(vehicle) local checkVehicle = Vehicles.Storage[vehicleHash] -- No storage or no glovebox if (checkVehicle == 0 or checkVehicle == 2) or (not Vehicles.glovebox[vehicleClass] and not Vehicles.glovebox.models[vehicleHash]) then return end local isOpen = client.openInventory('glovebox', { id = 'glove'..GetVehicleNumberPlateText(vehicle), netid = NetworkGetNetworkIdFromEntity(vehicle) }) if isOpen then currentInventory.entity = vehicle end end local primary = lib.addKeybind({ name = 'inv', description = locale('open_player_inventory'), defaultKey = client.keys[1], onPressed = function() if invOpen then return client.closeInventory() end if cache.vehicle then return openGlovebox(cache.vehicle) end local closest = lib.points.getClosestPoint() if closest and closest.currentDistance < 1.2 and (not closest.instance or closest.instance == currentInstance) then if closest.inv == 'crafting' then return client.openInventory('crafting', { id = closest.id, index = closest.index }) elseif closest.inv ~= 'license' and closest.inv ~= 'policeevidence' then return client.openInventory(closest.inv or 'drop', { id = closest.invId, type = closest.type }) end end return client.openInventory() end }) lib.addKeybind({ name = 'inv2', description = locale('open_secondary_inventory'), defaultKey = client.keys[2], onPressed = function(self) if primary:getCurrentKey() == self:getCurrentKey() then return warn(("secondary inventory keybind '%s' disabled (keybind cannot match primary inventory keybind)"):format(self:getCurrentKey())) end if invOpen then return client.closeInventory() end if invBusy or not canOpenInventory() then return lib.notify({ id = 'inventory_player_access', type = 'error', description = locale('inventory_player_access') }) end if StashTarget then return client.openInventory('stash', StashTarget) end if cache.vehicle then return openGlovebox(cache.vehicle) end local entity, entityType = Utils.Raycast(2|16) if not entity then return end if not shared.target and entityType == 3 then local model = GetEntityModel(entity) if Inventory.Dumpsters[model] then return Inventory.OpenDumpster(entity) end end if entityType ~= 2 then return end Inventory.OpenTrunk(entity) end }) lib.addKeybind({ name = 'reloadweapon', description = locale('reload_weapon'), defaultKey = 'r', onPressed = function(self) if not currentWeapon or EnableWeaponWheel or not canUseItem(true) then return end if currentWeapon.ammo then if currentWeapon.metadata.durability > 0 then local slotId = Inventory.GetSlotIdWithItem(currentWeapon.ammo, { type = currentWeapon.metadata.specialAmmo }, false) if slotId then useSlot(slotId) end else lib.notify({ id = 'no_durability', type = 'error', description = locale('no_durability', currentWeapon.label) }) end end end }) lib.addKeybind({ name = 'hotbar', description = locale('disable_hotbar'), defaultKey = client.keys[3], onPressed = function() if EnableWeaponWheel or IsNuiFocused() or lib.progressActive() then return end SendNUIMessage({ action = 'toggleHotbar' }) end }) for i = 1, 5 do lib.addKeybind({ name = ('hotkey%s'):format(i), description = locale('use_hotbar', i), defaultKey = tostring(i), onPressed = function() if invOpen or EnableWeaponWheel or not invHotkeys or IsNuiFocused() then return end useSlot(i) end }) end registerCommands = nil end function client.closeInventory(server) -- because somehow people are triggering this when the inventory isn't loaded -- and they're incapable of debugging, and I can't repro on a fresh install if not client.interval then return end if invOpen then invOpen = nil SetNuiFocus(false, false) SetNuiFocusKeepInput(false) TriggerScreenblurFadeOut(0) closeTrunk() SendNUIMessage({ action = 'closeInventory' }) SetInterval(client.interval, 200) Wait(200) if invOpen ~= nil then return end if not server and currentInventory then TriggerServerEvent('ox_inventory:closeInventory') end currentInventory = nil plyState.invOpen = false defaultInventory.coords = nil end end RegisterNetEvent('ox_inventory:closeInventory', client.closeInventory) exports('closeInventory', client.closeInventory) ---@param data updateSlot[] ---@param weight number local function updateInventory(data, weight) local changes = {} ---@type table local itemCount = {} for i = 1, #data do local v = data[i] if not v.inventory or v.inventory == cache.serverId then v.inventory = 'player' local item = v.item if currentWeapon?.slot == item?.slot then if item.metadata then currentWeapon.metadata = item.metadata TriggerEvent('ox_inventory:currentWeapon', currentWeapon) else currentWeapon = Weapon.Disarm(currentWeapon, true) end end local curItem = PlayerData.inventory[item.slot] if curItem and curItem.name then itemCount[curItem.name] = (itemCount[curItem.name] or 0) - curItem.count end if item.count then itemCount[item.name] = (itemCount[item.name] or 0) + item.count end changes[item.slot] = item.count and item or false if not item.count then item.name = nil end PlayerData.inventory[item.slot] = item.name and item or nil end end SendNUIMessage({ action = 'refreshSlots', data = { items = data, itemCount = itemCount} }) if weight ~= PlayerData.weight then client.setPlayerData('weight', weight) end for itemName, count in pairs(itemCount) do local item = Items(itemName) if item then item.count += count TriggerEvent('ox_inventory:itemCount', item.name, item.count) if count < 0 then if shared.framework == 'esx' then TriggerEvent('esx:removeInventoryItem', item.name, item.count) end if item.client?.remove then item.client.remove(item.count) end elseif count > 0 then if shared.framework == 'esx' then TriggerEvent('esx:addInventoryItem', item.name, item.count) end if item.client?.add then item.client.add(item.count) end end end end client.setPlayerData('inventory', PlayerData.inventory) TriggerEvent('ox_inventory:updateInventory', changes) end RegisterNetEvent('ox_inventory:updateSlots', function(items, weights) if source ~= '' and next(items) then updateInventory(items, weights) end end) RegisterNetEvent('ox_inventory:inventoryReturned', function(data) if source == '' then return end if currentWeapon then currentWeapon = Weapon.Disarm(currentWeapon) end lib.notify({ description = locale('items_returned') }) client.closeInventory() local num, items = 0, {} for _, slotData in pairs(data[1]) do num += 1 items[num] = { item = slotData, inventory = cache.serverId } end updateInventory(items, data[3]) end) RegisterNetEvent('ox_inventory:inventoryConfiscated', function(message) if source == '' then return end if message then lib.notify({ description = locale('items_confiscated') }) end if currentWeapon then currentWeapon = Weapon.Disarm(currentWeapon) end client.closeInventory() local num, items = 0, {} for slot in pairs(PlayerData.inventory) do num += 1 items[num] = { item = { slot = slot }, inventory = cache.serverId } end updateInventory(items, 0) end) ---@param point CPoint local function nearbyDrop(point) if not point.instance or point.instance == currentInstance then ---@diagnostic disable-next-line: param-type-mismatch DrawMarker(2, point.coords.x, point.coords.y, point.coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.2, 0.15, 150, 30, 30, 222, false, false, 0, true, false, false, false) end end ---@param point CPoint local function onEnterDrop(point) if not point.instance or point.instance == currentInstance and not point.entity then local model = point.model or client.dropmodel -- Prevent breaking inventory on invalid point.model instead use default client.dropmodel if not IsModelValid(model) and not IsModelInCdimage(model) then model = client.dropmodel end lib.requestModel(model) local entity = CreateObject(model, point.coords.x, point.coords.y, point.coords.z, false, true, true) SetModelAsNoLongerNeeded(model) PlaceObjectOnGroundProperly(entity) FreezeEntityPosition(entity, true) SetEntityCollision(entity, false, true) point.entity = entity end end local function onExitDrop(point) local entity = point.entity if entity then Utils.DeleteEntity(entity) point.entity = nil end end local function createDrop(dropId, data) local point = lib.points.new({ coords = data.coords, distance = 16, invId = dropId, instance = data.instance, model = data.model }) if point.model or client.dropprops then point.distance = 30 point.onEnter = onEnterDrop point.onExit = onExitDrop else point.nearby = nearbyDrop end client.drops[dropId] = point end RegisterNetEvent('ox_inventory:createDrop', function(dropId, data, owner, slot) if client.drops then createDrop(dropId, data) end if owner == cache.serverId then if currentWeapon?.slot == slot then currentWeapon = Weapon.Disarm(currentWeapon) end if invOpen and #(GetEntityCoords(playerPed) - data.coords) <= 1 then if not cache.vehicle then client.openInventory('drop', dropId) else SendNUIMessage({ action = 'setupInventory', data = { rightInventory = currentInventory } }) end end end end) RegisterNetEvent('ox_inventory:removeDrop', function(dropId) if client.drops then local point = client.drops[dropId] if point then client.drops[dropId] = nil point:remove() if point.entity then Utils.DeleteEntity(point.entity) end end end end) ---@type function? local function setStateBagHandler(stateId) AddStateBagChangeHandler('invOpen', stateId, function(_, _, value) invOpen = value end) AddStateBagChangeHandler('invBusy', stateId, function(_, _, value) invBusy = value end) AddStateBagChangeHandler('canUseWeapons', stateId, function(_, _, value) if not value and currentWeapon then currentWeapon = Weapon.Disarm(currentWeapon) end end) AddStateBagChangeHandler('instance', stateId, function(_, _, value) currentInstance = value if client.drops then -- Iterate over known drops and remove any points in a different instance (ignoring no instance) for dropId, point in pairs(client.drops) do if point.instance then if point.instance ~= value then if point.entity then Utils.DeleteEntity(point.entity) point.entity = nil end point:remove() else -- Recreate the drop using data from the old point createDrop(dropId, point) end end end end end) AddStateBagChangeHandler('dead', stateId, function(_, _, value) Utils.WeaponWheel() PlayerData.dead = value end) AddStateBagChangeHandler('invHotkeys', stateId, function(_, _, value) invHotkeys = value end) setStateBagHandler = nil end lib.onCache('seat', function(seat) if seat then local hasWeapon = GetCurrentPedVehicleWeapon(cache.ped) if hasWeapon then return Utils.WeaponWheel(true) end end Utils.WeaponWheel(false) end) lib.onCache('vehicle', function() if invOpen and (not currentInventory.entity or currentInventory.entity == cache.vehicle) then return client.closeInventory() end end) RegisterNetEvent('ox_inventory:setPlayerInventory', function(currentDrops, inventory, weight, player) if source == '' then return end ---@class PlayerData ---@field inventory table ---@field weight number PlayerData = player PlayerData.id = cache.playerId PlayerData.source = cache.serverId PlayerData.maxWeight = shared.playerweight setmetatable(PlayerData, { __index = function(self, key) if key == 'ped' then return PlayerPedId() end end }) if setStateBagHandler then setStateBagHandler(('player:%s'):format(cache.serverId)) end local ItemData = table.create(0, #Items) for _, v in pairs(Items --[[@as table]]) do local buttons = v.buttons and {} or nil if buttons then for i = 1, #v.buttons do buttons[i] = {label = v.buttons[i].label, group = v.buttons[i].group} end end ItemData[v.name] = { label = v.label, stack = v.stack, close = v.close, count = 0, description = v.description, buttons = buttons, ammoName = v.ammoname, image = v.client?.image } end for _, data in pairs(inventory) do local item = Items[data.name] if item then item.count += data.count ItemData[data.name].count += data.count local add = item.client?.add if add then add(item.count) end end end local phone = Items.phone if phone and phone.count < 1 then pcall(function() return exports.npwd:setPhoneDisabled(true) end) end client.setPlayerData('inventory', inventory) client.setPlayerData('weight', weight) currentWeapon = nil Weapon.ClearAll() local uiLocales = {} local locales = lib.getLocales() for k, v in pairs(locales) do if k:find('^ui_')then uiLocales[k] = v end end uiLocales['$'] = locales['$'] uiLocales.ammo_type = locales.ammo_type client.drops = currentDrops for dropId, data in pairs(currentDrops) do createDrop(dropId, data) end local hasTextUi local uiOptions = { icon = 'fa-id-card' } ---@param point CPoint local function nearbyLicense(point) ---@diagnostic disable-next-line: param-type-mismatch DrawMarker(2, point.coords.x, point.coords.y, point.coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.2, 0.15, 30, 150, 30, 222, false, false, 0, true, false, false, false) if point.isClosest and point.currentDistance < 1.2 then if not hasTextUi then hasTextUi = point lib.showTextUI(point.message, uiOptions) end if IsControlJustReleased(0, 38) then lib.callback('ox_inventory:buyLicense', 1000, function(success, message) if success ~= nil then lib.notify({ id = message, type = success == false and 'error' or 'success', description = locale(message, locale('license', point.type:gsub("^%l", string.upper))) }) end end, point.invId) end elseif hasTextUi == point then hasTextUi = false lib.hideTextUI() end end for id, data in pairs(lib.load('data.licenses')) do lib.points.new({ coords = data.coords, distance = 16, inv = 'license', type = data.name, price = data.price, invId = id, nearby = nearbyLicense, message = ('**%s** \n%s'):format(locale('purchase_license', data.name), locale('interact_prompt', GetControlInstructionalButton(0, 38, true):sub(3))) }) end while not client.uiLoaded do Wait(50) end SendNUIMessage({ action = 'init', data = { locale = uiLocales, items = ItemData, leftInventory = { id = cache.playerId, slots = shared.playerslots, items = PlayerData.inventory, maxWeight = shared.playerweight, }, imagepath = client.imagepath } }) PlayerData.loaded = true lib.notify({ description = locale('inventory_setup') }) Shops.refreshShops() Inventory.Stashes() Inventory.Evidence() if registerCommands then registerCommands() end TriggerEvent('ox_inventory:updateInventory', PlayerData.inventory) client.interval = SetInterval(function() if invOpen == false then playerCoords = GetEntityCoords(playerPed) if currentWeapon and IsPedUsingActionMode(playerPed) then SetPedUsingActionMode(playerPed, false, -1, 'DEFAULT_ACTION') end elseif invOpen == true then if not canOpenInventory() then client.closeInventory() else playerCoords = GetEntityCoords(playerPed) if currentInventory and not currentInventory.ignoreSecurityChecks then local maxDistance = (currentInventory.distance or currentInventory.type == 'stash' and 4.8 or 1.8) + 0.2 if currentInventory.type == 'otherplayer' then local id = GetPlayerFromServerId(currentInventory.id) local ped = GetPlayerPed(id) local pedCoords = GetEntityCoords(ped) if not id or #(playerCoords - pedCoords) > maxDistance or not (client.hasGroup(shared.police) or canOpenTarget(ped)) then client.closeInventory() lib.notify({ id = 'inventory_lost_access', type = 'error', description = locale('inventory_lost_access') }) else TaskTurnPedToFaceCoord(playerPed, pedCoords.x, pedCoords.y, pedCoords.z, 50) end elseif currentInventory.coords and (#(playerCoords - currentInventory.coords) > maxDistance or canOpenTarget(playerPed)) then client.closeInventory() lib.notify({ id = 'inventory_lost_access', type = 'error', description = locale('inventory_lost_access') }) end end end end if client.parachute and GetPedParachuteState(playerPed) ~= -1 then Utils.DeleteEntity(client.parachute[1]) client.parachute = false end if EnableWeaponWheel then return end local weaponHash = GetSelectedPedWeapon(playerPed) if currentWeapon then if weaponHash ~= currentWeapon.hash and currentWeapon.timer then local weaponCount = Items[currentWeapon.name]?.count if weaponCount > 0 then SetCurrentPedWeapon(playerPed, currentWeapon.hash, true) SetAmmoInClip(playerPed, currentWeapon.hash, currentWeapon.metadata.ammo) SetPedCurrentWeaponVisible(playerPed, true, false, false, false) weaponHash = GetSelectedPedWeapon(playerPed) end if weaponHash ~= currentWeapon.hash then lib.print.info(('%s cannot be used in current context (default game behaviour)'):format(currentWeapon.name)) currentWeapon = Weapon.Disarm(currentWeapon, true) end end elseif client.weaponmismatch and not client.ignoreweapons[weaponHash] then local weaponType = GetWeapontypeGroup(weaponHash) if weaponType ~= 0 and weaponType ~= `GROUP_UNARMED` then Weapon.Disarm(currentWeapon, true) end end end, 200) local playerId = cache.playerId local EnableKeys = client.enablekeys local DisablePlayerVehicleRewards = DisablePlayerVehicleRewards local DisableAllControlActions = DisableAllControlActions local HideHudAndRadarThisFrame = HideHudAndRadarThisFrame local EnableControlAction = EnableControlAction local DisablePlayerFiring = DisablePlayerFiring local HudWeaponWheelIgnoreSelection = HudWeaponWheelIgnoreSelection local DisableControlAction = DisableControlAction local IsPedShooting = IsPedShooting local IsControlJustReleased = IsControlJustReleased client.tick = SetInterval(function() DisablePlayerVehicleRewards(playerId) if invOpen then DisableAllControlActions(0) HideHudAndRadarThisFrame() for i = 1, #EnableKeys do EnableControlAction(0, EnableKeys[i], true) end if currentInventory.type == 'newdrop' then EnableControlAction(0, 30, true) EnableControlAction(0, 31, true) end else if invBusy then DisableControlAction(0, 23, true) DisableControlAction(0, 36, true) end if usingItem or invOpen or IsPedCuffed(playerPed) then DisablePlayerFiring(playerId, true) end if not EnableWeaponWheel then HudWeaponWheelIgnoreSelection() DisableControlAction(0, 37, true) end if currentWeapon and currentWeapon.timer then DisableControlAction(0, 80, true) DisableControlAction(0, 140, true) if currentWeapon.metadata.durability <= 0 or not currentWeapon.timer then DisablePlayerFiring(playerId, true) elseif client.aimedfiring and not currentWeapon.melee and currentWeapon.group ~= `GROUP_PETROLCAN` and not IsPlayerFreeAiming(playerId) then DisablePlayerFiring(playerId, true) end local weaponAmmo = currentWeapon.metadata.ammo if not invBusy and currentWeapon.timer ~= 0 and currentWeapon.timer < GetGameTimer() then currentWeapon.timer = 0 if weaponAmmo then TriggerServerEvent('ox_inventory:updateWeapon', 'ammo', weaponAmmo) if client.autoreload and currentWeapon.ammo and GetAmmoInPedWeapon(playerPed, currentWeapon.hash) == 0 then local slotId = Inventory.GetSlotIdWithItem(currentWeapon.ammo, { type = currentWeapon.metadata.specialAmmo }, false) if slotId then CreateThread(function() useSlot(slotId) end) end end elseif currentWeapon.metadata.durability then TriggerServerEvent('ox_inventory:updateWeapon', 'melee', currentWeapon.melee) currentWeapon.melee = 0 end elseif weaponAmmo then if IsPedShooting(playerPed) then local currentAmmo local durabilityDrain = Items[currentWeapon.name].durability if currentWeapon.group == `GROUP_PETROLCAN` or currentWeapon.group == `GROUP_FIREEXTINGUISHER` then currentAmmo = weaponAmmo - durabilityDrain < 0 and 0 or weaponAmmo - durabilityDrain currentWeapon.metadata.durability = currentAmmo currentWeapon.metadata.ammo = (weaponAmmo < currentAmmo) and 0 or currentAmmo if currentAmmo <= 0 then SetPedInfiniteAmmo(playerPed, false, currentWeapon.hash) end else currentAmmo = GetAmmoInPedWeapon(playerPed, currentWeapon.hash) if currentAmmo < weaponAmmo then currentAmmo = (weaponAmmo < currentAmmo) and 0 or currentAmmo currentWeapon.metadata.ammo = currentAmmo currentWeapon.metadata.durability = currentWeapon.metadata.durability - (durabilityDrain * math.abs((weaponAmmo or 0.1) - currentAmmo)) end end if currentAmmo <= 0 then if cache.vehicle then TaskSwapWeapon(playerPed, true) end currentWeapon.timer = GetGameTimer() + 200 else currentWeapon.timer = GetGameTimer() + (GetWeaponTimeBetweenShots(currentWeapon.hash) * 1000) + 100 end end elseif currentWeapon.throwable then if not invBusy and IsControlPressed(0, 24) then invBusy = 1 CreateThread(function() local weapon = currentWeapon while currentWeapon and (not IsPedWeaponReadyToShoot(cache.ped) or IsDisabledControlPressed(0, 24)) and GetSelectedPedWeapon(playerPed) == weapon.hash do Wait(0) end if GetSelectedPedWeapon(playerPed) == weapon.hash then Wait(700) end while IsPedPlantingBomb(playerPed) do Wait(0) end TriggerServerEvent('ox_inventory:updateWeapon', 'throw', nil, weapon.slot) plyState:set('invBusy', false, true) currentWeapon = nil RemoveWeaponFromPed(playerPed, weapon.hash) TriggerEvent('ox_inventory:currentWeapon') end) end elseif currentWeapon.melee and IsControlJustReleased(0, 24) and IsPedPerformingMeleeAction(playerPed) then currentWeapon.melee += 1 currentWeapon.timer = GetGameTimer() + 200 end end end end) plyState:set('invBusy', false, true) plyState:set('invOpen', false, false) plyState:set('invHotkeys', true, false) plyState:set('canUseWeapons', true, false) collectgarbage('collect') end) AddEventHandler('onResourceStop', function(resourceName) if shared.resource == resourceName then client.onLogout() end end) RegisterNetEvent('ox_inventory:viewInventory', function(left, right) if source == '' then return end plyState.invOpen = true SetInterval(client.interval, 100) SetNuiFocus(true, true) SetNuiFocusKeepInput(true) closeTrunk() if client.screenblur then TriggerScreenblurFadeIn(0) end currentInventory = right or defaultInventory currentInventory.ignoreSecurityChecks = true currentInventory.type = 'inspect' left.items = PlayerData.inventory left.groups = PlayerData.groups SendNUIMessage({ action = 'setupInventory', data = { leftInventory = left, rightInventory = currentInventory } }) end) RegisterNUICallback('uiLoaded', function(_, cb) client.uiLoaded = true cb(1) end) RegisterNUICallback('getItemData', function(itemName, cb) cb(Items[itemName]) end) RegisterNUICallback('removeComponent', function(data, cb) cb(1) if not currentWeapon then return TriggerServerEvent('ox_inventory:updateWeapon', 'component', data) end if data.slot ~= currentWeapon.slot then return lib.notify({ id = 'weapon_hand_wrong', type = 'error', description = locale('weapon_hand_wrong') }) end local itemSlot = PlayerData.inventory[currentWeapon.slot] if not itemSlot then return end for _, component in pairs(Items[data.component].client.component) do if HasPedGotWeaponComponent(playerPed, currentWeapon.hash, component) then for k, v in pairs(itemSlot.metadata.components) do if v == data.component then local success = lib.callback.await('ox_inventory:updateWeapon', false, 'component', k) if success then RemoveWeaponComponentFromPed(playerPed, currentWeapon.hash, component) TriggerEvent('ox_inventory:updateWeaponComponent', 'removed', component, data.component) end break end end end end end) RegisterNUICallback('removeAmmo', function(slot, cb) cb(1) local slotData = PlayerData.inventory[slot] if not slotData or not slotData.metadata.ammo or slotData.metadata.ammo == 0 then return end local success = lib.callback.await('ox_inventory:removeAmmoFromWeapon', false, slot) if success and slot == currentWeapon?.slot then SetPedAmmo(playerPed, currentWeapon.hash, 0) end end) RegisterNUICallback('useItem', function(slot, cb) useSlot(slot --[[@as number]]) cb(1) end) local function giveItemToTarget(serverId, slotId, count) if type(slotId) ~= 'number' then return TypeError('slotId', 'number', type(slotId)) end if count and type(count) ~= 'number' then return TypeError('count', 'number', type(count)) end if slotId == currentWeapon?.slot then currentWeapon = Weapon.Disarm(currentWeapon) end Utils.PlayAnim(0, 'mp_common', 'givetake1_a', 1.0, 1.0, 2000, 50, 0.0, 0, 0, 0) local notification = lib.callback.await('ox_inventory:giveItem', false, slotId, serverId, count or 0) if notification then lib.notify({ type = 'error', description = locale(table.unpack(notification)) }) end end exports('giveItemToTarget', giveItemToTarget) local function isGiveTargetValid(ped, coords) if cache.vehicle and GetVehiclePedIsIn(ped, false) == cache.vehicle then return true end local entity = Utils.Raycast(1|2|4|8|16, coords + vec3(0, 0, 0.5), 0.2) return entity == ped and IsEntityVisible(ped) end RegisterNUICallback('giveItem', function(data, cb) cb(1) if usingItem then return end if client.giveplayerlist then local nearbyPlayers = lib.getNearbyPlayers(GetEntityCoords(playerPed), 3.0) local nearbyCount = #nearbyPlayers if nearbyCount == 0 then return end if nearbyCount == 1 then local option = nearbyPlayers[1] if not isGiveTargetValid(option.ped, option.coords) then return end return giveItemToTarget(GetPlayerServerId(option.id), data.slot, data.count) end local giveList, n = {}, 0 for i = 1, #nearbyPlayers do local option = nearbyPlayers[i] if isGiveTargetValid(option.ped, option.coords) then local playerName = GetPlayerName(option.id) option.id = GetPlayerServerId(option.id) ---@diagnostic disable-next-line: inject-field option.label = ('[%s] %s'):format(option.id, playerName) n += 1 giveList[n] = option end end if n == 0 then return end lib.registerMenu({ id = 'ox_inventory:givePlayerList', title = 'Give item', options = giveList, }, function(selected) giveItemToTarget(giveList[selected].id, data.slot, data.count) end) return lib.showMenu('ox_inventory:givePlayerList') end if cache.vehicle then local seats = GetVehicleMaxNumberOfPassengers(cache.vehicle) - 1 if seats >= 0 then local passenger = GetPedInVehicleSeat(cache.vehicle, cache.seat - 2 * (cache.seat % 2) + 1) if passenger ~= 0 and IsEntityVisible(passenger) then return giveItemToTarget(GetPlayerServerId(NetworkGetPlayerIndexFromPed(passenger)), data.slot, data.count) end end return end local entity = Utils.Raycast(1|2|4|8|16, GetOffsetFromEntityInWorldCoords(cache.ped, 0.0, 3.0, 0.5), 0.2) if entity and IsPedAPlayer(entity) and IsEntityVisible(entity) and #(GetEntityCoords(playerPed, true) - GetEntityCoords(entity, true)) < 3.0 then return giveItemToTarget(GetPlayerServerId(NetworkGetPlayerIndexFromPed(entity)), data.slot, data.count) end end) RegisterNUICallback('useButton', function(data, cb) useButton(data.id, data.slot) cb(1) end) RegisterNUICallback('exit', function(_, cb) client.closeInventory() cb(1) end) lib.callback.register('ox_inventory:startCrafting', function(id, recipe) recipe = CraftingBenches[id].items[recipe] return lib.progressCircle({ label = locale('crafting_item', recipe.metadata?.label or Items[recipe.name].label), duration = recipe.duration or 3000, canCancel = true, disable = { move = true, combat = true, }, anim = { dict = 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', clip = 'machinic_loop_mechandplayer', } }) end) local swapActive = false ---Synchronise and validate all item movement between the NUI and server. RegisterNUICallback('swapItems', function(data, cb) if swapActive or not invOpen or invBusy or usingItem then return cb(false) end swapActive = true if data.toType == 'newdrop' then if cache.vehicle or IsPedFalling(playerPed) then swapActive = false return cb(false) end local coords = GetEntityCoords(playerPed) if IsEntityInWater(playerPed) then local destination = vec3(coords.x, coords.y, -200) local handle = StartShapeTestLosProbe(coords.x, coords.y, coords.z, destination.x, destination.y, destination.z, 511, cache.ped, 4) while true do Wait(0) local retval, hit, endCoords = GetShapeTestResult(handle) if retval ~= 1 then if not hit then return end data.coords = vec3(endCoords.x, endCoords.y, endCoords.z + 1.0) break end end else data.coords = coords end end if currentInstance then data.instance = currentInstance end if currentWeapon and data.fromType ~= data.toType then if (data.fromType == 'player' and data.fromSlot == currentWeapon.slot) or (data.toType == 'player' and data.toSlot == currentWeapon.slot) then currentWeapon = Weapon.Disarm(currentWeapon, true) end end local success, response, weaponSlot = lib.callback.await('ox_inventory:swapItems', false, data) swapActive = false cb(success or false) if success then if weaponSlot and currentWeapon then currentWeapon.slot = weaponSlot end if response then updateInventory(response.items, response.weight) end elseif response then if type(response) == 'table' then SendNUIMessage({ action = 'refreshSlots', data = { items = response } }) else lib.notify({ type = 'error', description = locale(response) }) end end end) RegisterNUICallback('buyItem', function(data, cb) ---@type boolean, false | { [1]: number, [2]: SlotWithItem, [3]: SlotWithItem | false, [4]: number}, NotifyProps local response, data, message = lib.callback.await('ox_inventory:buyItem', 100, data) if data then updateInventory({ { item = data[2], inventory = cache.serverId } }, data[4]) if data[3] then SendNUIMessage({ action = 'refreshSlots', data = { items = { { item = data[3], inventory = 'shop' } } } }) end end if message then lib.notify(message) end cb(response) end) RegisterNUICallback('craftItem', function(data, cb) cb(true) local id, index = currentInventory.id, currentInventory.index for i = 1, data.count do local success, response = lib.callback.await('ox_inventory:craftItem', 200, id, index, data.fromSlot, data.toSlot) if not success then if response then lib.notify({ type = 'error', description = locale(response or 'cannot_perform') }) end break end end if not currentInventory or currentInventory.type ~= 'crafting' then client.openInventory('crafting', { id = id, index = index }) end end) lib.callback.register('ox_inventory:getVehicleData', function(netid) local entity = NetworkGetEntityFromNetworkId(netid) if entity then return GetEntityModel(entity), GetVehicleClass(entity) end end)