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.
This commit is contained in:
Leon.Schmidt
2026-08-18 20:06:50 +02:00
parent 4984fbc609
commit df6b06c6db
6 changed files with 168 additions and 6 deletions
+3 -1
View File
@@ -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
+27 -1
View File
@@ -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')
})
})
+3 -3
View File
@@ -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,
+19 -1
View File
@@ -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
@@ -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
+4
View File
@@ -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 = {