forked from Simnation/Main
Yaca Voice
This commit is contained in:
parent
4ea0392f6a
commit
654e882335
120 changed files with 11193 additions and 8799 deletions
|
@ -1,21 +0,0 @@
|
||||||
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.
|
|
|
@ -1,180 +0,0 @@
|
||||||
# pma-voice
|
|
||||||
A voice system designed around the use of FiveM/RedM internal mumble server.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Please report any issues you have in the GitHub [Issues](https://github.com/AvarianKnight/pma-voice/issues)
|
|
||||||
|
|
||||||
### NOTE: It is expected for servers to be on the latest recommended version, which you can find [here for Windows](https://runtime.fivem.net/artifacts/fivem/build_server_windows/master/) and [here for Linux](https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/).
|
|
||||||
|
|
||||||
# Compatibility Notice:
|
|
||||||
|
|
||||||
This script is not compatible with other voice systems (duh), that means if you have vMenus voice chat you will **have** to [disable](https://docs.vespura.com/vmenu/faq/#q-how-do-i-disable-voice-chat) it.
|
|
||||||
|
|
||||||
Please do not override `NetworkSetTalkerProximity`, `MumbleSetAudioInputDistance`, `MumbleSetAudioOutputDistance` or `NetworkSetVoiceActive` in any of your other scripts as there have been cases where it breaks pma-voice.
|
|
||||||
|
|
||||||
# Credits
|
|
||||||
|
|
||||||
- @Frazzle for mumble-voip (for which the concept came from)
|
|
||||||
- @pichotm for pVoice (where the grid concept came from)
|
|
||||||
|
|
||||||
# FiveM/RedM Config
|
|
||||||
|
|
||||||
### NOTE: Only use one of the Audio options (don't enable 3d Audio & Native Audio at the same time), its also recommended to always use voice_useSendingRangeOnly.
|
|
||||||
|
|
||||||
You only need to add the convar **if** you're changing the value.
|
|
||||||
|
|
||||||
All of the configs here are set using `setr [voice_configOption] [boolean]`
|
|
||||||
|
|
||||||
Native audio will not work on RedM, you will have to use 3d audio.
|
|
||||||
|
|
||||||
| ConVar | Default | Description | Parameter(s) |
|
|
||||||
|----------------------------|---------|---------------------------------------------------------------|--------------|
|
|
||||||
| voice_useNativeAudio | false | **This will not work for RedM** Uses the games native audio, will add 3d sound, echo, reverb, and more. **Required for submixs** | boolean |
|
|
||||||
| voice_use2dAudio | false | Uses 2d audio, will result in same volume sound no matter where they're at until they leave proximity. | boolean
|
|
||||||
| voice_use3dAudio | false | Uses 3d audio | boolean |
|
|
||||||
| voice_useSendingRangeOnly | false | Only allows you to hear people within your hear/send range, prevents people from connecting to your mumble server and trolling. | boolean |
|
|
||||||
|
|
||||||
# Config
|
|
||||||
|
|
||||||
### PLEASE NOTE: Any keybind changes only affect new players, if you want to change your key bind go to Key Bindings -> FiveM -> Look for keybinds under 'pma-voice'.
|
|
||||||
|
|
||||||
All of the config is done via ConVars in order to streamline the process.
|
|
||||||
|
|
||||||
The ints are used like a boolean to 0 would be false, 1 true.
|
|
||||||
|
|
||||||
All of the configs here are set using `setr [voice_configOption] [int]` OR `setr [voice_configOption] "[string]"`
|
|
||||||
|
|
||||||
#### Note: If a convar defaults to 1 (true) you don't have set it again unless you want to disable it.
|
|
||||||
|
|
||||||
### General Voice Settings
|
|
||||||
|
|
||||||
| ConVar | Default | Description | Parameter(s) |
|
|
||||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
|
||||||
| voice_enableUi | 1 | Enables the built in user interface | int |
|
|
||||||
| voice_enableProximityCycle | 1 | Enables the usage of the F11 proximity key, if disabled players are stuck on the first proximity | int |
|
|
||||||
| voice_defaultCycle | F11 | The default key to cycle the players proximity. You can find a list of valid keys [in the Cfx docs](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/) | string |
|
|
||||||
| voice_defaultRadioVolume | 30 | The default volume to set the radio to (has to be between 1 and 100) *NOTE: Only new joins will have the new value, players that already joined will not.* | float |
|
|
||||||
| voice_defaultPhoneVolume | 60 | The default volume to set the phone 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 |
|
|
||||||
|
|
||||||
### Phone & Radio
|
|
||||||
|
|
||||||
| ConVar | Default | Description | Parameter(s) |
|
|
||||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
|
||||||
| voice_enableRadios | 1 | Enables the radio sub-modules | int |
|
|
||||||
| voice_enablePhones | 1 | Enables the phone sub-modules | int |
|
|
||||||
| voice_enableSubmix | 1 | Enables the submix which adds a radio/phone style submix to their voice **NOTE: Submixs require native audio** | int |
|
|
||||||
| voice_enableRadioAnim | 0 | Enables (grab shoulder mic) animation while talking on the radio. | int |
|
|
||||||
| voice_defaultRadio | LMENU | The default key to use the radio. You can find a list of valid keys [in the FiveM docs](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/) | string |
|
|
||||||
|
|
||||||
### Sync
|
|
||||||
|
|
||||||
| ConVar | Default | Description | Parameter(s) |
|
|
||||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
|
||||||
| voice_refreshRate | 200 | How often the UI/Proximity is refreshed | int |
|
|
||||||
|
|
||||||
### External Server & Misc.
|
|
||||||
| ConVar | Default | Description | Parameter(s) |
|
|
||||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
|
||||||
| voice_allowSetIntent | 1 | Whether or not to allow players to set their audio intents (you can see more [here](https://docs.fivem.net/natives/?_0x6383526B)) | int |
|
|
||||||
| voice_externalAddress | none | The external address to use to connect to the mumble server | string |
|
|
||||||
| voice_externalPort | 0 | The external port to use | int |
|
|
||||||
| voice_debugMode | 0 | 1 for basic logs, 4 for verbose logs | int |
|
|
||||||
| voice_externalDisallowJoin | 0 | Disables players being allowed to join the server, should only be used if you're using a FXServer as a external mumble server. | int |
|
|
||||||
| voice_hideEndpoints | 1 | Hides the mumble address in logs *NOTE: You should only care to hide this for a external server.* | int |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Aces
|
|
||||||
|
|
||||||
pma-voice comes with a built in /muteply (tgtPly) (duration) command, in order to allow your staff to use it you will have to grand them the ace!
|
|
||||||
|
|
||||||
Example:
|
|
||||||
`add_ace group.superadmin command.muteply allow;`
|
|
||||||
|
|
||||||
This would only allow the superadmin group to mute players.
|
|
||||||
|
|
||||||
### Exports
|
|
||||||
|
|
||||||
#### Client
|
|
||||||
|
|
||||||
##### Setters
|
|
||||||
|
|
||||||
| Export | Description | Parameter(s) |
|
|
||||||
|---------------------|-----------------------------|--------------|
|
|
||||||
| [setVoiceProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any |
|
|
||||||
| [setRadioChannel](docs/client-setters/setRadioChannel.md) | Set radio channel | int |
|
|
||||||
| [setCallChannel](docs/client-setters/setCallChannel.md) | Set call channel | int |
|
|
||||||
| [setRadioVolume](docs/client-setters/setRadioVolume.md) | Set radio volume for player | int |
|
|
||||||
| [setCallVolume](docs/client-setters/setCallVolume.md) | Set call volume for player | int |
|
|
||||||
| [addPlayerToRadio](docs/client-setters/setRadioChannel.md) | Set radio channel | int |
|
|
||||||
| [addPlayerToCall](docs/client-setters/setCallChannel.md) | Set call channel | int |
|
|
||||||
| [removePlayerFromRadio](docs/client-setters/removePlayerFromRadio.md) | Remove player from radio | |
|
|
||||||
| [removePlayerFromCall](docs/client-setters/removePlayerFromCall.md) | Remove player from call | |
|
|
||||||
|
|
||||||
##### Toggles
|
|
||||||
|
|
||||||
| Export | Description | Parameter(s) |
|
|
||||||
|---------------------|--------------------------------------------------------|--------------|
|
|
||||||
| toggleMutePlayer | Toggles the selected player muted for the local client | int |
|
|
||||||
|
|
||||||
Supported from mumble-voip / toko-voip
|
|
||||||
|
|
||||||
| Export | Description | Parameter(s) |
|
|
||||||
|-----------------------|--------------------------|--------------|
|
|
||||||
| [SetMumbleProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any |
|
|
||||||
| [SetTokoProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any |
|
|
||||||
| [SetRadioChannel](docs/client-setters/setRadioChannel.md) | Set radio channel | int |
|
|
||||||
| [SetCallChannel](docs/client-setters/setCallChannel.md) | Set call channel | int |
|
|
||||||
|
|
||||||
#### Getters
|
|
||||||
|
|
||||||
The majority of setters are done through player states, while a small
|
|
||||||
|
|
||||||
|
|
||||||
| State Bag | Description | Return Type |
|
|
||||||
|---------------|--------------------------------------------------------------|--------------|
|
|
||||||
| [proximity](docs/state-getters/stateBagGetters.md) | Returns a table with the mode index, distance, and mode name | table |
|
|
||||||
| [radioChannel](docs/state-getters/stateBagGetters.md) | Returns the players current radio channel, or 0 for none | int |
|
|
||||||
| [callChannel](docs/state-getters/stateBagGetters.md) | Returns the players current call channel, or 0 for none | int |
|
|
||||||
|
|
||||||
#### Events
|
|
||||||
|
|
||||||
These are events designed for third-party resource integration. These are emitted only to the current client.
|
|
||||||
|
|
||||||
| Event | Description | Event Params |
|
|
||||||
|--------------------------|--------------------------------------------------------------|----------------|
|
|
||||||
| [pma-voice:settingsCallback](docs/client-getters/events.md) | When emited it will return the current pma-voice settings. | cb(voiceSettings) |
|
|
||||||
| [pma-voice:radioActive](docs/client-getters/events.md) | Triggered when the radio is activated / deactivated | boolean |
|
|
||||||
| [pma-voice:setTalkingMode](docs/client-getters/events.md) | Triggered on proximity mode change with the voice mode id | int |
|
|
||||||
|
|
||||||
|
|
||||||
#### Server
|
|
||||||
|
|
||||||
##### Setters
|
|
||||||
|
|
||||||
| Export | Description | Parameter(s) |
|
|
||||||
|----------------------|--------------------------------------|--------------|
|
|
||||||
| [setPlayerRadio](docs/server-setters/setPlayerRadio.md) | Sets the players radio channel | int, int |
|
|
||||||
| [setPlayerCall](docs/server-setters/setPlayerCall.md) | Sets the players call channel | int, int |
|
|
||||||
| [addChannelCheck](docs/server-setters/addChannelCheck.md) | Adds a channel check to the players radio channel | int, function |
|
|
||||||
|
|
||||||
|
|
||||||
##### Getters
|
|
||||||
|
|
||||||
###### State Bags
|
|
||||||
You can access the state with `Player(source).state['state bag here']`
|
|
||||||
|
|
||||||
| State Bag | Description | Return Type |
|
|
||||||
|---------------|--------------------------------------------------------------|--------------|
|
|
||||||
| [proximity](docs/state-getters/stateBagGetters.md) | Returns a table with the mode index, distance, and mode name | table |
|
|
||||||
| [radioChannel](docs/state-getters/stateBagGetters.md) | Returns the players current radio channel, or 0 for none | int |
|
|
||||||
| [callChannel](docs/state-getters/stateBagGetters.md) | Returns the players current call channel, or 0 for none | int |
|
|
||||||
| [voiceIntent](docs/state-getters/stateBagGetters.md) | Returns the players current voice intent, either 'speech' or 'music' | string |
|
|
||||||
|
|
||||||
###### Exports
|
|
||||||
|
|
||||||
| Export | Description | Parameter(s) |
|
|
||||||
|------------------------------|---------------------------------------------------|------|
|
|
||||||
| [getPlayersInRadioChannel](docs/server-getters/getPlayersInRadioChannel.md) | Gets the current players in a radio channel | int |
|
|
|
@ -1,10 +0,0 @@
|
||||||
## TODO
|
|
||||||
- [ ] Rename everything that uses 'phone' to 'call' for consistency.
|
|
||||||
- [ ] Ability to display radio members on the client
|
|
||||||
- [ ] Use commands to define voiceModes in shared.lua and only leave debug logs in shared.lua
|
|
||||||
- [ ] Convert the UI to React.
|
|
||||||
- [ ] Multiple radio channels
|
|
||||||
|
|
||||||
## DONE
|
|
||||||
- [ x ] Implement a easy way to get the players current radio channel on the server
|
|
||||||
- [ x ] Add the ability to override proximity with exports
|
|
|
@ -1,74 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
||||||
-- TODO: Better implementation of this?
|
|
||||||
RegisterCommand('vol', function(_, args)
|
|
||||||
if not args[1] then return end
|
|
||||||
setVolume(tonumber(args[1]))
|
|
||||||
end)
|
|
||||||
|
|
||||||
exports('setAllowProximityCycleState', function(state)
|
|
||||||
type_check({state, "boolean"})
|
|
||||||
disableProximityCycle = state
|
|
||||||
end)
|
|
||||||
|
|
||||||
function setProximityState(proximityRange, isCustom)
|
|
||||||
local voiceModeData = Cfg.voiceModes[mode]
|
|
||||||
MumbleSetTalkerProximity(proximityRange + 0.0)
|
|
||||||
LocalPlayer.state:set('proximity', {
|
|
||||||
index = mode,
|
|
||||||
distance = proximityRange,
|
|
||||||
mode = isCustom and "Custom" or voiceModeData[2],
|
|
||||||
}, true)
|
|
||||||
sendUIMessage({
|
|
||||||
-- JS expects this value to be - 1, "custom" voice is on the last index
|
|
||||||
voiceMode = isCustom and #Cfg.voiceModes or mode - 1
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
exports("overrideProximityRange", function(range, disableCycle)
|
|
||||||
type_check({range, "number"})
|
|
||||||
setProximityState(range, true)
|
|
||||||
if disableCycle then
|
|
||||||
disableProximityCycle = true
|
|
||||||
wasProximityDisabledFromOverride = true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
exports("clearProximityOverride", function()
|
|
||||||
local voiceModeData = Cfg.voiceModes[mode]
|
|
||||||
setProximityState(voiceModeData[1], false)
|
|
||||||
if wasProximityDisabledFromOverride then
|
|
||||||
disableProximityCycle = false
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterCommand('cycleproximity', function()
|
|
||||||
-- Proximity is either disabled, or manually overwritten.
|
|
||||||
if GetConvarInt('voice_enableProximityCycle', 1) ~= 1 or disableProximityCycle then return end
|
|
||||||
local newMode = mode + 1
|
|
||||||
|
|
||||||
-- If we're within the range of our voice modes, allow the increase, otherwise reset to the first state
|
|
||||||
if newMode <= #Cfg.voiceModes then
|
|
||||||
mode = newMode
|
|
||||||
else
|
|
||||||
mode = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
setProximityState(Cfg.voiceModes[mode][1], false)
|
|
||||||
TriggerEvent('pma-voice:setTalkingMode', mode)
|
|
||||||
end, false)
|
|
||||||
if gameVersion == 'fivem' then
|
|
||||||
RegisterKeyMapping('cycleproximity', 'Cycle Proximity', 'keyboard', GetConvar('voice_defaultCycle', 'F11'))
|
|
||||||
end
|
|
|
@ -1,41 +0,0 @@
|
||||||
function handleInitialState()
|
|
||||||
local voiceModeData = Cfg.voiceModes[mode]
|
|
||||||
MumbleSetTalkerProximity(voiceModeData[1] + 0.0)
|
|
||||||
MumbleClearVoiceTarget(voiceTarget)
|
|
||||||
MumbleSetVoiceTarget(voiceTarget)
|
|
||||||
MumbleSetVoiceChannel(playerServerId)
|
|
||||||
|
|
||||||
while MumbleGetVoiceChannelFromServerId(playerServerId) ~= playerServerId do
|
|
||||||
Wait(250)
|
|
||||||
end
|
|
||||||
|
|
||||||
MumbleAddVoiceTargetChannel(voiceTarget, playerServerId)
|
|
||||||
|
|
||||||
addNearbyPlayers()
|
|
||||||
end
|
|
||||||
|
|
||||||
AddEventHandler('mumbleConnected', function(address, isReconnecting)
|
|
||||||
logger.info('Connected to mumble server with address of %s, is this a reconnect %s', GetConvarInt('voice_hideEndpoints', 1) == 1 and 'HIDDEN' or address, isReconnecting)
|
|
||||||
|
|
||||||
logger.log('Connecting to mumble, setting targets.')
|
|
||||||
-- don't try to set channel instantly, we're still getting data.
|
|
||||||
local voiceModeData = Cfg.voiceModes[mode]
|
|
||||||
LocalPlayer.state:set('proximity', {
|
|
||||||
index = mode,
|
|
||||||
distance = voiceModeData[1],
|
|
||||||
mode = voiceModeData[2],
|
|
||||||
}, true)
|
|
||||||
|
|
||||||
handleInitialState()
|
|
||||||
|
|
||||||
logger.log('Finished connection logic')
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler('mumbleDisconnected', function(address)
|
|
||||||
logger.info('Disconnected from mumble server with address of %s', GetConvarInt('voice_hideEndpoints', 1) == 1 and 'HIDDEN' or address)
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- TODO: Convert the last Cfg to a Convar, while still keeping it simple.
|
|
||||||
AddEventHandler('pma-voice:settingsCallback', function(cb)
|
|
||||||
cb(Cfg)
|
|
||||||
end)
|
|
|
@ -1,42 +0,0 @@
|
||||||
|
|
||||||
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', tostring(true))
|
|
||||||
else
|
|
||||||
if micClicksKvp ~= 'true' and micClicksKvp ~= 'false' then
|
|
||||||
error('Invalid Kvp, throwing error for automatic cleaning')
|
|
||||||
end
|
|
||||||
micClicks = micClicksKvp
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
logger.warn('Failed to load resource Kvp, likely was inappropriately modified by another server, resetting the Kvp.')
|
|
||||||
SetResourceKvp('pma-voice_enableMicClicks', tostring(true))
|
|
||||||
micClicks = 'true'
|
|
||||||
end
|
|
||||||
sendUIMessage({
|
|
||||||
uiEnabled = GetConvarInt("voice_enableUi", 1) == 1,
|
|
||||||
voiceModes = json.encode(Cfg.voiceModes),
|
|
||||||
voiceMode = mode - 1
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Reinitialize channels if they're set.
|
|
||||||
if LocalPlayer.state.radioChannel ~= 0 then
|
|
||||||
setRadioChannel(LocalPlayer.state.radioChannel)
|
|
||||||
end
|
|
||||||
|
|
||||||
if LocalPlayer.state.callChannel ~= 0 then
|
|
||||||
setCallChannel(LocalPlayer.state.callChannel)
|
|
||||||
end
|
|
||||||
print('Script initialization finished.')
|
|
||||||
end)
|
|
|
@ -1,224 +0,0 @@
|
||||||
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', 30) / 100,
|
|
||||||
['phone'] = GetConvarInt('voice_defaultPhoneVolume', 60) / 100,
|
|
||||||
}
|
|
||||||
|
|
||||||
radioEnabled, radioPressed, mode = true, false, GetConvarInt('voice_defaultVoiceMode', 2)
|
|
||||||
radioData = {}
|
|
||||||
callData = {}
|
|
||||||
|
|
||||||
--- function setVolume
|
|
||||||
--- Toggles the players volume
|
|
||||||
---@param volume number between 0 and 100
|
|
||||||
---@param volumeType string the volume type (currently radio & call) to set the volume of (opt)
|
|
||||||
function setVolume(volume, volumeType)
|
|
||||||
type_check({volume, "number"})
|
|
||||||
local volume = volume / 100
|
|
||||||
|
|
||||||
if volumeType then
|
|
||||||
local volumeTbl = volumes[volumeType]
|
|
||||||
if volumeTbl then
|
|
||||||
LocalPlayer.state:set(volumeType, volume, true)
|
|
||||||
volumes[volumeType] = volume
|
|
||||||
else
|
|
||||||
error(('setVolume got a invalid volume type %s'):format(volumeType))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- _ is here to not mess with global 'type' function
|
|
||||||
for _type, vol in pairs(volumes) do
|
|
||||||
volumes[_type] = volume
|
|
||||||
LocalPlayer.state:set(_type, volume, true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
exports('setRadioVolume', function(vol)
|
|
||||||
setVolume(vol, 'radio')
|
|
||||||
end)
|
|
||||||
exports('getRadioVolume', function()
|
|
||||||
return volumes['radio']
|
|
||||||
end)
|
|
||||||
exports("setCallVolume", function(vol)
|
|
||||||
setVolume(vol, 'phone')
|
|
||||||
end)
|
|
||||||
exports('getCallVolume', function()
|
|
||||||
return volumes['phone']
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
-- default submix incase people want to fiddle with it.
|
|
||||||
-- freq_low = 389.0
|
|
||||||
-- freq_hi = 3248.0
|
|
||||||
-- fudge = 0.0
|
|
||||||
-- rm_mod_freq = 0.0
|
|
||||||
-- rm_mix = 0.16
|
|
||||||
-- o_freq_lo = 348.0
|
|
||||||
-- 0_freq_hi = 4900.0
|
|
||||||
|
|
||||||
if gameVersion == 'fivem' then
|
|
||||||
radioEffectId = CreateAudioSubmix('Radio')
|
|
||||||
SetAudioSubmixEffectRadioFx(radioEffectId, 0)
|
|
||||||
SetAudioSubmixEffectParamInt(radioEffectId, 0, `default`, 1)
|
|
||||||
AddAudioSubmixOutput(radioEffectId, 0)
|
|
||||||
|
|
||||||
phoneEffectId = CreateAudioSubmix('Phone')
|
|
||||||
SetAudioSubmixEffectRadioFx(phoneEffectId, 1)
|
|
||||||
SetAudioSubmixEffectParamInt(phoneEffectId, 1, `default`, 1)
|
|
||||||
SetAudioSubmixEffectParamFloat(phoneEffectId, 1, `freq_low`, 300.0)
|
|
||||||
SetAudioSubmixEffectParamFloat(phoneEffectId, 1, `freq_hi`, 6000.0)
|
|
||||||
AddAudioSubmixOutput(phoneEffectId, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local submixFunctions = {
|
|
||||||
['radio'] = function(plySource)
|
|
||||||
MumbleSetSubmixForServerId(plySource, radioEffectId)
|
|
||||||
end,
|
|
||||||
['phone'] = function(plySource)
|
|
||||||
MumbleSetSubmixForServerId(plySource, phoneEffectId)
|
|
||||||
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)
|
|
||||||
if enabled then
|
|
||||||
MumbleSetVolumeOverrideByServerId(plySource, enabled and volumes[moduleType])
|
|
||||||
if GetConvarInt('voice_enableSubmix', 1) == 1 and gameVersion == 'fivem' then
|
|
||||||
if moduleType then
|
|
||||||
disableSubmixReset[plySource] = true
|
|
||||||
submixFunctions[moduleType](plySource)
|
|
||||||
else
|
|
||||||
MumbleSetSubmixForServerId(plySource, -1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if GetConvarInt('voice_enableSubmix', 1) == 1 and gameVersion == 'fivem' then
|
|
||||||
-- garbage collect it
|
|
||||||
disableSubmixReset[plySource] = nil
|
|
||||||
SetTimeout(250, function()
|
|
||||||
if not disableSubmixReset[plySource] then
|
|
||||||
MumbleSetSubmixForServerId(plySource, -1)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
MumbleSetVolumeOverrideByServerId(plySource, -1.0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- function playerTargets
|
|
||||||
---Adds players voices to the local players listen channels allowing
|
|
||||||
---Them to communicate at long range, ignoring proximity range.
|
|
||||||
---@diagnostic disable-next-line: undefined-doc-param
|
|
||||||
---@param targets table expects multiple tables to be sent over
|
|
||||||
function playerTargets(...)
|
|
||||||
local targets = {...}
|
|
||||||
local addedPlayers = {
|
|
||||||
[playerServerId] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for i = 1, #targets do
|
|
||||||
for id, _ in pairs(targets[i]) do
|
|
||||||
-- we don't want to log ourself, or listen to ourself
|
|
||||||
if addedPlayers[id] and id ~= playerServerId then
|
|
||||||
logger.verbose('[main] %s is already target don\'t re-add', id)
|
|
||||||
goto skip_loop
|
|
||||||
end
|
|
||||||
if not addedPlayers[id] then
|
|
||||||
logger.verbose('[main] Adding %s as a voice target', id)
|
|
||||||
addedPlayers[id] = true
|
|
||||||
MumbleAddVoiceTargetPlayerByServerId(voiceTarget, id)
|
|
||||||
end
|
|
||||||
::skip_loop::
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- function playMicClicks
|
|
||||||
---plays the mic click if the player has them enabled.
|
|
||||||
---@param clickType boolean whether to play the 'on' or 'off' click.
|
|
||||||
function playMicClicks(clickType)
|
|
||||||
if micClicks ~= 'true' then return logger.verbose("Not playing mic clicks because client has them disabled") end
|
|
||||||
sendUIMessage({
|
|
||||||
sound = (clickType and "audio_on" or "audio_off"),
|
|
||||||
volume = (clickType and volumes["radio"] or 0.05)
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
--- toggles the targeted player muted
|
|
||||||
---@param source number the player to mute
|
|
||||||
function toggleMutePlayer(source)
|
|
||||||
if mutedPlayers[source] then
|
|
||||||
mutedPlayers[source] = nil
|
|
||||||
MumbleSetVolumeOverrideByServerId(source, -1.0)
|
|
||||||
else
|
|
||||||
mutedPlayers[source] = true
|
|
||||||
MumbleSetVolumeOverrideByServerId(source, 0.0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
exports('toggleMutePlayer', toggleMutePlayer)
|
|
||||||
|
|
||||||
--- function setVoiceProperty
|
|
||||||
--- sets the specified voice property
|
|
||||||
---@param type string what voice property you want to change (only takes 'radioEnabled' and 'micClicks')
|
|
||||||
---@param value any the value to set the type to.
|
|
||||||
function setVoiceProperty(type, value)
|
|
||||||
if type == "radioEnabled" then
|
|
||||||
radioEnabled = value
|
|
||||||
sendUIMessage({
|
|
||||||
radioEnabled = value
|
|
||||||
})
|
|
||||||
elseif type == "micClicks" then
|
|
||||||
local val = tostring(value)
|
|
||||||
micClicks = val
|
|
||||||
SetResourceKvp('pma-voice_enableMicClicks', val)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
exports('setVoiceProperty', setVoiceProperty)
|
|
||||||
-- compatibility
|
|
||||||
exports('SetMumbleProperty', setVoiceProperty)
|
|
||||||
exports('SetTokoProperty', setVoiceProperty)
|
|
||||||
|
|
||||||
|
|
||||||
-- cache their external servers so if it changes in runtime we can reconnect the client.
|
|
||||||
local externalAddress = ''
|
|
||||||
local externalPort = 0
|
|
||||||
CreateThread(function()
|
|
||||||
while true do
|
|
||||||
Wait(500)
|
|
||||||
-- only change if what we have doesn't match the cache
|
|
||||||
if GetConvar('voice_externalAddress', '') ~= externalAddress or GetConvarInt('voice_externalPort', 0) ~= externalPort then
|
|
||||||
externalAddress = GetConvar('voice_externalAddress', '')
|
|
||||||
externalPort = GetConvarInt('voice_externalPort', 0)
|
|
||||||
MumbleSetServerAddress(GetConvar('voice_externalAddress', ''), GetConvarInt('voice_externalPort', 0))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
if gameVersion == 'redm' then
|
|
||||||
CreateThread(function()
|
|
||||||
while true do
|
|
||||||
if IsControlJustPressed(0, 0xA5BDCD3C --[[ Right Bracket ]]) then
|
|
||||||
ExecuteCommand('cycleproximity')
|
|
||||||
end
|
|
||||||
if IsControlJustPressed(0, 0x430593AA --[[ Left Bracket ]]) then
|
|
||||||
ExecuteCommand('+radiotalk')
|
|
||||||
elseif IsControlJustReleased(0, 0x430593AA --[[ Left Bracket ]]) then
|
|
||||||
ExecuteCommand('-radiotalk')
|
|
||||||
end
|
|
||||||
|
|
||||||
Wait(0)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
|
@ -1,156 +0,0 @@
|
||||||
-- used when muted
|
|
||||||
local disableUpdates = false
|
|
||||||
local isListenerEnabled = false
|
|
||||||
local plyCoords = GetEntityCoords(PlayerPedId())
|
|
||||||
|
|
||||||
function orig_addProximityCheck(ply)
|
|
||||||
local tgtPed = GetPlayerPed(ply)
|
|
||||||
local voiceModeData = Cfg.voiceModes[mode]
|
|
||||||
local distance = GetConvar('voice_useNativeAudio', 'false') == 'true' and voiceModeData[1] * 3 or voiceModeData[1]
|
|
||||||
|
|
||||||
return #(plyCoords - GetEntityCoords(tgtPed)) < 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())
|
|
||||||
|
|
||||||
MumbleClearVoiceTargetChannels(voiceTarget)
|
|
||||||
local players = GetActivePlayers()
|
|
||||||
for i = 1, #players do
|
|
||||||
local ply = players[i]
|
|
||||||
local serverId = GetPlayerServerId(ply)
|
|
||||||
|
|
||||||
if addProximityCheck(ply) then
|
|
||||||
if isTarget then goto skip_loop end
|
|
||||||
|
|
||||||
logger.verbose('Added %s as a voice target', serverId)
|
|
||||||
MumbleAddVoiceTargetChannel(voiceTarget, serverId)
|
|
||||||
end
|
|
||||||
|
|
||||||
::skip_loop::
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function setSpectatorMode(enabled)
|
|
||||||
logger.info('Setting spectate mode to %s', enabled)
|
|
||||||
isListenerEnabled = enabled
|
|
||||||
local players = GetActivePlayers()
|
|
||||||
if isListenerEnabled then
|
|
||||||
for i = 1, #players do
|
|
||||||
local ply = players[i]
|
|
||||||
local serverId = GetPlayerServerId(ply)
|
|
||||||
if serverId == playerServerId then goto skip_loop end
|
|
||||||
logger.verbose("Adding %s to listen table", serverId)
|
|
||||||
MumbleAddVoiceChannelListen(serverId)
|
|
||||||
::skip_loop::
|
|
||||||
end
|
|
||||||
else
|
|
||||||
for i = 1, #players do
|
|
||||||
local ply = players[i]
|
|
||||||
local serverId = GetPlayerServerId(ply)
|
|
||||||
if serverId == playerServerId then goto skip_loop end
|
|
||||||
logger.verbose("Removing %s from listen table", serverId)
|
|
||||||
MumbleRemoveVoiceChannelListen(serverId)
|
|
||||||
::skip_loop::
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
RegisterNetEvent('onPlayerJoining', function(serverId)
|
|
||||||
if isListenerEnabled then
|
|
||||||
MumbleAddVoiceChannelListen(serverId)
|
|
||||||
logger.verbose("Adding %s to listen table", serverId)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent('onPlayerDropped', function(serverId)
|
|
||||||
if isListenerEnabled then
|
|
||||||
MumbleRemoveVoiceChannelListen(serverId)
|
|
||||||
logger.verbose("Removing %s from listen table", serverId)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- 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"
|
|
||||||
Citizen.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() 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()
|
|
||||||
local isSpectating = NetworkIsInSpectatorMode()
|
|
||||||
if isSpectating and not isListenerEnabled then
|
|
||||||
setSpectatorMode(true)
|
|
||||||
elseif not isSpectating and isListenerEnabled then
|
|
||||||
setSpectatorMode(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Wait(GetConvarInt('voice_refreshRate', 200))
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
exports("setVoiceState", function(_voiceState, channel)
|
|
||||||
if _voiceState ~= "proximity" and _voiceState ~= "channel" then
|
|
||||||
logger.error("Didn't get a proper voice state, expected proximity or channel, got %s", _voiceState)
|
|
||||||
end
|
|
||||||
voiceState = _voiceState
|
|
||||||
if voiceState == "channel" then
|
|
||||||
type_check({channel, "number"})
|
|
||||||
-- 65535 is the highest a client id can go, so we add that to the base channel so we don't manage to get onto a players channel
|
|
||||||
channel = channel + 65535
|
|
||||||
MumbleSetVoiceChannel(channel)
|
|
||||||
while MumbleGetVoiceChannelFromServerId(playerServerId) ~= channel do
|
|
||||||
Wait(250)
|
|
||||||
end
|
|
||||||
MumbleAddVoiceTargetChannel(voiceTarget, channel)
|
|
||||||
elseif voiceState == "proximity" then
|
|
||||||
handleInitialState()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
AddEventHandler("onClientResourceStop", function(resource)
|
|
||||||
if type(addProximityCheck) == "table" then
|
|
||||||
local proximityCheckRef = addProximityCheck.__cfx_functionReference
|
|
||||||
if proximityCheckRef then
|
|
||||||
local isResource = string.match(proximityCheckRef, resource)
|
|
||||||
if isResource then
|
|
||||||
addProximityCheck = orig_addProximityCheck
|
|
||||||
logger.warn('Reset proximity check to default, the original resource [%s] which provided the function restarted', resource)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
|
@ -1,91 +0,0 @@
|
||||||
local callChannel = 0
|
|
||||||
|
|
||||||
---function createPhoneThread
|
|
||||||
---creates a phone thread to listen for key presses
|
|
||||||
local function createPhoneThread()
|
|
||||||
Citizen.CreateThread(function()
|
|
||||||
local changed = false
|
|
||||||
while callChannel ~= 0 do
|
|
||||||
-- check if they're pressing voice keybinds
|
|
||||||
if MumbleIsPlayerTalking(PlayerId()) and not changed then
|
|
||||||
changed = true
|
|
||||||
playerTargets(radioPressed and radioData or {}, callData)
|
|
||||||
TriggerServerEvent('pma-voice:setTalkingOnCall', true)
|
|
||||||
elseif changed and MumbleIsPlayerTalking(PlayerId()) ~= 1 then
|
|
||||||
changed = false
|
|
||||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
|
||||||
TriggerServerEvent('pma-voice:setTalkingOnCall', false)
|
|
||||||
end
|
|
||||||
Wait(0)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
RegisterNetEvent('pma-voice:syncCallData', function(callTable, channel)
|
|
||||||
callData = callTable
|
|
||||||
for tgt, enabled in pairs(callTable) do
|
|
||||||
if tgt ~= playerServerId then
|
|
||||||
toggleVoice(tgt, enabled, 'phone')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent('pma-voice:setTalkingOnCall', function(tgt, enabled)
|
|
||||||
if tgt ~= playerServerId then
|
|
||||||
callData[tgt] = enabled
|
|
||||||
toggleVoice(tgt, enabled, 'phone')
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent('pma-voice:addPlayerToCall', function(plySource)
|
|
||||||
callData[plySource] = false
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent('pma-voice:removePlayerFromCall', function(plySource)
|
|
||||||
if plySource == playerServerId then
|
|
||||||
for tgt, _ in pairs(callData) do
|
|
||||||
if tgt ~= playerServerId then
|
|
||||||
toggleVoice(tgt, false, 'phone')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
callData = {}
|
|
||||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
|
||||||
playerTargets(radioPressed and radioData or {}, callData)
|
|
||||||
else
|
|
||||||
callData[plySource] = nil
|
|
||||||
toggleVoice(plySource, false, 'phone')
|
|
||||||
if MumbleIsPlayerTalking(PlayerId()) then
|
|
||||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
|
||||||
playerTargets(radioPressed and radioData or {}, callData)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
function setCallChannel(channel)
|
|
||||||
if GetConvarInt('voice_enablePhones', 1) ~= 1 then return end
|
|
||||||
TriggerServerEvent('pma-voice:setPlayerCall', channel)
|
|
||||||
callChannel = channel
|
|
||||||
sendUIMessage({
|
|
||||||
callInfo = channel
|
|
||||||
})
|
|
||||||
createPhoneThread()
|
|
||||||
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_enablePhones', 1) ~= 1 then return end
|
|
||||||
callChannel = _callChannel
|
|
||||||
createPhoneThread()
|
|
||||||
end)
|
|
|
@ -1,211 +0,0 @@
|
||||||
local radioChannel = 0
|
|
||||||
local radioNames = {}
|
|
||||||
local disableRadioAnim = false
|
|
||||||
|
|
||||||
--- event syncRadioData
|
|
||||||
--- syncs the current players on the radio to the client
|
|
||||||
---@param radioTable table the table of the current players on the radio
|
|
||||||
---@param localPlyRadioName string the local players name
|
|
||||||
function syncRadioData(radioTable, localPlyRadioName)
|
|
||||||
radioData = radioTable
|
|
||||||
logger.info('[radio] Syncing radio table.')
|
|
||||||
if GetConvarInt('voice_debugMode', 0) >= 4 then
|
|
||||||
print('-------- RADIO TABLE --------')
|
|
||||||
tPrint(radioData)
|
|
||||||
print('-----------------------------')
|
|
||||||
end
|
|
||||||
for tgt, enabled in pairs(radioTable) do
|
|
||||||
if tgt ~= playerServerId then
|
|
||||||
toggleVoice(tgt, enabled, 'radio')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
sendUIMessage({
|
|
||||||
radioChannel = radioChannel,
|
|
||||||
radioEnabled = radioEnabled
|
|
||||||
})
|
|
||||||
if GetConvarInt("voice_syncPlayerNames", 0) == 1 then
|
|
||||||
radioNames[playerServerId] = localPlyRadioName
|
|
||||||
end
|
|
||||||
end
|
|
||||||
RegisterNetEvent('pma-voice:syncRadioData', syncRadioData)
|
|
||||||
|
|
||||||
--- event setTalkingOnRadio
|
|
||||||
--- sets the players talking status, triggered when a player starts/stops talking.
|
|
||||||
---@param plySource number the players server id.
|
|
||||||
---@param enabled boolean whether the player is talking or not.
|
|
||||||
function setTalkingOnRadio(plySource, enabled)
|
|
||||||
toggleVoice(plySource, enabled, 'radio')
|
|
||||||
radioData[plySource] = enabled
|
|
||||||
playMicClicks(enabled)
|
|
||||||
end
|
|
||||||
RegisterNetEvent('pma-voice:setTalkingOnRadio', setTalkingOnRadio)
|
|
||||||
|
|
||||||
--- event addPlayerToRadio
|
|
||||||
--- adds a player onto the radio.
|
|
||||||
---@param plySource number the players server id to add to the radio.
|
|
||||||
function addPlayerToRadio(plySource, plyRadioName)
|
|
||||||
radioData[plySource] = false
|
|
||||||
if GetConvarInt("voice_syncPlayerNames", 0) == 1 then
|
|
||||||
radioNames[plySource] = plyRadioName
|
|
||||||
end
|
|
||||||
if radioPressed then
|
|
||||||
logger.info('[radio] %s joined radio %s while we were talking, adding them to targets', plySource, radioChannel)
|
|
||||||
playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
|
||||||
else
|
|
||||||
logger.info('[radio] %s joined radio %s', plySource, radioChannel)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
RegisterNetEvent('pma-voice:addPlayerToRadio', addPlayerToRadio)
|
|
||||||
|
|
||||||
--- event removePlayerFromRadio
|
|
||||||
--- removes the player (or self) from the radio
|
|
||||||
---@param plySource number the players server id to remove from the radio.
|
|
||||||
function removePlayerFromRadio(plySource)
|
|
||||||
if plySource == playerServerId then
|
|
||||||
logger.info('[radio] Left radio %s, cleaning up.', radioChannel)
|
|
||||||
for tgt, _ in pairs(radioData) do
|
|
||||||
if tgt ~= playerServerId then
|
|
||||||
toggleVoice(tgt, false, 'radio')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
sendUIMessage({
|
|
||||||
radioChannel = 0,
|
|
||||||
radioEnabled = radioEnabled
|
|
||||||
})
|
|
||||||
radioNames = {}
|
|
||||||
radioData = {}
|
|
||||||
playerTargets(MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
|
||||||
else
|
|
||||||
toggleVoice(plySource, false)
|
|
||||||
if radioPressed then
|
|
||||||
logger.info('[radio] %s left radio %s while we were talking, updating targets.', plySource, radioChannel)
|
|
||||||
playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
|
||||||
else
|
|
||||||
logger.info('[radio] %s has left radio %s', plySource, radioChannel)
|
|
||||||
end
|
|
||||||
radioData[plySource] = nil
|
|
||||||
if GetConvarInt("voice_syncPlayerNames", 0) == 1 then
|
|
||||||
radioNames[plySource] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
RegisterNetEvent('pma-voice:removePlayerFromRadio', removePlayerFromRadio)
|
|
||||||
|
|
||||||
--- function setRadioChannel
|
|
||||||
--- sets the local players current radio channel and updates the server
|
|
||||||
---@param channel number the channel to set the player to, or 0 to remove them.
|
|
||||||
function setRadioChannel(channel)
|
|
||||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
|
||||||
type_check({channel, "number"})
|
|
||||||
TriggerServerEvent('pma-voice:setPlayerRadio', channel)
|
|
||||||
radioChannel = channel
|
|
||||||
end
|
|
||||||
|
|
||||||
--- exports setRadioChannel
|
|
||||||
--- sets the local players current radio channel and updates the server
|
|
||||||
---@param channel number the channel to set the player to, or 0 to remove them.
|
|
||||||
exports('setRadioChannel', setRadioChannel)
|
|
||||||
-- mumble-voip compatability
|
|
||||||
exports('SetRadioChannel', setRadioChannel)
|
|
||||||
|
|
||||||
--- exports removePlayerFromRadio
|
|
||||||
--- sets the local players current radio channel and updates the server
|
|
||||||
exports('removePlayerFromRadio', function()
|
|
||||||
setRadioChannel(0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
--- exports addPlayerToRadio
|
|
||||||
--- sets the local players current radio channel and updates the server
|
|
||||||
---@param _radio number the channel to set the player to, or 0 to remove them.
|
|
||||||
exports('addPlayerToRadio', function(_radio)
|
|
||||||
local radio = tonumber(_radio)
|
|
||||||
if radio then
|
|
||||||
setRadioChannel(radio)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
--- exports toggleRadioAnim
|
|
||||||
--- toggles whether the client should play radio anim or not, if the animation should be played or notvaliddance
|
|
||||||
exports('toggleRadioAnim', function()
|
|
||||||
disableRadioAnim = not disableRadioAnim
|
|
||||||
TriggerEvent('pma-voice:toggleRadioAnim', disableRadioAnim)
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- exports disableRadioAnim
|
|
||||||
--- returns whether the client is undercover or not
|
|
||||||
exports('getRadioAnimState', function()
|
|
||||||
return toggleRadioAnim
|
|
||||||
end)
|
|
||||||
|
|
||||||
--- check if the player is dead
|
|
||||||
--- seperating this so if people use different methods they can customize
|
|
||||||
--- it to their need as this will likely never be changed
|
|
||||||
--- but you can integrate the below state bag to your death resources.
|
|
||||||
--- LocalPlayer.state:set('isDead', true or false, false)
|
|
||||||
function isDead()
|
|
||||||
if LocalPlayer.state.isDead then
|
|
||||||
return true
|
|
||||||
elseif IsPlayerDead(PlayerId()) then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
RegisterCommand('+radiotalk', function()
|
|
||||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
|
||||||
if isDead() then return end
|
|
||||||
|
|
||||||
if not radioPressed and radioEnabled then
|
|
||||||
if radioChannel > 0 then
|
|
||||||
logger.info('[radio] Start broadcasting, update targets and notify server.')
|
|
||||||
playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
|
||||||
TriggerServerEvent('pma-voice:setTalkingOnRadio', true)
|
|
||||||
radioPressed = true
|
|
||||||
playMicClicks(true)
|
|
||||||
if GetConvarInt('voice_enableRadioAnim', 0) == 1 and not (GetConvarInt('voice_disableVehicleRadioAnim', 0) == 1 and IsPedInAnyVehicle(PlayerPedId(), false)) then
|
|
||||||
if not disableRadioAnim then
|
|
||||||
RequestAnimDict('random@arrests')
|
|
||||||
while not HasAnimDictLoaded('random@arrests') do
|
|
||||||
Citizen.Wait(10)
|
|
||||||
end
|
|
||||||
TaskPlayAnim(PlayerPedId(), "random@arrests", "generic_radio_enter", 8.0, 2.0, -1, 50, 2.0, 0, 0, 0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Citizen.CreateThread(function()
|
|
||||||
TriggerEvent("pma-voice:radioActive", true)
|
|
||||||
while radioPressed do
|
|
||||||
Wait(0)
|
|
||||||
SetControlNormal(0, 249, 1.0)
|
|
||||||
SetControlNormal(1, 249, 1.0)
|
|
||||||
SetControlNormal(2, 249, 1.0)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end, false)
|
|
||||||
|
|
||||||
RegisterCommand('-radiotalk', function()
|
|
||||||
if radioChannel > 0 or radioEnabled and radioPressed then
|
|
||||||
radioPressed = false
|
|
||||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
|
||||||
playerTargets(MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
|
||||||
TriggerEvent("pma-voice:radioActive", false)
|
|
||||||
playMicClicks(false)
|
|
||||||
if GetConvarInt('voice_enableRadioAnim', 0) == 1 then
|
|
||||||
StopAnimTask(PlayerPedId(), "random@arrests", "generic_radio_enter", -4.0)
|
|
||||||
end
|
|
||||||
TriggerServerEvent('pma-voice:setTalkingOnRadio', false)
|
|
||||||
end
|
|
||||||
end, false)
|
|
||||||
if gameVersion == 'fivem' then
|
|
||||||
RegisterKeyMapping('+radiotalk', 'Talk over Radio', 'keyboard', GetConvar('voice_defaultRadio', 'LMENU'))
|
|
||||||
end
|
|
||||||
|
|
||||||
--- event syncRadio
|
|
||||||
--- syncs the players radio, only happens if the radio was set server side.
|
|
||||||
---@param _radioChannel number the radio channel to set the player to.
|
|
||||||
function syncRadio(_radioChannel)
|
|
||||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
|
||||||
logger.info('[radio] radio set serverside update to radio %s', radioChannel)
|
|
||||||
radioChannel = _radioChannel
|
|
||||||
end
|
|
||||||
RegisterNetEvent('pma-voice:clSetPlayerRadio', syncRadio)
|
|
|
@ -1,11 +0,0 @@
|
||||||
local uiReady = promise.new()
|
|
||||||
function sendUIMessage(message)
|
|
||||||
Citizen.Await(uiReady)
|
|
||||||
SendNUIMessage(message)
|
|
||||||
end
|
|
||||||
|
|
||||||
RegisterNUICallback("uiReady", function(data, cb)
|
|
||||||
uiReady:resolve(true)
|
|
||||||
|
|
||||||
cb('ok')
|
|
||||||
end)
|
|
|
@ -1 +0,0 @@
|
||||||
theme: jekyll-theme-midnight
|
|
|
@ -1,27 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,12 +0,0 @@
|
||||||
## 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()
|
|
||||||
```
|
|
|
@ -1,12 +0,0 @@
|
||||||
## 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()
|
|
||||||
```
|
|
|
@ -1,25 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,14 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,26 +0,0 @@
|
||||||
## 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
|
|
||||||
expots ['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)
|
|
||||||
```
|
|
|
@ -1,14 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,17 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,3 +0,0 @@
|
||||||
## Routing Buckets
|
|
||||||
|
|
||||||
pma-voice natively supports routing buckets.
|
|
|
@ -1,21 +0,0 @@
|
||||||
## 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
|
|
||||||
```
|
|
|
@ -1,22 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,14 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,14 +0,0 @@
|
||||||
## 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)
|
|
||||||
```
|
|
|
@ -1,17 +0,0 @@
|
||||||
## 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
|
|
||||||
```
|
|
|
@ -1,69 +0,0 @@
|
||||||
game 'common'
|
|
||||||
|
|
||||||
fx_version 'cerulean'
|
|
||||||
author 'AvarianKnight'
|
|
||||||
description 'VOIP built using FiveM\'s built in mumble.'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
'/onesync',
|
|
||||||
}
|
|
||||||
|
|
||||||
lua54 'yes'
|
|
||||||
|
|
||||||
shared_script 'shared.lua'
|
|
||||||
|
|
||||||
client_scripts {
|
|
||||||
'client/utils/*',
|
|
||||||
'client/init/proximity.lua',
|
|
||||||
'client/init/init.lua',
|
|
||||||
'client/init/main.lua',
|
|
||||||
'client/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 phone volume", "$voice_defaultPhoneVolume", "CV_INT", "60" },
|
|
||||||
{ "Enable radios", "$voice_enableRadios", "CV_INT", "1" },
|
|
||||||
{ "Enable phones", "$voice_enablePhones", "CV_INT", "1" },
|
|
||||||
{ "Enable submix", "$voice_enableSubmix", "CV_INT", "1" },
|
|
||||||
{ "Enable radio animation", "$voice_enableRadioAnim", "CV_INT", "0" },
|
|
||||||
{ "Radio key", "$voice_defaultRadio", "CV_STRING", "LALT" },
|
|
||||||
{ "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" },
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
voiceData = {}
|
|
||||||
radioData = {}
|
|
||||||
callData = {}
|
|
||||||
|
|
||||||
function defaultTable(source)
|
|
||||||
handleStateBagInitilization(source)
|
|
||||||
return {
|
|
||||||
radio = 0,
|
|
||||||
call = 0,
|
|
||||||
lastRadio = 0,
|
|
||||||
lastCall = 0
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function handleStateBagInitilization(source)
|
|
||||||
local plyState = Player(source).state
|
|
||||||
if not plyState.pmaVoiceInit then
|
|
||||||
plyState:set('radio', GetConvarInt('voice_defaultRadioVolume', 30), true)
|
|
||||||
plyState:set('phone', GetConvarInt('voice_defaultPhoneVolume', 60), true)
|
|
||||||
plyState:set('proximity', {}, true)
|
|
||||||
plyState:set('callChannel', 0, true)
|
|
||||||
plyState:set('radioChannel', 0, true)
|
|
||||||
plyState:set('voiceIntent', 'speech', true)
|
|
||||||
-- We want to save voice inits because we'll automatically reinitalize calls and channels
|
|
||||||
plyState:set('pmaVoiceInit', true, false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Citizen.CreateThread(function()
|
|
||||||
|
|
||||||
local plyTbl = GetPlayers()
|
|
||||||
for i = 1, #plyTbl do
|
|
||||||
local ply = tonumber(plyTbl[i])
|
|
||||||
voiceData[ply] = defaultTable(plyTbl[i])
|
|
||||||
end
|
|
||||||
|
|
||||||
Wait(5000)
|
|
||||||
|
|
||||||
local nativeAudio = GetConvar('voice_useNativeAudio', 'false')
|
|
||||||
local _3dAudio = GetConvar('voice_use3dAudio', 'false')
|
|
||||||
local _2dAudio = GetConvar('voice_use2dAudio', 'false')
|
|
||||||
local sendingRangeOnly = GetConvar('voice_useSendingRangeOnly', 'false')
|
|
||||||
local gameVersion = GetConvar('gamename', 'fivem')
|
|
||||||
|
|
||||||
-- handle no convars being set (default drag n' drop)
|
|
||||||
if
|
|
||||||
nativeAudio == 'false'
|
|
||||||
and _3dAudio == 'false'
|
|
||||||
and _2dAudio == 'false'
|
|
||||||
then
|
|
||||||
if gameVersion == 'fivem' then
|
|
||||||
SetConvarReplicated('voice_useNativeAudio', 'true')
|
|
||||||
if sendingRangeOnly == 'false' then
|
|
||||||
SetConvarReplicated('voice_useSendingRangeOnly', 'true')
|
|
||||||
end
|
|
||||||
logger.info('No convars detected for voice mode, defaulting to \'setr voice_useNativeAudio true\' and \'setr voice_useSendingRangeOnly true\'')
|
|
||||||
else
|
|
||||||
SetConvarReplicated('voice_use3dAudio', 'true')
|
|
||||||
if sendingRangeOnly == 'false' then
|
|
||||||
SetConvarReplicated('voice_useSendingRangeOnly', 'true')
|
|
||||||
end
|
|
||||||
logger.info('No convars detected for voice mode, defaulting to \'setr voice_use3dAudio true\' and \'setr voice_useSendingRangeOnly true\'')
|
|
||||||
end
|
|
||||||
elseif sendingRangeOnly == 'false' then
|
|
||||||
logger.warn('It\'s recommended to have \'voice_useSendingRangeOnly\' set to true you can do that with \'setr voice_useSendingRangeOnly true\', this prevents players who directly join the mumble server from broadcasting to players.')
|
|
||||||
end
|
|
||||||
|
|
||||||
if GetConvar('gamename', 'fivem') == 'rdr3' then
|
|
||||||
if nativeAudio == 'true' then
|
|
||||||
logger.warn("RedM doesn't currently support native audio, automatically switching to 3d audio. This also means that submixes will not work.")
|
|
||||||
SetConvarReplicated('voice_useNativeAudio', 'false')
|
|
||||||
SetConvarReplicated('voice_use3dAudio', 'true')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local radioVolume = GetConvarInt("voice_defaultRadioVolume", 30)
|
|
||||||
local phoneVolume = GetConvarInt("voice_defaultPhoneVolume", 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
|
|
||||||
phoneVolume == 0 or phoneVolume == 1
|
|
||||||
then
|
|
||||||
SetConvarReplicated("voice_defaultRadioVolume", 30)
|
|
||||||
SetConvarReplicated("voice_defaultPhoneVolume", 60)
|
|
||||||
for i = 1, 5 do
|
|
||||||
Wait(5000)
|
|
||||||
logger.warn("`voice_defaultRadioVolume` or `voice_defaultPhoneVolume` have their value set as a float, this is going to automatically be fixed but please update your convars.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler('playerJoining', function()
|
|
||||||
if not voiceData[source] then
|
|
||||||
voiceData[source] = defaultTable(source)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler("playerDropped", function()
|
|
||||||
local source = source
|
|
||||||
if voiceData[source] then
|
|
||||||
local plyData = voiceData[source]
|
|
||||||
|
|
||||||
if plyData.radio ~= 0 then
|
|
||||||
removePlayerFromRadio(source, plyData.radio)
|
|
||||||
end
|
|
||||||
|
|
||||||
if plyData.call ~= 0 then
|
|
||||||
removePlayerFromCall(source, plyData.call)
|
|
||||||
end
|
|
||||||
|
|
||||||
voiceData[source] = nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
if GetConvarInt('voice_externalDisallowJoin', 0) == 1 then
|
|
||||||
AddEventHandler('playerConnecting', function(_, _, deferral)
|
|
||||||
deferral.defer()
|
|
||||||
Wait(0)
|
|
||||||
deferral.done('This server is not accepting connections.')
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- only meant for internal use so no documentation
|
|
||||||
function isValidPlayer(source)
|
|
||||||
return voiceData[source]
|
|
||||||
end
|
|
||||||
exports('isValidPlayer', isValidPlayer)
|
|
||||||
|
|
||||||
function getPlayersInRadioChannel(channel)
|
|
||||||
local returnChannel = radioData[channel]
|
|
||||||
if returnChannel then
|
|
||||||
return returnChannel
|
|
||||||
end
|
|
||||||
-- channel doesnt exist
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
exports('getPlayersInRadioChannel', getPlayersInRadioChannel)
|
|
||||||
exports('GetPlayersInRadioChannel', getPlayersInRadioChannel)
|
|
|
@ -1,94 +0,0 @@
|
||||||
--- 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('[phone] 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('[phone] 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] = false
|
|
||||||
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_enablePhones', 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)
|
|
||||||
|
|
||||||
function setTalkingOnCall(talking)
|
|
||||||
if GetConvarInt('voice_enablePhones', 1) ~= 1 then return end
|
|
||||||
local source = source
|
|
||||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
|
||||||
local plyVoice = voiceData[source]
|
|
||||||
local callTbl = callData[plyVoice.call]
|
|
||||||
if callTbl then
|
|
||||||
logger.verbose('[phone] %s %s talking in call %s', source, talking and 'started' or 'stopped', plyVoice.call)
|
|
||||||
for player, _ in pairs(callTbl) do
|
|
||||||
if player ~= source then
|
|
||||||
logger.verbose('[call] Sending event to %s to tell them that %s is talking', player, source)
|
|
||||||
TriggerClientEvent('pma-voice:setTalkingOnCall', player, source, talking)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
logger.verbose('[phone] %s tried to talk in call %s, but it doesnt exist.', source, plyVoice.call)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
RegisterNetEvent('pma-voice:setTalkingOnCall', setTalkingOnCall)
|
|
|
@ -1,165 +0,0 @@
|
||||||
local radioChecks = {}
|
|
||||||
|
|
||||||
--- checks if the player can join the channel specified
|
|
||||||
--- @param source number the source of the player
|
|
||||||
--- @param radioChannel number the channel they're trying to join
|
|
||||||
--- @return boolean if the user can join the channel
|
|
||||||
function canJoinChannel(source, radioChannel)
|
|
||||||
if radioChecks[radioChannel] then
|
|
||||||
return radioChecks[radioChannel](source)
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
--- adds a check to the channel, function is expected to return a boolean of true or false
|
|
||||||
---@param channel number the channel to add a check to
|
|
||||||
---@param cb function the function to execute the check on
|
|
||||||
function addChannelCheck(channel, cb)
|
|
||||||
local channelType = type(channel)
|
|
||||||
local cbType = type(cb)
|
|
||||||
if channelType ~= "number" then
|
|
||||||
error(("'channel' expected 'number' got '%s'"):format(channelType))
|
|
||||||
end
|
|
||||||
if cbType ~= 'table' or not cb.__cfx_functionReference then
|
|
||||||
error(("'cb' expected 'function' got '%s'"):format(cbType))
|
|
||||||
end
|
|
||||||
radioChecks[channel] = cb
|
|
||||||
logger.info("%s added a check to channel %s", GetInvokingResource(), channel)
|
|
||||||
end
|
|
||||||
exports('addChannelCheck', addChannelCheck)
|
|
||||||
|
|
||||||
local function radioNameGetter_orig(source)
|
|
||||||
return GetPlayerName(source)
|
|
||||||
end
|
|
||||||
local radioNameGetter = radioNameGetter_orig
|
|
||||||
|
|
||||||
--- adds a check to the channel, function is expected to return a boolean of true or false
|
|
||||||
---@param cb function the function to execute the check on
|
|
||||||
function overrideRadioNameGetter(channel, cb)
|
|
||||||
local cbType = type(cb)
|
|
||||||
if cbType == 'table' and not cb.__cfx_functionReference then
|
|
||||||
error(("'cb' expected 'function' got '%s'"):format(cbType))
|
|
||||||
end
|
|
||||||
radioNameGetter = cb
|
|
||||||
logger.info("%s added a check to channel %s", GetInvokingResource(), channel)
|
|
||||||
end
|
|
||||||
exports('overrideRadioNameGetter', overrideRadioNameGetter)
|
|
||||||
|
|
||||||
--- adds a player to the specified radion channel
|
|
||||||
---@param source number the player to add to the channel
|
|
||||||
---@param radioChannel number the channel to set them to
|
|
||||||
function addPlayerToRadio(source, radioChannel)
|
|
||||||
if not canJoinChannel(source, radioChannel) then
|
|
||||||
-- remove the player from the radio client side
|
|
||||||
return TriggerClientEvent('pma-voice:removePlayerFromRadio', source, source)
|
|
||||||
end
|
|
||||||
logger.verbose('[radio] Added %s to radio %s', source, radioChannel)
|
|
||||||
|
|
||||||
-- check if the channel exists, if it does set the varaible to it
|
|
||||||
-- if not create it (basically if not radiodata make radiodata)
|
|
||||||
radioData[radioChannel] = radioData[radioChannel] or {}
|
|
||||||
local plyName = radioNameGetter(source)
|
|
||||||
for player, _ in pairs(radioData[radioChannel]) do
|
|
||||||
TriggerClientEvent('pma-voice:addPlayerToRadio', player, source, plyName)
|
|
||||||
end
|
|
||||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
|
||||||
voiceData[source].radio = radioChannel
|
|
||||||
radioData[radioChannel][source] = false
|
|
||||||
TriggerClientEvent('pma-voice:syncRadioData', source, radioData[radioChannel], GetConvarInt("voice_syncPlayerNames", 0) == 1 and plyName)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- removes a player from the specified channel
|
|
||||||
---@param source number the player to remove
|
|
||||||
---@param radioChannel number the current channel to remove them from
|
|
||||||
function removePlayerFromRadio(source, radioChannel)
|
|
||||||
logger.verbose('[radio] Removed %s from radio %s', source, radioChannel)
|
|
||||||
radioData[radioChannel] = radioData[radioChannel] or {}
|
|
||||||
for player, _ in pairs(radioData[radioChannel]) do
|
|
||||||
TriggerClientEvent('pma-voice:removePlayerFromRadio', player, source)
|
|
||||||
end
|
|
||||||
radioData[radioChannel][source] = nil
|
|
||||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
|
||||||
voiceData[source].radio = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- TODO: Implement this in a way that allows players to be on multiple channels
|
|
||||||
--- sets the players current radio channel
|
|
||||||
---@param source number the player to set the channel of
|
|
||||||
---@param _radioChannel number the radio channel to set them to (or 0 to remove them from radios)
|
|
||||||
function setPlayerRadio(source, _radioChannel)
|
|
||||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
|
||||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
|
||||||
local isResource = GetInvokingResource()
|
|
||||||
local plyVoice = voiceData[source]
|
|
||||||
local radioChannel = tonumber(_radioChannel)
|
|
||||||
if not radioChannel then
|
|
||||||
-- only full error if its sent from another server-side resource
|
|
||||||
if isResource then
|
|
||||||
error(("'radioChannel' expected 'number', got: %s"):format(type(_radioChannel)))
|
|
||||||
else
|
|
||||||
return logger.warn("%s sent a invalid radio, 'radioChannel' expected 'number', got: %s", source,type(_radioChannel))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if isResource then
|
|
||||||
-- got set in a export, need to update the client to tell them that their radio
|
|
||||||
-- changed
|
|
||||||
TriggerClientEvent('pma-voice:clSetPlayerRadio', source, radioChannel)
|
|
||||||
end
|
|
||||||
Player(source).state.radioChannel = radioChannel
|
|
||||||
if radioChannel ~= 0 and plyVoice.radio == 0 then
|
|
||||||
addPlayerToRadio(source, radioChannel)
|
|
||||||
elseif radioChannel == 0 then
|
|
||||||
removePlayerFromRadio(source, plyVoice.radio)
|
|
||||||
elseif plyVoice.radio > 0 then
|
|
||||||
removePlayerFromRadio(source, plyVoice.radio)
|
|
||||||
addPlayerToRadio(source, radioChannel)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
exports('setPlayerRadio', setPlayerRadio)
|
|
||||||
|
|
||||||
RegisterNetEvent('pma-voice:setPlayerRadio', function(radioChannel)
|
|
||||||
setPlayerRadio(source, radioChannel)
|
|
||||||
end)
|
|
||||||
|
|
||||||
--- syncs the player talking across all radio members
|
|
||||||
---@param talking boolean sets if the palyer is talking.
|
|
||||||
function setTalkingOnRadio(talking)
|
|
||||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
|
||||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
|
||||||
local plyVoice = voiceData[source]
|
|
||||||
local radioTbl = radioData[plyVoice.radio]
|
|
||||||
if radioTbl then
|
|
||||||
radioTbl[source] = talking
|
|
||||||
logger.verbose('[radio] Set %s to talking: %s on radio %s',source, talking, plyVoice.radio)
|
|
||||||
for player, _ in pairs(radioTbl) do
|
|
||||||
if player ~= source then
|
|
||||||
TriggerClientEvent('pma-voice:setTalkingOnRadio', player, source, talking)
|
|
||||||
logger.verbose('[radio] Sync %s to let them know %s is %s',player, source, talking and 'talking' or 'not talking')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
RegisterNetEvent('pma-voice:setTalkingOnRadio', setTalkingOnRadio)
|
|
||||||
|
|
||||||
AddEventHandler("onResourceStop", function(resource)
|
|
||||||
for channel, cfxFunctionRef in pairs(radioChecks) do
|
|
||||||
local functionRef = cfxFunctionRef.__cfx_functionReference
|
|
||||||
local functionResource = string.match(functionRef, resource)
|
|
||||||
if functionResource then
|
|
||||||
radioChecks[channel] = nil
|
|
||||||
logger.warn('Channel %s had its radio check removed because the resource that gave the checks stopped', channel)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if type(radioNameGetter) == "table" then
|
|
||||||
local radioRef = radioNameGetter.__cfx_functionReference
|
|
||||||
if radioRef then
|
|
||||||
local isResource = string.match(functionRef, resource)
|
|
||||||
if isResource then
|
|
||||||
radioNameGetter = radioNameGetter_orig
|
|
||||||
logger.warn('Radio name getter is resetting to default because the resource that gave the cb got turned off')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end)
|
|
|
@ -1,26 +0,0 @@
|
||||||
let mutedPlayers = {}
|
|
||||||
// this is implemented in JS due to Lua's lack of a ClearTimeout
|
|
||||||
// muteply instead of mute because mute conflicts with rp-radio
|
|
||||||
RegisterCommand('muteply', (source, args) => {
|
|
||||||
const mutePly = parseInt(args[0])
|
|
||||||
const duration = parseInt(args[1]) || 900
|
|
||||||
if (mutePly && exports['pma-voice'].isValidPlayer(mutePly)) {
|
|
||||||
const isMuted = !MumbleIsPlayerMuted(mutePly);
|
|
||||||
Player(mutePly).state.muted = isMuted;
|
|
||||||
MumbleSetPlayerMuted(mutePly, isMuted);
|
|
||||||
emit('pma-voice:playerMuted', mutePly, source, isMuted, duration);
|
|
||||||
// since this is a toggle, if theres a mutedPlayers entry it can be assumed
|
|
||||||
// that they're currently muted, so we'll clear the timeout and unmute
|
|
||||||
if (mutedPlayers[mutePly]) {
|
|
||||||
clearTimeout(mutedPlayers[mutePly]);
|
|
||||||
MumbleSetPlayerMuted(mutePly, isMuted)
|
|
||||||
Player(mutePly).state.muted = isMuted;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutedPlayers[mutePly] = setTimeout(() => {
|
|
||||||
MumbleSetPlayerMuted(mutePly, !isMuted)
|
|
||||||
Player(mutePly).state.muted = !isMuted;
|
|
||||||
delete mutedPlayers[mutePly]
|
|
||||||
}, duration * 1000)
|
|
||||||
}
|
|
||||||
}, true)
|
|
|
@ -1,93 +0,0 @@
|
||||||
Cfg = {}
|
|
||||||
|
|
||||||
voiceTarget = 1
|
|
||||||
|
|
||||||
gameVersion = GetGameName()
|
|
||||||
|
|
||||||
-- these are just here to satisfy linting
|
|
||||||
if not IsDuplicityVersion() then
|
|
||||||
LocalPlayer = LocalPlayer
|
|
||||||
playerServerId = GetPlayerServerId(PlayerId())
|
|
||||||
end
|
|
||||||
Player = Player
|
|
||||||
Entity = Entity
|
|
||||||
|
|
||||||
if GetConvar('voice_useNativeAudio', 'false') == 'true' then
|
|
||||||
-- native audio distance seems to be larger then regular gta units
|
|
||||||
Cfg.voiceModes = {
|
|
||||||
{1.5, "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
|
|
||||||
|
|
||||||
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
|
|
|
@ -1 +0,0 @@
|
||||||
.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}
|
|
|
@ -1 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,2 +0,0 @@
|
||||||
(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.
|
@ -1,3 +0,0 @@
|
||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
23
resources/[voice]/pma-voice/voice-ui/.gitignore
vendored
23
resources/[voice]/pma-voice/voice-ui/.gitignore
vendored
|
@ -1,23 +0,0 @@
|
||||||
.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?
|
|
|
@ -1,24 +0,0 @@
|
||||||
# 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/).
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
6692
resources/[voice]/pma-voice/voice-ui/pnpm-lock.yaml
generated
6692
resources/[voice]/pma-voice/voice-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,17 +0,0 @@
|
||||||
<!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.
|
@ -1,112 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { createApp } from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
|
|
@ -1,7 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
publicPath: './',
|
|
||||||
productionSourceMap: true,
|
|
||||||
filenameHashing: false,
|
|
||||||
outputDir: "../ui",
|
|
||||||
|
|
||||||
}
|
|
19
resources/[voice]/yaca/.deepsource.toml
Normal file
19
resources/[voice]/yaca/.deepsource.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
exclude_patterns = [
|
||||||
|
"resources/**",
|
||||||
|
"dist/**"
|
||||||
|
]
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "secrets"
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "javascript"
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
environment = [
|
||||||
|
"jquery",
|
||||||
|
"nodejs",
|
||||||
|
"browser"
|
||||||
|
]
|
723
resources/[voice]/yaca/README.md
Normal file
723
resources/[voice]/yaca/README.md
Normal file
|
@ -0,0 +1,723 @@
|
||||||
|
# [yaca.systems](https://yaca.systems/) for [FiveM](https://fivem.net/) & [RedM](https://redm.net/)
|
||||||
|
|
||||||
|
This is a example implementation for [FiveM](https://fivem.net/) & [RedM](https://redm.net/).
|
||||||
|
Feel free to report bugs via issues or contribute via pull requests.
|
||||||
|
|
||||||
|
Join our [Discord](http://discord.yaca.systems/) to get help or make suggestions and start
|
||||||
|
using [yaca.systems](https://yaca.systems/) today!
|
||||||
|
|
||||||
|
# Setup Steps
|
||||||
|
|
||||||
|
Before you start, make sure you have OneSync enabled and your server artifacts are up to date.
|
||||||
|
|
||||||
|
1. Download and install the latest [release](https://github.com/yaca-systems/fivem-yaca-typescript/releases) of this
|
||||||
|
resource.
|
||||||
|
2. Add `start yaca-voice` into your `server.cfg`.
|
||||||
|
3. Open `config/server.json5` and adjust the variables to your needs.
|
||||||
|
4. Open `config/shared.json5` and adjust the variables to your needs.
|
||||||
|
|
||||||
|
# Exports
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="font-size: x-large">Client</summary>
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
#### `getVoiceRange(): int`
|
||||||
|
|
||||||
|
Get the current voice range of the player as `int`.
|
||||||
|
|
||||||
|
#### `getVoiceRanges(): int[]`
|
||||||
|
|
||||||
|
Get all voice ranges as `int[]`.
|
||||||
|
|
||||||
|
#### `changeVoiceRange(increase: boolean): void`
|
||||||
|
|
||||||
|
Change the voice range of the player to the next range.
|
||||||
|
|
||||||
|
#### `setVoiceRange(range: number): void`
|
||||||
|
|
||||||
|
Set the voice range of the player.
|
||||||
|
|
||||||
|
#### `setVoiceRangeChangeAllowedState(state: boolean): void`
|
||||||
|
|
||||||
|
Enable or disable the possibility to change the voice range.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|------------------------------------------------|
|
||||||
|
| state | `boolean` | `true` to allow the voice range change, `false` to disable |
|
||||||
|
|
||||||
|
#### `getVoiceRangeChangeAllowedState(): boolean`
|
||||||
|
|
||||||
|
Get the voice range change allowed state of the player as `boolean`.
|
||||||
|
|
||||||
|
#### `setMaxVoiceRange(range: number): void`
|
||||||
|
|
||||||
|
Set the maximum allowed voice range of the player in meters to limit the voice range temporarily.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|------------------------------------------------|
|
||||||
|
| range | `number` | `-1` to disable the limit, or a number in meters to set the limit |
|
||||||
|
|
||||||
|
#### `getMaxVoiceRange(): number`
|
||||||
|
|
||||||
|
Get the maximum allowed voice range of the player in meters.
|
||||||
|
|
||||||
|
#### `getMicrophoneMuteState(): boolean`
|
||||||
|
|
||||||
|
Get the microphone mute state of the player as `boolean`.
|
||||||
|
|
||||||
|
#### `getMicrophoneDisabledState(): boolean`
|
||||||
|
|
||||||
|
Get the microphone disabled state of the player as `boolean`.
|
||||||
|
|
||||||
|
#### `getSoundMuteState(): boolean`
|
||||||
|
|
||||||
|
Get the sound mute state of the player as `boolean`.
|
||||||
|
|
||||||
|
#### `getSoundDisabledState(): boolean`
|
||||||
|
|
||||||
|
Get the sound disabled state of the player as `boolean`.
|
||||||
|
|
||||||
|
#### `getPluginState(): string`
|
||||||
|
|
||||||
|
Get the current plugin state as `string`.
|
||||||
|
|
||||||
|
The state can be one of the following:
|
||||||
|
|
||||||
|
- `"NOT_CONNECTED"`: The plugin is not connected
|
||||||
|
- `"CONNECTED`: The plugin is connected
|
||||||
|
- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard
|
||||||
|
- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server
|
||||||
|
- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel
|
||||||
|
- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel
|
||||||
|
|
||||||
|
#### `getGlobalErrorLevel(): number`
|
||||||
|
|
||||||
|
Get the global error level as `number`.
|
||||||
|
|
||||||
|
#### `setSpectatingPlayer(playerId: number | false)`
|
||||||
|
|
||||||
|
Set the player to spectate.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-------------------|-------------------|
|
||||||
|
| playerId | `number \| false` | the player to set |
|
||||||
|
|
||||||
|
#### `getSpectatingPlayer(): number`
|
||||||
|
|
||||||
|
Get the player the user is spectating as `number`.
|
||||||
|
|
||||||
|
#### `setVoiceRangeMarkerColor(red: number, green: number, blue: number, alpha: number)`
|
||||||
|
|
||||||
|
Set the voice range marker color.
|
||||||
|
|
||||||
|
#### `getVoiceRangeMarkerColor(): [number, number, number, number]`
|
||||||
|
|
||||||
|
Get the voice range marker color as `[red, green, blue, alpha]`.
|
||||||
|
|
||||||
|
#### `resetVoiceRangeMarkerColor()`
|
||||||
|
|
||||||
|
Reset the voice range marker color to the default color defined in the config.
|
||||||
|
|
||||||
|
### Radio
|
||||||
|
|
||||||
|
#### `enableRadio(state: boolean)`
|
||||||
|
|
||||||
|
Enables or disables the radio system.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|------------------------------------------------|
|
||||||
|
| state | `boolean` | `true` to enable the radio, `false` to disable |
|
||||||
|
|
||||||
|
#### `isRadioEnabled(): boolean`
|
||||||
|
|
||||||
|
Returns whether the radio system is enabled as `boolean`.
|
||||||
|
|
||||||
|
#### `changeRadioFrequency(frequency: string)`
|
||||||
|
|
||||||
|
Changes the radio frequency of the active channel.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|--------------------------------------------|
|
||||||
|
| frequency | `string` | The frequency to set the active channel to |
|
||||||
|
|
||||||
|
#### `changeRadioFrequencyRaw(channel: number, frequency: string)`
|
||||||
|
|
||||||
|
Changes the radio frequency.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|---------------------------------------------------------------------------------------|
|
||||||
|
| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. |
|
||||||
|
| frequency | `string` | the frequency to set the channel to |
|
||||||
|
|
||||||
|
#### `getRadioFrequency(channel: number): string`
|
||||||
|
|
||||||
|
Returns the frequency of a radio channel as `string`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|---------------------------------------------------------------------------------------|
|
||||||
|
| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. |
|
||||||
|
|
||||||
|
#### `muteRadioChannel(state?: boolean)`
|
||||||
|
|
||||||
|
Mutes the current active radio channel.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|-----------------------------------------------------------------------------|
|
||||||
|
| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined |
|
||||||
|
|
||||||
|
#### `muteRadioChannelRaw(channel: number, state?: boolean)`
|
||||||
|
|
||||||
|
Mutes a radio channel.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|----------------------------------------------------------------------------------------|
|
||||||
|
| channel? | `number` | the channel to mute. Defaults to the current active channel when no channel is passed. |
|
||||||
|
| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined |
|
||||||
|
|
||||||
|
#### `isRadioChannelMuted(channel: number): boolean`
|
||||||
|
|
||||||
|
Returns whether a radio channel is muted as `boolean`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|--------------------|
|
||||||
|
| channel | `number` | the channel number |
|
||||||
|
|
||||||
|
#### `setActiveRadioChannel(channel: number): bool`
|
||||||
|
|
||||||
|
Changes the active radio channel. Returns whether the operation was successful as `bool`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-----------------------|
|
||||||
|
| channel | `number` | the new radio channel |
|
||||||
|
|
||||||
|
#### `getActiveRadioChannel(): number`
|
||||||
|
|
||||||
|
Returns the active radio channel as `number`.
|
||||||
|
|
||||||
|
#### `setSecondaryRadioChannel(channel: number): bool`
|
||||||
|
|
||||||
|
Changes the secondary radio channel. Returns whether the operation was successful as `bool`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-----------------------|
|
||||||
|
| channel | `number` | the new radio channel |
|
||||||
|
|
||||||
|
#### `getSecondaryRadioChannel(): number`
|
||||||
|
|
||||||
|
Returns the secondary radio channel as `number`.
|
||||||
|
|
||||||
|
#### `changeRadioChannelVolume(higher: boolean): bool`
|
||||||
|
|
||||||
|
Changes the volume of the active radio channel. Returns whether the operation was successful as `bool`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------------------|
|
||||||
|
| higher | `boolean` | whether to increase the volume |
|
||||||
|
|
||||||
|
#### `changeRadioChannelVolumeRaw(channel: number, volume: number): bool`
|
||||||
|
|
||||||
|
Changes the volume of a radio channel. Returns whether the operation was successful as `bool`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|--------------------|
|
||||||
|
| channel | `number` | the channel number |
|
||||||
|
| volume | `number` | the volume to set |
|
||||||
|
|
||||||
|
#### `getRadioChannelVolume(channel: number): number`
|
||||||
|
|
||||||
|
Returns the volume of a radio channel as `number`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|--------------------|
|
||||||
|
| channel | `number` | the channel number |
|
||||||
|
|
||||||
|
#### `changeRadioChannelStereo(): bool`
|
||||||
|
|
||||||
|
Changes the stereo mode of the active radio channel. Returns whether the operation was successful as `bool`.
|
||||||
|
|
||||||
|
#### `changeRadioChannelStereoRaw(channel: number, stereo: string): bool`
|
||||||
|
|
||||||
|
Changes the stereo mode of a radio channel. Returns whether the operation was successful as `bool`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|---------------------------------------------------------------|
|
||||||
|
| channel | `number` | the channel number |
|
||||||
|
| stereo | `string` | the stereo mode (`"MONO_LEFT"`, `"MONO_RIGHT"` or `"STEREO"`) |
|
||||||
|
|
||||||
|
#### `getRadioChannelStereo(channel: number): string`
|
||||||
|
|
||||||
|
Returns the stereo mode of a radio channel as `string`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|--------------------|
|
||||||
|
| channel | `number` | the channel number |
|
||||||
|
|
||||||
|
#### `radioTalkingStart(state: boolean, channel: number)`
|
||||||
|
|
||||||
|
Starts or stops talking on the radio.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|------------------------------------------|
|
||||||
|
| state | `boolean` | `true` to start talking, `false` to stop |
|
||||||
|
| channel | `number` | the channel to talk on |
|
||||||
|
|
||||||
|
#### `setRadioMode(mode: string)`
|
||||||
|
|
||||||
|
Sets the radio mode.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------------------------------------------------------------------------|
|
||||||
|
| mode | `string` | the radio mode to set. Can be either `None`, `Direct` or `Tower` |
|
||||||
|
|
||||||
|
#### `getRadioMode(): string`
|
||||||
|
|
||||||
|
Returns the radio mode as `string`.
|
||||||
|
|
||||||
|
### Phone
|
||||||
|
|
||||||
|
#### `isInCall(): boolean`
|
||||||
|
|
||||||
|
Returns whether the player is in a phone call as a `boolean`.
|
||||||
|
|
||||||
|
### Megaphone
|
||||||
|
|
||||||
|
#### `getCanUseMegaphone(): boolean`
|
||||||
|
|
||||||
|
Returns whether the player can use the megaphone as a `boolean`.
|
||||||
|
|
||||||
|
#### `setCanUseMegaphone(state: boolean)`
|
||||||
|
|
||||||
|
Sets whether the player can use the megaphone.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|---------------------------------------------------------|
|
||||||
|
| state | `boolean` | `true` to allow using of megaphone, `false` to disallow |
|
||||||
|
|
||||||
|
### `useMegaphone(state: boolean)`
|
||||||
|
|
||||||
|
Starts or stops using the megaphone.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|----------------------------------------|
|
||||||
|
| state | `boolean` | `true` to start using, `false` to stop |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="font-size: x-large">Server</summary>
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
#### `connectToVoice(source: number)`
|
||||||
|
Connects a player to the voice system.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
|
||||||
|
#### `getPlayerAliveStatus(source: number): bool`
|
||||||
|
|
||||||
|
Get the alive status of a player as `bool`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
|
||||||
|
#### `setPlayerAliveStatus(source: number, state: bool)`
|
||||||
|
|
||||||
|
Set the alive status of a player.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|---------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| state | `boolean` | the new alive state |
|
||||||
|
|
||||||
|
#### `getPlayerVoiceRange(source: number): number`
|
||||||
|
|
||||||
|
Get the voice range of a player as `number`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
|
||||||
|
#### `setPlayerVoiceRange(source: number, range: number)`
|
||||||
|
|
||||||
|
Set the voice range of a player.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|---------------------------------------------------------------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| range | `number` | The new voice range. Defaults to the default voice range if not provided. |
|
||||||
|
|
||||||
|
### Radio
|
||||||
|
|
||||||
|
#### `getPlayersInRadioFrequency(frequency: string): int[]`
|
||||||
|
|
||||||
|
Returns all players in a radio frequency as `int[]`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|----------------------|
|
||||||
|
| frequency | `string` | the frequency to get |
|
||||||
|
|
||||||
|
#### `setPlayerRadioChannel(source: number, channel: number, frequency: string)`
|
||||||
|
|
||||||
|
Sets the radio channel of a player.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|----------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| channel | `number` | the channel to set |
|
||||||
|
| frequency | `string` | the frequency to set |
|
||||||
|
|
||||||
|
#### `getPlayerHasLongRange(source: number): bool`
|
||||||
|
|
||||||
|
Returns whether a player has long range enabled as `bool`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
|
||||||
|
#### `setPlayerHasLongRange(source: number, state: bool)`
|
||||||
|
|
||||||
|
Sets the long range state of a player.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|----------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| state | `boolean` | the long range state |
|
||||||
|
|
||||||
|
### Phone
|
||||||
|
|
||||||
|
#### `callPlayer(source: number, target: number, state: bool)`
|
||||||
|
|
||||||
|
Creates a phone call between two players.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| target | `number` | the target player source |
|
||||||
|
| state | `boolean` | the state of the call |
|
||||||
|
|
||||||
|
#### `callPlayerOldEffect(source: number, target: number, state: bool)`
|
||||||
|
|
||||||
|
Creates a phone call between two players with the old effect.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| target | `number` | the target player source |
|
||||||
|
| state | `boolean` | the state of the call |
|
||||||
|
|
||||||
|
#### `muteOnPhone(source: number, state: bool)`
|
||||||
|
|
||||||
|
Mutes the player when using the phone.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|-------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| state | `boolean` | the mute state |
|
||||||
|
|
||||||
|
#### `enablePhoneSpeaker(source: number, state: bool)`
|
||||||
|
|
||||||
|
Enable or disable the phone speaker for a player.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|-------------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
| state | `boolean` | the phone speaker state |
|
||||||
|
|
||||||
|
#### `isPlayerInCall(source: number): [bool, number[]]`
|
||||||
|
|
||||||
|
Returns whether a player is in a phone call as `[bool, number[]]`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-------------------|
|
||||||
|
| source | `number` | the player source |
|
||||||
|
|
||||||
|
#### `setGlobalErrorLevel(level: number)`
|
||||||
|
|
||||||
|
Sets the global error level.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-----------------|
|
||||||
|
| level | `number` | the error level |
|
||||||
|
|
||||||
|
#### `getGlobalErrorLevel(): number`
|
||||||
|
|
||||||
|
Returns the global error level as `number`.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# Events
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="font-size: x-large">Client</summary>
|
||||||
|
|
||||||
|
### yaca:external:pluginInitialized
|
||||||
|
|
||||||
|
The event is triggered when the plugin is initialized.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-------|----------------------------------------------|
|
||||||
|
| clientId | `int` | the client id of the local user in teamspeak |
|
||||||
|
|
||||||
|
### yaca:external:pluginStateChanged
|
||||||
|
|
||||||
|
The event is triggered when the plugin state changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|----------------------------------------------|
|
||||||
|
| state | `string` | the current plugin state, as explained below |
|
||||||
|
|
||||||
|
The state can be one of the following:
|
||||||
|
|
||||||
|
- `"NOT_CONNECTED"`: The plugin is not connected
|
||||||
|
- `"CONNECTED`: The plugin is connected
|
||||||
|
- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard
|
||||||
|
- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server
|
||||||
|
- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel
|
||||||
|
- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel
|
||||||
|
|
||||||
|
### yaca:external:voiceRangeUpdate
|
||||||
|
|
||||||
|
This event is triggered when the voice range of a player is updated.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|------------|-------|---------------------------|
|
||||||
|
| range | `int` | the newly set voice range |
|
||||||
|
| rangeIndex | `int` | the index of the range |
|
||||||
|
|
||||||
|
### yaca:external:muteStateChanged
|
||||||
|
|
||||||
|
DEPRECATED: Use `yaca:external:microphoneMuteStateChanged` instead.
|
||||||
|
The event is triggered when the mute state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------|
|
||||||
|
| state | `boolean` | the new mute state |
|
||||||
|
|
||||||
|
### yaca:external:microphoneMuteStateChanged
|
||||||
|
|
||||||
|
The event is triggered when the microphone mute state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------|
|
||||||
|
| state | `boolean` | the new mute state |
|
||||||
|
|
||||||
|
### yaca:external:microphoneDisabledStateChanged
|
||||||
|
|
||||||
|
The event is triggered when the microphone disabled state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------|
|
||||||
|
| state | `boolean` | the new mute state |
|
||||||
|
|
||||||
|
### yaca:external:soundMuteStateChanged
|
||||||
|
|
||||||
|
The event is triggered when the sound mute state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------|
|
||||||
|
| state | `boolean` | the new mute state |
|
||||||
|
|
||||||
|
### yaca:external:soundDisabledStateChanged
|
||||||
|
|
||||||
|
The event is triggered when the sound disabled state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------|
|
||||||
|
| state | `boolean` | the new mute state |
|
||||||
|
|
||||||
|
### yaca:external:isTalking
|
||||||
|
|
||||||
|
The event is triggered when a player starts or stops talking.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|-----------------------|
|
||||||
|
| state | `boolean` | the new talking state |
|
||||||
|
|
||||||
|
### yaca:external:megaphoneState
|
||||||
|
|
||||||
|
The event is triggered when the megaphone state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|-------------------------|
|
||||||
|
| state | `boolean` | the new megaphone state |
|
||||||
|
|
||||||
|
### yaca:external:setRadioMuteState
|
||||||
|
|
||||||
|
The event is triggered when the radio mute state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|---------------------------------------------|
|
||||||
|
| channel | `number` | the channel where the mute state is changed |
|
||||||
|
| state | `boolean` | the new mute state |
|
||||||
|
|
||||||
|
### yaca:external:isRadioEnabled
|
||||||
|
|
||||||
|
The event is triggered when the radio state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|----------------------------------------------------------------------|
|
||||||
|
| state | `boolean` | `true` when the radio is enabled, `false` when the radio is disabled |
|
||||||
|
|
||||||
|
### yaca:external:changedActiveRadioChannel
|
||||||
|
|
||||||
|
The event is triggered when the active radio channel of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|------------------------------|
|
||||||
|
| channel | `number` | the new active radio channel |
|
||||||
|
|
||||||
|
### yaca:external:changedSecondaryRadioChannel
|
||||||
|
|
||||||
|
The event is triggered when the secondary radio channel of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|---------------------------------------------------|
|
||||||
|
| channel | `number` | the new active radio channel, or `-1` if disabled |
|
||||||
|
|
||||||
|
### yaca:external:setRadioVolume
|
||||||
|
|
||||||
|
The event is triggered when the radio volume of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-----------------------|
|
||||||
|
| channel | `number` | the channel to change |
|
||||||
|
| volume | `number` | the new volume to set |
|
||||||
|
|
||||||
|
### yaca:external:setRadioChannelStereo
|
||||||
|
|
||||||
|
The event is triggered when the stereo mode of a radio channel changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-----------------------------------------------------------------------------------------------|
|
||||||
|
| channel | `number` | the channel to change |
|
||||||
|
| stereo | `string` | `"MONO_LEFT"` for the left ear, `"MONO_RIGHT"` for the right ear and `"STEREO"` for both ears |
|
||||||
|
|
||||||
|
### yaca:external:setRadioFrequency
|
||||||
|
|
||||||
|
The event is triggered when the radio frequency of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|----------------------|
|
||||||
|
| channel | `number` | the channel to set |
|
||||||
|
| frequency | `string` | the frequency to set |
|
||||||
|
|
||||||
|
### yaca:external:isRadioTalking
|
||||||
|
|
||||||
|
The event is triggered when a player starts or stops talking on the radio.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|--------------------------------------------|
|
||||||
|
| state | `boolean` | the new talking state |
|
||||||
|
| channel | `number` | the channel where the player is talking at |
|
||||||
|
|
||||||
|
### yaca:external:isRadioReceiving
|
||||||
|
|
||||||
|
The event is triggered when a player starts or stops receiving on the radio.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|------------------------------------------------|
|
||||||
|
| state | `boolean` | the new receiver state |
|
||||||
|
| channel | `number` | the channel from which the player is receiving |
|
||||||
|
|
||||||
|
### yaca:external:notification
|
||||||
|
|
||||||
|
The event is triggered when a notification should be shown.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|--------------------------------------------------------------|
|
||||||
|
| message | `string` | the message to show |
|
||||||
|
| type | `string` | the type of the message (`"inform"`, `"error"`, `"success"`) |
|
||||||
|
|
||||||
|
Example for custom notification:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
AddEventHandler('yaca:external:notification', function (message, type)
|
||||||
|
-- Call your Notifications System here.
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### yaca:external:channelChanged
|
||||||
|
|
||||||
|
The event is triggered when the player changes the channel to the ingame or excluded channel.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-------------|----------|------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| channelType | `string` | `INGAME_CHANNEL` when moving into the ingame channel and `EXCLUDED_CHANNEL` when moving into a excluded channel. |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary style="font-size: x-large">Server</summary>
|
||||||
|
|
||||||
|
### yaca:external:changeMegaphoneState
|
||||||
|
|
||||||
|
The event is triggered when the megaphone state of a player changes.
|
||||||
|
|
||||||
|
| Parametr | Type | Description |
|
||||||
|
|----------|-----------|-------------------------|
|
||||||
|
| source | `int` | the player source |
|
||||||
|
| state | `boolean` | the new megaphone state |
|
||||||
|
|
||||||
|
### yaca:external:phoneCall
|
||||||
|
|
||||||
|
The event is triggered when a phone call is started or ended.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------------------|---------------------------------------------------------------------------------|
|
||||||
|
| source | `int` | the player source |
|
||||||
|
| target | `int` | the target player source |
|
||||||
|
| state | `boolean` | the new phone call state |
|
||||||
|
| filter | `YacaFilterEnum` | the used filter for the phone call, can be either `PHONE` or `PHONE_HISTORICAL` |
|
||||||
|
|
||||||
|
### yaca:external:phoneSpeaker
|
||||||
|
|
||||||
|
The event is triggered when the phone speaker state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|-----------------------------|
|
||||||
|
| source | `int` | the player source |
|
||||||
|
| state | `boolean` | the new phone speaker state |
|
||||||
|
|
||||||
|
### yaca:external:changedRadioFrequency
|
||||||
|
|
||||||
|
The event is triggered when the radio frequency of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|----------|-----------------------------------------|
|
||||||
|
| source | `int` | the player source |
|
||||||
|
| channel | `int` | the channel where the frequency was set |
|
||||||
|
| frequency | `string` | the frequency to set |
|
||||||
|
|
||||||
|
### yaca:external:changedRadioMuteState
|
||||||
|
|
||||||
|
The event is triggered when the radio mute state of a player changes.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|-----------|----------------------------------------------|
|
||||||
|
| source | `int` | the player source |
|
||||||
|
| channel | `int` | the channel where the mute state was changed |
|
||||||
|
| state | `boolean` | the new mute state |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# Developers
|
||||||
|
|
||||||
|
If you want to contribute to this project, feel free to do so. We are happy about every contribution. If you have any
|
||||||
|
questions, feel free to ask in our [Discord](http://discord.yaca.systems/).
|
||||||
|
|
||||||
|
## Building the resource
|
||||||
|
|
||||||
|
To build the resource, you need to have [Node.js](https://nodejs.org/) installed. After that, you can run the following
|
||||||
|
commands to build the resource:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The built resource will be located in the `resource` folder, which you can then use in your FiveM server.
|
25
resources/[voice]/yaca/apps/yaca-client/build.js
Normal file
25
resources/[voice]/yaca/apps/yaca-client/build.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { build } from 'esbuild'
|
||||||
|
|
||||||
|
const production = process.argv.includes('--mode=production')
|
||||||
|
|
||||||
|
build({
|
||||||
|
entryPoints: ['src/index.ts'],
|
||||||
|
outfile: './dist/client.js',
|
||||||
|
bundle: true,
|
||||||
|
loader: {
|
||||||
|
'.ts': 'ts',
|
||||||
|
'.js': 'js',
|
||||||
|
},
|
||||||
|
write: true,
|
||||||
|
platform: 'browser',
|
||||||
|
target: 'es2021',
|
||||||
|
format: 'iife',
|
||||||
|
minify: production,
|
||||||
|
sourcemap: production ? false : 'inline',
|
||||||
|
dropLabels: production ? ['DEV'] : undefined,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Client built successfully')
|
||||||
|
})
|
||||||
|
// skipcq: JS-0263
|
||||||
|
.catch(() => process.exit(1))
|
22
resources/[voice]/yaca/apps/yaca-client/package.json
Normal file
22
resources/[voice]/yaca/apps/yaca-client/package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "yaca-client",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node build.js --mode=production",
|
||||||
|
"dev": "node build.js",
|
||||||
|
"typecheck": "tsc --project tsconfig.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter2": "^6.4.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@citizenfx/client": "latest",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
|
"@types/node": "^20.16.10",
|
||||||
|
"@yaca-voice/common": "workspace:*",
|
||||||
|
"@yaca-voice/types": "workspace:*",
|
||||||
|
"@yaca-voice/typescript-config": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
1088
resources/[voice]/yaca/apps/yaca-client/pnpm-lock.yaml
generated
Normal file
1088
resources/[voice]/yaca/apps/yaca-client/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
181
resources/[voice]/yaca/apps/yaca-client/src/bridge/saltychat.ts
Normal file
181
resources/[voice]/yaca/apps/yaca-client/src/bridge/saltychat.ts
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import { saltyChatExport, sleep } from '@yaca-voice/common'
|
||||||
|
import { YacaPluginStates } from '@yaca-voice/types'
|
||||||
|
import { cache } from '../utils'
|
||||||
|
import type { YaCAClientModule } from '../yaca'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SaltyChat bridge for the client.
|
||||||
|
*/
|
||||||
|
export class YaCAClientSaltyChatBridge {
|
||||||
|
private clientModule: YaCAClientModule
|
||||||
|
|
||||||
|
private currentPluginState = -1
|
||||||
|
|
||||||
|
private isPrimarySending = false
|
||||||
|
private isSecondarySending = false
|
||||||
|
|
||||||
|
private isPrimaryReceiving = false
|
||||||
|
private isSecondaryReceiving = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the SaltyChat bridge.
|
||||||
|
*
|
||||||
|
* @param {YaCAClientModule} clientModule - The client module.
|
||||||
|
*/
|
||||||
|
constructor(clientModule: YaCAClientModule) {
|
||||||
|
this.clientModule = clientModule
|
||||||
|
|
||||||
|
this.registerSaltyChatExports()
|
||||||
|
this.enableRadio().then()
|
||||||
|
|
||||||
|
console.log('[YaCA] SaltyChat bridge loaded')
|
||||||
|
|
||||||
|
on('onResourceStop', (resourceName: string) => {
|
||||||
|
if (cache.resource !== resourceName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('onClientResourceStop', 'saltychat')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the radio on bridge load.
|
||||||
|
*/
|
||||||
|
async enableRadio() {
|
||||||
|
while (!this.clientModule.isPluginInitialized(true)) {
|
||||||
|
await sleep(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientModule.radioModule.enableRadio(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register SaltyChat exports.
|
||||||
|
*/
|
||||||
|
registerSaltyChatExports() {
|
||||||
|
saltyChatExport('GetVoiceRange', () => this.clientModule.getVoiceRange())
|
||||||
|
|
||||||
|
saltyChatExport('GetRadioChannel', (primary: boolean) => {
|
||||||
|
const channel = primary ? 1 : 2
|
||||||
|
|
||||||
|
const currentFrequency = this.clientModule.radioModule.getRadioFrequency(channel)
|
||||||
|
|
||||||
|
if (currentFrequency === '0') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentFrequency
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('GetRadioVolume', () => {
|
||||||
|
return this.clientModule.radioModule.getRadioChannelVolume(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('GetRadioSpeaker', () => {
|
||||||
|
console.warn('GetRadioSpeaker is not implemented in YaCA')
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('GetMicClick', () => {
|
||||||
|
console.warn('GetMicClick is not implemented in YaCA')
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetRadioChannel', (radioChannelName: string, primary: boolean) => {
|
||||||
|
const channel = primary ? 1 : 2
|
||||||
|
const newRadioChannelName = radioChannelName === '' ? '0' : radioChannelName
|
||||||
|
|
||||||
|
this.clientModule.radioModule.changeRadioFrequencyRaw(newRadioChannelName, channel)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetRadioVolume', (volume: number) => {
|
||||||
|
this.clientModule.radioModule.changeRadioChannelVolumeRaw(volume, 1)
|
||||||
|
this.clientModule.radioModule.changeRadioChannelVolumeRaw(volume, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetRadioSpeaker', () => {
|
||||||
|
console.warn('SetRadioSpeaker is not implemented in YaCA')
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetMicClick', () => {
|
||||||
|
console.warn('SetMicClick is not implemented in YaCA')
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('GetPluginState', () => {
|
||||||
|
return this.currentPluginState
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the plugin state change.
|
||||||
|
*
|
||||||
|
* @param response - The last response code.
|
||||||
|
*/
|
||||||
|
handleChangePluginState(response: YacaPluginStates) {
|
||||||
|
let state = 0
|
||||||
|
|
||||||
|
switch (response) {
|
||||||
|
case YacaPluginStates.IN_EXCLUDED_CHANNEL:
|
||||||
|
state = 3
|
||||||
|
break
|
||||||
|
case YacaPluginStates.IN_INGAME_CHANNEL:
|
||||||
|
state = 2
|
||||||
|
break
|
||||||
|
case YacaPluginStates.CONNECTED:
|
||||||
|
state = 1
|
||||||
|
break
|
||||||
|
case YacaPluginStates.WRONG_TS_SERVER:
|
||||||
|
case YacaPluginStates.OUTDATED_VERSION:
|
||||||
|
state = 0
|
||||||
|
break
|
||||||
|
case YacaPluginStates.NOT_CONNECTED:
|
||||||
|
state = -1
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('SaltyChat_PluginStateChanged', state)
|
||||||
|
this.currentPluginState = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the radio talking state.
|
||||||
|
*/
|
||||||
|
sendRadioTalkingState() {
|
||||||
|
emit('SaltyChat_RadioTrafficStateChanged', this.isPrimaryReceiving, this.isPrimarySending, this.isSecondaryReceiving, this.isSecondarySending)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle radio talking state change.
|
||||||
|
*
|
||||||
|
* @param state - The state of the radio talking.
|
||||||
|
* @param channel - The radio channel.
|
||||||
|
*/
|
||||||
|
handleRadioTalkingStateChange(state: boolean, channel: number) {
|
||||||
|
if (channel === 1) {
|
||||||
|
this.isPrimarySending = state
|
||||||
|
} else {
|
||||||
|
this.isSecondarySending = state
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendRadioTalkingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle radio receiving state change.
|
||||||
|
*
|
||||||
|
* @param state - The state of the radio receiving.
|
||||||
|
* @param channel - The radio channel.
|
||||||
|
*/
|
||||||
|
handleRadioReceivingStateChange(state: boolean, channel: number) {
|
||||||
|
if (channel === 1) {
|
||||||
|
this.isPrimaryReceiving = state
|
||||||
|
} else {
|
||||||
|
this.isSecondaryReceiving = state
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendRadioTalkingState()
|
||||||
|
}
|
||||||
|
}
|
8
resources/[voice]/yaca/apps/yaca-client/src/index.ts
Normal file
8
resources/[voice]/yaca/apps/yaca-client/src/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/// <reference types="@citizenfx/client" />
|
||||||
|
|
||||||
|
import { initCache } from './utils'
|
||||||
|
import { YaCAClientModule } from './yaca'
|
||||||
|
|
||||||
|
initCache()
|
||||||
|
|
||||||
|
new YaCAClientModule()
|
79
resources/[voice]/yaca/apps/yaca-client/src/utils/cache.ts
Normal file
79
resources/[voice]/yaca/apps/yaca-client/src/utils/cache.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import type { ClientCache } from '@yaca-voice/types'
|
||||||
|
|
||||||
|
const playerId = PlayerId()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached values for the client.
|
||||||
|
*/
|
||||||
|
const cache: ClientCache = new Proxy(
|
||||||
|
{
|
||||||
|
playerId,
|
||||||
|
serverId: GetPlayerServerId(playerId),
|
||||||
|
ped: PlayerPedId(),
|
||||||
|
vehicle: false,
|
||||||
|
seat: false,
|
||||||
|
resource: GetCurrentResourceName(),
|
||||||
|
game: GetGameName() as 'fivem' | 'redm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
set(target: ClientCache, key: keyof ClientCache, value: never) {
|
||||||
|
if (target[key] === value) return true
|
||||||
|
|
||||||
|
target[key] = value
|
||||||
|
emit(`yaca:cache:${key}`, value)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
get(target: ClientCache, key: keyof ClientCache) {
|
||||||
|
return target[key]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the cache and starts updating it.
|
||||||
|
*/
|
||||||
|
function initCache() {
|
||||||
|
/**
|
||||||
|
* This function will update the cache every 100ms.
|
||||||
|
*/
|
||||||
|
const updateCache = () => {
|
||||||
|
const ped = PlayerPedId()
|
||||||
|
cache.ped = ped
|
||||||
|
|
||||||
|
const vehicle = GetVehiclePedIsIn(ped, false)
|
||||||
|
|
||||||
|
if (vehicle > 0) {
|
||||||
|
if (vehicle !== cache.vehicle) {
|
||||||
|
cache.seat = false
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.vehicle = vehicle
|
||||||
|
|
||||||
|
if (!cache.seat || GetPedInVehicleSeat(vehicle, cache.seat) !== ped) {
|
||||||
|
for (let i = -1; i < GetVehicleMaxNumberOfPassengers(vehicle) - 1; i++) {
|
||||||
|
if (GetPedInVehicleSeat(vehicle, i) === ped) {
|
||||||
|
cache.seat = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache.vehicle = false
|
||||||
|
cache.seat = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateCache, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for cache updates.
|
||||||
|
*
|
||||||
|
* @param key - The cache key to listen for.
|
||||||
|
* @param cb - The callback to execute when the cache updates.
|
||||||
|
*/
|
||||||
|
export const onCache = <T = never>(key: keyof ClientCache, cb: (value: T) => void) => {
|
||||||
|
on(`yaca:cache:${key}`, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initCache, cache }
|
40
resources/[voice]/yaca/apps/yaca-client/src/utils/index.ts
Normal file
40
resources/[voice]/yaca/apps/yaca-client/src/utils/index.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { cache } from './cache'
|
||||||
|
|
||||||
|
export * from './cache'
|
||||||
|
export * from './props'
|
||||||
|
export * from './redm'
|
||||||
|
export * from './streaming'
|
||||||
|
export * from './vectors'
|
||||||
|
export * from './vehicle'
|
||||||
|
export * from './websocket'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds a float to a specified number of decimal places.
|
||||||
|
* Defaults to 2 decimal places if not provided.
|
||||||
|
*
|
||||||
|
* @param {number} num - The number to round.
|
||||||
|
* @param {number} decimalPlaces - The number of decimal places to round to.
|
||||||
|
*
|
||||||
|
* @returns {number} The rounded number.
|
||||||
|
*/
|
||||||
|
export function roundFloat(num: number, decimalPlaces = 17): number {
|
||||||
|
return Number.parseFloat(num.toFixed(decimalPlaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert camera rotation to direction vector.
|
||||||
|
*
|
||||||
|
* @returns {x: number, y: number, z: number} The direction vector.
|
||||||
|
*/
|
||||||
|
export function getCamDirection(): { x: number; y: number; z: number } {
|
||||||
|
const rotVector = GetGameplayCamRot(0)
|
||||||
|
const num = rotVector[2] * 0.0174532924
|
||||||
|
const num2 = rotVector[0] * 0.0174532924
|
||||||
|
const num3 = Math.abs(Math.cos(num2))
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: roundFloat(-Math.sin(num) * num3),
|
||||||
|
y: roundFloat(Math.cos(num) * num3),
|
||||||
|
z: roundFloat(GetEntityForwardVector(cache.ped)[2]),
|
||||||
|
}
|
||||||
|
}
|
49
resources/[voice]/yaca/apps/yaca-client/src/utils/props.ts
Normal file
49
resources/[voice]/yaca/apps/yaca-client/src/utils/props.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { cache } from './cache'
|
||||||
|
import { requestModel } from './streaming'
|
||||||
|
|
||||||
|
export const joaat = (input: string, ignore_casing = true) => {
|
||||||
|
input = !ignore_casing ? input.toLowerCase() : input
|
||||||
|
const length = input.length
|
||||||
|
|
||||||
|
let hash: number
|
||||||
|
let i: number
|
||||||
|
|
||||||
|
for (hash = i = 0; i < length; i++) {
|
||||||
|
hash += input.charCodeAt(i)
|
||||||
|
hash += hash << 10
|
||||||
|
hash ^= hash >>> 6
|
||||||
|
}
|
||||||
|
|
||||||
|
hash += hash << 3
|
||||||
|
hash ^= hash >>> 11
|
||||||
|
hash += hash << 15
|
||||||
|
|
||||||
|
return hash >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a prop and attach it to the player.
|
||||||
|
*
|
||||||
|
* @param model - The model of the prop.
|
||||||
|
* @param boneId - The bone id to attach the prop to.
|
||||||
|
* @param offset - The offset of the prop.
|
||||||
|
* @param rotation - The rotation of the prop.
|
||||||
|
*/
|
||||||
|
export const createProp = async (
|
||||||
|
model: string | number,
|
||||||
|
boneId: number,
|
||||||
|
offset: [number, number, number] = [0.0, 0.0, 0.0],
|
||||||
|
rotation: [number, number, number] = [0.0, 0.0, 0.0],
|
||||||
|
) => {
|
||||||
|
const modelHash = await requestModel(model)
|
||||||
|
if (!modelHash) return
|
||||||
|
|
||||||
|
const [x, y, z] = GetEntityCoords(cache.ped, true)
|
||||||
|
const [ox, oy, oz] = offset
|
||||||
|
const [rx, ry, rz] = rotation
|
||||||
|
const object = CreateObject(modelHash, x, y, z, true, true, false)
|
||||||
|
SetEntityCollision(object, false, false)
|
||||||
|
AttachEntityToEntity(object, cache.ped, GetPedBoneIndex(cache.ped, boneId), ox, oy, oz, rx, ry, rz, true, false, false, true, 2, true)
|
||||||
|
|
||||||
|
return object
|
||||||
|
}
|
62
resources/[voice]/yaca/apps/yaca-client/src/utils/redm.ts
Normal file
62
resources/[voice]/yaca/apps/yaca-client/src/utils/redm.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { REDM_KEY_TO_HASH } from '../yaca'
|
||||||
|
import { requestAnimDict } from './streaming'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a facial animation on a ped.
|
||||||
|
*
|
||||||
|
* @param ped - The ped to play the facial animation on.
|
||||||
|
* @param animName - The animation name to use.
|
||||||
|
* @param animDict - The animation dictionary to use.
|
||||||
|
*/
|
||||||
|
export const playRdrFacialAnim = async (ped: number, animName: string, animDict: string) => {
|
||||||
|
const loadedAnimDict = await requestAnimDict(animDict)
|
||||||
|
if (!loadedAnimDict) return
|
||||||
|
|
||||||
|
SetFacialIdleAnimOverride(ped, animName, loadedAnimDict)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a notification in RDR.
|
||||||
|
*
|
||||||
|
* @param text - The text to display.
|
||||||
|
* @param duration - The duration to display the notification for.
|
||||||
|
*/
|
||||||
|
export const displayRdrNotification = (text: string, duration: number) => {
|
||||||
|
// @ts-expect-error VarString is a redm native
|
||||||
|
const str = VarString(10, 'LITERAL_STRING', text)
|
||||||
|
|
||||||
|
const struct1 = new DataView(new ArrayBuffer(96))
|
||||||
|
struct1.setUint32(0, duration, true)
|
||||||
|
|
||||||
|
const struct2 = new DataView(new ArrayBuffer(8 + 8))
|
||||||
|
struct2.setBigUint64(8, BigInt(str), true)
|
||||||
|
|
||||||
|
Citizen.invokeNative('0x049D5C615BD38BAD', struct1, struct2, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a keybind for RDR.
|
||||||
|
*
|
||||||
|
* @param key - The key to bind.
|
||||||
|
* @param onPressed - The function to call when the key is pressed.
|
||||||
|
* @param onReleased - The function to call when the key is released.
|
||||||
|
*/
|
||||||
|
export const registerRdrKeyBind = (key: string, onPressed?: () => void, onReleased?: () => void) => {
|
||||||
|
const keyHash = REDM_KEY_TO_HASH[key]
|
||||||
|
|
||||||
|
if (!keyHash) {
|
||||||
|
console.error(`[YaCA] No key hash available for ${key}, please choose another keybind`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTick(() => {
|
||||||
|
DisableControlAction(0, keyHash, true)
|
||||||
|
if (onPressed && IsDisabledControlJustPressed(0, keyHash)) {
|
||||||
|
onPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onReleased && IsDisabledControlJustReleased(0, keyHash)) {
|
||||||
|
onReleased()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { waitFor } from '@yaca-voice/common'
|
||||||
|
import { joaat } from './props'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request an asset and wait for it to load.
|
||||||
|
*
|
||||||
|
* @param request - The function to request the asset
|
||||||
|
* @param hasLoaded - The function to check if the asset has loaded
|
||||||
|
* @param assetType - The type of the asset
|
||||||
|
* @param asset - The asset to request
|
||||||
|
* @param timeout - The timeout in ms
|
||||||
|
*/
|
||||||
|
async function streamingRequest<T extends string | number>(
|
||||||
|
request: (asset: T) => unknown,
|
||||||
|
hasLoaded: (asset: T) => boolean,
|
||||||
|
assetType: string,
|
||||||
|
asset: T,
|
||||||
|
timeout = 30000,
|
||||||
|
) {
|
||||||
|
if (hasLoaded(asset)) return asset
|
||||||
|
|
||||||
|
request(asset)
|
||||||
|
|
||||||
|
return waitFor(
|
||||||
|
() => {
|
||||||
|
if (hasLoaded(asset)) return asset
|
||||||
|
},
|
||||||
|
`failed to load ${assetType} '${asset}' - this may be caused by\n- too many loaded assets\n- oversized, invalid, or corrupted assets`,
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a animation dictionary.
|
||||||
|
*
|
||||||
|
* @param animDict - The animation dictionary to request.
|
||||||
|
* @returns A promise that resolves to the animation dictionary once it is loaded.
|
||||||
|
* @throws Will throw an error if the animation dictionary is not valid or if the animation dictionary fails to load within the timeout.
|
||||||
|
*/
|
||||||
|
export const requestAnimDict = (animDict: string) => {
|
||||||
|
if (!DoesAnimDictExist(animDict)) throw new Error(`attempted to load invalid animDict '${animDict}'`)
|
||||||
|
|
||||||
|
return streamingRequest(RequestAnimDict, HasAnimDictLoaded, 'animDict', animDict)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a model by its name or hash key.
|
||||||
|
*
|
||||||
|
* @param modelName - The name or hash key of the model to load.
|
||||||
|
* @returns A promise that resolves to the model hash key once the model is loaded.
|
||||||
|
* @throws Will throw an error if the model is not valid or if the model fails to load within the timeout.
|
||||||
|
*/
|
||||||
|
export const requestModel = (modelName: string | number) => {
|
||||||
|
if (typeof modelName !== 'number') modelName = joaat(modelName)
|
||||||
|
if (!IsModelValid(modelName)) throw new Error(`attempted to load invalid model '${modelName}'`)
|
||||||
|
|
||||||
|
return streamingRequest(RequestModel, HasModelLoaded, 'model', modelName)
|
||||||
|
}
|
38
resources/[voice]/yaca/apps/yaca-client/src/utils/vectors.ts
Normal file
38
resources/[voice]/yaca/apps/yaca-client/src/utils/vectors.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { roundFloat } from './index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the distance between two points in 3D space
|
||||||
|
*
|
||||||
|
* @param firstPoint - The first point
|
||||||
|
* @param secondPoint - The second point
|
||||||
|
*/
|
||||||
|
export function calculateDistanceVec3(firstPoint: number[], secondPoint: number[]) {
|
||||||
|
return Math.sqrt((firstPoint[0] - secondPoint[0]) ** 2 + (firstPoint[1] - secondPoint[1]) ** 2 + (firstPoint[2] - secondPoint[2]) ** 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the distance between two points in 2D space
|
||||||
|
*
|
||||||
|
* @param firstPoint - The first point
|
||||||
|
* @param secondPoint - The second point
|
||||||
|
*/
|
||||||
|
export function calculateDistanceVec2(firstPoint: number[], secondPoint: number[]) {
|
||||||
|
return Math.sqrt((firstPoint[0] - secondPoint[0]) ** 2 + (firstPoint[1] - secondPoint[1]) ** 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array of numbers to an object with x, y, and z properties
|
||||||
|
*
|
||||||
|
* @param array - The array to convert
|
||||||
|
*/
|
||||||
|
export function convertNumberArrayToXYZ(array: number[]): {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
z: number
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
x: roundFloat(array[0]),
|
||||||
|
y: roundFloat(array[1]),
|
||||||
|
z: roundFloat(array[2]),
|
||||||
|
}
|
||||||
|
}
|
93
resources/[voice]/yaca/apps/yaca-client/src/utils/vehicle.ts
Normal file
93
resources/[voice]/yaca/apps/yaca-client/src/utils/vehicle.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* Checks if the vehicle has a window.
|
||||||
|
*
|
||||||
|
* @param vehicle - The vehicle.
|
||||||
|
* @param windowId - The window ID to check.
|
||||||
|
* @returns {boolean} - Whether the vehicle has a window.
|
||||||
|
*/
|
||||||
|
export function hasWindow(vehicle: number, windowId: number): boolean {
|
||||||
|
switch (windowId) {
|
||||||
|
case 0:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'window_lf') !== -1
|
||||||
|
case 1:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'window_rf') !== -1
|
||||||
|
case 2:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'window_lr') !== -1
|
||||||
|
case 3:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'window_rr') !== -1
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the vehicle has a door.
|
||||||
|
*
|
||||||
|
* @param vehicle - The vehicle.
|
||||||
|
* @param doorId - The door ID to check.
|
||||||
|
* @returns {boolean} - Whether the vehicle has a door.
|
||||||
|
*/
|
||||||
|
export function hasDoor(vehicle: number, doorId: number): boolean {
|
||||||
|
switch (doorId) {
|
||||||
|
case 0:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'door_dside_f') !== -1
|
||||||
|
case 1:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'door_pside_f') !== -1
|
||||||
|
case 2:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'door_dside_r') !== -1
|
||||||
|
case 3:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'door_pside_r') !== -1
|
||||||
|
case 4:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'bonnet') !== -1
|
||||||
|
case 5:
|
||||||
|
return GetEntityBoneIndexByName(vehicle, 'boot') !== -1
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the vehicle has an opening.
|
||||||
|
*
|
||||||
|
* @param vehicle - The vehicle.
|
||||||
|
* @returns {boolean} - Whether the vehicle has an opening.
|
||||||
|
*/
|
||||||
|
export function vehicleHasOpening(vehicle: number): boolean {
|
||||||
|
const doors = []
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
if (i === 4 || !hasDoor(vehicle, i)) continue
|
||||||
|
doors.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doors.length === 0) return true
|
||||||
|
for (const door of doors) {
|
||||||
|
const doorAngle = GetVehicleDoorAngleRatio(vehicle, door)
|
||||||
|
if (doorAngle > 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsVehicleDoorDamaged(vehicle, door)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AreAllVehicleWindowsIntact(vehicle)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 8 /* max windows */; i++) {
|
||||||
|
const hasWindows = hasWindow(vehicle, i)
|
||||||
|
if (hasWindows && !IsVehicleWindowIntact(vehicle, i)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsVehicleAConvertible(vehicle, false)) {
|
||||||
|
const roofState = GetConvertibleRoofState(vehicle)
|
||||||
|
if (roofState !== 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { sleep } from '@yaca-voice/common'
|
||||||
|
import EventEmitter2 from 'eventemitter2'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WebSocket class handles the communication between the nui and the client.
|
||||||
|
*/
|
||||||
|
export class WebSocket extends EventEmitter2 {
|
||||||
|
public readyState = 0
|
||||||
|
nuiReady = false
|
||||||
|
initialized = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the WebSocket class.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
RegisterNuiCallbackType('YACA_OnMessage')
|
||||||
|
RegisterNuiCallbackType('YACA_OnConnected')
|
||||||
|
RegisterNuiCallbackType('YACA_OnDisconnected')
|
||||||
|
|
||||||
|
on('__cfx_nui:YACA_OnMessage', (data: object, cb: (data: unknown) => void) => {
|
||||||
|
this.emit('message', data)
|
||||||
|
cb({})
|
||||||
|
})
|
||||||
|
|
||||||
|
on('__cfx_nui:YACA_OnConnected', (_: unknown, cb: (data: unknown) => void) => {
|
||||||
|
this.readyState = 1
|
||||||
|
this.emit('open')
|
||||||
|
cb({})
|
||||||
|
})
|
||||||
|
|
||||||
|
on('__cfx_nui:YACA_OnDisconnected', (data: { code: number; reason: string }, cb: (data: unknown) => void) => {
|
||||||
|
this.readyState = 3
|
||||||
|
this.emit('close', data.code, data.reason)
|
||||||
|
cb({})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the message to the nui that the websocket should connect.
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
while (!this.nuiReady) {
|
||||||
|
await sleep(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
SendNuiMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
action: 'connect',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the message to the nui that the websocket should disconnect.
|
||||||
|
*
|
||||||
|
* @param data - The data to send.
|
||||||
|
*/
|
||||||
|
send(data: object) {
|
||||||
|
if (this.readyState !== 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SendNuiMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
action: 'command',
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the message to the nui that the websocket should disconnect.
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
if (this.readyState === 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SendNuiMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
action: 'close',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
97
resources/[voice]/yaca/apps/yaca-client/src/yaca/data.ts
Normal file
97
resources/[voice]/yaca/apps/yaca-client/src/yaca/data.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
const localLipSyncAnimations: Record<'fivem' | 'redm', Record<string, { name: string; dict: string }>> = {
|
||||||
|
fivem: {
|
||||||
|
true: {
|
||||||
|
name: 'mic_chatter',
|
||||||
|
dict: 'mp_facial',
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
name: 'mood_normal_1',
|
||||||
|
dict: 'facials@gen_male@variations@normal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redm: {
|
||||||
|
true: {
|
||||||
|
name: 'mood_talking_normal',
|
||||||
|
dict: 'face_human@gen_male@base',
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
name: 'mood_normal',
|
||||||
|
dict: 'face_human@gen_male@base',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const REDM_KEY_TO_HASH: Record<string, number | null> = {
|
||||||
|
// Letters
|
||||||
|
A: 0x7065027d,
|
||||||
|
B: 0x4cc0e2fe,
|
||||||
|
C: 0x9959a6f0,
|
||||||
|
D: 0xb4e465b4,
|
||||||
|
E: 0xcefd9220,
|
||||||
|
F: 0xb2f377e8,
|
||||||
|
G: 0x760a9c6f,
|
||||||
|
H: 0x24978a28,
|
||||||
|
I: 0xc1989f95,
|
||||||
|
J: 0xf3830d8e,
|
||||||
|
K: null,
|
||||||
|
L: 0x80f28e95,
|
||||||
|
M: 0xe31c6a41,
|
||||||
|
N: 0x4bc9dabb, // (Push to Talk)
|
||||||
|
O: 0xf1301666,
|
||||||
|
P: 0xd82e0bd2,
|
||||||
|
Q: 0xde794e3e,
|
||||||
|
R: 0xe30cd707,
|
||||||
|
S: 0xd27782e3,
|
||||||
|
T: null,
|
||||||
|
U: 0xd8f73058,
|
||||||
|
V: 0x7f8d09b8,
|
||||||
|
W: 0x8fd015d8,
|
||||||
|
X: 0x8cc9cd42,
|
||||||
|
Y: null,
|
||||||
|
Z: 0x26e9dc00,
|
||||||
|
|
||||||
|
// Symbol Keys
|
||||||
|
RIGHTBRACKET: 0xa5bdcd3c,
|
||||||
|
LEFTBRACKET: 0x430593aa,
|
||||||
|
|
||||||
|
// Mouse buttons
|
||||||
|
MOUSE1: 0x07ce1e61,
|
||||||
|
MOUSE2: 0xf84fa74f,
|
||||||
|
MOUSE3: 0xcee12b50,
|
||||||
|
MWUP: 0x3076e97c,
|
||||||
|
|
||||||
|
// Modifier Keys
|
||||||
|
CTRL: 0xdb096b85,
|
||||||
|
TAB: 0xb238fe0b,
|
||||||
|
SHIFT: 0x8ffc75d6,
|
||||||
|
SPACEBAR: 0xd9d0e1c0,
|
||||||
|
ENTER: 0xc7b5340a,
|
||||||
|
BACKSPACE: 0x156f7119,
|
||||||
|
LALT: 0x8aaa0ad4,
|
||||||
|
DEL: 0x4af4d473,
|
||||||
|
PGUP: 0x446258b6,
|
||||||
|
PGDN: 0x3c3dd371,
|
||||||
|
|
||||||
|
// Function Keys
|
||||||
|
F1: 0xa8e3f467,
|
||||||
|
F4: 0x1f6d95e5,
|
||||||
|
F6: 0x3c0a40f2,
|
||||||
|
|
||||||
|
// Number Keys
|
||||||
|
'1': 0xe6f612e4,
|
||||||
|
'2': 0x1ce6d9eb,
|
||||||
|
'3': 0x4f49cc4c,
|
||||||
|
'4': 0x8f9f9e58,
|
||||||
|
'5': 0xab62e997,
|
||||||
|
'6': 0xa1fde2a6,
|
||||||
|
'7': 0xb03a913b,
|
||||||
|
'8': 0x42385422,
|
||||||
|
|
||||||
|
// Arrow Keys
|
||||||
|
DOWN: 0x05ca7c52,
|
||||||
|
UP: 0x6319db71,
|
||||||
|
LEFT: 0xa65ebab4,
|
||||||
|
RIGHT: 0xdeb34313,
|
||||||
|
}
|
||||||
|
|
||||||
|
export { localLipSyncAnimations, REDM_KEY_TO_HASH }
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './data'
|
||||||
|
export * from './intercom'
|
||||||
|
export * from './main'
|
||||||
|
export * from './megaphone'
|
||||||
|
export * from './phone'
|
||||||
|
export * from './radio'
|
59
resources/[voice]/yaca/apps/yaca-client/src/yaca/intercom.ts
Normal file
59
resources/[voice]/yaca/apps/yaca-client/src/yaca/intercom.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { CommDeviceMode, YacaFilterEnum, type YacaPlayerData } from '@yaca-voice/types'
|
||||||
|
import type { YaCAClientModule } from './main'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The intercom module for the client.
|
||||||
|
*/
|
||||||
|
export class YaCAClientIntercomModule {
|
||||||
|
clientModule: YaCAClientModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the intercom module.
|
||||||
|
*
|
||||||
|
* @param clientModule - The client module.
|
||||||
|
*/
|
||||||
|
constructor(clientModule: YaCAClientModule) {
|
||||||
|
this.clientModule = clientModule
|
||||||
|
|
||||||
|
this.registerEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the intercom events.
|
||||||
|
*/
|
||||||
|
registerEvents() {
|
||||||
|
/**
|
||||||
|
* Handles the "client:yaca:addRemovePlayerIntercomFilter" server event.
|
||||||
|
*
|
||||||
|
* @param {number[] | number} playerIDs - The IDs of the players to be added or removed from the intercom filter.
|
||||||
|
* @param {boolean} state - The state indicating whether to add or remove the players.
|
||||||
|
*/
|
||||||
|
onNet('client:yaca:addRemovePlayerIntercomFilter', (playerIDs: number | number[], state: boolean) => {
|
||||||
|
if (!Array.isArray(playerIDs)) {
|
||||||
|
playerIDs = [playerIDs]
|
||||||
|
}
|
||||||
|
|
||||||
|
const playersToAddRemove: Set<YacaPlayerData> = new Set()
|
||||||
|
for (const playerID of playerIDs) {
|
||||||
|
const player = this.clientModule.getPlayerByID(playerID)
|
||||||
|
if (!player) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
playersToAddRemove.add(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playersToAddRemove.size < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
Array.from(playersToAddRemove),
|
||||||
|
YacaFilterEnum.INTERCOM,
|
||||||
|
state,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CommDeviceMode.TRANSCEIVER,
|
||||||
|
CommDeviceMode.TRANSCEIVER,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1733
resources/[voice]/yaca/apps/yaca-client/src/yaca/main.ts
Normal file
1733
resources/[voice]/yaca/apps/yaca-client/src/yaca/main.ts
Normal file
File diff suppressed because it is too large
Load diff
218
resources/[voice]/yaca/apps/yaca-client/src/yaca/megaphone.ts
Normal file
218
resources/[voice]/yaca/apps/yaca-client/src/yaca/megaphone.ts
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
import { locale, MEGAPHONE_STATE_NAME } from '@yaca-voice/common'
|
||||||
|
import { CommDeviceMode, YacaFilterEnum } from '@yaca-voice/types'
|
||||||
|
import { cache, joaat, onCache, registerRdrKeyBind } from '../utils'
|
||||||
|
import type { YaCAClientModule } from './main'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The megaphone module for the client.
|
||||||
|
*/
|
||||||
|
export class YaCAClientMegaphoneModule {
|
||||||
|
clientModule: YaCAClientModule
|
||||||
|
|
||||||
|
canUseMegaphone = false
|
||||||
|
lastMegaphoneState = false
|
||||||
|
|
||||||
|
megaphoneVehicleWhitelistHashes = new Set<number>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the megaphone module.
|
||||||
|
*
|
||||||
|
* @param clientModule - The client module.
|
||||||
|
*/
|
||||||
|
constructor(clientModule: YaCAClientModule) {
|
||||||
|
this.clientModule = clientModule
|
||||||
|
|
||||||
|
this.registerEvents()
|
||||||
|
if (this.clientModule.isFiveM) {
|
||||||
|
this.registerKeybinds()
|
||||||
|
|
||||||
|
for (const vehicleModel of this.clientModule.sharedConfig.megaphone.allowedVehicleModels) {
|
||||||
|
this.megaphoneVehicleWhitelistHashes.add(joaat(vehicleModel))
|
||||||
|
}
|
||||||
|
} else if (this.clientModule.isRedM) {
|
||||||
|
this.registerRdrKeybinds()
|
||||||
|
}
|
||||||
|
this.registerExports()
|
||||||
|
this.registerStateBagHandlers()
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEvents() {
|
||||||
|
/**
|
||||||
|
* Handles the "client:yaca:setLastMegaphoneState" server event.
|
||||||
|
*
|
||||||
|
* @param {boolean} state - The state of the megaphone.
|
||||||
|
*/
|
||||||
|
onNet('client:yaca:setLastMegaphoneState', (state: boolean) => {
|
||||||
|
this.lastMegaphoneState = state
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.clientModule.isFiveM && this.clientModule.sharedConfig.megaphone.automaticVehicleDetection) {
|
||||||
|
/**
|
||||||
|
* Checks if the player can use the megaphone when they enter a vehicle.
|
||||||
|
* If they can, it sets the `canUseMegaphone` property to `true`.
|
||||||
|
* If they can't, it sets the `canUseMegaphone` property to `false`.
|
||||||
|
* If the player is not in a vehicle, it sets the `canUseMegaphone` property to `false` and emits the "server:yaca:playerLeftVehicle" event.
|
||||||
|
*/
|
||||||
|
onCache<number | false>('seat', (seat) => {
|
||||||
|
if (seat === false || seat > 0 || !cache.vehicle) {
|
||||||
|
this.canUseMegaphone = false
|
||||||
|
emitNet('server:yaca:playerLeftVehicle')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const vehicleClass = GetVehicleClass(cache.vehicle)
|
||||||
|
const vehicleModel = GetEntityModel(cache.vehicle)
|
||||||
|
|
||||||
|
this.canUseMegaphone =
|
||||||
|
this.clientModule.sharedConfig.megaphone.allowedVehicleClasses.includes(vehicleClass) ||
|
||||||
|
this.megaphoneVehicleWhitelistHashes.has(vehicleModel)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the command and key mapping for the megaphone.
|
||||||
|
* This is only available in FiveM.
|
||||||
|
*/
|
||||||
|
registerKeybinds() {
|
||||||
|
if (this.clientModule.sharedConfig.keyBinds.megaphone === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the command and key mapping for the megaphone.
|
||||||
|
*/
|
||||||
|
RegisterCommand(
|
||||||
|
'+yaca:megaphone',
|
||||||
|
() => {
|
||||||
|
this.useMegaphone(true)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
RegisterCommand(
|
||||||
|
'-yaca:megaphone',
|
||||||
|
() => {
|
||||||
|
this.useMegaphone(false)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
RegisterKeyMapping('+yaca:megaphone', locale('use_megaphone'), 'keyboard', this.clientModule.sharedConfig.keyBinds.megaphone)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the keybindings for the megaphone.
|
||||||
|
* This is only available in RedM.
|
||||||
|
*/
|
||||||
|
registerRdrKeybinds() {
|
||||||
|
if (this.clientModule.sharedConfig.keyBinds.megaphone === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the command and key mapping for the megaphone.
|
||||||
|
*/
|
||||||
|
registerRdrKeyBind(this.clientModule.sharedConfig.keyBinds.megaphone, () => {
|
||||||
|
this.useMegaphone(!this.lastMegaphoneState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerExports() {
|
||||||
|
/**
|
||||||
|
* Gets the `canUseMegaphone` property.
|
||||||
|
*
|
||||||
|
* @returns {boolean} - The `canUseMegaphone` property.
|
||||||
|
*/
|
||||||
|
exports('getCanUseMegaphone', () => {
|
||||||
|
return this.canUseMegaphone
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the `canUseMegaphone` property.
|
||||||
|
*
|
||||||
|
* @param {boolean} state - The state to set the `canUseMegaphone` property to.
|
||||||
|
*/
|
||||||
|
exports('setCanUseMegaphone', (state: boolean) => {
|
||||||
|
this.canUseMegaphone = state
|
||||||
|
|
||||||
|
if (!state && this.lastMegaphoneState) {
|
||||||
|
emitNet('server:yaca:playerLeftVehicle')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the use of the megaphone.
|
||||||
|
*
|
||||||
|
* @param {boolean} [state=false] - The state of the megaphone. Defaults to false if not provided.
|
||||||
|
*/
|
||||||
|
exports('useMegaphone', (state = false) => {
|
||||||
|
this.useMegaphone(state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerStateBagHandlers() {
|
||||||
|
/**
|
||||||
|
* Handles the megaphone state bag change.
|
||||||
|
*/
|
||||||
|
AddStateBagChangeHandler(MEGAPHONE_STATE_NAME, '', (bagName: string, _: string, value: number | null, __: number, replicated: boolean) => {
|
||||||
|
if (replicated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerId = GetPlayerFromStateBagName(bagName)
|
||||||
|
if (playerId === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerSource = GetPlayerServerId(playerId)
|
||||||
|
if (playerSource === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerSource === cache.serverId) {
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
[],
|
||||||
|
YacaFilterEnum.MEGAPHONE,
|
||||||
|
typeof value === 'number',
|
||||||
|
undefined,
|
||||||
|
value,
|
||||||
|
CommDeviceMode.SENDER,
|
||||||
|
CommDeviceMode.RECEIVER,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const player = this.clientModule.getPlayerByID(playerSource)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
player,
|
||||||
|
YacaFilterEnum.MEGAPHONE,
|
||||||
|
typeof value === 'number',
|
||||||
|
undefined,
|
||||||
|
value,
|
||||||
|
CommDeviceMode.RECEIVER,
|
||||||
|
CommDeviceMode.SENDER,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the use of the megaphone.
|
||||||
|
*
|
||||||
|
* @param {boolean} [state=false] - The state of the megaphone. Defaults to false if not provided.
|
||||||
|
*/
|
||||||
|
useMegaphone(state = false) {
|
||||||
|
if (
|
||||||
|
(!cache.vehicle && this.clientModule.sharedConfig.megaphone.automaticVehicleDetection) ||
|
||||||
|
!this.canUseMegaphone ||
|
||||||
|
state === this.lastMegaphoneState
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastMegaphoneState = !this.lastMegaphoneState
|
||||||
|
emitNet('server:yaca:useMegaphone', state)
|
||||||
|
emit('yaca:external:megaphoneState', state)
|
||||||
|
}
|
||||||
|
}
|
288
resources/[voice]/yaca/apps/yaca-client/src/yaca/phone.ts
Normal file
288
resources/[voice]/yaca/apps/yaca-client/src/yaca/phone.ts
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
import { GLOBAL_ERROR_LEVEL_STATE_NAME, PHONE_SPEAKER_STATE_NAME } from '@yaca-voice/common'
|
||||||
|
import { CommDeviceMode, YacaFilterEnum, type YacaPlayerData } from '@yaca-voice/types'
|
||||||
|
import { cache } from '../utils'
|
||||||
|
import type { YaCAClientModule } from './main'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The phone module for the client.
|
||||||
|
*/
|
||||||
|
export class YaCAClientPhoneModule {
|
||||||
|
clientModule: YaCAClientModule
|
||||||
|
|
||||||
|
inCallWith = new Set<number>()
|
||||||
|
phoneSpeakerActive = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the phone module.
|
||||||
|
*
|
||||||
|
* @param clientModule - The client module.
|
||||||
|
*/
|
||||||
|
constructor(clientModule: YaCAClientModule) {
|
||||||
|
this.clientModule = clientModule
|
||||||
|
|
||||||
|
this.registerEvents()
|
||||||
|
this.registerExports()
|
||||||
|
this.registerStateBagHandlers()
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEvents() {
|
||||||
|
/**
|
||||||
|
* Handles the "client:yaca:phone" server event.
|
||||||
|
*
|
||||||
|
* @param {number | number[]} targetIDs - The ID of the target.
|
||||||
|
* @param {boolean} state - The state of the phone.
|
||||||
|
*/
|
||||||
|
onNet('client:yaca:phone', (targetIDs: number | number[], state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) => {
|
||||||
|
if (!Array.isArray(targetIDs)) {
|
||||||
|
targetIDs = [targetIDs]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enablePhoneCall(targetIDs, state, filter)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "client:yaca:phoneHearAround" server event.
|
||||||
|
*
|
||||||
|
* @param {number[]} targetClientIds - The IDs of the targets.
|
||||||
|
* @param {boolean} state - The state of the phone hear around.
|
||||||
|
*/
|
||||||
|
onNet('client:yaca:phoneHearAround', (targetClientIds: number[], state: boolean) => {
|
||||||
|
if (!targetClientIds.length) return
|
||||||
|
|
||||||
|
const commTargets = Array.from(targetClientIds).map((clientId) => ({ clientId }))
|
||||||
|
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
commTargets,
|
||||||
|
YacaFilterEnum.PHONE,
|
||||||
|
state,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CommDeviceMode.TRANSCEIVER,
|
||||||
|
CommDeviceMode.TRANSCEIVER,
|
||||||
|
GlobalState[PHONE_SPEAKER_STATE_NAME] ?? undefined,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "client:yaca:phoneMute" server event.
|
||||||
|
*
|
||||||
|
* @param {number} targetID - The ID of the target.
|
||||||
|
* @param {boolean} state - The state of the phone mute.
|
||||||
|
* @param {boolean} onCallStop - The state of the call.
|
||||||
|
*/
|
||||||
|
onNet('client:yaca:phoneMute', (targetID: number, state: boolean, onCallStop = false) => {
|
||||||
|
const target = this.clientModule.getPlayerByID(targetID)
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target.mutedOnPhone = state
|
||||||
|
|
||||||
|
if (onCallStop) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.clientModule.useWhisper && target.remoteID === cache.serverId) {
|
||||||
|
this.clientModule.setPlayersCommType([], YacaFilterEnum.PHONE, !state, undefined, undefined, CommDeviceMode.SENDER)
|
||||||
|
} else if (!this.clientModule.useWhisper && this.inCallWith.has(targetID)) {
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
target,
|
||||||
|
YacaFilterEnum.PHONE,
|
||||||
|
state,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CommDeviceMode.TRANSCEIVER,
|
||||||
|
CommDeviceMode.TRANSCEIVER,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "client:yaca:phoneSpeaker" server event.
|
||||||
|
*
|
||||||
|
* @param {number | number[]} playerIDs - The IDs of the players to be added or removed from the phone speaker.
|
||||||
|
* @param {boolean} state - The state indicating whether to add or remove the players.
|
||||||
|
*/
|
||||||
|
onNet('client:yaca:playersToPhoneSpeakerEmitWhisper', (playerIDs: number | number[], state: boolean) => {
|
||||||
|
if (!this.clientModule.useWhisper) return
|
||||||
|
|
||||||
|
if (!Array.isArray(playerIDs)) {
|
||||||
|
playerIDs = [playerIDs]
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = new Set<YacaPlayerData>()
|
||||||
|
for (const playerID of playerIDs) {
|
||||||
|
const player = this.clientModule.getPlayerByID(playerID)
|
||||||
|
if (!player) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.add(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.size < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
Array.from(targets),
|
||||||
|
YacaFilterEnum.PHONE_SPEAKER,
|
||||||
|
state,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CommDeviceMode.SENDER,
|
||||||
|
CommDeviceMode.RECEIVER,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerExports() {
|
||||||
|
/**
|
||||||
|
* Exports the "isInCall" function.
|
||||||
|
* This function returns whether the player is in a phone call.
|
||||||
|
*
|
||||||
|
* @returns {boolean} - Whether the player is in a phone call.
|
||||||
|
*/
|
||||||
|
exports('isInCall', () => this.inCallWith.size > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerStateBagHandlers() {
|
||||||
|
/**
|
||||||
|
* Handles the "yaca:phone" state bag change.
|
||||||
|
*/
|
||||||
|
AddStateBagChangeHandler(PHONE_SPEAKER_STATE_NAME, '', (bagName: string, _: string, value: number | number[] | null) => {
|
||||||
|
const playerId = GetPlayerFromStateBagName(bagName)
|
||||||
|
if (playerId === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerSource = GetPlayerServerId(playerId)
|
||||||
|
if (playerSource === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerSource === cache.serverId) {
|
||||||
|
this.phoneSpeakerActive = value !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removePhoneSpeakerFromEntity(playerSource)
|
||||||
|
if (value !== null) {
|
||||||
|
this.clientModule.setPlayerVariable(playerSource, 'phoneCallMemberIds', Array.isArray(value) ? value : [value])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the phone speaker effect from a player entity.
|
||||||
|
*
|
||||||
|
* @param {number} player - The player entity from which the phone speaker effect is to be removed.
|
||||||
|
*/
|
||||||
|
removePhoneSpeakerFromEntity(player: number) {
|
||||||
|
const entityData = this.clientModule.getPlayerByID(player)
|
||||||
|
if (!entityData?.phoneCallMemberIds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const playersToSet = []
|
||||||
|
for (const phoneCallMemberId of entityData.phoneCallMemberIds) {
|
||||||
|
const phoneCallMember = this.clientModule.getPlayerByID(phoneCallMemberId)
|
||||||
|
if (!phoneCallMember) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
playersToSet.push(phoneCallMember)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
playersToSet,
|
||||||
|
YacaFilterEnum.PHONE_SPEAKER,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CommDeviceMode.RECEIVER,
|
||||||
|
CommDeviceMode.SENDER,
|
||||||
|
)
|
||||||
|
|
||||||
|
entityData.phoneCallMemberIds = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the disconnection of a player from a phone call.
|
||||||
|
*
|
||||||
|
* @param {number} targetID - The ID of the target.
|
||||||
|
*/
|
||||||
|
handleDisconnect(targetID: number) {
|
||||||
|
this.inCallWith.delete(targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reestablishes a phone call with a target, when a player has restarted the voice plugin.
|
||||||
|
*
|
||||||
|
* @param {number | number[]} targetIDs - The IDs of the targets.
|
||||||
|
*/
|
||||||
|
reestablishCalls(targetIDs: number | number[]) {
|
||||||
|
if (!this.inCallWith.size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(targetIDs)) {
|
||||||
|
targetIDs = [targetIDs]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetIDs.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetsToReestablish = []
|
||||||
|
for (const targetId of targetIDs) {
|
||||||
|
if (this.inCallWith.has(targetId)) {
|
||||||
|
targetsToReestablish.push(targetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetsToReestablish.length) {
|
||||||
|
this.enablePhoneCall(targetsToReestablish, true, YacaFilterEnum.PHONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables a phone call.
|
||||||
|
*
|
||||||
|
* @param {number[]} targetIDs - The IDs of the targets.
|
||||||
|
* @param {boolean} state - The state of the phone call.
|
||||||
|
* @param {YacaFilterEnum} filter - The filter to use.
|
||||||
|
*/
|
||||||
|
enablePhoneCall(targetIDs: number[], state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) {
|
||||||
|
if (!targetIDs.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const commTargets = []
|
||||||
|
for (const targetID of targetIDs) {
|
||||||
|
const target = this.clientModule.getPlayerByID(targetID)
|
||||||
|
if (!target) {
|
||||||
|
if (!state) this.inCallWith.delete(targetID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
this.inCallWith.add(targetID)
|
||||||
|
} else {
|
||||||
|
this.inCallWith.delete(targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
commTargets.push(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientModule.setPlayersCommType(
|
||||||
|
commTargets,
|
||||||
|
filter,
|
||||||
|
state,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
state || (!state && this.inCallWith.size) ? CommDeviceMode.TRANSCEIVER : undefined,
|
||||||
|
CommDeviceMode.TRANSCEIVER,
|
||||||
|
GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
1124
resources/[voice]/yaca/apps/yaca-client/src/yaca/radio.ts
Normal file
1124
resources/[voice]/yaca/apps/yaca-client/src/yaca/radio.ts
Normal file
File diff suppressed because it is too large
Load diff
9
resources/[voice]/yaca/apps/yaca-client/tsconfig.json
Normal file
9
resources/[voice]/yaca/apps/yaca-client/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "@yaca-voice/typescript-config/fivem.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"types": ["@citizenfx/client"]
|
||||||
|
},
|
||||||
|
"include": ["./"]
|
||||||
|
}
|
23
resources/[voice]/yaca/apps/yaca-server/build.js
Normal file
23
resources/[voice]/yaca/apps/yaca-server/build.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { build } from 'esbuild'
|
||||||
|
|
||||||
|
const production = process.argv.includes('--mode=production')
|
||||||
|
|
||||||
|
build({
|
||||||
|
entryPoints: ['src/index.ts'],
|
||||||
|
outfile: './dist/server.js',
|
||||||
|
bundle: true,
|
||||||
|
loader: {
|
||||||
|
'.ts': 'ts',
|
||||||
|
'.js': 'js',
|
||||||
|
},
|
||||||
|
write: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: 'node16',
|
||||||
|
sourcemap: production ? false : 'inline',
|
||||||
|
dropLabels: production ? ['DEV'] : undefined,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Server built successfully')
|
||||||
|
})
|
||||||
|
// skipcq: JS-0263
|
||||||
|
.catch(() => process.exit(1))
|
25
resources/[voice]/yaca/apps/yaca-server/package.json
Normal file
25
resources/[voice]/yaca/apps/yaca-server/package.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "yaca-server",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node build.js --mode=production",
|
||||||
|
"dev": "node build.js",
|
||||||
|
"typecheck": "tsc --project tsconfig.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^3.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@citizenfx/server": "latest",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
|
"@types/node": "^20.16.10",
|
||||||
|
"@yaca-voice/common": "workspace:*",
|
||||||
|
"@yaca-voice/types": "workspace:*",
|
||||||
|
"@yaca-voice/typescript-config": "workspace:*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9.1"
|
||||||
|
}
|
||||||
|
}
|
1136
resources/[voice]/yaca/apps/yaca-server/pnpm-lock.yaml
generated
Normal file
1136
resources/[voice]/yaca/apps/yaca-server/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
165
resources/[voice]/yaca/apps/yaca-server/src/bridge/saltychat.ts
Normal file
165
resources/[voice]/yaca/apps/yaca-server/src/bridge/saltychat.ts
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import { saltyChatExport } from '@yaca-voice/common'
|
||||||
|
import { cache } from '../utils'
|
||||||
|
import type { YaCAServerModule } from '../yaca'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SaltyChat bridge for the server.
|
||||||
|
*/
|
||||||
|
export class YaCAServerSaltyChatBridge {
|
||||||
|
serverModule: YaCAServerModule
|
||||||
|
|
||||||
|
private callMap = new Map<string, Set<number>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the SaltyChat bridge.
|
||||||
|
*
|
||||||
|
* @param {YaCAServerModule} serverModule - The server module.
|
||||||
|
*/
|
||||||
|
constructor(serverModule: YaCAServerModule) {
|
||||||
|
this.serverModule = serverModule
|
||||||
|
|
||||||
|
this.registerSaltyChatEvents()
|
||||||
|
|
||||||
|
console.log('[YaCA] SaltyChat bridge loaded')
|
||||||
|
|
||||||
|
on('onResourceStop', (resourceName: string) => {
|
||||||
|
if (cache.resource !== resourceName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('onServerResourceStop', 'saltychat')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register SaltyChat events.
|
||||||
|
*/
|
||||||
|
registerSaltyChatEvents() {
|
||||||
|
saltyChatExport('GetPlayerAlive', (netId: number) => {
|
||||||
|
this.serverModule.getPlayerAliveStatus(netId)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetPlayerAlive', (netId: number, isAlive: boolean) => {
|
||||||
|
this.serverModule.changePlayerAliveStatus(netId, isAlive)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('GetPlayerVoiceRange', (netId: number) => {
|
||||||
|
this.serverModule.getPlayerVoiceRange(netId)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetPlayerVoiceRange', (netId: number, voiceRange: number) => {
|
||||||
|
this.serverModule.changeVoiceRange(netId, voiceRange)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('AddPlayerToCall', (callIdentifier: string, playerHandle: number) => this.addPlayerToCall(callIdentifier, playerHandle))
|
||||||
|
|
||||||
|
saltyChatExport('AddPlayersToCall', (callIdentifier: string, playerHandles: number[]) => this.addPlayerToCall(callIdentifier, playerHandles))
|
||||||
|
|
||||||
|
saltyChatExport('RemovePlayerFromCall', (callIdentifier: string, playerHandle: number) => this.removePlayerFromCall(callIdentifier, playerHandle))
|
||||||
|
|
||||||
|
saltyChatExport('RemovePlayersFromCall', (callIdentifier: string, playerHandles: number[]) => this.removePlayerFromCall(callIdentifier, playerHandles))
|
||||||
|
|
||||||
|
saltyChatExport('SetPhoneSpeaker', (playerHandle: number, toggle: boolean) => {
|
||||||
|
this.serverModule.phoneModule.enablePhoneSpeaker(playerHandle, toggle)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetPlayerRadioSpeaker', () => {
|
||||||
|
console.warn('SetPlayerRadioSpeaker is not implemented in YaCA')
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('GetPlayersInRadioChannel', (radioChannelName: string) => this.serverModule.radioModule.getPlayersInRadioFrequency(radioChannelName))
|
||||||
|
|
||||||
|
saltyChatExport('SetPlayerRadioChannel', (netId: number, radioChannelName: string, primary = true) => {
|
||||||
|
const channel = primary ? 1 : 2
|
||||||
|
const newRadioChannelName = radioChannelName === '' ? '0' : radioChannelName
|
||||||
|
|
||||||
|
this.serverModule.radioModule.changeRadioFrequency(netId, channel, newRadioChannelName)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('RemovePlayerRadioChannel', (netId: number, primary: boolean) => {
|
||||||
|
const channel = primary ? 1 : 2
|
||||||
|
this.serverModule.radioModule.changeRadioFrequency(netId, channel, '0')
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('SetRadioTowers', () => {
|
||||||
|
console.warn('SetRadioTowers is not implemented in YaCA')
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('EstablishCall', (callerId: number, targetId: number) => {
|
||||||
|
this.serverModule.phoneModule.callPlayer(callerId, targetId, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
saltyChatExport('EndCall', (callerId: number, targetId: number) => {
|
||||||
|
this.serverModule.phoneModule.callPlayer(callerId, targetId, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a player to a call.
|
||||||
|
*
|
||||||
|
* @param callIdentifier - The call identifier.
|
||||||
|
* @param playerHandle - The player handles.
|
||||||
|
*/
|
||||||
|
addPlayerToCall(callIdentifier: string, playerHandle: number | number[]) {
|
||||||
|
if (!Array.isArray(playerHandle)) {
|
||||||
|
playerHandle = [playerHandle]
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentlyInCall = this.callMap.get(callIdentifier) ?? new Set<number>()
|
||||||
|
const newInCall = new Set<number>()
|
||||||
|
|
||||||
|
for (const player of playerHandle) {
|
||||||
|
if (!currentlyInCall.has(player)) {
|
||||||
|
currentlyInCall.add(player)
|
||||||
|
newInCall.add(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callMap.set(callIdentifier, currentlyInCall)
|
||||||
|
|
||||||
|
for (const player of currentlyInCall) {
|
||||||
|
for (const otherPlayer of newInCall) {
|
||||||
|
if (player !== otherPlayer) {
|
||||||
|
this.serverModule.phoneModule.callPlayer(player, otherPlayer, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a player from a call.
|
||||||
|
*
|
||||||
|
* @param callIdentifier - The call identifier.
|
||||||
|
* @param playerHandle - The player handles.
|
||||||
|
*/
|
||||||
|
removePlayerFromCall(callIdentifier: string, playerHandle: number | number[]) {
|
||||||
|
if (!Array.isArray(playerHandle)) {
|
||||||
|
playerHandle = [playerHandle]
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeInCall = this.callMap.get(callIdentifier)
|
||||||
|
if (!beforeInCall) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowInCall = new Set<number>(beforeInCall)
|
||||||
|
|
||||||
|
const removedFromCall = new Set<number>()
|
||||||
|
for (const player of playerHandle) {
|
||||||
|
if (beforeInCall.has(player)) {
|
||||||
|
nowInCall.delete(player)
|
||||||
|
removedFromCall.add(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callMap.set(callIdentifier, nowInCall)
|
||||||
|
|
||||||
|
for (const player of removedFromCall) {
|
||||||
|
for (const otherPlayer of beforeInCall) {
|
||||||
|
if (player !== otherPlayer) {
|
||||||
|
this.serverModule.phoneModule.callPlayer(player, otherPlayer, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
resources/[voice]/yaca/apps/yaca-server/src/index.ts
Normal file
5
resources/[voice]/yaca/apps/yaca-server/src/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="@citizenfx/server" />
|
||||||
|
|
||||||
|
import { YaCAServerModule } from 'src/yaca'
|
||||||
|
|
||||||
|
new YaCAServerModule()
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { ServerCache } from '@yaca-voice/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached values for the server.
|
||||||
|
*/
|
||||||
|
export const cache: ServerCache = {
|
||||||
|
resource: GetCurrentResourceName(),
|
||||||
|
}
|
23
resources/[voice]/yaca/apps/yaca-server/src/utils/events.ts
Normal file
23
resources/[voice]/yaca/apps/yaca-server/src/utils/events.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Send a event to one or multiple clients.
|
||||||
|
*
|
||||||
|
* @param eventName - The name of the event.
|
||||||
|
* @param targetIds - The target ids.
|
||||||
|
* @param args - The arguments to send.
|
||||||
|
*/
|
||||||
|
export const triggerClientEvent = (eventName: string, targetIds: number[] | number, ...args: unknown[]) => {
|
||||||
|
if (!Array.isArray(targetIds)) {
|
||||||
|
targetIds = [targetIds]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetIds.length < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - msgpack_pack is not typed but available in the global scope.
|
||||||
|
const dataSerialized = msgpack_pack(args)
|
||||||
|
|
||||||
|
for (const targetId of targetIds) {
|
||||||
|
TriggerClientEventInternal(eventName, targetId.toString(), dataSerialized, dataSerialized.length)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random name and insert it into the database.
|
||||||
|
*
|
||||||
|
* @param src The ID of the player.
|
||||||
|
* @param nameSet The set of names to check against.
|
||||||
|
* @param namePattern The pattern to use for the name.
|
||||||
|
*/
|
||||||
|
export function generateRandomName(src: number, nameSet: Set<string>, namePattern: string): string | undefined {
|
||||||
|
let name: string | undefined
|
||||||
|
|
||||||
|
const playerName = GetPlayerName(src.toString())
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
let generatedName = namePattern
|
||||||
|
generatedName = generatedName.replace('{serverid}', src.toString())
|
||||||
|
generatedName = generatedName.replace('{playername}', playerName)
|
||||||
|
generatedName = generatedName.replace('{guid}', randomUUID().replace(/-/g, ''))
|
||||||
|
generatedName = generatedName.slice(0, 30)
|
||||||
|
|
||||||
|
if (!nameSet.has(generatedName)) {
|
||||||
|
name = generatedName
|
||||||
|
nameSet.add(generatedName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
console.error(`YaCA: Couldn't generate a random name for player ${playerName} (ID: ${src}).`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './cache'
|
||||||
|
export * from './events'
|
||||||
|
export * from './generator'
|
||||||
|
export * from './versioncheck'
|
|
@ -0,0 +1,57 @@
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import { cache } from './cache'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the version of the resource against the latest release on GitHub.
|
||||||
|
* If the resource is outdated, a message will be printed to the console.
|
||||||
|
*/
|
||||||
|
export const checkVersion = async () => {
|
||||||
|
const currentVersion = GetResourceMetadata(cache.resource, 'version', 0)
|
||||||
|
|
||||||
|
if (!currentVersion) {
|
||||||
|
console.error('[YaCA] Version check failed, no version found in resource manifest.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedVersion = currentVersion.match(/\d+\.\d+\.\d+/g)
|
||||||
|
|
||||||
|
if (!parsedVersion) {
|
||||||
|
console.error('[YaCA] Version check failed, version in resource manifest is not in the correct format.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('https://api.github.com/repos/yaca-systems/fivem-yaca-typescript/releases/latest')
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('[YaCA] Version check failed, unable to fetch latest release.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as { tag_name: string; html_url: string }
|
||||||
|
|
||||||
|
const latestVersion = data.tag_name
|
||||||
|
if (!latestVersion && latestVersion === currentVersion) {
|
||||||
|
console.log('[YaCA] You are running the latest version of YaCA.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedLatestVersion = latestVersion.match(/\d+\.\d+\.\d+/g)
|
||||||
|
if (!parsedLatestVersion) {
|
||||||
|
console.error('[YaCA] Version check failed, latest release is not in the correct format.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < parsedVersion.length; i++) {
|
||||||
|
const current = Number.parseInt(parsedVersion[i])
|
||||||
|
const latest = Number.parseInt(parsedLatestVersion[i])
|
||||||
|
|
||||||
|
if (current !== latest) {
|
||||||
|
if (current < latest) {
|
||||||
|
console.error(
|
||||||
|
`[YaCA] You are running an outdated version of YaCA. (current: ${currentVersion}, latest: ${latestVersion}) \r\n ${data.html_url}`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './main'
|
||||||
|
export * from './megaphone'
|
||||||
|
export * from './phone'
|
||||||
|
export * from './radio'
|
394
resources/[voice]/yaca/apps/yaca-server/src/yaca/main.ts
Normal file
394
resources/[voice]/yaca/apps/yaca-server/src/yaca/main.ts
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
import { GLOBAL_ERROR_LEVEL_STATE_NAME, getGlobalErrorLevel, initLocale, loadConfig, setGlobalErrorLevel, VOICE_RANGE_STATE_NAME } from '@yaca-voice/common'
|
||||||
|
import {
|
||||||
|
type DataObject,
|
||||||
|
defaultServerConfig,
|
||||||
|
defaultSharedConfig,
|
||||||
|
defaultTowerConfig,
|
||||||
|
type ServerCache,
|
||||||
|
type YacaServerConfig,
|
||||||
|
type YacaSharedConfig,
|
||||||
|
type YacaTowerConfig,
|
||||||
|
} from '@yaca-voice/types'
|
||||||
|
import { YaCAServerSaltyChatBridge } from '../bridge/saltychat'
|
||||||
|
import { checkVersion, generateRandomName } from '../utils'
|
||||||
|
import { triggerClientEvent } from '../utils/events'
|
||||||
|
import { YaCAServerMegaphoneModule } from './megaphone'
|
||||||
|
import { YaCAServerPhoneModle } from './phone'
|
||||||
|
import { YaCAServerRadioModule } from './radio'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The player data type for YaCA.
|
||||||
|
*/
|
||||||
|
export type YaCAPlayer = {
|
||||||
|
voiceSettings: {
|
||||||
|
voiceFirstConnect: boolean
|
||||||
|
forceMuted: boolean
|
||||||
|
ingameName: string
|
||||||
|
mutedOnPhone: boolean
|
||||||
|
inCallWith: Set<number>
|
||||||
|
emittedPhoneSpeaker: Map<number, Set<number>>
|
||||||
|
}
|
||||||
|
radioSettings: {
|
||||||
|
activated: boolean
|
||||||
|
hasLong: boolean
|
||||||
|
frequencies: Record<number, string>
|
||||||
|
}
|
||||||
|
voicePlugin?: {
|
||||||
|
playerId: number
|
||||||
|
clientId: number
|
||||||
|
forceMuted: boolean
|
||||||
|
mutedOnPhone: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main server module for YaCA.
|
||||||
|
*/
|
||||||
|
export class YaCAServerModule {
|
||||||
|
cache: ServerCache
|
||||||
|
|
||||||
|
nameSet: Set<string> = new Set()
|
||||||
|
players: Map<number, YaCAPlayer> = new Map()
|
||||||
|
|
||||||
|
defaultVoiceRange: number
|
||||||
|
|
||||||
|
serverConfig: YacaServerConfig
|
||||||
|
sharedConfig: YacaSharedConfig
|
||||||
|
towerConfig: YacaTowerConfig
|
||||||
|
|
||||||
|
phoneModule: YaCAServerPhoneModle
|
||||||
|
radioModule: YaCAServerRadioModule
|
||||||
|
megaphoneModule: YaCAServerMegaphoneModule
|
||||||
|
|
||||||
|
saltChatBridge?: YaCAServerSaltyChatBridge
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the server module.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
console.log('~g~ --> YaCA: Server loaded')
|
||||||
|
|
||||||
|
this.serverConfig = loadConfig<YacaServerConfig>('config/server.json5', defaultServerConfig)
|
||||||
|
this.sharedConfig = loadConfig<YacaSharedConfig>('config/shared.json5', defaultSharedConfig)
|
||||||
|
this.towerConfig = loadConfig<YacaTowerConfig>('config/tower.json5', defaultTowerConfig)
|
||||||
|
|
||||||
|
initLocale(this.sharedConfig.locale)
|
||||||
|
|
||||||
|
if (this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex]) {
|
||||||
|
this.defaultVoiceRange = this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex]
|
||||||
|
} else {
|
||||||
|
this.defaultVoiceRange = 1
|
||||||
|
this.sharedConfig.voiceRange.ranges = [1]
|
||||||
|
|
||||||
|
console.error('[YaCA] Default voice range is not set correctly in the config.')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.phoneModule = new YaCAServerPhoneModle(this)
|
||||||
|
this.radioModule = new YaCAServerRadioModule(this)
|
||||||
|
this.megaphoneModule = new YaCAServerMegaphoneModule(this)
|
||||||
|
|
||||||
|
this.registerExports()
|
||||||
|
this.registerEvents()
|
||||||
|
|
||||||
|
if (this.sharedConfig.saltyChatBridge) {
|
||||||
|
this.saltChatBridge = new YaCAServerSaltyChatBridge(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sharedConfig.versionCheck) {
|
||||||
|
checkVersion().then()
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalState.set(GLOBAL_ERROR_LEVEL_STATE_NAME, 0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the player data for a specific player.
|
||||||
|
*/
|
||||||
|
getPlayer(playerId: number): YaCAPlayer | undefined {
|
||||||
|
return this.players.get(playerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the player on first connect.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to initialize.
|
||||||
|
*/
|
||||||
|
connectToVoice(src: number) {
|
||||||
|
const name = generateRandomName(src, this.nameSet, this.serverConfig.userNamePattern)
|
||||||
|
if (!name) {
|
||||||
|
DropPlayer(src.toString(), '[YaCA] Failed to generate a random name.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = Player(src).state
|
||||||
|
playerState.set(VOICE_RANGE_STATE_NAME, this.defaultVoiceRange, true)
|
||||||
|
|
||||||
|
this.players.set(src, {
|
||||||
|
voiceSettings: {
|
||||||
|
voiceFirstConnect: false,
|
||||||
|
forceMuted: false,
|
||||||
|
ingameName: name,
|
||||||
|
mutedOnPhone: false,
|
||||||
|
inCallWith: new Set<number>(),
|
||||||
|
emittedPhoneSpeaker: new Map<number, Set<number>>(),
|
||||||
|
},
|
||||||
|
radioSettings: {
|
||||||
|
activated: false,
|
||||||
|
hasLong: true,
|
||||||
|
frequencies: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connect(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all exports for the YaCA module.
|
||||||
|
*/
|
||||||
|
registerExports() {
|
||||||
|
exports('connectToVoice', (src: number) => this.connectToVoice(src))
|
||||||
|
/**
|
||||||
|
* Get the alive status of a player.
|
||||||
|
*
|
||||||
|
* @param {number} playerId - The ID of the player to get the alive status for.
|
||||||
|
* @returns {boolean} - The alive status of the player.
|
||||||
|
*/
|
||||||
|
exports('getPlayerAliveStatus', (playerId: number) => this.getPlayerAliveStatus(playerId))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the alive status of a player.
|
||||||
|
*
|
||||||
|
* @param {number} playerId - The ID of the player to set the alive status for.
|
||||||
|
* @param {boolean} state - The new alive status.
|
||||||
|
*/
|
||||||
|
exports('setPlayerAliveStatus', (playerId: number, state: boolean) => this.changePlayerAliveStatus(playerId, state))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the voice range of a player.
|
||||||
|
*
|
||||||
|
* @param {number} playerId - The ID of the player to get the voice range for.
|
||||||
|
* @returns {number} - The voice range of the player.
|
||||||
|
*/
|
||||||
|
exports('getPlayerVoiceRange', (playerId: number) => this.getPlayerVoiceRange(playerId))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the voice range of a player.
|
||||||
|
*
|
||||||
|
* @param {number} playerId - The ID of the player to set the voice range for.
|
||||||
|
*/
|
||||||
|
exports('setPlayerVoiceRange', (playerId: number, range: number) => this.changeVoiceRange(playerId, range))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the global error level.
|
||||||
|
*
|
||||||
|
* @param {number} errorLevel - The new error level. Between 0 and 1.
|
||||||
|
*/
|
||||||
|
exports('setGlobalErrorLevel', (errorLevel: number) => setGlobalErrorLevel(errorLevel))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the global error level.
|
||||||
|
*
|
||||||
|
* @returns {number} - The global error level.
|
||||||
|
*/
|
||||||
|
exports('getGlobalErrorLevel', () => getGlobalErrorLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all events for the YaCA module.
|
||||||
|
*/
|
||||||
|
registerEvents() {
|
||||||
|
// FiveM: player dropped
|
||||||
|
on('playerDropped', (_reason: string) => {
|
||||||
|
this.handlePlayerDisconnect(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
// YaCA: connect to voice when NUI is ready
|
||||||
|
onNet('server:yaca:nuiReady', () => {
|
||||||
|
if (!this.sharedConfig.autoConnectOnJoin) return
|
||||||
|
this.connectToVoice(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
// YaCA:successful voice connection and client-id sync
|
||||||
|
onNet('server:yaca:addPlayer', (clientId: number) => {
|
||||||
|
this.addNewPlayer(source, clientId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// YaCa: voice restart
|
||||||
|
onNet('server:yaca:wsReady', () => {
|
||||||
|
this.playerReconnect(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TxAdmin: spectate stop event
|
||||||
|
onNet('txsv:req:spectate:end', () => {
|
||||||
|
emitNet('client:yaca:txadmin:stopspectate', source)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle various cases if player disconnects.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player who disconnected.
|
||||||
|
*/
|
||||||
|
handlePlayerDisconnect(src: number) {
|
||||||
|
const player = this.players.get(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nameSet.delete(player.voiceSettings?.ingameName)
|
||||||
|
|
||||||
|
const allFrequencies = this.radioModule.radioFrequencyMap
|
||||||
|
for (const [key, value] of allFrequencies) {
|
||||||
|
value.delete(src)
|
||||||
|
if (!value.size) {
|
||||||
|
this.radioModule.radioFrequencyMap.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [targetId, emitterTargets] of player.voiceSettings.emittedPhoneSpeaker) {
|
||||||
|
const target = this.players.get(targetId)
|
||||||
|
if (!target || !target.voicePlugin) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerClientEvent('client:yaca:phoneHearAround', Array.from(emitterTargets), [target.voicePlugin.clientId], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitNet('client:yaca:disconnect', -1, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs player alive status and mute him if he is dead or whatever.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to sync.
|
||||||
|
* @param {boolean} alive - The new alive status.
|
||||||
|
*/
|
||||||
|
changePlayerAliveStatus(src: number, alive: boolean) {
|
||||||
|
const player = this.players.get(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.voiceSettings.forceMuted = !alive
|
||||||
|
emitNet('client:yaca:muteTarget', -1, src, !alive)
|
||||||
|
|
||||||
|
if (player.voicePlugin) {
|
||||||
|
player.voicePlugin.forceMuted = !alive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the alive status of a player.
|
||||||
|
*
|
||||||
|
* @param playerId - The ID of the player to get the alive status for.
|
||||||
|
*/
|
||||||
|
getPlayerAliveStatus(playerId: number) {
|
||||||
|
return this.players.get(playerId)?.voiceSettings.forceMuted ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used if a player reconnects to the server.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to reconnect.
|
||||||
|
*/
|
||||||
|
playerReconnect(src: number) {
|
||||||
|
const player = this.players.get(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.voiceSettings.voiceFirstConnect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connect(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the voice range of a player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to change the voice range for.
|
||||||
|
* @param {number} range - The new voice range. Defaults to the default voice range if not provided.
|
||||||
|
*/
|
||||||
|
changeVoiceRange(src: number, range?: number) {
|
||||||
|
const playerState = Player(src).state
|
||||||
|
|
||||||
|
playerState.set(VOICE_RANGE_STATE_NAME, range ?? this.defaultVoiceRange, true)
|
||||||
|
emitNet('client:yaca:changeVoiceRange', src, range)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the voice range of a player.
|
||||||
|
*
|
||||||
|
* @param playerId - The ID of the player to get the voice range for.
|
||||||
|
*/
|
||||||
|
getPlayerVoiceRange(playerId: number) {
|
||||||
|
const playerState = Player(playerId).state
|
||||||
|
return playerState[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends initial data needed to connect to teamspeak plugin.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to connect
|
||||||
|
*/
|
||||||
|
connect(src: number) {
|
||||||
|
const player = this.players.get(src)
|
||||||
|
if (!player) {
|
||||||
|
console.error(`YaCA: Missing player data for ${src}.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.voiceSettings.voiceFirstConnect = true
|
||||||
|
|
||||||
|
const initObject: DataObject = {
|
||||||
|
suid: this.serverConfig.uniqueServerId,
|
||||||
|
chid: this.serverConfig.ingameChannelId,
|
||||||
|
deChid: this.serverConfig.defaultChannelId,
|
||||||
|
channelPassword: this.serverConfig.ingameChannelPassword,
|
||||||
|
ingameName: player.voiceSettings.ingameName,
|
||||||
|
useWhisper: this.serverConfig.useWhisper,
|
||||||
|
excludeChannels: this.serverConfig.excludeChannels,
|
||||||
|
}
|
||||||
|
emitNet('client:yaca:init', src, initObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new player to all other players on connect or reconnect, so they know about some variables.
|
||||||
|
*
|
||||||
|
* @param src - The source-id of the player to add.
|
||||||
|
* @param {number} clientId - The client ID of the player.
|
||||||
|
*/
|
||||||
|
addNewPlayer(src: number, clientId: number) {
|
||||||
|
const player = this.players.get(src)
|
||||||
|
if (!player || !clientId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.voicePlugin = {
|
||||||
|
playerId: src,
|
||||||
|
clientId,
|
||||||
|
forceMuted: player.voiceSettings.forceMuted,
|
||||||
|
mutedOnPhone: player.voiceSettings.mutedOnPhone,
|
||||||
|
}
|
||||||
|
|
||||||
|
emitNet('client:yaca:addPlayers', -1, player.voicePlugin)
|
||||||
|
|
||||||
|
const allPlayersData = []
|
||||||
|
for (const playerSource of getPlayers()) {
|
||||||
|
const intPlayerSource = Number.parseInt(playerSource)
|
||||||
|
const playerServer = this.players.get(intPlayerSource)
|
||||||
|
if (!playerServer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playerServer.voicePlugin || intPlayerSource === src) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
allPlayersData.push(playerServer.voicePlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitNet('client:yaca:addPlayers', src, allPlayersData)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { MEGAPHONE_STATE_NAME } from '@yaca-voice/common'
|
||||||
|
import type { YacaSharedConfig } from '@yaca-voice/types'
|
||||||
|
import type { YaCAServerModule } from './main'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server-side megaphone module.
|
||||||
|
*/
|
||||||
|
export class YaCAServerMegaphoneModule {
|
||||||
|
private serverModule: YaCAServerModule
|
||||||
|
private sharedConfig: YacaSharedConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the megaphone module.
|
||||||
|
*
|
||||||
|
* @param serverModule - The server module.
|
||||||
|
*/
|
||||||
|
constructor(serverModule: YaCAServerModule) {
|
||||||
|
this.serverModule = serverModule
|
||||||
|
this.sharedConfig = serverModule.sharedConfig
|
||||||
|
|
||||||
|
this.registerEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register server events.
|
||||||
|
*/
|
||||||
|
registerEvents() {
|
||||||
|
/**
|
||||||
|
* Changes megaphone state by player
|
||||||
|
*
|
||||||
|
* @param {boolean} state - The state of the megaphone effect.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:useMegaphone', (state: boolean) => {
|
||||||
|
this.playerUseMegaphone(source, state)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "server:yaca:playerLeftVehicle" event.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:playerLeftVehicle', () => {
|
||||||
|
this.changeMegaphoneState(source, false, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the megaphone effect on a specific player via client event.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to apply the megaphone effect to.
|
||||||
|
* @param {boolean} state - The state of the megaphone effect.
|
||||||
|
*/
|
||||||
|
playerUseMegaphone(src: number, state: boolean) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = Player(src).state
|
||||||
|
|
||||||
|
if ((!state && !playerState[MEGAPHONE_STATE_NAME]) || (state && playerState[MEGAPHONE_STATE_NAME])) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeMegaphoneState(src, state)
|
||||||
|
emit('yaca:external:changeMegaphoneState', src, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the megaphone effect on a specific player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to apply the megaphone effect to.
|
||||||
|
* @param {boolean} state - The state of the megaphone effect.
|
||||||
|
* @param {boolean} [forced=false] - Whether the change is forced. Defaults to false if not provided.
|
||||||
|
*/
|
||||||
|
changeMegaphoneState(src: number, state: boolean, forced = false) {
|
||||||
|
const playerState = Player(src).state
|
||||||
|
|
||||||
|
if (!state && playerState[MEGAPHONE_STATE_NAME]) {
|
||||||
|
playerState.set(MEGAPHONE_STATE_NAME, null, true)
|
||||||
|
if (forced) {
|
||||||
|
emitNet('client:yaca:setLastMegaphoneState', src, false)
|
||||||
|
}
|
||||||
|
} else if (state && !playerState[MEGAPHONE_STATE_NAME]) {
|
||||||
|
playerState.set(MEGAPHONE_STATE_NAME, this.sharedConfig.megaphone.range, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
278
resources/[voice]/yaca/apps/yaca-server/src/yaca/phone.ts
Normal file
278
resources/[voice]/yaca/apps/yaca-server/src/yaca/phone.ts
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
import { PHONE_SPEAKER_STATE_NAME } from '@yaca-voice/common'
|
||||||
|
import { YacaFilterEnum } from '@yaca-voice/types'
|
||||||
|
import { triggerClientEvent } from '../utils/events'
|
||||||
|
import type { YaCAServerModule } from './main'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The phone module for the server.
|
||||||
|
*/
|
||||||
|
export class YaCAServerPhoneModle {
|
||||||
|
private serverModule: YaCAServerModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the phone module.
|
||||||
|
*
|
||||||
|
* @param {YaCAServerModule} serverModule - The server module.
|
||||||
|
*/
|
||||||
|
constructor(serverModule: YaCAServerModule) {
|
||||||
|
this.serverModule = serverModule
|
||||||
|
|
||||||
|
this.registerEvents()
|
||||||
|
this.registerExports()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register server events.
|
||||||
|
*/
|
||||||
|
registerEvents() {
|
||||||
|
/**
|
||||||
|
* Handles the "server:yaca:phoneSpeakerEmitWhisper" event.
|
||||||
|
*
|
||||||
|
* @param {number[]} enableForTargets - The IDs of the players to enable the phone speaker for.
|
||||||
|
* @param {number[]} disableForTargets - The IDs of the players to disable the phone speaker for.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:phoneSpeakerEmitWhisper', (enableForTargets?: number[], disableForTargets?: number[]) => {
|
||||||
|
const player = this.serverModule.players.get(source)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = new Set<number>()
|
||||||
|
|
||||||
|
for (const callTarget of player.voiceSettings.inCallWith) {
|
||||||
|
const target = this.serverModule.players.get(callTarget)
|
||||||
|
if (!target) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.add(callTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.size && enableForTargets?.length) {
|
||||||
|
triggerClientEvent('client:yaca:playersToPhoneSpeakerEmitWhisper', Array.from(targets), enableForTargets, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.size && disableForTargets?.length) {
|
||||||
|
triggerClientEvent('client:yaca:playersToPhoneSpeakerEmitWhisper', Array.from(targets), disableForTargets, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "server:yaca:phoneEmit" event.
|
||||||
|
*
|
||||||
|
* @param {number[]} enableForTargets - The IDs of the players to enable the phone speaker for.
|
||||||
|
* @param {number[]} disableForTargets - The IDs of the players to disable the phone speaker for.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:phoneEmit', (enableForTargets?: number[], disableForTargets?: number[]) => {
|
||||||
|
if (!this.serverModule.sharedConfig.phoneHearPlayersNearby) return
|
||||||
|
|
||||||
|
const player = this.serverModule.players.get(source)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableReceive = new Set<number>()
|
||||||
|
const disableReceive = new Set<number>()
|
||||||
|
|
||||||
|
if (enableForTargets?.length) {
|
||||||
|
for (const callTarget of player.voiceSettings.inCallWith) {
|
||||||
|
const target = this.serverModule.players.get(callTarget)
|
||||||
|
if (!target) continue
|
||||||
|
|
||||||
|
enableReceive.add(callTarget)
|
||||||
|
|
||||||
|
for (const targetID of enableForTargets) {
|
||||||
|
const map = player.voiceSettings.emittedPhoneSpeaker
|
||||||
|
const set = map.get(targetID) ?? new Set<number>()
|
||||||
|
set.add(callTarget)
|
||||||
|
map.set(targetID, set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableForTargets?.length) {
|
||||||
|
for (const targetID of disableForTargets) {
|
||||||
|
const emittedFor = player.voiceSettings.emittedPhoneSpeaker.get(targetID)
|
||||||
|
if (!emittedFor) continue
|
||||||
|
|
||||||
|
for (const emittedTarget of emittedFor) {
|
||||||
|
const target = this.serverModule.players.get(emittedTarget)
|
||||||
|
if (!target) continue
|
||||||
|
|
||||||
|
disableReceive.add(emittedTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.voiceSettings.emittedPhoneSpeaker.delete(targetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableReceive.size && enableForTargets?.length) {
|
||||||
|
const enableForTargetsData = new Set<number>()
|
||||||
|
|
||||||
|
for (const enableTarget of enableForTargets) {
|
||||||
|
const target = this.serverModule.players.get(enableTarget)
|
||||||
|
if (!target || !target.voicePlugin) continue
|
||||||
|
|
||||||
|
enableForTargetsData.add(target.voicePlugin.clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerClientEvent('client:yaca:phoneHearAround', Array.from(enableReceive), Array.from(enableForTargetsData), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableReceive.size && disableForTargets?.length) {
|
||||||
|
const disableForTargetsData = new Set<number>()
|
||||||
|
|
||||||
|
for (const disableTarget of disableForTargets) {
|
||||||
|
const target = this.serverModule.players.get(disableTarget)
|
||||||
|
if (!target || !target.voicePlugin) continue
|
||||||
|
|
||||||
|
disableForTargetsData.add(target.voicePlugin.clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerClientEvent('client:yaca:phoneHearAround', Array.from(disableReceive), Array.from(disableForTargetsData), false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerExports() {
|
||||||
|
/**
|
||||||
|
* Creates a phone call between two players.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player who is making the call.
|
||||||
|
* @param {number} target - The player who is being called.
|
||||||
|
* @param {boolean} state - The state of the call.
|
||||||
|
*/
|
||||||
|
exports('callPlayer', (src: number, target: number, state: boolean) => this.callPlayer(src, target, state))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a phone call between two players with the old effect.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player who is making the call.
|
||||||
|
* @param {number} target - The player who is being called.
|
||||||
|
* @param {boolean} state - The state of the call.
|
||||||
|
*/
|
||||||
|
exports('callPlayerOldEffect', (src: number, target: number, state: boolean) => this.callPlayer(src, target, state, YacaFilterEnum.PHONE_HISTORICAL))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute a player during a phone call.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to mute.
|
||||||
|
* @param {boolean} state - The mute state.
|
||||||
|
*/
|
||||||
|
exports('muteOnPhone', (src: number, state: boolean) => this.muteOnPhone(src, state))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the phone speaker for a player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to enable the phone speaker for.
|
||||||
|
* @param {boolean} state - The state of the phone speaker.
|
||||||
|
*/
|
||||||
|
exports('enablePhoneSpeaker', (src: number, state: boolean) => this.enablePhoneSpeaker(src, state))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is player in a phone call.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to check.
|
||||||
|
*/
|
||||||
|
exports('isPlayerInCall', (src: number): [boolean, number[]] => {
|
||||||
|
const player = this.serverModule.players.get(src)
|
||||||
|
if (!player) {
|
||||||
|
return [false, []]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [player.voiceSettings.inCallWith.size > 0, [...player.voiceSettings.inCallWith]]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call another player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player who is making the call.
|
||||||
|
* @param {number} target - The player who is being called.
|
||||||
|
* @param {boolean} state - The state of the call.
|
||||||
|
* @param {YacaFilterEnum} filter - The filter to use for the call. Defaults to PHONE if not provided.
|
||||||
|
*/
|
||||||
|
callPlayer(src: number, target: number, state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
const targetPlayer = this.serverModule.getPlayer(target)
|
||||||
|
if (!player || !targetPlayer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emitNet('client:yaca:phone', target, src, state, filter)
|
||||||
|
emitNet('client:yaca:phone', src, target, state, filter)
|
||||||
|
|
||||||
|
const playerState = Player(src).state
|
||||||
|
const targetState = Player(target).state
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
player.voiceSettings.inCallWith.add(target)
|
||||||
|
targetPlayer.voiceSettings.inCallWith.add(src)
|
||||||
|
|
||||||
|
if (playerState[PHONE_SPEAKER_STATE_NAME]) {
|
||||||
|
this.enablePhoneSpeaker(src, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetState[PHONE_SPEAKER_STATE_NAME]) {
|
||||||
|
this.enablePhoneSpeaker(target, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.muteOnPhone(src, false, true)
|
||||||
|
this.muteOnPhone(target, false, true)
|
||||||
|
|
||||||
|
player.voiceSettings.inCallWith.delete(target)
|
||||||
|
targetPlayer.voiceSettings.inCallWith.delete(src)
|
||||||
|
|
||||||
|
if (playerState[PHONE_SPEAKER_STATE_NAME]) {
|
||||||
|
this.enablePhoneSpeaker(src, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetState[PHONE_SPEAKER_STATE_NAME]) {
|
||||||
|
this.enablePhoneSpeaker(target, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('yaca:external:phoneCall', src, target, state, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute a player during a phone call.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to mute.
|
||||||
|
* @param {boolean} state - The mute state.
|
||||||
|
* @param {boolean} [onCallStop=false] - Whether the call has stopped. Defaults to false if not provided.
|
||||||
|
*/
|
||||||
|
muteOnPhone(src: number, state: boolean, onCallStop = false) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.voiceSettings.mutedOnPhone = state
|
||||||
|
emitNet('client:yaca:phoneMute', -1, src, state, onCallStop)
|
||||||
|
emit('yaca:external:phoneMute', src, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the phone speaker for a player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The source-id of the player to enable the phone speaker for.
|
||||||
|
* @param {boolean} state - The state of the phone speaker.
|
||||||
|
*/
|
||||||
|
enablePhoneSpeaker(src: number, state: boolean) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = Player(src).state
|
||||||
|
|
||||||
|
if (state && player.voiceSettings.inCallWith.size) {
|
||||||
|
playerState.set(PHONE_SPEAKER_STATE_NAME, Array.from(player.voiceSettings.inCallWith), true)
|
||||||
|
emit('yaca:external:phoneSpeaker', src, true)
|
||||||
|
} else {
|
||||||
|
playerState.set(PHONE_SPEAKER_STATE_NAME, null, true)
|
||||||
|
emit('yaca:external:phoneSpeaker', src, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
379
resources/[voice]/yaca/apps/yaca-server/src/yaca/radio.ts
Normal file
379
resources/[voice]/yaca/apps/yaca-server/src/yaca/radio.ts
Normal file
|
@ -0,0 +1,379 @@
|
||||||
|
import { locale } from '@yaca-voice/common'
|
||||||
|
import { YacaNotificationType, type YacaServerConfig, type YacaSharedConfig } from '@yaca-voice/types'
|
||||||
|
import { triggerClientEvent } from '../utils/events'
|
||||||
|
import type { YaCAServerModule } from './main'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server-side radio module.
|
||||||
|
*/
|
||||||
|
export class YaCAServerRadioModule {
|
||||||
|
private serverModule: YaCAServerModule
|
||||||
|
private sharedConfig: YacaSharedConfig
|
||||||
|
private serverConfig: YacaServerConfig
|
||||||
|
|
||||||
|
radioFrequencyMap = new Map<string, Map<number, { muted: boolean }>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the radio module.
|
||||||
|
*
|
||||||
|
* @param {YaCAServerModule} serverModule - The server module.
|
||||||
|
*/
|
||||||
|
constructor(serverModule: YaCAServerModule) {
|
||||||
|
this.serverModule = serverModule
|
||||||
|
this.sharedConfig = serverModule.sharedConfig
|
||||||
|
this.serverConfig = serverModule.serverConfig
|
||||||
|
|
||||||
|
this.registerEvents()
|
||||||
|
this.registerExports()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register server events.
|
||||||
|
*/
|
||||||
|
registerEvents() {
|
||||||
|
/**
|
||||||
|
* Handles the "server:yaca:enableRadio" server event.
|
||||||
|
*
|
||||||
|
* @param {boolean} state - The state of the radio.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:enableRadio', (state: boolean) => {
|
||||||
|
this.enableRadio(source, state)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "server:yaca:changeRadioFrequency" server event.
|
||||||
|
*
|
||||||
|
* @param {number} channel - The channel to change the frequency of.
|
||||||
|
* @param {string} frequency - The new frequency.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:changeRadioFrequency', (channel: number, frequency: string) => {
|
||||||
|
this.changeRadioFrequency(source, channel, frequency)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "server:yaca:muteRadioChannel" server event.
|
||||||
|
*
|
||||||
|
* @param {number} channel - The channel to mute.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:muteRadioChannel', (channel: number, state?: boolean) => {
|
||||||
|
this.radioChannelMute(source, channel, state)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "server:yaca:radioTalking" server event.
|
||||||
|
*
|
||||||
|
* @param {boolean} state - The state of the radio.
|
||||||
|
* @param {number} channel - The channel to change the talking state for.
|
||||||
|
* @param {number} distanceToTower - The distance to the tower.
|
||||||
|
*/
|
||||||
|
onNet('server:yaca:radioTalking', (state: boolean, channel: number, distanceToTower = -1) => {
|
||||||
|
this.radioTalkingState(source, state, channel, distanceToTower)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register server exports.
|
||||||
|
*/
|
||||||
|
registerExports() {
|
||||||
|
/**
|
||||||
|
* Get all players in a radio frequency.
|
||||||
|
*
|
||||||
|
* @param {string} frequency - The frequency to get the players for.
|
||||||
|
* @returns {number[]} - The players in the radio frequency.
|
||||||
|
*/
|
||||||
|
exports('getPlayersInRadioFrequency', (frequency: string) => this.getPlayersInRadioFrequency(frequency))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the radio channel for a player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to set the radio channel for.
|
||||||
|
* @param {number} channel - The channel to set.
|
||||||
|
* @param {string} frequency - The frequency to set.
|
||||||
|
*/
|
||||||
|
exports('setPlayerRadioChannel', (src: number, channel: number, frequency: string) => this.changeRadioFrequency(src, channel, frequency))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get if a player has long range radio.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to set the long range radio for.
|
||||||
|
*/
|
||||||
|
exports('getPlayerHasLongRange', (src: number) => this.getPlayerHasLongRange(src))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if a player has long range radio.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to set the long range radio for.
|
||||||
|
* @param {boolean} state - The new state of the long range radio.
|
||||||
|
*/
|
||||||
|
exports('setPlayerHasLongRange', (src: number, state: boolean) => this.setPlayerHasLongRange(src, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all players in a radio frequency.
|
||||||
|
*
|
||||||
|
* @param frequency - The frequency to get the players for.
|
||||||
|
*/
|
||||||
|
getPlayersInRadioFrequency(frequency: string) {
|
||||||
|
const allPlayersInChannel = this.radioFrequencyMap.get(frequency)
|
||||||
|
const playersArray: number[] = []
|
||||||
|
|
||||||
|
if (!allPlayersInChannel) {
|
||||||
|
return playersArray
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key] of allPlayersInChannel) {
|
||||||
|
const target = this.serverModule.getPlayer(key)
|
||||||
|
if (!target) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
playersArray.push(key)
|
||||||
|
}
|
||||||
|
return playersArray
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets if a player has long range radio.
|
||||||
|
*
|
||||||
|
* @param src - The player to get the long range radio for.
|
||||||
|
*/
|
||||||
|
getPlayerHasLongRange(src: number) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return player.radioSettings.hasLong
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets if a player has long range radio.
|
||||||
|
*
|
||||||
|
* @param src - The player to set the long range radio for.
|
||||||
|
* @param state - The new state of the long range radio.
|
||||||
|
*/
|
||||||
|
setPlayerHasLongRange(src: number, state: boolean) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.radioSettings.hasLong = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the radio for a player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to enable or disable the radio for.
|
||||||
|
* @param {boolean} state - The new state of the radio.
|
||||||
|
*/
|
||||||
|
enableRadio(src: number, state: boolean) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.radioSettings.activated = state
|
||||||
|
|
||||||
|
emit('yaca:export:enabledRadio', src, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the radio frequency for a player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to change the radio frequency for.
|
||||||
|
* @param {number} channel - The channel to change the frequency of.
|
||||||
|
* @param {string} frequency - The new frequency.
|
||||||
|
*/
|
||||||
|
changeRadioFrequency(src: number, channel: number, frequency: string) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.radioSettings.activated) {
|
||||||
|
emitNet('client:yaca:notification', src, locale('radio_not_activated'), YacaNotificationType.ERROR)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(channel) || channel < 1 || channel > this.sharedConfig.radioSettings.channelCount) {
|
||||||
|
emitNet('client:yaca:notification', src, locale('radio_channel_invalid'), YacaNotificationType.ERROR)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldFrequency = player.radioSettings.frequencies[channel]
|
||||||
|
|
||||||
|
// Leave the old frequency if the new one is 0
|
||||||
|
if (frequency === '0') {
|
||||||
|
this.leaveRadioFrequency(src, channel, oldFrequency)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave the old frequency if it's different from the new one
|
||||||
|
if (oldFrequency !== frequency) {
|
||||||
|
this.leaveRadioFrequency(src, channel, oldFrequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add player to channel map, so we know who is in which channel
|
||||||
|
if (!this.radioFrequencyMap.has(frequency)) {
|
||||||
|
this.radioFrequencyMap.set(frequency, new Map<number, { muted: boolean }>())
|
||||||
|
}
|
||||||
|
this.radioFrequencyMap.get(frequency)?.set(src, { muted: false })
|
||||||
|
|
||||||
|
player.radioSettings.frequencies[channel] = frequency
|
||||||
|
|
||||||
|
emitNet('client:yaca:setRadioFreq', src, channel, frequency)
|
||||||
|
emit('yaca:external:changedRadioFrequency', src, channel, frequency)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Add radio effect to player in new frequency
|
||||||
|
* const newPlayers = this.getPlayersInRadioFrequency(frequency);
|
||||||
|
* if (newPlayers.length) alt.emitClientRaw(newPlayers, "client:yaca:setRadioEffectInFrequency", frequency, player.id);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a player leave a radio frequency.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to leave the radio frequency.
|
||||||
|
* @param {number} channel - The channel to leave.
|
||||||
|
* @param {string} frequency - The frequency to leave.
|
||||||
|
*/
|
||||||
|
leaveRadioFrequency(src: number, channel: number, frequency: string) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPlayersInChannel = this.radioFrequencyMap.get(frequency)
|
||||||
|
if (!allPlayersInChannel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.radioSettings.frequencies[channel] = '0'
|
||||||
|
|
||||||
|
const playersArray = []
|
||||||
|
const allTargets = []
|
||||||
|
for (const [key] of allPlayersInChannel) {
|
||||||
|
const target = this.serverModule.getPlayer(key)
|
||||||
|
if (!target) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
playersArray.push(key)
|
||||||
|
|
||||||
|
if (key === src) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
allTargets.push(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.serverConfig.useWhisper) {
|
||||||
|
emitNet('client:yaca:radioTalking', src, allTargets, frequency, false, null, true)
|
||||||
|
} else if (player.voicePlugin) {
|
||||||
|
triggerClientEvent('client:yaca:leaveRadioChannel', playersArray, player.voicePlugin.clientId, frequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
allPlayersInChannel.delete(src)
|
||||||
|
if (!allPlayersInChannel.size) {
|
||||||
|
this.radioFrequencyMap.delete(frequency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute a radio channel for a player.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to mute the radio channel for.
|
||||||
|
* @param {number} channel - The channel to mute.
|
||||||
|
*/
|
||||||
|
radioChannelMute(src: number, channel: number, state?: boolean) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const radioFrequency = player.radioSettings.frequencies[channel]
|
||||||
|
const foundPlayer = this.radioFrequencyMap.get(radioFrequency)?.get(src)
|
||||||
|
if (!foundPlayer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foundPlayer.muted = typeof state !== 'undefined' ? state : !foundPlayer.muted
|
||||||
|
emitNet('client:yaca:setRadioMuteState', src, channel, foundPlayer.muted)
|
||||||
|
emit('yaca:external:changedRadioMuteState', src, channel, foundPlayer.muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the talking state of a player on the radio.
|
||||||
|
*
|
||||||
|
* @param {number} src - The player to change the talking state for.
|
||||||
|
* @param {boolean} state - The new talking state.
|
||||||
|
* @param {number} channel - The channel to change the talking state for.
|
||||||
|
* @param {number} distanceToTower - The distance to the tower.
|
||||||
|
*/
|
||||||
|
radioTalkingState(src: number, state: boolean, channel: number, distanceToTower: number) {
|
||||||
|
const player = this.serverModule.getPlayer(src)
|
||||||
|
if (!player || !player.radioSettings.activated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const radioFrequency = player.radioSettings.frequencies[channel]
|
||||||
|
if (!radioFrequency || radioFrequency === '0') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPlayers = this.radioFrequencyMap.get(radioFrequency)
|
||||||
|
if (!getPlayers) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targets: number[] = []
|
||||||
|
const targetsToSender: number[] = []
|
||||||
|
const radioInfos: Record<number, { shortRange: boolean }> = {}
|
||||||
|
|
||||||
|
for (const [key, values] of getPlayers) {
|
||||||
|
if (values.muted) {
|
||||||
|
if (key === src) {
|
||||||
|
targets = []
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === src) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = this.serverModule.getPlayer(key)
|
||||||
|
if (!target || !target.radioSettings.activated) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortRange = !player.radioSettings.hasLong && !target.radioSettings.hasLong
|
||||||
|
if ((player.radioSettings.hasLong && target.radioSettings.hasLong) || shortRange) {
|
||||||
|
targets.push(key)
|
||||||
|
|
||||||
|
radioInfos[key] = {
|
||||||
|
shortRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
targetsToSender.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerClientEvent(
|
||||||
|
'client:yaca:radioTalking',
|
||||||
|
targets,
|
||||||
|
src,
|
||||||
|
radioFrequency,
|
||||||
|
state,
|
||||||
|
radioInfos,
|
||||||
|
distanceToTower,
|
||||||
|
GetEntityCoords(GetPlayerPed(src.toString())),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (this.serverConfig.useWhisper) {
|
||||||
|
emitNet('client:yaca:radioTalkingWhisper', src, targetsToSender, radioFrequency, state, GetEntityCoords(GetPlayerPed(src.toString())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
resources/[voice]/yaca/apps/yaca-server/tsconfig.json
Normal file
9
resources/[voice]/yaca/apps/yaca-server/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "@yaca-voice/typescript-config/fivem.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"types": ["@types/node", "@citizenfx/server"]
|
||||||
|
},
|
||||||
|
"include": ["./"]
|
||||||
|
}
|
18
resources/[voice]/yaca/assets/yaca-voice/config/server.json5
Normal file
18
resources/[voice]/yaca/assets/yaca-voice/config/server.json5
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"uniqueServerId": "FAqZTlphJBka2Y0gZr/KrZyXzQY=", // The unique Server Identifier of the Teamspeak-Server
|
||||||
|
"ingameChannelId": 5, // The ID of the Ingame Channel
|
||||||
|
"ingameChannelPassword": "bgJ30EW6gS0ex00D", // The Password used to join the Ingame Channel
|
||||||
|
"defaultChannelId": 9, // The ID of the Channel where a players should be moved to when leaving Ingame
|
||||||
|
"useWhisper": false, // If you want to use the Whisper functions of TeamSpeak, if set to false it mutes and unmutes the players
|
||||||
|
"excludeChannels": [], // The channels that should be able to join while being Ingame without instantly being moved back into the Ingame channel
|
||||||
|
|
||||||
|
/* The pattern that is used to generate the username.
|
||||||
|
*
|
||||||
|
* Following placeholders will be replaced:
|
||||||
|
* - {serverid} with the Ingame-ID of the player
|
||||||
|
* - {playername} with the steam/fivem name of the player
|
||||||
|
* - {guid} with a string containing random letters and digits.
|
||||||
|
* - "userNamePattern": "[{serverid}] {guid}"
|
||||||
|
*/
|
||||||
|
"userNamePattern": "{serverid}"
|
||||||
|
}
|
174
resources/[voice]/yaca/assets/yaca-voice/config/shared.json5
Normal file
174
resources/[voice]/yaca/assets/yaca-voice/config/shared.json5
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
{
|
||||||
|
// Enable the version check to get notified about new versions.
|
||||||
|
"versionCheck": true,
|
||||||
|
// Enable manual or automatic connecting to the voice channel when joining the server.
|
||||||
|
"autoConnectOnJoin": true,
|
||||||
|
// The build type of the plugin. 0 = release, 1 = develop (develop allows using all yaca plugin version)
|
||||||
|
"buildType": 0,
|
||||||
|
// The locale that should be used.
|
||||||
|
"locale": "en",
|
||||||
|
// The time before the teamspeak client is being unmuted after joining the ingame channel.
|
||||||
|
"unmuteDelay": 400,
|
||||||
|
// The range in which you can hear the phone speaker when active.
|
||||||
|
"maxPhoneSpeakerRange": 5,
|
||||||
|
/* Choose if players near other players that are in a call, should be heard by the players on the opposite side of the call.
|
||||||
|
*
|
||||||
|
* Following options are available:
|
||||||
|
* - false: Disable the feature completely
|
||||||
|
* - true: Enable the feature always
|
||||||
|
* - "PHONE_SPEAKER": Enable the feature only when the player on the phone has the phone speaker enabled.
|
||||||
|
*/
|
||||||
|
"phoneHearPlayersNearby": false,
|
||||||
|
// Choose which notifications should be enabled.
|
||||||
|
"notifications": {
|
||||||
|
// Enable notifications from ox_lib
|
||||||
|
"oxLib": false,
|
||||||
|
// Enable notifications from okokNotify
|
||||||
|
"okoknotify": false,
|
||||||
|
// Enable notifications from GTA (FiveM only)
|
||||||
|
"gta": true,
|
||||||
|
// Enable notifications from Rdr 2 (RedM only)
|
||||||
|
"redm": false,
|
||||||
|
// Enable the option to implement a custom notification
|
||||||
|
"own": false
|
||||||
|
},
|
||||||
|
// Set the default key binds for the plugin, which can be then changed by the player.
|
||||||
|
"keyBinds": {
|
||||||
|
// The key to increase the voice range
|
||||||
|
"increaseVoiceRange": "ADD",
|
||||||
|
// The key to decrease the voice range
|
||||||
|
"decreaseVoiceRange": "SUBTRACT",
|
||||||
|
// The key to transmit on the primary radio
|
||||||
|
"primaryRadioTransmit": "N",
|
||||||
|
// The key to transmit on the secondary radio
|
||||||
|
"secondaryRadioTransmit": "CAPITAL",
|
||||||
|
// The key to use the megaphone
|
||||||
|
"megaphone": "B",
|
||||||
|
// The key to hold additional to change voice range via mousewheel by 1 meter, false to disable that feature
|
||||||
|
"voiceRangeWithMouseWheel": "LCONTROL"
|
||||||
|
},
|
||||||
|
"radioSettings": {
|
||||||
|
// Customize radio animation
|
||||||
|
"animation": {
|
||||||
|
"dictionary": "random@arrests",
|
||||||
|
"name": "generic_radio_chatter",
|
||||||
|
"flag": 49
|
||||||
|
},
|
||||||
|
"propWhileTalking": {
|
||||||
|
// The prop that should be shown while talking on the radio.
|
||||||
|
"prop": false,
|
||||||
|
// The bone that the prop should be attached to.
|
||||||
|
"boneId": 28422,
|
||||||
|
// The position of the prop.
|
||||||
|
"position": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
// The rotation of the prop.
|
||||||
|
"rotation": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// The maximum amount of radio channels that can be used.
|
||||||
|
"channelCount": 9,
|
||||||
|
/* Choose the mode of the radio system.
|
||||||
|
*
|
||||||
|
* Following options are available:
|
||||||
|
* - "Tower": The radio system is based on towers, which means that the range is limited by the distance to the next tower.
|
||||||
|
* - "Direct": The radio system is based on the distance between the players.
|
||||||
|
* - "None": The radio always works no matter the distance.
|
||||||
|
*/
|
||||||
|
"mode": "Tower",
|
||||||
|
// The maximum distance between two players or to the tower to be able to hear each other.
|
||||||
|
"maxDistance": 5000
|
||||||
|
},
|
||||||
|
"voiceRange": {
|
||||||
|
// The default index which should be used for the voice range when a player joins the server.
|
||||||
|
"defaultIndex": 1,
|
||||||
|
// The ranges that should be available for the players.
|
||||||
|
"ranges": [
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
8,
|
||||||
|
15,
|
||||||
|
20,
|
||||||
|
25,
|
||||||
|
30,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
// Choose if a notification should be sent when the voice range is changed.
|
||||||
|
"sendNotification": false,
|
||||||
|
"markerColor": {
|
||||||
|
// Choose if the marker should be enabled.
|
||||||
|
"enabled": true,
|
||||||
|
// The color of the marker, r = red, g = green, b = blue, a = alpha
|
||||||
|
"r": 0,
|
||||||
|
"g": 255,
|
||||||
|
"b": 0,
|
||||||
|
"a": 50,
|
||||||
|
// The duration the marker should be shown.
|
||||||
|
"duration": 1000,
|
||||||
|
"type": 1,
|
||||||
|
"rotate": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"megaphone": {
|
||||||
|
// The range of the megaphone.
|
||||||
|
"range": 30,
|
||||||
|
// Choose if the plugin should automatically detect if the player should be able to use the megaphone in the vehicle. (FiveM only)
|
||||||
|
"automaticVehicleDetection": true,
|
||||||
|
// The allowed vehicle classes for the megaphone. (FiveM only)
|
||||||
|
"allowedVehicleClasses": [
|
||||||
|
18,
|
||||||
|
19
|
||||||
|
],
|
||||||
|
// The allowed vehicle models for the megaphone. (FiveM only)
|
||||||
|
"allowedVehicleModels": [
|
||||||
|
"polmav"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Choose if the saltyChatBridge should be enabled.
|
||||||
|
"saltyChatBridge": false,
|
||||||
|
"mufflingSettings": {
|
||||||
|
// If set to -1, the player voice range is used, all values >= 0 sets the muffling range before it gets completely cut off
|
||||||
|
"mufflingRange": -1,
|
||||||
|
"vehicleMuffling": {
|
||||||
|
// If set to true, the vehicle muffling feature is enabled. (FiveM only)
|
||||||
|
"enabled": true,
|
||||||
|
// Whitelist of vehicles that should be not be affected by the vehicle muffling. (FiveM only)
|
||||||
|
"vehicleWhitelist": [
|
||||||
|
"gauntlet6",
|
||||||
|
"draugur",
|
||||||
|
"bodhi2",
|
||||||
|
"vagrant",
|
||||||
|
"outlaw",
|
||||||
|
"trophytruck",
|
||||||
|
"ratel",
|
||||||
|
"drifttampa",
|
||||||
|
"sm722",
|
||||||
|
"tornado4",
|
||||||
|
"swinger",
|
||||||
|
"locust",
|
||||||
|
"hotring"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// The intensities of the muffling. (0 = no muffling, 10 = full muffling)
|
||||||
|
"intensities": {
|
||||||
|
// The intensity when the players are in different rooms.
|
||||||
|
"differentRoom": 10,
|
||||||
|
// The intensity when both cars are closed. (FiveM only)
|
||||||
|
"bothCarsClosed": 10,
|
||||||
|
// The intensity when one car is closed. (FiveM only)
|
||||||
|
"oneCarClosed": 6,
|
||||||
|
// The intensity of muffling of the megaphone of a player in a different car. (FiveM only)
|
||||||
|
"megaPhoneInCar": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Cooldown in milliseconds which the player has to wait to use the radio again, defaults to false which disables the feature.
|
||||||
|
"radioAntiSpamCooldown": false,
|
||||||
|
// When set to true the plugin syncs the talk state via the plugin, instead of the default way via statebags. This imitates the way how saltychat syncs the talk state, but has some drawbacks.
|
||||||
|
"useLocalLipSync": false
|
||||||
|
}
|
519
resources/[voice]/yaca/assets/yaca-voice/config/towers.json5
Normal file
519
resources/[voice]/yaca/assets/yaca-voice/config/towers.json5
Normal file
|
@ -0,0 +1,519 @@
|
||||||
|
{
|
||||||
|
"towerPositions": [
|
||||||
|
[
|
||||||
|
2572,
|
||||||
|
5397,
|
||||||
|
56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2663,
|
||||||
|
4972,
|
||||||
|
56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2892,
|
||||||
|
3911,
|
||||||
|
56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2720,
|
||||||
|
3304,
|
||||||
|
64
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2388,
|
||||||
|
2949,
|
||||||
|
64
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1830,
|
||||||
|
2368,
|
||||||
|
64
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1650,
|
||||||
|
1316,
|
||||||
|
102
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1363,
|
||||||
|
680,
|
||||||
|
102
|
||||||
|
],
|
||||||
|
[
|
||||||
|
918,
|
||||||
|
230,
|
||||||
|
92
|
||||||
|
],
|
||||||
|
[
|
||||||
|
567,
|
||||||
|
303,
|
||||||
|
58
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-47,
|
||||||
|
-666,
|
||||||
|
74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-585,
|
||||||
|
-902,
|
||||||
|
53
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2572,
|
||||||
|
5397,
|
||||||
|
56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2338,
|
||||||
|
5940,
|
||||||
|
77
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1916,
|
||||||
|
6244,
|
||||||
|
65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1591,
|
||||||
|
6371,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
[
|
||||||
|
953,
|
||||||
|
6504,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76,
|
||||||
|
6606,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
[
|
||||||
|
408,
|
||||||
|
6587,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-338,
|
||||||
|
-579,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-293,
|
||||||
|
-632,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-269,
|
||||||
|
-962,
|
||||||
|
143
|
||||||
|
],
|
||||||
|
[
|
||||||
|
98,
|
||||||
|
-870,
|
||||||
|
136
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-214,
|
||||||
|
-744,
|
||||||
|
219
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-166,
|
||||||
|
-590,
|
||||||
|
199
|
||||||
|
],
|
||||||
|
[
|
||||||
|
124,
|
||||||
|
-654,
|
||||||
|
261
|
||||||
|
],
|
||||||
|
[
|
||||||
|
149,
|
||||||
|
-769,
|
||||||
|
261
|
||||||
|
],
|
||||||
|
[
|
||||||
|
580,
|
||||||
|
89,
|
||||||
|
117
|
||||||
|
],
|
||||||
|
[
|
||||||
|
423,
|
||||||
|
15,
|
||||||
|
151
|
||||||
|
],
|
||||||
|
[
|
||||||
|
424,
|
||||||
|
18,
|
||||||
|
151
|
||||||
|
],
|
||||||
|
[
|
||||||
|
551,
|
||||||
|
-28,
|
||||||
|
93
|
||||||
|
],
|
||||||
|
[
|
||||||
|
305,
|
||||||
|
-284,
|
||||||
|
68
|
||||||
|
],
|
||||||
|
[
|
||||||
|
299,
|
||||||
|
-313,
|
||||||
|
68
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1240,
|
||||||
|
-1090,
|
||||||
|
44
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-418,
|
||||||
|
-2804,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
[
|
||||||
|
802,
|
||||||
|
-2996,
|
||||||
|
27
|
||||||
|
],
|
||||||
|
[
|
||||||
|
253,
|
||||||
|
-3145,
|
||||||
|
39
|
||||||
|
],
|
||||||
|
[
|
||||||
|
207,
|
||||||
|
-3145,
|
||||||
|
39
|
||||||
|
],
|
||||||
|
[
|
||||||
|
207,
|
||||||
|
-3307,
|
||||||
|
39
|
||||||
|
],
|
||||||
|
[
|
||||||
|
247,
|
||||||
|
-3307,
|
||||||
|
39
|
||||||
|
],
|
||||||
|
[
|
||||||
|
484,
|
||||||
|
-2178,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
[
|
||||||
|
548,
|
||||||
|
-2219,
|
||||||
|
67
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-701,
|
||||||
|
58,
|
||||||
|
68
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-696,
|
||||||
|
208,
|
||||||
|
139
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-769,
|
||||||
|
255,
|
||||||
|
134
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-150,
|
||||||
|
-150,
|
||||||
|
96
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-202,
|
||||||
|
-327,
|
||||||
|
65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1913,
|
||||||
|
-3031,
|
||||||
|
22
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1918,
|
||||||
|
-3028,
|
||||||
|
22
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1039,
|
||||||
|
-2385,
|
||||||
|
27
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1042,
|
||||||
|
-2390,
|
||||||
|
27
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1583,
|
||||||
|
-3216,
|
||||||
|
28
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1590,
|
||||||
|
-3212,
|
||||||
|
28
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1308,
|
||||||
|
-2626,
|
||||||
|
36
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1311,
|
||||||
|
-2624,
|
||||||
|
36
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-984,
|
||||||
|
-2778,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-991,
|
||||||
|
-2774,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-556,
|
||||||
|
-119,
|
||||||
|
50
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-619,
|
||||||
|
-106,
|
||||||
|
51
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1167,
|
||||||
|
-575,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1152,
|
||||||
|
-443,
|
||||||
|
42
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1156,
|
||||||
|
-498,
|
||||||
|
49
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1290,
|
||||||
|
-445,
|
||||||
|
106
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-928,
|
||||||
|
-383,
|
||||||
|
135
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-902,
|
||||||
|
-443,
|
||||||
|
170
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-770,
|
||||||
|
-786,
|
||||||
|
83
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-824,
|
||||||
|
-719,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-598,
|
||||||
|
-917,
|
||||||
|
35
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-678,
|
||||||
|
-717,
|
||||||
|
54
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-669,
|
||||||
|
-804,
|
||||||
|
31
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1463,
|
||||||
|
-526,
|
||||||
|
83
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1525,
|
||||||
|
-596,
|
||||||
|
66
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1375,
|
||||||
|
-465,
|
||||||
|
83
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1711,
|
||||||
|
478,
|
||||||
|
127
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2311,
|
||||||
|
335,
|
||||||
|
187
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2214,
|
||||||
|
342,
|
||||||
|
198
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2234,
|
||||||
|
187,
|
||||||
|
193
|
||||||
|
],
|
||||||
|
[
|
||||||
|
202,
|
||||||
|
1204,
|
||||||
|
230
|
||||||
|
],
|
||||||
|
[
|
||||||
|
217,
|
||||||
|
1140,
|
||||||
|
230
|
||||||
|
],
|
||||||
|
[
|
||||||
|
668,
|
||||||
|
590,
|
||||||
|
136
|
||||||
|
],
|
||||||
|
[
|
||||||
|
722,
|
||||||
|
562,
|
||||||
|
134
|
||||||
|
],
|
||||||
|
[
|
||||||
|
838,
|
||||||
|
510,
|
||||||
|
138
|
||||||
|
],
|
||||||
|
[
|
||||||
|
773,
|
||||||
|
575,
|
||||||
|
138
|
||||||
|
],
|
||||||
|
[
|
||||||
|
735,
|
||||||
|
231,
|
||||||
|
145
|
||||||
|
],
|
||||||
|
[
|
||||||
|
450,
|
||||||
|
5566,
|
||||||
|
795
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-449,
|
||||||
|
6019,
|
||||||
|
35
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-142,
|
||||||
|
6286,
|
||||||
|
39
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-368,
|
||||||
|
6105,
|
||||||
|
38
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2792,
|
||||||
|
5996,
|
||||||
|
355
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2796,
|
||||||
|
5992,
|
||||||
|
354
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3460,
|
||||||
|
3653,
|
||||||
|
51
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3459,
|
||||||
|
3659,
|
||||||
|
51
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3615,
|
||||||
|
3642,
|
||||||
|
51
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3614,
|
||||||
|
3636,
|
||||||
|
51
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2180,
|
||||||
|
3252,
|
||||||
|
54
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2124,
|
||||||
|
3219,
|
||||||
|
54
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2050,
|
||||||
|
3178,
|
||||||
|
54
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1858,
|
||||||
|
3694,
|
||||||
|
37
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1695,
|
||||||
|
3614,
|
||||||
|
37
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1692,
|
||||||
|
2532,
|
||||||
|
60
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1692,
|
||||||
|
2647,
|
||||||
|
60
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1824,
|
||||||
|
2574,
|
||||||
|
60
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1407,
|
||||||
|
2117,
|
||||||
|
104
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
33
resources/[voice]/yaca/assets/yaca-voice/locales/de.json
Normal file
33
resources/[voice]/yaca/assets/yaca-voice/locales/de.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"connect_error": "Fehler beim Verbinden zum Teamspeak-Server, bitte verbinde dich erneut!",
|
||||||
|
"plugin_not_initialized": "Das Voice-Plugin wurde noch nicht initialisiert!",
|
||||||
|
|
||||||
|
"outdated_plugin": "Dein Voice-Plugin ist veraltet! Bitte aktualisiere auf die Version %s!",
|
||||||
|
"wrong_ts_server": "Du bist auf dem falschen Teamspeak-Server!",
|
||||||
|
"move_error": "Fehler beim Verschieben auf den Teamspeak-Server!",
|
||||||
|
"max_players_reached": "Ihre Lizenz hat die maximale Spieleranzahl erreicht. Bitte erweitern Sie Ihre Lizenz.",
|
||||||
|
"license_server_timed_out": "Die Verbindung zum Lizenzserver wurde beim Überprüfen der Lizenz unterbrochen. Bitte warte einen Moment.",
|
||||||
|
"unknown_error": "Unbekannter Fehlercode: %s",
|
||||||
|
|
||||||
|
"changed_stereo_mode": "Kanal %s ist jetzt auf %s zu hören.",
|
||||||
|
"left_ear": "dem linken Ohr",
|
||||||
|
"right_ear": "dem rechten Ohr",
|
||||||
|
"both_ears": "beiden Ohren",
|
||||||
|
"secondary_radio_channel_disabled": "Der sekundäre Funkkanal wurde deaktiviert.",
|
||||||
|
"secondary_radio_channel_enabled": "Kanal %s ist nun der Sekundäre Funkkanal.",
|
||||||
|
|
||||||
|
"use_megaphone": "Megaphon benutzen",
|
||||||
|
"use_radio": "Im primären Funkkanal senden",
|
||||||
|
"use_secondary_radio": "Im sekundären Funkkanal senden",
|
||||||
|
|
||||||
|
"use_salty_primary_radio": "Primäres Funkgerät benutzen",
|
||||||
|
"use_salty_secondary_radio": "Sekundäres Funkgerät benutzen",
|
||||||
|
|
||||||
|
"change_voice_range_increase": "Sprachreichweite erhöhen",
|
||||||
|
"change_voice_range_decrease": "Sprachreichweite verringern",
|
||||||
|
"voice_range_changed": "Sprachreichweite auf %s Meter geändert.",
|
||||||
|
"change_voice_range_via_mousewheel": "Sprachreichweite je 1 Meter ändern",
|
||||||
|
|
||||||
|
"radio_not_activated": "Das Funkgerät ist nicht aktiviert!",
|
||||||
|
"radio_channel_invalid": "Ungültiger Funkkanal!"
|
||||||
|
}
|
33
resources/[voice]/yaca/assets/yaca-voice/locales/en.json
Normal file
33
resources/[voice]/yaca/assets/yaca-voice/locales/en.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"connect_error": "Error while connecting to voice server, please reconnect!",
|
||||||
|
"plugin_not_initialized": "Plugin not initialized!",
|
||||||
|
|
||||||
|
"outdated_plugin": "Your plugin is outdated, please update to version %s!",
|
||||||
|
"wrong_ts_server": "You are connected to the wrong teamspeak server!",
|
||||||
|
"move_error": "Error while moving to the channel!",
|
||||||
|
"max_players_reached": "Your license reached the maximum player count. Please upgrade your license",
|
||||||
|
"license_server_timed_out": "The connection to the license server timed out, while verifying the license. Please wait a moment.",
|
||||||
|
"unknown_error": "Unknown error code: %s",
|
||||||
|
|
||||||
|
"changed_stereo_mode": "Channel %s is now only heard in %s.",
|
||||||
|
"left_ear": "the left ear",
|
||||||
|
"right_ear": "the right ear",
|
||||||
|
"both_ears": "both ears",
|
||||||
|
"secondary_radio_channel_disabled": "The secondary radio channel has been disabled.",
|
||||||
|
"secondary_radio_channel_enabled": "Channel %s is now the secondary radio channel.",
|
||||||
|
|
||||||
|
"use_megaphone": "Use megaphone",
|
||||||
|
"use_radio": "Use radio",
|
||||||
|
"use_secondary_radio": "Use secondary radio",
|
||||||
|
|
||||||
|
"use_salty_primary_radio": "Use primary radio",
|
||||||
|
"use_salty_secondary_radio": "Use secondary radio",
|
||||||
|
|
||||||
|
"change_voice_range_increase": "Increase voice range",
|
||||||
|
"change_voice_range_decrease": "Decrease voice range",
|
||||||
|
"voice_range_changed": "Voice range changed to %s meters.",
|
||||||
|
"change_voice_range_via_mousewheel": "Change voice range by 1 meter",
|
||||||
|
|
||||||
|
"radio_not_activated": "Your radio is not activated!",
|
||||||
|
"radio_channel_invalid": "The radio channel is invalid!"
|
||||||
|
}
|
13
resources/[voice]/yaca/assets/yaca-voice/web/index.html
Normal file
13
resources/[voice]/yaca/assets/yaca-voice/web/index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>YACA WebSocket</title>
|
||||||
|
|
||||||
|
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
|
||||||
|
<script src="script.js" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="display: none;"></body>
|
||||||
|
|
||||||
|
</html>
|
85
resources/[voice]/yaca/assets/yaca-voice/web/script.js
Normal file
85
resources/[voice]/yaca/assets/yaca-voice/web/script.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
let webSocket = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the YaCA voice plugin
|
||||||
|
*/
|
||||||
|
function connect() {
|
||||||
|
console.log('[YaCA-Websocket] Trying to Connect to YaCA WebSocket...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
webSocket = new window.WebSocket('ws://127.0.0.1:30125/')
|
||||||
|
} catch {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
webSocket.onmessage = (event) => {
|
||||||
|
if (!event) return
|
||||||
|
sendNuiData('YACA_OnMessage', event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
webSocket.onopen = (event) => {
|
||||||
|
if (!event) return
|
||||||
|
sendNuiData('YACA_OnConnected')
|
||||||
|
}
|
||||||
|
|
||||||
|
webSocket.onclose = (event) => {
|
||||||
|
if (!event) return
|
||||||
|
|
||||||
|
sendNuiData('YACA_OnDisconnected', {
|
||||||
|
code: event.code,
|
||||||
|
reason: event.reason,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
connect()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a command to the YaCA voice plugin
|
||||||
|
*
|
||||||
|
* @param command - The command to send as a object
|
||||||
|
*/
|
||||||
|
function runCommand(command) {
|
||||||
|
if (!webSocket || webSocket.readyState !== WebSocket.OPEN) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
webSocket.send(JSON.stringify(command))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a NUI message to the client
|
||||||
|
*
|
||||||
|
* @param event - The name of the callback
|
||||||
|
* @param data - The data to send
|
||||||
|
*/
|
||||||
|
function sendNuiData(event, data = {}) {
|
||||||
|
// skipcq: JS-0125
|
||||||
|
fetch(`https://${GetParentResourceName()}/${event}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).catch((error) => console.error('[YaCA-Websocket] Error sending NUI Message:', error))
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
sendNuiData('YACA_OnNuiReady')
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.data.action === 'connect') {
|
||||||
|
connect()
|
||||||
|
} else if (event.data.action === 'command') {
|
||||||
|
runCommand(event.data.data)
|
||||||
|
} else if (event.data.action === 'close') {
|
||||||
|
if (webSocket) webSocket.close()
|
||||||
|
} else {
|
||||||
|
console.error('[YaCA-Websocket] Unknown message:', event.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
47
resources/[voice]/yaca/biome.json
Normal file
47
resources/[voice]/yaca/biome.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||||
|
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true, "defaultBranch": "main" },
|
||||||
|
"files": { "ignoreUnknown": false, "includes": ["**", "!**/resource", "!**/dist"] },
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"useEditorconfig": true,
|
||||||
|
"formatWithErrors": false,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 4,
|
||||||
|
"lineWidth": 160,
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"includes": ["**", "!**/dist", "!**/pnpm-lock.yaml"]
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"style": {
|
||||||
|
"noParameterAssign": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"includes": ["**", "!**/dist"]
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"jsxQuoteStyle": "double",
|
||||||
|
"quoteProperties": "asNeeded",
|
||||||
|
"trailingCommas": "all",
|
||||||
|
"semicolons": "asNeeded",
|
||||||
|
"arrowParentheses": "always",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"bracketSpacing": true
|
||||||
|
},
|
||||||
|
"globals": []
|
||||||
|
}
|
||||||
|
}
|
27
resources/[voice]/yaca/package.json
Normal file
27
resources/[voice]/yaca/package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "yaca-voice",
|
||||||
|
"type": "module",
|
||||||
|
"description": "YACA Voice Integration for FiveM & RedM",
|
||||||
|
"author": "MineMalox & LuftigerLuca",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/yaca-systems/fivem-yaca-typescript"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"typecheck": "turbo typecheck --env-mode=loose",
|
||||||
|
"format": "pnpm biome format --write",
|
||||||
|
"lint": "pnpm biome lint --write",
|
||||||
|
"check": "pnpm biome check --formatter-enabled=true --linter-enabled=true --write",
|
||||||
|
"build": "turbo build --env-mode=loose",
|
||||||
|
"dev": "turbo dev --env-mode=loose",
|
||||||
|
"create-resource": "turbo create-resource --env-mode=loose && node scripts/create-resource.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.0.6",
|
||||||
|
"esbuild": "^0.24.2",
|
||||||
|
"turbo": "^2.3.4",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue