diff --git a/resources/[voice]/pma-voice/.gitignore b/resources/[voice]/pma-voice/.gitignore new file mode 100644 index 0000000..2e2d6d2 --- /dev/null +++ b/resources/[voice]/pma-voice/.gitignore @@ -0,0 +1,2 @@ +.idea +.vscode \ No newline at end of file diff --git a/resources/[voice]/pma-voice/LICENSE b/resources/[voice]/pma-voice/LICENSE new file mode 100644 index 0000000..ad5aece --- /dev/null +++ b/resources/[voice]/pma-voice/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Dillon Skaggs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/[voice]/pma-voice/README.md b/resources/[voice]/pma-voice/README.md new file mode 100644 index 0000000..bf693f3 --- /dev/null +++ b/resources/[voice]/pma-voice/README.md @@ -0,0 +1,187 @@ +## PLEASE NOTE: Currently master branch has some breaking changes + +If you previously used `voice_defaultPhoneVolume` you will instead need to use `voice_defaultCallVolume` +If you previously used `voice_enablePhones` you will instead need to use `voice_enableCalls` + +If you were previously using the state bag getter `Player(source).state.phone` you will instead need to use `Player(source).state.call` + +# pma-voice +A voice system designed around the use of FiveM/RedM internal mumble server. + +## Support + +Please report any issues you have in the GitHub [Issues](https://github.com/AvarianKnight/pma-voice/issues) + +### NOTE: It is expected for servers to be on the latest recommended version, which you can find [here for Windows](https://runtime.fivem.net/artifacts/fivem/build_server_windows/master/) and [here for Linux](https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/). + +# Compatibility Notice: + +This script is not compatible with other voice systems (duh), that means if you have vMenus voice chat you will **have** to [disable](https://docs.vespura.com/vmenu/faq/#q-how-do-i-disable-voice-chat) it. + +Please do not override `NetworkSetTalkerProximity`, `MumbleSetTalkerProximity`, `MumbleSetAudioInputDistance`, `MumbleSetAudioOutputDistance` or `NetworkSetVoiceActive` in any of your other scripts as there have been cases where it breaks pma-voice. + +# Credits + +- @Frazzle for mumble-voip (for which the concept came from) +- @pichotm for pVoice (where the grid concept came from) + +# FiveM/RedM Config + +### NOTE: Only use one of the Audio options (don't enable 3d Audio & Native Audio at the same time), its also recommended to always use voice_useSendingRangeOnly. + +You only need to add the convar **if** you're changing the value. + +All of the configs here are set using `setr [voice_configOption] [boolean]` + +Native audio will not work on RedM, you will have to use 3d audio. + +| ConVar | Default | Description | Parameter(s) | +|----------------------------|---------|---------------------------------------------------------------|--------------| +| voice_useNativeAudio | false | **This will not work for RedM** Uses the games native audio, will add 3d sound, echo, reverb, and more. **Required for submixs** | boolean | +| voice_use2dAudio | false | Uses 2d audio, will result in same volume sound no matter where they're at until they leave proximity. | boolean +| voice_use3dAudio | false | Uses 3d audio | boolean | +| voice_useSendingRangeOnly | false | Only allows you to hear people within your hear/send range, prevents people from connecting to your mumble server and trolling. | boolean | + +# Config + +### PLEASE NOTE: Any keybind changes only affect new players, if you want to change your key bind go to Key Bindings -> FiveM -> Look for keybinds under 'pma-voice'. + +All of the config is done via ConVars in order to streamline the process. + +The ints are used like a boolean to 0 would be false, 1 true. + +All of the configs here are set using `setr [voice_configOption] [int]` OR `setr [voice_configOption] "[string]"` + +#### Note: If a convar defaults to 1 (true) you don't have set it again unless you want to disable it. + +### General Voice Settings + +| ConVar | Default | Description | Parameter(s) | +|-------------------------|---------|--------------------------------------------------------------------|--------------| +| voice_enableUi | 1 | Enables the built in user interface | int | +| voice_enableProximityCycle | 1 | Enables the usage of the F11 proximity key, if disabled players are stuck on the first proximity | int | +| voice_defaultCycle | F11 | The default key to cycle the players proximity. You can find a list of valid keys [in the Cfx docs](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/) | string | +| voice_defaultRadioVolume | 30 | The default volume to set the radio to (has to be between 1 and 100) *NOTE: Only new joins will have the new value, players that already joined will not.* | float | +| voice_defaultCallVolume | 60 | The default volume to set the call to (has to be between 1 and 100) *NOTE: Only new joins will have the new value, players that already joined will not.* | float | +| voice_defaultVoiceMode | 2 | Default proximity voice value when player joins server. (Voice Modes; 1:Whisper, 2:Normal, 3:Shouting) | int | + +### Call & Radio + +| ConVar | Default | Description | Parameter(s) | +|-------------------------|---------|--------------------------------------------------------------------|--------------| +| voice_enableRadios | 1 | Enables the radio sub-modules | int | +| voice_enableCalls | 1 | Enables the call sub-modules | int | +| voice_enableSubmix | 1 | Enables the submix which adds a radio/call style submix to their voice **NOTE: Submixs require native audio** | int | +| voice_enableRadioAnim | 0 | Enables (grab shoulder mic) animation while talking on the radio. | int | +| voice_defaultRadio | LMENU | The default key to use the radio. You can find a list of valid keys [in the FiveM docs](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/) | string | + +### Sync + +| ConVar | Default | Description | Parameter(s) | +|-------------------------|---------|--------------------------------------------------------------------|--------------| +| voice_refreshRate | 200 | How often the UI/Proximity is refreshed | int | + +### External Server & Misc. +| ConVar | Default | Description | Parameter(s) | +|-------------------------|---------|--------------------------------------------------------------------|--------------| +| voice_allowSetIntent | 1 | Whether or not to allow players to set their audio intents (you can see more [here](https://docs.fivem.net/natives/?_0x6383526B)) | int | +| voice_externalAddress | none | The external address to use to connect to the mumble server | string | +| voice_externalPort | 0 | The external port to use | int | +| voice_debugMode | 0 | 1 for basic logs, 4 for verbose logs | int | +| voice_externalDisallowJoin | 0 | Disables players being allowed to join the server, should only be used if you're using a FXServer as a external mumble server. | int | +| voice_hideEndpoints | 1 | Hides the mumble address in logs *NOTE: You should only care to hide this for a external server.* | int | + + + +### Aces + +pma-voice comes with a built in /muteply (tgtPly) (duration) command, in order to allow your staff to use it you will have to grand them the ace! + +Example: +`add_ace group.superadmin command.muteply allow;` + +This would only allow the superadmin group to mute players. + +### Exports + +#### Client + +##### Setters + +| Export | Description | Parameter(s) | +|---------------------|-----------------------------|--------------| +| [setVoiceProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any | +| [setRadioChannel](docs/client-setters/setRadioChannel.md) | Set radio channel | int | +| [setCallChannel](docs/client-setters/setCallChannel.md) | Set call channel | int | +| [setRadioVolume](docs/client-setters/setRadioVolume.md) | Set radio volume for player | int | +| [setCallVolume](docs/client-setters/setCallVolume.md) | Set call volume for player | int | +| [addPlayerToRadio](docs/client-setters/setRadioChannel.md) | Set radio channel | int | +| [addPlayerToCall](docs/client-setters/setCallChannel.md) | Set call channel | int | +| [removePlayerFromRadio](docs/client-setters/removePlayerFromRadio.md) | Remove player from radio | | +| [removePlayerFromCall](docs/client-setters/removePlayerFromCall.md) | Remove player from call | | + +##### Toggles + +| Export | Description | Parameter(s) | +|---------------------|--------------------------------------------------------|--------------| +| toggleMutePlayer | Toggles the selected player muted for the local client | int | + +Supported from mumble-voip / toko-voip + +| Export | Description | Parameter(s) | +|-----------------------|--------------------------|--------------| +| [SetMumbleProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any | +| [SetTokoProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any | +| [SetRadioChannel](docs/client-setters/setRadioChannel.md) | Set radio channel | int | +| [SetCallChannel](docs/client-setters/setCallChannel.md) | Set call channel | int | + +#### Getters + +The majority of setters are done through player states, while a small + + +| State Bag | Description | Return Type | +|---------------|--------------------------------------------------------------|--------------| +| [proximity](docs/state-getters/stateBagGetters.md) | Returns a table with the mode index, distance, and mode name | table | +| [radioChannel](docs/state-getters/stateBagGetters.md) | Returns the players current radio channel, or 0 for none | int | +| [callChannel](docs/state-getters/stateBagGetters.md) | Returns the players current call channel, or 0 for none | int | + +#### Events + +These are events designed for third-party resource integration. These are emitted only to the current client. + +| Event | Description | Event Params | +|--------------------------|--------------------------------------------------------------|----------------| +| [pma-voice:settingsCallback](docs/client-getters/events.md) | When emited it will return the current pma-voice settings. | cb(voiceSettings) | +| [pma-voice:radioActive](docs/client-getters/events.md) | Triggered when the radio is activated / deactivated | boolean | +| [pma-voice:setTalkingMode](docs/client-getters/events.md) | Triggered on proximity mode change with the voice mode id | int | + + +#### Server + +##### Setters + +| Export | Description | Parameter(s) | +|----------------------|--------------------------------------|--------------| +| [setPlayerRadio](docs/server-setters/setPlayerRadio.md) | Sets the players radio channel | int, int | +| [setPlayerCall](docs/server-setters/setPlayerCall.md) | Sets the players call channel | int, int | +| [addChannelCheck](docs/server-setters/addChannelCheck.md) | Adds a channel check to the players radio channel | int, function | + + +##### Getters + +###### State Bags +You can access the state with `Player(source).state['state bag here']` + +| State Bag | Description | Return Type | +|---------------|--------------------------------------------------------------|--------------| +| [proximity](docs/state-getters/stateBagGetters.md) | Returns a table with the mode index, distance, and mode name | table | +| [radioChannel](docs/state-getters/stateBagGetters.md) | Returns the players current radio channel, or 0 for none | int | +| [callChannel](docs/state-getters/stateBagGetters.md) | Returns the players current call channel, or 0 for none | int | +| [voiceIntent](docs/state-getters/stateBagGetters.md) | Returns the players current voice intent, either 'speech' or 'music' | string | + +###### Exports + +| Export | Description | Parameter(s) | +|------------------------------|---------------------------------------------------|------| +| [getPlayersInRadioChannel](docs/server-getters/getPlayersInRadioChannel.md) | Gets the current players in a radio channel | int | diff --git a/resources/[voice]/pma-voice/TODO.md b/resources/[voice]/pma-voice/TODO.md new file mode 100644 index 0000000..0371ce2 --- /dev/null +++ b/resources/[voice]/pma-voice/TODO.md @@ -0,0 +1,10 @@ +## TODO +- [ ] Ability to display radio members on the client. +- [ ] Use commands to define voiceModes in shared.lua and only leave debug logs in shared.lua. +- [ ] Convert the UI to React. +- [ ] Multiple radio channels. + +## DONE +- [ x ] Implement a easy way to get the players current radio channel on the server. +- [ x ] Add the ability to override proximity with exports. +- [ x ] Rename everything that uses 'phone' to 'call' for consistency. diff --git a/resources/[voice]/pma-voice/client/commands.lua b/resources/[voice]/pma-voice/client/commands.lua new file mode 100644 index 0000000..1f80681 --- /dev/null +++ b/resources/[voice]/pma-voice/client/commands.lua @@ -0,0 +1,85 @@ +local wasProximityDisabledFromOverride = false +disableProximityCycle = false +RegisterCommand('setvoiceintent', function(source, args) + if GetConvarInt('voice_allowSetIntent', 1) == 1 then + local intent = args[1] + if intent == 'speech' then + MumbleSetAudioInputIntent(`speech`) + elseif intent == 'music' then + MumbleSetAudioInputIntent(`music`) + end + LocalPlayer.state:set('voiceIntent', intent, true) + end +end) + +RegisterCommand('resetvoice', function() -- chat command, You can change it to your liking. + NetworkClearVoiceChannel() + NetworkSessionVoiceLeave() + Wait(50) + NetworkSetVoiceActive(false) + MumbleClearVoiceTarget(2) + Wait(1000) + MumbleSetVoiceTarget(2) + NetworkSetVoiceActive(true) +end) + +-- TODO: Better implementation of this? +RegisterCommand('vol', function(_, args) + if not args[1] then return end + setVolume(tonumber(args[1])) +end) + +exports('setAllowProximityCycleState', function(state) + type_check({state, "boolean"}) + disableProximityCycle = state +end) + +function setProximityState(proximityRange, isCustom) + local voiceModeData = Cfg.voiceModes[mode] + MumbleSetTalkerProximity(proximityRange + 0.0) + LocalPlayer.state:set('proximity', { + index = mode, + distance = proximityRange, + mode = isCustom and "Custom" or voiceModeData[2], + }, true) + sendUIMessage({ + -- JS expects this value to be - 1, "custom" voice is on the last index + voiceMode = isCustom and #Cfg.voiceModes or mode - 1 + }) +end + +exports("overrideProximityRange", function(range, disableCycle) + type_check({range, "number"}) + setProximityState(range, true) + if disableCycle then + disableProximityCycle = true + wasProximityDisabledFromOverride = true + end +end) + +exports("clearProximityOverride", function() + local voiceModeData = Cfg.voiceModes[mode] + setProximityState(voiceModeData[1], false) + if wasProximityDisabledFromOverride then + disableProximityCycle = false + end +end) + +RegisterCommand('cycleproximity', function() + -- Proximity is either disabled, or manually overwritten. + if GetConvarInt('voice_enableProximityCycle', 1) ~= 1 or disableProximityCycle then return end + local newMode = mode + 1 + + -- If we're within the range of our voice modes, allow the increase, otherwise reset to the first state + if newMode <= #Cfg.voiceModes then + mode = newMode + else + mode = 1 + end + + setProximityState(Cfg.voiceModes[mode][1], false) + TriggerEvent('pma-voice:setTalkingMode', mode) +end, false) +if gameVersion == 'fivem' then + RegisterKeyMapping('cycleproximity', 'Skift talelængde', 'keyboard', GetConvar('voice_defaultCycle', 'F11')) +end diff --git a/resources/[voice]/pma-voice/client/events.lua b/resources/[voice]/pma-voice/client/events.lua new file mode 100644 index 0000000..7f8b769 --- /dev/null +++ b/resources/[voice]/pma-voice/client/events.lua @@ -0,0 +1,42 @@ +function handleInitialState() + local voiceModeData = Cfg.voiceModes[mode] + MumbleSetTalkerProximity(voiceModeData[1] + 0.0) + MumbleClearVoiceTarget(voiceTarget) + MumbleSetVoiceTarget(voiceTarget) + MumbleSetVoiceChannel(playerServerId) + + while MumbleGetVoiceChannelFromServerId(playerServerId) ~= playerServerId do + Wait(250) + MumbleSetVoiceChannel(playerServerId) + end + + MumbleAddVoiceTargetChannel(voiceTarget, playerServerId) + + addNearbyPlayers() +end + +AddEventHandler('mumbleConnected', function(address, isReconnecting) + logger.info('Connected to mumble server with address of %s, is this a reconnect %s', GetConvarInt('voice_hideEndpoints', 1) == 1 and 'HIDDEN' or address, isReconnecting) + + logger.log('Connecting to mumble, setting targets.') + -- don't try to set channel instantly, we're still getting data. + local voiceModeData = Cfg.voiceModes[mode] + LocalPlayer.state:set('proximity', { + index = mode, + distance = voiceModeData[1], + mode = voiceModeData[2], + }, true) + + handleInitialState() + + logger.log('Finished connection logic') +end) + +AddEventHandler('mumbleDisconnected', function(address) + logger.info('Disconnected from mumble server with address of %s', GetConvarInt('voice_hideEndpoints', 1) == 1 and 'HIDDEN' or address) +end) + +-- TODO: Convert the last Cfg to a Convar, while still keeping it simple. +AddEventHandler('pma-voice:settingsCallback', function(cb) + cb(Cfg) +end) \ No newline at end of file diff --git a/resources/[voice]/pma-voice/client/init/init.lua b/resources/[voice]/pma-voice/client/init/init.lua new file mode 100644 index 0000000..17aaf98 --- /dev/null +++ b/resources/[voice]/pma-voice/client/init/init.lua @@ -0,0 +1,46 @@ + +AddEventHandler('onClientResourceStart', function(resource) + if resource ~= GetCurrentResourceName() then + return + end + print('Starting script initialization') + + -- Some people modify pma-voice and mess up the resource Kvp, which means that if someone + -- joins another server that has pma-voice, it will error out, this will catch and fix the kvp. + local success = pcall(function() + local micClicksKvp = GetResourceKvpString('pma-voice_enableMicClicks') + if not micClicksKvp then + SetResourceKvp('pma-voice_enableMicClicks', "true") + else + if micClicksKvp ~= 'true' and micClicksKvp ~= 'false' then + error('Invalid Kvp, throwing error for automatic fix') + end + micClicks = micClicksKvp + end + end) + + if not success then + logger.warn('Failed to load resource Kvp, likely was inappropriately modified by another server, resetting the Kvp.') + SetResourceKvp('pma-voice_enableMicClicks', "true") + micClicks = 'true' + end + sendUIMessage({ + uiEnabled = GetConvarInt("voice_enableUi", 1) == 1, + voiceModes = json.encode(Cfg.voiceModes), + voiceMode = mode - 1 + }) + + local radioChannel = LocalPlayer.state.radioChannel or 0 + local callChannel = LocalPlayer.state.callChannel or 0 + + -- Reinitialize channels if they're set. + if radioChannel ~= 0 then + setRadioChannel(radioChannel) + end + + if callChannel ~= 0 then + setCallChannel(callChannel) + end + + print('Script initialization finished.') +end) diff --git a/resources/[voice]/pma-voice/client/init/main.lua b/resources/[voice]/pma-voice/client/init/main.lua new file mode 100644 index 0000000..10b8c41 --- /dev/null +++ b/resources/[voice]/pma-voice/client/init/main.lua @@ -0,0 +1,298 @@ +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 diff --git a/resources/[voice]/pma-voice/client/init/proximity.lua b/resources/[voice]/pma-voice/client/init/proximity.lua new file mode 100644 index 0000000..2c0059d --- /dev/null +++ b/resources/[voice]/pma-voice/client/init/proximity.lua @@ -0,0 +1,209 @@ +-- used when muted +local disableUpdates = false +local isListenerEnabled = false +local plyCoords = GetEntityCoords(PlayerPedId()) +proximity = MumbleGetTalkerProximity() +currentTargets = {} + +function orig_addProximityCheck(ply) + local tgtPed = GetPlayerPed(ply) + local voiceRange = GetConvar('voice_useNativeAudio', 'false') == 'true' and proximity * 3 or proximity + local distance = #(plyCoords - GetEntityCoords(tgtPed)) + return distance < voiceRange, distance +end +local addProximityCheck = orig_addProximityCheck + +exports("overrideProximityCheck", function(fn) + addProximityCheck = fn +end) + +exports("resetProximityCheck", function() + addProximityCheck = orig_addProximityCheck +end) + +function addNearbyPlayers() + if disableUpdates then return end + -- update here so we don't have to update every call of addProximityCheck + plyCoords = GetEntityCoords(PlayerPedId()) + proximity = MumbleGetTalkerProximity() + currentTargets = {} + MumbleClearVoiceTargetChannels(voiceTarget) + if LocalPlayer.state.disableProximity then return end + MumbleAddVoiceChannelListen(playerServerId) + MumbleAddVoiceTargetChannel(voiceTarget, playerServerId) + + for source, _ in pairs(callData) do + if source ~= playerServerId then + MumbleAddVoiceTargetChannel(voiceTarget, source) + end + end + + + local players = GetActivePlayers() + for i = 1, #players do + local ply = players[i] + local serverId = GetPlayerServerId(ply) + local shouldAdd, distance = addProximityCheck(ply) + if shouldAdd then + -- if distance then + -- currentTargets[serverId] = distance + -- else + -- -- backwards compat, maybe remove in v7 + -- currentTargets[serverId] = 15.0 + -- end + -- logger.verbose('Added %s as a voice target', serverId) + MumbleAddVoiceTargetChannel(voiceTarget, serverId) + end + end +end + +function setSpectatorMode(enabled) + logger.info('Setting spectate mode to %s', enabled) + isListenerEnabled = enabled + local players = GetActivePlayers() + if isListenerEnabled then + for i = 1, #players do + local ply = players[i] + local serverId = GetPlayerServerId(ply) + if serverId == playerServerId then goto skip_loop end + logger.verbose("Adding %s to listen table", serverId) + MumbleAddVoiceChannelListen(serverId) + ::skip_loop:: + end + else + for i = 1, #players do + local ply = players[i] + local serverId = GetPlayerServerId(ply) + if serverId == playerServerId then goto skip_loop end + logger.verbose("Removing %s from listen table", serverId) + MumbleRemoveVoiceChannelListen(serverId) + ::skip_loop:: + end + end +end + +RegisterNetEvent('onPlayerJoining', function(serverId) + if isListenerEnabled then + MumbleAddVoiceChannelListen(serverId) + logger.verbose("Adding %s to listen table", serverId) + end +end) + +RegisterNetEvent('onPlayerDropped', function(serverId) + if isListenerEnabled then + MumbleRemoveVoiceChannelListen(serverId) + logger.verbose("Removing %s from listen table", serverId) + end +end) + +local listenerOverride = false +exports("setListenerOverride", function(enabled) + type_check({enabled, "boolean"}) + listenerOverride = enabled +end) + +-- cache talking status so we only send a nui message when its not the same as what it was before +local lastTalkingStatus = false +local lastRadioStatus = false +local voiceState = "proximity" +CreateThread(function() + TriggerEvent('chat:addSuggestion', '/muteply', 'Muter spilleren med det angivede ID', { + { name = "id", help = "Spilleren der skal mutes" }, + { name = "tid", help = "(Valgfrit) Tid i sekunder (default: 900)" } + }) + while true do + -- wait for mumble to reconnect + while not MumbleIsConnected() do + Wait(100) + end + -- Leave the check here as we don't want to do any of this logic + if GetConvarInt('voice_enableUi', 1) == 1 then + local curTalkingStatus = MumbleIsPlayerTalking(PlayerId()) == 1 + if lastRadioStatus ~= radioPressed or lastTalkingStatus ~= curTalkingStatus then + lastRadioStatus = radioPressed + lastTalkingStatus = curTalkingStatus + sendUIMessage({ + usingRadio = lastRadioStatus, + talking = lastTalkingStatus + }) + end + end + + if voiceState == "proximity" then + addNearbyPlayers() + -- What a name, wowza + local cam = GetConvarInt("voice_disableAutomaticListenerOnCamera", 0) ~= 1 and GetRenderingCam() or -1 + local isSpectating = NetworkIsInSpectatorMode() or cam ~= -1 + if not isListenerEnabled and (isSpectating or listenerOverride) then + setSpectatorMode(true) + elseif isListenerEnabled and not isSpectating and not listenerOverride then + setSpectatorMode(false) + end + end + + Wait(GetConvarInt('voice_refreshRate', 200)) + end +end) + +exports("setVoiceState", function(_voiceState, channel) + if _voiceState ~= "proximity" and _voiceState ~= "channel" then + logger.error("Didn't get a proper voice state, expected proximity or channel, got %s", _voiceState) + end + voiceState = _voiceState + if voiceState == "channel" then + type_check({channel, "number"}) + -- 65535 is the highest a client id can go, so we add that to the base channel so we don't manage to get onto a players channel + channel = channel + 65535 + MumbleSetVoiceChannel(channel) + while MumbleGetVoiceChannelFromServerId(playerServerId) ~= channel do + Wait(250) + end + MumbleAddVoiceTargetChannel(voiceTarget, channel) + elseif voiceState == "proximity" then + handleInitialState() + end +end) + + +AddEventHandler("onClientResourceStop", function(resource) + if type(addProximityCheck) == "table" then + local proximityCheckRef = addProximityCheck.__cfx_functionReference + if proximityCheckRef then + local isResource = string.match(proximityCheckRef, resource) + if isResource then + addProximityCheck = orig_addProximityCheck + logger.warn('Reset proximity check to default, the original resource [%s] which provided the function restarted', resource) + end + end + end +end) + +exports("addVoiceMode", function(distance, name) + for i = 1, #Cfg.voiceModes do + local voiceMode = Cfg.voiceModes[i] + if voiceMode[2] == name then + logger.verbose("Already had %s, overwritting instead", name) + voiceMode[1] = distance + return + end + end + Cfg.voiceModes[#Cfg.voiceModes + 1] = {distance, name} +end) + +exports("removeVoiceMode", function(name) + for i = 1, #Cfg.voiceModes do + local voiceMode = Cfg.voiceModes[i] + if voiceMode[2] == name then + table.remove(Cfg.voiceModes, i) + -- Reset our current range if we had it + if mode == i then + local newMode = Cfg.voiceModes[1] + mode = 1 + setProximityState(newMode[i], false) + end + return true + end + end + + return false +end) diff --git a/resources/[voice]/pma-voice/client/init/submix.lua b/resources/[voice]/pma-voice/client/init/submix.lua new file mode 100644 index 0000000..2fc1332 --- /dev/null +++ b/resources/[voice]/pma-voice/client/init/submix.lua @@ -0,0 +1,14 @@ +AddStateBagChangeHandler("submix", "", function(bagName, _, value) + local tgtId = tonumber(bagName:gsub('player:', ''), 10) + if not tgtId then return end + logger.info("%s had their submix set to %s", tgtId, value) + -- We got an invalid submix, discard we don't care about it + if value and not submixIndicies[value] then return logger.warn("Player %s applied submix %s but it isn't valid", tgtId, value) end + -- we don't want to reset submix if the player is talking on the radio + if not value and not radioData[tgtId] and not callData[tgtId] then + logger.info("Resetting submix for player %s", tgtId) + MumbleSetSubmixForServerId(tgtId, -1) + return + end + MumbleSetSubmixForServerId(tgtId, submixIndicies[value]) +end) diff --git a/resources/[voice]/pma-voice/client/module/phone.lua b/resources/[voice]/pma-voice/client/module/phone.lua new file mode 100644 index 0000000..946342c --- /dev/null +++ b/resources/[voice]/pma-voice/client/module/phone.lua @@ -0,0 +1,63 @@ +local callChannel = 0 + +RegisterNetEvent('pma-voice:syncCallData', function(callTable, channel) + callData = callTable + for tgt, _ in pairs(callTable) do + if tgt ~= playerServerId then + toggleVoice(tgt, true, 'call') + end + end +end) + +RegisterNetEvent('pma-voice:addPlayerToCall', function(plySource) + toggleVoice(plySource, true, 'call') + callData[plySource] = true +end) + +RegisterNetEvent('pma-voice:removePlayerFromCall', function(plySource) + if plySource == playerServerId then + for tgt, _ in pairs(callData) do + if tgt ~= playerServerId then + toggleVoice(tgt, false, 'call') + end + end + callData = {} + MumbleClearVoiceTargetPlayers(voiceTarget) + playerTargets(radioPressed and radioData or {}, callData) + else + callData[plySource] = nil + toggleVoice(plySource, false, 'call') + if MumbleIsPlayerTalking(PlayerId()) then + MumbleClearVoiceTargetPlayers(voiceTarget) + playerTargets(radioPressed and radioData or {}, callData) + end + end +end) + +function setCallChannel(channel) + if GetConvarInt('voice_enableCalls', 1) ~= 1 then return end + TriggerServerEvent('pma-voice:setPlayerCall', channel) + callChannel = channel + sendUIMessage({ + callInfo = channel + }) +end + +exports('setCallChannel', setCallChannel) +exports('SetCallChannel', setCallChannel) + +exports('addPlayerToCall', function(_call) + local call = tonumber(_call) + if call then + setCallChannel(call) + end +end) +exports('removePlayerFromCall', function() + setCallChannel(0) +end) + +RegisterNetEvent('pma-voice:clSetPlayerCall', function(_callChannel) + if GetConvarInt('voice_enableCalls', 1) ~= 1 then return end + callChannel = _callChannel + createCallThread() +end) diff --git a/resources/[voice]/pma-voice/client/module/radio.lua b/resources/[voice]/pma-voice/client/module/radio.lua new file mode 100644 index 0000000..d4cca99 --- /dev/null +++ b/resources/[voice]/pma-voice/client/module/radio.lua @@ -0,0 +1,209 @@ +local radioChannel = 0 +local radioNames = {} +local disableRadioAnim = false + +--- event syncRadioData +--- syncs the current players on the radio to the client +---@param radioTable table the table of the current players on the radio +---@param localPlyRadioName string the local players name +function syncRadioData(radioTable, localPlyRadioName) + radioData = radioTable + logger.info('[radio] Syncing radio table.') + if GetConvarInt('voice_debugMode', 0) >= 4 then + print('-------- RADIO TABLE --------') + tPrint(radioData) + print('-----------------------------') + end + for tgt, enabled in pairs(radioTable) do + if tgt ~= playerServerId then + toggleVoice(tgt, enabled, 'radio') + end + end + sendUIMessage({ + radioChannel = radioChannel, + radioEnabled = radioEnabled + }) + if GetConvarInt("voice_syncPlayerNames", 0) == 1 then + radioNames[playerServerId] = localPlyRadioName + end +end +RegisterNetEvent('pma-voice:syncRadioData', syncRadioData) + +--- event setTalkingOnRadio +--- sets the players talking status, triggered when a player starts/stops talking. +---@param plySource number the players server id. +---@param enabled boolean whether the player is talking or not. +function setTalkingOnRadio(plySource, enabled) + toggleVoice(plySource, enabled, 'radio') + radioData[plySource] = enabled + playMicClicks(enabled) +end +RegisterNetEvent('pma-voice:setTalkingOnRadio', setTalkingOnRadio) + +--- event addPlayerToRadio +--- adds a player onto the radio. +---@param plySource number the players server id to add to the radio. +function addPlayerToRadio(plySource, plyRadioName) + radioData[plySource] = false + if GetConvarInt("voice_syncPlayerNames", 0) == 1 then + radioNames[plySource] = plyRadioName + end + if radioPressed then + logger.info('[radio] %s joined radio %s while we were talking, adding them to targets', plySource, radioChannel) + playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {}) + else + logger.info('[radio] %s joined radio %s', plySource, radioChannel) + end +end +RegisterNetEvent('pma-voice:addPlayerToRadio', addPlayerToRadio) + +--- event removePlayerFromRadio +--- removes the player (or self) from the radio +---@param plySource number the players server id to remove from the radio. +function removePlayerFromRadio(plySource) + if plySource == playerServerId then + logger.info('[radio] Left radio %s, cleaning up.', radioChannel) + for tgt, _ in pairs(radioData) do + if tgt ~= playerServerId then + toggleVoice(tgt, false, 'radio') + end + end + sendUIMessage({ + radioChannel = 0, + radioEnabled = radioEnabled + }) + radioNames = {} + radioData = {} + playerTargets(MumbleIsPlayerTalking(PlayerId()) and callData or {}) + else + toggleVoice(plySource, false , 'radio') + if radioPressed then + logger.info('[radio] %s left radio %s while we were talking, updating targets.', plySource, radioChannel) + playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {}) + else + logger.info('[radio] %s has left radio %s', plySource, radioChannel) + end + radioData[plySource] = nil + if GetConvarInt("voice_syncPlayerNames", 0) == 1 then + radioNames[plySource] = nil + end + end +end +RegisterNetEvent('pma-voice:removePlayerFromRadio', removePlayerFromRadio) + +--- function setRadioChannel +--- sets the local players current radio channel and updates the server +---@param channel number the channel to set the player to, or 0 to remove them. +function setRadioChannel(channel) + if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end + type_check({channel, "number"}) + TriggerServerEvent('pma-voice:setPlayerRadio', channel) + radioChannel = channel +end + +--- exports setRadioChannel +--- sets the local players current radio channel and updates the server +exports('setRadioChannel', setRadioChannel) +-- mumble-voip compatability +exports('SetRadioChannel', setRadioChannel) + +--- exports removePlayerFromRadio +--- sets the local players current radio channel and updates the server +exports('removePlayerFromRadio', function() + setRadioChannel(0) +end) + +--- exports addPlayerToRadio +--- sets the local players current radio channel and updates the server +---@param _radio number the channel to set the player to, or 0 to remove them. +exports('addPlayerToRadio', function(_radio) + local radio = tonumber(_radio) + if radio then + setRadioChannel(radio) + end +end) + +--- exports toggleRadioAnim +--- toggles whether the client should play radio anim or not, if the animation should be played or notvaliddance +exports('toggleRadioAnim', function() + disableRadioAnim = not disableRadioAnim + TriggerEvent('pma-voice:toggleRadioAnim', disableRadioAnim) +end) + +-- exports disableRadioAnim +--- returns whether the client is undercover or not +exports('getRadioAnimState', function() + return disableRadioAnim +end) + +--- check if the player is dead +--- seperating this so if people use different methods they can customize +--- it to their need as this will likely never be changed +--- but you can integrate the below state bag to your death resources. +--- LocalPlayer.state:set('isDead', true or false, false) +function isDead() + if LocalPlayer.state.isDead then + return true + elseif IsPlayerDead(PlayerId()) then + return true + end +end + +RegisterCommand('+radiotalk', function() + if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end + if isDead() or LocalPlayer.state.disableRadio then return end + + if not radioPressed and radioEnabled then + if radioChannel > 0 then + logger.info('[radio] Start broadcasting, update targets and notify server.') + playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {}) + TriggerServerEvent('pma-voice:setTalkingOnRadio', true) + radioPressed = true + playMicClicks(true) + if GetConvarInt('voice_enableRadioAnim', 0) == 1 and IsPedInAnyVehicle(PlayerPedId(), false) and not disableRadioAnim then + RequestAnimDict('random@arrests') + while not HasAnimDictLoaded('random@arrests') do + Wait(10) + end + TaskPlayAnim(PlayerPedId(), "random@arrests", "generic_radio_enter", 8.0, 2.0, -1, 50, 2.0, false, false, false) + end + CreateThread(function() + TriggerEvent("pma-voice:radioActive", true) + while radioPressed and not LocalPlayer.state.disableRadio do + Wait(0) + SetControlNormal(0, 249, 1.0) + SetControlNormal(1, 249, 1.0) + SetControlNormal(2, 249, 1.0) + end + end) + end + end + end, false) + + +RegisterCommand('-radiotalk', function() + if (radioChannel > 0 or radioEnabled) and radioPressed then + radioPressed = false + MumbleClearVoiceTargetPlayers(voiceTarget) + playerTargets(MumbleIsPlayerTalking(PlayerId()) and callData or {}) + TriggerEvent("pma-voice:radioActive", false) + playMicClicks(false) + if GetConvarInt('voice_enableRadioAnim', 0) == 1 then + StopAnimTask(PlayerPedId(), "random@arrests", "generic_radio_enter", -4.0) + end + TriggerServerEvent('pma-voice:setTalkingOnRadio', false) + end +end, false) +if gameVersion == 'fivem' then + RegisterKeyMapping('+radiotalk', 'Talk over Radio', 'keyboard', GetConvar('voice_defaultRadio', 'LMENU')) +end + +--- event syncRadio +--- syncs the players radio, only happens if the radio was set server side. +---@param _radioChannel number the radio channel to set the player to. +function syncRadio(_radioChannel) + if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end + logger.info('[radio] radio set serverside update to radio %s', radioChannel) + radioChannel = _radioChannel +end +RegisterNetEvent('pma-voice:clSetPlayerRadio', syncRadio) diff --git a/resources/[voice]/pma-voice/client/utils/Nui.lua b/resources/[voice]/pma-voice/client/utils/Nui.lua new file mode 100644 index 0000000..dd9e914 --- /dev/null +++ b/resources/[voice]/pma-voice/client/utils/Nui.lua @@ -0,0 +1,11 @@ +local uiReady = promise.new() +function sendUIMessage(message) + Citizen.Await(uiReady) + SendNUIMessage(message) +end + +RegisterNUICallback("uiReady", function(data, cb) + uiReady:resolve(true) + + cb('ok') +end) diff --git a/resources/[voice]/pma-voice/docs/_config.yml b/resources/[voice]/pma-voice/docs/_config.yml new file mode 100644 index 0000000..1885487 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-midnight \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/client-getters/events.md b/resources/[voice]/pma-voice/docs/client-getters/events.md new file mode 100644 index 0000000..9612437 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-getters/events.md @@ -0,0 +1,27 @@ +## setTalkingMode | settingsCallback | radioACtive + +## Description + +These event is designed to allow third part applications (like a hud) use the current voice mode of the player, radio state, etc. + +```lua +-- default voice mode is 2 +local voiceMode = 2 +local voiceModes = {} +local usingRadio = false +-- sets the current radio state boolean +AddEventHandler("pma-voice:radioActive", function(radioTalking) usingRadio = radioTalking end) +-- changes the current voice range index +AddEventHandler('pma-voice:setTalkingMode', function(newTalkingRange) voiceMode = newTalkingRange end) +-- returns registered voice modes from shared.lua's `Cfg.voiceModes` +TriggerEvent("pma-voice:settingsCallback", function(voiceSettings) + local voiceTable = voiceSettings.voiceModes + + -- loop through all voice modes and add them to the table + -- the percentage is used for the voice mode slider if this was an actual UI + for i = 1, #voiceTable do + local distance = math.ceil(((i/#voiceTable) * 100)) + voiceModes[i] = ("%s"):format(distance) + end +end) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/client-setters/removePlayerFromCall.md b/resources/[voice]/pma-voice/docs/client-setters/removePlayerFromCall.md new file mode 100644 index 0000000..638728d --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-setters/removePlayerFromCall.md @@ -0,0 +1,12 @@ +## removePlayerFromCall + +## Description + +Removes the player from the call + +## NOTE: This is just syntactic sugar for `setCallChannel(0)` + +```lua +-- Removes the player from the call channel +exports['pma-voice']:removePlayerFromCall() +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/client-setters/removePlayerFromRadio.md b/resources/[voice]/pma-voice/docs/client-setters/removePlayerFromRadio.md new file mode 100644 index 0000000..a15fd7a --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-setters/removePlayerFromRadio.md @@ -0,0 +1,12 @@ +## removePlayerFromRadio + +## Description + +Removes the player from the radio + +## NOTE: This is just syntactic sugar for `setRadioChannel(0)` + +```lua +-- Removes the player from the radio channel +exports['pma-voice']:removePlayerFromRadio() +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/client-setters/setCallChannel.md b/resources/[voice]/pma-voice/docs/client-setters/setCallChannel.md new file mode 100644 index 0000000..e2d98c4 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-setters/setCallChannel.md @@ -0,0 +1,25 @@ +## setCallChannel | addPlayerToCall | SetCallChannel + +## Description + +Sets the local players call channel. + +## Parameters + +* **callChannel**: the call channel to join + + +```lua +-- Joins call channel 1 +exports['pma-voice']:setCallChannel(1) + +-- This will remove them from the call channel +exports['pma-voice']:setCallChannel(0) +``` + +addPlayerToCall is provided as a 'easier to read' version of setCallChannel. + +```lua +-- Joins call channel 1 +exports['pma-voice']:addPlayerToCall(1) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/client-setters/setCallVolume.md b/resources/[voice]/pma-voice/docs/client-setters/setCallVolume.md new file mode 100644 index 0000000..93509b9 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-setters/setCallVolume.md @@ -0,0 +1,14 @@ +## setCallVolume + +## Description + +Sets the local players call channel volume + +## Parameters + +* **callVolume**: the call volume to set to between 0 - 100 percent + +```lua +-- set the call volume to 50 percent +exports['pma-voice']:setCallVolume(50) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/client-setters/setRadioChannel.md b/resources/[voice]/pma-voice/docs/client-setters/setRadioChannel.md new file mode 100644 index 0000000..ee0ea37 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-setters/setRadioChannel.md @@ -0,0 +1,26 @@ +## setRadioChannel | addPlayerToRadio | SetCallChannel + +## Description + +Sets the local players radio channel. + +## Parameters + +* **radioChannel**: the radio channel to join + +## NOTE: If the player fails the server side radio channel check they will be reset to no channel. + +```lua +-- Joins radio channel 1 +exports['pma-voice']:setRadioChannel(1) + +-- This will remove the player from all radio channels +exports ['pma-voice']:setRadioChannel(0) +``` + +addPlayerToRadio is provided as a 'easier to read' alternative to setRadioChannel. + +```lua +-- Joins radio channel 1 +exports['pma-voice']:addPlayerToRadio(1) +``` diff --git a/resources/[voice]/pma-voice/docs/client-setters/setRadioVolume.md b/resources/[voice]/pma-voice/docs/client-setters/setRadioVolume.md new file mode 100644 index 0000000..4f093cf --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-setters/setRadioVolume.md @@ -0,0 +1,14 @@ +## setRadioVolume + +## Description + +Sets the local players radio channel volume + +## Parameters + +* **radioVolume**: the radio volume to set to between 0 - 100 percent + +```lua +-- sets the radio volume to 50 percent +exports['pma-voice']:setRadioVolume(50) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/client-setters/setVoiceProperty.md b/resources/[voice]/pma-voice/docs/client-setters/setVoiceProperty.md new file mode 100644 index 0000000..737e961 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/client-setters/setVoiceProperty.md @@ -0,0 +1,17 @@ +## setVoiceProperty | SetMumbleProperty | SetTokoProperty + +## Description + +Sets the voice property, currently the only use is to enable/disable radios and radio clicks. + +## Parameters + +* **property**: The property to set +* **value**: The value to set the property to + +```lua +-- Enable the radio +exports['pma-voice']:setVoiceProperty('radioEnabled', true) +-- Disable radio clicks +exports['pma-voice']:setVoiceProperty('micClicks', false) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/routingBuckets.md b/resources/[voice]/pma-voice/docs/routingBuckets.md new file mode 100644 index 0000000..f9e4229 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/routingBuckets.md @@ -0,0 +1,3 @@ +## Routing Buckets + +pma-voice natively supports routing buckets. \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/server-getters/getPlayersInRadioChannel.md b/resources/[voice]/pma-voice/docs/server-getters/getPlayersInRadioChannel.md new file mode 100644 index 0000000..e0f9bb0 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/server-getters/getPlayersInRadioChannel.md @@ -0,0 +1,21 @@ +## getPlayersInRadioChannel + +## Description + +Gets a list of all of the players in the specified radio channel. + +## Parameters + +* **radioChannel**: The channel to get all the members of + +## Returns + +Returns a table of all of the players in the specified radio channel + +```lua +-- this will return all of the current players in radio channel 1 +local players = exports['pma-voice']:getPlayersInRadioChannel(1) +for source, isTalking in pairs(players) do + print(('%s is in radio channel 1, isTalking: %s'):format(GetPlayerName(source), isTalking)) +end +``` diff --git a/resources/[voice]/pma-voice/docs/server-setters/addChannelCheck.md b/resources/[voice]/pma-voice/docs/server-setters/addChannelCheck.md new file mode 100644 index 0000000..59e396e --- /dev/null +++ b/resources/[voice]/pma-voice/docs/server-setters/addChannelCheck.md @@ -0,0 +1,22 @@ +## addChannelCheck + +## Description + +Adds a channel check to radio channels. + +## Parameters + +* **channel**: The channel to add the check to. +* **function**: the function to call when the check is triggered, which should return a boolean of if the player is allowed to join the channel.. + + +```lua +-- Example for addChannelCheck +-- this always has to return true/false +exports['pma-voice']:addChannelCheck(1, function(source) + if IsPlayerAceAllowed(source, 'radio.police') then + return true + end + return false +end) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/server-setters/setPlayerCall.md b/resources/[voice]/pma-voice/docs/server-setters/setPlayerCall.md new file mode 100644 index 0000000..db5ae7e --- /dev/null +++ b/resources/[voice]/pma-voice/docs/server-setters/setPlayerCall.md @@ -0,0 +1,14 @@ +## setPlayerCall + +## Description + +Sets the players call channel. + +## Parameters + +* **source**: The player to set the radio channel of +* **callChannel**: the radio channel to set the player to + +```lua +exports['pma-voice']:setPlayerCall(source, 1) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/server-setters/setPlayerRadio.md b/resources/[voice]/pma-voice/docs/server-setters/setPlayerRadio.md new file mode 100644 index 0000000..11c8db2 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/server-setters/setPlayerRadio.md @@ -0,0 +1,14 @@ +## setPlayerRadio + +## Description + +Sets the players radio channel. + +## Parameters + +* **source**: The player to set the radio channel of +* **radioChannel**: the radio channel to set the player to + +```lua +exports['pma-voice']:setPlayerRadio(source, 1) +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/docs/state-getters/stateBagGetters.md b/resources/[voice]/pma-voice/docs/state-getters/stateBagGetters.md new file mode 100644 index 0000000..c21e878 --- /dev/null +++ b/resources/[voice]/pma-voice/docs/state-getters/stateBagGetters.md @@ -0,0 +1,17 @@ +## State Bag Getters/Setters + +## Description + +State bag getters are a little bit simpler, they just return the current value that is set in the state bag. + +#### Note: If you're on the client and only using it on the current player, you can replace Player(source) with LocalPlayer + +## Example for Proximity + +```lua +local plyState = Player(source).state +local proximity = plyState.proximity +print(proximity.index) -- prints the index of the proximity as seen in Cfg.voiceModes +print(proximity.distance) -- prints the distance of the proximity +print(proximity.mode) -- prints the mode name of the proximity +``` \ No newline at end of file diff --git a/resources/[voice]/pma-voice/fxmanifest.lua b/resources/[voice]/pma-voice/fxmanifest.lua new file mode 100644 index 0000000..073bb0f --- /dev/null +++ b/resources/[voice]/pma-voice/fxmanifest.lua @@ -0,0 +1,70 @@ +game 'common' + +fx_version 'cerulean' +author 'AvarianKnight' +description 'VOIP built using FiveM\'s built in mumble.' + +dependencies { + '/onesync', +} + +lua54 'yes' + +shared_script 'shared.lua' + +client_scripts { + 'client/utils/*', + 'client/init/proximity.lua', + 'client/init/init.lua', + 'client/init/main.lua', + 'client/init/submix.lua', + 'client/module/*.lua', + 'client/*.lua', +} + +server_scripts { + 'server/**/*.lua', + 'server/**/*.js' +} + +files { + 'ui/*.ogg', + 'ui/css/*.css', + 'ui/js/*.js', + 'ui/index.html', +} + +ui_page 'ui/index.html' + +provides { + 'mumble-voip', + 'tokovoip', + 'toko-voip', + 'tokovoip_script' +} + +convar_category 'PMA-Voice' { + "PMA-Voice Configuration Options", + { + { "Use native audio", "$voice_useNativeAudio", "CV_BOOL", "false" }, + { "Use 2D audio", "$voice_use2dAudio", "CV_BOOL", "false" }, + { "Use sending range only", "$voice_useSendingRangeOnly", "CV_BOOL", "false" }, + { "Enable UI", "$voice_enableUi", "CV_INT", "1" }, + { "Enable F11 proximity key", "$voice_enableProximityCycle", "CV_INT", "1" }, + { "Proximity cycle key", "$voice_defaultCycle", "CV_STRING", "F11" }, + { "Voice radio volume", "$voice_defaultRadioVolume", "CV_INT", "30" }, + { "Voice call volume", "$voice_defaultCallVolume", "CV_INT", "60" }, + { "Enable radios", "$voice_enableRadios", "CV_INT", "1" }, + { "Enable calls", "$voice_enableCalls", "CV_INT", "1" }, + { "Enable submix", "$voice_enableSubmix", "CV_INT", "1" }, + { "Enable radio animation", "$voice_enableRadioAnim", "CV_INT", "0" }, + { "Radio key", "$voice_defaultRadio", "CV_STRING", "LMENU" }, + { "UI refresh rate", "$voice_uiRefreshRate", "CV_INT", "200" }, + { "Allow players to set audio intent", "$voice_allowSetIntent", "CV_INT", "1" }, + { "External mumble server address", "$voice_externalAddress", "CV_STRING", "" }, + { "External mumble server port", "$voice_externalPort", "CV_INT", "0" }, + { "Voice debug mode", "$voice_debugMode", "CV_INT", "0" }, + { "Disable players being allowed to join", "$voice_externalDisallowJoin", "CV_INT", "0" }, + { "Hide server endpoints in logs", "$voice_hideEndpoints", "CV_INT", "1" }, + } +} diff --git a/resources/[voice]/pma-voice/server/main.lua b/resources/[voice]/pma-voice/server/main.lua new file mode 100644 index 0000000..28947b3 --- /dev/null +++ b/resources/[voice]/pma-voice/server/main.lua @@ -0,0 +1,140 @@ +voiceData = {} +radioData = {} +callData = {} + +function defaultTable(source) + handleStateBagInitilization(source) + return { + radio = 0, + call = 0, + lastRadio = 0, + lastCall = 0 + } +end + +function handleStateBagInitilization(source) + local plyState = Player(source).state + if not plyState.pmaVoiceInit then + plyState:set('radio', GetConvarInt('voice_defaultRadioVolume', 30), true) + plyState:set('call', GetConvarInt('voice_defaultCallVolume', 60), true) + plyState:set('submix', nil, true) + plyState:set('proximity', {}, true) + plyState:set('callChannel', 0, true) + plyState:set('radioChannel', 0, true) + plyState:set('voiceIntent', 'speech', true) + -- We want to save voice inits because we'll automatically reinitalize calls and channels + plyState:set('pmaVoiceInit', true, false) + end +end + +CreateThread(function() + + local plyTbl = GetPlayers() + for i = 1, #plyTbl do + local ply = tonumber(plyTbl[i]) + voiceData[ply] = defaultTable(plyTbl[i]) + end + + Wait(5000) + + local nativeAudio = GetConvar('voice_useNativeAudio', 'false') + local _3dAudio = GetConvar('voice_use3dAudio', 'false') + local _2dAudio = GetConvar('voice_use2dAudio', 'false') + local sendingRangeOnly = GetConvar('voice_useSendingRangeOnly', 'false') + local gameVersion = GetConvar('gamename', 'fivem') + + -- handle no convars being set (default drag n' drop) + if + nativeAudio == 'false' + and _3dAudio == 'false' + and _2dAudio == 'false' + then + if gameVersion == 'fivem' then + SetConvarReplicated('voice_useNativeAudio', 'true') + if sendingRangeOnly == 'false' then + SetConvarReplicated('voice_useSendingRangeOnly', 'true') + end + logger.info('No convars detected for voice mode, defaulting to \'setr voice_useNativeAudio true\' and \'setr voice_useSendingRangeOnly true\'') + else + SetConvarReplicated('voice_use3dAudio', 'true') + if sendingRangeOnly == 'false' then + SetConvarReplicated('voice_useSendingRangeOnly', 'true') + end + logger.info('No convars detected for voice mode, defaulting to \'setr voice_use3dAudio true\' and \'setr voice_useSendingRangeOnly true\'') + end + elseif sendingRangeOnly == 'false' then + logger.warn('It\'s recommended to have \'voice_useSendingRangeOnly\' set to true you can do that with \'setr voice_useSendingRangeOnly true\', this prevents players who directly join the mumble server from broadcasting to players.') + end + + if GetConvar('gamename', 'fivem') == 'rdr3' then + if nativeAudio == 'true' then + logger.warn("RedM doesn't currently support native audio, automatically switching to 3d audio. This also means that submixes will not work.") + SetConvarReplicated('voice_useNativeAudio', 'false') + SetConvarReplicated('voice_use3dAudio', 'true') + end + end + + local radioVolume = GetConvarInt("voice_defaultRadioVolume", 30) + local callVolume = GetConvarInt("voice_defaultCallVolume", 60) + + -- When casted to an integer these get set to 0 or 1, so warn on these values that they don't work + if + radioVolume == 0 or radioVolume == 1 or + callVolume == 0 or callVolume == 1 + then + SetConvarReplicated("voice_defaultRadioVolume", 30) + SetConvarReplicated("voice_defaultCallVolume", 60) + for i = 1, 5 do + Wait(5000) + logger.warn("`voice_defaultRadioVolume` or `voice_defaultCallVolume` have their value set as a float, this is going to automatically be fixed but please update your convars.") + end + end +end) + +AddEventHandler('playerJoining', function() + if not voiceData[source] then + voiceData[source] = defaultTable(source) + end +end) + +AddEventHandler("playerDropped", function() + local source = source + if voiceData[source] then + local plyData = voiceData[source] + + if plyData.radio ~= 0 then + removePlayerFromRadio(source, plyData.radio) + end + + if plyData.call ~= 0 then + removePlayerFromCall(source, plyData.call) + end + + voiceData[source] = nil + end +end) + +if GetConvarInt('voice_externalDisallowJoin', 0) == 1 then + AddEventHandler('playerConnecting', function(_, _, deferral) + deferral.defer() + Wait(0) + deferral.done('This server is not accepting connections.') + end) +end + +-- only meant for internal use so no documentation +function isValidPlayer(source) + return voiceData[source] +end +exports('isValidPlayer', isValidPlayer) + +function getPlayersInRadioChannel(channel) + local returnChannel = radioData[channel] + if returnChannel then + return returnChannel + end + -- channel doesnt exist + return {} +end +exports('getPlayersInRadioChannel', getPlayersInRadioChannel) +exports('GetPlayersInRadioChannel', getPlayersInRadioChannel) diff --git a/resources/[voice]/pma-voice/server/module/phone.lua b/resources/[voice]/pma-voice/server/module/phone.lua new file mode 100644 index 0000000..0344852 --- /dev/null +++ b/resources/[voice]/pma-voice/server/module/phone.lua @@ -0,0 +1,74 @@ +--- removes a player from the call for everyone in the call. +---@param source number the player to remove from the call +---@param callChannel number the call channel to remove them from +function removePlayerFromCall(source, callChannel) + logger.verbose('[call] Removed %s from call %s', source, callChannel) + + callData[callChannel] = callData[callChannel] or {} + for player, _ in pairs(callData[callChannel]) do + TriggerClientEvent('pma-voice:removePlayerFromCall', player, source) + end + callData[callChannel][source] = nil + voiceData[source] = voiceData[source] or defaultTable(source) + voiceData[source].call = 0 +end + +--- adds a player to a call +---@param source number the player to add to the call +---@param callChannel number the call channel to add them to +function addPlayerToCall(source, callChannel) + logger.verbose('[call] Added %s to call %s', source, callChannel) + -- check if the channel exists, if it does set the varaible to it + -- if not create it (basically if not callData make callData) + callData[callChannel] = callData[callChannel] or {} + for player, _ in pairs(callData[callChannel]) do + -- don't need to send to the source because they're about to get sync'd! + if player ~= source then + TriggerClientEvent('pma-voice:addPlayerToCall', player, source) + end + end + callData[callChannel][source] = true + voiceData[source] = voiceData[source] or defaultTable(source) + voiceData[source].call = callChannel + TriggerClientEvent('pma-voice:syncCallData', source, callData[callChannel]) +end + +--- set the players call channel +---@param source number the player to set the call off +---@param _callChannel number the channel to set the player to (or 0 to remove them from any call channel) +function setPlayerCall(source, _callChannel) + if GetConvarInt('voice_enableCalls', 1) ~= 1 then return end + voiceData[source] = voiceData[source] or defaultTable(source) + local isResource = GetInvokingResource() + local plyVoice = voiceData[source] + local callChannel = tonumber(_callChannel) + if not callChannel then + -- only full error if its sent from another server-side resource + if isResource then + error(("'callChannel' expected 'number', got: %s"):format(type(_callChannel))) + else + return logger.warn("%s sent a invalid call, 'callChannel' expected 'number', got: %s", source,type(_callChannel)) + end + end + if isResource then + -- got set in a export, need to update the client to tell them that their call + -- changed + TriggerClientEvent('pma-voice:clSetPlayerCall', source, callChannel) + end + + Player(source).state.callChannel = callChannel + + if callChannel ~= 0 and plyVoice.call == 0 then + addPlayerToCall(source, callChannel) + elseif callChannel == 0 then + removePlayerFromCall(source, plyVoice.call) + elseif plyVoice.call > 0 then + removePlayerFromCall(source, plyVoice.call) + addPlayerToCall(source, callChannel) + end +end +exports('setPlayerCall', setPlayerCall) + +RegisterNetEvent('pma-voice:setPlayerCall', function(callChannel) + setPlayerCall(source, callChannel) +end) diff --git a/resources/[voice]/pma-voice/server/module/radio.lua b/resources/[voice]/pma-voice/server/module/radio.lua new file mode 100644 index 0000000..ca87920 --- /dev/null +++ b/resources/[voice]/pma-voice/server/module/radio.lua @@ -0,0 +1,165 @@ +local radioChecks = {} + +--- checks if the player can join the channel specified +--- @param source number the source of the player +--- @param radioChannel number the channel they're trying to join +--- @return boolean if the user can join the channel +function canJoinChannel(source, radioChannel) + if radioChecks[radioChannel] then + return radioChecks[radioChannel](source) + end + return true +end + +--- adds a check to the channel, function is expected to return a boolean of true or false +---@param channel number the channel to add a check to +---@param cb function the function to execute the check on +function addChannelCheck(channel, cb) + local channelType = type(channel) + local cbType = type(cb) + if channelType ~= "number" then + error(("'channel' expected 'number' got '%s'"):format(channelType)) + end + if cbType ~= 'table' or not cb.__cfx_functionReference then + error(("'cb' expected 'function' got '%s'"):format(cbType)) + end + radioChecks[channel] = cb + logger.info("%s added a check to channel %s", GetInvokingResource(), channel) +end +exports('addChannelCheck', addChannelCheck) + +local function radioNameGetter_orig(source) + return GetPlayerName(source) +end +local radioNameGetter = radioNameGetter_orig + +--- adds a check to the channel, function is expected to return a boolean of true or false +---@param cb function the function to execute the check on +function overrideRadioNameGetter(channel, cb) + local cbType = type(cb) + if cbType == 'table' and not cb.__cfx_functionReference then + error(("'cb' expected 'function' got '%s'"):format(cbType)) + end + radioNameGetter = cb + logger.info("%s added a check to channel %s", GetInvokingResource(), channel) +end +exports('overrideRadioNameGetter', overrideRadioNameGetter) + +--- adds a player to the specified radion channel +---@param source number the player to add to the channel +---@param radioChannel number the channel to set them to +function addPlayerToRadio(source, radioChannel) + if not canJoinChannel(source, radioChannel) then + -- remove the player from the radio client side + return TriggerClientEvent('pma-voice:removePlayerFromRadio', source, source) + end + logger.verbose('[radio] Added %s to radio %s', source, radioChannel) + + -- check if the channel exists, if it does set the varaible to it + -- if not create it (basically if not radiodata make radiodata) + radioData[radioChannel] = radioData[radioChannel] or {} + local plyName = radioNameGetter(source) + for player, _ in pairs(radioData[radioChannel]) do + TriggerClientEvent('pma-voice:addPlayerToRadio', player, source, plyName) + end + voiceData[source] = voiceData[source] or defaultTable(source) + voiceData[source].radio = radioChannel + radioData[radioChannel][source] = false + TriggerClientEvent('pma-voice:syncRadioData', source, radioData[radioChannel], GetConvarInt("voice_syncPlayerNames", 0) == 1 and plyName) +end + +--- removes a player from the specified channel +---@param source number the player to remove +---@param radioChannel number the current channel to remove them from +function removePlayerFromRadio(source, radioChannel) + logger.verbose('[radio] Removed %s from radio %s', source, radioChannel) + radioData[radioChannel] = radioData[radioChannel] or {} + for player, _ in pairs(radioData[radioChannel]) do + TriggerClientEvent('pma-voice:removePlayerFromRadio', player, source) + end + radioData[radioChannel][source] = nil + voiceData[source] = voiceData[source] or defaultTable(source) + voiceData[source].radio = 0 +end + +-- TODO: Implement this in a way that allows players to be on multiple channels +--- sets the players current radio channel +---@param source number the player to set the channel of +---@param _radioChannel number the radio channel to set them to (or 0 to remove them from radios) +function setPlayerRadio(source, _radioChannel) + if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end + voiceData[source] = voiceData[source] or defaultTable(source) + local isResource = GetInvokingResource() + local plyVoice = voiceData[source] + local radioChannel = tonumber(_radioChannel) + if not radioChannel then + -- only full error if its sent from another server-side resource + if isResource then + error(("'radioChannel' expected 'number', got: %s"):format(type(_radioChannel))) + else + return logger.warn("%s sent a invalid radio, 'radioChannel' expected 'number', got: %s", source,type(_radioChannel)) + end + end + if isResource then + -- got set in a export, need to update the client to tell them that their radio + -- changed + TriggerClientEvent('pma-voice:clSetPlayerRadio', source, radioChannel) + end + Player(source).state.radioChannel = radioChannel + if radioChannel ~= 0 and plyVoice.radio == 0 then + addPlayerToRadio(source, radioChannel) + elseif radioChannel == 0 then + removePlayerFromRadio(source, plyVoice.radio) + elseif plyVoice.radio > 0 then + removePlayerFromRadio(source, plyVoice.radio) + addPlayerToRadio(source, radioChannel) + end +end +exports('setPlayerRadio', setPlayerRadio) + +RegisterNetEvent('pma-voice:setPlayerRadio', function(radioChannel) + setPlayerRadio(source, radioChannel) +end) + +--- syncs the player talking across all radio members +---@param talking boolean sets if the palyer is talking. +function setTalkingOnRadio(talking) + if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end + voiceData[source] = voiceData[source] or defaultTable(source) + local plyVoice = voiceData[source] + local radioTbl = radioData[plyVoice.radio] + if radioTbl then + radioTbl[source] = talking + logger.verbose('[radio] Set %s to talking: %s on radio %s',source, talking, plyVoice.radio) + for player, _ in pairs(radioTbl) do + if player ~= source then + TriggerClientEvent('pma-voice:setTalkingOnRadio', player, source, talking) + logger.verbose('[radio] Sync %s to let them know %s is %s',player, source, talking and 'talking' or 'not talking') + end + end + end +end +RegisterNetEvent('pma-voice:setTalkingOnRadio', setTalkingOnRadio) + +AddEventHandler("onResourceStop", function(resource) + for channel, cfxFunctionRef in pairs(radioChecks) do + local functionRef = cfxFunctionRef.__cfx_functionReference + local functionResource = string.match(functionRef, resource) + if functionResource then + radioChecks[channel] = nil + logger.warn('Channel %s had its radio check removed because the resource that gave the checks stopped', channel) + end + end + + if type(radioNameGetter) == "table" then + local radioRef = radioNameGetter.__cfx_functionReference + if radioRef then + local isResource = string.match(functionRef, resource) + if isResource then + radioNameGetter = radioNameGetter_orig + logger.warn('Radio name getter is resetting to default because the resource that gave the cb got turned off') + end + end + end + +end) \ No newline at end of file diff --git a/resources/[voice]/pma-voice/server/mute.js b/resources/[voice]/pma-voice/server/mute.js new file mode 100644 index 0000000..536b61e --- /dev/null +++ b/resources/[voice]/pma-voice/server/mute.js @@ -0,0 +1,26 @@ +let mutedPlayers = {} +// this is implemented in JS due to Lua's lack of a ClearTimeout +// muteply instead of mute because mute conflicts with rp-radio +RegisterCommand('muteply', (source, args) => { + const mutePly = parseInt(args[0]) + const duration = parseInt(args[1]) || 900 + if (mutePly && exports['pma-voice'].isValidPlayer(mutePly)) { + const isMuted = !MumbleIsPlayerMuted(mutePly); + Player(mutePly).state.muted = isMuted; + MumbleSetPlayerMuted(mutePly, isMuted); + emit('pma-voice:playerMuted', mutePly, source, isMuted, duration); + // since this is a toggle, if theres a mutedPlayers entry it can be assumed + // that they're currently muted, so we'll clear the timeout and unmute + if (mutedPlayers[mutePly]) { + clearTimeout(mutedPlayers[mutePly]); + MumbleSetPlayerMuted(mutePly, isMuted) + Player(mutePly).state.muted = isMuted; + return; + } + mutedPlayers[mutePly] = setTimeout(() => { + MumbleSetPlayerMuted(mutePly, !isMuted) + Player(mutePly).state.muted = !isMuted; + delete mutedPlayers[mutePly] + }, duration * 1000) + } +}, true) diff --git a/resources/[voice]/pma-voice/shared.lua b/resources/[voice]/pma-voice/shared.lua new file mode 100644 index 0000000..98b0de1 --- /dev/null +++ b/resources/[voice]/pma-voice/shared.lua @@ -0,0 +1,95 @@ +Cfg = {} + +voiceTarget = 1 + +gameVersion = GetGameName() + +-- these are just here to satisfy linting +if not IsDuplicityVersion() then + LocalPlayer = LocalPlayer + playerServerId = GetPlayerServerId(PlayerId()) +end +Player = Player +Entity = Entity + +if GetConvar('voice_useNativeAudio', 'false') == 'true' then + -- native audio distance seems to be larger then regular gta units + Cfg.voiceModes = { + {1.5, "Hvisker"}, -- Whisper speech distance in gta distance units + {3.0, "Normal"}, -- Normal speech distance in gta distance units + {6.0, "Råber"} -- Shout speech distance in gta distance units + } +else + Cfg.voiceModes = { + {3.0, "Hvisker"}, -- Whisper speech distance in gta distance units + {7.0, "Normal"}, -- Normal speech distance in gta distance units + {15.0, "Råber"} -- Shout speech distance in gta distance units + } +end + +logger = { + log = function(message, ...) + print((message):format(...)) + end, + info = function(message, ...) + if GetConvarInt('voice_debugMode', 0) >= 1 then + print(('[info] ' .. message):format(...)) + end + end, + warn = function(message, ...) + print(('[^1WARNING^7] ' .. message):format(...)) + end, + error = function(message, ...) + error((message):format(...)) + end, + verbose = function(message, ...) + if GetConvarInt('voice_debugMode', 0) >= 4 then + print(('[verbose] ' .. message):format(...)) + end + end, +} + + +function tPrint(tbl, indent) + indent = indent or 0 + for k, v in pairs(tbl) do + local tblType = type(v) + local formatting = string.rep(" ", indent) .. k .. ": " + + if tblType == "table" then + print(formatting) + tPrint(v, indent + 1) + elseif tblType == 'boolean' then + print(formatting .. tostring(v)) + elseif tblType == "function" then + print(formatting .. tostring(v)) + else + print(formatting .. v) + end + end +end + +local function types(args) + local argType = type(args[1]) + for i = 2, #args do + local arg = args[i] + if argType == arg then + return true, argType + end + end + return false, argType +end + +--- does a type check and errors if an invalid type is sent +---@param ... table a table with the variable being the first argument and the expected type being the second +function type_check(...) + local vars = {...} + for i = 1, #vars do + local var = vars[i] + local matchesType, varType = types(var) + if not matchesType then + table.remove(var, 1) + error(("Invalid type sent to argument #%s, expected %s, got %s"):format(i, table.concat(var, "|"), varType)) + end + end +end diff --git a/resources/[voice]/pma-voice/ui/css/app.css b/resources/[voice]/pma-voice/ui/css/app.css new file mode 100644 index 0000000..1a0b3a0 --- /dev/null +++ b/resources/[voice]/pma-voice/ui/css/app.css @@ -0,0 +1 @@ +.voiceInfo{font-family:Avenir,Helvetica,Arial,sans-serif;position:fixed;text-align:right;bottom:5px;padding:0;right:5px;font-size:12px;font-weight:700;color:#949697;text-shadow:1.25px 0 0 #000,0 -1.25px 0 #000,0 1.25px 0 #000,-1.25px 0 0 #000}.talking{color:hsla(0,0%,100%,.822)}p{margin:0} \ No newline at end of file diff --git a/resources/[voice]/pma-voice/ui/index.html b/resources/[voice]/pma-voice/ui/index.html new file mode 100644 index 0000000..8dc6f91 --- /dev/null +++ b/resources/[voice]/pma-voice/ui/index.html @@ -0,0 +1,23 @@ + + + +
+ + + + +\r\n\t\t\t\t[Call]\r\n\t\t\t
\r\n\t\t\t\r\n\t\t\t\t{{ voice.radioChannel }} Mhz [Radio]\r\n\t\t\t
\r\n\t\t\t\r\n\t\t\t\t{{ voice.voiceModes[voice.voiceMode][1] }} [Distance]\r\n\t\t\t
\r\n\t\tf?q(e,s,c,!0,!1,p):F(t,n,r,s,c,i,l,u,p)},$=(e,t,n,r,s,c,i,l,u)=>{let a=0;const f=t.length;let p=e.length-1,d=f-1;while(a<=p&&a<=d){const o=e[a],r=t[a]=u?Yn(t[a]):Qn(t[a]);if(!$n(o,r))break;g(o,r,n,null,s,c,i,l,u),a++}while(a<=p&&a<=d){const o=e[p],r=t[d]=u?Yn(t[d]):Qn(t[d]);if(!$n(o,r))break;g(o,r,n,null,s,c,i,l,u),p--,d--}if(a>p){if(a<=d){const e=d+1,o=e