mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
CLN - remove weather camera views
Remove the remote weather-camera experience from the phone UI, NUI test server, and weather state.\n\nDelete the associated client and server camera handlers, configuration, locale copy, and manifest entries while retaining the core weather forecast app.
This commit is contained in:
@@ -3515,119 +3515,6 @@ button {
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.weather-camera-card {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.weather-camera-preview {
|
||||
position: relative;
|
||||
min-height: 146px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 75% 20%, #bfe8ff9c, transparent 28%),
|
||||
linear-gradient(160deg, #82b4d5 0%, #496783 52%, #27364c 100%);
|
||||
}
|
||||
.weather-camera-preview--rain,
|
||||
.weather-camera-preview--thunder {
|
||||
background:
|
||||
radial-gradient(circle at 70% 18%, #9cb4c380, transparent 26%),
|
||||
linear-gradient(160deg, #667d8d 0%, #34495f 50%, #1a2839 100%);
|
||||
}
|
||||
.weather-camera-preview--clear,
|
||||
.weather-camera-preview--sunny {
|
||||
background:
|
||||
radial-gradient(circle at 76% 19%, #fff4c9 0 5%, #ffd78370 6%, transparent 29%),
|
||||
linear-gradient(160deg, #72b7df 0%, #4d86b2 55%, #35506e 100%);
|
||||
}
|
||||
.weather-camera-preview::after {
|
||||
position: absolute;
|
||||
inset: -50% 0 auto;
|
||||
height: 3px;
|
||||
background: #ffffff38;
|
||||
box-shadow: 0 0 12px #fff8;
|
||||
content: '';
|
||||
animation: weather-camera-scan 4s linear infinite;
|
||||
}
|
||||
.weather-camera-skyline {
|
||||
position: absolute;
|
||||
right: -5%;
|
||||
bottom: 0;
|
||||
left: -5%;
|
||||
height: 51%;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0 5%, #142333 5% 17%, transparent 17% 21%, #192b3c 21% 34%, transparent 34% 42%, #11202f 42% 60%, transparent 60% 65%, #182a3b 65% 76%, transparent 76% 81%, #102131 81% 95%, transparent 95%),
|
||||
linear-gradient(0deg, #111c2a 0 18%, transparent 18%);
|
||||
clip-path: polygon(0 43%, 9% 43%, 9% 22%, 18% 22%, 18% 57%, 27% 57%, 27% 10%, 37% 10%, 37% 48%, 48% 48%, 48% 30%, 61% 30%, 61% 62%, 71% 62%, 71% 16%, 83% 16%, 83% 49%, 100% 49%, 100% 100%, 0 100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.weather-camera-meta,
|
||||
.weather-camera-copy {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-shadow: 0 2px 8px #000c;
|
||||
}
|
||||
.weather-camera-meta {
|
||||
top: 11px;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.weather-camera-live {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.weather-camera-live i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #ff4e5b;
|
||||
box-shadow: 0 0 0 4px #ff4e5b2e;
|
||||
animation: weather-camera-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.weather-camera-copy {
|
||||
bottom: 10px;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
}
|
||||
.weather-camera-copy strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
.weather-camera-copy span {
|
||||
color: #ffffffc4;
|
||||
font-size: 10px;
|
||||
}
|
||||
.weather-camera-action {
|
||||
padding: 11px 12px 0;
|
||||
}
|
||||
.weather-camera-action button span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
}
|
||||
.weather-camera-action p {
|
||||
margin: 8px 3px 0;
|
||||
color: #ffd2d2;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.weather-camera-unavailable {
|
||||
padding: 16px;
|
||||
color: #ffffffb3;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
@keyframes weather-camera-scan {
|
||||
to { transform: translateY(290px); }
|
||||
}
|
||||
@keyframes weather-camera-pulse {
|
||||
50% { opacity: 0.45; }
|
||||
}
|
||||
.weather-hourly {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
|
||||
@@ -2338,28 +2338,6 @@ const defaultLocales: LocaleTree = {
|
||||
unavailable: 'Weather is unavailable',
|
||||
stale: 'Showing the last available weather update.',
|
||||
tryAgain: 'Try Again',
|
||||
webcam: {
|
||||
title: 'Weather Cameras',
|
||||
live: 'LIVE',
|
||||
view: 'View Live Camera',
|
||||
subtitle: 'A live look at current conditions',
|
||||
unavailable: 'No weather cameras are available.',
|
||||
locations: {
|
||||
legion_square: 'Legion Square',
|
||||
sandy_shores: 'Sandy Shores',
|
||||
cayo_airstrip: 'Cayo Perico Airstrip',
|
||||
},
|
||||
errors: {
|
||||
camera_disabled: 'Weather cameras are currently disabled.',
|
||||
camera_not_found: 'This weather camera is no longer available.',
|
||||
camera_requires_safe_position:
|
||||
'Stand still and exit your vehicle before opening a camera.',
|
||||
camera_active: 'A weather camera is already open.',
|
||||
rate_limited: 'Too many camera requests. Try again shortly.',
|
||||
request_failed: 'The weather camera could not be opened.',
|
||||
not_authenticated: 'Unlock the phone before opening a camera.',
|
||||
},
|
||||
},
|
||||
regions: {
|
||||
los_santos: 'Los Santos',
|
||||
blaine_county: 'Blaine County',
|
||||
|
||||
@@ -61,24 +61,4 @@ describe('weather store', () => {
|
||||
expect(weather.error).toBe('offline')
|
||||
})
|
||||
|
||||
it('loads the server-approved camera list and opens a camera by id', async () => {
|
||||
vi.mocked(nuiCall)
|
||||
.mockResolvedValueOnce({
|
||||
data: { cameras: [{ id: 'legion_square', region: 'los_santos' }] },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
const weather = useWeatherStore()
|
||||
|
||||
await weather.loadCameras()
|
||||
const opened = await weather.openCamera('legion_square')
|
||||
|
||||
expect(weather.cameras).toEqual([
|
||||
{ id: 'legion_square', region: 'los_santos' },
|
||||
])
|
||||
expect(nuiCall).toHaveBeenLastCalledWith('weather:camera-open', {
|
||||
id: 'legion_square',
|
||||
})
|
||||
expect(opened).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import type {
|
||||
RawWeatherSnapshot,
|
||||
WeatherCamera,
|
||||
WeatherForecast,
|
||||
} from '@/types/weather'
|
||||
import type { RawWeatherSnapshot, WeatherForecast } from '@/types/weather'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
import { buildWeatherForecast } from '@/utils/weather'
|
||||
|
||||
@@ -13,45 +9,12 @@ const REFRESH_INTERVAL = 30_000
|
||||
export const useWeatherStore = defineStore('weather', {
|
||||
state: () => ({
|
||||
error: null as string | null,
|
||||
cameraError: null as string | null,
|
||||
cameras: [] as WeatherCamera[],
|
||||
camerasLoaded: false,
|
||||
forecast: null as WeatherForecast | null,
|
||||
intervalId: undefined as number | undefined,
|
||||
isLoading: false,
|
||||
isOpeningCamera: false,
|
||||
lastFetchedAt: 0,
|
||||
}),
|
||||
actions: {
|
||||
async loadCameras(force = false): Promise<void> {
|
||||
if (this.camerasLoaded && !force) return
|
||||
const response = await nuiCall<{ cameras: WeatherCamera[] }>(
|
||||
'weather:cameras',
|
||||
)
|
||||
this.camerasLoaded = true
|
||||
if (!response.success || !Array.isArray(response.data?.cameras)) {
|
||||
this.cameraError = response.error ?? 'request_failed'
|
||||
return
|
||||
}
|
||||
this.cameras = response.data.cameras.filter(
|
||||
(camera) =>
|
||||
typeof camera?.id === 'string' &&
|
||||
['los_santos', 'blaine_county', 'cayo_perico'].includes(
|
||||
camera.region,
|
||||
),
|
||||
)
|
||||
this.cameraError = null
|
||||
},
|
||||
async openCamera(id: string): Promise<boolean> {
|
||||
if (this.isOpeningCamera) return false
|
||||
this.isOpeningCamera = true
|
||||
const response = await nuiCall('weather:camera-open', { id })
|
||||
this.isOpeningCamera = false
|
||||
this.cameraError = response.success
|
||||
? null
|
||||
: (response.error ?? 'request_failed')
|
||||
return response.success
|
||||
},
|
||||
async refresh(force = false): Promise<void> {
|
||||
if (this.isLoading) return
|
||||
if (!force && Date.now() - this.lastFetchedAt < REFRESH_INTERVAL) return
|
||||
@@ -68,7 +31,6 @@ export const useWeatherStore = defineStore('weather', {
|
||||
},
|
||||
start(): void {
|
||||
void this.refresh(true)
|
||||
void this.loadCameras()
|
||||
if (this.intervalId !== undefined) return
|
||||
this.intervalId = window.setInterval(() => void this.refresh(true), REFRESH_INTERVAL)
|
||||
},
|
||||
|
||||
@@ -10,11 +10,6 @@ export type WeatherConditionId =
|
||||
|
||||
export type WeatherRegionId = 'los_santos' | 'blaine_county' | 'cayo_perico'
|
||||
|
||||
export type WeatherCamera = {
|
||||
id: string
|
||||
region: WeatherRegionId
|
||||
}
|
||||
|
||||
export type WeatherClock = {
|
||||
day: number
|
||||
hour: number
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { kButton, kCard, kLink, kNavbar, kPage, kPreloader } from 'konsta/vue'
|
||||
import { kCard, kLink, kNavbar, kPage, kPreloader } from 'konsta/vue'
|
||||
import {
|
||||
CloudSun,
|
||||
Droplets,
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
RefreshCw,
|
||||
ThermometerSun,
|
||||
Umbrella,
|
||||
Video,
|
||||
Wind,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
@@ -21,11 +20,6 @@ import type { WeatherConditionId } from '@/types/weather'
|
||||
const phone = usePhoneStore()
|
||||
const weather = useWeatherStore()
|
||||
const forecast = computed(() => weather.forecast)
|
||||
const selectedCamera = computed(
|
||||
() =>
|
||||
weather.cameras.find((camera) => camera.region === forecast.value?.region) ??
|
||||
weather.cameras[0],
|
||||
)
|
||||
const rainy = computed(
|
||||
() =>
|
||||
forecast.value?.condition === 'rain' ||
|
||||
@@ -71,11 +65,6 @@ function formatHour(timestamp: number, index: number): string {
|
||||
}).format(timestamp)
|
||||
}
|
||||
|
||||
function cameraErrorLabel(): string {
|
||||
return phone.t(
|
||||
`Apps.weather.webcam.errors.${weather.cameraError ?? 'request_failed'}`,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -168,46 +157,6 @@ function cameraErrorLabel(): string {
|
||||
</k-card>
|
||||
</section>
|
||||
|
||||
<k-card
|
||||
v-if="selectedCamera"
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-panel weather-camera-card"
|
||||
>
|
||||
<div class="weather-camera-preview" :class="`weather-camera-preview--${forecast.condition}`">
|
||||
<div class="weather-camera-skyline" aria-hidden="true"></div>
|
||||
<div class="weather-camera-meta">
|
||||
<span class="weather-camera-live"><i></i>{{ phone.t('Apps.weather.webcam.live') }}</span>
|
||||
<span>{{ phone.t(`Apps.weather.webcam.locations.${selectedCamera.id}`) }}</span>
|
||||
</div>
|
||||
<div class="weather-camera-copy">
|
||||
<strong>{{ phone.t('Apps.weather.webcam.title') }}</strong>
|
||||
<span>{{ phone.t('Apps.weather.webcam.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weather-camera-action">
|
||||
<k-button
|
||||
large
|
||||
rounded
|
||||
:disabled="weather.isOpeningCamera"
|
||||
@click="weather.openCamera(selectedCamera.id)"
|
||||
>
|
||||
<Video :size="17" />
|
||||
{{ phone.t('Apps.weather.webcam.view') }}
|
||||
</k-button>
|
||||
<p v-if="weather.cameraError">{{ cameraErrorLabel() }}</p>
|
||||
</div>
|
||||
</k-card>
|
||||
|
||||
<k-card
|
||||
v-else-if="weather.camerasLoaded"
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-panel weather-camera-unavailable"
|
||||
>
|
||||
{{ phone.t('Apps.weather.webcam.unavailable') }}
|
||||
</k-card>
|
||||
|
||||
<k-card
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
|
||||
@@ -4779,28 +4779,6 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'weather:cameras') {
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
cameras: [
|
||||
{ id: 'legion_square', region: 'los_santos' },
|
||||
{ id: 'sandy_shores', region: 'blaine_county' },
|
||||
{ id: 'cayo_airstrip', region: 'cayo_perico' },
|
||||
],
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'weather:camera-open') {
|
||||
const ids = ['legion_square', 'sandy_shores', 'cayo_airstrip']
|
||||
response.json(
|
||||
ids.includes(request.body?.id)
|
||||
? { success: true }
|
||||
: { success: false, error: 'camera_not_found' },
|
||||
)
|
||||
return
|
||||
}
|
||||
if (endpoint === 'map:getPlayerCoords') {
|
||||
response.json({
|
||||
success: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
Locales["en"] = {
|
||||
CommandDescription = "Open your phone.",
|
||||
WeatherCameraExit = "Weather camera | Press BACKSPACE to return",
|
||||
FlipTokCommand = {
|
||||
usage = "Usage: /{command} <@handle> [on|off]",
|
||||
noPermission = "You do not have permission to manage FlipTok verification.",
|
||||
@@ -948,17 +947,6 @@ Locales["en"] = {
|
||||
feelsLike = "Feels Like", wind = "Wind", humidity = "Humidity", rain = "Precipitation",
|
||||
hourly = "Next Hours", unavailable = "Weather is unavailable",
|
||||
stale = "Showing the last available weather update.", tryAgain = "Try Again",
|
||||
webcam = {
|
||||
title = "Weather Cameras", live = "LIVE", view = "View Live Camera",
|
||||
subtitle = "A live look at current conditions", unavailable = "No weather cameras are available.",
|
||||
locations = { legion_square = "Legion Square", sandy_shores = "Sandy Shores", cayo_airstrip = "Cayo Perico Airstrip" },
|
||||
errors = {
|
||||
camera_disabled = "Weather cameras are currently disabled.", camera_not_found = "This weather camera is no longer available.",
|
||||
camera_requires_safe_position = "Stand still and exit your vehicle before opening a camera.",
|
||||
camera_active = "A weather camera is already open.", rate_limited = "Too many camera requests. Try again shortly.",
|
||||
request_failed = "The weather camera could not be opened.", not_authenticated = "Unlock the phone before opening a camera.",
|
||||
},
|
||||
},
|
||||
regions = { los_santos = "Los Santos", blaine_county = "Blaine County", cayo_perico = "Cayo Perico" },
|
||||
conditions = {
|
||||
sunny = "Sunny", clear = "Clear", partly_cloudy = "Partly Cloudy", cloudy = "Cloudy",
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
Config.WeatherCameras = {
|
||||
Enabled = true,
|
||||
RequestsPerMinute = 10,
|
||||
SessionSeconds = 30,
|
||||
UseStreamingFocus = true,
|
||||
Cameras = {
|
||||
{
|
||||
Id = "legion_square",
|
||||
Region = "los_santos",
|
||||
Coords = { x = -236.72, y = -1001.42, z = 45.20 },
|
||||
Rotation = { x = -16.0, y = 0.0, z = -28.0 },
|
||||
FieldOfView = 52.0,
|
||||
},
|
||||
{
|
||||
Id = "sandy_shores",
|
||||
Region = "blaine_county",
|
||||
Coords = { x = 1712.64, y = 3654.86, z = 47.25 },
|
||||
Rotation = { x = -13.0, y = 0.0, z = 142.0 },
|
||||
FieldOfView = 55.0,
|
||||
},
|
||||
{
|
||||
Id = "cayo_airstrip",
|
||||
Region = "cayo_perico",
|
||||
Coords = { x = 4437.38, y = -4471.62, z = 24.42 },
|
||||
Rotation = { x = -10.0, y = 0.0, z = 45.0 },
|
||||
FieldOfView = 58.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- Remote cameras never move the player or alter spectator/entity state. Streaming focus is
|
||||
-- local and is cleared when the camera closes. Disable it if an anti-cheat policy forbids it.
|
||||
@@ -32,7 +32,6 @@ client_scripts {
|
||||
'source/client/skyride.lua',
|
||||
'source/client/housing.lua',
|
||||
'source/client/crewlink.lua',
|
||||
'source/client/weather_camera.lua',
|
||||
'source/bridge/client/radio.lua',
|
||||
'source/client/payphones.lua',
|
||||
'source/client/main.lua',
|
||||
@@ -42,7 +41,6 @@ client_scripts {
|
||||
server_scripts {
|
||||
'@oxmysql/lib/MySQL.lua',
|
||||
'config/config.lua',
|
||||
'config/weather_cameras.lua',
|
||||
'config/media.lua',
|
||||
'config/music.lua',
|
||||
'config/locales/*.lua',
|
||||
@@ -57,7 +55,6 @@ server_scripts {
|
||||
'source/bridge/server/inventory/*.lua',
|
||||
'source/server/db_migrate.lua',
|
||||
'source/server/phone.lua',
|
||||
'source/server/weather.lua',
|
||||
'source/server/sim.lua',
|
||||
'source/server/calls.lua',
|
||||
'source/server/media.lua',
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
local active_camera = nil
|
||||
local focus_active = false
|
||||
local camera_session = 0
|
||||
local transition_ms = 350
|
||||
|
||||
local function stop_weather_camera(resume_phone)
|
||||
camera_session = camera_session + 1
|
||||
if active_camera and DoesCamExist(active_camera) then
|
||||
RenderScriptCams(false, true, transition_ms, true, false)
|
||||
DestroyCam(active_camera, false)
|
||||
end
|
||||
active_camera = nil
|
||||
if focus_active then ClearFocus() end
|
||||
focus_active = false
|
||||
if resume_phone then
|
||||
SetNuiFocus(true, true)
|
||||
SendNUIMessage({ type = "app:resume" })
|
||||
end
|
||||
end
|
||||
|
||||
local function valid_vector(value)
|
||||
return type(value) == "table"
|
||||
and type(value.x) == "number" and value.x == value.x
|
||||
and type(value.y) == "number" and value.y == value.y
|
||||
and type(value.z) == "number" and value.z == value.z
|
||||
end
|
||||
|
||||
local function start_weather_camera(camera)
|
||||
if type(camera) ~= "table" or not valid_vector(camera.coords) or not valid_vector(camera.rotation) then
|
||||
return false
|
||||
end
|
||||
local fov = tonumber(camera.fieldOfView)
|
||||
local duration = tonumber(camera.sessionSeconds)
|
||||
if not fov or fov < 20.0 or fov > 90.0 or not duration then return false end
|
||||
|
||||
camera_session = camera_session + 1
|
||||
local session = camera_session
|
||||
SetNuiFocus(false, false)
|
||||
SendNUIMessage({ type = "app:suspend" })
|
||||
active_camera = CreateCamWithParams(
|
||||
"DEFAULT_SCRIPTED_CAMERA",
|
||||
camera.coords.x, camera.coords.y, camera.coords.z,
|
||||
camera.rotation.x, camera.rotation.y, camera.rotation.z,
|
||||
fov, true, 2
|
||||
)
|
||||
if not active_camera or not DoesCamExist(active_camera) then
|
||||
stop_weather_camera(true)
|
||||
return false
|
||||
end
|
||||
if camera.useStreamingFocus == true then
|
||||
SetFocusPosAndVel(camera.coords.x, camera.coords.y, camera.coords.z, 0.0, 0.0, 0.0)
|
||||
focus_active = true
|
||||
end
|
||||
SetCamActive(active_camera, true)
|
||||
RenderScriptCams(true, true, transition_ms, true, false)
|
||||
|
||||
CreateThread(function()
|
||||
local expires_at = GetGameTimer() + math.floor(duration * 1000)
|
||||
while active_camera and session == camera_session do
|
||||
Wait(0)
|
||||
DisableAllControlActions(0)
|
||||
EnableControlAction(0, 177, true)
|
||||
BeginTextCommandDisplayHelp("STRING")
|
||||
AddTextComponentSubstringPlayerName(locale.WeatherCameraExit or "Weather camera | BACKSPACE to return")
|
||||
EndTextCommandDisplayHelp(0, false, true, -1)
|
||||
if IsControlJustReleased(0, 177) or IsEntityDead(PlayerPedId()) or GetGameTimer() >= expires_at then
|
||||
stop_weather_camera(true)
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
RegisterNUICallback("weather:cameras", function(_, cb)
|
||||
local response = Bridge.Callbacks.Trigger("sky_phone:weather:cameras", {})
|
||||
cb(response or { success = false, error = "request_failed" })
|
||||
end)
|
||||
|
||||
RegisterNUICallback("weather:camera-open", function(data, cb)
|
||||
if active_camera then cb({ success = false, error = "camera_active" }) return end
|
||||
local id = type(data) == "table" and data.id or nil
|
||||
if type(id) ~= "string" or not id:match("^[a-z0-9_]+$") then
|
||||
cb({ success = false, error = "camera_not_found" })
|
||||
return
|
||||
end
|
||||
local ped = PlayerPedId()
|
||||
if IsEntityDead(ped) or IsPedInAnyVehicle(ped, false) or GetEntitySpeed(ped) > 1.0 then
|
||||
cb({ success = false, error = "camera_requires_safe_position" })
|
||||
return
|
||||
end
|
||||
local response = Bridge.Callbacks.Trigger("sky_phone:weather:camera-open", { id = id })
|
||||
if not response or response.success ~= true or not start_weather_camera(response.data) then
|
||||
cb(response or { success = false, error = "request_failed" })
|
||||
return
|
||||
end
|
||||
cb({ success = true })
|
||||
end)
|
||||
|
||||
AddEventHandler("sky_phone:nuiClosed", function() stop_weather_camera(false) end)
|
||||
AddEventHandler("onResourceStop", function(resource)
|
||||
if resource == GetCurrentResourceName() then stop_weather_camera(false) end
|
||||
end)
|
||||
@@ -1,62 +0,0 @@
|
||||
local camera_config = Config.WeatherCameras or {}
|
||||
local cameras_by_id = {}
|
||||
local camera_summaries = {}
|
||||
|
||||
local function finite_number(value)
|
||||
return type(value) == "number" and value == value and value > -math.huge and value < math.huge
|
||||
end
|
||||
|
||||
for _, camera in ipairs(camera_config.Cameras or {}) do
|
||||
local id = type(camera.Id) == "string" and camera.Id or ""
|
||||
local coords = camera.Coords or {}
|
||||
local rotation = camera.Rotation or {}
|
||||
if id:match("^[a-z0-9_]+$")
|
||||
and type(camera.Region) == "string"
|
||||
and finite_number(coords.x) and finite_number(coords.y) and finite_number(coords.z)
|
||||
and finite_number(rotation.x) and finite_number(rotation.y) and finite_number(rotation.z)
|
||||
then
|
||||
cameras_by_id[id] = camera
|
||||
camera_summaries[#camera_summaries + 1] = { id = id, region = camera.Region }
|
||||
else
|
||||
print(("[sky_phone] Ignoring invalid weather camera configuration: %s"):format(tostring(camera.Id)))
|
||||
end
|
||||
end
|
||||
|
||||
local function authorize(source)
|
||||
local session = SkyPhone.RequireSession(source)
|
||||
if not session then return false, "not_authenticated" end
|
||||
if camera_config.Enabled == false then return false, "camera_disabled" end
|
||||
local limit = math.max(1, math.floor(tonumber(camera_config.RequestsPerMinute) or 10))
|
||||
if not SkyPhone.AllowOperation(source, "weather_camera", limit, 60) then
|
||||
return false, "rate_limited"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:weather:cameras", function(source)
|
||||
local allowed, error_code = authorize(source)
|
||||
if not allowed then return { success = false, error = error_code } end
|
||||
return { success = true, data = { cameras = camera_summaries } }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:weather:camera-open", function(source, data)
|
||||
local allowed, error_code = authorize(source)
|
||||
if not allowed then return { success = false, error = error_code } end
|
||||
local id = type(data) == "table" and data.id or nil
|
||||
local camera = type(id) == "string" and cameras_by_id[id] or nil
|
||||
if not camera then return { success = false, error = "camera_not_found" } end
|
||||
|
||||
local fov = math.max(20.0, math.min(90.0, tonumber(camera.FieldOfView) or 50.0))
|
||||
local duration = math.max(10, math.min(120, math.floor(tonumber(camera_config.SessionSeconds) or 30)))
|
||||
return {
|
||||
success = true,
|
||||
data = {
|
||||
id = id,
|
||||
coords = camera.Coords,
|
||||
rotation = camera.Rotation,
|
||||
fieldOfView = fov,
|
||||
sessionSeconds = duration,
|
||||
useStreamingFocus = camera_config.UseStreamingFocus == true,
|
||||
},
|
||||
}
|
||||
end)
|
||||
Reference in New Issue
Block a user