1444 lines
		
	
	
	
		
			49 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			1444 lines
		
	
	
	
		
			49 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| ---@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
 | 
