0.0.1
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Did you read the README?**: There's a lot of information in the README, if you ask a question that is answered in the README, you will be told to "Read the README"
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See an error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Server Info:**
|
||||
- Server Version: [e.g. 3456]
|
||||
- Client Version: [e.g. production|canary]
|
||||
- Client Build: [e.g. b1604|b2060|b2189|b2372]
|
||||
- Game: [e.g. FiveM|RedM]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- todo
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
.vscode
|
||||
@@ -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.
|
||||
@@ -0,0 +1,201 @@
|
||||
## 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)
|
||||
|
||||
# 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]`
|
||||
|
||||
| ConVar | Default | Description | Parameter(s) |
|
||||
|----------------------------|---------|---------------------------------------------------------------|--------------|
|
||||
| voice_useNativeAudio | false | 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_onClickVolume | 10 | The default volume to set the radio turn on mic click's 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_offClickVolume | 3 | The default volume to set the radio turn off mic click's 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 | 1 | 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 |
|
||||
| [setMicClickOnVolume](docs/client-setters/setMicClickOnVolume.md) | Set mic click on volume for player | int |
|
||||
| [setMicClickOffVolume](docs/client-setters/setCallVolume.md) | Set mic click off 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.
|
||||
|
||||
|
||||
| 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 |
|
||||
| [disableRadio](docs/state-getters/stateBagGetters.md) | Returns if the players radio is currently disabled, or 0 if its not. This is expected to be use as a bitwise, do *not* use a bool | 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 |
|
||||
| [removeChannelCheck](docs/server-setters/removeChannelCheck.md) | Removes a channel check for the players radio channel | int |
|
||||
|
||||
##### 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 |
|
||||
| [disableRadio](docs/state-getters/stateBagGetters.md) | Returns if the players radio is currently disabled, or 0 if its not. This is expected to be use as a bitwise, do *not* use a bool | int |
|
||||
|
||||
```ts
|
||||
enum DisabledRadioStates {
|
||||
Enabled = 0,
|
||||
IsDead = 1,
|
||||
IsCuffed = 2,
|
||||
IsPdCuffed = 4,
|
||||
IsUnderWater = 8,
|
||||
DoesntHaveItem = 16,
|
||||
PlayerDisabledRadio = 32,
|
||||
}
|
||||
```
|
||||
|
||||
###### Exports
|
||||
|
||||
| Export | Description | Parameter(s) |
|
||||
|------------------------------|---------------------------------------------------|------|
|
||||
| [getPlayersInRadioChannel](docs/server-getters/getPlayersInRadioChannel.md) | Gets the current players in a radio channel | int |
|
||||
@@ -0,0 +1,5 @@
|
||||
## 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 Svelte.
|
||||
- [ ] Multiple radio channels.
|
||||
@@ -0,0 +1,107 @@
|
||||
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)
|
||||
TriggerEvent('chat:addSuggestion', '/setvoiceintent', 'Sets the players voice intent', {
|
||||
{
|
||||
name = "intent",
|
||||
help = "speech is default and enables noise suppression & high pass filter, music disables both of these."
|
||||
},
|
||||
})
|
||||
|
||||
-- TODO: Better implementation of this?
|
||||
RegisterCommand('vol', function(_, args)
|
||||
if not args[1] then return end
|
||||
setVolume(tonumber(args[1]))
|
||||
end)
|
||||
TriggerEvent('chat:addSuggestion', '/vol', 'Sets the radio/phone volume', {
|
||||
{ name = "volume", help = "A range between 1-100 on how loud you want them to be" },
|
||||
})
|
||||
|
||||
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', 'Cycle Proximity', 'keyboard', GetConvar('voice_defaultCycle', 'F11'))
|
||||
end
|
||||
|
||||
-- hacky workaround to the fact that you can't bind secondary key mappings to PTT
|
||||
if gameVersion == 'fivem' then
|
||||
local isSecondaryPttPressed = false
|
||||
|
||||
RegisterCommand("+secondary_ptt", function()
|
||||
isSecondaryPttPressed = true
|
||||
CreateThread(function()
|
||||
while isSecondaryPttPressed do
|
||||
SetControlNormal(0, 249, 1.0)
|
||||
SetControlNormal(1, 249, 1.0)
|
||||
SetControlNormal(2, 249, 1.0)
|
||||
|
||||
Wait(0)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterCommand("-secondary_ptt", function()
|
||||
isSecondaryPttPressed = false
|
||||
end)
|
||||
|
||||
RegisterKeyMapping('+secondary_ptt', 'A keybind that lets you have a secondary PTT', 'PAD_ANALOGBUTTOn', GetConvar('voice_defaultSecondary', 'LUP_INDEX'))
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
isInitialized = false
|
||||
|
||||
function handleInitialState()
|
||||
local voiceModeData = Cfg.voiceModes[mode]
|
||||
MumbleSetTalkerProximity(voiceModeData[1] + 0.0)
|
||||
MumbleClearVoiceTarget(voiceTarget)
|
||||
MumbleSetVoiceTarget(voiceTarget)
|
||||
MumbleSetVoiceChannel(LocalPlayer.state.assignedChannel)
|
||||
|
||||
while MumbleGetVoiceChannelFromServerId(playerServerId) ~= LocalPlayer.state.assignedChannel do
|
||||
Wait(100)
|
||||
MumbleSetVoiceChannel(LocalPlayer.state.assignedChannel)
|
||||
end
|
||||
|
||||
isInitialized = true
|
||||
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, LocalPlayer.state.assignedChannel)
|
||||
|
||||
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)
|
||||
isInitialized = false
|
||||
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)
|
||||
@@ -0,0 +1,50 @@
|
||||
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")
|
||||
micClicks = true
|
||||
else
|
||||
if micClicksKvp ~= 'true' and micClicksKvp ~= 'false' then
|
||||
error('Invalid Kvp, throwing error for automatic fix')
|
||||
end
|
||||
micClicks = micClicksKvp == "true"
|
||||
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
|
||||
if not LocalPlayer.state.disableRadio then
|
||||
LocalPlayer.state:set("disableRadio", 0, true)
|
||||
end
|
||||
|
||||
print('Script initialization finished.')
|
||||
end)
|
||||
@@ -0,0 +1,336 @@
|
||||
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,
|
||||
['click_on'] = GetConvarInt('voice_onClickVolume', 10) / 100,
|
||||
['click_off'] = GetConvarInt('voice_offClickVolume', 3) / 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 volumeFraction = volume / 100
|
||||
|
||||
if volumeType then
|
||||
local volumeTbl = volumes[volumeType]
|
||||
if volumeTbl then
|
||||
LocalPlayer.state:set(volumeType, volume, true)
|
||||
volumes[volumeType] = volumeFraction
|
||||
resyncVolume(volumeType, volumeFraction)
|
||||
else
|
||||
error(('setVolume got a invalid volume type %s'):format(volumeType))
|
||||
end
|
||||
else
|
||||
for volumeType, _ in pairs(volumes) do
|
||||
volumes[volumeType] = volumeFraction
|
||||
LocalPlayer.state:set(volumeType, volume, true)
|
||||
end
|
||||
resyncVolume("all", volumeFraction)
|
||||
end
|
||||
end
|
||||
|
||||
exports('setRadioVolume', function(vol)
|
||||
setVolume(vol, 'radio')
|
||||
end)
|
||||
exports('getRadioVolume', function()
|
||||
return volumes['radio'] * 100
|
||||
end)
|
||||
exports("setCallVolume", function(vol)
|
||||
setVolume(vol, 'call')
|
||||
end)
|
||||
exports('getCallVolume', function()
|
||||
return volumes['call'] * 100
|
||||
end)
|
||||
exports('setMicClickOnVolume', function(vol)
|
||||
setVolume(vol, 'click_on')
|
||||
end)
|
||||
exports('getMicClickOnVolume', function()
|
||||
return volumes['click_on'] * 100
|
||||
end)
|
||||
exports('setMicClickOffVolume', function(vol)
|
||||
setVolume(vol, 'click_off')
|
||||
end)
|
||||
exports('getMicClickOffVolume', function()
|
||||
return volumes['click_off'] * 100
|
||||
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
|
||||
-- o_freq_hi = 4900.0
|
||||
|
||||
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")
|
||||
|
||||
--- 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
|
||||
print(volumes[moduleType])
|
||||
MumbleSetVolumeOverrideByServerId(plySource, enabled and volumes[moduleType])
|
||||
if GetConvarInt('voice_enableSubmix', 1) == 1 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 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
|
||||
|
||||
---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 addVoiceTargets(...)
|
||||
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 volumes['click_on'] or volumes['click_off'])
|
||||
})
|
||||
end
|
||||
|
||||
--- check if player is muted
|
||||
exports('isPlayerMuted', function(source)
|
||||
return mutedPlayers[source]
|
||||
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
|
||||
handleRadioEnabledChanged(value)
|
||||
sendUIMessage({
|
||||
radioEnabled = value
|
||||
})
|
||||
elseif type == "micClicks" then
|
||||
micClicks = value == true or value == "true"
|
||||
SetResourceKvp('pma-voice_enableMicClicks', tostring(micClicks))
|
||||
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
|
||||
function on_key_up() end
|
||||
|
||||
local KEY_F11 = 0x7A
|
||||
|
||||
RegisterRawKeymap("pma-voice_proximityCycle", on_key_up, function()
|
||||
ExecuteCommand('cycleproximity')
|
||||
end, KEY_F11, true)
|
||||
|
||||
local KEY_LEFT_ALT = 0xA4
|
||||
|
||||
RegisterRawKeymap("pma-voice_radioTalk", function()
|
||||
ExecuteCommand('+radiotalk')
|
||||
end, function()
|
||||
ExecuteCommand('-radiotalk')
|
||||
end, KEY_LEFT_ALT, true)
|
||||
end
|
||||
|
||||
--- handles initializiation for whenever radio or call data changes
|
||||
--- calls should always be last because they're assumed to always be enabled so
|
||||
--- theres no delay in talking.
|
||||
function handleRadioAndCallInit()
|
||||
for tgt, enabled in pairs(radioData) do
|
||||
if tgt ~= playerServerId then
|
||||
toggleVoice(tgt, enabled, 'radio')
|
||||
end
|
||||
end
|
||||
|
||||
for tgt, enabled in pairs(callData) do
|
||||
if tgt ~= playerServerId then
|
||||
toggleVoice(tgt, true, 'call')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,253 @@
|
||||
-- used when muted
|
||||
local disableUpdates = false
|
||||
local isListenerEnabled = false
|
||||
local plyCoords = GetEntityCoords(PlayerPedId())
|
||||
proximity = MumbleGetTalkerProximity()
|
||||
currentTargets = {}
|
||||
|
||||
-- a list of all the players the current client us listening to
|
||||
-- the value will be set to false if we didn't actually start listening to them (in situations where their channel didn't exist)
|
||||
-- TODO: PR a native to let us get if we're listening to a certain channel.
|
||||
local listeners = {}
|
||||
|
||||
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(LocalPlayer.state.assignedChannel)
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, LocalPlayer.state.assignedChannel)
|
||||
|
||||
for source, _ in pairs(callData) do
|
||||
if source ~= playerServerId then
|
||||
local channel = MumbleGetVoiceChannelFromServerId(source)
|
||||
if channel ~= -1 then
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, channel)
|
||||
end
|
||||
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)
|
||||
local channel = MumbleGetVoiceChannelFromServerId(serverId)
|
||||
if channel ~= -1 then
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function addChannelListener(serverId)
|
||||
-- not in the documentation, but this will return -1 whenever the client isn't in a channel
|
||||
local channel = MumbleGetVoiceChannelFromServerId(serverId)
|
||||
if channel ~= -1 then
|
||||
MumbleAddVoiceChannelListen(channel)
|
||||
logger.verbose("Adding %s to listen table", serverId)
|
||||
end
|
||||
listeners[serverId] = channel ~= -1
|
||||
end
|
||||
|
||||
function removeChannelListener(serverId)
|
||||
if listeners[serverId] then
|
||||
local channel = MumbleGetVoiceChannelFromServerId(serverId)
|
||||
if channel ~= -1 then
|
||||
MumbleRemoveVoiceChannelListen(channel)
|
||||
end
|
||||
logger.verbose("Removing %s from listen table", serverId)
|
||||
end
|
||||
-- remove the listener if they exist
|
||||
listeners[serverId] = nil
|
||||
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
|
||||
addChannelListener(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
|
||||
removeChannelListener(serverId)
|
||||
::skip_loop::
|
||||
end
|
||||
|
||||
-- cleanup table if we stop listening
|
||||
listeners = {}
|
||||
end
|
||||
end
|
||||
|
||||
function tryListeningToFailedListeners()
|
||||
for src, isListening in pairs(listeners) do
|
||||
-- if we failed to listen before we'll be set to false
|
||||
if not isListening then
|
||||
addChannelListener(src)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('onPlayerJoining', function(serverId)
|
||||
if isListenerEnabled then
|
||||
addChannelListener(serverId)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('onPlayerDropped', function(serverId)
|
||||
if isListenerEnabled then
|
||||
removeChannelListener(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', 'Mutes the player with the specified id', {
|
||||
{ name = "player id", help = "the player to toggle mute" },
|
||||
{ name = "duration", help = "(opt) the duration the mute in seconds (default: 900)" }
|
||||
})
|
||||
while true do
|
||||
-- wait for mumble to reconnect
|
||||
while not MumbleIsConnected() or not isInitialized 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
|
||||
tryListeningToFailedListeners()
|
||||
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[mode], false)
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end)
|
||||
@@ -0,0 +1,27 @@
|
||||
AddStateBagChangeHandler("submix", "", function(bagName, _, value)
|
||||
local tgtId = tonumber(bagName:gsub('player:', ''), 10)
|
||||
if not tgtId then return end
|
||||
-- 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 then
|
||||
if not radioData[tgtId] and not callData[tgtId] then
|
||||
logger.info("Resetting submix for player %s", tgtId)
|
||||
MumbleSetSubmixForServerId(tgtId, -1)
|
||||
end
|
||||
return
|
||||
end
|
||||
logger.info("%s had their submix set to %s", tgtId, value)
|
||||
MumbleSetSubmixForServerId(tgtId, submixIndicies[value])
|
||||
end)
|
||||
|
||||
RegisterNetEvent("onPlayerDropped", function(tgtId)
|
||||
if not radioData[tgtId] and not callData[tgtId] then
|
||||
logger.info("Resetting submix for player %s", tgtId)
|
||||
MumbleSetSubmixForServerId(tgtId, -1)
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,58 @@
|
||||
local callChannel = 0
|
||||
|
||||
RegisterNetEvent('pma-voice:syncCallData', function(callTable, channel)
|
||||
callData = callTable
|
||||
handleRadioAndCallInit()
|
||||
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)
|
||||
addVoiceTargets((radioPressed and isRadioEnabled()) and radioData or {}, callData)
|
||||
else
|
||||
callData[plySource] = nil
|
||||
toggleVoice(plySource, radioData[plySource], 'call')
|
||||
if MumbleIsPlayerTalking(PlayerId()) then
|
||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
||||
addVoiceTargets((radioPressed and isRadioEnabled()) 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
|
||||
end)
|
||||
@@ -0,0 +1,313 @@
|
||||
local radioChannel = 0
|
||||
local radioNames = {}
|
||||
local disableRadioAnim = false
|
||||
local radioAnim = {
|
||||
dict = "random@arrests",
|
||||
anim = "generic_radio_enter",
|
||||
}
|
||||
|
||||
---@return boolean isEnabled if radioEnabled is true and LocalPlayer.state.disableRadio is 0 (no bits set)
|
||||
function isRadioEnabled()
|
||||
return radioEnabled and LocalPlayer.state.disableRadio == 0
|
||||
end
|
||||
|
||||
--- 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
|
||||
|
||||
local isEnabled = isRadioEnabled()
|
||||
|
||||
if isEnabled then
|
||||
handleRadioAndCallInit()
|
||||
end
|
||||
|
||||
sendUIMessage({
|
||||
radioChannel = radioChannel,
|
||||
radioEnabled = isEnabled
|
||||
})
|
||||
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)
|
||||
radioData[plySource] = enabled
|
||||
|
||||
if not isRadioEnabled() then return logger.info("[radio] Ignoring setTalkingOnRadio. radioEnabled: %s disableRadio: %s", radioEnabled, LocalPlayer.state.disableRadio) end
|
||||
-- If we're on a call we don't want to toggle their voice disabled this will break calls.
|
||||
local enabled = enabled or callData[plySource]
|
||||
toggleVoice(plySource, enabled, 'radio')
|
||||
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
|
||||
logger.info('[radio] %s joined radio %s %s', plySource, radioChannel,
|
||||
radioPressed and " while we were talking, adding them to targets" or "")
|
||||
if radioPressed then
|
||||
addVoiceTargets(radioData, callData)
|
||||
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 = {}
|
||||
addVoiceTargets(callData)
|
||||
else
|
||||
toggleVoice(plySource, false, 'radio')
|
||||
if radioPressed then
|
||||
logger.info('[radio] %s left radio %s while we were talking, updating targets.', plySource, radioChannel)
|
||||
addVoiceTargets(radioData, callData)
|
||||
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)
|
||||
|
||||
RegisterNetEvent('pma-voice:radioChangeRejected', function()
|
||||
logger.info("The server rejected your radio change.")
|
||||
radioChannel = 0
|
||||
end)
|
||||
|
||||
--- 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("setDisableRadioAnim", function(shouldDisable)
|
||||
disableRadioAnim = shouldDisable
|
||||
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
|
||||
return false
|
||||
end
|
||||
|
||||
function isRadioAnimEnabled()
|
||||
if
|
||||
GetConvarInt('voice_enableRadioAnim', 1) == 1
|
||||
and not (GetConvarInt('voice_disableVehicleRadioAnim', 0) == 1
|
||||
and IsPedInAnyVehicle(PlayerPedId(), false))
|
||||
and not disableRadioAnim then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
RegisterCommand('+radiotalk', function()
|
||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||
if isDead() then return end
|
||||
if not isRadioEnabled() then return end
|
||||
if not radioPressed then
|
||||
if radioChannel > 0 then
|
||||
logger.info('[radio] Start broadcasting, update targets and notify server.')
|
||||
addVoiceTargets(radioData, callData)
|
||||
TriggerServerEvent('pma-voice:setTalkingOnRadio', true)
|
||||
radioPressed = true
|
||||
local shouldPlayAnimation = isRadioAnimEnabled()
|
||||
playMicClicks(true)
|
||||
-- localize here so in the off case someone changes this while its in use we
|
||||
-- still remove our dictionary down below here
|
||||
local dict = radioAnim.dict
|
||||
local anim = radioAnim.anim
|
||||
if shouldPlayAnimation then
|
||||
RequestAnimDict(dict)
|
||||
end
|
||||
CreateThread(function()
|
||||
TriggerEvent("pma-voice:radioActive", true)
|
||||
LocalPlayer.state:set("radioActive", true, true);
|
||||
local checkFailed = false
|
||||
while radioPressed do
|
||||
local ped = PlayerPedId()
|
||||
if radioChannel < 0 or isDead() or not isRadioEnabled() or (GetConvarBool("voice_disableRadioWhenShooting", false) and IsPedShooting(ped)) then
|
||||
checkFailed = true
|
||||
break
|
||||
end
|
||||
if shouldPlayAnimation and HasAnimDictLoaded(dict) then
|
||||
if not IsEntityPlayingAnim(ped, dict, anim, 3) then
|
||||
TaskPlayAnim(ped, dict, anim, 8.0, 2.0, -1, 50, 2.0, false,
|
||||
false,
|
||||
false)
|
||||
end
|
||||
end
|
||||
SetControlNormal(0, 249, 1.0)
|
||||
SetControlNormal(1, 249, 1.0)
|
||||
SetControlNormal(2, 249, 1.0)
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
|
||||
if checkFailed then
|
||||
logger.info("Canceling radio talking as the checks have failed.")
|
||||
ExecuteCommand("-radiotalk")
|
||||
end
|
||||
if shouldPlayAnimation then
|
||||
RemoveAnimDict(dict)
|
||||
end
|
||||
end)
|
||||
else
|
||||
logger.info("Player tried to talk but was not on a radio channel")
|
||||
end
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-radiotalk', function()
|
||||
if radioChannel > 0 and radioPressed then
|
||||
radioPressed = false
|
||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
||||
addVoiceTargets(callData)
|
||||
TriggerEvent("pma-voice:radioActive", false)
|
||||
LocalPlayer.state:set("radioActive", false, true);
|
||||
playMicClicks(false)
|
||||
if GetConvarInt('voice_enableRadioAnim', 1) == 1 then
|
||||
StopAnimTask(PlayerPedId(), radioAnim.dict, radioAnim.anim, -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
|
||||
|
||||
local function setRadioTalkAnim(dict, anim)
|
||||
type_check({dict, "string"}, {anim, "string"})
|
||||
if not DoesAnimDictExist(dict) then
|
||||
return error(("Dict: %s did not exist"):format(dict))
|
||||
end
|
||||
radioAnim.dict = dict
|
||||
radioAnim.anim = anim
|
||||
end
|
||||
|
||||
exports('setRadioTalkAnim', setRadioTalkAnim)
|
||||
|
||||
--- 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)
|
||||
|
||||
|
||||
--- handles "radioEnabled" changing
|
||||
---@param wasRadioEnabled boolean whether radio is enabled or not
|
||||
function handleRadioEnabledChanged(wasRadioEnabled)
|
||||
if wasRadioEnabled then
|
||||
syncRadioData(radioData, "")
|
||||
else
|
||||
removePlayerFromRadio(playerServerId)
|
||||
end
|
||||
end
|
||||
|
||||
--- adds the bit to the disableRadio bits
|
||||
---@param bit number the bit to add
|
||||
local function addRadioDisableBit(bit)
|
||||
local curVal = LocalPlayer.state.disableRadio or 0
|
||||
curVal = curVal | bit
|
||||
LocalPlayer.state:set("disableRadio", curVal, true)
|
||||
end
|
||||
exports("addRadioDisableBit", addRadioDisableBit)
|
||||
|
||||
--- removes the bit from disableRadio
|
||||
---@param bit number the bit to remove
|
||||
local function removeRadioDisableBit(bit)
|
||||
local curVal = LocalPlayer.state.disableRadio or 0
|
||||
curVal = curVal & (~bit)
|
||||
LocalPlayer.state:set("disableRadio", curVal, true)
|
||||
end
|
||||
exports("removeRadioDisableBit", removeRadioDisableBit)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-midnight
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
## setMicClickOffVolume
|
||||
|
||||
## Description
|
||||
|
||||
Sets the local players radio turn off mic click volume
|
||||
|
||||
## Parameters
|
||||
|
||||
* **volume**: the volume to change to between 0 - 100 percent
|
||||
|
||||
```lua
|
||||
-- sets the radio turn off mic click volume to 20 percent
|
||||
exports['pma-voice']:setMicClickOffVolume(20)
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
## setMicClickOnVolume
|
||||
|
||||
## Description
|
||||
|
||||
Sets the local players radio turn on mic click volume
|
||||
|
||||
## Parameters
|
||||
|
||||
* **volume**: the volume to change to between 0 - 100 percent
|
||||
|
||||
```lua
|
||||
-- sets the radio turn on mic click volume to 20 percent
|
||||
exports['pma-voice']:setMicClickOnVolume(20)
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
## Routing Buckets
|
||||
|
||||
pma-voice natively supports routing buckets.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
## removeChannelCheck
|
||||
|
||||
## Description
|
||||
|
||||
Removes a channel check for radio channel.
|
||||
|
||||
## Parameters
|
||||
|
||||
* **channel**: The channel to remove the check from.
|
||||
|
||||
```lua
|
||||
-- Example for Removes
|
||||
exports['pma-voice']:removeChannelCheck(1)
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
game 'common'
|
||||
version '7.0.1'
|
||||
|
||||
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" },
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
voiceData = {}
|
||||
radioData = {}
|
||||
callData = {}
|
||||
|
||||
local mappedChannels = {}
|
||||
function firstFreeChannel()
|
||||
for i = 1, 2048 do
|
||||
if not mappedChannels[i] then
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
local assignedChannel = firstFreeChannel()
|
||||
plyState:set('assignedChannel', assignedChannel, true)
|
||||
if assignedChannel ~= 0 then
|
||||
mappedChannels[assignedChannel] = source
|
||||
logger.verbose('[reuse] Assigned %s to channel %s', source, assignedChannel)
|
||||
else
|
||||
logger.error('[reuse] Failed to find a free channel for %s', source)
|
||||
end
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
local plyTbl = GetPlayers()
|
||||
for i = 1, #plyTbl do
|
||||
local ply = tonumber(plyTbl[i])
|
||||
voiceData[ply] = defaultTable(ply)
|
||||
end
|
||||
|
||||
Wait(5000)
|
||||
|
||||
local nativeAudio = GetConvar('voice_useNativeAudio', 'not-set')
|
||||
local _3dAudio = GetConvar('voice_use3dAudio', 'not-set')
|
||||
local _2dAudio = GetConvar('voice_use2dAudio', 'not-set')
|
||||
local sendingRangeOnly = GetConvar('voice_useSendingRangeOnly', 'not-set')
|
||||
local gameVersion = GetConvar('gamename', 'fivem')
|
||||
|
||||
-- handle no convars being set (default drag n' drop)
|
||||
if
|
||||
nativeAudio == 'not-set'
|
||||
and _3dAudio == 'not-set'
|
||||
and _2dAudio == 'not-set'
|
||||
then
|
||||
SetConvarReplicated('voice_useNativeAudio', 'true')
|
||||
logger.info('No voice mod detected, defaulting to \'setr voice_useNativeAudio true\'')
|
||||
elseif sendingRangeOnly == 'true' then
|
||||
logger.warn(
|
||||
"It's recommended to have 'voice_useSendingRangeOnly' set to false, you can do that with 'setr voice_useSendingRangeOnly false', this makes clients ignore the position information sent from the client.")
|
||||
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
|
||||
local mappedChannel = Player(source).state.assignedChannel
|
||||
|
||||
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
|
||||
|
||||
if mappedChannel then
|
||||
mappedChannels[mappedChannel] = nil
|
||||
logger.verbose('[reuse] Unassigned %s from channel %s', source, mappedChannel)
|
||||
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)
|
||||
@@ -0,0 +1,76 @@
|
||||
--- 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)
|
||||
@@ -0,0 +1,193 @@
|
||||
|
||||
local msgpack_pack_args = msgpack.pack_args
|
||||
|
||||
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)
|
||||
|
||||
--- removes any check for the channel
|
||||
---@param channel number the channel to add a check to
|
||||
function removeChannelCheck(channel)
|
||||
local channelType = type(channel)
|
||||
if channelType ~= "number" then
|
||||
error(("'channel' expected 'number' got '%s'"):format(channelType))
|
||||
end
|
||||
radioChecks[channel] = nil
|
||||
logger.info("%s removed check for channel %s", GetInvokingResource(), channel)
|
||||
end
|
||||
|
||||
exports("removeChannelCheck", removeChannelCheck)
|
||||
|
||||
local function radioNameGetter_orig(source)
|
||||
return GetPlayerName(source)
|
||||
end
|
||||
local radioNameGetter = radioNameGetter_orig
|
||||
|
||||
--- triggers an event for all of the players in the table while only doing msgpack
|
||||
--- serialization once
|
||||
local function triggerEventForRadioChannel(eventName, radioTbl, ...)
|
||||
local payload = msgpack_pack_args(...)
|
||||
for player, _ in pairs(radioTbl) do
|
||||
TriggerClientEventInternal(eventName, player, payload, payload:len())
|
||||
end
|
||||
end
|
||||
|
||||
--- 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
|
||||
---@return boolean wasAdded if the player was successfuly added to the radio channel, or if it failed.
|
||||
function addPlayerToRadio(source, radioChannel)
|
||||
if not canJoinChannel(source, radioChannel) then
|
||||
-- remove the player from the radio client side
|
||||
TriggerClientEvent("pma-voice:radioChangeRejected", source)
|
||||
TriggerClientEvent('pma-voice:removePlayerFromRadio', source, source)
|
||||
return false
|
||||
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)
|
||||
triggerEventForRadioChannel('pma-voice:addPlayerToRadio', radioData[radioChannel], source, plyName)
|
||||
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)
|
||||
return true
|
||||
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 {}
|
||||
triggerEventForRadioChannel('pma-voice:removePlayerFromRadio', radioData[radioChannel], source)
|
||||
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
|
||||
if radioChannel ~= 0 then
|
||||
if plyVoice.radio > 0 then
|
||||
removePlayerFromRadio(source, plyVoice.radio)
|
||||
end
|
||||
local wasAdded = addPlayerToRadio(source, radioChannel)
|
||||
Player(source).state.radioChannel = wasAdded and radioChannel or 0
|
||||
elseif radioChannel == 0 then
|
||||
removePlayerFromRadio(source, plyVoice.radio)
|
||||
Player(source).state.radioChannel = 0
|
||||
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)
|
||||
triggerEventForRadioChannel('pma-voice:setTalkingOnRadio', radioTbl, source, talking)
|
||||
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(radioRef, 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)
|
||||
@@ -0,0 +1,27 @@
|
||||
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[GetCurrentResourceName()].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]);
|
||||
delete 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)
|
||||
@@ -0,0 +1,127 @@
|
||||
Cfg = {}
|
||||
|
||||
voiceTarget = 1
|
||||
|
||||
gameVersion = GetGameName()
|
||||
|
||||
-- these are just here to satisfy linting
|
||||
if not IsDuplicityVersion() then
|
||||
LocalPlayer = LocalPlayer
|
||||
playerServerId = GetPlayerServerId(PlayerId())
|
||||
|
||||
if gameVersion == "redm" then
|
||||
function CreateAudioSubmix(name)
|
||||
return Citizen.InvokeNative(0x658d2bc8, name, Citizen.ResultAsInteger())
|
||||
end
|
||||
|
||||
function AddAudioSubmixOutput(submixId, outputSubmixId)
|
||||
Citizen.InvokeNative(0xAC6E290D, submixId, outputSubmixId)
|
||||
end
|
||||
|
||||
function MumbleSetSubmixForServerId(serverId, submixId)
|
||||
Citizen.InvokeNative(0xFE3A3054, serverId, submixId)
|
||||
end
|
||||
|
||||
function SetAudioSubmixEffectParamFloat(submixId, effectSlot, paramIndex, paramValue)
|
||||
Citizen.InvokeNative(0x9A209B3C, submixId, effectSlot, paramIndex, paramValue)
|
||||
end
|
||||
|
||||
function SetAudioSubmixEffectParamInt(submixId, effectSlot, paramIndex, paramValue)
|
||||
Citizen.InvokeNative(0x77FAE2B8, submixId, effectSlot, paramIndex, paramValue)
|
||||
end
|
||||
|
||||
function SetAudioSubmixEffectRadioFx(submixId, effectSlot)
|
||||
Citizen.InvokeNative(0xAAA94D53, submixId, effectSlot)
|
||||
end
|
||||
|
||||
function SetAudioSubmixOutputVolumes(submixId, outputSlot, frontLeftVolume, frontRightVolume, rearLeftVolume,
|
||||
rearRightVolume, channel5Volume, channel6Volume)
|
||||
Citizen.InvokeNative(0x825DC0D1, submixId, outputSlot, frontLeftVolume, frontRightVolume, rearLeftVolume,
|
||||
rearRightVolume, channel5Volume, channel6Volume)
|
||||
end
|
||||
end
|
||||
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, "Whisper" }, -- Whisper speech distance in gta distance units
|
||||
{ 3.0, "Normal" }, -- Normal speech distance in gta distance units
|
||||
{ 6.0, "Shouting" } -- Shout speech distance in gta distance units
|
||||
}
|
||||
else
|
||||
Cfg.voiceModes = {
|
||||
{ 3.0, "Whisper" }, -- Whisper speech distance in gta distance units
|
||||
{ 7.0, "Normal" }, -- Normal speech distance in gta distance units
|
||||
{ 15.0, "Shouting" } -- 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
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>voice-ui</title><link href="css/app.css" rel="preload" as="style"><link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but voice-ui doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/app.js"></script></body></html>
|
||||
@@ -0,0 +1,2 @@
|
||||
(function(e){function o(o){for(var t,a,r=o[0],d=o[1],l=o[2],s=0,b=[];s<r.length;s++)a=r[s],Object.prototype.hasOwnProperty.call(i,a)&&i[a]&&b.push(i[a][0]),i[a]=0;for(t in d)Object.prototype.hasOwnProperty.call(d,t)&&(e[t]=d[t]);u&&u(o);while(b.length)b.shift()();return c.push.apply(c,l||[]),n()}function n(){for(var e,o=0;o<c.length;o++){for(var n=c[o],t=!0,r=1;r<n.length;r++){var d=n[r];0!==i[d]&&(t=!1)}t&&(c.splice(o--,1),e=a(a.s=n[0]))}return e}var t={},i={app:0},c=[];function a(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,a),n.l=!0,n.exports}a.m=e,a.c=t,a.d=function(e,o,n){a.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:n})},a.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,o){if(1&o&&(e=a(e)),8&o)return e;if(4&o&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(a.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var t in e)a.d(n,t,function(o){return e[o]}.bind(null,t));return n},a.n=function(e){var o=e&&e.__esModule?function(){return e["default"]}:function(){return e};return a.d(o,"a",o),o},a.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},a.p="";var r=window["webpackJsonp"]=window["webpackJsonp"]||[],d=r.push.bind(r);r.push=o,r=r.slice();for(var l=0;l<r.length;l++)o(r[l]);var u=d;c.push([0,"chunk-vendors"]),n()})({0:function(e,o,n){e.exports=n("56d7")},"0154":function(e,o,n){},"56d7":function(e,o,n){"use strict";n.r(o);var t=n("7edb");const i=Object(t["d"])("audio",{id:"audio_on",src:"mic_click_on.ogg"},null,-1),c=Object(t["d"])("audio",{id:"audio_off",src:"mic_click_off.ogg"},null,-1),a={key:0,class:"voiceInfo"};function r(e,o,n,r,d,l){return Object(t["f"])(),Object(t["c"])("body",null,[i,c,r.voice.uiEnabled?(Object(t["f"])(),Object(t["c"])("div",a,[0!==r.voice.callInfo?(Object(t["f"])(),Object(t["c"])("p",{key:0,class:Object(t["e"])({talking:r.voice.talking})}," [Call] ",2)):Object(t["b"])("",!0),r.voice.radioEnabled&&0!==r.voice.radioChannel?(Object(t["f"])(),Object(t["c"])("p",{key:1,class:Object(t["e"])({talking:r.voice.usingRadio})},Object(t["h"])(r.voice.radioChannel)+" Mhz [Radio] ",3)):Object(t["b"])("",!0),r.voice.voiceModes.length?(Object(t["f"])(),Object(t["c"])("p",{key:2,class:Object(t["e"])({talking:r.voice.talking})},Object(t["h"])(r.voice.voiceModes[r.voice.voiceMode][1])+" [Range] ",3)):Object(t["b"])("",!0)])):Object(t["b"])("",!0)])}var d={name:"App",setup(){const e=Object(t["g"])({uiEnabled:!0,voiceModes:[],voiceMode:0,radioChannel:0,radioEnabled:!0,usingRadio:!1,callInfo:0,talking:!1});return window.addEventListener("message",(function(o){const n=o.data;if(void 0!==n.uiEnabled&&(e.uiEnabled=n.uiEnabled),void 0!==n.voiceModes){e.voiceModes=JSON.parse(n.voiceModes);let o=[...e.voiceModes];o.push([0,"Custom"]),e.voiceModes=o}if(void 0!==n.voiceMode&&(e.voiceMode=n.voiceMode),void 0!==n.radioChannel&&(e.radioChannel=n.radioChannel),void 0!==n.radioEnabled&&(e.radioEnabled=n.radioEnabled),void 0!==n.callInfo&&(e.callInfo=n.callInfo),void 0!==n.usingRadio&&n.usingRadio!==e.usingRadio&&(e.usingRadio=n.usingRadio),void 0===n.talking||e.usingRadio||(e.talking=n.talking),n.sound&&e.radioEnabled&&0!==e.radioChannel){let e=document.getElementById(n.sound);e.load(),e.volume=n.volume,e.play().catch(e=>{})}})),fetch(`https://${GetParentResourceName()}/uiReady`,{method:"POST"}),{voice:e}}},l=(n("9253"),n("85dd")),u=n.n(l);const s=u()(d,[["render",r]]);var b=s;Object(t["a"])(b).mount("#app")},9253:function(e,o,n){"use strict";n("0154")}});
|
||||
//# sourceMappingURL=app.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,24 @@
|
||||
# voice-ui
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "voice-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<body>
|
||||
<audio id="audio_on" src="mic_click_on.ogg"></audio>
|
||||
<audio id="audio_off" src="mic_click_off.ogg"></audio>
|
||||
<div v-if="voice.uiEnabled" class="voiceInfo">
|
||||
<p v-if="voice.callInfo !== 0" :class="{ talking: voice.talking }">
|
||||
[Call]
|
||||
</p>
|
||||
<p v-if="voice.radioEnabled && voice.radioChannel !== 0" :class="{ talking: voice.usingRadio }">
|
||||
{{ voice.radioChannel }} Mhz [Radio]
|
||||
</p>
|
||||
<p v-if="voice.voiceModes.length" :class="{ talking: voice.talking }">
|
||||
{{ voice.voiceModes[voice.voiceMode][1] }} [Range]
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive } from "vue";
|
||||
export default {
|
||||
name: "App",
|
||||
setup() {
|
||||
const voice = reactive({
|
||||
uiEnabled: true,
|
||||
voiceModes: [],
|
||||
voiceMode: 0,
|
||||
radioChannel: 0,
|
||||
radioEnabled: true,
|
||||
usingRadio: false,
|
||||
callInfo: 0,
|
||||
talking: false,
|
||||
});
|
||||
|
||||
// stops from toggling voice at the end of talking
|
||||
window.addEventListener("message", function(event) {
|
||||
const data = event.data;
|
||||
|
||||
if (data.uiEnabled !== undefined) {
|
||||
voice.uiEnabled = data.uiEnabled
|
||||
}
|
||||
|
||||
if (data.voiceModes !== undefined) {
|
||||
voice.voiceModes = JSON.parse(data.voiceModes);
|
||||
// Push our own custom type for modes that have their range changed
|
||||
let voiceModes = [...voice.voiceModes]
|
||||
voiceModes.push([0.0, "Custom"])
|
||||
voice.voiceModes = voiceModes
|
||||
}
|
||||
|
||||
if (data.voiceMode !== undefined) {
|
||||
voice.voiceMode = data.voiceMode;
|
||||
}
|
||||
|
||||
if (data.radioChannel !== undefined) {
|
||||
voice.radioChannel = data.radioChannel;
|
||||
}
|
||||
|
||||
if (data.radioEnabled !== undefined) {
|
||||
voice.radioEnabled = data.radioEnabled;
|
||||
}
|
||||
|
||||
if (data.callInfo !== undefined) {
|
||||
voice.callInfo = data.callInfo;
|
||||
}
|
||||
|
||||
if (data.usingRadio !== undefined && data.usingRadio !== voice.usingRadio) {
|
||||
voice.usingRadio = data.usingRadio;
|
||||
}
|
||||
|
||||
if ((data.talking !== undefined) && !voice.usingRadio) {
|
||||
voice.talking = data.talking;
|
||||
}
|
||||
|
||||
if (data.sound && voice.radioEnabled && voice.radioChannel !== 0) {
|
||||
let click = document.getElementById(data.sound);
|
||||
// discard these errors as its usually just a 'uncaught promise' from two clicks happening too fast.
|
||||
click.load();
|
||||
click.volume = data.volume;
|
||||
click.play().catch((e) => {});
|
||||
}
|
||||
});
|
||||
|
||||
fetch(`https://${GetParentResourceName()}/uiReady`, { method: 'POST' });
|
||||
|
||||
return { voice };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.voiceInfo {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
position: fixed;
|
||||
text-align: right;
|
||||
bottom: 5px;
|
||||
padding: 0;
|
||||
right: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: rgb(148, 150, 151);
|
||||
/* https://stackoverflow.com/questions/4772906/css-is-it-possible-to-add-a-black-outline-around-each-character-in-text */
|
||||
text-shadow: 1.25px 0 0 #000, 0 -1.25px 0 #000, 0 1.25px 0 #000,
|
||||
-1.25px 0 0 #000;
|
||||
}
|
||||
.talking {
|
||||
color: rgba(255, 255, 255, 0.822);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
publicPath: './',
|
||||
productionSourceMap: true,
|
||||
filenameHashing: false,
|
||||
outputDir: "../ui",
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user