Merge branch 'dev' into feat/inter-font

This commit is contained in:
DerEchteAlec
2026-08-19 19:21:42 +02:00
committed by GitHub
18 changed files with 370 additions and 32 deletions
+4 -2
View File
@@ -89,7 +89,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
| --- | --- |
| **Frameworks** | ESX Legacy, QBCore, Qbox |
| **Inventories** | ox_inventory, qb-inventory, lj-inventory, qs-inventory, codem-inventory, core_inventory, mf-inventory, smx-inventory, hex_4_inventory, and native ESX inventory |
| **Calls** | PMA Voice, SaltyChat |
| **Calls** | YACA, PMA Voice, SaltyChat |
| **Radio** | YACA, PMA Voice, SaltyChat |
| **Housing** | ESX Property, qbx_properties |
| **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge |
@@ -140,6 +140,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
Phone calls support:
- YACA
- PMA Voice
- SaltyChat
@@ -433,10 +434,11 @@ Config.Calls.VoiceProvider = "pma"
Supported values:
- `yaca` or `yaca-voice`
- `pma` or `pma-voice`
- `saltychat` or `salty`
SaltyChat supports the provider-backed call speaker feature. PMA Voice keeps the speaker option unavailable.
YACA supports calls, payphone calls, provider-backed speaker mode, and real microphone mute. SaltyChat supports provider-backed speaker mode. PMA Voice keeps speaker and mute controls unavailable.
### Radio
+1 -1
View File
@@ -82,7 +82,7 @@ Config.Speaker = {
}
Config.Calls = {
VoiceProvider = "pma", -- pma (alias: pma-voice), saltychat (alias: salty)
VoiceProvider = "pma", -- yaca (alias: yaca-voice), pma (alias: pma-voice), saltychat (alias: salty)
RingSeconds = 30,
ContactNameMaxLength = 80,
ContactNotesMaxLength = 500,
+2 -1
View File
@@ -1008,8 +1008,9 @@ Locales["de"] = {
message_unavailable = "Das Gespräch konnte nicht eröffnet werden.", contact_remove_failed = "Der Kontakt konnte nicht entfernt werden.", contact_favorite_failed = "Der Favorit konnte nicht aktualisiert werden.",
blocked = "Diese Nummer ist blockiert.", recipient_not_found = "Diese Nummer ist nicht bekannt.",
rate_limited = "Zu viele Anrufe. Versuch es in einer Minute erneut.", voice_unavailable = "Der konfigurierte Sprachdienst ist nicht verfügbar.",
call_not_connected = "Schließ den Anruf an, bevor du den Lautsprechermodus ermöglichst.", speaker_unavailable = "Der Lautsprechermodus ist für den konfigurierten Handy-Sprachdienst nicht verfügbar.",
call_not_connected = "Nimm den Anruf an, bevor du die Audiosteuerung änderst.", speaker_unavailable = "Der Lautsprechermodus ist für den konfigurierten Handy-Sprachdienst nicht verfügbar.",
speaker_unsupported = "Der konfigurierte Handy-Sprachdienst unterstützt den Lautsprechermodus nicht.",
mute_unavailable = "Die Stummschaltung ist für den konfigurierten Handy-Sprachdienst nicht verfügbar.", mute_unsupported = "Der konfigurierte Handy-Sprachdienst unterstützt keine Stummschaltung.",
inventory_full = "Es gibt keinen Platz für die ausgeworfene SIM-Karte.", request_failed = "Die Telefonanfrage ist fehlgeschlagen.",
operation_in_progress = "Eine andere Handy-Aktion wird bereits ausgeführt.", sim_request_expired = "Die SIM-Auswahl ist abgelaufen. Verwende die SIM-Karte erneut.",
sim_not_owned = "Diese SIM Karte ist nicht mehr in deinem Inventar.", phone_not_owned = "Das Handy ist nicht mehr in deinem Inventar.",
+2 -1
View File
@@ -1008,8 +1008,9 @@ Locales["en"] = {
message_unavailable = "The conversation could not be opened.", contact_remove_failed = "The contact could not be removed.", contact_favorite_failed = "The favorite could not be updated.",
blocked = "This number is blocked.", recipient_not_found = "This number is not known.",
rate_limited = "Too many calls. Try again in a minute.", voice_unavailable = "The configured phone voice service is unavailable.",
call_not_connected = "Connect the call before enabling speaker mode.", speaker_unavailable = "Speaker mode is not available for the configured phone voice service.",
call_not_connected = "Connect the call before changing its audio controls.", speaker_unavailable = "Speaker mode is not available for the configured phone voice service.",
speaker_unsupported = "The configured phone voice service does not support speaker mode.",
mute_unavailable = "Mute is not available for the configured phone voice service.", mute_unsupported = "The configured phone voice service does not support mute.",
inventory_full = "There is no room for the ejected SIM card.", request_failed = "The phone request failed.",
operation_in_progress = "Another phone operation is already in progress.", sim_request_expired = "The SIM selection expired. Use the SIM card again.",
sim_not_owned = "That SIM card is no longer in your inventory.", phone_not_owned = "That phone is no longer in your inventory.",
+10 -3
View File
@@ -1,8 +1,10 @@
local provider_resources = {
yaca = "yaca-voice",
pma = "pma-voice",
saltychat = "saltychat",
}
local provider_aliases = {
["yaca-voice"] = "yaca",
["pma-voice"] = "pma",
salty = "saltychat",
}
@@ -22,7 +24,12 @@ function Bridge.Calls.GetProvider()
end
function Bridge.Calls.SupportsSpeaker()
return Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"
local selected = resolve_provider()
return Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")
end
function Bridge.Calls.SupportsMute()
return resolve_provider() == "yaca"
end
function Bridge.Calls.Join(channel)
@@ -37,8 +44,8 @@ function Bridge.Calls.Join(channel)
return true
end
if selected == "saltychat" then
-- SaltyChat call membership is owned by the server bridge.
if selected == "yaca" or selected == "saltychat" then
-- Yaca and SaltyChat call membership is owned by the server bridge.
return true
end
+6 -2
View File
@@ -101,6 +101,10 @@ function Bridge.Radio.Join(primary, secondary)
local selected = resolve_provider()
if selected == "yaca" then
local voice = exports["yaca-voice"]
if not voice:isEnabled() then
Bridge.Debug("error", "[sky_phone] Yaca is started but its voice system is disabled.")
return false
end
if not voice:isRadioEnabled() then
voice:enableRadio(true)
Wait(100)
@@ -150,9 +154,9 @@ end
function Bridge.Radio.SetVolume(volume)
local selected = resolve_provider()
if selected == "yaca" then
exports["yaca-voice"]:changeRadioChannelVolumeRaw(1, volume / 100)
exports["yaca-voice"]:changeRadioChannelVolumeRaw(volume / 100, 1)
if Bridge.Radio.SupportsSecondary() then
exports["yaca-voice"]:changeRadioChannelVolumeRaw(2, volume / 100)
exports["yaca-voice"]:changeRadioChannelVolumeRaw(volume / 100, 2)
end
elseif selected == "pma" then
exports["pma-voice"]:setRadioVolume(volume)
+124 -5
View File
@@ -1,8 +1,10 @@
local call_provider_resources = {
yaca = "yaca-voice",
pma = "pma-voice",
saltychat = "saltychat",
}
local call_provider_aliases = {
["yaca-voice"] = "yaca",
["pma-voice"] = "pma",
salty = "saltychat",
}
@@ -17,6 +19,26 @@ local radio_provider_aliases = {
salty = "saltychat",
}
local function yaca_is_enabled()
if GetResourceState("yaca-voice") ~= "started" then
return false
end
local success, enabled = pcall(function()
return exports["yaca-voice"]:isEnabled()
end)
if not success then
Bridge.Debug(
"error",
"[sky_phone] Yaca could not report its availability: %s",
tostring(enabled),
{ always = true }
)
return false
end
return enabled == true
end
local function resolve_call_provider()
local configured = tostring(Config.Calls.VoiceProvider or "")
local selected = call_provider_aliases[configured] or configured
@@ -51,11 +73,17 @@ function Bridge.Calls.GetProvider()
end
function Bridge.Calls.IsAvailable()
return resolve_call_provider() ~= nil
local selected = resolve_call_provider()
return selected ~= nil and (selected ~= "yaca" or yaca_is_enabled())
end
function Bridge.Calls.SupportsSpeaker()
return Bridge.Speaker.IsEnabled() and resolve_call_provider() == "saltychat"
local selected = resolve_call_provider()
return Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")
end
function Bridge.Calls.SupportsMute()
return resolve_call_provider() == "yaca"
end
function Bridge.Calls.Start(identifier, player_handles)
@@ -63,6 +91,37 @@ function Bridge.Calls.Start(identifier, player_handles)
if selected == "pma" then
return true, selected
end
if selected == "yaca" then
if not yaca_is_enabled() then
return false, selected
end
local caller_source = tonumber(player_handles[1])
local target_source = tonumber(player_handles[2])
if not caller_source or not target_source then
Bridge.Debug(
"error",
"[sky_phone] Yaca refused call %s because a player source was invalid.",
tostring(identifier),
{ always = true }
)
return false, selected
end
local success, error_message = pcall(function()
exports["yaca-voice"]:callPlayer(caller_source, target_source, true)
end)
if not success then
Bridge.Debug(
"error",
"[sky_phone] Yaca could not start call %s: %s",
tostring(identifier),
tostring(error_message),
{ always = true }
)
return false, selected
end
return true, selected
end
if selected ~= "saltychat" then
return false, nil
end
@@ -85,6 +144,36 @@ end
function Bridge.Calls.Stop(identifier, player_handles, provider)
local selected = provider or resolve_call_provider()
if selected == "yaca" then
if GetResourceState("yaca-voice") ~= "started" then
return
end
local caller_source = tonumber(player_handles[1])
local target_source = tonumber(player_handles[2])
if not caller_source or not target_source then
Bridge.Debug(
"error",
"[sky_phone] Yaca could not stop call %s because a player source was invalid.",
tostring(identifier),
{ always = true }
)
return
end
local success, error_message = pcall(function()
exports["yaca-voice"]:callPlayer(caller_source, target_source, false)
end)
if not success then
Bridge.Debug(
"error",
"[sky_phone] Yaca could not stop call %s: %s",
tostring(identifier),
tostring(error_message),
{ always = true }
)
end
return
end
if selected ~= "saltychat" or GetResourceState("saltychat") ~= "started" then
return
end
@@ -108,17 +197,47 @@ function Bridge.Calls.SetSpeaker(player_source, enabled, provider)
return false
end
local selected = provider or resolve_call_provider()
if selected ~= "saltychat" or GetResourceState("saltychat") ~= "started" then
local resource_name = call_provider_resources[selected]
if (selected ~= "yaca" and selected ~= "saltychat")
or GetResourceState(resource_name) ~= "started"
then
return false
end
local success, error_message = pcall(function()
exports.saltychat:SetPhoneSpeaker(tonumber(player_source), enabled == true)
if selected == "yaca" then
exports["yaca-voice"]:enablePhoneSpeaker(tonumber(player_source), enabled == true)
else
exports.saltychat:SetPhoneSpeaker(tonumber(player_source), enabled == true)
end
end)
if not success then
Bridge.Debug(
"error",
"[sky_phone] SaltyChat could not update the phone speaker for source %s: %s",
"[sky_phone] %s could not update the phone speaker for source %s: %s",
selected == "yaca" and "Yaca" or "SaltyChat",
tostring(player_source),
tostring(error_message),
{ always = true }
)
return false
end
return true
end
function Bridge.Calls.SetMuted(player_source, enabled, provider)
local selected = provider or resolve_call_provider()
if selected ~= "yaca" or GetResourceState("yaca-voice") ~= "started" then
return false
end
local success, error_message = pcall(function()
exports["yaca-voice"]:muteOnPhone(tonumber(player_source), enabled == true)
end)
if not success then
Bridge.Debug(
"error",
"[sky_phone] Yaca could not update the phone mute state for source %s: %s",
tostring(player_source),
tostring(error_message),
{ always = true }
+8
View File
@@ -11,6 +11,14 @@ function Bridge.Speaker.IsEnabled()
return not Config.Speaker or Config.Speaker.Enabled ~= false
end
function Bridge.Calls.SupportsMute()
return false
end
function Bridge.Calls.SetMuted()
return false
end
function Bridge.Radio.SupportsSpeaker()
return false
end
+1
View File
@@ -245,6 +245,7 @@ local server_callbacks = {
"calls:dial",
"calls:answer",
"calls:set-speaker",
"calls:set-muted",
"calls:decline",
"calls:hangup",
"calls:block",
+40 -2
View File
@@ -113,9 +113,12 @@ end
local function send_state(call, source, state, channel)
local outgoing = source == call.caller_source
local speaker_supported = Bridge.Speaker.IsEnabled() and (
call.voice_provider == "saltychat"
call.voice_provider == "yaca"
or call.voice_provider == "saltychat"
or (not call.voice_provider and Bridge.Calls.SupportsSpeaker())
)
local mute_supported = call.voice_provider == "yaca"
or (not call.voice_provider and Bridge.Calls.SupportsMute())
local payload = {
id = call.id,
state = state,
@@ -126,6 +129,8 @@ local function send_state(call, source, state, channel)
channel = channel,
speakerEnabled = call.speakers and call.speakers[source] == true or false,
speakerSupported = speaker_supported,
muted = call.muted and call.muted[source] == true or false,
muteSupported = mute_supported,
}
if call.payphone and outgoing then
payload.elapsedSeconds = call.payphone.elapsed_seconds or 0
@@ -252,6 +257,7 @@ local function finish_call(call, status)
end
Bridge.Calls.Stop(call.id, player_handles, call.voice_provider)
call.speakers = {}
call.muted = {}
call.voice_started = false
end
local ended_at = os.time()
@@ -1247,6 +1253,7 @@ Bridge.Callbacks.Register("sky_phone:calls:answer", function(source, data)
call.voice_provider = voice_provider
call.voice_started = true
call.speakers = {}
call.muted = {}
call.answered_at = os.time()
call.channel = next_voice_channel
next_voice_channel = next_voice_channel + 1
@@ -1281,7 +1288,7 @@ Bridge.Callbacks.Register("sky_phone:calls:set-speaker", function(source, data)
if not call or call.id ~= data.id or not call.answered_at or call.ended or not call.voice_started then
return { success = false, error = "call_not_found" }
end
if call.voice_provider ~= "saltychat" then
if call.voice_provider ~= "yaca" and call.voice_provider ~= "saltychat" then
return { success = false, error = "speaker_unsupported" }
end
if not Bridge.Speaker.IsEnabled() then
@@ -1302,6 +1309,37 @@ Bridge.Callbacks.Register("sky_phone:calls:set-speaker", function(source, data)
}
end)
Bridge.Callbacks.Register("sky_phone:calls:set-muted", function(source, data)
if type(data) ~= "table" or type(data.id) ~= "string" or type(data.enabled) ~= "boolean" then
return { success = false, error = "invalid_request" }
end
if not SkyPhone.AllowOperation(source, "call_mute", 30, 60) then
return { success = false, error = "rate_limited" }
end
local call_id = active_by_source[source]
local call = call_id and calls[call_id] or nil
if not call or call.id ~= data.id or not call.answered_at or call.ended or not call.voice_started then
return { success = false, error = "call_not_found" }
end
if call.voice_provider ~= "yaca" then
return { success = false, error = "mute_unsupported" }
end
if not Bridge.Calls.SetMuted(source, data.enabled, call.voice_provider) then
return { success = false, error = "voice_unavailable" }
end
call.muted[source] = data.enabled
send_state(call, source, "connected", call.channel)
return {
success = true,
data = {
muted = data.enabled,
muteSupported = true,
},
}
end)
Bridge.Callbacks.Register("sky_phone:calls:decline", function(source, data)
local call = type(data) == "table" and calls[data.id] or nil
if not call or call.callee_source ~= source or call.answered_at or call.rerouting then