Files
Leon.Schmidt 30559048fd FIX - complete LB custom app compatibility (#11)
* FIX - preserve LB custom app lifecycle

* FIX - reset LB export aliases before startup

* FIX - report competing custom app providers

* FIX - identify LB app frames as live NUI

* FIX - complete LB custom app compatibility

* FIX - provide LB PicChat runtime exports

* FIX - route LB PicChat through Sky Phone

* FIX - return cached LB equipped phone number

* FIX - harden PicChat compatibility migration

* ENH - complete modular phone provider and Creator APIs

* DOC - document the Sky Phone Creator API

---------

Co-authored-by: DerEchteAlec <bycraky@gmail.com>
Co-authored-by: DerEchteAlec <alec.schitzkat@luwan.io>
2026-08-20 18:19:16 +02:00

323 lines
11 KiB
Lua

Bridge.Database.AfterMigration("sky_phone", function()
if type(SkyPhone) ~= "table"
or type(SkyPhone.GetEquippedPhoneNumber) ~= "function"
or type(SkyPhone.GetSourceFromNumber) ~= "function"
or type(SkyPhone.FindDeviceSlots) ~= "function"
then
error("[sky_phone] Device directory initialized before the phone core.")
end
SkyPhoneDeviceDirectory = {}
local unique_phones = Config.Phone.Unique ~= false
local function normalize_source(source)
if type(source) ~= "number"
or source ~= source
or source <= 0
or source >= math.huge
or source ~= math.floor(source)
then
return nil
end
return source
end
local function normalize_identifier(identifier)
if type(identifier) ~= "string" then
return nil
end
identifier = identifier:match("^%s*(.-)%s*$")
if identifier == "" or #identifier > 80 then
return nil
end
return identifier
end
local function normalize_imei(imei)
return SkyPhoneImei.IsValid(imei) and imei or nil
end
local function normalize_phone_number(phone_number)
return SkyPhoneSimNumber.Normalize(
phone_number,
Config.Sim.NumberLength,
Config.Sim.NumberPrefix
)
end
local function build_device_identity(row)
if not row then
return nil
end
return {
accountId = tonumber(row.account_id),
deviceName = row.device_name,
equipped = false,
imei = row.imei,
mappedIdentifier = row.mapped_identifier,
online = false,
phoneNumber = row.phone_number,
registeredIdentifier = row.sim_owner_identifier,
simId = row.sim_id,
simType = row.sim_type,
source = nil,
}
end
local function load_stored_device(where_clause, parameter)
local rows = Bridge.Database.Query(([[
SELECT d.`imei`, d.`device_name`, d.`account_id`, d.`sim_id`,
s.`phone_number`, s.`sim_type`, s.`owner_identifier` AS `sim_owner_identifier`,
c.`owner_identifier` AS `mapped_identifier`
FROM `sky_phone_devices` d
LEFT JOIN `sky_phone_sims` s ON s.`id` = d.`sim_id`
LEFT JOIN `sky_phone_character_devices` c ON c.`device_imei` = d.`imei`
WHERE %s
LIMIT 1
]]):format(where_clause), { parameter })
return build_device_identity(rows[1])
end
local function mark_equipped(identity, source, identifier)
identity.equipped = true
identity.identifier = identifier
identity.online = true
identity.source = source
return identity
end
local function get_inventory_device_imeis(source)
local seen = {}
local imeis = {}
local slots = Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)
for index = 1, #slots do
local slot = slots[index]
local amount = tonumber(slot.amount or slot.count) or 0
local imei = amount == 1 and slot.metadata and normalize_imei(slot.metadata.imei) or nil
if imei and not seen[imei] then
seen[imei] = true
imeis[#imeis + 1] = imei
end
end
table.sort(imeis)
return imeis
end
function SkyPhoneDeviceDirectory.GetStoredDeviceByImei(imei)
imei = normalize_imei(imei)
if not imei then
return nil, "invalid_imei"
end
local identity = load_stored_device("d.`imei` = ?", imei)
if not identity then
return nil, "device_not_found"
end
return identity
end
function SkyPhoneDeviceDirectory.GetStoredDeviceByPhoneNumber(phone_number)
phone_number = normalize_phone_number(phone_number)
if not phone_number then
return nil, "invalid_phone_number"
end
local identity = load_stored_device("s.`phone_number` = ?", phone_number)
if not identity then
return nil, "device_not_found"
end
return identity
end
function SkyPhoneDeviceDirectory.GetStoredDeviceByIdentifier(identifier)
identifier = normalize_identifier(identifier)
if not identifier then
return nil, "invalid_identifier"
end
if unique_phones then
return nil, "identity_scope_unsupported"
end
local identity = load_stored_device("c.`owner_identifier` = ?", identifier)
if not identity then
return nil, "device_not_found"
end
identity.identifier = identifier
return identity
end
function SkyPhoneDeviceDirectory.GetStoredSimByPhoneNumber(phone_number)
phone_number = normalize_phone_number(phone_number)
if not phone_number then
return nil, "invalid_phone_number"
end
local rows = Bridge.Database.Query([[
SELECT s.`id`, s.`phone_number`, s.`sim_type`, s.`owner_identifier`, d.`imei`
FROM `sky_phone_sims` s
LEFT JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id`
WHERE s.`phone_number` = ?
LIMIT 1
]], { phone_number })
local row = rows[1]
if not row then
return nil, "sim_not_found"
end
return {
deviceImei = row.imei,
phoneNumber = row.phone_number,
registeredIdentifier = row.owner_identifier,
simId = row.id,
simType = row.sim_type,
}
end
function SkyPhoneDeviceDirectory.GetOnlineBySource(source)
source = normalize_source(source)
if not source then
return nil, "invalid_source"
end
local identifier = normalize_identifier(Bridge.Framework.GetIdentifier(source))
if not identifier then
return nil, "player_unavailable"
end
local phone_number = SkyPhone.GetEquippedPhoneNumber(source)
if phone_number then
local identity = SkyPhoneDeviceDirectory.GetStoredDeviceByPhoneNumber(phone_number)
if not identity then
Bridge.Debug(
"error",
"[sky_phone] Equipped phone number %s for source %s has no attached device.",
tostring(phone_number),
tostring(source)
)
return nil, "identity_inconsistent"
end
if not SkyPhone.FindDeviceSlots(source, identity.imei)[1] then
return nil, "device_not_equipped"
end
return mark_equipped(identity, source, identifier)
end
local identity
if unique_phones then
local imeis = get_inventory_device_imeis(source)
if #imeis == 0 then
return nil, "device_not_equipped"
end
if #imeis > 1 then
return nil, "equipped_device_ambiguous"
end
identity = SkyPhoneDeviceDirectory.GetStoredDeviceByImei(imeis[1])
else
identity = SkyPhoneDeviceDirectory.GetStoredDeviceByIdentifier(identifier)
end
if not identity then
return nil, "device_not_found"
end
if not SkyPhone.FindDeviceSlots(source, identity.imei)[1] then
return nil, "device_not_equipped"
end
return mark_equipped(identity, source, identifier)
end
function SkyPhoneDeviceDirectory.GetOnlineByPhoneNumber(phone_number)
phone_number = normalize_phone_number(phone_number)
if not phone_number then
return nil, "invalid_phone_number"
end
local source = SkyPhone.GetSourceFromNumber(phone_number)
if not source then
return nil, "player_unavailable"
end
local identity, identity_error = SkyPhoneDeviceDirectory.GetOnlineBySource(source)
if not identity then
return nil, identity_error
end
if identity.phoneNumber ~= phone_number then
return nil, "identity_inconsistent"
end
return identity
end
function SkyPhoneDeviceDirectory.GetOnlineByIdentifier(identifier)
identifier = normalize_identifier(identifier)
if not identifier then
return nil, "invalid_identifier"
end
local matched_source
for _, player in ipairs(Bridge.Framework.GetPlayers()) do
local source = tonumber(player)
local current_identifier = source and normalize_identifier(Bridge.Framework.GetIdentifier(source)) or nil
if current_identifier == identifier then
if matched_source then
Bridge.Debug(
"error",
"[sky_phone] Multiple online sources expose identifier %s.",
identifier
)
return nil, "identifier_ambiguous"
end
matched_source = source
end
end
if not matched_source then
return nil, "player_unavailable"
end
return SkyPhoneDeviceDirectory.GetOnlineBySource(matched_source)
end
function SkyPhoneDeviceDirectory.GetOnlineByImei(imei)
imei = normalize_imei(imei)
if not imei then
return nil, "invalid_imei"
end
local stored, stored_error = SkyPhoneDeviceDirectory.GetStoredDeviceByImei(imei)
if not stored then
return nil, stored_error
end
if stored.phoneNumber then
local source = SkyPhone.GetSourceFromNumber(stored.phoneNumber)
if source then
local identity, identity_error = SkyPhoneDeviceDirectory.GetOnlineBySource(source)
if not identity then
return nil, identity_error
end
if identity.imei ~= imei then
return nil, "identity_inconsistent"
end
return identity
end
end
local matched_identity
for _, player in ipairs(Bridge.Framework.GetPlayers()) do
local source = tonumber(player)
if source then
local identity = SkyPhoneDeviceDirectory.GetOnlineBySource(source)
if identity and identity.imei == imei then
if matched_identity then
Bridge.Debug(
"error",
"[sky_phone] Device IMEI %s is equipped by multiple online sources.",
imei
)
return nil, "device_holder_ambiguous"
end
matched_identity = identity
end
end
end
if not matched_identity then
return nil, "player_unavailable"
end
return matched_identity
end
end)