From ea959f59f25d9e2efda5d06995990ed5aad5a392 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" <159480018+leonw21342315@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:50:25 +0200 Subject: [PATCH] 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 --- frontend/src/permissions.contract.test.ts | 10 ++- frontend/src/stores/phone.ts | 4 +- .../src/views/apps/CameraApp.contract.test.ts | 43 ++++++++--- frontend/src/views/apps/CameraApp.vue | 35 +-------- sky_phone/config/locales/de.lua | 4 +- sky_phone/config/locales/en.lua | 4 +- sky_phone/config/locales/es.lua | 4 +- sky_phone/source/client/camera.lua | 72 +++++++++++++++---- sky_phone/source/client/focus.lua | 21 ++++-- .../source/server/phone_configurator.lua | 14 ++-- tests/client_camera.lua | 68 ++++++++++++++++-- tests/client_focus.lua | 60 +++++++++++++++- 12 files changed, 253 insertions(+), 86 deletions(-) diff --git a/frontend/src/permissions.contract.test.ts b/frontend/src/permissions.contract.test.ts index f8daf4a..bf60bb5 100644 --- a/frontend/src/permissions.contract.test.ts +++ b/frontend/src/permissions.contract.test.ts @@ -46,10 +46,16 @@ describe('fixed server permissions', () => { expect(configurator).toContain(`["${path}"] = true`) } expect(configurator).toContain( - 'Phone Configurator enabled: file-based settings from config.lua', + 'SKY PHONE CONFIGURATION FILES ARE DISABLED', ) 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', ) }) diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index e15996b..6def827 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -4523,8 +4523,8 @@ const defaultLocales: LocaleTree = { video: 'Video', microphoneOn: 'Microphone on', microphoneOff: 'Microphone muted', - focusHelp: 'Space for movement', - returnHelp: 'Space to return', + focusHelp: 'Hold the look key to look around', + returnHelp: 'Release the look key for controls', uploading: '{count} uploading', saving: 'Saving video...', openGallery: 'Open Photos', diff --git a/frontend/src/views/apps/CameraApp.contract.test.ts b/frontend/src/views/apps/CameraApp.contract.test.ts index 6dc41e2..db5bc62 100644 --- a/frontend/src/views/apps/CameraApp.contract.test.ts +++ b/frontend/src/views/apps/CameraApp.contract.test.ts @@ -77,7 +77,8 @@ describe('Camera app controls', () => { it('locks look controls without changing the global gameplay camera', () => { expect(cameraView).toContain("nuiCall('camera:setLocked'") 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('INPUT_LOOK_LR') expect(cameraClient).toContain('INPUT_LOOK_UD') @@ -95,10 +96,26 @@ describe('Camera app controls', () => { expect(cameraClient).not.toContain('ensure_ultrawide_camera') }) - it('keeps the selfie camera stable while Space still allows movement', () => { - expect(cameraView).toMatch( - /event\.code !== 'Space'[\s\S]*cameraLocked\.value/, + it('uses the configured HoldToLook control and Space in camera modes', () => { + expect(cameraView).not.toContain("event.code !== 'Space'") + 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( '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_fov = 32.0') 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 dot = (to_camera.x * forward_vector.x)', - ) + expect(cameraClient).toContain('GetDisabledControlNormal(0, 1)') + 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(focusClient).toContain( '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('PointCamAtCoord(') expect(cameraClient).not.toContain('SetCamRot(') @@ -121,10 +146,6 @@ describe('Camera app controls', () => { expect(cameraClient).not.toContain('front_camera_position') expect(cameraClient).not.toContain('AttachCamToEntity(') 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', () => { diff --git a/frontend/src/views/apps/CameraApp.vue b/frontend/src/views/apps/CameraApp.vue index d260274..4823e35 100644 --- a/frontend/src/views/apps/CameraApp.vue +++ b/frontend/src/views/apps/CameraApp.vue @@ -56,7 +56,6 @@ const microphoneEnabled = ref(true) const frontCamera = ref(false) const shutterActive = ref(false) const cameraLocked = ref(false) -const movementEnabled = ref(false) const recording = ref(false) const savingVideo = ref(false) const recordingStartedAt = ref(0) @@ -238,10 +237,6 @@ async function toggleFacing(): Promise { async function toggleCameraLock(): Promise { 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 }) } @@ -340,26 +335,6 @@ function updateRecordingTimer(): void { 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 { if (!isTrustedRootMessageSource(event.source, window)) return const message = event.data as { @@ -442,8 +417,6 @@ onMounted(() => { { data: { zoom: selectedZoom.value }, type: 'camera:zoom' }, '*', ) - window.addEventListener('keydown', onKeydown) - window.addEventListener('keyup', onKeyup) window.addEventListener('message', onMessage) void nuiCall('camera:setActive', { active: true }) void nuiCall('media:config').then((response) => { @@ -459,13 +432,7 @@ onBeforeUnmount(() => { if (shutterTimer !== undefined) window.clearTimeout(shutterTimer) if (noticeTimer !== undefined) window.clearTimeout(noticeTimer) if (recordingTimer !== undefined) window.clearInterval(recordingTimer) - window.removeEventListener('keydown', onKeydown) - window.removeEventListener('keyup', onKeyup) window.removeEventListener('message', onMessage) - if (movementEnabled.value) { - movementEnabled.value = false - void nuiCall('camera:setFocus', { focused: true }) - } if (renderFrameId !== undefined) window.cancelAnimationFrame(renderFrameId) resizeObserver?.disconnect() gameView?.dispose() @@ -587,7 +554,7 @@ onBeforeUnmount(() => { > - {{ phone.t('Apps.camera.spaceKey') }} + {{ phone.t('Apps.camera.lookKey') }}