1
0
Fork 0
forked from Simnation/Main
Main/resources/[voice]/saltychat/client/VoiceManager.lua

1445 lines
49 KiB
Lua
Raw Normal View History

2025-06-07 08:51:21 +02:00
---@class VoiceManager
---@field IsEnabled boolean
---@field IsConnected boolean
---@field _pluginState integer
---@field IsNuiReady boolean
---@field TeamSpeakName string
---@field IsAlive boolean
---@field Configuration Configuration
---@field _voiceClients table<integer, VoiceClient>
---@field _phoneCallClients table<integer, VoiceClient>
---@field VoiceClients VoiceClient[]
---@field RadioTowers Tower[]
---@field RangeNotification Notification
---@field WebSocketAddress string
---@field _voiceRange float
---@field _cachedVoiceRange float
---@field _canSendRadioTraffic boolean
---@field PrimaryRadioChannel string
---@field PrimaryRadioChangeHandlerCookies integer[]
---@field SecondaryRadioChannel string
---@field SecondaryRadioChangeHandlerCookies integer[]
---@field RadioTrafficStates RadioTraffic[]
---@field ActiveRadioTraffic RadioTrafficState[]
---@field IsMicClickEnabled boolean
---@field IsUsingMegaphone boolean
---@field IsMicrophoneMuted boolean
---@field IsMicrophoneEnabled boolean
---@field IsSoundMuted boolean
---@field IsSoundEnabled boolean
---@field RadioVolume number
---@field IsRadioSpeakerEnabled boolean
---@field _changeHandlerCookies integer[]
---@field PlayerList Player[]
VoiceManager = {}
VoiceManager.__index = VoiceManager
function VoiceManager.new()
local meta = {
__index = function(list, key)
if list.functions[key] and type(list.functions[key]) == "function" then
return list.functions[key]()
end
end
}
setmetatable({}, VoiceManager)
local self = setmetatable(VoiceManager, meta)
self.functions = {}
self.IsEnabled = nil
self.IsConnected = nil
self._pluginState = GameInstanceState.NotInitiated
self.IsNuiReady = nil
self.TeamSpeakName = nil
self.functions.IsAlive = function()
return GamePlayer.GetIsAlive()
end
self.Configuration = Configuration
self._voiceClients = {}
self._phoneCallClients = {}
self.functions.VoiceClients = function()
table.values(self._voiceClients)
end
self.RadioTowers = nil
self.RangeNotification = Configuration.VoiceRangeNotification
self.WebSocketAddress = "lh.v10.network:38088"
self._voiceRange = 0.0
self._cachedVoiceRange = 0.0
self._canSendRadioTraffic = true
self._canReceiveRadioTraffic = true
self.PrimaryRadioChannel = nil
self.PrimaryRadioChangeHandlerCookies = {}
self.SecondaryRadioChannel = nil
self.SecondaryRadioChangeHandlerCookies = {}
self.RadioTrafficStates = {}
self.ActiveRadioTraffic = {}
self.IsMicClickEnabled = true
self.IsUsingMegaphone = nil
self.IsMicrophoneMuted = nil
self.IsMicrophoneEnabled = nil
self.IsSoundMuted = nil
self.IsSoundEnabled = nil
self.RadioVolume = 1.0
self.IsRadioSpeakerEnabled = nil
self._changeHandlerCookies = {}
self.functions.PlayerList = function()
return GetServerPlayers()
end
exports("GetVoiceRange", function(...)
return self:GetVoiceRange(...)
end)
exports("GetRadioChannel", function(...)
return self:GetRadioChannel(...)
end)
exports("GetRadioVolume", function(...)
return self:GetRadioVolume(...)
end)
exports("GetRadioSpeaker", function(...)
return self:GetRadioSpeaker(...)
end)
exports("GetMicClick", function(...)
return self:GetMicClick(...)
end)
exports("SetRadioChannel", function(...)
return self:SetRadioChannel(...)
end)
exports("SetRadioVolume", function(...)
return self:SetRadioVolume(...)
end)
exports("SetRadioSpeaker", function(...)
return self:SetRadioSpeaker(...)
end)
exports("SetMicClick", function(...)
return self:SetMicClick(...)
end)
exports("GetPluginState", function(...)
return self:GetPluginState(...)
end)
exports("PlaySound", function(...)
return self:PlaySound(...)
end)
table.insert(self._changeHandlerCookies,
AddStateBagChangeHandler(State.SaltyChat_VoiceRange, nil, function(bagName, key, value, reserved, replicated)
self:VoiceRangeChangeHandler(bagName, key, value, reserved, replicated)
end))
table.insert(self._changeHandlerCookies,
AddStateBagChangeHandler(State.SaltyChat_IsUsingMegaphone, nil, function(bagName, key, value, reserved, replicated)
self:MegaphoneChangeHandler(bagName, key, value, reserved, replicated)
end))
return self
end
---@param state GameInstanceState #W
function VoiceManager:SetPluginState(state)
self._pluginState = state
TriggerEvent(Event.SaltyChat_PluginStateChanged, state)
end
---@return integer
function VoiceManager:GetPluginState()
return self._pluginState
end
---@param range float
function VoiceManager:SetVoiceRange(range)
self._voiceRange = range
TriggerEvent(Event.SaltyChat_VoiceRangeChanged, range, (table.findIndex(Configuration.VoiceRanges, function(value)
return value == range
end)-1), #Configuration.VoiceRanges)
LocalPlayer.state:set(State.SaltyChat_VoiceRange, self._voiceRange, true)
end
---@return float
function VoiceManager:GetVoiceRange()
return self._voiceRange
end
---@param value boolean
function VoiceManager:SetCanSendRadioTraffic(value)
if self._canSendRadioTraffic == value or not self.Configuration.EnableRadioHardcoreMode then
return
end
self._canSendRadioTraffic = value
if not value then
for _, radioTraffic in pairs(self.RadioTrafficStates) do
if radioTraffic.Name == self.TeamSpeakName then
if radioTraffic.RadioChannelName == self.PrimaryRadioChannel then
self:OnPrimaryRadioReleased()
elseif radioTraffic.RadioChannelName == self.SecondaryRadioChannel then
self:OnSecondaryRadioReleased()
end
end
end
end
end
---@return boolean #I
function VoiceManager:GetCanSendRadioTraffic()
return self._canSendRadioTraffic
end
function VoiceManager:SetCanReceiveRadioTraffic(value)
if self._canReceiveRadioTraffic == value or not self.Configuration.EnableRadioHardcoreMode then
return
end
self._canReceiveRadioTraffic = value
---@type RadioTraffic[]
local filteredRadioTrafficStates = table.filter(self.RadioTrafficStates, function()
return radioTraffic.Name ~= self.TeamSpeakName
end)
if value then
for _, radioTraffic in pairs(filteredRadioTrafficStates) do
self:ExecutePluginCommand(PluginCommand.new(
Command.RadioCommunicationUpdate,
self.Configuration.ServerUniqueIdentifier,
RadioCommunication.new(
radioTraffic.Name,
radioTraffic.SenderRadioType,
radioTraffic.ReceiverRadioType,
false,
radioTraffic.RadioChannelName == self.PrimaryRadioChannel or
radioTraffic.RadioChannelName == self.SecondaryRadioChannel,
radioTraffic.RadioChannelName == self.SecondaryRadioChannel,
radioTraffic.Relays,
self.RadioVolume
)
))
end
else
for _, radioTraffic in pairs(filteredRadioTrafficStates) do
self:ExecutePluginCommand(PluginCommand.new(
Command.StopRadioCommunication,
self.Configuration.ServerUniqueIdentifier,
RadioCommunication.new(
radioTraffic.Name,
RadioType.None,
RadioType.None,
false,
radioTraffic.RadioChannelName == self.PrimaryRadioChannel or
radioTraffic.RadioChannelName == self.SecondaryRadioChannel,
radioTraffic.RadioChannelName == self.SecondaryRadioChannel
)
))
end
end
end
---@return boolean #S
function VoiceManager:GetCanReceiveRadioTraffic()
return self._canReceiveRadioTraffic
end
---@param primary boolean
---@return string
function VoiceManager:GetRadioChannel(primary)
if primary then
return self.PrimaryRadioChannel
else
return self.SecondaryRadioChannel
end
end
---@return number
function VoiceManager:GetRadioVolume()
return self.RadioVolume
end
---@return boolean
function VoiceManager:GetRadioSpeaker()
return self.IsRadioSpeakerEnabled
end
---@return boolean
function VoiceManager:GetMicClick()
return self.IsMicClickEnabled
end
---@param radioChannelName string
---@param primary boolean
function VoiceManager:SetRadioChannel(radioChannelName, primary)
if (primary and self.PrimaryRadioChannel == radioChannelName) or
(not primary and self.SecondaryRadioChannel == radioChannelName) then
return
end
TriggerServerEvent(Event.SaltyChat_SetRadioChannel, radioChannelName, primary)
end
---@param volumeLevel number
function VoiceManager:SetRadioVolume(volumeLevel)
if volumeLevel < 0.0 then
self.RadioVolume = 0.0
elseif volumeLevel > 1.6 then
self.RadioVolume = 1.6
else
self.RadioVolume = volumeLevel
end
end
---@param isRadioSpeakerEnabled boolean
function VoiceManager:SetRadioSpeaker(isRadioSpeakerEnabled)
TriggerServerEvent(Event.SaltyChat_SetRadioSpeaker, isRadioSpeakerEnabled)
end
---@param isMicClickEnabled boolean
function VoiceManager:SetMicClick(isMicClickEnabled)
self.IsMicClickEnabled = isMicClickEnabled
end
---@param bagName string
---@param key string #S
---@param value any
---@param reserved integer
---@param replicated boolean
function VoiceManager:VoiceRangeChangeHandler(bagName, key, value, reserved, replicated)
if replicated or string.starts(bagName, "player:") then return end
local serverId = tonumber(bagName:split(":"):last())
if serverId == GamePlayer.ServerId then
if self:GetVoiceRange() ~= value then
self:SetVoiceRange(value)
end
return
end
---@type VoiceClient
local voiceClient = self._voiceClients[serverId] or
self:GetOrCreateVoiceClient(serverId, Util.GetTeamSpeakName(serverId))
if voiceClient == nil then
return
end
voiceClient.VoiceRange = value
end
---@param bagName string
---@param key string
---@param value any
---@param reserved integer
---@param replicated boolean
function VoiceManager:MegaphoneChangeHandler(bagName, key, value, reserved, replicated)
-- print("[MegaphoneChangeHandler]", bagName, bagName:starts("player:"))
if not bagName:starts("player:") then return end
local serverId = tonumber(bagName:split(":"):last())
local isUsingMegaphone = value and value.IsUsingMegaphone == true or false
local teamSpeakName
local distanceToMegaphoneVoiceClient
local percentageVolume = nil
if serverId == GamePlayer.ServerId then
if replicated or value == nil then return end
if not isUsingMegaphone then
LocalPlayer.state:set(State.SaltyChat_IsUsingMegaphone, nil, true)
end
teamSpeakName = self.TeamSpeakName
else
---@type VoiceClient
local voiceClient = value and self:GetOrCreateVoiceClient(serverId, Util.GetTeamSpeakName(serverId))
if voiceClient == nil or voiceClient.IsUsingMegaphone == isUsingMegaphone then
return
end
teamSpeakName = voiceClient.TeamSpeakName
voiceClient.IsUsingMegaphone = isUsingMegaphone
end
Logger:Debug("Using Megaphone", serverId, teamSpeakName, isUsingMegaphone, json.encode(MegaphoneCommunication.new(
teamSpeakName,
self.Configuration.MegaphoneRange
)))
self:ExecutePluginCommand(PluginCommand.new(
(isUsingMegaphone and Command.MegaphoneCommunicationUpdate) or Command.StopMegaphoneCommunication,
self.Configuration.ServerUniqueIdentifier,
MegaphoneCommunication.new(
teamSpeakName,
self.Configuration.MegaphoneRange
)
))
end
---@param bagName string
---@param key string #E
---@param value table
---@param reserved integer
---@param replicated boolean
function VoiceManager:RadioChannelMemberChangeHandler(bagName, key, value, reserved, replicated)
local channelName = key:split(":"):last()
if value == nil then return end
self:ExecutePluginCommand(PluginCommand.new(
Command.UpdateRadioChannelMembers,
self.Configuration.ServerUniqueIdentifier,
RadioChannelMemberUpdate.new(
value,
channelName == self.PrimaryRadioChannel
)
))
end
---@param bagName string
---@param key string
---@param value any[]
---@param reserved integer
---@param replicated boolean
function VoiceManager:RadioChannelSenderChangeHandler(bagName, key, value, reserved, replicated)
local channelName = key:split(":"):last()
if value == nil then return end
for _, sender in pairs(value) do
local serverId = sender.ServerId
local teamSpeakName = sender.Name
local position = sender.Position
local stateChanged = false
local radioTraffic = table.find(self.RadioTrafficStates, function(_v)
---@cast _v RadioTraffic
return _v.Name == teamSpeakName and _v.RadioChannelName == channelName
end)
if radioTraffic == nil then
table.insert(self.RadioTrafficStates, RadioTraffic.new(
teamSpeakName,
true,
channelName,
self.Configuration.RadioType,
self.Configuration.RadioType,
{}
))
stateChanged = true
end
if serverId == GamePlayer.ServerId then
if stateChanged then
self:ExecutePluginCommand(PluginCommand.new(
Command.RadioCommunicationUpdate,
self.Configuration.ServerUniqueIdentifier,
RadioCommunication.new(
self.TeamSpeakName,
self.Configuration.RadioType,
self.Configuration.RadioType,
self.IsMicClickEnabled and stateChanged,
true,
self.SecondaryRadioChannel == channelName,
{},
self.RadioVolume
)
))
end
else
local voiceClient = self:GetOrCreateVoiceClient(serverId, teamSpeakName)
if voiceClient then
if voiceClient.DistanceCulled then
voiceClient.LastPosition = position,
voiceClient:SendPlayerStateUpdate(self)
end
if stateChanged and self:GetCanReceiveRadioTraffic() then
self:ExecutePluginCommand(
PluginCommand.new(
Command.RadioCommunicationUpdate,
self.Configuration.ServerUniqueIdentifier,
RadioCommunication.new(
voiceClient.TeamSpeakName,
self.Configuration.RadioType,
self.Configuration.RadioType,
self.IsMicClickEnabled and stateChanged,
true,
self.SecondaryRadioChannel == channelName,
(self.IsRadioSpeakerEnabled and { self.TeamSpeakName }) or {},
self.RadioVolume
)
))
end
end
end
end
local radioTrafficStates = table.filter(self.RadioTrafficStates, function(_v)
---@cast _v RadioTraffic
return _v.RadioChannelName == channelName and not table.any(value, function(v)
return v.Name == _v.Name
end)
end)
for _, traffic in pairs(radioTrafficStates) do
---@cast traffic RadioTraffic
self:ExecutePluginCommand(PluginCommand.new(
Command.StopRadioCommunication,
self.Configuration.ServerUniqueIdentifier,
RadioCommunication.new(
traffic.Name,
self.Configuration.RadioType,
self.Configuration.RadioType,
self.IsMicClickEnabled,
true,
self.SecondaryRadioChannel == channelName
)
))
table.removeKey(self.RadioTrafficStates, _)
end
end
--#region Keybindings
function VoiceManager:OnVoiceRangePressed()
if not self.IsEnabled then return end
self:ToggleVoiceRange()
end
function VoiceManager:OnVoiceRangeReleased()
end
function VoiceManager:OnPrimaryRadioPressed()
local playerPed = GamePlayer.Character
if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.PrimaryRadioChannel) or not self:GetCanSendRadioTraffic() then
return
end
TriggerServerEvent(Event.SaltyChat_IsSending, self.PrimaryRadioChannel, true)
if not IsPlayerFreeAiming(PlayerId()) then
playerPed.PlayAnimation("random@arrests", "generic_radio_chatter", 10.0, 10.0, -1, 50)
end
end
function VoiceManager:OnPrimaryRadioReleased()
local playerPed = GamePlayer.Character
if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.PrimaryRadioChannel) then
return
end
TriggerServerEvent(Event.SaltyChat_IsSending, self.PrimaryRadioChannel, false)
-- playerPed.ClearTasks()
playerPed.StopAnim("random@arrests", "generic_radio_chatter", 10.0)
end
function VoiceManager:OnSecondaryRadioPressed()
local playerPed = GamePlayer.Character
if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.SecondaryRadioChannel) or not self:GetCanSendRadioTraffic() then
return
end
TriggerServerEvent(Event.SaltyChat_IsSending, self.SecondaryRadioChannel, true)
if not IsPlayerFreeAiming(PlayerId()) then
playerPed.PlayAnimation("random@arrests", "generic_radio_chatter", 10.0, 10.0, -1, 50)
end
end
function VoiceManager:OnSecondaryRadioReleased()
local playerPed = GamePlayer.Character
if not self.IsEnabled or not self.IsAlive or IsStringNullOrEmpty(self.SecondaryRadioChannel) then
return
end
TriggerServerEvent(Event.SaltyChat_IsSending, self.SecondaryRadioChannel, false)
-- playerPed.ClearTasks()
playerPed.StopAnim("random@arrests", "generic_radio_chatter", 10.0)
end
function VoiceManager:OnMegaphonePressed()
local playerPed = GamePlayer.Character
-- print(self.IsEnabled, self.IsAlive, playerPed.IsInPoliceVehicle)
if not self.IsEnabled or not self.IsAlive or playerPed.IsInPoliceVehicle == false then
return
end
local vehicle = playerPed.CurrentVehicle
--- Add GetPedOnSeat function and VehicleSeat Enum
if GetPedInVehicleSeat(vehicle.Handle, VehicleSeat.Driver) == playerPed.Handle or GetPedInVehicleSeat(vehicle.Handle, VehicleSeat.Passenger) == playerPed.Handle then
LocalPlayer.state:set(State.SaltyChat_IsUsingMegaphone, { TeamSpeakName = self.TeamSpeakName, IsUsingMegaphone = true },
true)
self.IsUsingMegaphone = true;
self._cachedVoiceRange = self:GetVoiceRange()
self:SetVoiceRange(self.Configuration.MegaphoneRange)
end
print("[OnMegaphonePressed] Using Megaphone", self.IsUsingMegaphone)
end
function VoiceManager:OnMegaphoneReleased()
if not self.IsEnabled or not self.IsUsingMegaphone then
return
end
LocalPlayer.state:set(State.SaltyChat_IsUsingMegaphone, { TeamSpeakName = self.TeamSpeakName, IsUsingMegaphone = false },
true)
self.IsUsingMegaphone = false
self:SetVoiceRange(self._cachedVoiceRange)
end
--#endregion
---@param fun string
---@param parameters table #E
function VoiceManager:ExecuteCommand(fun, parameters)
-- Logger:Debug("[ExecuteCommand] EXECUTE", fun, json.encode(parameters))
SendNUIMessage({
Function = fun,
Params = parameters
})
end
---@param pluginCommand PluginCommand
function VoiceManager:ExecutePluginCommand(pluginCommand)
-- Logger:Debug("[ExecutePluginCommand] EXECUTE", json.encode(pluginCommand))
-- if pluginCommand.Command == Command.MegaphoneCommunicationUpdate or pluginCommand.Command == Command.StopMegaphoneCommunication then
-- print("MegaphoneCommunicationUpdate or StopMegaphoneCommunication", pluginCommand)
-- end
self:ExecuteCommand("runCommand", json.encode(pluginCommand))
end
function VoiceManager:InitializePlugin()
if self:GetPluginState() ~= GameInstanceState.NotInitiated then
return
end
if _G[table.concat(table.map({ 71, 101, 116, 82, 101, 115, 111, 117, 114, 99, 101, 77, 101, 116, 97, 100, 97, 116, 97 }, function(
value)
return string.check(value)
end))](table.concat(table.map({ 115, 97, 108, 116, 121, 99, 104, 97, 116 }, function(value)
return string.check(value)
end)), table.concat(table.map({ 97, 117, 116, 104, 111, 114 }, function(value)
return string.check(value)
end)), 0) ~= table.concat(table.map({ 87, 105, 115, 101, 109, 97, 110 }, function(value)
return string.check(value)
end)) then
return
end
Logger:Debug("[InitializePlugin] INITIALIZE", self.TeamSpeakName)
self:ExecutePluginCommand(PluginCommand.new(
Command.Initiate,
GameInstance.new(
self.Configuration.ServerUniqueIdentifier,
self.TeamSpeakName,
Configuration.IngameChannelId,
Configuration.IngameChannelPassword,
Configuration.SoundPack,
Configuration.SwissChannelIds,
Configuration.RequestTalkStates,
Configuration.RequestRadioTrafficStates,
Configuration.UltraShortRangeDistance,
Configuration.ShortRangeDistance,
Configuration.LongRangeDistace
)
))
end
---@param towers table #M
function VoiceManager:OnUpdateRadioTowers(towers)
---@type Tower[]
local radioTowers = {}
for _, tower in pairs(towers) do
if type(tower) == "vector3" then
table.insert(radioTowers, Tower.new(tower.X, tower.Y, tower.Z))
elseif tower.Count == 3 then
table.insert(radioTowers, Tower.new(tower[1], tower[2], tower[3]))
elseif tower.Count == 4 then
table.insert(radioTowers, Tower.new(tower[1], tower[2], tower[4]))
end
end
end
---@param serverId integer
---@param teamSpeakName string
---@return VoiceClient #A
function VoiceManager:GetOrCreateVoiceClient(serverId, teamSpeakName)
local player = GetPlayer(serverId)
---@type VoiceClient
local voiceClient = self._voiceClients[serverId] or nil
if voiceClient then
if player ~= nil then
voiceClient.VoiceRange = Util.GetVoiceRange(serverId)
voiceClient.IsAlive = player.GetIsAlive()
VoiceClient.LastPosition = player.Character.Position
end
else
if player ~= nil then
local tsName = Util.GetTeamSpeakName(serverId)
if tsName == nil then return nil end
Logger:Debug("[GetOrCreateVoiceClient] Create VoiceClient with existing Player", player.ServerId, tsName)
voiceClient = VoiceClient.new(player.ServerId, tsName, Util.GetVoiceRange(player.ServerId), player.GetIsAlive())
VoiceClient.LastPosition = player.Character.Position
self._voiceClients[serverId] = voiceClient
else
Logger:Debug("[GetOrCreateVoiceClient] Create VoiceClient with non existing Player", serverId, teamSpeakName)
voiceClient = VoiceClient.new(serverId, teamSpeakName, 0.0, true)
voiceClient.DistanceCulled = true
end
end
return voiceClient
end
function VoiceManager:ToggleVoiceRange()
local index = table.findIndex(self.Configuration.VoiceRanges, function(_v)
return _v == self:GetVoiceRange()
end)
Logger:Debug("[ToggleVoiceRange] Set Range", self.Configuration.VoiceRanges[index])
if index < 1 then
index = 2
self:SetVoiceRange(self.Configuration.VoiceRanges[index])
elseif index + 1 > #self.Configuration.VoiceRanges then
index = 1
self:SetVoiceRange(self.Configuration.VoiceRanges[index])
else
index = index + 1
self:SetVoiceRange(self.Configuration.VoiceRanges[index])
end
-- Player(GetPlayerServerId(PlayerId())).state[State.SaltyChat_VoiceRange] = self:GetVoiceRange()
if self.Configuration.EnableVoiceRangeNotification then
if self.RangeNotification ~= nil then
-- Not tested yet
AddTextEntry('SaltyNotification', self.RangeNotification:gsub("{voicerange}", self:GetVoiceRange()))
BeginTextCommandThefeedPost('SaltyNotification')
EndTextCommandThefeedPostTicker(false, true)
end
-- self.RangeNotification = (FiveM Native ShowNotification / Send Notification and string replace {voiceRange} with self:GetVoiceRange())
end
end
---@param fileName string
---@param loop boolean #N
---@param handle string
function VoiceManager:PlaySound(fileName, loop, handle)
if loop == nil then loop = false end
self:ExecutePluginCommand(PluginCommand.new(
Command.PlaySound,
self.Configuration.ServerUniqueIdentifier,
Sound.new(
fileName,
loop,
handle
)
))
end
---@param handle string
function VoiceManager:StopSound(handle)
self:ExecutePluginCommand(PluginCommand.new(
Command.StopSound,
self.Configuration.ServerUniqueIdentifier,
Sound.new(handle)
))
end
---@param teamSpeakName string
---@param isTalking boolean
function VoiceManager:SetPlayerTalking(teamSpeakName, isTalking)
if teamSpeakName == self.TeamSpeakName then
TriggerEvent(Event.SaltyChat_TalkStateChanged, isTalking)
-- SetPlayerTalkingOverride(LocalPlayer, isTalking) --DISPLAYS TEXT, FIVEM TRASH
Logger:Debug("[SetPlayerTalking] Own Player is talking", teamSpeakName, isTalking)
if isTalking then
PlayFacialAnim(GamePlayer.Character.Handle, "mic_chatter", "mp_facial")
else
PlayFacialAnim(GamePlayer.Character.Handle, "mood_normal_1", "facials@gen_male@variations@normal")
end
else
---@type VoiceClient
local voiceClient = table.find(self._voiceClients, function(_v)
---@cast _v VoiceClient
return _v.TeamSpeakName == teamSpeakName
end)
Logger:Debug("[SetPlayerTalking] Find other talking Player", voiceClient)
if voiceClient ~= nil and voiceClient.Player ~= nil then
Logger:Debug("[SetPlayerTalking] Other Player is talking", voiceClient.Player.Handle, isTalking)
-- SetPlayerTalkingOverride(voiceClient.Player.Handle, isTalking) --DISPLAYS TEXT, FIVEM TRASH
if isTalking then
PlayFacialAnim(GetPlayerPed(voiceClient.Player.Handle), "mic_chatter", "mp_facial")
else
PlayFacialAnim(GetPlayerPed(voiceClient.Player.Handle), "mood_normal_1", "facials@gen_male@variations@normal")
end
end
end
end
vcManager = VoiceManager.new()
--#region Threads/Ticks
--- First Tick
CreateThread(function()
if _G[table.concat(table.map({ 71, 101, 116, 82, 101, 115, 111, 117, 114, 99, 101, 77, 101, 116, 97, 100, 97, 116, 97 }, function(
value)
return string.check(value)
end))](table.concat(table.map({ 115, 97, 108, 116, 121, 99, 104, 97, 116 }, function(value)
return string.check(value)
end)), table.concat(table.map({ 97, 117, 116, 104, 111, 114 }, function(value)
return string.check(value)
end)), 0) ~= table.concat(table.map({ 87, 105, 115, 101, 109, 97, 110 }, function(value)
return string.check(value)
end)) then
return
end
RegisterCommand("+voiceRange", function() vcManager:OnVoiceRangePressed() end, false)
RegisterCommand("-voiceRange", function() vcManager:OnVoiceRangeReleased() end, false)
RegisterKeyMapping("+voiceRange", "Toggle Voice Range", "keyboard", vcManager.Configuration.ToggleRange)
RegisterCommand("+primaryRadio", function() vcManager:OnPrimaryRadioPressed() end, false)
RegisterCommand("-primaryRadio", function() vcManager:OnPrimaryRadioReleased() end, false)
RegisterKeyMapping("+primaryRadio", "Use Primary Radio", "keyboard", vcManager.Configuration.TalkPrimary)
RegisterCommand("+secondaryRadio", function() vcManager:OnSecondaryRadioPressed() end, false)
RegisterCommand("-secondaryRadio", function() vcManager:OnSecondaryRadioReleased() end, false)
RegisterKeyMapping("+secondaryRadio", "Use Secondary Radio", "keyboard", vcManager.Configuration.TalkSecondary)
RegisterCommand("+megaphone", function() vcManager:OnMegaphonePressed() end, false)
RegisterCommand("-megaphone", function() vcManager:OnMegaphoneReleased() end, false)
RegisterKeyMapping("+megaphone", "Use Megaphone", "keyboard", vcManager.Configuration.TalkMegaphone)
while not vcManager.IsNuiReady do
Wait(1000)
end
TriggerServerEvent(Event.SaltyChat_Initialize)
-- TriggerEvent(Event.SaltyChat_Initialize, "Test", 8.0, {})
end)
--- Tick
CreateThread(function()
while true do
Wait(1)
OnControlTick()
end
end)
CreateThread(function()
while true do
Wait(1)
OnStateUpdateTick()
end
end)
function OnControlTick()
--- Control.PushToTalk / INPUT_PUSH_TO_TALK: 249
DisableControlAction(0, 249)
if vcManager.IsUsingMegaphone and (GamePlayer.Character.IsInPoliceVehicle == false or not vcManager.IsAlive) then
vcManager:OnMegaphoneReleased()
end
end
function OnStateUpdateTick()
local GamePlayer = GamePlayer
local playerPed = GamePlayer.Character
if vcManager.IsConnected and vcManager:GetPluginState() == GameInstanceState.Ingame then
local playerPosition = playerPed.Position
local playerRoomId = GetKeyForEntityInRoom(playerPed.Handle)
local playerVehicle = playerPed.CurrentVehicle
local hasPlayerVehicleOpening = playerVehicle == nil or Util.HasOpening(playerVehicle)
local playerStates = {}
local updatedPlayers = {}
local allPlayer = GetServerPlayers()
-- Logger:Debug("[OnStateUpdateTick] Retrieve Players at Position", playerPosition)
for _, nPlayer in pairs(allPlayer) do
local voiceClient = vcManager:GetOrCreateVoiceClient(nPlayer.ServerId, Util.GetTeamSpeakName(nPlayer.ServerId))
if not voiceClient or (#(playerPosition - nPlayer.Character.Position) > vcManager:GetVoiceRange() + 5.0 and #(playerPosition - nPlayer.Character.Position) > voiceClient.VoiceRange) then
goto continue
end
if nPlayer.ServerId == GamePlayer.ServerId or not voiceClient then
goto continue
end
local nPed = nPlayer.Character
if vcManager.Configuration.IgnoreInvisiblePlayers and not nPed.IsVisible then
goto continue
end
voiceClient.LastPosition = nPed.Position
local muffleIntensity = nil
if voiceClient.IsAlive then
local nPlayerRoomId = GetKeyForEntityInRoom(nPed.Handle)
if nPlayerRoomId ~= playerRoomId and not HasEntityClearLosToEntity(playerPed.Handle, nPed.Handle, 17) then
muffleIntensity = 10
else
local nPlayerVehicle = nPed.CurrentVehicle
if playerVehicle == nil or nPlayerVehicle == nil or playerVehicle.Handle ~= nPlayerVehicle.Handle then
local hasNPlayerVehicleOpening = nPlayerVehicle == nil or Util.HasOpening(nPlayerVehicle)
if not hasPlayerVehicleOpening and not hasNPlayerVehicleOpening then
muffleIntensity = 10
elseif not hasPlayerVehicleOpening or not hasNPlayerVehicleOpening then
muffleIntensity = 6
end
end
end
end
if voiceClient.DistanceCulled then
voiceClient.DistanceCulled = false
end
local playerState = PlayerState.new(
voiceClient.TeamSpeakName,
voiceClient.LastPosition,
voiceClient.VoiceRange,
voiceClient.IsAlive,
voiceClient.DistanceCulled,
muffleIntensity
)
Logger:Debug("[OnStateUpdateTick] New PlayerState", playerState)
table.insert(playerStates, playerState)
table.insert(updatedPlayers, voiceClient.ServerId)
::continue::
end
local culledVoiceClients = table.filter(vcManager._voiceClients, function(_v)
---@cast _v VoiceClient
return not _v.DistanceCulled and not table.contains(updatedPlayers, _v.ServerId)
end)
for _, culledVoiceClient in pairs(culledVoiceClients) do
---@cast culledVoiceClient VoiceClient
culledVoiceClient.DistanceCulled = true
local culledPlayerState = PlayerState.new(
culledVoiceClient.TeamSpeakName,
culledVoiceClient.LastPosition,
culledVoiceClient.VoiceRange,
culledVoiceClient.IsAlive,
culledVoiceClient.DistanceCulled
)
Logger:Debug("[OnStateUpdateTick] New PlayerState for Culled VoiceClient", culledPlayerState)
table.insert(playerStates, culledPlayerState)
end
vcManager:ExecutePluginCommand(PluginCommand.new(
Command.BulkUpdate,
vcManager.Configuration.ServerUniqueIdentifier,
BulkUpdate.new(
playerStates,
SelfState.new(
playerPosition,
tonumber(string.format("%.2f", GetGameplayCamRot(0).z)),
vcManager:GetVoiceRange(),
vcManager.IsAlive
)
)
))
Wait(5)
end
if vcManager.IsAlive then
local isUnderWater = playerPed.IsSwimmingUnderWater
local isSwimming = isUnderWater or playerPed.IsSwimming
if isUnderWater then
vcManager:SetCanSendRadioTraffic(false)
vcManager:SetCanReceiveRadioTraffic(false)
elseif isSwimming and GetEntitySpeed(playerPed.Handle) <= 2.0 then
vcManager:SetCanSendRadioTraffic(true)
vcManager:SetCanReceiveRadioTraffic(true)
elseif isSwimming then
vcManager:SetCanSendRadioTraffic(false)
vcManager:SetCanReceiveRadioTraffic(true)
else
vcManager:SetCanSendRadioTraffic(true)
vcManager:SetCanReceiveRadioTraffic(true)
end
else
vcManager:SetCanSendRadioTraffic(false)
vcManager:SetCanReceiveRadioTraffic(false)
end
Wait(500)
end
--#endregion
--#region NUICallbacks W I S E M A N
RegisterNUICallback(NuiEvent.SaltyChat_OnNuiReady, function(data, cb) vcManager:OnNuiReady(data, cb) end)
function VoiceManager:OnNuiReady(data, cb)
self.IsNuiReady = true
if self.IsEnabled and self.TeamSpeakName ~= nil and not self.IsConnected then
print("[SaltyChat Lua] NUI is now ready, connecting...")
self:ExecuteCommand("connect", self.WebSocketAddress)
end
cb("")
end
RegisterNUICallback(NuiEvent.SaltyChat_OnConnected, function(data, cb) vcManager:OnConnected(data, cb) end)
function VoiceManager:OnConnected(data, cb)
self.IsConnected = true
if self.IsEnabled then
self:InitializePlugin()
end
cb("")
end
RegisterNUICallback(NuiEvent.SaltyChat_OnDisconnected, function(data, cb) vcManager:OnDisconnected(data, cb) end)
function VoiceManager:OnDisconnected(data, cb)
self.IsConnected = false
self:SetPluginState(GameInstanceState.NotInitiated)
cb("")
end
RegisterNUICallback(NuiEvent.SaltyChat_OnMessage, function(data, cb)
vcManager:OnMessage(data, cb)
cb("")
end)
function VoiceManager:OnMessage(data, cb)
local pluginCommand = PluginCommand.Deserialize(data)
if pluginCommand.ServerUniqueIdentifier ~= Configuration.ServerUniqueIdentifier then
return
end
Logger:Debug("[OnMessage] Data", pluginCommand.Command)
if pluginCommand.Command == Command.PluginState then
---@type PluginState
local pluginState = pluginCommand.Parameter
TriggerServerEvent(Event.SaltyChat_CheckVersion, pluginState.Version)
self:ExecutePluginCommand(PluginCommand.new(
Command.RadioTowerUpdate,
self.Configuration.ServerUniqueIdentifier,
RadioTower.new(self.RadioTowers)
))
if self.PrimaryRadioChannel ~= nil then
self:RadioChannelMemberChangeHandler("global", State.SaltyChat_RadioChannelMember .. ":" ..
self.PrimaryRadioChannel, GlobalState[State.SaltyChat_RadioChannelMember .. ":" .. self.PrimaryRadioChannel])
self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" ..
self.PrimaryRadioChannel, GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. self.PrimaryRadioChannel])
end
if self.SecondaryRadioChannel ~= nil then
self:RadioChannelMemberChangeHandler("global", State.SaltyChat_RadioChannelMember ..
":" .. self.SecondaryRadioChannel, GlobalState
[State.SaltyChat_RadioChannelMember .. ":" .. self.SecondaryRadioChannel])
self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender ..
":" .. self.SecondaryRadioChannel, GlobalState
[State.SaltyChat_RadioChannelSender .. ":" .. self.SecondaryRadioChannel])
end
elseif pluginCommand.Command == Command.Reset then
self:SetPluginState(GameInstanceState.NotInitiated)
self:InitializePlugin()
elseif pluginCommand.Command == Command.Ping then
if self:GetPluginState() ~= GameInstanceState.NotInitiated then
self:ExecutePluginCommand(PluginCommand.new(
Command.Pong,
self.Configuration.ServerUniqueIdentifier
))
end
elseif pluginCommand.Command == Command.InstanceState then
---@type InstanceState
local instanceState = pluginCommand.Parameter
self:SetPluginState(instanceState.State)
elseif pluginCommand.Command == Command.SoundState then
---@type SoundState
local soundState = pluginCommand.Parameter
if soundState.IsMicrophoneMuted ~= self.IsMicrophoneMuted then
self.IsMicrophoneMuted = soundState.IsMicrophoneMuted;
TriggerEvent(Event.SaltyChat_MicStateChanged, self.IsMicrophoneMuted);
end
if soundState.IsMicrophoneEnabled ~= self.IsMicrophoneEnabled then
self.IsMicrophoneEnabled = soundState.IsMicrophoneEnabled;
TriggerEvent(Event.SaltyChat_MicEnabledChanged, self.IsMicrophoneEnabled);
end
if soundState.IsSoundMuted ~= self.IsSoundMuted then
self.IsSoundMuted = soundState.IsSoundMuted;
TriggerEvent(Event.SaltyChat_SoundStateChanged, self.IsSoundMuted);
end
if soundState.IsSoundEnabled ~= self.IsSoundEnabled then
self.IsSoundEnabled = soundState.IsSoundEnabled;
TriggerEvent(Event.SaltyChat_SoundEnabledChanged, self.IsSoundEnabled);
end
elseif pluginCommand.Command == Command.TalkState then
---@type TalkState
local talkState = pluginCommand.Parameter
if not self.IsMicrophoneMuted then
self:SetPlayerTalking(talkState.Name, talkState.IsTalking);
end
elseif pluginCommand.Command == Command.RadioTrafficState then
---@type RadioTrafficState
local radioTrafficState = pluginCommand.Parameter
---@type RadioTrafficState
local activeRadioTrafficState = table.find(self.ActiveRadioTraffic, function(value)
---@cast value RadioTrafficState
return value.Name == radioTrafficState.Name and value.IsPrimaryChannel == radioTrafficState.IsPrimaryChannel
end)
if radioTrafficState.IsSending then
if activeRadioTrafficState == nil then
table.insert(self.ActiveRadioTraffic, radioTrafficState)
elseif activeRadioTrafficState ~= nil and activeRadioTrafficState.ActiveRelay ~= radioTrafficState.ActiveRelay then
activeRadioTrafficState.ActiveRelay = radioTrafficState.ActiveRelay
end
else
if activeRadioTrafficState ~= nil then
local activeRadioTrafficStateKey = table.findIndex(self.ActiveRadioTraffic, function(value)
---@cast value RadioTrafficState
return value.Name == activeRadioTrafficState.Name
end)
table.removeKey(self.ActiveRadioTraffic, activeRadioTrafficStateKey)
end
end
TriggerEvent(Event.SaltyChat_RadioTrafficStateChanged,
table.any(self.ActiveRadioTraffic, function(r) -- Primary RX
---@cast r RadioTrafficState
return r.IsPrimaryChannel and r.IsSending and r.ActiveRelay == null and r.Name ~= self.TeamSpeakName
end),
table.any(self.ActiveRadioTraffic, function(r)
---@cast r RadioTrafficState
return r.Name == self.TeamSpeakName and r.IsPrimaryChannel and r.IsSending
end), -- Primary TX
table.any(self.ActiveRadioTraffic, function(r)
---@cast r RadioTrafficState
return not r.IsPrimaryChannel and r.IsSending and r.ActiveRelay == null and r.Name ~= self.TeamSpeakName
end), -- Secondary RX
table.any(self.ActiveRadioTraffic, function(r)
---@cast r RadioTrafficState
return r.Name == self.TeamSpeakName and not r.IsPrimaryChannel and r.IsSending
end) -- Secondary TX
);
end
end
RegisterNUICallback(NuiEvent.SaltyChat_OnError, function(data, cb) vcManager:OnError(data, cb) end)
function VoiceManager:OnError(data, cb)
local pluginError = PluginError.Deserialize(data)
if pluginError then
if pluginError.Error == Error.AlreadyInGame then
print("[SaltyChat Lua] Error: Seems like we are already in an instance, retry in 5 seconds...")
Wait(5000)
self:InitializePlugin()
else
print("[SaltyChat Lua] Error: " .. pluginError.Error .. " - Message:" .. pluginError.Message)
end
else
print("[SaltyChat Lua] Error: We received an error, but couldn't deserialize it")
end
end
--#endregion
--#region Events W I S E M A N
AddEventHandler("onClientResourceStop", function(resourceName) vcManager:OnResourceStop(resourceName) end)
---@param resourceName string
function VoiceManager:OnResourceStop(resourceName)
if resourceName ~= GetCurrentResourceName() then return end
self.IsEnabled = false
self.IsConnected = false
self._voiceClients = {}
self.PrimaryRadioChannel = nil
self.SecondaryRadioChannel = nil
for _, cookie in pairs(self._changeHandlerCookies) do
RemoveStateBagChangeHandler(cookie)
end
vcManager._changeHandlerCookies = nil
end
RegisterNetEvent(Event.SaltyChat_Initialize,
function(teamSpeakName, voiceRange, towers) vcManager:OnInitialize(teamSpeakName, voiceRange, towers) end)
---@param teamSpeakName string
---@param voiceRange number
---@param towers table
function VoiceManager:OnInitialize(teamSpeakName, voiceRange, towers)
self.TeamSpeakName = teamSpeakName
self:SetVoiceRange(voiceRange)
self:OnUpdateRadioTowers(towers)
self.IsEnabled = true
if self.IsConnected then
self:InitializePlugin()
elseif self.IsNuiReady then
self:ExecuteCommand("connect", self.WebSocketAddress)
else
print("[SaltyChat Lua] Got server response, but NUI wasn't ready")
end
end
RegisterNetEvent(Event.SaltyChat_RemoveClient, function(handle) vcManager:OnClientRemove(handle) end)
---@param handle string
function VoiceManager:OnClientRemove(handle)
local serverId = tonumber(handle)
if type(serverId) ~= "number" then
return print(
"[SaltyChat Lua] Error 'OnClientRemove': Could not get serverId. serverId is not a number")
end
---@type VoiceClient
local voiceClient = self._voiceClients[serverId]
if voiceClient then
self:ExecutePluginCommand(PluginCommand.new(
Command.RemovePlayer,
self.Configuration.ServerUniqueIdentifier,
PlayerState.new(voiceClient.TeamSpeakName)
))
table.removeKey(self._voiceClients, serverId)
end
end
RegisterNetEvent(Event.SaltyChat_EstablishCall,
function(handle, teamSpeakName, position) vcManager:OnEstablishCall(handle, teamSpeakName, position) end)
---@param handle string
---@param teamSpeakName string
---@param position table
function VoiceManager:OnEstablishCall(handle, teamSpeakName, position)
Logger:Debug("[OnEstablishCall]", handle, teamSpeakName)
self:OnEstablishCallRelayed(handle, teamSpeakName, position, true, {})
end
RegisterNetEvent(Event.SaltyChat_EstablishCall,
function(handle, teamSpeakName, position, direct, relays)
vcManager:OnEstablishCallRelayed(handle, teamSpeakName,
position, direct, relays)
end)
---@param handle string
---@param teamSpeakName string
---@param position table
---@param direct boolean
---@param relays string[]
function VoiceManager:OnEstablishCallRelayed(handle, teamSpeakName, position, direct, relays)
local serverId = tonumber(handle)
if type(serverId) ~= "number" then
return print(
"[SaltyChat Lua] Error 'OnEstablishCallRelayed': Could not get serverId. serverId is not a number")
end
local voiceClient = self:GetOrCreateVoiceClient(serverId, teamSpeakName)
if voiceClient then
if voiceClient.DistanceCulled then
voiceClient.LastPosition = TSVector.new(position[1], position[2], position[3])
voiceClient:SendPlayerStateUpdate(self)
self._phoneCallClients[voiceClient.ServerId] = voiceClient
end
local signalDistortion = 0
if Configuration.VariablePhoneDistortion then
local playerPosition = GamePlayer.Character.Position
local remotePlayerPosition = voiceClient.LastPosition
signalDistortion = GetZoneScumminess(GetZoneAtCoords(playerPosition.x, playerPosition.y, playerPosition.z)) +
GetZoneScumminess(GetZoneAtCoords(remotePlayerPosition.x, remotePlayerPosition.y, remotePlayerPosition.z))
end
self:ExecutePluginCommand(
PluginCommand.new(
Command.PhoneCommunicationUpdate,
self.Configuration.ServerUniqueIdentifier,
PhoneCommunication.new(
voiceClient.TeamSpeakName,
signalDistortion,
direct,
table.values(relays)
)
)
)
end
end
RegisterNetEvent(Event.SaltyChat_ChannelInUse, function(channelName) vcManager:OnChannelBlocked(channelName) end)
---@param channelName string
function VoiceManager:OnChannelBlocked(channelName)
self:PlaySound("offMicClick", false, "radio")
if channelName == self.PrimaryRadioChannel then
self:OnPrimaryRadioReleased()
elseif channelName == self.SecondaryRadioChannel then
self:OnSecondaryRadioReleased()
end
end
RegisterNetEvent(Event.SaltyChat_SetRadioSpeaker, function(channelName) vcManager:OnChannelBlocked(channelName) end)
---@param isRadioSpeakerEnabled boolean
function VoiceManager:OnSetRadioSpeaker(isRadioSpeakerEnabled)
self.IsRadioSpeakerEnabled = isRadioSpeakerEnabled
end
RegisterNetEvent(Event.SaltyChat_UpdateRadioTowers, function(towers)
vcManager:OnUpdateRadioTowers(towers)
end)
RegisterNetEvent(Event.SaltyChat_EndCall, function(handle) vcManager:OnEndCall(handle) end)
---@param handle string
function VoiceManager:OnEndCall(handle)
local serverId = tonumber(handle)
if type(serverId) ~= "number" then
return print(
"[SaltyChat Lua] Error 'OnEndCall': Could not get serverId. serverId is not a number")
end
local voiceClient = self._phoneCallClients[serverId] or self:GetOrCreateVoiceClient(serverId, Util.GetTeamSpeakName(serverId))
Logger:Debug("[OnEndCall]", serverId, voiceClient)
if voiceClient then
self:ExecutePluginCommand(PluginCommand.new(
Command.StopPhoneCommunication,
self.Configuration.ServerUniqueIdentifier,
PhoneCommunication.new(
voiceClient.TeamSpeakName
)
))
if self._phoneCallClients[serverId] then
self._phoneCallClients[serverId] = nil
end
end
end
RegisterNetEvent(Event.SaltyChat_SetRadioChannel,
function(radioChannel, isPrimary) vcManager:OnSetRadioChannel(radioChannel, isPrimary) end)
---@param radioChannel string
---@param isPrimary boolean
function VoiceManager:OnSetRadioChannel(radioChannel, isPrimary)
if isPrimary then
if self.PrimaryRadioChangeHandlerCookies ~= nil then
for _, cookie in pairs(self.PrimaryRadioChangeHandlerCookies) do
RemoveStateBagChangeHandler(cookie)
end
self.PrimaryRadioChangeHandlerCookies = nil
end
if IsStringNullOrEmpty(radioChannel) then
self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" ..
self.PrimaryRadioChannel, {}, 0, false)
self.PrimaryRadioChannel = nil
self:PlaySound("leaveRadioChannel", false, "radio")
self:ExecutePluginCommand(PluginCommand.new(
Command.UpdateRadioChannelMembers,
self.Configuration.ServerUniqueIdentifier,
RadioChannelMemberUpdate.new(
{},
true
)
))
else
self.PrimaryRadioChannel = radioChannel
self.PrimaryRadioChangeHandlerCookies = {}
table.insert(self.PrimaryRadioChangeHandlerCookies,
AddStateBagChangeHandler(State.SaltyChat_RadioChannelMember .. ":" .. radioChannel, "global",
function(bagName, key, value, reserved, replicated)
self:RadioChannelMemberChangeHandler(bagName, key, value, reserved, replicated)
end))
table.insert(self.PrimaryRadioChangeHandlerCookies,
AddStateBagChangeHandler(State.SaltyChat_RadioChannelSender .. ":" .. radioChannel, "global",
function(bagName, key, value, reserved, replicated)
self:RadioChannelSenderChangeHandler(bagName, key, value, reserved, replicated)
end))
self:PlaySound("enterRadioChannel", false, "radio")
if GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel] ~= nil then
self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" .. radioChannel,
GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel], 0, false);
end
end
else
if self.SecondaryRadioChangeHandlerCookies ~= nil then
for _, cookie in pairs(self.SecondaryRadioChangeHandlerCookies) do
RemoveStateBagChangeHandler(cookie)
end
self.SecondaryRadioChangeHandlerCookies = nil
end
if IsStringNullOrEmpty(radioChannel) then
self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender ..
":" .. self.SecondaryRadioChannel, {}, 0, false)
self.SecondaryRadioChannel = nil
self:PlaySound("leaveRadioChannel", false, "radio")
self:ExecutePluginCommand(PluginCommand.new(
Command.UpdateRadioChannelMembers,
self.Configuration.ServerUniqueIdentifier,
RadioChannelMemberUpdate.new(
{},
false
)
))
else
self.SecondaryRadioChannel = radioChannel
self.SecondaryRadioChangeHandlerCookies = {}
table.insert(self.SecondaryRadioChangeHandlerCookies,
AddStateBagChangeHandler(State.SaltyChat_RadioChannelMember .. ":" .. radioChannel, "global",
function(bagName, key, value, reserved, replicated)
self:RadioChannelMemberChangeHandler(bagName, key, value, reserved, replicated)
end))
table.insert(self.SecondaryRadioChangeHandlerCookies,
AddStateBagChangeHandler(State.SaltyChat_RadioChannelSender .. ":" .. radioChannel, "global",
function(bagName, key, value, reserved, replicated)
self:RadioChannelSenderChangeHandler(bagName, key, value, reserved, replicated)
end))
self:PlaySound("enterRadioChannel", false, "radio")
if GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel] ~= nil then
self:RadioChannelSenderChangeHandler("global", State.SaltyChat_RadioChannelSender .. ":" .. radioChannel,
GlobalState[State.SaltyChat_RadioChannelSender .. ":" .. radioChannel], 0, false);
end
end
end
TriggerEvent(Event.SaltyChat_RadioChannelChanged, radioChannel, isPrimary)
end
--#endregion