---@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 ---@field _phoneCallClients table ---@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