Files
Leon.Schmidt ea959f59f2 FIX - restore camera input and configurator warning (#30)
* FIX - honor configured camera look control

Camera focus was owned by hard-coded Space DOM handlers, bypassing Config.Phone.HoldToLook. Move focus input to the FiveM client and add a bounded scripted-camera orbit so selfie mode can rotate while the configured control is held.

* FIX - preserve camera movement passthrough

* FIX - restore Phone Configurator startup warning

* FIX - keep camera modifier input readable
2026-08-23 15:50:25 +02:00

210 lines
7.3 KiB
Lua

local callbacks = {}
local camera_coord = nil
local camera_target = nil
local camera_created = false
local camera_destroyed = false
local scripted_camera_rendering = false
local event_handlers = {}
local threads = {}
local hold_to_look_pressed = false
local disabled_controls = {}
local disabled_pressed_controls = {}
local disabled_control_normals = {}
local triggered_events = {}
local thread_stop = {}
SkyPhoneFocus = {
IsHoldToLookPressed = function()
return hold_to_look_pressed
end,
}
local vector_meta = {}
vector_meta.__index = vector_meta
function vector_meta.__add(left, right)
return vector3(left.x + right.x, left.y + right.y, left.z + right.z)
end
function vector_meta.__sub(left, right)
return vector3(left.x - right.x, left.y - right.y, left.z - right.z)
end
function vector_meta.__mul(left, right)
if type(left) == "number" then
left, right = right, left
end
return vector3(left.x * right, left.y * right, left.z * right)
end
function vector3(x, y, z)
return setmetatable({ x = x, y = y, z = z }, vector_meta)
end
function RegisterNUICallback(name, callback)
callbacks[name] = callback
end
function RegisterNetEvent() end
function AddEventHandler(name, callback)
event_handlers[name] = callback
end
function TriggerEvent(name, data)
triggered_events[#triggered_events + 1] = { name = name, data = data }
end
function TriggerServerEvent() end
function SendNUIMessage() end
function CreateThread(callback)
threads[#threads + 1] = callback
end
function Wait()
error(thread_stop, 0)
end
function PlayerPedId() return 7 end
function PlayerId() return 8 end
function GetFollowPedCamViewMode() return 1 end
function GetFollowVehicleCamViewMode() return 2 end
function IsRadarHidden() return false end
function DisplayRadar() end
function SetFollowPedCamViewMode() end
function SetFollowVehicleCamViewMode() end
function IsPedInAnyVehicle() return false end
function DisableControlAction(group, control, disabled)
assert(group == 0 and disabled, "camera controls must be disabled in the primary input group")
disabled_controls[control] = true
end
function DisablePlayerFiring() end
function HideHudAndRadarThisFrame() end
function GetCurrentResourceName() return "sky_phone" end
function IsDisabledControlJustPressed() return false end
function IsDisabledControlPressed(group, control)
assert(group == 0, "camera passthrough must read the primary input group")
return disabled_pressed_controls[control] == true
end
function GetDisabledControlNormal(_, control) return disabled_control_normals[control] or 0.0 end
function GetEntityCoords()
return vector3(10.0, 20.0, 1.0)
end
function GetPedBoneCoords()
return vector3(10.0, 20.0, 2.7)
end
function GetEntityForwardVector()
return vector3(0.0, 1.0, 0.0)
end
function CreateCam(name, active)
assert(name == "DEFAULT_SCRIPTED_CAMERA" and active, "selfie camera must be created active")
camera_created = true
camera_destroyed = false
return 73
end
function DoesCamExist(handle)
return camera_created and not camera_destroyed and handle == 73
end
function SetCamFov() end
function SetCamActive() end
function RenderScriptCams(active)
scripted_camera_rendering = active
end
function SetCamCoord(_, x, y, z)
camera_coord = vector3(x, y, z)
end
function PointCamAtCoord(_, x, y, z)
camera_target = vector3(x, y, z)
end
function DestroyCam(handle)
assert(handle == 73, "the selfie camera handle must be destroyed")
camera_destroyed = true
end
local function response_from(name, data)
local response = nil
callbacks[name](data, function(value)
response = value
end)
return response
end
local function close_enough(actual, expected)
return math.abs(actual - expected) < 0.0001
end
dofile("sky_phone/source/client/camera.lua")
assert(response_from("camera:setActive", { active = true }).success)
assert(response_from("camera:setFacing", { front = true }).success)
assert(camera_created and scripted_camera_rendering, "selfie mode must render a scripted camera")
assert(camera_coord and camera_target, "selfie mode must position and aim the camera")
assert(close_enough(camera_coord.x, 10.0))
assert(close_enough(camera_coord.y, 21.05))
assert(close_enough(camera_coord.z, 2.75))
assert(close_enough(camera_target.x, 10.0))
assert(close_enough(camera_target.y, 20.0))
assert(close_enough(camera_target.z, 2.73))
event_handlers["sky_phone:client:cameraFocusApplied"]({
active = true,
cursor = false,
focused = true,
gameInput = true,
})
disabled_pressed_controls[22] = true
disabled_control_normals[1] = 0.2
local watcher_ok, watcher_error = pcall(threads[2])
assert(not watcher_ok and watcher_error == thread_stop, "camera watcher must run one controlled frame")
local camera_focus_event = triggered_events[#triggered_events]
assert(
camera_focus_event.name == "sky_phone:client:setCameraFocus"
and camera_focus_event.data.active
and not camera_focus_event.data.nuiFocused,
"holding Space must release the camera cursor for simultaneous look and movement"
)
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
local camera_ok, camera_error = pcall(threads[1])
assert(not camera_ok and camera_error == thread_stop, "selfie camera must render one controlled frame")
assert(not close_enough(camera_coord.x, 10.0), "horizontal look input must orbit the selfie camera around the player")
assert(camera_coord.y < 21.05, "selfie orbit must retain its configured distance from the player")
disabled_pressed_controls[22] = false
disabled_control_normals[1] = 0.0
assert(response_from("camera:setFacing", { front = false }).success)
assert(camera_destroyed and not scripted_camera_rendering, "rear mode must release the selfie camera")
assert(type(SkyPhoneCamera.EnableWalkable) == "function", "walkable camera enable seam must exist")
assert(type(SkyPhoneCamera.DisableWalkable) == "function", "walkable camera disable seam must exist")
assert(type(SkyPhoneCamera.SetSelfie) == "function", "selfie camera seam must exist")
assert(type(SkyPhoneCamera.ToggleFrozen) == "function", "frozen camera seam must exist")
assert(type(SkyPhoneCamera.SetFlashlight) == "function", "flashlight seam must exist")
assert(type(SkyPhoneCamera.GetState) == "function", "camera state seam must exist")
SkyPhoneCamera.SetFlashlight(true)
assert(SkyPhoneCamera.GetState().flashEnabled, "flashlight state must report enabled")
SkyPhoneCamera.SetFlashlight(false)
assert(not SkyPhoneCamera.GetState().flashEnabled, "flashlight state must report disabled")
SkyPhoneCamera.EnableWalkable(true)
local walkable_state = SkyPhoneCamera.GetState()
assert(walkable_state.walkable, "walkable camera must report enabled")
assert(walkable_state.selfie, "walkable camera must preserve selfie mode")
assert(walkable_state.active, "walkable camera must open the camera")
SkyPhoneCamera.SetSelfie(false)
assert(not SkyPhoneCamera.GetState().selfie, "selfie camera must switch back to rear mode")
SkyPhoneCamera.ToggleFrozen()
SkyPhoneCamera.DisableWalkable()
local closed_state = SkyPhoneCamera.GetState()
assert(not closed_state.walkable, "walkable camera must report disabled")
assert(not closed_state.active, "walkable camera disable must close the camera")
print("Client camera tests passed")