local mutedPlayers = {} -- we can't use GetConvarInt because its not a integer, and theres no way to get a float... so use a hacky way it is! local volumes = { -- people are setting this to 1 instead of 1.0 and expecting it to work. ['radio'] = GetConvarInt('voice_defaultRadioVolume', 60) / 100, ['call'] = GetConvarInt('voice_defaultCallVolume', 60) / 100, } radioEnabled, radioPressed, mode = true, false, GetConvarInt('voice_defaultVoiceMode', 2) radioData = {} callData = {} submixIndicies = {} --- function setVolume --- Toggles the players volume ---@param volume number between 0 and 100 ---@param volumeType string the volume type (currently radio & call) to set the volume of (opt) function setVolume(volume, volumeType) type_check({volume, "number"}) local volume = volume / 100 if volumeType then local volumeTbl = volumes[volumeType] if volumeTbl then LocalPlayer.state:set(volumeType, volume, true) volumes[volumeType] = volume resyncVolume(volumeType, volume) else error(('setVolume got a invalid volume type %s'):format(volumeType)) end else for volumeType, _ in pairs(volumes) do volumes[volumeType] = volume LocalPlayer.state:set(volumeType, volume, true) end resyncVolume("all", volume) end end exports('setRadioVolume', function(vol) setVolume(vol, 'radio') end) exports('getRadioVolume', function() return volumes['radio'] end) exports("setCallVolume", function(vol) setVolume(vol, 'call') end) exports('getCallVolume', function() return volumes['call'] end) -- default submix incase people want to fiddle with it. -- freq_low = 389.0 -- freq_hi = 3248.0 -- fudge = 0.0 -- rm_mod_freq = 0.0 -- rm_mix = 0.16 -- o_freq_lo = 348.0 -- 0_freq_hi = 4900.0 if gameVersion == 'fivem' then local radioEffectId = CreateAudioSubmix('Radio') SetAudioSubmixEffectRadioFx(radioEffectId, 0) -- This is a GetHashKey on purpose, backticks break treesitter in nvim :| SetAudioSubmixEffectParamInt(radioEffectId, 0, GetHashKey('default'), 1) SetAudioSubmixOutputVolumes( radioEffectId, 0, 1.0 --[[ frontLeftVolume ]], 0.25 --[[ frontRightVolume ]], 0.0 --[[ rearLeftVolume ]], 0.0 --[[ rearRightVolume ]], 1.0 --[[ channel5Volume ]], 1.0 --[[ channel6Volume ]] ) AddAudioSubmixOutput(radioEffectId, 0) submixIndicies['radio'] = radioEffectId local callEffectId = CreateAudioSubmix('Call') SetAudioSubmixOutputVolumes( callEffectId, 1, 0.10 --[[ frontLeftVolume ]], 0.50 --[[ frontRightVolume ]], 0.0 --[[ rearLeftVolume ]], 0.0 --[[ rearRightVolume ]], 1.0 --[[ channel5Volume ]], 1.0 --[[ channel6Volume ]] ) AddAudioSubmixOutput(callEffectId, 1) submixIndicies['call'] = callEffectId -- Callback is expected to return data in an array, this is for compatibility sake with js, index 0 should be the name and index 1 should be the submixId -- the callback is sent the effectSlot it can register to, not sure if this is needed, but its here for safety exports("registerCustomSubmix", function(callback) local submixTable = callback() type_check({submixTable, "table"}) local submixName, submixId = submixTable[1], submixTable[2] type_check({submixName, "string"}, {submixId, "number"}) logger.info("Creating submix %s with submixId %s", submixName, submixId) submixIndicies[submixName] = submixId end) TriggerEvent("pma-voice:registerCustomSubmixes") end --- export setEffectSubmix --- Sets a user defined audio submix for radio and phonecall effects ---@param type string either "call" or "radio" ---@param effectId number submix id returned from CREATE_AUDIO_SUBMIX exports("setEffectSubmix", function(type, effectId) type_check({type, "string"}, {effectId, "number"}) if submixIndicies[type] then submixIndicies[type] = effectId end end) function restoreDefaultSubmix(plyServerId) local submix = Player(plyServerId).state.submix local submixEffect = submixIndicies[submix] if not submix or not submixEffect then MumbleSetSubmixForServerId(plyServerId, -1) return end MumbleSetSubmixForServerId(plyServerId, submixEffect) end -- used to prevent a race condition if they talk again afterwards, which would lead to their voice going to default. local disableSubmixReset = {} --- function toggleVoice --- Toggles the players voice ---@param plySource number the players server id to override the volume for ---@param enabled boolean if the players voice is getting activated or deactivated ---@param moduleType string the volume & submix to use for the voice. function toggleVoice(plySource, enabled, moduleType) if mutedPlayers[plySource] then return end logger.verbose('[main] Updating %s to talking: %s with submix %s', plySource, enabled, moduleType) local distance = currentTargets[plySource] if enabled and (not distance or distance > 4.0) then MumbleSetVolumeOverrideByServerId(plySource, enabled and volumes[moduleType]) if GetConvarInt('voice_enableSubmix', 1) == 1 and gameVersion == 'fivem' then if moduleType then disableSubmixReset[plySource] = true if submixIndicies[moduleType] then MumbleSetSubmixForServerId(plySource, submixIndicies[moduleType]) end else restoreDefaultSubmix(plySource) end end elseif not enabled then if GetConvarInt('voice_enableSubmix', 1) == 1 and gameVersion == 'fivem' then -- garbage collect it disableSubmixReset[plySource] = nil SetTimeout(250, function() if not disableSubmixReset[plySource] then restoreDefaultSubmix(plySource) end end) end MumbleSetVolumeOverrideByServerId(plySource, -1.0) end end local function updateVolumes(voiceTable, override) for serverId, talking in pairs(voiceTable) do if serverId == playerServerId then goto skip_iter end MumbleSetVolumeOverrideByServerId(serverId, talking and override or -1.0) ::skip_iter:: end end --- resyncs the call/radio/etc volume to the new volume ---@param volumeType any function resyncVolume(volumeType, newVolume) if volumeType == "all" then resyncVolume("radio", newVolume) resyncVolume("call", newVolume) elseif volumeType == "radio" then updateVolumes(radioData, newVolume) elseif volumeType == "call" then updateVolumes(callData, newVolume) end end --- function playerTargets ---Adds players voices to the local players listen channels allowing ---Them to communicate at long range, ignoring proximity range. ---@diagnostic disable-next-line: undefined-doc-param ---@param targets table expects multiple tables to be sent over function playerTargets(...) local targets = {...} local addedPlayers = { [playerServerId] = true } for i = 1, #targets do for id, _ in pairs(targets[i]) do -- we don't want to log ourself, or listen to ourself if addedPlayers[id] and id ~= playerServerId then logger.verbose('[main] %s is already target don\'t re-add', id) goto skip_loop end if not addedPlayers[id] then logger.verbose('[main] Adding %s as a voice target', id) addedPlayers[id] = true MumbleAddVoiceTargetPlayerByServerId(voiceTarget, id) end ::skip_loop:: end end end --- function playMicClicks ---plays the mic click if the player has them enabled. ---@param clickType boolean whether to play the 'on' or 'off' click. function playMicClicks(clickType) if micClicks ~= 'true' then return logger.verbose("Not playing mic clicks because client has them disabled") end -- TODO: Add customizable radio click volumes sendUIMessage({ sound = (clickType and "audio_on" or "audio_off"), volume = (clickType and 0.1 or 0.03) }) end --- getter for mutedPlayers exports('getMutedPlayers', function() return mutedPlayers end) --- toggles the targeted player muted ---@param source number the player to mute function toggleMutePlayer(source) if mutedPlayers[source] then mutedPlayers[source] = nil MumbleSetVolumeOverrideByServerId(source, -1.0) else mutedPlayers[source] = true MumbleSetVolumeOverrideByServerId(source, 0.0) end end exports('toggleMutePlayer', toggleMutePlayer) --- function setVoiceProperty --- sets the specified voice property ---@param type string what voice property you want to change (only takes 'radioEnabled' and 'micClicks') ---@param value any the value to set the type to. function setVoiceProperty(type, value) if type == "radioEnabled" then radioEnabled = value sendUIMessage({ radioEnabled = value }) elseif type == "micClicks" then local val = tostring(value) micClicks = val SetResourceKvp('pma-voice_enableMicClicks', val) end end exports('setVoiceProperty', setVoiceProperty) -- compatibility exports('SetMumbleProperty', setVoiceProperty) exports('SetTokoProperty', setVoiceProperty) -- cache their external servers so if it changes in runtime we can reconnect the client. local externalAddress = '' local externalPort = 0 CreateThread(function() while true do Wait(500) -- only change if what we have doesn't match the cache if GetConvar('voice_externalAddress', '') ~= externalAddress or GetConvarInt('voice_externalPort', 0) ~= externalPort then externalAddress = GetConvar('voice_externalAddress', '') externalPort = GetConvarInt('voice_externalPort', 0) MumbleSetServerAddress(GetConvar('voice_externalAddress', ''), GetConvarInt('voice_externalPort', 0)) end end end) if gameVersion == 'redm' then CreateThread(function() while true do if IsControlJustPressed(0, 0xA5BDCD3C --[[ Right Bracket ]]) then ExecuteCommand('cycleproximity') end if IsControlJustPressed(0, 0x430593AA --[[ Left Bracket ]]) then ExecuteCommand('+radiotalk') elseif IsControlJustReleased(0, 0x430593AA --[[ Left Bracket ]]) then ExecuteCommand('-radiotalk') end Wait(0) end end) end