mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
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
This commit is contained in:
@@ -46,10 +46,16 @@ describe('fixed server permissions', () => {
|
|||||||
expect(configurator).toContain(`["${path}"] = true`)
|
expect(configurator).toContain(`["${path}"] = true`)
|
||||||
}
|
}
|
||||||
expect(configurator).toContain(
|
expect(configurator).toContain(
|
||||||
'Phone Configurator enabled: file-based settings from config.lua',
|
'SKY PHONE CONFIGURATION FILES ARE DISABLED',
|
||||||
)
|
)
|
||||||
expect(configurator).toContain(
|
expect(configurator).toContain(
|
||||||
'(except Config.CommandPermissions) and media.lua are disabled',
|
'^1 Runtime settings from config.lua and media.lua are DISABLED.^0',
|
||||||
|
)
|
||||||
|
expect(configurator).toContain(
|
||||||
|
'^1 Configure all phone and media settings IN GAME through /phonepanel.^0',
|
||||||
|
)
|
||||||
|
expect(configurator).toContain(
|
||||||
|
'^1 Only Config.PhoneConfigurator.Enabled and Config.CommandPermissions remain file-based.^0',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4523,8 +4523,8 @@ const defaultLocales: LocaleTree = {
|
|||||||
video: 'Video',
|
video: 'Video',
|
||||||
microphoneOn: 'Microphone on',
|
microphoneOn: 'Microphone on',
|
||||||
microphoneOff: 'Microphone muted',
|
microphoneOff: 'Microphone muted',
|
||||||
focusHelp: 'Space for movement',
|
focusHelp: 'Hold the look key to look around',
|
||||||
returnHelp: 'Space to return',
|
returnHelp: 'Release the look key for controls',
|
||||||
uploading: '{count} uploading',
|
uploading: '{count} uploading',
|
||||||
saving: 'Saving video...',
|
saving: 'Saving video...',
|
||||||
openGallery: 'Open Photos',
|
openGallery: 'Open Photos',
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ describe('Camera app controls', () => {
|
|||||||
it('locks look controls without changing the global gameplay camera', () => {
|
it('locks look controls without changing the global gameplay camera', () => {
|
||||||
expect(cameraView).toContain("nuiCall('camera:setLocked'")
|
expect(cameraView).toContain("nuiCall('camera:setLocked'")
|
||||||
expect(cameraView).toContain('cameraLocked.value')
|
expect(cameraView).toContain('cameraLocked.value')
|
||||||
expect(cameraView).toContain('Apps.camera.spaceKey')
|
expect(cameraView).toContain('Apps.camera.lookKey')
|
||||||
|
expect(cameraView).not.toContain('Apps.camera.spaceKey')
|
||||||
expect(cameraClient).toContain('RegisterNUICallback("camera:setLocked"')
|
expect(cameraClient).toContain('RegisterNUICallback("camera:setLocked"')
|
||||||
expect(cameraClient).toContain('INPUT_LOOK_LR')
|
expect(cameraClient).toContain('INPUT_LOOK_LR')
|
||||||
expect(cameraClient).toContain('INPUT_LOOK_UD')
|
expect(cameraClient).toContain('INPUT_LOOK_UD')
|
||||||
@@ -95,10 +96,26 @@ describe('Camera app controls', () => {
|
|||||||
expect(cameraClient).not.toContain('ensure_ultrawide_camera')
|
expect(cameraClient).not.toContain('ensure_ultrawide_camera')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the selfie camera stable while Space still allows movement', () => {
|
it('uses the configured HoldToLook control and Space in camera modes', () => {
|
||||||
expect(cameraView).toMatch(
|
expect(cameraView).not.toContain("event.code !== 'Space'")
|
||||||
/event\.code !== 'Space'[\s\S]*cameraLocked\.value/,
|
expect(cameraView).not.toContain("window.addEventListener('keydown'")
|
||||||
|
expect(focusClient).toContain(
|
||||||
|
'function SkyPhoneFocus.IsHoldToLookPressed()',
|
||||||
)
|
)
|
||||||
|
expect(focusClient).toContain(
|
||||||
|
'IsDisabledControlPressed(0, hold_to_look_control)',
|
||||||
|
)
|
||||||
|
expect(cameraClient).toContain('SkyPhoneFocus.IsHoldToLookPressed()')
|
||||||
|
expect(cameraClient).toContain(
|
||||||
|
'IsDisabledControlPressed(0, camera_passthrough_control)',
|
||||||
|
)
|
||||||
|
expect(cameraClient).toMatch(
|
||||||
|
/if data\.active then\s+watch_camera_controls\(\)/,
|
||||||
|
)
|
||||||
|
expect(cameraClient).not.toContain('IsDisabledControlJustReleased(0, 22)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('orbits the stable selfie camera while HoldToLook allows movement', () => {
|
||||||
expect(cameraClient).toContain(
|
expect(cameraClient).toContain(
|
||||||
'if camera_state.locked or camera_state.front_camera then',
|
'if camera_state.locked or camera_state.front_camera then',
|
||||||
)
|
)
|
||||||
@@ -106,14 +123,22 @@ describe('Camera app controls', () => {
|
|||||||
expect(cameraClient).toContain('local front_camera_view_mode = 0')
|
expect(cameraClient).toContain('local front_camera_view_mode = 0')
|
||||||
expect(cameraClient).toContain('local front_camera_fov = 32.0')
|
expect(cameraClient).toContain('local front_camera_fov = 32.0')
|
||||||
expect(cameraClient).toContain('local front_camera_distance = 1.05')
|
expect(cameraClient).toContain('local front_camera_distance = 1.05')
|
||||||
|
expect(cameraClient).toContain('local front_camera_horizontal_limit = 75.0')
|
||||||
|
expect(cameraClient).toContain('local front_camera_vertical_limit = 35.0')
|
||||||
expect(cameraClient).toContain('local head_position = GetPedBoneCoords')
|
expect(cameraClient).toContain('local head_position = GetPedBoneCoords')
|
||||||
expect(cameraClient).toContain(
|
expect(cameraClient).toContain('GetDisabledControlNormal(0, 1)')
|
||||||
'local dot = (to_camera.x * forward_vector.x)',
|
expect(cameraClient).toContain('GetDisabledControlNormal(0, 2)')
|
||||||
)
|
expect(cameraClient).toContain('update_front_camera_orbit()')
|
||||||
|
expect(cameraClient).toContain('camera_state.front_camera_yaw')
|
||||||
|
expect(cameraClient).toContain('camera_state.front_camera_pitch')
|
||||||
expect(cameraClient).toContain('front_camera_target_height')
|
expect(cameraClient).toContain('front_camera_target_height')
|
||||||
expect(focusClient).toContain(
|
expect(focusClient).toContain(
|
||||||
'return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true }',
|
'return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true }',
|
||||||
)
|
)
|
||||||
|
expect(focusClient).toContain(
|
||||||
|
'return { block_game = true, block_look = true, cursor = true, focused = true, game_input = false, keep_input = true }',
|
||||||
|
)
|
||||||
|
expect(focusClient).toContain('gameInput = focus.game_input')
|
||||||
expect(cameraClient).toContain('SetCamCoord(')
|
expect(cameraClient).toContain('SetCamCoord(')
|
||||||
expect(cameraClient).toContain('PointCamAtCoord(')
|
expect(cameraClient).toContain('PointCamAtCoord(')
|
||||||
expect(cameraClient).not.toContain('SetCamRot(')
|
expect(cameraClient).not.toContain('SetCamRot(')
|
||||||
@@ -121,10 +146,6 @@ describe('Camera app controls', () => {
|
|||||||
expect(cameraClient).not.toContain('front_camera_position')
|
expect(cameraClient).not.toContain('front_camera_position')
|
||||||
expect(cameraClient).not.toContain('AttachCamToEntity(')
|
expect(cameraClient).not.toContain('AttachCamToEntity(')
|
||||||
expect(cameraClient).not.toContain('PointCamAtEntity(')
|
expect(cameraClient).not.toContain('PointCamAtEntity(')
|
||||||
expect(cameraView).toContain("window.addEventListener('keyup', onKeyup)")
|
|
||||||
expect(cameraView).toContain(
|
|
||||||
"nuiCall('camera:setFocus', { focused: true })",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses a looping camera-hold pose instead of the old selfie dance', () => {
|
it('uses a looping camera-hold pose instead of the old selfie dance', () => {
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ const microphoneEnabled = ref(true)
|
|||||||
const frontCamera = ref(false)
|
const frontCamera = ref(false)
|
||||||
const shutterActive = ref(false)
|
const shutterActive = ref(false)
|
||||||
const cameraLocked = ref(false)
|
const cameraLocked = ref(false)
|
||||||
const movementEnabled = ref(false)
|
|
||||||
const recording = ref(false)
|
const recording = ref(false)
|
||||||
const savingVideo = ref(false)
|
const savingVideo = ref(false)
|
||||||
const recordingStartedAt = ref(0)
|
const recordingStartedAt = ref(0)
|
||||||
@@ -238,10 +237,6 @@ async function toggleFacing(): Promise<void> {
|
|||||||
|
|
||||||
async function toggleCameraLock(): Promise<void> {
|
async function toggleCameraLock(): Promise<void> {
|
||||||
cameraLocked.value = !cameraLocked.value
|
cameraLocked.value = !cameraLocked.value
|
||||||
if (cameraLocked.value && movementEnabled.value) {
|
|
||||||
movementEnabled.value = false
|
|
||||||
await nuiCall('camera:setFocus', { focused: true })
|
|
||||||
}
|
|
||||||
await nuiCall('camera:setLocked', { locked: cameraLocked.value })
|
await nuiCall('camera:setLocked', { locked: cameraLocked.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,26 +335,6 @@ function updateRecordingTimer(): void {
|
|||||||
elapsed.value = formatRecordingDuration(Date.now() - recordingStartedAt.value)
|
elapsed.value = formatRecordingDuration(Date.now() - recordingStartedAt.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown(event: KeyboardEvent): void {
|
|
||||||
if (
|
|
||||||
event.code !== 'Space' ||
|
|
||||||
event.repeat ||
|
|
||||||
cameraLocked.value ||
|
|
||||||
movementEnabled.value
|
|
||||||
)
|
|
||||||
return
|
|
||||||
event.preventDefault()
|
|
||||||
movementEnabled.value = true
|
|
||||||
void nuiCall('camera:setFocus', { focused: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKeyup(event: KeyboardEvent): void {
|
|
||||||
if (event.code !== 'Space' || !movementEnabled.value) return
|
|
||||||
event.preventDefault()
|
|
||||||
movementEnabled.value = false
|
|
||||||
void nuiCall('camera:setFocus', { focused: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMessage(event: MessageEvent): void {
|
function onMessage(event: MessageEvent): void {
|
||||||
if (!isTrustedRootMessageSource(event.source, window)) return
|
if (!isTrustedRootMessageSource(event.source, window)) return
|
||||||
const message = event.data as {
|
const message = event.data as {
|
||||||
@@ -442,8 +417,6 @@ onMounted(() => {
|
|||||||
{ data: { zoom: selectedZoom.value }, type: 'camera:zoom' },
|
{ data: { zoom: selectedZoom.value }, type: 'camera:zoom' },
|
||||||
'*',
|
'*',
|
||||||
)
|
)
|
||||||
window.addEventListener('keydown', onKeydown)
|
|
||||||
window.addEventListener('keyup', onKeyup)
|
|
||||||
window.addEventListener('message', onMessage)
|
window.addEventListener('message', onMessage)
|
||||||
void nuiCall('camera:setActive', { active: true })
|
void nuiCall('camera:setActive', { active: true })
|
||||||
void nuiCall<MediaConfig>('media:config').then((response) => {
|
void nuiCall<MediaConfig>('media:config').then((response) => {
|
||||||
@@ -459,13 +432,7 @@ onBeforeUnmount(() => {
|
|||||||
if (shutterTimer !== undefined) window.clearTimeout(shutterTimer)
|
if (shutterTimer !== undefined) window.clearTimeout(shutterTimer)
|
||||||
if (noticeTimer !== undefined) window.clearTimeout(noticeTimer)
|
if (noticeTimer !== undefined) window.clearTimeout(noticeTimer)
|
||||||
if (recordingTimer !== undefined) window.clearInterval(recordingTimer)
|
if (recordingTimer !== undefined) window.clearInterval(recordingTimer)
|
||||||
window.removeEventListener('keydown', onKeydown)
|
|
||||||
window.removeEventListener('keyup', onKeyup)
|
|
||||||
window.removeEventListener('message', onMessage)
|
window.removeEventListener('message', onMessage)
|
||||||
if (movementEnabled.value) {
|
|
||||||
movementEnabled.value = false
|
|
||||||
void nuiCall('camera:setFocus', { focused: true })
|
|
||||||
}
|
|
||||||
if (renderFrameId !== undefined) window.cancelAnimationFrame(renderFrameId)
|
if (renderFrameId !== undefined) window.cancelAnimationFrame(renderFrameId)
|
||||||
resizeObserver?.disconnect()
|
resizeObserver?.disconnect()
|
||||||
gameView?.dispose()
|
gameView?.dispose()
|
||||||
@@ -587,7 +554,7 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<LockKeyhole v-if="cameraLocked" :size="12" />
|
<LockKeyhole v-if="cameraLocked" :size="12" />
|
||||||
<LockOpen v-else :size="12" />
|
<LockOpen v-else :size="12" />
|
||||||
<kbd>{{ phone.t('Apps.camera.spaceKey') }}</kbd>
|
<kbd>{{ phone.t('Apps.camera.lookKey') }}</kbd>
|
||||||
</SkyButton>
|
</SkyButton>
|
||||||
<sky-fab
|
<sky-fab
|
||||||
component="button"
|
component="button"
|
||||||
|
|||||||
@@ -1449,8 +1449,8 @@ Locales["de"] = {
|
|||||||
camera = {
|
camera = {
|
||||||
name = "Kamera", flash = "Blitz", flip = "Kamera wechseln", landscape = "Zum Querformat wechseln",
|
name = "Kamera", flash = "Blitz", flip = "Kamera wechseln", landscape = "Zum Querformat wechseln",
|
||||||
portrait = "Zum Hochformat wechseln", photo = "Foto", video = "Video", microphoneOn = "Mikrofon eingeschaltet", microphoneOff = "Mikrofon stumm",
|
portrait = "Zum Hochformat wechseln", photo = "Foto", video = "Video", microphoneOn = "Mikrofon eingeschaltet", microphoneOff = "Mikrofon stumm",
|
||||||
focusHelp = "Halte die Leertaste gedrückt, um dich umzusehen", returnHelp = "Leertaste für Steuerung", lockCamera = "Kamerabewegung sperren",
|
focusHelp = "Halte die Umschautaste gedrückt, um dich umzusehen", returnHelp = "Umschautaste loslassen für Steuerung", lockCamera = "Kamerabewegung sperren",
|
||||||
unlockCamera = "Kamerabewegung entsperren", spaceKey = "Leertaste", uploading = "Upload: {count}",
|
unlockCamera = "Kamerabewegung entsperren", lookKey = "Umschauen", uploading = "Upload: {count}",
|
||||||
saving = "Video speichern...", openGallery = "Fotos öffnen", takePhoto = "Foto machen",
|
saving = "Video speichern...", openGallery = "Fotos öffnen", takePhoto = "Foto machen",
|
||||||
startRecording = "Aufnahme starten", stopRecording = "Aufnahme beenden", saved = "Auf Fotos gespeichert.",
|
startRecording = "Aufnahme starten", stopRecording = "Aufnahme beenden", saved = "Auf Fotos gespeichert.",
|
||||||
zoom = "Kamerazoom auf {zoom} einstellen",
|
zoom = "Kamerazoom auf {zoom} einstellen",
|
||||||
|
|||||||
@@ -1449,8 +1449,8 @@ Locales["en"] = {
|
|||||||
camera = {
|
camera = {
|
||||||
name = "Camera", flash = "Flash", flip = "Flip camera", landscape = "Switch to landscape",
|
name = "Camera", flash = "Flash", flip = "Flip camera", landscape = "Switch to landscape",
|
||||||
portrait = "Switch to portrait", photo = "Photo", video = "Video", microphoneOn = "Microphone on", microphoneOff = "Microphone muted",
|
portrait = "Switch to portrait", photo = "Photo", video = "Video", microphoneOn = "Microphone on", microphoneOff = "Microphone muted",
|
||||||
focusHelp = "Hold Space to look around", returnHelp = "Release Space for controls", lockCamera = "Lock camera movement",
|
focusHelp = "Hold the look key to look around", returnHelp = "Release the look key for controls", lockCamera = "Lock camera movement",
|
||||||
unlockCamera = "Unlock camera movement", spaceKey = "Space", uploading = "{count} uploading",
|
unlockCamera = "Unlock camera movement", lookKey = "Look", uploading = "{count} uploading",
|
||||||
saving = "Saving video...", openGallery = "Open Photos", takePhoto = "Take photo",
|
saving = "Saving video...", openGallery = "Open Photos", takePhoto = "Take photo",
|
||||||
startRecording = "Start recording", stopRecording = "Stop recording", saved = "Saved to Photos.",
|
startRecording = "Start recording", stopRecording = "Stop recording", saved = "Saved to Photos.",
|
||||||
zoom = "Set camera zoom to {zoom}",
|
zoom = "Set camera zoom to {zoom}",
|
||||||
|
|||||||
@@ -1449,8 +1449,8 @@ Locales["es"] = {
|
|||||||
camera = {
|
camera = {
|
||||||
name = "Cámara", flash = "Linterna", flip = "Cambiar de cámara", landscape = "Cambiar a paisaje",
|
name = "Cámara", flash = "Linterna", flip = "Cambiar de cámara", landscape = "Cambiar a paisaje",
|
||||||
portrait = "Cambiar a vertical", photo = "Foto", video = "Vídeo", microphoneOn = "Micrófono encendido", microphoneOff = "Micrófono silenciado",
|
portrait = "Cambiar a vertical", photo = "Foto", video = "Vídeo", microphoneOn = "Micrófono encendido", microphoneOff = "Micrófono silenciado",
|
||||||
focusHelp = "Presionar Espacio para mirar alrededor", returnHelp = "Soltar espacio para controles", lockCamera = "Bloquear movimiento de la cámara",
|
focusHelp = "Mantén pulsada la tecla de vista para mirar alrededor", returnHelp = "Suelta la tecla de vista para usar los controles", lockCamera = "Bloquear movimiento de la cámara",
|
||||||
unlockCamera = "Desbloquear movimiento de la cámara", spaceKey = "Espacio", uploading = "Subiendo {count}",
|
unlockCamera = "Desbloquear movimiento de la cámara", lookKey = "Mirar", uploading = "Subiendo {count}",
|
||||||
saving = "Guardando video...", openGallery = "Abrir fotos", takePhoto = "Tomar foto",
|
saving = "Guardando video...", openGallery = "Abrir fotos", takePhoto = "Tomar foto",
|
||||||
startRecording = "Iniciar grabación", stopRecording = "Detener grabación", saved = "Guardado en fotos.",
|
startRecording = "Iniciar grabación", stopRecording = "Detener grabación", saved = "Guardado en fotos.",
|
||||||
zoom = "Establecer zoom de la cámara en {zoom}",
|
zoom = "Establecer zoom de la cámara en {zoom}",
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ local front_camera_fov = 32.0
|
|||||||
local front_camera_distance = 1.05
|
local front_camera_distance = 1.05
|
||||||
local front_camera_height = 0.05
|
local front_camera_height = 0.05
|
||||||
local front_camera_target_height = 0.03
|
local front_camera_target_height = 0.03
|
||||||
|
local front_camera_horizontal_limit = 75.0
|
||||||
|
local front_camera_vertical_limit = 35.0
|
||||||
|
local front_camera_rotate_speed = 5.0
|
||||||
|
local camera_passthrough_control = 22 -- INPUT_JUMP (Space by default)
|
||||||
local blocked_camera_controls = {
|
local blocked_camera_controls = {
|
||||||
0, -- INPUT_NEXT_CAMERA
|
0, -- INPUT_NEXT_CAMERA
|
||||||
22, -- INPUT_JUMP
|
camera_passthrough_control,
|
||||||
24, -- INPUT_ATTACK
|
24, -- INPUT_ATTACK
|
||||||
25, -- INPUT_AIM
|
25, -- INPUT_AIM
|
||||||
37, -- INPUT_SELECT_WEAPON
|
37, -- INPUT_SELECT_WEAPON
|
||||||
@@ -45,6 +49,8 @@ local camera_state = {
|
|||||||
focus_watcher = false,
|
focus_watcher = false,
|
||||||
front_camera = false,
|
front_camera = false,
|
||||||
front_camera_handle = nil,
|
front_camera_handle = nil,
|
||||||
|
front_camera_pitch = 0.0,
|
||||||
|
front_camera_yaw = 0.0,
|
||||||
game_input = false,
|
game_input = false,
|
||||||
landscape = false,
|
landscape = false,
|
||||||
locked = false,
|
locked = false,
|
||||||
@@ -89,20 +95,39 @@ end
|
|||||||
local function get_front_camera_transform(ped)
|
local function get_front_camera_transform(ped)
|
||||||
local head_position = GetPedBoneCoords(ped, 31086, 0.0, 0.0, 0.0)
|
local head_position = GetPedBoneCoords(ped, 31086, 0.0, 0.0, 0.0)
|
||||||
local forward = GetEntityForwardVector(ped)
|
local forward = GetEntityForwardVector(ped)
|
||||||
local forward_vector = vector3(forward.x, forward.y, forward.z)
|
local yaw = math.rad(camera_state.front_camera_yaw)
|
||||||
local camera_offset = forward_vector * front_camera_distance
|
local pitch = math.rad(camera_state.front_camera_pitch)
|
||||||
local camera_position = head_position + camera_offset + vector3(0.0, 0.0, front_camera_height)
|
local orbit_direction = vector3(
|
||||||
local to_camera = camera_position - head_position
|
(forward.x * math.cos(yaw)) - (forward.y * math.sin(yaw)),
|
||||||
local dot = (to_camera.x * forward_vector.x)
|
(forward.x * math.sin(yaw)) + (forward.y * math.cos(yaw)),
|
||||||
+ (to_camera.y * forward_vector.y)
|
0.0
|
||||||
+ (to_camera.z * forward_vector.z)
|
)
|
||||||
if dot < 0.0 then
|
local camera_position = head_position
|
||||||
camera_position = head_position - camera_offset + vector3(0.0, 0.0, front_camera_height)
|
+ (orbit_direction * (math.cos(pitch) * front_camera_distance))
|
||||||
end
|
+ vector3(0.0, 0.0, front_camera_height + (math.sin(pitch) * front_camera_distance))
|
||||||
local target_position = head_position + vector3(0.0, 0.0, front_camera_target_height)
|
local target_position = head_position + vector3(0.0, 0.0, front_camera_target_height)
|
||||||
return camera_position, target_position
|
return camera_position, target_position
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function update_front_camera_orbit()
|
||||||
|
local horizontal_input = GetDisabledControlNormal(0, 1)
|
||||||
|
local vertical_input = GetDisabledControlNormal(0, 2)
|
||||||
|
camera_state.front_camera_yaw = math.max(
|
||||||
|
-front_camera_horizontal_limit,
|
||||||
|
math.min(
|
||||||
|
front_camera_horizontal_limit,
|
||||||
|
camera_state.front_camera_yaw - (horizontal_input * front_camera_rotate_speed)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
camera_state.front_camera_pitch = math.max(
|
||||||
|
-front_camera_vertical_limit,
|
||||||
|
math.min(
|
||||||
|
front_camera_vertical_limit,
|
||||||
|
camera_state.front_camera_pitch - (vertical_input * front_camera_rotate_speed)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
local function apply_front_camera(ped)
|
local function apply_front_camera(ped)
|
||||||
if not camera_state.front_camera_handle or not DoesCamExist(camera_state.front_camera_handle) then
|
if not camera_state.front_camera_handle or not DoesCamExist(camera_state.front_camera_handle) then
|
||||||
camera_state.front_camera_handle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
|
camera_state.front_camera_handle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
|
||||||
@@ -198,7 +223,18 @@ local function watch_camera_controls()
|
|||||||
CreateThread(function()
|
CreateThread(function()
|
||||||
while camera_state.active do
|
while camera_state.active do
|
||||||
apply_camera_controls()
|
apply_camera_controls()
|
||||||
|
if not camera_state.walkable then
|
||||||
|
local passthrough_pressed = SkyPhoneFocus.IsHoldToLookPressed()
|
||||||
|
or IsDisabledControlPressed(0, camera_passthrough_control)
|
||||||
|
local should_focus = camera_state.locked or not passthrough_pressed
|
||||||
|
if camera_state.nui_focused ~= should_focus then
|
||||||
|
set_camera_focus(should_focus)
|
||||||
|
end
|
||||||
|
end
|
||||||
if camera_state.game_input then
|
if camera_state.game_input then
|
||||||
|
if camera_state.front_camera and not camera_state.locked then
|
||||||
|
update_front_camera_orbit()
|
||||||
|
end
|
||||||
if
|
if
|
||||||
IsDisabledControlJustPressed(0, 241)
|
IsDisabledControlJustPressed(0, 241)
|
||||||
or IsDisabledControlJustPressed(0, 261)
|
or IsDisabledControlJustPressed(0, 261)
|
||||||
@@ -214,9 +250,6 @@ local function watch_camera_controls()
|
|||||||
math.max(minimum_zoom, camera_state.zoom - mouse_wheel_zoom_step)
|
math.max(minimum_zoom, camera_state.zoom - mouse_wheel_zoom_step)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
if IsDisabledControlJustReleased(0, 22) then
|
|
||||||
set_camera_focus(true)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
Wait(0)
|
Wait(0)
|
||||||
end
|
end
|
||||||
@@ -243,7 +276,7 @@ AddEventHandler("sky_phone:client:cameraFocusApplied", function(data)
|
|||||||
camera_state.applied_nui_focus = data.focused
|
camera_state.applied_nui_focus = data.focused
|
||||||
SendNUIMessage({ type = "camera:focus", data = { focused = data.focused } })
|
SendNUIMessage({ type = "camera:focus", data = { focused = data.focused } })
|
||||||
end
|
end
|
||||||
if data.active and data.gameInput then
|
if data.active then
|
||||||
watch_camera_controls()
|
watch_camera_controls()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -256,6 +289,8 @@ local function set_camera_active(active)
|
|||||||
TriggerEvent("sky_phone:client:cameraActiveChanged", active)
|
TriggerEvent("sky_phone:client:cameraActiveChanged", active)
|
||||||
if active then
|
if active then
|
||||||
camera_state.front_camera = false
|
camera_state.front_camera = false
|
||||||
|
camera_state.front_camera_pitch = 0.0
|
||||||
|
camera_state.front_camera_yaw = 0.0
|
||||||
camera_state.landscape = false
|
camera_state.landscape = false
|
||||||
camera_state.locked = false
|
camera_state.locked = false
|
||||||
camera_state.zoom = 1.0
|
camera_state.zoom = 1.0
|
||||||
@@ -290,6 +325,8 @@ local function set_camera_active(active)
|
|||||||
end
|
end
|
||||||
set_flash_enabled(false)
|
set_flash_enabled(false)
|
||||||
camera_state.front_camera = false
|
camera_state.front_camera = false
|
||||||
|
camera_state.front_camera_pitch = 0.0
|
||||||
|
camera_state.front_camera_yaw = 0.0
|
||||||
camera_state.game_input = false
|
camera_state.game_input = false
|
||||||
camera_state.landscape = false
|
camera_state.landscape = false
|
||||||
camera_state.locked = false
|
camera_state.locked = false
|
||||||
@@ -314,6 +351,8 @@ local function set_front_camera(active)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if active then
|
if active then
|
||||||
|
camera_state.front_camera_pitch = 0.0
|
||||||
|
camera_state.front_camera_yaw = 0.0
|
||||||
apply_front_camera(PlayerPedId())
|
apply_front_camera(PlayerPedId())
|
||||||
else
|
else
|
||||||
clear_front_camera()
|
clear_front_camera()
|
||||||
@@ -405,6 +444,9 @@ RegisterNUICallback("camera:setLocked", function(data, cb)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
camera_state.locked = data.locked == true
|
camera_state.locked = data.locked == true
|
||||||
|
if camera_state.locked and not camera_state.walkable then
|
||||||
|
set_camera_focus(true)
|
||||||
|
end
|
||||||
cb({ success = true })
|
cb({ success = true })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ AddEventHandler("sky_phone:configurator:updated", function()
|
|||||||
SkyPhoneFocus.Reapply()
|
SkyPhoneFocus.Reapply()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
function SkyPhoneFocus.IsHoldToLookPressed()
|
||||||
|
if not hold_to_look_enabled then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if IsControlPressed(0, hold_to_look_control) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return IsDisabledControlPressed(0, hold_to_look_control)
|
||||||
|
end
|
||||||
|
|
||||||
function SkyPhoneFocus.ApplyFocusedControls()
|
function SkyPhoneFocus.ApplyFocusedControls()
|
||||||
for _, group in ipairs(focused_control_groups) do
|
for _, group in ipairs(focused_control_groups) do
|
||||||
DisableAllControlActions(group)
|
DisableAllControlActions(group)
|
||||||
@@ -96,6 +106,10 @@ function SkyPhoneFocus.Resolve(state)
|
|||||||
if state.camera_active and not state.camera_nui_focused then
|
if state.camera_active and not state.camera_nui_focused then
|
||||||
return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true }
|
return { block_game = false, block_look = false, cursor = false, focused = true, game_input = true, keep_input = true }
|
||||||
end
|
end
|
||||||
|
if state.camera_active then
|
||||||
|
-- Forward controls so disabled inputs remain readable while the NUI cursor owns focus.
|
||||||
|
return { block_game = true, block_look = true, cursor = true, focused = true, game_input = false, keep_input = true }
|
||||||
|
end
|
||||||
local game_input = state.is_open
|
local game_input = state.is_open
|
||||||
and allows_game_input(state)
|
and allows_game_input(state)
|
||||||
and not state.camera_active
|
and not state.camera_active
|
||||||
@@ -137,7 +151,7 @@ function SkyPhoneFocus.Reapply()
|
|||||||
active = state.camera_active,
|
active = state.camera_active,
|
||||||
cursor = focus.cursor,
|
cursor = focus.cursor,
|
||||||
focused = focus.focused,
|
focused = focus.focused,
|
||||||
gameInput = focus.keep_input,
|
gameInput = focus.game_input,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -234,10 +248,9 @@ end
|
|||||||
CreateThread(function()
|
CreateThread(function()
|
||||||
while true do
|
while true do
|
||||||
if game_input or block_game then
|
if game_input or block_game then
|
||||||
local look_passthrough = hold_to_look_enabled
|
local look_passthrough = game_input
|
||||||
and game_input
|
|
||||||
and not state.cursor_disabled
|
and not state.cursor_disabled
|
||||||
and IsControlPressed(0, hold_to_look_control)
|
and SkyPhoneFocus.IsHoldToLookPressed()
|
||||||
if look_passthrough ~= state.look_passthrough then
|
if look_passthrough ~= state.look_passthrough then
|
||||||
state.look_passthrough = look_passthrough
|
state.look_passthrough = look_passthrough
|
||||||
SkyPhoneFocus.Reapply()
|
SkyPhoneFocus.Reapply()
|
||||||
|
|||||||
@@ -26,10 +26,16 @@ local FIXED_CONFIG_PATHS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if configurator_enabled then
|
if configurator_enabled then
|
||||||
print(
|
local border = "======================================================================"
|
||||||
"[sky_phone] Phone Configurator enabled: file-based settings from config.lua " ..
|
print(([[
|
||||||
"(except Config.CommandPermissions) and media.lua are disabled; SQL configuration is active."
|
^1%s^0
|
||||||
)
|
^1 SKY PHONE CONFIGURATION FILES ARE DISABLED ^0
|
||||||
|
^1%s^0
|
||||||
|
^1 The Phone Configurator is ENABLED.^0
|
||||||
|
^1 Runtime settings from config.lua and media.lua are DISABLED.^0
|
||||||
|
^1 Configure all phone and media settings IN GAME through /phonepanel.^0
|
||||||
|
^1 Only Config.PhoneConfigurator.Enabled and Config.CommandPermissions remain file-based.^0
|
||||||
|
^1%s^0]]):format(border, border, border))
|
||||||
end
|
end
|
||||||
|
|
||||||
local CLIENT_CONFIG_KEYS = {
|
local CLIENT_CONFIG_KEYS = {
|
||||||
|
|||||||
+63
-5
@@ -4,6 +4,20 @@ local camera_target = nil
|
|||||||
local camera_created = false
|
local camera_created = false
|
||||||
local camera_destroyed = false
|
local camera_destroyed = false
|
||||||
local scripted_camera_rendering = 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 = {}
|
local vector_meta = {}
|
||||||
vector_meta.__index = vector_meta
|
vector_meta.__index = vector_meta
|
||||||
@@ -32,12 +46,20 @@ function RegisterNUICallback(name, callback)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RegisterNetEvent() end
|
function RegisterNetEvent() end
|
||||||
function AddEventHandler() end
|
function AddEventHandler(name, callback)
|
||||||
function TriggerEvent() end
|
event_handlers[name] = callback
|
||||||
|
end
|
||||||
|
function TriggerEvent(name, data)
|
||||||
|
triggered_events[#triggered_events + 1] = { name = name, data = data }
|
||||||
|
end
|
||||||
function TriggerServerEvent() end
|
function TriggerServerEvent() end
|
||||||
function SendNUIMessage() end
|
function SendNUIMessage() end
|
||||||
function CreateThread() end
|
function CreateThread(callback)
|
||||||
function Wait() end
|
threads[#threads + 1] = callback
|
||||||
|
end
|
||||||
|
function Wait()
|
||||||
|
error(thread_stop, 0)
|
||||||
|
end
|
||||||
function PlayerPedId() return 7 end
|
function PlayerPedId() return 7 end
|
||||||
function PlayerId() return 8 end
|
function PlayerId() return 8 end
|
||||||
function GetFollowPedCamViewMode() return 1 end
|
function GetFollowPedCamViewMode() return 1 end
|
||||||
@@ -47,10 +69,19 @@ function DisplayRadar() end
|
|||||||
function SetFollowPedCamViewMode() end
|
function SetFollowPedCamViewMode() end
|
||||||
function SetFollowVehicleCamViewMode() end
|
function SetFollowVehicleCamViewMode() end
|
||||||
function IsPedInAnyVehicle() return false end
|
function IsPedInAnyVehicle() return false end
|
||||||
function DisableControlAction() 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 DisablePlayerFiring() end
|
||||||
function HideHudAndRadarThisFrame() end
|
function HideHudAndRadarThisFrame() end
|
||||||
function GetCurrentResourceName() return "sky_phone" 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()
|
function GetEntityCoords()
|
||||||
return vector3(10.0, 20.0, 1.0)
|
return vector3(10.0, 20.0, 1.0)
|
||||||
@@ -120,6 +151,33 @@ assert(close_enough(camera_target.x, 10.0))
|
|||||||
assert(close_enough(camera_target.y, 20.0))
|
assert(close_enough(camera_target.y, 20.0))
|
||||||
assert(close_enough(camera_target.z, 2.73))
|
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(response_from("camera:setFacing", { front = false }).success)
|
||||||
assert(camera_destroyed and not scripted_camera_rendering, "rear mode must release the selfie camera")
|
assert(camera_destroyed and not scripted_camera_rendering, "rear mode must release the selfie camera")
|
||||||
|
|
||||||
|
|||||||
+57
-3
@@ -5,6 +5,9 @@ local event_handlers = {}
|
|||||||
local nui_callbacks = {}
|
local nui_callbacks = {}
|
||||||
local nui_focus = nil
|
local nui_focus = nil
|
||||||
local nui_keep_input = nil
|
local nui_keep_input = nil
|
||||||
|
local pressed_controls = {}
|
||||||
|
local disabled_pressed_controls = {}
|
||||||
|
local triggered_events = {}
|
||||||
|
|
||||||
Config = {
|
Config = {
|
||||||
Phone = {
|
Phone = {
|
||||||
@@ -34,7 +37,19 @@ function SetNuiFocusKeepInput(keep_input)
|
|||||||
nui_keep_input = keep_input
|
nui_keep_input = keep_input
|
||||||
end
|
end
|
||||||
|
|
||||||
function TriggerEvent() 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)
|
function DisableControlAction(group, control, disabled)
|
||||||
assert(group == 0 and disabled, "phone controls must be disabled in the primary input group")
|
assert(group == 0 and disabled, "phone controls must be disabled in the primary input group")
|
||||||
@@ -56,6 +71,25 @@ end
|
|||||||
|
|
||||||
dofile("sky_phone/source/client/focus.lua")
|
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 function resolve(overrides)
|
||||||
local state = {
|
local state = {
|
||||||
activity_suspended = false,
|
activity_suspended = false,
|
||||||
@@ -233,6 +267,9 @@ disabled_controls = {}
|
|||||||
firing_disabled = false
|
firing_disabled = false
|
||||||
SkyPhoneFocus.ApplyGameInputControls(false)
|
SkyPhoneFocus.ApplyGameInputControls(false)
|
||||||
assert(not disabled_controls[1] and not disabled_controls[2], "camera passthrough must preserve camera look")
|
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")
|
assert(firing_disabled, "player attacks must remain disabled during camera passthrough")
|
||||||
|
|
||||||
local movable_notification = resolve({ allow_movement = true, notification_focus = true })
|
local movable_notification = resolve({ allow_movement = true, notification_focus = true })
|
||||||
@@ -264,11 +301,28 @@ local focused_camera = resolve({
|
|||||||
assert(
|
assert(
|
||||||
focused_camera.cursor
|
focused_camera.cursor
|
||||||
and focused_camera.focused
|
and focused_camera.focused
|
||||||
and not focused_camera.keep_input
|
and focused_camera.keep_input
|
||||||
and not focused_camera.game_input,
|
and not focused_camera.game_input,
|
||||||
"focused camera must override movement configuration until Space enables passthrough"
|
"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({
|
local camera_interrupted_by_call = resolve({
|
||||||
call_focus = true,
|
call_focus = true,
|
||||||
camera_active = true,
|
camera_active = true,
|
||||||
|
|||||||
Reference in New Issue
Block a user