mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 23:01:37 +00:00
398 lines
14 KiB
Lua
398 lines
14 KiB
Lua
local disabled_controls = {}
|
|
local all_controls_disabled = {}
|
|
local firing_disabled = false
|
|
local event_handlers = {}
|
|
local nui_callbacks = {}
|
|
local nui_focus = nil
|
|
local nui_keep_input = nil
|
|
local pressed_controls = {}
|
|
local disabled_pressed_controls = {}
|
|
local triggered_events = {}
|
|
|
|
Config = {
|
|
Phone = {
|
|
AllowMovement = true,
|
|
HoldToLook = { Enabled = true, Control = 19 },
|
|
},
|
|
}
|
|
Bridge = { Debug = function() end }
|
|
|
|
function CreateThread(callback)
|
|
assert(type(callback) == "function", "focus runtime must register its control thread")
|
|
end
|
|
|
|
function AddEventHandler(event_name, callback)
|
|
event_handlers[event_name] = callback
|
|
end
|
|
|
|
function RegisterNUICallback(callback_name, callback)
|
|
nui_callbacks[callback_name] = callback
|
|
end
|
|
|
|
function SetNuiFocus(focused, cursor)
|
|
nui_focus = { focused = focused, cursor = cursor }
|
|
end
|
|
|
|
function SetNuiFocusKeepInput(keep_input)
|
|
nui_keep_input = keep_input
|
|
end
|
|
|
|
function TriggerEvent(name, data)
|
|
triggered_events[#triggered_events + 1] = { name = name, data = data }
|
|
end
|
|
|
|
function IsControlPressed(group, control)
|
|
assert(group == 0, "HoldToLook must read the primary input group")
|
|
return pressed_controls[control] == true
|
|
end
|
|
|
|
function IsDisabledControlPressed(group, control)
|
|
assert(group == 0, "HoldToLook must read disabled controls from the primary input group")
|
|
return disabled_pressed_controls[control] == true
|
|
end
|
|
|
|
function DisableControlAction(group, control, disabled)
|
|
assert(group == 0 and disabled, "phone controls must be disabled in the primary input group")
|
|
disabled_controls[control] = true
|
|
end
|
|
|
|
function DisableAllControlActions(group)
|
|
all_controls_disabled[group] = true
|
|
end
|
|
|
|
function PlayerId()
|
|
return 7
|
|
end
|
|
|
|
function DisablePlayerFiring(player, disabled)
|
|
assert(player == 7, "movement filtering must target the local player")
|
|
firing_disabled = disabled
|
|
end
|
|
|
|
dofile("sky_phone/source/client/focus.lua")
|
|
|
|
assert(not SkyPhoneFocus.IsHoldToLookPressed(), "HoldToLook must be idle until its configured control is held")
|
|
pressed_controls[19] = true
|
|
assert(SkyPhoneFocus.IsHoldToLookPressed(), "HoldToLook must read its configured active control")
|
|
pressed_controls[19] = false
|
|
Config.Phone.HoldToLook.Control = 38
|
|
event_handlers["sky_phone:configurator:updated"]()
|
|
disabled_pressed_controls[38] = true
|
|
assert(
|
|
SkyPhoneFocus.IsHoldToLookPressed(),
|
|
"HoldToLook must read its configured control while NUI focus has disabled GTA input"
|
|
)
|
|
disabled_pressed_controls[38] = false
|
|
Config.Phone.HoldToLook.Enabled = false
|
|
event_handlers["sky_phone:configurator:updated"]()
|
|
assert(not SkyPhoneFocus.IsHoldToLookPressed(), "disabled HoldToLook must reject every control state")
|
|
Config.Phone.HoldToLook.Enabled = true
|
|
Config.Phone.HoldToLook.Control = 19
|
|
event_handlers["sky_phone:configurator:updated"]()
|
|
|
|
local function resolve(overrides)
|
|
local state = {
|
|
activity_suspended = false,
|
|
allow_movement = false,
|
|
call_focus = false,
|
|
camera_active = false,
|
|
camera_nui_focused = true,
|
|
cursor_disabled = false,
|
|
external_game_input = nil,
|
|
is_open = false,
|
|
look_passthrough = false,
|
|
notification_focus = false,
|
|
payphone_focus = false,
|
|
sim_picker_open = false,
|
|
text_input_focused = false,
|
|
}
|
|
for key, value in pairs(overrides or {}) do
|
|
state[key] = value
|
|
end
|
|
return SkyPhoneFocus.Resolve(state)
|
|
end
|
|
|
|
local idle = resolve()
|
|
assert(
|
|
not idle.cursor and not idle.focused and not idle.keep_input,
|
|
"idle NUI must release focus and game input override"
|
|
)
|
|
|
|
local minimized_call = resolve()
|
|
assert(not minimized_call.focused, "a replayed call without an attention claim must stay unfocused")
|
|
|
|
local incoming_call = resolve({ call_focus = true })
|
|
assert(
|
|
incoming_call.cursor and incoming_call.focused and not incoming_call.keep_input,
|
|
"incoming call attention must focus the NUI"
|
|
)
|
|
|
|
local stationary_phone = resolve({ is_open = true })
|
|
assert(
|
|
stationary_phone.cursor
|
|
and stationary_phone.focused
|
|
and stationary_phone.block_game
|
|
and not stationary_phone.keep_input,
|
|
"an open phone must block game input when movement is disabled"
|
|
)
|
|
|
|
local movable_phone = resolve({ allow_movement = true, is_open = true })
|
|
assert(
|
|
movable_phone.cursor
|
|
and movable_phone.focused
|
|
and movable_phone.keep_input
|
|
and movable_phone.game_input
|
|
and not movable_phone.block_game
|
|
and movable_phone.block_look,
|
|
"an open phone must allow movement without hiding the NUI cursor"
|
|
)
|
|
|
|
local typing_phone = resolve({
|
|
allow_movement = true,
|
|
is_open = true,
|
|
text_input_focused = true,
|
|
})
|
|
assert(
|
|
typing_phone.cursor
|
|
and typing_phone.focused
|
|
and not typing_phone.keep_input
|
|
and not typing_phone.game_input
|
|
and typing_phone.block_game,
|
|
"a focused phone text input must stop game-input passthrough and retain the NUI cursor"
|
|
)
|
|
|
|
local external_movement_phone = resolve({
|
|
external_game_input = true,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
external_movement_phone.cursor
|
|
and external_movement_phone.focused
|
|
and external_movement_phone.keep_input
|
|
and external_movement_phone.game_input
|
|
and not external_movement_phone.block_game,
|
|
"an external input claim must preserve movement while the app cursor is active"
|
|
)
|
|
|
|
local external_movement_typing_phone = resolve({
|
|
external_game_input = true,
|
|
is_open = true,
|
|
text_input_focused = true,
|
|
})
|
|
assert(
|
|
external_movement_typing_phone.cursor
|
|
and external_movement_typing_phone.focused
|
|
and not external_movement_typing_phone.keep_input
|
|
and not external_movement_typing_phone.game_input
|
|
and external_movement_typing_phone.block_game,
|
|
"a focused text input must override an external movement claim"
|
|
)
|
|
|
|
local external_typing_phone = resolve({
|
|
allow_movement = true,
|
|
external_game_input = false,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
external_typing_phone.cursor
|
|
and external_typing_phone.focused
|
|
and not external_typing_phone.keep_input
|
|
and not external_typing_phone.game_input
|
|
and external_typing_phone.block_game,
|
|
"an external typing claim must block GTA input"
|
|
)
|
|
|
|
local movable_cursor_disabled_phone = resolve({
|
|
allow_movement = true,
|
|
cursor_disabled = true,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
not movable_cursor_disabled_phone.cursor
|
|
and movable_cursor_disabled_phone.focused
|
|
and movable_cursor_disabled_phone.keep_input
|
|
and movable_cursor_disabled_phone.game_input
|
|
and not movable_cursor_disabled_phone.block_game
|
|
and not movable_cursor_disabled_phone.block_look,
|
|
"LB noFocus must preserve movement and camera look without retaining the NUI cursor"
|
|
)
|
|
|
|
local movable_look_passthrough_phone = resolve({
|
|
allow_movement = true,
|
|
is_open = true,
|
|
look_passthrough = true,
|
|
})
|
|
assert(
|
|
not movable_look_passthrough_phone.cursor
|
|
and movable_look_passthrough_phone.focused
|
|
and movable_look_passthrough_phone.keep_input
|
|
and movable_look_passthrough_phone.game_input
|
|
and not movable_look_passthrough_phone.block_game
|
|
and not movable_look_passthrough_phone.block_look,
|
|
"HoldToLook must temporarily release the NUI cursor without disabling configured movement"
|
|
)
|
|
|
|
local stationary_cursor_disabled_phone = resolve({
|
|
cursor_disabled = true,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
not stationary_cursor_disabled_phone.cursor
|
|
and stationary_cursor_disabled_phone.focused
|
|
and not stationary_cursor_disabled_phone.keep_input,
|
|
"LB noFocus must hide the cursor even when phone movement is disabled"
|
|
)
|
|
|
|
SkyPhoneFocus.ApplyFocusedControls()
|
|
for _, group in ipairs({ 0, 1, 2 }) do
|
|
assert(all_controls_disabled[group], ("focused phone cursor must block input group %d while typing"):format(group))
|
|
end
|
|
assert(firing_disabled, "focused phone cursor must block attacks while typing")
|
|
|
|
all_controls_disabled = {}
|
|
firing_disabled = false
|
|
SkyPhoneFocus.ApplyGameInputControls(true)
|
|
for _, control in ipairs({ 24, 140, 141, 142, 199, 200, 257, 263, 264 }) do
|
|
assert(disabled_controls[control], ("phone control %d must remain disabled"):format(control))
|
|
end
|
|
assert(not disabled_controls[19], "Alt must remain available while no phone text input is focused")
|
|
for _, control in ipairs({ 1, 2, 3, 4, 5, 6 }) do
|
|
assert(disabled_controls[control], ("look control %d must be disabled while the phone cursor is active"):format(control))
|
|
end
|
|
assert(not disabled_controls[21] and not disabled_controls[22], "sprint and jump must stay enabled")
|
|
assert(not disabled_controls[30] and not disabled_controls[31], "movement axes must stay enabled")
|
|
assert(firing_disabled, "player attacks must remain disabled while the phone is open")
|
|
|
|
disabled_controls = {}
|
|
firing_disabled = false
|
|
SkyPhoneFocus.ApplyGameInputControls(false)
|
|
assert(not disabled_controls[1] and not disabled_controls[2], "camera passthrough must preserve camera look")
|
|
for _, control in ipairs({ 30, 31, 32, 33, 34, 35 }) do
|
|
assert(not disabled_controls[control], ("camera passthrough must preserve movement control %d"):format(control))
|
|
end
|
|
assert(firing_disabled, "player attacks must remain disabled during camera passthrough")
|
|
|
|
local movable_notification = resolve({ allow_movement = true, notification_focus = true })
|
|
assert(
|
|
movable_notification.focused and not movable_notification.keep_input,
|
|
"movement configuration must not affect a notification without an open phone"
|
|
)
|
|
|
|
local camera_game_input = resolve({
|
|
camera_active = true,
|
|
camera_nui_focused = false,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
not camera_game_input.cursor
|
|
and camera_game_input.focused
|
|
and camera_game_input.keep_input
|
|
and camera_game_input.game_input
|
|
and not camera_game_input.block_look,
|
|
"camera movement must keep keyboard focus and GTA movement without retaining the NUI cursor"
|
|
)
|
|
|
|
local focused_camera = resolve({
|
|
allow_movement = true,
|
|
camera_active = true,
|
|
camera_nui_focused = true,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
focused_camera.cursor
|
|
and focused_camera.focused
|
|
and focused_camera.keep_input
|
|
and not focused_camera.game_input,
|
|
"focused camera must forward readable input while blocking game actions until passthrough is held"
|
|
)
|
|
|
|
event_handlers["sky_phone:client:setCameraFocus"]({ active = true, nuiFocused = true })
|
|
local focused_camera_event = triggered_events[#triggered_events]
|
|
assert(
|
|
nui_focus.focused and nui_focus.cursor and nui_keep_input
|
|
and focused_camera_event.name == "sky_phone:client:cameraFocusApplied"
|
|
and not focused_camera_event.data.gameInput,
|
|
"focused camera must keep controls readable without reporting movement passthrough"
|
|
)
|
|
event_handlers["sky_phone:client:setCameraFocus"]({ active = true, nuiFocused = false })
|
|
local passthrough_camera_event = triggered_events[#triggered_events]
|
|
assert(
|
|
nui_focus.focused and not nui_focus.cursor and nui_keep_input
|
|
and passthrough_camera_event.data.gameInput,
|
|
"camera passthrough must apply keyboard focus without a cursor and keep GTA input enabled"
|
|
)
|
|
event_handlers["sky_phone:client:setCameraFocus"]({ active = false, nuiFocused = true })
|
|
|
|
local camera_interrupted_by_call = resolve({
|
|
call_focus = true,
|
|
camera_active = true,
|
|
camera_nui_focused = false,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
camera_interrupted_by_call.cursor
|
|
and camera_interrupted_by_call.focused
|
|
and not camera_interrupted_by_call.keep_input,
|
|
"incoming call attention must override camera passthrough input"
|
|
)
|
|
|
|
local camera_after_connected_call = resolve({
|
|
call_focus = false,
|
|
camera_active = true,
|
|
camera_nui_focused = false,
|
|
is_open = true,
|
|
})
|
|
assert(
|
|
not camera_after_connected_call.cursor
|
|
and camera_after_connected_call.focused
|
|
and camera_after_connected_call.keep_input,
|
|
"connected call without an attention claim must restore camera movement input"
|
|
)
|
|
|
|
local payphone_closed_behind_phone = resolve({ is_open = true, payphone_focus = false })
|
|
assert(payphone_closed_behind_phone.focused, "releasing payphone focus must not clear mobile phone focus")
|
|
|
|
local suspended = resolve({ activity_suspended = true, call_focus = true, is_open = true })
|
|
assert(not suspended.focused and not suspended.keep_input, "suspended activities must mask every focus claim")
|
|
|
|
SkyPhoneFocus.SetPhone(true, true)
|
|
assert(
|
|
nui_focus.focused and not nui_focus.cursor and nui_keep_input,
|
|
"runtime phone focus must preserve the no-focus opening contract"
|
|
)
|
|
|
|
SkyPhoneFocus.SetPhone(false)
|
|
SkyPhoneFocus.SetPhone(true)
|
|
assert(nui_focus.cursor, "closing the phone must clear the previous no-focus claim")
|
|
SkyPhoneFocus.SetTextInputFocused(true)
|
|
assert(not nui_keep_input, "runtime text focus must stop game-input passthrough")
|
|
SkyPhoneFocus.SetTextInputFocused(false)
|
|
assert(nui_keep_input, "leaving a text input must restore configured phone movement")
|
|
|
|
local external_success, external_error = SkyPhoneFocus.SetExternalGameInput("custom_app", true)
|
|
assert(external_success and external_error == nil and nui_keep_input, "external movement claim must apply")
|
|
external_success, external_error = SkyPhoneFocus.SetExternalGameInput("custom_app", false)
|
|
assert(external_success and external_error == nil and not nui_keep_input, "external typing claim must apply")
|
|
event_handlers["onClientResourceStop"]("custom_app")
|
|
assert(nui_keep_input, "resource stop must restore configured phone movement")
|
|
|
|
SkyPhoneFocus.SetPhone(false)
|
|
external_success, external_error = SkyPhoneFocus.SetExternalGameInput("custom_app", true)
|
|
assert(not external_success and external_error == "phone_closed", "closed phones must reject external focus claims")
|
|
|
|
local notification_result
|
|
SkyPhoneFocus.SetPhone(false)
|
|
nui_callbacks["notification:focus"]({ active = true }, function(result)
|
|
notification_result = result
|
|
end)
|
|
assert(notification_result.success and nui_focus.focused, "notification focus must be applied through the focus owner")
|
|
|
|
SkyPhoneFocus.BeginNuiHydration()
|
|
SkyPhoneFocus.Reapply()
|
|
assert(not nui_focus.focused, "CEF hydration must discard browser-owned notification focus")
|
|
|
|
SkyPhoneFocus.Reset()
|
|
assert(not nui_focus.focused and not nui_focus.cursor and not nui_keep_input, "focus reset must release NUI input")
|
|
|
|
print("Client focus tests passed")
|