Files
sky_phone-sky-systems/sky_phone/source/client/custom_app_compat.lua
Leon.Schmidt dcf7b47113 FIX - Add detailed custom-app debug logging
Enable verbose tracing for custom apps and wire diagnostics through the stack. Sets Config.CustomApps.Debug = true, adds a debug_custom_app logger exposed as SkyPhoneApps.Debug, and instruments client and shared compatibility code (registration, normalization, provider exports, catalog sync, lifecycle hooks, resource stop, and app add/update/remove) with structured debug messages. Frontend sends a debug flag with the catalog and will call back to NUI (custom-app:catalog-debug) when debug is enabled. Tests updated to enable debug and assert fixBlur preservation and NUI catalog ACK handling. This provides detailed traces for exports, registration, catalog sync and lifecycle events.
2026-08-19 01:27:38 +02:00

748 lines
24 KiB
Lua

local RESOURCE_NAME = GetCurrentResourceName()
local providers = SkyPhoneCompatibility.Providers
local core = SkyPhoneApps.CompatibilityCore
local debug_custom_app = SkyPhoneApps.Debug or function() end
local provider_apps = {}
local high_client_apps = {}
local high_server_apps = {}
local function get_calling_resource(export_name)
local owner_resource = GetInvokingResource()
if owner_resource then
return owner_resource
end
Bridge.Debug("warn", "[%s] %s rejected: the export must be called by another resource.",
RESOURCE_NAME,
export_name
)
return nil, "invalid_owner"
end
local function copy_record_data(value)
if type(value) ~= "table" then
return nil
end
local copied = {}
for key, nested_value in pairs(value) do
copied[key] = nested_value
end
return copied
end
local function register_provider_app(provider, owner_resource, definition, vendor_data)
local app_id = definition.id
local existing = provider_apps[app_id]
debug_custom_app(
"provider",
"registration requested provider=%s id=%s owner=%s operation=%s",
tostring(provider),
tostring(app_id),
tostring(owner_resource),
existing and "update" or "add"
)
if existing and (existing.owner_resource ~= owner_resource or existing.provider ~= provider) then
debug_custom_app(
"provider",
"registration rejected provider=%s id=%s owner=%s existing_provider=%s existing_owner=%s error=duplicate_app_id",
tostring(provider),
tostring(app_id),
tostring(owner_resource),
tostring(existing.provider),
tostring(existing.owner_resource)
)
return false, "duplicate_app_id"
end
local success, error_message
if existing then
success, error_message = core.Update(owner_resource, definition)
else
success, error_message = core.Add(owner_resource, definition)
end
if success then
provider_apps[app_id] = {
definition = definition,
owner_resource = owner_resource,
provider = provider,
vendor_data = vendor_data,
}
end
debug_custom_app(
"provider",
"registration result provider=%s id=%s owner=%s success=%s error=%s",
tostring(provider),
tostring(app_id),
tostring(owner_resource),
tostring(success),
tostring(error_message)
)
return success, error_message
end
local function get_provider_app(owner_resource, app_id, allowed_providers)
if type(app_id) ~= "string" then
return nil, "invalid_app_id"
end
local record = provider_apps[app_id]
if not record then
return nil, "app_not_found"
end
if record.owner_resource ~= owner_resource then
return nil, "app_owner_mismatch"
end
if allowed_providers and not allowed_providers[record.provider] then
return nil, "app_provider_mismatch"
end
return record
end
local function remove_provider_app(owner_resource, app_id, allowed_providers)
local record, record_error = get_provider_app(owner_resource, app_id, allowed_providers)
if not record then
return false, record_error
end
return core.Remove(owner_resource, app_id)
end
function SkyPhoneApps.RegisterCompatibilityExport(owner_resource, app_data)
debug_custom_app(
"provider",
"compatibility export received owner=%s identifier=%s key=%s data_type=%s",
tostring(owner_resource),
type(app_data) == "table" and tostring(app_data.identifier) or "nil",
type(app_data) == "table" and tostring(app_data.key) or "nil",
type(app_data)
)
local provider
local definition
local definition_error
if app_data.identifier ~= nil then
provider = providers.lb
definition, definition_error = SkyPhoneCompatibility.BuildLbDefinition(owner_resource, app_data)
elseif app_data.key ~= nil then
provider = providers.yseries
definition, definition_error = SkyPhoneCompatibility.BuildYSeriesDefinition(app_data)
else
debug_custom_app(
"provider",
"compatibility export rejected owner=%s error=unknown_app_provider",
tostring(owner_resource)
)
return false, "unknown_app_provider"
end
if not definition then
debug_custom_app(
"provider",
"definition rejected provider=%s owner=%s error=%s",
tostring(provider),
tostring(owner_resource),
tostring(definition_error)
)
Bridge.Debug("warn", "[%s] %s registration rejected for %s: %s.",
RESOURCE_NAME,
provider,
owner_resource,
definition_error
)
return false, definition_error
end
debug_custom_app(
"provider",
"definition built provider=%s id=%s owner=%s ui=%s icon=%s default_installed=%s fix_blur=%s",
tostring(provider),
tostring(definition.id),
tostring(owner_resource),
tostring(definition.ui),
tostring(definition.icon),
tostring(definition.defaultInstalled),
definition.compatibility and tostring(definition.compatibility.fixBlur) or "nil"
)
return register_provider_app(provider, owner_resource, definition, copy_record_data(app_data))
end
SkyPhoneApps.OnCompatibilityAppRemoved = function(owner_resource, app_id)
local record = provider_apps[app_id]
if record and record.owner_resource == owner_resource then
provider_apps[app_id] = nil
end
end
local function add_17mov_application(app_data)
local owner_resource, owner_error = get_calling_resource("AddApplication")
if not owner_resource then
return false, owner_error
end
local definition, definition_error = SkyPhoneCompatibility.Build17MovDefinition(app_data)
if not definition then
Bridge.Debug("warn", "[%s] 17mov registration rejected for %s: %s.",
RESOURCE_NAME,
owner_resource,
definition_error
)
return false, definition_error
end
return register_provider_app(
providers.seventeen,
owner_resource,
definition,
copy_record_data(app_data)
)
end
local function remove_17mov_application(app_name, _resource_name)
local owner_resource, owner_error = get_calling_resource("RemoveApplication")
if not owner_resource then
return false, owner_error
end
local app_id = type(app_name) == "table" and app_name.name or app_name
return remove_provider_app(owner_resource, app_id, {
[providers.seventeen] = true,
})
end
local function send_app_message(app_id, data)
local owner_resource, owner_error = get_calling_resource("SendAppMessage")
if not owner_resource then
return false, owner_error
end
local record, record_error = get_provider_app(owner_resource, app_id, {
[providers.seventeen] = true,
[providers.yseries] = true,
})
if not record then
return false, record_error
end
return core.SendMessage(owner_resource, app_id, data)
end
local function add_high_application(app_name, data, locales)
local owner_resource, owner_error = get_calling_resource("addApplication")
if not owner_resource then
return false, owner_error
end
local definition, definition_error = SkyPhoneCompatibility.BuildHighDefinition(
owner_resource,
app_name,
data,
locales
)
if not definition then
Bridge.Debug("warn", "[%s] High Phone registration rejected for %s: %s.",
RESOURCE_NAME,
owner_resource,
definition_error
)
return false, definition_error
end
high_client_apps[app_name] = {
definition = definition,
owner_resource = owner_resource,
}
local server_record = high_server_apps[app_name]
if server_record then
if server_record.owner_resource ~= owner_resource then
return false, "duplicate_app_id"
end
if server_record.registered then
return true
end
return false, "server_definition_not_registered"
end
return register_provider_app(
providers.high,
owner_resource,
definition,
copy_record_data(data)
)
end
local function send_high_app_nui(app_name, data)
local owner_resource, owner_error = get_calling_resource("sendAppNui")
if not owner_resource then
return false, owner_error
end
local record, record_error = get_provider_app(owner_resource, app_name, {
[providers.high] = true,
})
if not record then
return false, record_error
end
return core.SendMessage(owner_resource, app_name, data)
end
local function add_quasar_app_for_owner(owner_resource, app_data)
local definition, definition_error = SkyPhoneCompatibility.BuildQuasarDefinition(app_data)
if not definition then
Bridge.Debug("warn", "[%s] Quasar registration rejected for %s: %s.",
RESOURCE_NAME,
owner_resource,
definition_error
)
return false, definition_error
end
return register_provider_app(
providers.quasar,
owner_resource,
definition,
SkyPhoneCompatibility.CopyQuasarData(app_data)
)
end
local function add_quasar_app(app_data)
local owner_resource, owner_error = get_calling_resource("addCustomApp")
if not owner_resource then
return false, owner_error
end
if type(app_data) ~= "table" then
return false, "invalid_app_data"
end
return add_quasar_app_for_owner(owner_resource, app_data)
end
local function add_quasar_apps_batch(apps)
local owner_resource, owner_error = get_calling_resource("addCustomAppsBatch")
if not owner_resource then
return false, owner_error
end
if type(apps) ~= "table" then
return false, "invalid_app_batch"
end
for index = 1, #apps do
local success, error_message = add_quasar_app_for_owner(owner_resource, apps[index])
if not success then
return false, error_message
end
end
return true
end
local function update_quasar_app(app_id, patch)
local owner_resource, owner_error = get_calling_resource("updateCustomApp")
if not owner_resource then
return false, owner_error
end
if type(patch) ~= "table" then
return false, "invalid_patch"
end
local record, record_error = get_provider_app(owner_resource, app_id, {
[providers.quasar] = true,
})
if not record then
return false, record_error
end
local merged = SkyPhoneCompatibility.CopyQuasarData(record.vendor_data)
for key, value in pairs(patch) do
if key == "iframe" and type(value) == "table" then
merged.iframe = merged.iframe or {}
for iframe_key, iframe_value in pairs(value) do
merged.iframe[iframe_key] = iframe_value
end
elseif key ~= "id" then
merged[key] = value
end
end
merged.id = app_id
local definition, definition_error = SkyPhoneCompatibility.BuildQuasarDefinition(merged)
if not definition then
return false, definition_error
end
local success, error_message = core.Update(owner_resource, definition)
if success then
record.definition = definition
record.vendor_data = SkyPhoneCompatibility.CopyQuasarData(merged)
end
return success, error_message
end
local function remove_quasar_app(app_id)
local owner_resource, owner_error = get_calling_resource("removeCustomApp")
if not owner_resource then
return false, owner_error
end
return remove_provider_app(owner_resource, app_id, {
[providers.quasar] = true,
})
end
local function get_quasar_apps()
local ids = {}
for app_id, record in pairs(provider_apps) do
if record.provider == providers.quasar then
ids[#ids + 1] = app_id
end
end
table.sort(ids)
local apps = {}
for index = 1, #ids do
apps[index] = SkyPhoneCompatibility.CopyQuasarData(provider_apps[ids[index]].vendor_data)
end
return apps
end
local function open_quasar_app(app_id)
local owner_resource, owner_error = get_calling_resource("OpenPhoneApp")
if not owner_resource then
return false, owner_error
end
local record, record_error = get_provider_app(owner_resource, app_id, {
[providers.quasar] = true,
})
if not record then
return false, record_error
end
return core.Open(owner_resource, app_id)
end
local function open_phone_app(app_id, data)
local owner_resource, owner_error = get_calling_resource("OpenApp")
if not owner_resource then
return false, owner_error
end
local record, record_error = get_provider_app(owner_resource, app_id)
if not record then
return false, record_error
end
return core.Open(owner_resource, app_id, data)
end
local function close_phone_app(options)
local owner_resource, owner_error = get_calling_resource("CloseApp")
if not owner_resource then
return false, owner_error
end
local app_id = type(options) == "table" and options.app or nil
if app_id then
local record, record_error = get_provider_app(owner_resource, app_id, {
[providers.lb] = true,
[providers.yseries] = true,
})
if not record then
return false, record_error
end
return core.Close(owner_resource, app_id)
end
return core.CloseActive(owner_resource)
end
local function normalize_lb_notification_text(value, maximum_length, error_code)
if type(value) ~= "string" then
return nil, error_code
end
local normalized = value:match("^%s*(.-)%s*$")
if normalized == "" or #normalized > maximum_length then
return nil, error_code
end
return normalized
end
local function send_lb_notification(data)
local owner_resource, owner_error = get_calling_resource("SendNotification")
if not owner_resource then
return false, owner_error
end
if type(data) ~= "table" then
return false, "invalid_notification"
end
local app_id = data.app or data.identifier
local valid_app_id, app_id_error = SkyPhoneApps.ValidateAppId(app_id)
if not valid_app_id then
return false, app_id_error
end
local registered_record = provider_apps[app_id]
if registered_record then
local record, record_error = get_provider_app(owner_resource, app_id, {
[providers.lb] = true,
})
if not record then
return false, record_error
end
elseif not SkyPhoneApps.ReservedAppIds[app_id] then
return false, "app_not_found"
end
local title, title_error = normalize_lb_notification_text(
data.title,
128,
"invalid_notification_title"
)
if not title then
return false, title_error
end
local content, content_error = normalize_lb_notification_text(
data.content or data.message or data.text,
512,
"invalid_notification_text"
)
if not content then
return false, content_error
end
SendNUIMessage({
type = "notification:show",
data = {
appId = app_id,
route = "/apps/" .. app_id,
text = content,
title = title,
},
})
return true
end
local function register_high_server_app(owner_resource, definition, revision)
if type(owner_resource) ~= "string"
or type(definition) ~= "table"
or type(definition.id) ~= "string"
or type(revision) ~= "number"
or revision ~= math.floor(revision)
or revision < 1
then
Bridge.Debug("warn", "[%s] Rejected invalid High Phone server application snapshot.", RESOURCE_NAME)
return nil
end
local app_id = definition.id
local existing = high_server_apps[app_id]
if existing and revision <= existing.revision then
if revision < existing.revision or existing.registered then
return app_id
end
if existing.owner_resource ~= owner_resource then
Bridge.Debug("warn", "[%s] Rejected conflicting High Phone owner for %s.",
RESOURCE_NAME,
app_id
)
return app_id
end
end
local provider_record = provider_apps[app_id]
local success, error_message
local retained_registration = false
if provider_record then
if provider_record.owner_resource ~= owner_resource or provider_record.provider ~= providers.high then
success, error_message = false, "duplicate_app_id"
else
retained_registration = true
success, error_message = core.Update(owner_resource, definition)
end
else
success, error_message = core.Add(owner_resource, definition)
end
high_server_apps[app_id] = {
definition = definition,
last_error = success and nil or error_message,
owner_resource = owner_resource,
registered = success or retained_registration,
revision = revision,
}
if success then
provider_apps[app_id] = {
definition = definition,
owner_resource = owner_resource,
provider = providers.high,
}
elseif not existing or existing.revision ~= revision or existing.last_error ~= error_message then
Bridge.Debug("error", "[%s] Could not register High Phone server application %s revision %s: %s.",
RESOURCE_NAME,
app_id,
revision,
error_message or "unknown_error"
)
end
return app_id
end
local function remove_high_server_app(owner_resource, app_id)
local record = high_server_apps[app_id]
if not record or record.owner_resource ~= owner_resource then
return
end
high_server_apps[app_id] = nil
if record.registered then
core.Remove(owner_resource, app_id)
end
local client_record = high_client_apps[app_id]
if client_record then
local success, error_message = register_provider_app(
providers.high,
client_record.owner_resource,
client_record.definition,
nil
)
if not success then
Bridge.Debug("error", "[%s] Could not restore High Phone client application %s: %s.",
RESOURCE_NAME,
app_id,
error_message or "unknown_error"
)
end
end
end
RegisterNetEvent("sky_phone:compat:high:client:syncApplication", function(owner_resource, definition, revision)
if source ~= 65535 then
Bridge.Debug("warn", "[%s] Rejected locally invoked High Phone application sync.", RESOURCE_NAME)
return
end
register_high_server_app(owner_resource, definition, revision)
end)
RegisterNetEvent("sky_phone:compat:high:client:removeApplication", function(owner_resource, app_id)
if source ~= 65535 then
Bridge.Debug("warn", "[%s] Rejected locally invoked High Phone application removal.", RESOURCE_NAME)
return
end
if type(owner_resource) ~= "string" or type(app_id) ~= "string" then
Bridge.Debug("warn", "[%s] Rejected invalid High Phone application removal.", RESOURCE_NAME)
return
end
remove_high_server_app(owner_resource, app_id)
end)
RegisterNetEvent("sky_phone:compat:high:client:replaceSnapshot", function(snapshot)
if source ~= 65535 then
Bridge.Debug("warn", "[%s] Rejected locally invoked High Phone application snapshot.", RESOURCE_NAME)
return
end
if type(snapshot) ~= "table" then
Bridge.Debug("warn", "[%s] Rejected invalid High Phone application snapshot.", RESOURCE_NAME)
return
end
local seen = {}
for index = 1, #snapshot do
local record = snapshot[index]
if type(record) == "table" then
local app_id = register_high_server_app(
record.owner_resource,
record.definition,
record.revision
)
if app_id then
seen[app_id] = true
end
end
end
local removed = {}
for app_id, record in pairs(high_server_apps) do
if not seen[app_id] then
removed[#removed + 1] = {
app_id = app_id,
owner_resource = record.owner_resource,
}
end
end
for index = 1, #removed do
remove_high_server_app(removed[index].owner_resource, removed[index].app_id)
end
end)
AddEventHandler("onClientResourceStart", function(resource_name)
for _, record in pairs(high_server_apps) do
if record.owner_resource == resource_name and not record.registered then
register_high_server_app(record.owner_resource, record.definition, record.revision)
end
end
end)
AddEventHandler("onClientResourceStop", function(resource_name)
for app_id, record in pairs(high_client_apps) do
if record.owner_resource == resource_name then
high_client_apps[app_id] = nil
end
end
end)
exports("AddApplication", add_17mov_application)
exports("RemoveApplication", remove_17mov_application)
exports("SendAppMessage", send_app_message)
exports("addApplication", add_high_application)
exports("sendAppNui", send_high_app_nui)
exports("addCustomApp", add_quasar_app)
exports("addCustomAppsBatch", add_quasar_apps_batch)
exports("updateCustomApp", update_quasar_app)
exports("removeCustomApp", remove_quasar_app)
exports("getCustomApps", get_quasar_apps)
exports("OpenPhoneApp", open_quasar_app)
exports("OpenApp", open_phone_app)
exports("CloseApp", close_phone_app)
exports("SendNotification", send_lb_notification)
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "OpenApp", open_phone_app)
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "CloseApp", close_phone_app)
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "SendNotification", send_lb_notification)
SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "AddApplication", add_17mov_application)
SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "RemoveApplication", remove_17mov_application)
SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "SendAppMessage", send_app_message)
SkyPhoneCompatibility.RegisterExportAlias("high-phone", "addApplication", add_high_application)
SkyPhoneCompatibility.RegisterExportAlias("high-phone", "sendAppNui", send_high_app_nui)
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomApp", add_quasar_app)
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomAppsBatch", add_quasar_apps_batch)
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "updateCustomApp", update_quasar_app)
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "removeCustomApp", remove_quasar_app)
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "getCustomApps", get_quasar_apps)
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "OpenPhoneApp", open_quasar_app)
SkyPhoneCompatibility.RegisterExportAlias("yseries", "SendAppMessage", send_app_message)
SkyPhoneCompatibility.RegisterExportAlias("yseries", "CloseApp", close_phone_app)
SkyPhoneCompatibility.RegisterExportAlias("yseries", "GetDataLoaded", function()
return true
end)
CreateThread(function()
debug_custom_app("provider", "requesting provider snapshots and emitting compatibility ready signals")
TriggerServerEvent("sky_phone:compat:high:server:requestSnapshot")
TriggerEvent("17mov_Phone:Client:Ready")
local provider_resources = {
"lb-phone",
"17mov_Phone",
"high-phone",
"qs-smartphone",
"yseries",
}
for index = 1, #provider_resources do
debug_custom_app(
"provider",
"emitting onResourceStart compatibility signal for %s (state=%s)",
provider_resources[index],
tostring(GetResourceState(provider_resources[index]))
)
TriggerEvent("onResourceStart", provider_resources[index])
end
end)