From df6b06c6db357d769750ad1b8c99e71a0e935717 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Tue, 18 Aug 2026 20:06:50 +0200 Subject: [PATCH] FIX - support HEX inventory compatibility Auto-detect hex_4_inventory and add a count-based ESX adapter for non-unique, virtual-SIM phone configurations. Expose the LB Phone IsOpen alias required by HEX and document/test the supported configuration boundaries. --- README.md | 4 +- frontend/src/phoneInventory.contract.test.ts | 28 ++++- sky_phone/config/config.lua | 6 +- sky_phone/source/bridge/server/inventory.lua | 20 +++- .../source/bridge/server/inventory/esx.lua | 112 ++++++++++++++++++ sky_phone/source/client/main.lua | 4 + 6 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 sky_phone/source/bridge/server/inventory/esx.lua diff --git a/README.md b/README.md index 510ed3c..ae2df30 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,9 @@ Default entries for unique phones with physical SIM cards: Do not configure an LB Phone client event or client export. Sky Phone registers the usable items through its server-side inventory adapter. -The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ox`, `qb`, `lj`, `qs`, `codem`, `core`, `mf`, and `smx`. Resource startup fails visibly if the selected adapter cannot complete that registration. +The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ox`, `qb`, `lj`, `qs`, `codem`, `core`, `mf`, `smx`, `hex`, and `esx`. Resource startup fails visibly if the selected adapter cannot complete that registration. + +The `hex` and `esx` adapters use ESX's count-based item API. They require both `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` because this API cannot persist per-item phone or physical SIM metadata. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected. ### QBCore-style item tables diff --git a/frontend/src/phoneInventory.contract.test.ts b/frontend/src/phoneInventory.contract.test.ts index c84e079..0f8f4a9 100644 --- a/frontend/src/phoneInventory.contract.test.ts +++ b/frontend/src/phoneInventory.contract.test.ts @@ -14,6 +14,8 @@ const inventoryAdapters = [ ['core', 'source/bridge/server/inventory/core.lua'], ['mf', 'source/bridge/server/inventory/mf.lua'], ['smx', 'source/bridge/server/inventory/smx.lua'], + ['hex', 'source/bridge/server/inventory/esx.lua'], + ['esx', 'source/bridge/server/inventory/esx.lua'], ] as const describe('phone inventory contracts', () => { @@ -35,6 +37,28 @@ describe('phone inventory contracts', () => { expect(phoneServer).toContain('if not usable_registered then') }) + it('auto-detects HEX and limits count-based ESX inventories to metadata-free modes', () => { + const inventoryBridge = readResourceFile( + 'source/bridge/server/inventory.lua', + ) + + expect(inventoryBridge).toContain( + 'GetResourceState("hex_4_inventory") == "started"', + ) + expect(inventoryBridge).toContain('configured_inventory = "hex"') + expect(inventoryBridge).toContain('Config.Phone.Unique ~= false') + expect(inventoryBridge).toContain('Config.Sim.Enabled ~= false') + }) + + it('provides the LB IsOpen export alias from the authoritative client state', () => { + const phoneClient = readResourceFile('source/client/main.lua') + + expect(phoneClient).toContain( + 'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsOpen"', + ) + expect(phoneClient).toContain('return is_open') + }) + it('opens from a configurable F1 mapping without client-provided device identity', () => { const config = readResourceFile('config/config.lua') const phoneClient = readResourceFile('source/client/main.lua') @@ -56,7 +80,9 @@ describe('phone inventory contracts', () => { const phoneServer = readResourceFile('source/server/phone.lua') expect(phoneServer).toContain('local preferred_device_imeis = {}') - expect(phoneServer).toContain('local preferred_imei = preferred_device_imeis[source]') + expect(phoneServer).toContain( + 'local preferred_imei = preferred_device_imeis[source]', + ) expect(phoneServer).toContain('preferred_device_imeis[source] = imei') }) }) diff --git a/sky_phone/config/config.lua b/sky_phone/config/config.lua index f7eaa06..abdd5f0 100644 --- a/sky_phone/config/config.lua +++ b/sky_phone/config/config.lua @@ -16,7 +16,7 @@ Config.Bridge = { Framework = "auto", -- auto, esx, qbox, qb - Inventory = "auto", -- auto, ox, qb, lj, qs, codem, core, mf, smx + Inventory = "auto", -- auto, ox, qb, lj, qs, codem, core, mf, smx, hex, esx Locale = "en", CallbackTimeout = 15000, Debug = false, -- true: show debug/info output; warnings and errors are always shown @@ -26,7 +26,7 @@ Config.Command = "phone" Config.Phone = { Item = "phone", - Unique = true, -- true: data follows each phone item; false: one persistent phone per character + Unique = true, -- true: data follows each phone item; false: one persistent phone per character; hex/esx require false Keybind = "F1", -- false disables the configurable phone key mapping AllowMovement = true, -- true: game input stays active while the mobile phone is open DevelopmentCommand = true, @@ -64,7 +64,7 @@ Config.Security = { } Config.Sim = { - Enabled = true, -- false: devices without a SIM receive a persistent random number automatically + Enabled = true, -- false: devices receive a persistent random number automatically; hex/esx require false RegisteredItem = "sky_phone_sim_registered", AnonymousItem = "sky_phone_sim_anonymous", NumberLength = 10, diff --git a/sky_phone/source/bridge/server/inventory.lua b/sky_phone/source/bridge/server/inventory.lua index 42d4b83..5278d57 100644 --- a/sky_phone/source/bridge/server/inventory.lua +++ b/sky_phone/source/bridge/server/inventory.lua @@ -17,6 +17,10 @@ if configured_inventory == "auto" then configured_inventory = "mf" elseif GetResourceState("smx-inventory") == "started" then configured_inventory = "smx" + elseif GetResourceState("hex_4_inventory") == "started" then + configured_inventory = "hex" + elseif Bridge.Framework.GetName() == "esx" then + configured_inventory = "esx" end end @@ -29,10 +33,24 @@ local supported_inventories = { core = true, mf = true, smx = true, + hex = true, + esx = true, } if not supported_inventories[configured_inventory] then - error(("[sky_phone] Unsupported or unavailable inventory '%s'. A metadata-capable inventory adapter is required."):format(tostring(configured_inventory))) + error(("[sky_phone] Unsupported or unavailable inventory '%s'. Configure a supported inventory adapter."):format(tostring(configured_inventory))) +end + +if configured_inventory == "hex" or configured_inventory == "esx" then + if Bridge.Framework.GetName() ~= "esx" then + error(("[sky_phone] Inventory '%s' is only supported with ESX."):format(configured_inventory)) + end + if Config.Phone.Unique ~= false then + error(("[sky_phone] Inventory '%s' cannot store unique phone metadata. Set Config.Phone.Unique = false or configure a metadata-capable inventory."):format(configured_inventory)) + end + if Config.Sim.Enabled ~= false then + error(("[sky_phone] Inventory '%s' cannot store physical SIM metadata. Set Config.Sim.Enabled = false or configure a metadata-capable inventory."):format(configured_inventory)) + end end Bridge.Inventory.Name = configured_inventory diff --git a/sky_phone/source/bridge/server/inventory/esx.lua b/sky_phone/source/bridge/server/inventory/esx.lua new file mode 100644 index 0000000..8bbd85d --- /dev/null +++ b/sky_phone/source/bridge/server/inventory/esx.lua @@ -0,0 +1,112 @@ +if Bridge.Inventory.Name ~= "esx" and Bridge.Inventory.Name ~= "hex" then + return +end + +if GetResourceState("es_extended") ~= "started" then + error("[sky_phone] The ESX inventory adapter requires es_extended to be started.") +end +if Bridge.Inventory.Name == "hex" and GetResourceState("hex_4_inventory") ~= "started" then + error("[sky_phone] hex_4_inventory is configured, but the resource is not started.") +end + +local ESX = exports["es_extended"]:getSharedObject() + +local function get_player(source) + return ESX.GetPlayerFromId(source) +end + +local function get_item(source, item_name) + local player = get_player(source) + local item = player and player.getInventoryItem(item_name) or nil + local count = item and tonumber(item.count) or 0 + if count < 1 then + return nil + end + + return { + name = item_name, + slot = item_name, + count = count, + amount = count, + metadata = {}, + } +end + +local function has_metadata(metadata) + return type(metadata) == "table" and next(metadata) ~= nil +end + +function Bridge.Inventory.GetResourceName() + return Bridge.Inventory.Name == "hex" and "hex_4_inventory" or "es_extended" +end + +function Bridge.Inventory.GetSlot(source, slot_id) + return get_item(source, tostring(slot_id)) +end + +function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + local item = get_item(source, item_name) + if not item or has_metadata(metadata) then + return {} + end + return { item } +end + +function Bridge.Inventory.SetSlotMetadata() + return false +end + +function Bridge.Inventory.CanCarryItem(source, item_name, count, metadata) + if has_metadata(metadata) then + return false + end + local player = get_player(source) + return player and player.canCarryItem(item_name, count) or false +end + +function Bridge.Inventory.AddItem(source, item_name, count, _, metadata) + if not Bridge.Inventory.CanCarryItem(source, item_name, count, metadata) then + return false + end + + local player = get_player(source) + local before = player.getInventoryItem(item_name) + local before_count = before and tonumber(before.count) or 0 + player.addInventoryItem(item_name, count) + local after = player.getInventoryItem(item_name) + return (after and tonumber(after.count) or 0) - before_count == count +end + +function Bridge.Inventory.RemoveItem(source, item_name, count, _, metadata) + if has_metadata(metadata) then + return 0 + end + + local player = get_player(source) + local before = player and player.getInventoryItem(item_name) or nil + local before_count = before and tonumber(before.count) or 0 + if not player or before_count < count then + return 0 + end + + player.removeInventoryItem(item_name, count) + local after = player.getInventoryItem(item_name) + return before_count - (after and tonumber(after.count) or 0) +end + +function Bridge.Inventory.RegisterUsableItem(item_name, callback) + ESX.RegisterUsableItem(item_name, function(source) + local item = get_item(source, item_name) + if not item then + Bridge.Debug( + "warn", + "[sky_phone] ESX usable callback could not resolve item '%s' for source %s.", + item_name, + tostring(source) + ) + return + end + callback(source, item) + end) + return true +end diff --git a/sky_phone/source/client/main.lua b/sky_phone/source/client/main.lua index e8e78b7..9f128aa 100644 --- a/sky_phone/source/client/main.lua +++ b/sky_phone/source/client/main.lua @@ -17,6 +17,10 @@ local phone_block_look = false local phone_game_input = false local phone_cursor_disabled = false +SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsOpen", function() + return is_open +end) + Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true }) local server_callbacks = {