mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 09:18:57 +00:00
a9143b0fba
* ADD - build in-game admin command center * ADD - open admin panel by command * ENH - rebuild admin panel as standalone editor * ENH - compact admin workspace navigation * ENH - expand admin device controls * ENH - refine admin panel navigation style * ADD - add SQL phone configurator * FIX - expose complete phone configuration * FIX - make company jobs freely configurable * FIX - align configurator tables to left * ENH - organize configurator collection controls * ENH - organize nested configurator sections * ENH - add configurator field descriptions * FIX - hide fixed configurator add controls * ENH - refine configurator editing experience * ENH - refine admin configurator controls * FIX - apply configurator settings instantly * FIX - close live activity command registration --------- Co-authored-by: Leon.Schmidt <leonmauricewill15@gmail.com>
456 lines
14 KiB
Lua
456 lines
14 KiB
Lua
local MODE_HIDDEN = "hidden"
|
|
local MODE_PHONE_READ = "phone_read"
|
|
local MODE_CALL = "call"
|
|
local MODE_CAMERA_REAR = "camera_rear"
|
|
local MODE_CAMERA_SELFIE = "camera_selfie"
|
|
local LOOPED_UPPER_BODY_FLAGS = 49
|
|
local TRANSITION_UPPER_BODY_FLAGS = 48
|
|
|
|
local animation_state = {
|
|
call_direction = nil,
|
|
call_state = nil,
|
|
camera_active = false,
|
|
camera_front = false,
|
|
camera_landscape = false,
|
|
current_animation = nil,
|
|
current_mode = MODE_HIDDEN,
|
|
ped = nil,
|
|
phone_open = false,
|
|
prop = nil,
|
|
revision = 0,
|
|
watcher_active = false,
|
|
}
|
|
|
|
local reevaluate
|
|
|
|
local function load_model(model_hash)
|
|
RequestModel(model_hash)
|
|
local deadline = GetGameTimer() + Config.Animations.LoadTimeoutMs
|
|
while not HasModelLoaded(model_hash) do
|
|
if GetGameTimer() >= deadline then
|
|
Bridge.Debug(
|
|
"error",
|
|
"[sky_phone] Timed out loading phone prop model '%s'.",
|
|
Config.Animations.PropModel
|
|
)
|
|
return false
|
|
end
|
|
Wait(0)
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function load_animation_dictionary(dictionary)
|
|
RequestAnimDict(dictionary)
|
|
local deadline = GetGameTimer() + Config.Animations.LoadTimeoutMs
|
|
while not HasAnimDictLoaded(dictionary) do
|
|
if GetGameTimer() >= deadline then
|
|
Bridge.Debug(
|
|
"error",
|
|
"[sky_phone] Timed out loading animation dictionary '%s'.",
|
|
dictionary
|
|
)
|
|
return false
|
|
end
|
|
Wait(0)
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function can_animate(ped)
|
|
if not DoesEntityExist(ped) or IsEntityDead(ped) then
|
|
return false
|
|
end
|
|
if IsPedRagdoll(ped) or IsPedFalling(ped) or IsPedClimbing(ped) then
|
|
return false
|
|
end
|
|
if IsPedSwimming(ped) or IsPedSwimmingUnderWater(ped) or IsPedInParachuteFreeFall(ped) then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function get_phone_dictionary(ped)
|
|
if not IsPedInAnyVehicle(ped, false) then
|
|
return Config.Animations.Dictionaries.OnFoot, "on_foot"
|
|
end
|
|
|
|
local vehicle = GetVehiclePedIsIn(ped, false)
|
|
if GetPedInVehicleSeat(vehicle, -1) == ped then
|
|
return Config.Animations.Dictionaries.Driver, "driver:" .. tostring(vehicle)
|
|
end
|
|
return Config.Animations.Dictionaries.Passenger, "passenger:" .. tostring(vehicle)
|
|
end
|
|
|
|
local function derive_mode()
|
|
if not Config.Animations.Enabled then
|
|
return MODE_HIDDEN
|
|
end
|
|
if animation_state.call_state == "connected" then
|
|
return MODE_CALL
|
|
end
|
|
if animation_state.call_state == "ringing" and animation_state.call_direction == "outgoing" then
|
|
return MODE_CALL
|
|
end
|
|
if animation_state.camera_active and animation_state.camera_front then
|
|
return MODE_CAMERA_SELFIE
|
|
end
|
|
if animation_state.camera_active then
|
|
return MODE_CAMERA_REAR
|
|
end
|
|
if animation_state.call_state == "ringing" or animation_state.phone_open then
|
|
return MODE_PHONE_READ
|
|
end
|
|
return MODE_HIDDEN
|
|
end
|
|
|
|
local function stop_current_animation()
|
|
local current = animation_state.current_animation
|
|
if not current then
|
|
return
|
|
end
|
|
|
|
local ped = animation_state.ped
|
|
if ped and DoesEntityExist(ped) then
|
|
StopAnimTask(ped, current.dictionary, current.clip, 3.0)
|
|
end
|
|
animation_state.current_animation = nil
|
|
end
|
|
|
|
local function delete_phone_prop()
|
|
local prop = animation_state.prop
|
|
if not prop then
|
|
animation_state.ped = nil
|
|
return true
|
|
end
|
|
if not DoesEntityExist(prop) then
|
|
animation_state.prop = nil
|
|
animation_state.ped = nil
|
|
return true
|
|
end
|
|
|
|
DetachEntity(prop, true, true)
|
|
SetEntityAsMissionEntity(prop, true, true)
|
|
DeleteEntity(prop)
|
|
if DoesEntityExist(prop) then
|
|
Bridge.Debug("error", "[sky_phone] Failed to delete the attached phone prop.")
|
|
return false
|
|
end
|
|
|
|
animation_state.prop = nil
|
|
animation_state.ped = nil
|
|
return true
|
|
end
|
|
|
|
local function cleanup_phone()
|
|
stop_current_animation()
|
|
delete_phone_prop()
|
|
end
|
|
|
|
local function ensure_phone_prop(ped, revision)
|
|
if animation_state.prop and DoesEntityExist(animation_state.prop) and animation_state.ped == ped then
|
|
return true
|
|
end
|
|
|
|
if animation_state.prop then
|
|
delete_phone_prop()
|
|
end
|
|
|
|
local model_hash = joaat(Config.Animations.PropModel)
|
|
if not load_model(model_hash) then
|
|
return false
|
|
end
|
|
if animation_state.revision ~= revision then
|
|
SetModelAsNoLongerNeeded(model_hash)
|
|
return false
|
|
end
|
|
|
|
local coords = GetEntityCoords(ped)
|
|
local prop = CreateObject(model_hash, coords.x, coords.y, coords.z, true, true, false)
|
|
SetModelAsNoLongerNeeded(model_hash)
|
|
if not prop or prop == 0 or not DoesEntityExist(prop) then
|
|
Bridge.Debug("error", "[sky_phone] Failed to create the phone prop.")
|
|
return false
|
|
end
|
|
|
|
SetEntityCollision(prop, false, false)
|
|
animation_state.prop = prop
|
|
animation_state.ped = ped
|
|
return true
|
|
end
|
|
|
|
local function get_transform(mode)
|
|
if (mode == MODE_CAMERA_REAR or mode == MODE_CAMERA_SELFIE) and animation_state.camera_landscape then
|
|
return Config.Animations.Transforms.Landscape
|
|
end
|
|
return Config.Animations.Transforms.Portrait
|
|
end
|
|
|
|
local function attach_phone_prop(ped, mode)
|
|
local transform = get_transform(mode)
|
|
local position = transform.position
|
|
local rotation = transform.rotation
|
|
AttachEntityToEntity(
|
|
animation_state.prop,
|
|
ped,
|
|
GetPedBoneIndex(ped, Config.Animations.PropBone),
|
|
position.x,
|
|
position.y,
|
|
position.z,
|
|
rotation.x,
|
|
rotation.y,
|
|
rotation.z,
|
|
true,
|
|
false,
|
|
false,
|
|
false,
|
|
2,
|
|
true
|
|
)
|
|
end
|
|
|
|
local function play_animation(ped, dictionary, clip, looped, revision)
|
|
if not load_animation_dictionary(dictionary) or animation_state.revision ~= revision then
|
|
return nil
|
|
end
|
|
|
|
local duration = -1
|
|
local flags = LOOPED_UPPER_BODY_FLAGS
|
|
if not looped then
|
|
local duration_seconds = GetAnimDuration(dictionary, clip)
|
|
if duration_seconds <= 0.0 then
|
|
Bridge.Debug(
|
|
"error",
|
|
"[sky_phone] Animation '%s' was not found in dictionary '%s'.",
|
|
clip,
|
|
dictionary
|
|
)
|
|
return nil
|
|
end
|
|
duration = math.max(1, math.floor(duration_seconds * 1000.0))
|
|
flags = TRANSITION_UPPER_BODY_FLAGS
|
|
end
|
|
|
|
stop_current_animation()
|
|
TaskPlayAnim(ped, dictionary, clip, 4.0, -4.0, duration, flags, 1.0, false, false, false)
|
|
animation_state.current_animation = {
|
|
clip = clip,
|
|
dictionary = dictionary,
|
|
}
|
|
return duration
|
|
end
|
|
|
|
local function get_base_animation(ped, mode)
|
|
local dictionary = get_phone_dictionary(ped)
|
|
if mode == MODE_CALL then
|
|
return dictionary, Config.Animations.Clips.CallListen
|
|
end
|
|
if
|
|
(mode == MODE_CAMERA_REAR or mode == MODE_CAMERA_SELFIE)
|
|
and not IsPedInAnyVehicle(ped, false)
|
|
then
|
|
return Config.Animations.Dictionaries.Camera, Config.Animations.Clips.Camera
|
|
end
|
|
return dictionary, Config.Animations.Clips.TextRead
|
|
end
|
|
|
|
local function get_transition_clip(previous_mode, mode)
|
|
if previous_mode == MODE_HIDDEN then
|
|
return Config.Animations.Clips.TextIn
|
|
end
|
|
if previous_mode == MODE_CALL and mode ~= MODE_CALL then
|
|
return Config.Animations.Clips.CallToText
|
|
end
|
|
if previous_mode ~= MODE_CALL and mode == MODE_CALL then
|
|
return Config.Animations.Clips.TextToCall
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function apply_visible_mode(previous_mode, mode, revision)
|
|
local ped = PlayerPedId()
|
|
if not can_animate(ped) or not ensure_phone_prop(ped, revision) then
|
|
return
|
|
end
|
|
|
|
attach_phone_prop(ped, mode)
|
|
local transition_clip = get_transition_clip(previous_mode, mode)
|
|
if transition_clip then
|
|
local dictionary = get_phone_dictionary(ped)
|
|
local duration = play_animation(ped, dictionary, transition_clip, false, revision)
|
|
if not duration then
|
|
cleanup_phone()
|
|
return
|
|
end
|
|
Wait(duration)
|
|
if animation_state.revision ~= revision or not can_animate(ped) then
|
|
return
|
|
end
|
|
attach_phone_prop(ped, mode)
|
|
end
|
|
|
|
local dictionary, clip = get_base_animation(ped, mode)
|
|
if not play_animation(ped, dictionary, clip, true, revision) then
|
|
cleanup_phone()
|
|
end
|
|
end
|
|
|
|
local function apply_hidden_mode(previous_mode, revision)
|
|
local ped = animation_state.ped
|
|
if not ped or not DoesEntityExist(ped) or not animation_state.prop or not DoesEntityExist(animation_state.prop) then
|
|
cleanup_phone()
|
|
return
|
|
end
|
|
|
|
local clip = previous_mode == MODE_CALL and Config.Animations.Clips.CallOut or Config.Animations.Clips.TextOut
|
|
local dictionary = get_phone_dictionary(ped)
|
|
local duration = play_animation(ped, dictionary, clip, false, revision)
|
|
if not duration then
|
|
cleanup_phone()
|
|
return
|
|
end
|
|
Wait(duration)
|
|
if animation_state.revision == revision then
|
|
cleanup_phone()
|
|
end
|
|
end
|
|
|
|
local function apply_mode(previous_mode, mode, revision)
|
|
if mode == MODE_HIDDEN then
|
|
apply_hidden_mode(previous_mode, revision)
|
|
return
|
|
end
|
|
apply_visible_mode(previous_mode, mode, revision)
|
|
end
|
|
|
|
local function ensure_context_watcher()
|
|
if animation_state.watcher_active or derive_mode() == MODE_HIDDEN then
|
|
return
|
|
end
|
|
|
|
animation_state.watcher_active = true
|
|
CreateThread(function()
|
|
local observed_ped = nil
|
|
local observed_context = nil
|
|
local was_available = false
|
|
while derive_mode() ~= MODE_HIDDEN do
|
|
local ped = PlayerPedId()
|
|
local available = can_animate(ped)
|
|
local context = "unavailable"
|
|
if available then
|
|
local _, current_context = get_phone_dictionary(ped)
|
|
context = current_context
|
|
end
|
|
|
|
if not available and (was_available or animation_state.prop) then
|
|
animation_state.revision = animation_state.revision + 1
|
|
animation_state.current_mode = MODE_HIDDEN
|
|
cleanup_phone()
|
|
elseif available and (not was_available or observed_ped ~= ped or observed_context ~= context) then
|
|
reevaluate(true)
|
|
end
|
|
|
|
observed_ped = ped
|
|
observed_context = context
|
|
was_available = available
|
|
Wait(Config.Animations.ContextPollMs)
|
|
end
|
|
animation_state.watcher_active = false
|
|
end)
|
|
end
|
|
|
|
reevaluate = function(force)
|
|
local mode = derive_mode()
|
|
local ped = PlayerPedId()
|
|
if mode ~= MODE_HIDDEN and not can_animate(ped) then
|
|
animation_state.revision = animation_state.revision + 1
|
|
animation_state.current_mode = MODE_HIDDEN
|
|
cleanup_phone()
|
|
ensure_context_watcher()
|
|
return
|
|
end
|
|
if mode == animation_state.current_mode and not force then
|
|
ensure_context_watcher()
|
|
return
|
|
end
|
|
|
|
local previous_mode = animation_state.current_mode
|
|
animation_state.current_mode = mode
|
|
animation_state.revision = animation_state.revision + 1
|
|
local revision = animation_state.revision
|
|
ensure_context_watcher()
|
|
CreateThread(function()
|
|
apply_mode(previous_mode, mode, revision)
|
|
end)
|
|
end
|
|
|
|
AddEventHandler("sky_phone:configurator:updated", function()
|
|
animation_state.revision = animation_state.revision + 1
|
|
cleanup_phone()
|
|
reevaluate(true)
|
|
end)
|
|
|
|
local function reset_animation_state()
|
|
animation_state.phone_open = false
|
|
animation_state.call_state = nil
|
|
animation_state.call_direction = nil
|
|
animation_state.camera_active = false
|
|
animation_state.camera_front = false
|
|
animation_state.camera_landscape = false
|
|
animation_state.current_mode = MODE_HIDDEN
|
|
animation_state.revision = animation_state.revision + 1
|
|
cleanup_phone()
|
|
end
|
|
|
|
AddEventHandler("sky_phone:animation:phone", function(open)
|
|
if type(open) ~= "boolean" then
|
|
Bridge.Debug("warn", "[sky_phone] Ignored invalid phone animation state.")
|
|
return
|
|
end
|
|
animation_state.phone_open = open
|
|
|
|
if not open then
|
|
animation_state.camera_active = false
|
|
animation_state.camera_front = false
|
|
animation_state.camera_landscape = false
|
|
|
|
if derive_mode() == MODE_HIDDEN then
|
|
animation_state.current_mode = MODE_HIDDEN
|
|
animation_state.revision = animation_state.revision + 1
|
|
cleanup_phone()
|
|
return
|
|
end
|
|
end
|
|
|
|
reevaluate(false)
|
|
end)
|
|
|
|
AddEventHandler("sky_phone:animation:call", function(data)
|
|
if type(data) ~= "table" or type(data.state) ~= "string" or type(data.direction) ~= "string" then
|
|
Bridge.Debug("warn", "[sky_phone] Ignored invalid call animation state.")
|
|
return
|
|
end
|
|
animation_state.call_state = data.state
|
|
animation_state.call_direction = data.direction
|
|
reevaluate(false)
|
|
end)
|
|
|
|
AddEventHandler("sky_phone:animation:camera", function(data)
|
|
if type(data) ~= "table" or type(data.active) ~= "boolean" then
|
|
Bridge.Debug("warn", "[sky_phone] Ignored invalid camera animation state.")
|
|
return
|
|
end
|
|
animation_state.camera_active = data.active and animation_state.phone_open
|
|
animation_state.camera_front = data.front == true
|
|
animation_state.camera_landscape = data.landscape == true
|
|
reevaluate(true)
|
|
end)
|
|
|
|
AddEventHandler("sky_phone:animation:reset", reset_animation_state)
|
|
|
|
AddEventHandler("onResourceStop", function(resource_name)
|
|
if resource_name == GetCurrentResourceName() then
|
|
reset_animation_state()
|
|
end
|
|
end)
|