mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 00:01:29 +00:00
FIX - stabilize payphone call lifecycle
This commit is contained in:
@@ -65,6 +65,7 @@ Config.Payphones = {
|
||||
Dictionary = "anim@scripted@payphone_hits@male@",
|
||||
PedClip = "FXFR_PAV_1_INTRO_MALE",
|
||||
PropClip = "FXFR_PAV_1_INTRO_PHONE",
|
||||
HangupDurationMs = 1200,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -55,5 +55,5 @@ function Bridge.Framework.ShowHelpNotification(message, key)
|
||||
local control = key == "E" and "~INPUT_CONTEXT~" or ("[%s]"):format(tostring(key or "E"))
|
||||
BeginTextCommandDisplayHelp("STRING")
|
||||
AddTextComponentSubstringPlayerName(("%s %s"):format(control, tostring(message or "")))
|
||||
EndTextCommandDisplayHelp(0, false, true, -1)
|
||||
EndTextCommandDisplayHelp(0, false, false, -1)
|
||||
end
|
||||
|
||||
@@ -10,7 +10,10 @@ local call_channel = 0
|
||||
local replacement_prop = nil
|
||||
local hidden_prop = nil
|
||||
local animation_scene = nil
|
||||
local animation_ped = nil
|
||||
local animation_floor_z = nil
|
||||
local visuals_starting = false
|
||||
local visuals_ending = false
|
||||
local hangup_requested = false
|
||||
|
||||
local configured_models = {}
|
||||
@@ -68,9 +71,10 @@ end
|
||||
|
||||
local function stop_call_visuals()
|
||||
local animation = Config.Payphones.Animation
|
||||
local ped = PlayerPedId()
|
||||
local ped = animation_ped or PlayerPedId()
|
||||
local visuals_active = animation_scene ~= nil or replacement_prop ~= nil
|
||||
visuals_starting = false
|
||||
visuals_ending = false
|
||||
|
||||
if visuals_active and DoesEntityExist(ped) then
|
||||
StopAnimTask(ped, animation.Dictionary, animation.PedClip, 0.0)
|
||||
@@ -83,6 +87,9 @@ local function stop_call_visuals()
|
||||
animation_scene = nil
|
||||
end
|
||||
|
||||
animation_ped = nil
|
||||
animation_floor_z = nil
|
||||
|
||||
if replacement_prop and DoesEntityExist(replacement_prop) then
|
||||
StopEntityAnim(replacement_prop, animation.PropClip, animation.Dictionary, 0.0)
|
||||
SetEntityVisible(replacement_prop, false, false)
|
||||
@@ -98,6 +105,69 @@ local function stop_call_visuals()
|
||||
RemoveAnimDict(animation.Dictionary)
|
||||
end
|
||||
|
||||
local function keep_animation_ped_grounded()
|
||||
if not animation_ped or not animation_floor_z or not DoesEntityExist(animation_ped) then
|
||||
return
|
||||
end
|
||||
local coords = GetEntityCoords(animation_ped)
|
||||
if coords.z >= animation_floor_z - 0.15 then
|
||||
return
|
||||
end
|
||||
SetEntityCoordsNoOffset(animation_ped, coords.x, coords.y, animation_floor_z, false, false, false)
|
||||
SetEntityVelocity(animation_ped, 0.0, 0.0, 0.0)
|
||||
end
|
||||
|
||||
local function play_hangup_visuals()
|
||||
if visuals_ending then
|
||||
return
|
||||
end
|
||||
if not animation_scene or not animation_ped or not DoesEntityExist(animation_ped) then
|
||||
stop_call_visuals()
|
||||
active_booth = nil
|
||||
return
|
||||
end
|
||||
|
||||
visuals_ending = true
|
||||
local scene = animation_scene
|
||||
local starting_phase = math.max(0.0, math.min(1.0, GetSynchronizedScenePhase(scene)))
|
||||
local duration_ms = math.max(250, math.floor(tonumber(Config.Payphones.Animation.HangupDurationMs) or 1200))
|
||||
local started_at = GetGameTimer()
|
||||
SetSynchronizedSceneRate(scene, 0.0)
|
||||
if replacement_prop and DoesEntityExist(replacement_prop) then
|
||||
SetEntityAnimSpeed(
|
||||
replacement_prop,
|
||||
Config.Payphones.Animation.Dictionary,
|
||||
Config.Payphones.Animation.PropClip,
|
||||
0.0
|
||||
)
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while animation_scene == scene do
|
||||
local progress = math.min(1.0, (GetGameTimer() - started_at) / duration_ms)
|
||||
local phase = starting_phase * (1.0 - progress)
|
||||
SetSynchronizedScenePhase(scene, phase)
|
||||
if replacement_prop and DoesEntityExist(replacement_prop) then
|
||||
SetEntityAnimCurrentTime(
|
||||
replacement_prop,
|
||||
Config.Payphones.Animation.Dictionary,
|
||||
Config.Payphones.Animation.PropClip,
|
||||
phase
|
||||
)
|
||||
end
|
||||
keep_animation_ped_grounded()
|
||||
if progress >= 1.0 then
|
||||
break
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
if animation_scene == scene then
|
||||
stop_call_visuals()
|
||||
active_booth = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function start_call_visuals()
|
||||
if visuals_starting or replacement_prop or not active_call_id
|
||||
or not active_booth or not DoesEntityExist(active_booth.entity)
|
||||
@@ -168,6 +238,15 @@ local function start_call_visuals()
|
||||
return
|
||||
end
|
||||
SetSynchronizedSceneHoldLastFrame(animation_scene, true)
|
||||
animation_ped = ped
|
||||
local ped_coords = GetEntityCoords(animation_ped)
|
||||
local found_ground, ground_z = GetGroundZFor_3dCoord(
|
||||
ped_coords.x,
|
||||
ped_coords.y,
|
||||
ped_coords.z + 1.0,
|
||||
false
|
||||
)
|
||||
animation_floor_z = found_ground and ground_z or ped_coords.z
|
||||
TaskSynchronizedScene(
|
||||
ped,
|
||||
animation_scene,
|
||||
@@ -334,15 +413,14 @@ RegisterNetEvent("sky_phone:payphone:state", function(data)
|
||||
else
|
||||
clear_active_call_state()
|
||||
leave_call_voice()
|
||||
stop_call_visuals()
|
||||
active_booth = nil
|
||||
play_hangup_visuals()
|
||||
end
|
||||
SendNUIMessage({ type = "payphone:state", data = data })
|
||||
end)
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
if not Config.Payphones.Enabled or payphone_open or active_call_id then
|
||||
if not Config.Payphones.Enabled or payphone_open or active_call_id or visuals_ending then
|
||||
nearest_payphone = nil
|
||||
Wait(Config.Payphones.ScanIntervalMs)
|
||||
else
|
||||
@@ -391,19 +469,27 @@ end)
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
if active_call_id and active_booth then
|
||||
local call_id = active_call_id
|
||||
local booth = active_booth
|
||||
if call_id and booth then
|
||||
keep_animation_ped_grounded()
|
||||
Bridge.Framework.ShowHelpNotification(call_help_message(), "E")
|
||||
if IsControlJustReleased(0, 38) and not hangup_requested then
|
||||
hangup_requested = true
|
||||
Bridge.Callbacks.Trigger("sky_phone:payphone:hangup", { id = active_call_id })
|
||||
Bridge.Callbacks.Trigger("sky_phone:payphone:hangup", { id = call_id })
|
||||
end
|
||||
|
||||
local distance = #(GetEntityCoords(PlayerPedId()) - active_booth.coords)
|
||||
if distance > Config.Payphones.MaximumCallDistance and not hangup_requested then
|
||||
hangup_requested = true
|
||||
Bridge.Callbacks.Trigger("sky_phone:payphone:hangup", { id = active_call_id })
|
||||
if active_call_id == call_id and active_booth == booth then
|
||||
local distance = #(GetEntityCoords(PlayerPedId()) - booth.coords)
|
||||
if distance > Config.Payphones.MaximumCallDistance and not hangup_requested then
|
||||
hangup_requested = true
|
||||
Bridge.Callbacks.Trigger("sky_phone:payphone:hangup", { id = call_id })
|
||||
end
|
||||
end
|
||||
Wait(0)
|
||||
elseif visuals_ending then
|
||||
keep_animation_ped_grounded()
|
||||
Wait(0)
|
||||
else
|
||||
Wait(250)
|
||||
end
|
||||
|
||||
@@ -100,8 +100,8 @@ local function send_state(call, source, state, channel)
|
||||
channel = channel,
|
||||
}
|
||||
if call.payphone and outgoing then
|
||||
payload.elapsedSeconds = call.payphone.billed_seconds or 0
|
||||
payload.totalCost = (call.payphone.billed_seconds or 0) * call.payphone.price_per_second
|
||||
payload.elapsedSeconds = call.payphone.elapsed_seconds or 0
|
||||
payload.totalCost = call.payphone.total_cost or 0
|
||||
TriggerClientEvent("sky_phone:payphone:state", source, payload)
|
||||
return
|
||||
end
|
||||
@@ -116,6 +116,59 @@ local function notify_recents(device, source)
|
||||
end
|
||||
end
|
||||
|
||||
local function settle_payphone_call(call, duration)
|
||||
local payphone = call.payphone
|
||||
local elapsed_seconds = math.max(0, math.floor(tonumber(duration) or 0))
|
||||
local price_per_second = math.max(0, math.floor(tonumber(payphone.price_per_second) or 0))
|
||||
local billable_seconds = elapsed_seconds
|
||||
local total_cost = billable_seconds * price_per_second
|
||||
|
||||
if total_cost > 0 then
|
||||
local available_money = tonumber(Bridge.Framework.GetMoney(call.caller_source, Config.Payphones.PaymentAccount))
|
||||
if not available_money then
|
||||
Bridge.Debug(
|
||||
"error",
|
||||
"[sky_phone] Payphone settlement could not read the balance for source %s.",
|
||||
tostring(call.caller_source)
|
||||
)
|
||||
billable_seconds = 0
|
||||
total_cost = 0
|
||||
elseif available_money < total_cost then
|
||||
billable_seconds = math.min(elapsed_seconds, math.floor(math.max(0, available_money) / price_per_second))
|
||||
total_cost = billable_seconds * price_per_second
|
||||
end
|
||||
end
|
||||
|
||||
local charged = total_cost == 0
|
||||
if total_cost > 0 then
|
||||
local success, result = pcall(
|
||||
Bridge.Framework.RemoveMoney,
|
||||
call.caller_source,
|
||||
Config.Payphones.PaymentAccount,
|
||||
total_cost
|
||||
)
|
||||
charged = success and result and true or false
|
||||
if not success then
|
||||
Bridge.Debug(
|
||||
"error",
|
||||
"[sky_phone] Payphone settlement failed for source %s: %s",
|
||||
tostring(call.caller_source),
|
||||
tostring(result)
|
||||
)
|
||||
elseif not result then
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
"[sky_phone] Payphone settlement was rejected for source %s.",
|
||||
tostring(call.caller_source)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
payphone.elapsed_seconds = elapsed_seconds
|
||||
payphone.total_cost = charged and total_cost or 0
|
||||
return charged and billable_seconds == elapsed_seconds
|
||||
end
|
||||
|
||||
local function finish_call(call, status)
|
||||
if not call or call.ended then
|
||||
return
|
||||
@@ -123,6 +176,11 @@ local function finish_call(call, status)
|
||||
call.ended = true
|
||||
local ended_at = os.time()
|
||||
local duration = call.answered_at and math.max(0, ended_at - call.answered_at) or 0
|
||||
if call.payphone and call.answered_at and not settle_payphone_call(call, duration)
|
||||
and status ~= "disconnected"
|
||||
then
|
||||
status = "insufficient_funds"
|
||||
end
|
||||
local callee_status = status
|
||||
if status == "no_answer" or status == "cancelled" then
|
||||
callee_status = "missed"
|
||||
@@ -552,7 +610,8 @@ Bridge.Callbacks.Register("sky_phone:payphone:dial", function(source, data)
|
||||
callee_device = target,
|
||||
started_at = os.time(),
|
||||
payphone = {
|
||||
billed_seconds = 0,
|
||||
elapsed_seconds = 0,
|
||||
total_cost = 0,
|
||||
coords = booth_coords,
|
||||
price_per_second = price_per_second,
|
||||
},
|
||||
@@ -675,33 +734,16 @@ CreateThread(function()
|
||||
calls_to_finish[call_id] = "disconnected"
|
||||
elseif call.payphone and call.answered_at and not call.ended then
|
||||
local elapsed_seconds = math.max(0, os.time() - call.answered_at)
|
||||
local seconds_due = elapsed_seconds - call.payphone.billed_seconds
|
||||
if seconds_due > 0 then
|
||||
local amount_due = seconds_due * call.payphone.price_per_second
|
||||
local charged = amount_due == 0
|
||||
if amount_due > 0 then
|
||||
local success, result = pcall(
|
||||
Bridge.Framework.RemoveMoney,
|
||||
call.caller_source,
|
||||
Config.Payphones.PaymentAccount,
|
||||
amount_due
|
||||
)
|
||||
charged = success and result and true or false
|
||||
if not success then
|
||||
Bridge.Debug(
|
||||
"error",
|
||||
"[sky_phone] Payphone billing failed for source %s: %s",
|
||||
tostring(call.caller_source),
|
||||
tostring(result)
|
||||
)
|
||||
end
|
||||
end
|
||||
if charged then
|
||||
call.payphone.billed_seconds = elapsed_seconds
|
||||
send_state(call, call.caller_source, "connected", call.channel)
|
||||
else
|
||||
calls_to_finish[call_id] = "insufficient_funds"
|
||||
end
|
||||
local total_cost = elapsed_seconds * call.payphone.price_per_second
|
||||
local available_money = tonumber(
|
||||
Bridge.Framework.GetMoney(call.caller_source, Config.Payphones.PaymentAccount)
|
||||
)
|
||||
if total_cost > 0 and (not available_money or available_money < total_cost) then
|
||||
calls_to_finish[call_id] = "insufficient_funds"
|
||||
elseif call.payphone.elapsed_seconds ~= elapsed_seconds then
|
||||
call.payphone.elapsed_seconds = elapsed_seconds
|
||||
call.payphone.total_cost = total_cost
|
||||
send_state(call, call.caller_source, "connected", call.channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user