mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 21:01:30 +00:00
30559048fd
* 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>
387 lines
14 KiB
Lua
387 lines
14 KiB
Lua
local alias_handlers = {}
|
|
local event_handlers = {}
|
|
local net_events = {}
|
|
local registered_commands = {}
|
|
local registered_exports = {}
|
|
local toggle_calls = {}
|
|
local flashlight_calls = {}
|
|
local selfie_calls = {}
|
|
local navigation_calls = {}
|
|
local focus_calls = {}
|
|
local dial_calls = {}
|
|
local terminate_calls = 0
|
|
local answer_calls = 0
|
|
local decline_calls = 0
|
|
local notification_calls = {}
|
|
local quasar_push_notifications = {}
|
|
local phone_state = {
|
|
inCall = false,
|
|
onScreen = true,
|
|
open = true,
|
|
}
|
|
|
|
Config = {
|
|
Sim = {
|
|
NumberGroups = { 3, 4 },
|
|
NumberLength = 7,
|
|
NumberPrefix = "",
|
|
},
|
|
}
|
|
|
|
Bridge = {
|
|
Debug = function() end,
|
|
}
|
|
|
|
SkyPhoneApps = {
|
|
CompatibilityCore = {
|
|
Add = function() return true end,
|
|
Close = function() return true end,
|
|
CloseActive = function() return true end,
|
|
Open = function() return true end,
|
|
Remove = function() return true end,
|
|
SendMessage = function() return true end,
|
|
Update = function() return true end,
|
|
},
|
|
Debug = function() end,
|
|
}
|
|
|
|
SkyPhoneClient = {
|
|
GetEquippedPhoneNumber = function()
|
|
return "5550101"
|
|
end,
|
|
GetState = function()
|
|
return phone_state
|
|
end,
|
|
Toggle = function(...)
|
|
toggle_calls[#toggle_calls + 1] = table.pack(...)
|
|
return true
|
|
end,
|
|
}
|
|
|
|
SkyPhoneCalls = {
|
|
Answer = function()
|
|
answer_calls = answer_calls + 1
|
|
return true
|
|
end,
|
|
Decline = function()
|
|
decline_calls = decline_calls + 1
|
|
return true
|
|
end,
|
|
Dial = function(phone_number)
|
|
dial_calls[#dial_calls + 1] = phone_number
|
|
if phone_number == "5559999" then
|
|
return false, "target_unavailable"
|
|
end
|
|
return true
|
|
end,
|
|
Terminate = function()
|
|
terminate_calls = terminate_calls + 1
|
|
return true
|
|
end,
|
|
}
|
|
|
|
SkyPhoneNotifications = {
|
|
Send = function(notification)
|
|
notification_calls[#notification_calls + 1] = notification
|
|
return true
|
|
end,
|
|
Show = function()
|
|
error("provider notifications must use the server-authoritative Send path")
|
|
end,
|
|
}
|
|
|
|
SkyPhoneCamera = {
|
|
DisableWalkable = function() end,
|
|
EnableWalkable = function() end,
|
|
GetState = function()
|
|
return {
|
|
active = false,
|
|
flashEnabled = flashlight_calls[#flashlight_calls] == true,
|
|
selfie = selfie_calls[#selfie_calls] == true,
|
|
walkable = false,
|
|
}
|
|
end,
|
|
SetFlashlight = function(enabled)
|
|
flashlight_calls[#flashlight_calls + 1] = enabled
|
|
end,
|
|
SetSelfie = function(enabled)
|
|
selfie_calls[#selfie_calls + 1] = enabled
|
|
end,
|
|
ToggleFrozen = function() end,
|
|
}
|
|
|
|
SkyPhoneNavigation = {
|
|
Close = function(app_id)
|
|
navigation_calls[#navigation_calls + 1] = { action = "close", appId = app_id }
|
|
return true
|
|
end,
|
|
GetCurrent = function(app_id)
|
|
if app_id ~= nil then
|
|
return app_id == "messages"
|
|
end
|
|
return "messages"
|
|
end,
|
|
IsInstalled = function(app_id)
|
|
return app_id == "messages"
|
|
end,
|
|
Open = function(app_id)
|
|
navigation_calls[#navigation_calls + 1] = { action = "open", appId = app_id }
|
|
if app_id == "messages" then
|
|
return true
|
|
end
|
|
return false, "app_not_installed"
|
|
end,
|
|
}
|
|
|
|
SkyPhoneFocus = {
|
|
SetExternalGameInput = function(owner_resource, allow_game_input)
|
|
focus_calls[#focus_calls + 1] = {
|
|
allow = allow_game_input,
|
|
owner = owner_resource,
|
|
}
|
|
return true
|
|
end,
|
|
}
|
|
|
|
exports = setmetatable({}, {
|
|
__call = function(_, export_name, handler)
|
|
registered_exports[export_name] = handler
|
|
end,
|
|
})
|
|
|
|
function GetCurrentResourceName()
|
|
return "sky_phone"
|
|
end
|
|
|
|
function GetInvokingResource()
|
|
return "provider_test"
|
|
end
|
|
|
|
function AddEventHandler(event_name, handler)
|
|
local handlers = event_handlers[event_name] or {}
|
|
handlers[#handlers + 1] = handler
|
|
event_handlers[event_name] = handlers
|
|
end
|
|
|
|
function RegisterNetEvent(event_name, handler)
|
|
net_events[event_name] = handler
|
|
end
|
|
|
|
function RegisterCommand(command_name, handler)
|
|
registered_commands[command_name] = handler
|
|
end
|
|
|
|
function TriggerEvent(event_name, ...)
|
|
local handlers = event_handlers[event_name] or {}
|
|
for index = 1, #handlers do
|
|
handlers[index](...)
|
|
end
|
|
end
|
|
|
|
function SendNUIMessage() end
|
|
|
|
dofile("sky_phone/source/shared/sim_number.lua")
|
|
dofile("sky_phone/source/bridge/phones/shared.lua")
|
|
for _, path in ipairs({
|
|
"sky_phone/source/bridge/phones/shared/lb.lua",
|
|
"sky_phone/source/bridge/phones/shared/seventeen.lua",
|
|
"sky_phone/source/bridge/phones/shared/high.lua",
|
|
"sky_phone/source/bridge/phones/shared/quasar.lua",
|
|
"sky_phone/source/bridge/phones/shared/yseries.lua",
|
|
}) do
|
|
dofile(path)
|
|
end
|
|
for _, path in ipairs({
|
|
"sky_phone/source/bridge/phones/client/core.lua",
|
|
"sky_phone/source/bridge/phones/client/lb.lua",
|
|
"sky_phone/source/bridge/phones/client/seventeen.lua",
|
|
"sky_phone/source/bridge/phones/client/high.lua",
|
|
"sky_phone/source/bridge/phones/client/quasar.lua",
|
|
"sky_phone/source/bridge/phones/client/yseries.lua",
|
|
}) do
|
|
dofile(path)
|
|
end
|
|
|
|
local function get_alias(resource_name, export_name)
|
|
local event_name = ("__cfx_export_%s_%s"):format(resource_name, export_name)
|
|
local handlers = assert(event_handlers[event_name], "missing alias event: " .. event_name)
|
|
local alias_handler
|
|
handlers[#handlers](function(handler)
|
|
alias_handler = handler
|
|
end)
|
|
alias_handlers[event_name] = assert(alias_handler, "missing alias handler: " .. event_name)
|
|
return alias_handler
|
|
end
|
|
|
|
local function assert_no_return(handler, ...)
|
|
assert(table.pack(handler(...)).n == 0, "documented no-return export leaked a return value")
|
|
end
|
|
|
|
local mov_open = get_alias("17mov_Phone", "OpenPhone")
|
|
local mov_close = get_alias("17mov_Phone", "ClosePhone")
|
|
local mov_is_open = get_alias("17mov_Phone", "IsPhoneOpen")
|
|
local mov_get_number = get_alias("17mov_Phone", "GetPlayerNumber")
|
|
local mov_toggle_flashlight = get_alias("17mov_Phone", "ToggleFlashlight")
|
|
local mov_get_flashlight = get_alias("17mov_Phone", "GetFlashlightState")
|
|
local mov_create_notification = get_alias("17mov_Phone", "CreateNotification")
|
|
local mov_open_app = get_alias("17mov_Phone", "OpenApp")
|
|
local mov_close_app = get_alias("17mov_Phone", "CloseApp")
|
|
|
|
assert_no_return(mov_open)
|
|
assert(toggle_calls[#toggle_calls].n == 1 and toggle_calls[#toggle_calls][1] == true)
|
|
assert_no_return(mov_close)
|
|
assert(toggle_calls[#toggle_calls].n == 1 and toggle_calls[#toggle_calls][1] == false)
|
|
assert(mov_is_open() == true)
|
|
assert(mov_get_number() == "5550101")
|
|
local flashlight_count = #flashlight_calls
|
|
assert_no_return(mov_toggle_flashlight, "true")
|
|
assert(#flashlight_calls == flashlight_count, "17Movement flashlight must reject non-booleans")
|
|
assert_no_return(mov_toggle_flashlight, true)
|
|
assert(flashlight_calls[#flashlight_calls] == true and mov_get_flashlight() == true)
|
|
assert_no_return(mov_create_notification, {
|
|
app = "MESSAGES",
|
|
message = "Hello",
|
|
title = "Message",
|
|
})
|
|
assert(notification_calls[#notification_calls].appId == "messages")
|
|
assert(notification_calls[#notification_calls].text == "Hello")
|
|
local notification_count = #notification_calls
|
|
assert_no_return(mov_create_notification, {
|
|
app = "MESSAGES",
|
|
message = { key = "Messages:NewMessage" },
|
|
title = "Message",
|
|
})
|
|
assert(#notification_calls == notification_count,
|
|
"17Movement locale-object notifications must remain visibly unsupported")
|
|
assert_no_return(mov_open_app, "messages")
|
|
assert(navigation_calls[#navigation_calls].action == "open" and navigation_calls[#navigation_calls].appId == "messages")
|
|
assert_no_return(mov_close_app, "messages")
|
|
assert(navigation_calls[#navigation_calls].action == "close" and navigation_calls[#navigation_calls].appId == "messages")
|
|
|
|
local high_close = get_alias("high-phone", "closePhone")
|
|
local high_start_call = get_alias("high-phone", "startCall")
|
|
local high_end_call = get_alias("high-phone", "endCall")
|
|
local high_send_notification = get_alias("high-phone", "sendNotification")
|
|
local high_set_facing = get_alias("high-phone", "setCameraFacing")
|
|
local high_format_number = get_alias("high-phone", "formatNumber")
|
|
local high_use_phone_item = get_alias("high-phone", "usePhoneItem")
|
|
|
|
assert_no_return(high_close)
|
|
assert(toggle_calls[#toggle_calls][1] == false)
|
|
assert_no_return(high_start_call, "5550101", false)
|
|
assert(dial_calls[#dial_calls] == "5550101")
|
|
local dial_count = #dial_calls
|
|
assert_no_return(high_start_call, "5550102", true)
|
|
assert(#dial_calls == dial_count, "High Phone video calls must be rejected")
|
|
assert_no_return(high_start_call, "5550102", "false")
|
|
assert(#dial_calls == dial_count, "High Phone must reject a non-boolean video flag")
|
|
assert_no_return(high_end_call)
|
|
assert(terminate_calls == 1)
|
|
assert_no_return(high_send_notification, {
|
|
application = { name = "twizzler" },
|
|
content = "World",
|
|
duration = 5000,
|
|
title = "Hello",
|
|
})
|
|
assert(notification_calls[#notification_calls].appId == "flare")
|
|
assert(notification_calls[#notification_calls].text == "World")
|
|
notification_count = #notification_calls
|
|
assert_no_return(high_send_notification, {
|
|
content = "No application",
|
|
})
|
|
assert(#notification_calls == notification_count,
|
|
"High notifications without an application remain a documented partial")
|
|
assert_no_return(high_set_facing, "front")
|
|
assert(selfie_calls[#selfie_calls] == true)
|
|
assert_no_return(high_set_facing, "rear")
|
|
assert(selfie_calls[#selfie_calls] == false)
|
|
local selfie_count = #selfie_calls
|
|
assert_no_return(high_set_facing, "side")
|
|
assert(#selfie_calls == selfie_count, "High Phone facing must reject unknown values")
|
|
assert(high_format_number("5550101") == "555 0101")
|
|
assert_no_return(high_use_phone_item, { ignored = true })
|
|
assert(toggle_calls[#toggle_calls][1] == true)
|
|
|
|
local quasar_is_open = get_alias("qs-smartphone", "IsPhoneOpen")
|
|
local quasar_call = get_alias("qs-smartphone", "call")
|
|
local quasar_open_app = get_alias("qs-smartphone", "OpenPhoneApp")
|
|
assert(quasar_is_open() == true)
|
|
local open_result = table.pack(quasar_open_app("messages"))
|
|
assert(open_result.n == 1 and open_result[1] == true, "Quasar OpenPhoneApp must return one boolean")
|
|
open_result = table.pack(quasar_open_app("missing"))
|
|
assert(open_result.n == 1 and open_result[1] == false, "Quasar OpenPhoneApp failure leaked an error")
|
|
local call_result = quasar_call("5550102", "audio")
|
|
assert(call_result.success == true and call_result.error == nil)
|
|
call_result = quasar_call("5559999", "audio")
|
|
assert(call_result.success == false and call_result.error == "target_unavailable")
|
|
dial_count = #dial_calls
|
|
call_result = quasar_call("5550103", "video")
|
|
assert(call_result.success == false and call_result.error == "video_unsupported")
|
|
assert(#dial_calls == dial_count, "Quasar video calls must be rejected before dialing")
|
|
call_result = quasar_call("5550103", "fax")
|
|
assert(call_result.success == false and call_result.error == "invalid_call_type")
|
|
assert(type(registered_commands["phone:toggle"]) == "function")
|
|
registered_commands["phone:toggle"]()
|
|
assert(toggle_calls[#toggle_calls].n == 0, "Quasar toggle command must use neutral toggle semantics")
|
|
assert(type(registered_commands["phone_peek_call_accept"]) == "function")
|
|
assert(type(registered_commands["phone_peek_call_reject"]) == "function")
|
|
registered_commands["phone_peek_call_accept"]()
|
|
registered_commands["phone_peek_call_reject"]()
|
|
assert(answer_calls == 1 and decline_calls == 1)
|
|
AddEventHandler("phone:pushNotification", function(notification)
|
|
quasar_push_notifications[#quasar_push_notifications + 1] = notification
|
|
end)
|
|
TriggerEvent("sky_phone:client:pushNotification", {
|
|
appId = "messages",
|
|
text = "Push",
|
|
title = "Quasar",
|
|
})
|
|
assert(#quasar_push_notifications == 1, "Quasar push notification event was not forwarded")
|
|
assert(quasar_push_notifications[1].appId == "messages")
|
|
assert(quasar_push_notifications[1].text == "Push")
|
|
|
|
local yseries_toggle = get_alias("yseries", "ToggleOpen")
|
|
local yseries_is_open = get_alias("yseries", "IsOpen")
|
|
local yseries_toggle_flashlight = get_alias("yseries", "ToggleFlashlight")
|
|
local yseries_get_flashlight = get_alias("yseries", "GetFlashlightState")
|
|
local yseries_close_app = get_alias("yseries", "CloseApp")
|
|
local yseries_is_app_installed = get_alias("yseries", "IsAppInstalled")
|
|
local yseries_get_current_app = get_alias("yseries", "GetCurrentAppId")
|
|
local yseries_set_focus = get_alias("yseries", "SetNuiFocusKeepInput")
|
|
local yseries_cancel_call = get_alias("yseries", "CancelCall")
|
|
|
|
assert_no_return(yseries_toggle, false)
|
|
assert(toggle_calls[#toggle_calls][1] == false and yseries_is_open() == true)
|
|
flashlight_count = #flashlight_calls
|
|
assert_no_return(yseries_toggle_flashlight, 1)
|
|
assert(#flashlight_calls == flashlight_count, "YSeries flashlight must reject non-booleans")
|
|
assert_no_return(yseries_toggle_flashlight, false)
|
|
assert(flashlight_calls[#flashlight_calls] == false and yseries_get_flashlight() == false)
|
|
assert_no_return(yseries_close_app)
|
|
assert(navigation_calls[#navigation_calls].action == "close" and navigation_calls[#navigation_calls].appId == nil)
|
|
assert(yseries_is_app_installed("messages") and not yseries_is_app_installed("mail"))
|
|
assert(yseries_get_current_app() == "messages")
|
|
assert(yseries_get_current_app("messages") and not yseries_get_current_app("mail"))
|
|
assert_no_return(yseries_set_focus, false)
|
|
assert(focus_calls[#focus_calls].owner == "provider_test" and focus_calls[#focus_calls].allow == false)
|
|
assert_no_return(yseries_cancel_call)
|
|
assert(terminate_calls == 2)
|
|
|
|
assert(next(registered_exports) == nil, "provider bridge must not publish bare vendor exports")
|
|
for _, path in ipairs({
|
|
"sky_phone/source/bridge/phones/client/core.lua",
|
|
"sky_phone/source/bridge/phones/client/lb.lua",
|
|
"sky_phone/source/bridge/phones/client/seventeen.lua",
|
|
"sky_phone/source/bridge/phones/client/high.lua",
|
|
"sky_phone/source/bridge/phones/client/quasar.lua",
|
|
"sky_phone/source/bridge/phones/client/yseries.lua",
|
|
"sky_phone/source/bridge/phones/client/lifecycle.lua",
|
|
}) do
|
|
local file = assert(io.open(path, "rb"))
|
|
local source = file:read("*a")
|
|
file:close()
|
|
assert(not source:match("[^%a]exports%s*%("), "bare exports() registration found in " .. path)
|
|
end
|
|
|
|
print("Provider client compatibility tests passed")
|