ENH - detect payphones and spawn custom locations

Remove the static vanilla payphone coordinate list so configured prop models are detected directly in the streamed world. Add vector4-based custom locations that spawn the selected custom prop, and validate detected booth models and player proximity server-side.
This commit is contained in:
Leon.Schmidt
2026-08-17 23:14:33 +02:00
parent 29fb234d5c
commit cc1a8ae151
6 changed files with 127 additions and 348 deletions
+65
View File
@@ -18,6 +18,7 @@ local visuals_starting = false
local visuals_ending = false
local hangup_requested = false
local remote_visuals = {}
local custom_payphone_props = {}
local configured_models = {}
for _, model_name in ipairs(Config.Payphones.Props or {}) do
@@ -97,6 +98,64 @@ local function load_model(model_hash)
return HasModelLoaded(model_hash)
end
local function spawn_custom_payphones()
local locations = Config.Payphones.CustomLocations or {}
if not Config.Payphones.Enabled or #locations == 0 then
return
end
local model_name = Config.Payphones.CustomProp
local model_hash = type(model_name) == "string" and joaat(model_name) or 0
if not configured_models[model_hash] then
Bridge.Debug(
"error",
"[sky_phone] Config.Payphones.CustomProp must also be listed in Config.Payphones.Props.",
{ always = true }
)
return
end
if not load_model(model_hash) then
Bridge.Debug("error", "[sky_phone] The configured custom payphone prop could not be loaded.", {
always = true,
})
return
end
for index, coords in ipairs(locations) do
local coords_type = type(coords)
local x = (coords_type == "vector4" or coords_type == "table")
and valid_visual_number(coords.x, 10000.0) or nil
local y = x and valid_visual_number(coords.y, 10000.0) or nil
local z = y and valid_visual_number(coords.z, 2000.0) or nil
local heading = z and valid_visual_number(coords.w, 360000.0) or nil
if not heading then
Bridge.Debug(
"error",
"[sky_phone] Ignored invalid Config.Payphones.CustomLocations entry %s; use vector4(x, y, z, heading).",
index,
{ always = true }
)
else
local entity = CreateObjectNoOffset(model_hash, x, y, z, false, true, false)
if entity == 0 or not DoesEntityExist(entity) then
Bridge.Debug(
"error",
"[sky_phone] Failed to spawn custom payphone %s.",
index,
{ always = true }
)
else
SetEntityHeading(entity, heading % 360.0)
FreezeEntityPosition(entity, true)
custom_payphone_props[#custom_payphone_props + 1] = entity
end
end
end
SetModelAsNoLongerNeeded(model_hash)
end
CreateThread(spawn_custom_payphones)
local function load_animation(dictionary)
if HasAnimDictLoaded(dictionary) then
return true
@@ -667,4 +726,10 @@ AddEventHandler("onResourceStop", function(resource_name)
for _, id in ipairs(visual_ids) do
restore_remote_visual(id)
end
for _, entity in ipairs(custom_payphone_props) do
if DoesEntityExist(entity) then
SetEntityAsMissionEntity(entity, true, true)
DeleteEntity(entity)
end
end
end)
+8 -19
View File
@@ -1074,34 +1074,23 @@ for _, model_name in ipairs(Config.Payphones.Props or {}) do
end
end
local payphone_locations, rejected_payphone_locations = SkyPhonePayphones.ValidateLocations(
Config.Payphones.Locations,
payphone_models
)
if Config.Payphones.Enabled and #payphone_locations == 0 then
if Config.Payphones.Enabled and not next(payphone_models) then
Bridge.Debug(
"error",
"[sky_phone] Payphones are enabled, but no valid server-owned locations are configured; payphone calls will be rejected.",
{ always = true }
)
elseif Config.Payphones.Enabled and rejected_payphone_locations > 0 then
Bridge.Debug(
"warn",
"[sky_phone] Ignored %s invalid server-owned payphone location(s).",
rejected_payphone_locations,
"[sky_phone] Payphones are enabled, but Config.Payphones.Props contains no valid models; payphone calls will be rejected.",
{ always = true }
)
end
local function valid_payphone_position(source)
local function valid_payphone_position(source, detected_booth)
local ped = GetPlayerPed(source)
if not ped or ped == 0 then
return nil
end
local player_coords = GetEntityCoords(ped)
local location = SkyPhonePayphones.FindNearest(
payphone_locations,
player_coords,
local location = SkyPhonePayphones.ValidateDetected(
detected_booth,
payphone_models,
GetEntityCoords(ped),
Config.Payphones.ServerValidationDistance
)
if not location then
@@ -1129,7 +1118,7 @@ Bridge.Callbacks.Register("sky_phone:payphone:dial", function(source, data)
if not Config.Payphones.Enabled or not SkyPhone.AllowOperation(source, "payphone_dial", 15, 60) then
return { success = false, error = "rate_limited" }
end
local booth_coords, booth_model = valid_payphone_position(source)
local booth_coords, booth_model = valid_payphone_position(source, data)
if not booth_coords then
return { success = false, error = "invalid_payphone" }
end
+12 -47
View File
@@ -12,7 +12,8 @@ end
local function normalize_coordinates(value, allow_vector)
local value_type = type(value)
if value_type ~= "table" and (not allow_vector or value_type ~= "vector3") then
local supported_vector = value_type == "vector3" or value_type == "vector4"
if value_type ~= "table" and (not allow_vector or not supported_vector) then
return nil
end
@@ -46,60 +47,24 @@ local function normalize_location(location, allowed_models)
}
end
function SkyPhonePayphones.ValidateLocations(locations, allowed_models)
if type(locations) ~= "table" or type(allowed_models) ~= "table" then
return {}, 0
end
local validated = {}
local rejected = 0
for index, location in pairs(locations) do
local valid_index = type(index) == "number" and index >= 1 and index % 1 == 0
local normalized = valid_index and normalize_location(location, allowed_models) or nil
if normalized then
normalized.index = index
validated[#validated + 1] = normalized
else
rejected = rejected + 1
end
end
table.sort(validated, function(left, right)
return left.index < right.index
end)
for index = 1, #validated do
validated[index].index = nil
end
return validated, rejected
end
function SkyPhonePayphones.FindNearest(locations, player_coords, maximum_distance)
if type(locations) ~= "table" then
function SkyPhonePayphones.ValidateDetected(location, allowed_models, player_coords, maximum_distance)
if type(allowed_models) ~= "table" then
return nil
end
local normalized = normalize_location(location, allowed_models)
local coords = normalize_coordinates(player_coords, true)
local distance = finite_number(maximum_distance)
if not coords or not distance or distance <= 0 then
if not normalized or not coords or not distance or distance <= 0 then
return nil
end
local nearest = nil
local nearest_distance_squared = distance * distance
for index = 1, #locations do
local location = locations[index]
if type(location) == "table" and type(location.coords) == "table" then
local dx = coords.x - location.coords.x
local dy = coords.y - location.coords.y
local dz = coords.z - location.coords.z
local distance_squared = dx * dx + dy * dy + dz * dz
if distance_squared <= nearest_distance_squared then
nearest = location
nearest_distance_squared = distance_squared
end
end
local dx = coords.x - normalized.coords.x
local dy = coords.y - normalized.coords.y
local dz = coords.z - normalized.coords.z
if dx * dx + dy * dy + dz * dz > distance * distance then
return nil
end
return nearest
return normalized
end