forked from Simnation/Main
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
|