Files
sky_phone-sky-systems/sky_phone/source/shared/custom_app_compat.lua
T
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

298 lines
9.7 KiB
Lua

SkyPhoneCompatibility = {}
function SkyPhoneCompatibility.RegisterExportAlias(resource_name, export_name, handler)
assert(type(resource_name) == "string" and resource_name ~= "", "Export alias resource must be a non-empty string")
assert(type(export_name) == "string" and export_name ~= "", "Export alias name must be a non-empty string")
assert(type(handler) == "function", "Export alias handler must be a function")
AddEventHandler(("__cfx_export_%s_%s"):format(resource_name, export_name), function(set_callback)
set_callback(function(...)
local debug_custom_app = SkyPhoneApps and SkyPhoneApps.Debug
local invoking_resource = GetInvokingResource and GetInvokingResource() or nil
if debug_custom_app then
debug_custom_app(
"export",
"call provider=%s export=%s invoking_resource=%s argument_count=%s",
resource_name,
export_name,
tostring(invoking_resource),
select("#", ...)
)
end
local results = table.pack(handler(...))
if debug_custom_app then
debug_custom_app(
"export",
"result provider=%s export=%s invoking_resource=%s success=%s error=%s",
resource_name,
export_name,
tostring(invoking_resource),
tostring(results[1]),
tostring(results[2])
)
end
return table.unpack(results, 1, results.n)
end)
end)
end
SkyPhoneCompatibility.Providers = {
lb = "lb_phone",
seventeen = "17mov",
high = "high_phone",
quasar = "quasar_v3",
yseries = "yseries",
}
local function normalize_at_resource_url(owner_resource, url, error_prefix)
if type(url) ~= "string" or url == "" then
return nil, "invalid_" .. error_prefix
end
if url:sub(1, 7) == "http://" then
return nil, "insecure_" .. error_prefix
end
if url:sub(1, 1) ~= "@" then
return url
end
local url_owner, url_path = url:match("^@([^/]+)/(.+)$")
if url_owner ~= owner_resource or not url_path then
return nil, error_prefix .. "_owner_mismatch"
end
return owner_resource .. "/" .. url_path
end
local function resolve_lb_asset_resource(owner_resource, app_data)
local ui = app_data.ui
if type(ui) ~= "string" then
return nil
end
local explicit_ui_resource = ui:match("^https://cfx%-nui%-([^/]+)/")
or ui:match("^nui://([^/]+)/")
local asset_resource = explicit_ui_resource or ui:match("^([%w][%w._-]*)/")
if not asset_resource or asset_resource == owner_resource then
return nil
end
if explicit_ui_resource or app_data.resource == asset_resource then
return asset_resource
end
local icon = app_data.icon
local icon_resource = type(icon) == "string" and (
icon:match("^https://cfx%-nui%-([^/]+)/") or icon:match("^nui://([^/]+)/")
) or nil
if icon_resource == asset_resource then
return asset_resource
end
return nil
end
local function build_locale_maps(app_name, locales)
local names = {}
local descriptions = {}
if type(locales) == "table" then
for locale, locale_data in pairs(locales) do
if type(locale) == "string" and type(locale_data) == "table" then
if type(locale_data.label) == "string" and locale_data.label ~= "" then
names[locale] = locale_data.label
end
if type(locale_data.description) == "string" and locale_data.description ~= "" then
descriptions[locale] = locale_data.description
end
end
end
end
if not next(names) then
return app_name, app_name
end
for locale, label in pairs(names) do
if not descriptions[locale] then
descriptions[locale] = label
end
end
return names, descriptions
end
function SkyPhoneCompatibility.CopyQuasarData(app_data)
local copy = {}
for key, value in pairs(app_data) do
if key == "iframe" and type(value) == "table" then
copy.iframe = {}
for iframe_key, iframe_value in pairs(value) do
copy.iframe[iframe_key] = iframe_value
end
else
copy[key] = value
end
end
return copy
end
function SkyPhoneCompatibility.BuildLbDefinition(owner_resource, app_data)
if type(app_data) ~= "table" or type(app_data.identifier) ~= "string" then
return nil, "invalid_app_data"
end
local ui, ui_error = normalize_at_resource_url(owner_resource, app_data.ui, "ui")
if not ui then
return nil, ui_error
end
return {
schemaVersion = 1,
id = app_data.identifier,
name = app_data.name,
description = app_data.description,
developer = app_data.developer,
category = app_data.game and "games" or "utilities",
ui = ui,
assetResource = resolve_lb_asset_resource(owner_resource, app_data),
bridgeMode = "legacy",
icon = app_data.icon,
defaultInstalled = app_data.defaultApp or false,
removable = not app_data.defaultApp,
orientation = app_data.landscape and "landscape" or "portrait",
onInstall = app_data.onInstall,
onOpen = app_data.onOpen or app_data.onUse,
onClose = app_data.onClose,
compatibility = {
provider = SkyPhoneCompatibility.Providers.lb,
apiVersion = 1,
fixBlur = app_data.fixBlur == true,
resourceName = type(app_data.resource) == "string" and app_data.resource or owner_resource,
},
}
end
function SkyPhoneCompatibility.Build17MovDefinition(app_data)
if type(app_data) ~= "table" or type(app_data.name) ~= "string" then
return nil, "invalid_app_data"
end
return {
schemaVersion = 1,
id = app_data.name,
name = app_data.label,
description = app_data.description or app_data.label,
category = "utilities",
ui = app_data.ui,
bridgeMode = "legacy",
icon = app_data.icon,
iconBackground = type(app_data.iconBackground) == "string" and app_data.iconBackground or nil,
defaultInstalled = app_data.default or app_data.preInstalled or false,
removable = not app_data.default,
orientation = "portrait",
compatibility = {
provider = SkyPhoneCompatibility.Providers.seventeen,
apiVersion = 1,
},
}
end
function SkyPhoneCompatibility.BuildHighDefinition(owner_resource, app_name, data, locales)
if type(owner_resource) ~= "string" or type(app_name) ~= "string" or type(data) ~= "table" then
return nil, "invalid_app_data"
end
if #app_name > 64 or not app_name:match("^[a-z0-9][a-z0-9._-]+$") then
return nil, "invalid_app_id"
end
local external_url, url_error = normalize_at_resource_url(
owner_resource,
data.externalUrl,
"external_url"
)
if not external_url then
return nil, url_error
end
local name, description = build_locale_maps(app_name, locales)
local icon = type(data.icon) == "table" and data.icon.imageUrl or data.icon
return {
schemaVersion = 1,
id = app_name,
name = name,
description = description,
developer = data.developer,
category = "utilities",
ui = external_url,
bridgeMode = "legacy",
icon = icon,
iconBackground = type(data.icon) == "table" and data.icon.background or nil,
defaultInstalled = data.preAdded or false,
removable = data.removable ~= false,
orientation = "portrait",
compatibility = {
provider = SkyPhoneCompatibility.Providers.high,
apiVersion = 1,
},
}
end
function SkyPhoneCompatibility.BuildQuasarDefinition(app_data)
local iframe_url = type(app_data) == "table"
and type(app_data.iframe) == "table"
and app_data.iframe.url
or nil
if type(app_data) ~= "table"
or type(app_data.id) ~= "string"
or type(app_data.label) ~= "string"
or type(iframe_url) ~= "string"
then
return nil, "invalid_app_data"
end
return {
schemaVersion = 1,
id = app_data.id,
name = app_data.label,
description = app_data.description or app_data.label,
category = "utilities",
ui = iframe_url,
bridgeMode = "legacy",
icon = app_data.icon,
defaultInstalled = app_data.defaultInstalled or false,
removable = true,
orientation = "portrait",
compatibility = {
provider = SkyPhoneCompatibility.Providers.quasar,
apiVersion = 1,
},
}
end
function SkyPhoneCompatibility.BuildYSeriesDefinition(app_data)
if type(app_data) ~= "table" or type(app_data.key) ~= "string" then
return nil, "invalid_app_data"
end
local icon = app_data.icon
if type(icon) == "table" then
icon = icon.yos or icon.humanoid
end
return {
schemaVersion = 1,
id = app_data.key,
name = app_data.name,
description = app_data.description or app_data.name,
category = app_data.game and "games" or "utilities",
ui = app_data.ui,
bridgeMode = "legacy",
icon = icon,
defaultInstalled = app_data.defaultApp or false,
removable = true,
orientation = "portrait",
compatibility = {
provider = SkyPhoneCompatibility.Providers.yseries,
apiVersion = 1,
},
}
end