forked from Simnation/Main
Saltychat Remove and PMA install
This commit is contained in:
parent
0bff8ae174
commit
2fd3c1fe70
94 changed files with 8799 additions and 5199 deletions
21
resources/[voice]/pma-voice/LICENSE
Normal file
21
resources/[voice]/pma-voice/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Dillon Skaggs
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
180
resources/[voice]/pma-voice/README.md
Normal file
180
resources/[voice]/pma-voice/README.md
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
# 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 |
|
10
resources/[voice]/pma-voice/TODO.md
Normal file
10
resources/[voice]/pma-voice/TODO.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
## 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
|
74
resources/[voice]/pma-voice/client/commands.lua
Normal file
74
resources/[voice]/pma-voice/client/commands.lua
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
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
|
41
resources/[voice]/pma-voice/client/events.lua
Normal file
41
resources/[voice]/pma-voice/client/events.lua
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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)
|
42
resources/[voice]/pma-voice/client/init/init.lua
Normal file
42
resources/[voice]/pma-voice/client/init/init.lua
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
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)
|
224
resources/[voice]/pma-voice/client/init/main.lua
Normal file
224
resources/[voice]/pma-voice/client/init/main.lua
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
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
|
156
resources/[voice]/pma-voice/client/init/proximity.lua
Normal file
156
resources/[voice]/pma-voice/client/init/proximity.lua
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
-- 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)
|
91
resources/[voice]/pma-voice/client/module/phone.lua
Normal file
91
resources/[voice]/pma-voice/client/module/phone.lua
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
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)
|
211
resources/[voice]/pma-voice/client/module/radio.lua
Normal file
211
resources/[voice]/pma-voice/client/module/radio.lua
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
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)
|
11
resources/[voice]/pma-voice/client/utils/Nui.lua
Normal file
11
resources/[voice]/pma-voice/client/utils/Nui.lua
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
local uiReady = promise.new()
|
||||||
|
function sendUIMessage(message)
|
||||||
|
Citizen.Await(uiReady)
|
||||||
|
SendNUIMessage(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
RegisterNUICallback("uiReady", function(data, cb)
|
||||||
|
uiReady:resolve(true)
|
||||||
|
|
||||||
|
cb('ok')
|
||||||
|
end)
|
1
resources/[voice]/pma-voice/docs/_config.yml
Normal file
1
resources/[voice]/pma-voice/docs/_config.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
theme: jekyll-theme-midnight
|
27
resources/[voice]/pma-voice/docs/client-getters/events.md
Normal file
27
resources/[voice]/pma-voice/docs/client-getters/events.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
## setTalkingMode | settingsCallback | radioACtive
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
These event is designed to allow third part applications (like a hud) use the current voice mode of the player, radio state, etc.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- default voice mode is 2
|
||||||
|
local voiceMode = 2
|
||||||
|
local voiceModes = {}
|
||||||
|
local usingRadio = false
|
||||||
|
-- sets the current radio state boolean
|
||||||
|
AddEventHandler("pma-voice:radioActive", function(radioTalking) usingRadio = radioTalking end)
|
||||||
|
-- changes the current voice range index
|
||||||
|
AddEventHandler('pma-voice:setTalkingMode', function(newTalkingRange) voiceMode = newTalkingRange end)
|
||||||
|
-- returns registered voice modes from shared.lua's `Cfg.voiceModes`
|
||||||
|
TriggerEvent("pma-voice:settingsCallback", function(voiceSettings)
|
||||||
|
local voiceTable = voiceSettings.voiceModes
|
||||||
|
|
||||||
|
-- loop through all voice modes and add them to the table
|
||||||
|
-- the percentage is used for the voice mode slider if this was an actual UI
|
||||||
|
for i = 1, #voiceTable do
|
||||||
|
local distance = math.ceil(((i/#voiceTable) * 100))
|
||||||
|
voiceModes[i] = ("%s"):format(distance)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
|
@ -0,0 +1,12 @@
|
||||||
|
## removePlayerFromCall
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Removes the player from the call
|
||||||
|
|
||||||
|
## NOTE: This is just syntactic sugar for `setCallChannel(0)`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Removes the player from the call channel
|
||||||
|
exports['pma-voice']:removePlayerFromCall()
|
||||||
|
```
|
|
@ -0,0 +1,12 @@
|
||||||
|
## removePlayerFromRadio
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Removes the player from the radio
|
||||||
|
|
||||||
|
## NOTE: This is just syntactic sugar for `setRadioChannel(0)`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Removes the player from the radio channel
|
||||||
|
exports['pma-voice']:removePlayerFromRadio()
|
||||||
|
```
|
|
@ -0,0 +1,25 @@
|
||||||
|
## setCallChannel | addPlayerToCall | SetCallChannel
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Sets the local players call channel.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **callChannel**: the call channel to join
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Joins call channel 1
|
||||||
|
exports['pma-voice']:setCallChannel(1)
|
||||||
|
|
||||||
|
-- This will remove them from the call channel
|
||||||
|
exports['pma-voice']:setCallChannel(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
addPlayerToCall is provided as a 'easier to read' version of setCallChannel.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Joins call channel 1
|
||||||
|
exports['pma-voice']:addPlayerToCall(1)
|
||||||
|
```
|
|
@ -0,0 +1,14 @@
|
||||||
|
## setCallVolume
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Sets the local players call channel volume
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **callVolume**: the call volume to set to between 0 - 100 percent
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- set the call volume to 50 percent
|
||||||
|
exports['pma-voice']:setCallVolume(50)
|
||||||
|
```
|
|
@ -0,0 +1,26 @@
|
||||||
|
## setRadioChannel | addPlayerToRadio | SetCallChannel
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Sets the local players radio channel.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **radioChannel**: the radio channel to join
|
||||||
|
|
||||||
|
## NOTE: If the player fails the server side radio channel check they will be reset to no channel.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Joins radio channel 1
|
||||||
|
exports['pma-voice']:setRadioChannel(1)
|
||||||
|
|
||||||
|
-- This will remove the player from all radio channels
|
||||||
|
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)
|
||||||
|
```
|
|
@ -0,0 +1,14 @@
|
||||||
|
## setRadioVolume
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Sets the local players radio channel volume
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **radioVolume**: the radio volume to set to between 0 - 100 percent
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- sets the radio volume to 50 percent
|
||||||
|
exports['pma-voice']:setRadioVolume(50)
|
||||||
|
```
|
|
@ -0,0 +1,17 @@
|
||||||
|
## setVoiceProperty | SetMumbleProperty | SetTokoProperty
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Sets the voice property, currently the only use is to enable/disable radios and radio clicks.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **property**: The property to set
|
||||||
|
* **value**: The value to set the property to
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Enable the radio
|
||||||
|
exports['pma-voice']:setVoiceProperty('radioEnabled', true)
|
||||||
|
-- Disable radio clicks
|
||||||
|
exports['pma-voice']:setVoiceProperty('micClicks', false)
|
||||||
|
```
|
3
resources/[voice]/pma-voice/docs/routingBuckets.md
Normal file
3
resources/[voice]/pma-voice/docs/routingBuckets.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## Routing Buckets
|
||||||
|
|
||||||
|
pma-voice natively supports routing buckets.
|
|
@ -0,0 +1,21 @@
|
||||||
|
## getPlayersInRadioChannel
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Gets a list of all of the players in the specified radio channel.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **radioChannel**: The channel to get all the members of
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
Returns a table of all of the players in the specified radio channel
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- this will return all of the current players in radio channel 1
|
||||||
|
local players = exports['pma-voice']:getPlayersInRadioChannel(1)
|
||||||
|
for source, isTalking in pairs(players) do
|
||||||
|
print(('%s is in radio channel 1, isTalking: %s'):format(GetPlayerName(source), isTalking))
|
||||||
|
end
|
||||||
|
```
|
|
@ -0,0 +1,22 @@
|
||||||
|
## addChannelCheck
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Adds a channel check to radio channels.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **channel**: The channel to add the check to.
|
||||||
|
* **function**: the function to call when the check is triggered, which should return a boolean of if the player is allowed to join the channel..
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Example for addChannelCheck
|
||||||
|
-- this always has to return true/false
|
||||||
|
exports['pma-voice']:addChannelCheck(1, function(source)
|
||||||
|
if IsPlayerAceAllowed(source, 'radio.police') then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
```
|
|
@ -0,0 +1,14 @@
|
||||||
|
## setPlayerCall
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Sets the players call channel.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **source**: The player to set the radio channel of
|
||||||
|
* **callChannel**: the radio channel to set the player to
|
||||||
|
|
||||||
|
```lua
|
||||||
|
exports['pma-voice']:setPlayerCall(source, 1)
|
||||||
|
```
|
|
@ -0,0 +1,14 @@
|
||||||
|
## setPlayerRadio
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Sets the players radio channel.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
* **source**: The player to set the radio channel of
|
||||||
|
* **radioChannel**: the radio channel to set the player to
|
||||||
|
|
||||||
|
```lua
|
||||||
|
exports['pma-voice']:setPlayerRadio(source, 1)
|
||||||
|
```
|
|
@ -0,0 +1,17 @@
|
||||||
|
## State Bag Getters/Setters
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
State bag getters are a little bit simpler, they just return the current value that is set in the state bag.
|
||||||
|
|
||||||
|
#### Note: If you're on the client and only using it on the current player, you can replace Player(source) with LocalPlayer
|
||||||
|
|
||||||
|
## Example for Proximity
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local plyState = Player(source).state
|
||||||
|
local proximity = plyState.proximity
|
||||||
|
print(proximity.index) -- prints the index of the proximity as seen in Cfg.voiceModes
|
||||||
|
print(proximity.distance) -- prints the distance of the proximity
|
||||||
|
print(proximity.mode) -- prints the mode name of the proximity
|
||||||
|
```
|
69
resources/[voice]/pma-voice/fxmanifest.lua
Normal file
69
resources/[voice]/pma-voice/fxmanifest.lua
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
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" },
|
||||||
|
}
|
||||||
|
}
|
139
resources/[voice]/pma-voice/server/main.lua
Normal file
139
resources/[voice]/pma-voice/server/main.lua
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
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)
|
94
resources/[voice]/pma-voice/server/module/phone.lua
Normal file
94
resources/[voice]/pma-voice/server/module/phone.lua
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
--- 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)
|
165
resources/[voice]/pma-voice/server/module/radio.lua
Normal file
165
resources/[voice]/pma-voice/server/module/radio.lua
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
local radioChecks = {}
|
||||||
|
|
||||||
|
--- checks if the player can join the channel specified
|
||||||
|
--- @param source number the source of the player
|
||||||
|
--- @param radioChannel number the channel they're trying to join
|
||||||
|
--- @return boolean if the user can join the channel
|
||||||
|
function canJoinChannel(source, radioChannel)
|
||||||
|
if radioChecks[radioChannel] then
|
||||||
|
return radioChecks[radioChannel](source)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- adds a check to the channel, function is expected to return a boolean of true or false
|
||||||
|
---@param channel number the channel to add a check to
|
||||||
|
---@param cb function the function to execute the check on
|
||||||
|
function addChannelCheck(channel, cb)
|
||||||
|
local channelType = type(channel)
|
||||||
|
local cbType = type(cb)
|
||||||
|
if channelType ~= "number" then
|
||||||
|
error(("'channel' expected 'number' got '%s'"):format(channelType))
|
||||||
|
end
|
||||||
|
if cbType ~= 'table' or not cb.__cfx_functionReference then
|
||||||
|
error(("'cb' expected 'function' got '%s'"):format(cbType))
|
||||||
|
end
|
||||||
|
radioChecks[channel] = cb
|
||||||
|
logger.info("%s added a check to channel %s", GetInvokingResource(), channel)
|
||||||
|
end
|
||||||
|
exports('addChannelCheck', addChannelCheck)
|
||||||
|
|
||||||
|
local function radioNameGetter_orig(source)
|
||||||
|
return GetPlayerName(source)
|
||||||
|
end
|
||||||
|
local radioNameGetter = radioNameGetter_orig
|
||||||
|
|
||||||
|
--- adds a check to the channel, function is expected to return a boolean of true or false
|
||||||
|
---@param cb function the function to execute the check on
|
||||||
|
function overrideRadioNameGetter(channel, cb)
|
||||||
|
local cbType = type(cb)
|
||||||
|
if cbType == 'table' and not cb.__cfx_functionReference then
|
||||||
|
error(("'cb' expected 'function' got '%s'"):format(cbType))
|
||||||
|
end
|
||||||
|
radioNameGetter = cb
|
||||||
|
logger.info("%s added a check to channel %s", GetInvokingResource(), channel)
|
||||||
|
end
|
||||||
|
exports('overrideRadioNameGetter', overrideRadioNameGetter)
|
||||||
|
|
||||||
|
--- adds a player to the specified radion channel
|
||||||
|
---@param source number the player to add to the channel
|
||||||
|
---@param radioChannel number the channel to set them to
|
||||||
|
function addPlayerToRadio(source, radioChannel)
|
||||||
|
if not canJoinChannel(source, radioChannel) then
|
||||||
|
-- remove the player from the radio client side
|
||||||
|
return TriggerClientEvent('pma-voice:removePlayerFromRadio', source, source)
|
||||||
|
end
|
||||||
|
logger.verbose('[radio] Added %s to radio %s', source, radioChannel)
|
||||||
|
|
||||||
|
-- check if the channel exists, if it does set the varaible to it
|
||||||
|
-- if not create it (basically if not radiodata make radiodata)
|
||||||
|
radioData[radioChannel] = radioData[radioChannel] or {}
|
||||||
|
local plyName = radioNameGetter(source)
|
||||||
|
for player, _ in pairs(radioData[radioChannel]) do
|
||||||
|
TriggerClientEvent('pma-voice:addPlayerToRadio', player, source, plyName)
|
||||||
|
end
|
||||||
|
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||||
|
voiceData[source].radio = radioChannel
|
||||||
|
radioData[radioChannel][source] = false
|
||||||
|
TriggerClientEvent('pma-voice:syncRadioData', source, radioData[radioChannel], GetConvarInt("voice_syncPlayerNames", 0) == 1 and plyName)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- removes a player from the specified channel
|
||||||
|
---@param source number the player to remove
|
||||||
|
---@param radioChannel number the current channel to remove them from
|
||||||
|
function removePlayerFromRadio(source, radioChannel)
|
||||||
|
logger.verbose('[radio] Removed %s from radio %s', source, radioChannel)
|
||||||
|
radioData[radioChannel] = radioData[radioChannel] or {}
|
||||||
|
for player, _ in pairs(radioData[radioChannel]) do
|
||||||
|
TriggerClientEvent('pma-voice:removePlayerFromRadio', player, source)
|
||||||
|
end
|
||||||
|
radioData[radioChannel][source] = nil
|
||||||
|
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||||
|
voiceData[source].radio = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: Implement this in a way that allows players to be on multiple channels
|
||||||
|
--- sets the players current radio channel
|
||||||
|
---@param source number the player to set the channel of
|
||||||
|
---@param _radioChannel number the radio channel to set them to (or 0 to remove them from radios)
|
||||||
|
function setPlayerRadio(source, _radioChannel)
|
||||||
|
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||||
|
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||||
|
local isResource = GetInvokingResource()
|
||||||
|
local plyVoice = voiceData[source]
|
||||||
|
local radioChannel = tonumber(_radioChannel)
|
||||||
|
if not radioChannel then
|
||||||
|
-- only full error if its sent from another server-side resource
|
||||||
|
if isResource then
|
||||||
|
error(("'radioChannel' expected 'number', got: %s"):format(type(_radioChannel)))
|
||||||
|
else
|
||||||
|
return logger.warn("%s sent a invalid radio, 'radioChannel' expected 'number', got: %s", source,type(_radioChannel))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if isResource then
|
||||||
|
-- got set in a export, need to update the client to tell them that their radio
|
||||||
|
-- changed
|
||||||
|
TriggerClientEvent('pma-voice:clSetPlayerRadio', source, radioChannel)
|
||||||
|
end
|
||||||
|
Player(source).state.radioChannel = radioChannel
|
||||||
|
if radioChannel ~= 0 and plyVoice.radio == 0 then
|
||||||
|
addPlayerToRadio(source, radioChannel)
|
||||||
|
elseif radioChannel == 0 then
|
||||||
|
removePlayerFromRadio(source, plyVoice.radio)
|
||||||
|
elseif plyVoice.radio > 0 then
|
||||||
|
removePlayerFromRadio(source, plyVoice.radio)
|
||||||
|
addPlayerToRadio(source, radioChannel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
exports('setPlayerRadio', setPlayerRadio)
|
||||||
|
|
||||||
|
RegisterNetEvent('pma-voice:setPlayerRadio', function(radioChannel)
|
||||||
|
setPlayerRadio(source, radioChannel)
|
||||||
|
end)
|
||||||
|
|
||||||
|
--- syncs the player talking across all radio members
|
||||||
|
---@param talking boolean sets if the palyer is talking.
|
||||||
|
function setTalkingOnRadio(talking)
|
||||||
|
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||||
|
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||||
|
local plyVoice = voiceData[source]
|
||||||
|
local radioTbl = radioData[plyVoice.radio]
|
||||||
|
if radioTbl then
|
||||||
|
radioTbl[source] = talking
|
||||||
|
logger.verbose('[radio] Set %s to talking: %s on radio %s',source, talking, plyVoice.radio)
|
||||||
|
for player, _ in pairs(radioTbl) do
|
||||||
|
if player ~= source then
|
||||||
|
TriggerClientEvent('pma-voice:setTalkingOnRadio', player, source, talking)
|
||||||
|
logger.verbose('[radio] Sync %s to let them know %s is %s',player, source, talking and 'talking' or 'not talking')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
RegisterNetEvent('pma-voice:setTalkingOnRadio', setTalkingOnRadio)
|
||||||
|
|
||||||
|
AddEventHandler("onResourceStop", function(resource)
|
||||||
|
for channel, cfxFunctionRef in pairs(radioChecks) do
|
||||||
|
local functionRef = cfxFunctionRef.__cfx_functionReference
|
||||||
|
local functionResource = string.match(functionRef, resource)
|
||||||
|
if functionResource then
|
||||||
|
radioChecks[channel] = nil
|
||||||
|
logger.warn('Channel %s had its radio check removed because the resource that gave the checks stopped', channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(radioNameGetter) == "table" then
|
||||||
|
local radioRef = radioNameGetter.__cfx_functionReference
|
||||||
|
if radioRef then
|
||||||
|
local isResource = string.match(functionRef, resource)
|
||||||
|
if isResource then
|
||||||
|
radioNameGetter = radioNameGetter_orig
|
||||||
|
logger.warn('Radio name getter is resetting to default because the resource that gave the cb got turned off')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end)
|
26
resources/[voice]/pma-voice/server/mute.js
Normal file
26
resources/[voice]/pma-voice/server/mute.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
let mutedPlayers = {}
|
||||||
|
// this is implemented in JS due to Lua's lack of a ClearTimeout
|
||||||
|
// muteply instead of mute because mute conflicts with rp-radio
|
||||||
|
RegisterCommand('muteply', (source, args) => {
|
||||||
|
const mutePly = parseInt(args[0])
|
||||||
|
const duration = parseInt(args[1]) || 900
|
||||||
|
if (mutePly && exports['pma-voice'].isValidPlayer(mutePly)) {
|
||||||
|
const isMuted = !MumbleIsPlayerMuted(mutePly);
|
||||||
|
Player(mutePly).state.muted = isMuted;
|
||||||
|
MumbleSetPlayerMuted(mutePly, isMuted);
|
||||||
|
emit('pma-voice:playerMuted', mutePly, source, isMuted, duration);
|
||||||
|
// since this is a toggle, if theres a mutedPlayers entry it can be assumed
|
||||||
|
// that they're currently muted, so we'll clear the timeout and unmute
|
||||||
|
if (mutedPlayers[mutePly]) {
|
||||||
|
clearTimeout(mutedPlayers[mutePly]);
|
||||||
|
MumbleSetPlayerMuted(mutePly, isMuted)
|
||||||
|
Player(mutePly).state.muted = isMuted;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutedPlayers[mutePly] = setTimeout(() => {
|
||||||
|
MumbleSetPlayerMuted(mutePly, !isMuted)
|
||||||
|
Player(mutePly).state.muted = !isMuted;
|
||||||
|
delete mutedPlayers[mutePly]
|
||||||
|
}, duration * 1000)
|
||||||
|
}
|
||||||
|
}, true)
|
93
resources/[voice]/pma-voice/shared.lua
Normal file
93
resources/[voice]/pma-voice/shared.lua
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
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
resources/[voice]/pma-voice/ui/css/app.css
Normal file
1
resources/[voice]/pma-voice/ui/css/app.css
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.voiceInfo{font-family:Avenir,Helvetica,Arial,sans-serif;position:fixed;text-align:right;bottom:5px;padding:0;right:5px;font-size:12px;font-weight:700;color:#949697;text-shadow:1.25px 0 0 #000,0 -1.25px 0 #000,0 1.25px 0 #000,-1.25px 0 0 #000}.talking{color:hsla(0,0%,100%,.822)}p{margin:0}
|
1
resources/[voice]/pma-voice/ui/index.html
Normal file
1
resources/[voice]/pma-voice/ui/index.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>voice-ui</title><link href="css/app.css" rel="preload" as="style"><link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but voice-ui doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/app.js"></script></body></html>
|
2
resources/[voice]/pma-voice/ui/js/app.js
Normal file
2
resources/[voice]/pma-voice/ui/js/app.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
(function(e){function o(o){for(var t,a,r=o[0],d=o[1],l=o[2],s=0,b=[];s<r.length;s++)a=r[s],Object.prototype.hasOwnProperty.call(i,a)&&i[a]&&b.push(i[a][0]),i[a]=0;for(t in d)Object.prototype.hasOwnProperty.call(d,t)&&(e[t]=d[t]);u&&u(o);while(b.length)b.shift()();return c.push.apply(c,l||[]),n()}function n(){for(var e,o=0;o<c.length;o++){for(var n=c[o],t=!0,r=1;r<n.length;r++){var d=n[r];0!==i[d]&&(t=!1)}t&&(c.splice(o--,1),e=a(a.s=n[0]))}return e}var t={},i={app:0},c=[];function a(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,a),n.l=!0,n.exports}a.m=e,a.c=t,a.d=function(e,o,n){a.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:n})},a.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,o){if(1&o&&(e=a(e)),8&o)return e;if(4&o&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(a.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var t in e)a.d(n,t,function(o){return e[o]}.bind(null,t));return n},a.n=function(e){var o=e&&e.__esModule?function(){return e["default"]}:function(){return e};return a.d(o,"a",o),o},a.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},a.p="";var r=window["webpackJsonp"]=window["webpackJsonp"]||[],d=r.push.bind(r);r.push=o,r=r.slice();for(var l=0;l<r.length;l++)o(r[l]);var u=d;c.push([0,"chunk-vendors"]),n()})({0:function(e,o,n){e.exports=n("56d7")},"0154":function(e,o,n){},"56d7":function(e,o,n){"use strict";n.r(o);var t=n("7edb");const i=Object(t["d"])("audio",{id:"audio_on",src:"mic_click_on.ogg"},null,-1),c=Object(t["d"])("audio",{id:"audio_off",src:"mic_click_off.ogg"},null,-1),a={key:0,class:"voiceInfo"};function r(e,o,n,r,d,l){return Object(t["f"])(),Object(t["c"])("body",null,[i,c,r.voice.uiEnabled?(Object(t["f"])(),Object(t["c"])("div",a,[0!==r.voice.callInfo?(Object(t["f"])(),Object(t["c"])("p",{key:0,class:Object(t["e"])({talking:r.voice.talking})}," [Call] ",2)):Object(t["b"])("",!0),r.voice.radioEnabled&&0!==r.voice.radioChannel?(Object(t["f"])(),Object(t["c"])("p",{key:1,class:Object(t["e"])({talking:r.voice.usingRadio})},Object(t["h"])(r.voice.radioChannel)+" Mhz [Radio] ",3)):Object(t["b"])("",!0),r.voice.voiceModes.length?(Object(t["f"])(),Object(t["c"])("p",{key:2,class:Object(t["e"])({talking:r.voice.talking})},Object(t["h"])(r.voice.voiceModes[r.voice.voiceMode][1])+" [Range] ",3)):Object(t["b"])("",!0)])):Object(t["b"])("",!0)])}var d={name:"App",setup(){const e=Object(t["g"])({uiEnabled:!0,voiceModes:[],voiceMode:0,radioChannel:0,radioEnabled:!0,usingRadio:!1,callInfo:0,talking:!1});return window.addEventListener("message",(function(o){const n=o.data;if(void 0!==n.uiEnabled&&(e.uiEnabled=n.uiEnabled),void 0!==n.voiceModes){e.voiceModes=JSON.parse(n.voiceModes);let o=[...e.voiceModes];o.push([0,"Custom"]),e.voiceModes=o}if(void 0!==n.voiceMode&&(e.voiceMode=n.voiceMode),void 0!==n.radioChannel&&(e.radioChannel=n.radioChannel),void 0!==n.radioEnabled&&(e.radioEnabled=n.radioEnabled),void 0!==n.callInfo&&(e.callInfo=n.callInfo),void 0!==n.usingRadio&&n.usingRadio!==e.usingRadio&&(e.usingRadio=n.usingRadio),void 0===n.talking||e.usingRadio||(e.talking=n.talking),n.sound&&e.radioEnabled&&0!==e.radioChannel){let e=document.getElementById(n.sound);e.load(),e.volume=n.volume,e.play().catch(e=>{})}})),fetch(`https://${GetParentResourceName()}/uiReady`,{method:"POST"}),{voice:e}}},l=(n("9253"),n("85dd")),u=n.n(l);const s=u()(d,[["render",r]]);var b=s;Object(t["a"])(b).mount("#app")},9253:function(e,o,n){"use strict";n("0154")}});
|
||||||
|
//# sourceMappingURL=app.js.map
|
1
resources/[voice]/pma-voice/ui/js/app.js.map
Normal file
1
resources/[voice]/pma-voice/ui/js/app.js.map
Normal file
File diff suppressed because one or more lines are too long
2
resources/[voice]/pma-voice/ui/js/chunk-vendors.js
Normal file
2
resources/[voice]/pma-voice/ui/js/chunk-vendors.js
Normal file
File diff suppressed because one or more lines are too long
1
resources/[voice]/pma-voice/ui/js/chunk-vendors.js.map
Normal file
1
resources/[voice]/pma-voice/ui/js/chunk-vendors.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/[voice]/pma-voice/ui/mic_click_off.ogg
Normal file
BIN
resources/[voice]/pma-voice/ui/mic_click_off.ogg
Normal file
Binary file not shown.
BIN
resources/[voice]/pma-voice/ui/mic_click_on.ogg
Normal file
BIN
resources/[voice]/pma-voice/ui/mic_click_on.ogg
Normal file
Binary file not shown.
3
resources/[voice]/pma-voice/voice-ui/.browserslistrc
Normal file
3
resources/[voice]/pma-voice/voice-ui/.browserslistrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
23
resources/[voice]/pma-voice/voice-ui/.gitignore
vendored
Normal file
23
resources/[voice]/pma-voice/voice-ui/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
24
resources/[voice]/pma-voice/voice-ui/README.md
Normal file
24
resources/[voice]/pma-voice/voice-ui/README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# voice-ui
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
yarn serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
resources/[voice]/pma-voice/voice-ui/babel.config.js
Normal file
5
resources/[voice]/pma-voice/voice-ui/babel.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
18
resources/[voice]/pma-voice/voice-ui/package.json
Normal file
18
resources/[voice]/pma-voice/voice-ui/package.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "voice-ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"@vue/compiler-sfc": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
6692
resources/[voice]/pma-voice/voice-ui/pnpm-lock.yaml
generated
Normal file
6692
resources/[voice]/pma-voice/voice-ui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
17
resources/[voice]/pma-voice/voice-ui/public/index.html
Normal file
17
resources/[voice]/pma-voice/voice-ui/public/index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
resources/[voice]/pma-voice/voice-ui/public/mic_click_off.ogg
Normal file
BIN
resources/[voice]/pma-voice/voice-ui/public/mic_click_off.ogg
Normal file
Binary file not shown.
BIN
resources/[voice]/pma-voice/voice-ui/public/mic_click_on.ogg
Normal file
BIN
resources/[voice]/pma-voice/voice-ui/public/mic_click_on.ogg
Normal file
Binary file not shown.
112
resources/[voice]/pma-voice/voice-ui/src/App.vue
Normal file
112
resources/[voice]/pma-voice/voice-ui/src/App.vue
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<body>
|
||||||
|
<audio id="audio_on" src="mic_click_on.ogg"></audio>
|
||||||
|
<audio id="audio_off" src="mic_click_off.ogg"></audio>
|
||||||
|
<div v-if="voice.uiEnabled" class="voiceInfo">
|
||||||
|
<p v-if="voice.callInfo !== 0" :class="{ talking: voice.talking }">
|
||||||
|
[Call]
|
||||||
|
</p>
|
||||||
|
<p v-if="voice.radioEnabled && voice.radioChannel !== 0" :class="{ talking: voice.usingRadio }">
|
||||||
|
{{ voice.radioChannel }} Mhz [Radio]
|
||||||
|
</p>
|
||||||
|
<p v-if="voice.voiceModes.length" :class="{ talking: voice.talking }">
|
||||||
|
{{ voice.voiceModes[voice.voiceMode][1] }} [Range]
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { reactive } from "vue";
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
setup() {
|
||||||
|
const voice = reactive({
|
||||||
|
uiEnabled: true,
|
||||||
|
voiceModes: [],
|
||||||
|
voiceMode: 0,
|
||||||
|
radioChannel: 0,
|
||||||
|
radioEnabled: true,
|
||||||
|
usingRadio: false,
|
||||||
|
callInfo: 0,
|
||||||
|
talking: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// stops from toggling voice at the end of talking
|
||||||
|
window.addEventListener("message", function(event) {
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (data.uiEnabled !== undefined) {
|
||||||
|
voice.uiEnabled = data.uiEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.voiceModes !== undefined) {
|
||||||
|
voice.voiceModes = JSON.parse(data.voiceModes);
|
||||||
|
// Push our own custom type for modes that have their range changed
|
||||||
|
let voiceModes = [...voice.voiceModes]
|
||||||
|
voiceModes.push([0.0, "Custom"])
|
||||||
|
voice.voiceModes = voiceModes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.voiceMode !== undefined) {
|
||||||
|
voice.voiceMode = data.voiceMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.radioChannel !== undefined) {
|
||||||
|
voice.radioChannel = data.radioChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.radioEnabled !== undefined) {
|
||||||
|
voice.radioEnabled = data.radioEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.callInfo !== undefined) {
|
||||||
|
voice.callInfo = data.callInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.usingRadio !== undefined && data.usingRadio !== voice.usingRadio) {
|
||||||
|
voice.usingRadio = data.usingRadio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((data.talking !== undefined) && !voice.usingRadio) {
|
||||||
|
voice.talking = data.talking;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.sound && voice.radioEnabled && voice.radioChannel !== 0) {
|
||||||
|
let click = document.getElementById(data.sound);
|
||||||
|
// discard these errors as its usually just a 'uncaught promise' from two clicks happening too fast.
|
||||||
|
click.load();
|
||||||
|
click.volume = data.volume;
|
||||||
|
click.play().catch((e) => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`https://${GetParentResourceName()}/uiReady`, { method: 'POST' });
|
||||||
|
|
||||||
|
return { voice };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.voiceInfo {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
position: fixed;
|
||||||
|
text-align: right;
|
||||||
|
bottom: 5px;
|
||||||
|
padding: 0;
|
||||||
|
right: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgb(148, 150, 151);
|
||||||
|
/* https://stackoverflow.com/questions/4772906/css-is-it-possible-to-add-a-black-outline-around-each-character-in-text */
|
||||||
|
text-shadow: 1.25px 0 0 #000, 0 -1.25px 0 #000, 0 1.25px 0 #000,
|
||||||
|
-1.25px 0 0 #000;
|
||||||
|
}
|
||||||
|
.talking {
|
||||||
|
color: rgba(255, 255, 255, 0.822);
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
4
resources/[voice]/pma-voice/voice-ui/src/main.js
Normal file
4
resources/[voice]/pma-voice/voice-ui/src/main.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
7
resources/[voice]/pma-voice/voice-ui/vue.config.js
Normal file
7
resources/[voice]/pma-voice/voice-ui/vue.config.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
publicPath: './',
|
||||||
|
productionSourceMap: true,
|
||||||
|
filenameHashing: false,
|
||||||
|
outputDir: "../ui",
|
||||||
|
|
||||||
|
}
|
|
@ -1,674 +0,0 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
|
@ -1,172 +0,0 @@
|
||||||
<!DOCTYPE HTML>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Salty Chat WebSocket</title>
|
|
||||||
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
|
|
||||||
</head>
|
|
||||||
<body style="display: none; position: absolute; top: 15vh; font-family:Arial; font-size:26px;
|
|
||||||
color:white; outline:thin; outline-color:black; text-shadow: 1px 1px 1px black">
|
|
||||||
<div id="demo">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let pluginAddress = "127.0.0.1:8088";
|
|
||||||
let isConnected = false;
|
|
||||||
let serverUniqueIdentifierFilter = null;
|
|
||||||
|
|
||||||
// Packet Stats
|
|
||||||
let packetsSent = 0;
|
|
||||||
let packetsReceived = 0;
|
|
||||||
let lastCommand = "";
|
|
||||||
|
|
||||||
function connect(address){
|
|
||||||
if (typeof address === "string"){
|
|
||||||
pluginAddress = address
|
|
||||||
|
|
||||||
console.log("new address: " + address);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("connecting...");
|
|
||||||
|
|
||||||
try{
|
|
||||||
window.webSocket = new window.WebSocket(`ws://${pluginAddress}/`);
|
|
||||||
}
|
|
||||||
catch{
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocket.onmessage = function (evt) {
|
|
||||||
let object = JSON.parse(evt.data);
|
|
||||||
if (typeof serverUniqueIdentifierFilter === "string")
|
|
||||||
{
|
|
||||||
if (object.ServerUniqueIdentifier === serverUniqueIdentifierFilter)
|
|
||||||
sendNuiData("SaltyChat_OnMessage", evt.data);
|
|
||||||
else if (typeof object.ServerUniqueIdentifier === "undefined")
|
|
||||||
sendNuiData("SaltyChat_OnError", evt.data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (typeof object.ServerUniqueIdentifier === "string")
|
|
||||||
sendNuiData("SaltyChat_OnMessage", evt.data);
|
|
||||||
else
|
|
||||||
sendNuiData("SaltyChat_OnError", evt.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
packetsReceived++;
|
|
||||||
updateHtml();
|
|
||||||
};
|
|
||||||
|
|
||||||
webSocket.onopen = function () {
|
|
||||||
isConnected = true;
|
|
||||||
|
|
||||||
sendNuiData("SaltyChat_OnConnected");
|
|
||||||
console.log("connected")
|
|
||||||
};
|
|
||||||
|
|
||||||
webSocket.onclose = function () {
|
|
||||||
isConnected = false;
|
|
||||||
|
|
||||||
sendNuiData("SaltyChat_OnDisconnected");
|
|
||||||
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWebSocketAddress(address)
|
|
||||||
{
|
|
||||||
if (typeof address === "string")
|
|
||||||
pluginAddress = address;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setServerUniqueIdentifierFilter(serverUniqueIdentifier)
|
|
||||||
{
|
|
||||||
if (typeof serverUniqueIdentifier === "string")
|
|
||||||
serverUniqueIdentifierFilter = serverUniqueIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runCommand(command)
|
|
||||||
{
|
|
||||||
// console.log(JSON.stringify(command), typeof command, isConnected)
|
|
||||||
if (!isConnected || typeof command !== "string")
|
|
||||||
{
|
|
||||||
lastCommand = "unexpected command";
|
|
||||||
updateHtml();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocket.send(command);
|
|
||||||
|
|
||||||
packetsSent++;
|
|
||||||
|
|
||||||
let cmdJson = JSON.parse(command)
|
|
||||||
if(cmdJson.Command == 9){
|
|
||||||
lastCommand = command;
|
|
||||||
updateHtml();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHtml()
|
|
||||||
{
|
|
||||||
// console.log(lastCommand)
|
|
||||||
$("#demo").html(`Last Command: ${lastCommand}</br>Packets Sent: ${packetsSent}</br>Packets Received ${packetsReceived}`);
|
|
||||||
// W I S E M A N
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendNuiData(event, data)
|
|
||||||
{
|
|
||||||
if (typeof data === "undefined")
|
|
||||||
{
|
|
||||||
$.post(`http://${GetParentResourceName()}/${event}`)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$.post(`http://${GetParentResourceName()}/${event}`, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showBody(show)
|
|
||||||
{
|
|
||||||
if (show)
|
|
||||||
{
|
|
||||||
$("body").show();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$("body").hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(function()
|
|
||||||
{
|
|
||||||
window.addEventListener("DOMContentLoaded", function(){
|
|
||||||
loaded = true
|
|
||||||
// W I S E
|
|
||||||
//connect();
|
|
||||||
updateHtml();
|
|
||||||
sendNuiData("SaltyChat_OnNuiReady");
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('message', function(event)
|
|
||||||
{
|
|
||||||
if (typeof event.data.Function === "string")
|
|
||||||
{
|
|
||||||
if (typeof event.data.Params === "undefined")
|
|
||||||
{
|
|
||||||
window[event.data.Function]();
|
|
||||||
}
|
|
||||||
else if (Array.isArray(event.data.Params) && event.data.Params.length == 1)
|
|
||||||
{
|
|
||||||
window[event.data.Function](event.data.Params[0]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
window[event.data.Function](event.data.Params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
// M A N
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,279 +0,0 @@
|
||||||
# Salty Chat in Lua for [FiveM](https://fivem.net/)
|
|
||||||
|
|
||||||
[](https://hits.seeyoufarm.com)
|
|
||||||
|
|
||||||
An example implementation of Salty Chat for [FiveM](https://fivem.net/) OneSync and OneSync Infinity.
|
|
||||||
|
|
||||||
Join the [Discord](https://gaming.v10networks.com/Discord) of @v10networkscom and start with [Salty Chat](https://gaming.v10networks.com/SaltyChat)!
|
|
||||||
|
|
||||||
Also checkout my [Discord](https://wise-scripts.vip/discord) for any questions about the Saltychat Lua version.
|
|
||||||
You can report bugs or make sugguestions via issues, or contribute via pull requests - we appreciate any contribution.
|
|
||||||
|
|
||||||
# Setup Steps
|
|
||||||
Before starting with the setup, make sure you have OneSync enabled and your server artifacts are up to date.
|
|
||||||
|
|
||||||
1. Download the latest [release](https://github.com/FirstWiseman/saltychat-fivem-lua/releases) and extract it into your resources
|
|
||||||
2. Add `start saltychat` in your `server.cfg`
|
|
||||||
3. Open `shared/Configuration.lua` and adjust the [variables](https://github.com/v10networkscom/saltychat-docs/blob/master/setup.md#config-variables)
|
|
||||||
```
|
|
||||||
"VoiceEnabled": true,
|
|
||||||
"ServerUniqueIdentifier": "NMjxHW5psWaLNmFh0+kjnQik7Qc=",
|
|
||||||
"MinimumPluginVersion": "",
|
|
||||||
"SoundPack": "default",
|
|
||||||
"IngameChannelId" : 25,
|
|
||||||
"IngameChannelPassword": "5V88FWWME615",
|
|
||||||
"SwissChannelIds": [ 61, 62 ],
|
|
||||||
```
|
|
||||||
4. (Optional) Change keybinds in `shared/Configuration.lua`, see [default values](https://github.com/FirstWiseman/saltychat-fivem-lua#keybinds) below
|
|
||||||
5. (Optional) Look into our recommended [TeamSpeak server settings](https://github.com/v10networkscom/saltychat-docs#teamspeak-server-settings)
|
|
||||||
|
|
||||||
**Attantion**: CFX team implemented a NUI blacklist and blocked local (`127.0.0.1` and `localhost`) WebSocket connections.
|
|
||||||
If the clientside can't connect to the WebSocket, make sure that you can resolve `lh.v10.network`:
|
|
||||||
1. Open `Windows Command Prompt` by searching `cmd`
|
|
||||||
2. Execute `nslookup lh.v10.network`
|
|
||||||
|
|
||||||
If it resolved to `127.0.0.1` then your issue is probably somewhere else, if not then you can use e.g. [Google DNS servers](https://developers.google.com/speed/public-dns/docs/using#addresses).
|
|
||||||
|
|
||||||
# Config
|
|
||||||
Variable | Type | Description
|
|
||||||
------------- | ------------- | -------------
|
|
||||||
VoiceRanges | `float[]` | Array of possible voice ranges
|
|
||||||
EnableVoiceRangeNotification | `bool` | Enables/disables a notification when chaning the voice range
|
|
||||||
VoiceRangeNotification | `string` | Text of the notification when changing the voice range, `{voicerange}` will be replaced by the voice range
|
|
||||||
IgnoreInvisiblePlayers | `bool` | Sets invisible players as distance culled to ignore them in proximity calculations
|
|
||||||
RadioType | `int` | Radio type which will be used for radio communication - [see possible values](https://github.com/v10networkscom/saltychat-docs/blob/master/enums.md#radio-type)
|
|
||||||
EnableRadioHardcoreMode | `bool` | Limits some radio functions like using the radio while swimming/diving and allows only one sender at a time
|
|
||||||
UltraShortRangeDistance | `float` | Maximum range of USR radio mode
|
|
||||||
ShortRangeDistance | `float` | Maximum range of SR radio mode
|
|
||||||
LongRangeDistace | `float` | Maximum range of LR radio mode
|
|
||||||
MegaphoneRange | `float` | Range of the megaphone (only available while driving a police car)
|
|
||||||
VariablePhoneDistortion | `bool` | Enables/disables variable phone distortion based on position of players
|
|
||||||
NamePattern | `string` | Naming schema of TeamSpeak clients, `{serverid}` will be replaced by the FiveM server ID of the client, `{playername}` by the name of the client and `{guid}` by a generated GUID
|
|
||||||
RequestTalkStates | `bool` | Enables/disables [TalkState's](https://github.com/v10networkscom/saltychat-docs/blob/master/commands.md#11--talkstate)
|
|
||||||
RequestRadioTrafficStates | `bool` | Enables/disables [RadioTrafficState's](https://github.com/v10networkscom/saltychat-docs/blob/master/commands.md#33--radiotrafficstate)
|
|
||||||
|
|
||||||
# Keybinds
|
|
||||||
Below are the default keybinds which will be written to your client config (`%appdata%\CitizenFX\fivem.cfg`).
|
|
||||||
Changing the default values wont change the values saved to your config.
|
|
||||||
Keybinds can be changed in game through the keybinding options of GTA V (`ESC` > `Settings` > `Key Bindings` > `FiveM`).
|
|
||||||
Default keybinds can be changed in `shared/Configuration.lua`, see [FiveM docs](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/) for possible values.
|
|
||||||
|
|
||||||
Variable | Description | Default
|
|
||||||
:---: | :---: | :---:
|
|
||||||
ToggleRange | Toggles voice range | F1
|
|
||||||
TalkPrimary | Talk on primary radio | N
|
|
||||||
TalkSecondary | Talk on secondary radio | Caps
|
|
||||||
TalkMegaphone | Use the Megaphone (only in police vehicles) | B
|
|
||||||
|
|
||||||
# Events
|
|
||||||
## Client
|
|
||||||
### SaltyChat_PluginStateChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
pluginState | `int` | Current state of the plugin (e.g. client is in a swiss channel), see [GameInstanceState](https://github.com/v10networkscom/saltychat-docs/blob/master/enums.md#game-instance-state) for possible values
|
|
||||||
|
|
||||||
### SaltyChat_TalkStateChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
isTalking | `bool` | `true` when player starts talking, `false` when the player stops talking
|
|
||||||
|
|
||||||
### SaltyChat_VoiceRangeChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
voiceRange | `float` | current voice range
|
|
||||||
index | `int` | index of the current voice range (starts at `0`)
|
|
||||||
availableVoiceRanges | `int` | count of available voice ranges
|
|
||||||
|
|
||||||
### SaltyChat_MicStateChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
isMicrophoneMuted | `bool` | `true` when player mutes mic, `false` when the player unmutes mic
|
|
||||||
|
|
||||||
### SaltyChat_MicEnabledChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
isMicrophoneEnabled | `bool` | `false` when player disabled mic, `true` when the player enabled mic
|
|
||||||
|
|
||||||
### SaltyChat_SoundStateChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
isSoundMuted | `bool` | `true` when player mutes sound, `false` when the player unmutes sound
|
|
||||||
|
|
||||||
### SaltyChat_SoundEnabledChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
isSoundEnabled | `bool` | `false` when player disabled sound, `true` when the player enabled sound
|
|
||||||
|
|
||||||
### SaltyChat_RadioChannelChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
radioChannel | `string` | Name of the radio channel, `null` if channel was left
|
|
||||||
isPrimaryChannel | `bool` | `true` when chanel is primary, `false` when secondary
|
|
||||||
|
|
||||||
### SaltyChat_RadioTrafficStateChanged
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
primaryReceive | `bool` | `true` when radio traffic is received on primary radio channel
|
|
||||||
primaryTransmit | `bool` | `true` when radio traffic is transmitted on primary radio channel
|
|
||||||
secondaryReceive | `bool` | `true` when radio traffic is received on secondary radio channel
|
|
||||||
secondaryTransmit | `bool` | `true` when radio traffic is transmitted on secondary radio channel
|
|
||||||
|
|
||||||
# Exports
|
|
||||||
## Client
|
|
||||||
### GetVoiceRange
|
|
||||||
Returns the current voice range as float.
|
|
||||||
|
|
||||||
### GetRadioChannel
|
|
||||||
Get the current radio channel.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
primary | `bool` | Whether to get the primary or secondary channel
|
|
||||||
|
|
||||||
### GetRadioVolume
|
|
||||||
Returns the current radio volume as float (0.0f - 1.6f).
|
|
||||||
|
|
||||||
### GetRadioSpeaker
|
|
||||||
Returns the current state of the radio speaker as bool (`true` speaker on, `false` speaker off).
|
|
||||||
|
|
||||||
### GetMicClick
|
|
||||||
Returns the current state of radio mic clicks as bool (`true` enabled, `false` disabled).
|
|
||||||
|
|
||||||
### SetRadioChannel
|
|
||||||
Set the current radio channel.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
radioChannelName | `string` | Name of the radio channel
|
|
||||||
primary | `bool` | Whether to set the primary or secondary channel
|
|
||||||
|
|
||||||
### SetRadioVolume
|
|
||||||
Adjust the radio's volume
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
volumeLevel | `float` | Overrides the volume in percent (0f - 1.6f / 0 - 160%)
|
|
||||||
|
|
||||||
### SetRadioSpeaker
|
|
||||||
Turn the radio speaker on (`true`) or off (`false`).
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
isRadioSpeakEnabled | `bool` | `true` to enable speaker, `false` to disable speaker
|
|
||||||
|
|
||||||
### SetMicClick
|
|
||||||
Turn radio mic clicks on (`true`) or off (`false`).
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
isMicClickEnabled | `bool` | `true` to enable mic clicks, `false` to disable mic clicks
|
|
||||||
|
|
||||||
## Server
|
|
||||||
### GetPlayerAlive
|
|
||||||
Returns player `IsAlive` flag as `bool`.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
netId | `int` | Server ID of the player
|
|
||||||
|
|
||||||
### SetPlayerAlive
|
|
||||||
Sets player `IsAlive` flag.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
netId | `int` | Server ID of the player
|
|
||||||
isAlive | `bool` | `true` if player is alive, otherwise `false`
|
|
||||||
|
|
||||||
### GetPlayerVoiceRange
|
|
||||||
Returns player voice range as `float`.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
netId | `int` | Server ID of the player
|
|
||||||
|
|
||||||
### SetPlayerVoiceRange
|
|
||||||
Sets player voice range.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
netId | `int` | Server ID of the player
|
|
||||||
voiceRange | `float` | Voice range that should be set
|
|
||||||
|
|
||||||
### AddPlayerToCall
|
|
||||||
Adds a player to a call, creates call if it doesn't exist.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
callIdentifier | `string` | Identifier of the call
|
|
||||||
playerHandle | `int` | Server ID of the player
|
|
||||||
|
|
||||||
### AddPlayersToCall
|
|
||||||
Adds an array of players to a call, creates call if it doesn't exist.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
callIdentifier | `string` | Identifier of the call
|
|
||||||
playerHandles | `int[]` | Server IDs of the players
|
|
||||||
|
|
||||||
### RemovePlayerFromCall
|
|
||||||
Removes a player from a call.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
callIdentifier | `string` | Identifier of the call
|
|
||||||
playerHandle | `int` | Server ID of the player
|
|
||||||
|
|
||||||
### RemovePlayersFromCall
|
|
||||||
Removes an array of players from a call.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
callIdentifier | `string` | Identifier of the call
|
|
||||||
playerHandles | `int[]` | Server IDs of the players
|
|
||||||
|
|
||||||
### SetPhoneSpeaker
|
|
||||||
Turns phone speaker of an player on/off.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
playerHandle | `int` | Server ID of the player
|
|
||||||
toggle | `bool` | `true` to turn on speaker, `false` to turn it off
|
|
||||||
|
|
||||||
### SetPlayerRadioSpeaker
|
|
||||||
Turns radio speaker of an player on/off.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
netId | `int` | Server ID of the player
|
|
||||||
toggle | `bool` | `true` to turn on speaker, `false` to turn it off
|
|
||||||
|
|
||||||
### GetPlayersInRadioChannel
|
|
||||||
Returns an `int` array with all player handles that are members of the specified radio channel.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
radioChannelName | `string` | Name of the radio channel
|
|
||||||
|
|
||||||
### SetPlayerRadioChannel
|
|
||||||
Sets a player's radio channel.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
netId | `int` | Server ID of the player
|
|
||||||
radioChannelName | `string` | Name of the radio channel
|
|
||||||
isPrimary | `bool` | `true` to set the channel as primary, `false` to set it as secondary
|
|
||||||
|
|
||||||
### RemovePlayerRadioChannel
|
|
||||||
Removes a player from the radio channel.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
netId | `int` | Server ID of the player
|
|
||||||
radioChannelName | `string` | Name of the radio channel
|
|
||||||
|
|
||||||
### SetRadioTowers
|
|
||||||
Sets the radio towers.
|
|
||||||
|
|
||||||
Parameter | Type | Description
|
|
||||||
------------ | ------------- | -------------
|
|
||||||
towers | `float[][]` | Array with radio tower positions and ranges (X, Y, Z, range)
|
|
|
@ -1,8 +0,0 @@
|
||||||
---@class NuiEvent
|
|
||||||
NuiEvent = {
|
|
||||||
SaltyChat_OnConnected = "SaltyChat_OnConnected";
|
|
||||||
SaltyChat_OnDisconnected = "SaltyChat_OnDisconnected";
|
|
||||||
SaltyChat_OnMessage = "SaltyChat_OnMessage";
|
|
||||||
SaltyChat_OnError = "SaltyChat_OnError";
|
|
||||||
SaltyChat_OnNuiReady = "SaltyChat_OnNuiReady";
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
---@class Util
|
|
||||||
Util = {
|
|
||||||
-- #region Player Extensions
|
|
||||||
---@param netid integer
|
|
||||||
---@return string
|
|
||||||
GetTeamSpeakName = function (netid)
|
|
||||||
--- WHERE TO GET FROM????
|
|
||||||
return Player(netid).state[State.SaltyChat_TeamSpeakName]
|
|
||||||
end,
|
|
||||||
|
|
||||||
---@param netid integer
|
|
||||||
---@return number
|
|
||||||
GetVoiceRange = function (netid)
|
|
||||||
return Player(netid).state[State.SaltyChat_VoiceRange] or 0.0
|
|
||||||
end,
|
|
||||||
|
|
||||||
---@param netid integer
|
|
||||||
---@return boolean
|
|
||||||
GetIsAlive = function (netid)
|
|
||||||
return Player(netid).state[State.SaltyChat_IsAlive] == true
|
|
||||||
end,
|
|
||||||
-- #endregion
|
|
||||||
|
|
||||||
-- #region Vehicle Extensions
|
|
||||||
---@param vehicle Vehicle
|
|
||||||
---@return boolean
|
|
||||||
HasOpening = function (vehicle)
|
|
||||||
if type(vehicle) ~= "table" then return nil end
|
|
||||||
|
|
||||||
local doors = vehicle.Doors
|
|
||||||
return doors.Length == 0 or table.any(doors.GetAll(), function (d)
|
|
||||||
return d.Index ~= VehicleDoorIndex.Hood and (d.IsBroken or d.IsOpen)
|
|
||||||
end) or not vehicle.Windows.AreAllIntact or table.any(vehicle.Windows.GetAllWindows(), function (a)
|
|
||||||
return not a.Intact
|
|
||||||
end) or (vehicle.IsConvertible and vehicle.RoofState ~= VehicleRoofState.Closed)
|
|
||||||
end
|
|
||||||
-- #endregion
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,38 +0,0 @@
|
||||||
---@enum Command
|
|
||||||
Command = {
|
|
||||||
-- Plugin
|
|
||||||
PluginState = 0,
|
|
||||||
|
|
||||||
-- Instance
|
|
||||||
Initiate = 1,
|
|
||||||
Reset = 2,
|
|
||||||
Ping = 3,
|
|
||||||
Pong = 4,
|
|
||||||
InstanceState = 5,
|
|
||||||
SoundState = 6,
|
|
||||||
SelfStateUpdate = 7,
|
|
||||||
PlayerStateUpdate = 8,
|
|
||||||
BulkUpdate = 9,
|
|
||||||
RemovePlayer = 10,
|
|
||||||
TalkState = 11,
|
|
||||||
PlaySound = 18,
|
|
||||||
StopSound = 19,
|
|
||||||
|
|
||||||
-- Phone
|
|
||||||
PhoneCommunicationUpdate = 20,
|
|
||||||
StopPhoneCommunication = 21,
|
|
||||||
|
|
||||||
-- Radio
|
|
||||||
RadioCommunicationUpdate = 30,
|
|
||||||
StopRadioCommunication = 31,
|
|
||||||
RadioTowerUpdate = 32,
|
|
||||||
RadioTrafficState = 33,
|
|
||||||
|
|
||||||
AddRadioChannelMember = 37,
|
|
||||||
UpdateRadioChannelMembers = 38,
|
|
||||||
RemoveRadioChannelMember = 39,
|
|
||||||
|
|
||||||
-- Megaphone
|
|
||||||
MegaphoneCommunicationUpdate = 40,
|
|
||||||
StopMegaphoneCommunication = 41,
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
---@enum Error
|
|
||||||
Error = {
|
|
||||||
OK = 0,
|
|
||||||
InvalidJson = 1,
|
|
||||||
NotConnectedToServer = 2,
|
|
||||||
AlreadyInGame = 3,
|
|
||||||
ChannelNotAvailable = 4,
|
|
||||||
NameNotAvailable = 5,
|
|
||||||
InvalidValue = 6,
|
|
||||||
|
|
||||||
ServerBlacklisted = 100,
|
|
||||||
ServerUnderlicensed = 101
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
---@enum RadioType
|
|
||||||
RadioType = {
|
|
||||||
---<summary>
|
|
||||||
---No radio communication
|
|
||||||
---</summary>
|
|
||||||
None = 1,
|
|
||||||
|
|
||||||
---<summary>
|
|
||||||
---Short range radio communication - appx. 3 kilometers
|
|
||||||
---</summary>
|
|
||||||
ShortRange = 2,
|
|
||||||
|
|
||||||
---<summary>
|
|
||||||
---Long range radio communication - appx. 8 kilometers
|
|
||||||
---</summary>
|
|
||||||
LongRange = 4,
|
|
||||||
|
|
||||||
---<summary>
|
|
||||||
---Distributed radio communication, depending on <see cref="RadioTower"/> - appx. 1.8 (ultra short range), appx. 3 (short range) or 8 (long range) kilometers
|
|
||||||
---</summary>
|
|
||||||
Distributed = 8,
|
|
||||||
|
|
||||||
---<summary>
|
|
||||||
---Ultra Short range radio communication - appx. 1.8 kilometers
|
|
||||||
---</summary>
|
|
||||||
UltraShortRange = 16,
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
GameInstance = {}
|
|
||||||
GameInstance.__index = GameInstance
|
|
||||||
|
|
||||||
---@param serverUniqueIdentifier string
|
|
||||||
---@param name string
|
|
||||||
---@param channelId number
|
|
||||||
---@param channelPassword string
|
|
||||||
---@param soundPack string
|
|
||||||
---@param swissChannels number[]
|
|
||||||
---@param sendTalkStates boolean
|
|
||||||
---@param sendRadioTrafficStates boolean
|
|
||||||
---@param ultraShortRangeDistance number
|
|
||||||
---@param shortRangeDistance number
|
|
||||||
---@param longRangeDistace number
|
|
||||||
---@return table
|
|
||||||
function GameInstance.new(serverUniqueIdentifier, name, channelId, channelPassword, soundPack, swissChannels, sendTalkStates, sendRadioTrafficStates, ultraShortRangeDistance, shortRangeDistance, longRangeDistace)
|
|
||||||
local self = setmetatable({}, GameInstance)
|
|
||||||
self.ServerUniqueIdentifier = serverUniqueIdentifier
|
|
||||||
self.Name = name
|
|
||||||
self.ChannelId = channelId
|
|
||||||
self.ChannelPassword = channelPassword
|
|
||||||
self.SoundPack = soundPack
|
|
||||||
self.SwissChannelIds = swissChannels
|
|
||||||
self.SendTalkStates = sendTalkStates
|
|
||||||
self.SendRadioTrafficStates = sendRadioTrafficStates
|
|
||||||
self.UltraShortRangeDistance = ultraShortRangeDistance
|
|
||||||
self.ShortRangeDistance = shortRangeDistance
|
|
||||||
self.LongRangeDistace = longRangeDistace
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,18 +0,0 @@
|
||||||
---@enum GameInstanceState
|
|
||||||
GameInstanceState = {
|
|
||||||
NotInitiated = -1,
|
|
||||||
NotConnected = 0,
|
|
||||||
Connected = 1,
|
|
||||||
Ingame = 2,
|
|
||||||
InSwissChannel = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class InstanceState
|
|
||||||
---@field IsConnectedToServer boolean
|
|
||||||
---@field IsReady boolean
|
|
||||||
---@field State GameInstanceState
|
|
||||||
InstanceState = {
|
|
||||||
IsConnectedToServer = nil,
|
|
||||||
IsReady = nil,
|
|
||||||
State = nil
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
---@class MegaphoneCommunication
|
|
||||||
---@field Name string
|
|
||||||
---@field Range number
|
|
||||||
---@field Volume number?
|
|
||||||
MegaphoneCommunication = {}
|
|
||||||
MegaphoneCommunication.__index = MegaphoneCommunication
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
---@param range number
|
|
||||||
---@param volume number?
|
|
||||||
---@return MegaphoneCommunication
|
|
||||||
function MegaphoneCommunication.new(name, range, volume)
|
|
||||||
local self = setmetatable({}, MegaphoneCommunication)
|
|
||||||
self.Name = name
|
|
||||||
self.Range = range
|
|
||||||
self.Volume = volume or nil
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return boolean
|
|
||||||
function MegaphoneCommunication:ShouldSerializeVolume()
|
|
||||||
return self.Volume ~= nil
|
|
||||||
end
|
|
|
@ -1,34 +0,0 @@
|
||||||
---@class PhoneCommunication
|
|
||||||
---@field Name string
|
|
||||||
---@field SignalStrength integer?
|
|
||||||
---@field Volume number?
|
|
||||||
---@field Direct boolean
|
|
||||||
---@field RelayedBy string[]
|
|
||||||
PhoneCommunication = {}
|
|
||||||
PhoneCommunication.__index = PhoneCommunication
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
---@param signalStrength integer?
|
|
||||||
---@param volume number?
|
|
||||||
---@param direct boolean?
|
|
||||||
---@param relayedBy string[]?
|
|
||||||
---@return PhoneCommunication
|
|
||||||
function PhoneCommunication.new(name, signalStrength, volume, direct, relayedBy)
|
|
||||||
local self = setmetatable({}, PhoneCommunication)
|
|
||||||
self.Name = name
|
|
||||||
self.SignalStrength = signalStrength
|
|
||||||
self.Volume = volume
|
|
||||||
|
|
||||||
if direct then
|
|
||||||
self.Direct = direct
|
|
||||||
else
|
|
||||||
self.Direct = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if relayedBy then
|
|
||||||
self.RelayedBy = relayedBy
|
|
||||||
else
|
|
||||||
self.RelayedBy = {}
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,54 +0,0 @@
|
||||||
---@class ClientPlayer
|
|
||||||
---@field Handle integer
|
|
||||||
---@field Name string
|
|
||||||
---@field State table
|
|
||||||
---@field ServerId integer
|
|
||||||
---@field Character PlayerPed
|
|
||||||
---@field GetIsAlive fun(): boolean
|
|
||||||
ClientPlayer = {}
|
|
||||||
ClientPlayer.__index = ClientPlayer
|
|
||||||
|
|
||||||
function ClientPlayer.new(playerIndex)
|
|
||||||
local self = setmetatable({}, ClientPlayer)
|
|
||||||
self.Handle = playerIndex
|
|
||||||
self.Name = GetPlayerName(playerIndex)
|
|
||||||
self.State = {}
|
|
||||||
self.ServerId = GetPlayerServerId(playerIndex)
|
|
||||||
self.Character = PlayerPed.new(playerIndex)
|
|
||||||
self.GetIsAlive = function ()
|
|
||||||
return not IsPlayerDead(playerIndex)
|
|
||||||
end
|
|
||||||
|
|
||||||
setmetatable(self.State, {
|
|
||||||
__index = function (list, key)
|
|
||||||
return Player(self.ServerId).state[key]
|
|
||||||
end,
|
|
||||||
|
|
||||||
__newindex = function (list, key, value)
|
|
||||||
Player(self.ServerId).state:set(key, value, true)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return table<integer, ClientPlayer>
|
|
||||||
function GetServerPlayers()
|
|
||||||
local playersKnownToClient = {}
|
|
||||||
for _, playerIndex in pairs(GetActivePlayers()) do
|
|
||||||
local player = ClientPlayer.new(playerIndex)
|
|
||||||
playersKnownToClient[player.ServerId] = player
|
|
||||||
end
|
|
||||||
|
|
||||||
return playersKnownToClient
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param serverId integer
|
|
||||||
---@return ClientPlayer
|
|
||||||
function GetPlayer(serverId)
|
|
||||||
local players = GetServerPlayers()
|
|
||||||
return players[serverId]
|
|
||||||
end
|
|
||||||
|
|
||||||
---@alias GamePlayer
|
|
||||||
GamePlayer = ClientPlayer.new(PlayerId())
|
|
|
@ -1,78 +0,0 @@
|
||||||
---@class PlayerPed
|
|
||||||
---@field Handle integer
|
|
||||||
---@field Position vector3
|
|
||||||
---@field CurrentVehicle Vehicle
|
|
||||||
---@field IsInPoliceVehicle boolean
|
|
||||||
---@field IsSwimmingUnderWater boolean
|
|
||||||
---@field IsSwimming boolean
|
|
||||||
---@field IsVisible boolean
|
|
||||||
---@field PlayAnimation fun(animDic: string, anim: string, blendInSpeed: number, blendOutSpeed: number, duration: integer, flag: integer)
|
|
||||||
---@field ClearTasks fun()
|
|
||||||
---@field StopAnim fun(animDic: string, anim: string, exitSpeed: number)
|
|
||||||
PlayerPed = {}
|
|
||||||
PlayerPed.__index = PlayerPed
|
|
||||||
|
|
||||||
function PlayerPed.new(playerIndex)
|
|
||||||
local self = setmetatable({}, PlayerPed)
|
|
||||||
local metatable = {
|
|
||||||
__index = function(list, key)
|
|
||||||
if list.ped[key] and type(list.ped[key]) == "function" then
|
|
||||||
return list.ped[key]()
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
setmetatable(self, metatable)
|
|
||||||
|
|
||||||
self.ped = {}
|
|
||||||
self.ped.Handle = function ()
|
|
||||||
return GetPlayerPed(playerIndex)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ped.Position = function ()
|
|
||||||
local x, y, z = table.unpack(GetEntityCoords(self.Handle))
|
|
||||||
return TSVector.new(x, y, z)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ped.CurrentVehicle = function ()
|
|
||||||
local vehicleHandle = GetVehiclePedIsIn(self.Handle, false)
|
|
||||||
local vehicle = Vehicle.new(vehicleHandle)
|
|
||||||
return (vehicleHandle ~= 0 and vehicle) or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ped.IsInPoliceVehicle = function ()
|
|
||||||
return IsPedInAnyPoliceVehicle(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ped.IsSwimmingUnderWater = function ()
|
|
||||||
return IsPedSwimmingUnderWater(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ped.IsSwimming = function ()
|
|
||||||
return IsPedSwimming(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ped.IsVisible = function ()
|
|
||||||
return IsEntityVisible(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.PlayAnimation = function (animDic, anim, blendInSpeed, blendOutSpeed, duration, flag)
|
|
||||||
while (not HasAnimDictLoaded(animDic)) do
|
|
||||||
RequestAnimDict(animDic)
|
|
||||||
Wait(5)
|
|
||||||
end
|
|
||||||
|
|
||||||
TaskPlayAnim(self.Handle, animDic, anim, blendInSpeed, blendOutSpeed, duration, flag, 0, false, false, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ClearTasks = function ()
|
|
||||||
ClearPedTasks(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.StopAnim = function(animDic, anim, exitSpeed)
|
|
||||||
StopAnimTask(self.Handle, animDic, anim, exitSpeed)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,124 +0,0 @@
|
||||||
-- #region Sub Classes
|
|
||||||
---@class EchoEffect
|
|
||||||
---@field Duration integer
|
|
||||||
---@field Rolloff float
|
|
||||||
---@field Delay integer
|
|
||||||
EchoEffect = {}
|
|
||||||
EchoEffect.__index = EchoEffect
|
|
||||||
|
|
||||||
---@param duration integer
|
|
||||||
---@param rolloff number
|
|
||||||
---@param delay integer
|
|
||||||
---@return EchoEffect
|
|
||||||
function EchoEffect.new(duration, rolloff, delay)
|
|
||||||
local self = setmetatable({}, EchoEffect)
|
|
||||||
self.Duration = duration or 100
|
|
||||||
self.Rolloff = rolloff or 0.3
|
|
||||||
self.Delay = delay or 250
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
-- #endregion
|
|
||||||
|
|
||||||
-- #region SelfState
|
|
||||||
---@class SelfState
|
|
||||||
---@field Position TSVectorStruc
|
|
||||||
---@field Rotation number
|
|
||||||
---@field VoiceRange number
|
|
||||||
---@field IsAlive boolean
|
|
||||||
---@field Echo EchoEffect
|
|
||||||
SelfState = {}
|
|
||||||
SelfState.__index = SelfState
|
|
||||||
|
|
||||||
function SelfState.new(positiion, rotation, voiceRange, isAlive, echo)
|
|
||||||
if not echo then echo = false end
|
|
||||||
local self = setmetatable({}, SelfState)
|
|
||||||
self.Position = positiion
|
|
||||||
self.Rotation = rotation
|
|
||||||
self.VoiceRange = voiceRange
|
|
||||||
self.IsAlive = isAlive
|
|
||||||
|
|
||||||
if echo then
|
|
||||||
self.Echo = EchoEffect.new()
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
-- #endregion
|
|
||||||
|
|
||||||
-- #region Sub Classes
|
|
||||||
---@class MuffleEffect
|
|
||||||
---@field Intensity integer
|
|
||||||
MuffleEffect = {}
|
|
||||||
MuffleEffect.__index = MuffleEffect
|
|
||||||
|
|
||||||
---@param intensity integer
|
|
||||||
---@return MuffleEffect
|
|
||||||
function MuffleEffect.new(intensity)
|
|
||||||
local self = setmetatable({}, MuffleEffect)
|
|
||||||
self.Intensity = intensity
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
-- #endregion
|
|
||||||
|
|
||||||
-- #region PlayerState
|
|
||||||
---@class PlayerState
|
|
||||||
---@field Name string
|
|
||||||
---@field Position TSVectorStruc
|
|
||||||
---@field VoiceRange number
|
|
||||||
---@field IsAlive boolean
|
|
||||||
---@field VolumeOverride number?
|
|
||||||
---@field DistanceCulled boolean
|
|
||||||
---@field Muffle MuffleEffect
|
|
||||||
PlayerState = {}
|
|
||||||
PlayerState.__index = PlayerState
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
---@param position vector3
|
|
||||||
---@param voiceRange number
|
|
||||||
---@param isAlive boolean
|
|
||||||
---@param volumeOverride number
|
|
||||||
---@param distanceCulled boolean
|
|
||||||
---@param muffleIntensity integer?
|
|
||||||
---@return PlayerState
|
|
||||||
function PlayerState.new(name, position, voiceRange, isAlive, distanceCulled, muffleIntensity, volumeOverride)
|
|
||||||
local self = setmetatable({}, PlayerState)
|
|
||||||
self.Name = name;
|
|
||||||
self.Position = position
|
|
||||||
self.VoiceRange = voiceRange or nil;
|
|
||||||
self.IsAlive = isAlive or nil;
|
|
||||||
self.DistanceCulled = distanceCulled or false;
|
|
||||||
|
|
||||||
if volumeOverride then
|
|
||||||
if volumeOverride > 1.6 then
|
|
||||||
self.VolumeOverride = 1.6
|
|
||||||
elseif volumeOverride < 0.0 then
|
|
||||||
self.VolumeOverride = 0.0
|
|
||||||
else
|
|
||||||
self.VolumeOverride = volumeOverride
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if muffleIntensity then
|
|
||||||
self.Muffle = MuffleEffect.new(muffleIntensity)
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
-- #endregion
|
|
||||||
|
|
||||||
-- #region BulkUpdate
|
|
||||||
---@class BulkUpdate
|
|
||||||
---@field PlayerStates PlayerState[]
|
|
||||||
---@field SelfState SelfState
|
|
||||||
BulkUpdate = {}
|
|
||||||
BulkUpdate.__index = BulkUpdate
|
|
||||||
|
|
||||||
---@param playerStates PlayerState[]
|
|
||||||
---@param selfState SelfState
|
|
||||||
---@return BulkUpdate
|
|
||||||
function BulkUpdate.new(playerStates, selfState)
|
|
||||||
local self = setmetatable({}, BulkUpdate)
|
|
||||||
self.PlayerStates = playerStates
|
|
||||||
self.SelfState = selfState
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
-- #endregion
|
|
|
@ -1,66 +0,0 @@
|
||||||
---@class PluginCommand
|
|
||||||
---@field Command Command
|
|
||||||
---@field ServerUniqueIdentifier string
|
|
||||||
---@field Parameter table
|
|
||||||
PluginCommand = {}
|
|
||||||
PluginCommand.__index = PluginCommand
|
|
||||||
|
|
||||||
---@param command Command?
|
|
||||||
---@param serverUniqueIdentifier string
|
|
||||||
---@param parameter table?
|
|
||||||
---@return PluginCommand
|
|
||||||
function PluginCommand.new(command, serverUniqueIdentifier, parameter)
|
|
||||||
local self = setmetatable({}, PluginCommand)
|
|
||||||
self.Command = command or Command.Pong
|
|
||||||
|
|
||||||
-- Logger:Debug("[New PluginCommand]", serverUniqueIdentifier, parameter)
|
|
||||||
if type(serverUniqueIdentifier) == "string" then
|
|
||||||
self.ServerUniqueIdentifier = serverUniqueIdentifier
|
|
||||||
self.Parameter = json.decode(json.encode(parameter))
|
|
||||||
else
|
|
||||||
self.Parameter = json.decode(json.encode(serverUniqueIdentifier))
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--#region Methodes
|
|
||||||
---@param pluginCommand PluginCommand
|
|
||||||
---@return string
|
|
||||||
function PluginCommand.Serialize(pluginCommand)
|
|
||||||
return json.encode({
|
|
||||||
pluginCommand.Command,
|
|
||||||
pluginCommand.ServerUniqueIdentifier,
|
|
||||||
pluginCommand.Parameter
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param obj table
|
|
||||||
function PluginCommand.Deserialize(obj)
|
|
||||||
-- Logger:Debug("[PluginCommand] Deserialize", obj)
|
|
||||||
if type(obj) == "string" then
|
|
||||||
obj = json.decode(obj)
|
|
||||||
end
|
|
||||||
-- Logger:Debug("[PluginCommand] Deserialize Encode", json.encode(obj))
|
|
||||||
|
|
||||||
return PluginCommand.new(obj.Command, obj.ServerUniqueIdentifier, obj.Parameter or nil)
|
|
||||||
end
|
|
||||||
--#endregion
|
|
||||||
|
|
||||||
-- TryGetPayload NEEDED ???
|
|
||||||
-- C#
|
|
||||||
-- public bool TryGetPayload<T>(out T payload)
|
|
||||||
-- {
|
|
||||||
-- try
|
|
||||||
-- {
|
|
||||||
-- payload = this.Parameter.ToObject<T>();
|
|
||||||
|
|
||||||
-- return true;
|
|
||||||
-- }
|
|
||||||
-- catch
|
|
||||||
-- {
|
|
||||||
-- // do nothing
|
|
||||||
-- }
|
|
||||||
|
|
||||||
-- payload = default;
|
|
||||||
-- return false;
|
|
||||||
-- }
|
|
|
@ -1,26 +0,0 @@
|
||||||
---@class PluginError
|
|
||||||
---@field Error Error
|
|
||||||
---@field Message string
|
|
||||||
---@field ServerIdentifier string
|
|
||||||
PluginError = {}
|
|
||||||
PluginError.__index = PluginError
|
|
||||||
|
|
||||||
---@param error Error
|
|
||||||
---@param message string
|
|
||||||
---@param serverIdentifier string
|
|
||||||
---@return PluginError
|
|
||||||
function PluginError.new(error, message, serverIdentifier)
|
|
||||||
local self = setmetatable({}, PluginError)
|
|
||||||
self.Error = error
|
|
||||||
self.Message = message
|
|
||||||
self.ServerIdentifier = serverIdentifier
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param obj table
|
|
||||||
---@return PluginError
|
|
||||||
function PluginError.Deserialize(obj)
|
|
||||||
if type(obj) == "string" then obj = json.decode(jsonString) end
|
|
||||||
|
|
||||||
return PluginError.new(obj.Error, obj.Message, obj.ServerIdentifier)
|
|
||||||
end
|
|
|
@ -1,7 +0,0 @@
|
||||||
---@class PluginState
|
|
||||||
---@field Version string
|
|
||||||
---@field ActiveInstances integer
|
|
||||||
PluginState = {
|
|
||||||
Version = nil,
|
|
||||||
ActiveInstances = nil
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
---@class RadioTower
|
|
||||||
---@field Towers Tower[]
|
|
||||||
RadioTower = {}
|
|
||||||
RadioTower.__index = RadioTower
|
|
||||||
|
|
||||||
---@param towers Tower[]
|
|
||||||
---@return RadioTower
|
|
||||||
function RadioTower.new(towers)
|
|
||||||
local self = setmetatable({}, RadioTower)
|
|
||||||
self.Towers = towers
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class Tower
|
|
||||||
---@field X number
|
|
||||||
---@field Y number
|
|
||||||
---@field Z number
|
|
||||||
---@field Range number
|
|
||||||
Tower = {}
|
|
||||||
Tower.__index = Tower
|
|
||||||
---@param x number
|
|
||||||
---@param y number
|
|
||||||
---@param z number
|
|
||||||
---@param range number?
|
|
||||||
---@return Tower
|
|
||||||
function Tower.new(x, y, z, range)
|
|
||||||
local self = setmetatable({}, Tower)
|
|
||||||
self.X = x
|
|
||||||
self.Y = y
|
|
||||||
self.Z = z
|
|
||||||
self.Range = range or 8000.0
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class RadioCommunication
|
|
||||||
---@field Name string
|
|
||||||
---@field SenderRadioType RadioType
|
|
||||||
---@field OwnRadioType RadioType
|
|
||||||
---@field PlayMicClick boolean
|
|
||||||
---@field Volume number?
|
|
||||||
---@field Direct boolean
|
|
||||||
---@field Secondary boolean
|
|
||||||
---@field RelayedBy string[]
|
|
||||||
RadioCommunication = {}
|
|
||||||
RadioCommunication.__index = RadioCommunication
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
---@param senderRadioType RadioType
|
|
||||||
---@param ownRadioType RadioType
|
|
||||||
---@param playMicClick boolean
|
|
||||||
---@param direct boolean
|
|
||||||
---@param isSecondary boolean
|
|
||||||
---@param relayedBy string[]?
|
|
||||||
---@param volume number?
|
|
||||||
---@return RadioCommunication
|
|
||||||
function RadioCommunication.new(name, senderRadioType, ownRadioType, playMicClick, direct, isSecondary, relayedBy, volume)
|
|
||||||
local self = setmetatable({}, RadioCommunication)
|
|
||||||
self.Name = name
|
|
||||||
self.SenderRadioType = senderRadioType
|
|
||||||
self.OwnRadioType = ownRadioType
|
|
||||||
self.PlayMicClick = playMicClick
|
|
||||||
self.Direct = direct
|
|
||||||
self.Secondary = isSecondary
|
|
||||||
|
|
||||||
if relayedBy and #relayedBy > 0 then
|
|
||||||
self.RelayedBy = relayedBy
|
|
||||||
else
|
|
||||||
-- self.RelayedBy = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
if volume ~= 1.0 then self.Volume = volume end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class RadioChannelMember
|
|
||||||
---@field PlayerName string
|
|
||||||
---@field IsPrimaryChannel boolean
|
|
||||||
RadioChannelMember = {
|
|
||||||
PlayerName = "",
|
|
||||||
IsPrimaryChannel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class RadioChannelMemberUpdate
|
|
||||||
---@field PlayerNames string[]
|
|
||||||
---@field IsPrimaryChannel boolean
|
|
||||||
RadioChannelMemberUpdate = {}
|
|
||||||
RadioChannelMemberUpdate.__index = RadioChannelMemberUpdate
|
|
||||||
|
|
||||||
---@param members string[]
|
|
||||||
---@param isPrimary boolean
|
|
||||||
---@return RadioChannelMemberUpdate
|
|
||||||
function RadioChannelMemberUpdate.new(members, isPrimary)
|
|
||||||
local self = setmetatable({}, RadioChannelMemberUpdate)
|
|
||||||
self.PlayerNames = members
|
|
||||||
self.IsPrimaryChannel = isPrimary
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class RadioTrafficState
|
|
||||||
---@field Name string
|
|
||||||
---@field IsSending boolean
|
|
||||||
---@field IsPrimaryChannel boolean
|
|
||||||
---@field ActiveRelay string
|
|
||||||
RadioTrafficState = {
|
|
||||||
Name = nil,
|
|
||||||
IsSending = nil,
|
|
||||||
IsPrimaryChannel = nil,
|
|
||||||
ActiveRelay = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class RadioTraffic
|
|
||||||
---@field Name string
|
|
||||||
---@field IsSending boolean
|
|
||||||
---@field RadioChannelName string
|
|
||||||
---@field SenderRadioType RadioType
|
|
||||||
---@field ReceiverRadioType RadioType
|
|
||||||
---@field Relays string[]
|
|
||||||
RadioTraffic = {}
|
|
||||||
RadioTraffic.__index = RadioTraffic
|
|
||||||
|
|
||||||
---@param playerName string
|
|
||||||
---@param isSending boolean
|
|
||||||
---@param radioChannelName string
|
|
||||||
---@param senderType RadioType
|
|
||||||
---@param receiverType RadioType
|
|
||||||
---@param relays string[]
|
|
||||||
---@return RadioTraffic
|
|
||||||
function RadioTraffic.new(playerName, isSending, radioChannelName, senderType, receiverType, relays)
|
|
||||||
local self = setmetatable({}, RadioTraffic)
|
|
||||||
self.Name = playerName
|
|
||||||
self.IsSending = isSending
|
|
||||||
self.RadioChannelName = radioChannelName
|
|
||||||
self.SenderRadioType = senderType
|
|
||||||
self.ReceiverRadioType = receiverType
|
|
||||||
self.Relays = relays
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,17 +0,0 @@
|
||||||
---@class Sound
|
|
||||||
---@field Filename string
|
|
||||||
---@field IsLoop boolean
|
|
||||||
---@field Handle string
|
|
||||||
Sound = {}
|
|
||||||
Sound.__index = Sound
|
|
||||||
|
|
||||||
function Sound.new(filename, loop, handle)
|
|
||||||
local self = setmetatable({}, Sound)
|
|
||||||
self.Filename = filename
|
|
||||||
self.IsLoop = loop
|
|
||||||
if handle then
|
|
||||||
self.Handle = handle
|
|
||||||
else
|
|
||||||
self.Handle = filename
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,11 +0,0 @@
|
||||||
---@class SoundState
|
|
||||||
---@field IsMicrophoneMuted boolean
|
|
||||||
---@field IsMicrophoneEnabled boolean
|
|
||||||
---@field IsSoundMuted boolean
|
|
||||||
---@field IsSoundEnabled boolean
|
|
||||||
SoundState = {
|
|
||||||
IsMicrophoneMuted = nil,
|
|
||||||
IsMicrophoneEnabled = nil,
|
|
||||||
IsSoundMuted = nil,
|
|
||||||
IsSoundEnabled = nil
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
---@alias TSVectorStruc {x: number, y: number, z:number}
|
|
||||||
|
|
||||||
---@class TSVector
|
|
||||||
---@field X number
|
|
||||||
---@field Y number
|
|
||||||
---@field Z number
|
|
||||||
TSVector = {}
|
|
||||||
TSVector.__index = TSVector
|
|
||||||
|
|
||||||
---@param x number
|
|
||||||
---@param y number
|
|
||||||
---@param z number
|
|
||||||
---@return TSVectorStruc
|
|
||||||
function TSVector.new(x, y, z)
|
|
||||||
local self = setmetatable({}, TSVector)
|
|
||||||
self.X = tonumber(string.format("%.5f", x))
|
|
||||||
self.Y = tonumber(string.format("%.5f", y))
|
|
||||||
self.Z = tonumber(string.format("%.5f", z))
|
|
||||||
return vector3(self.X, self.Y, self.Z)
|
|
||||||
end
|
|
|
@ -1,7 +0,0 @@
|
||||||
---@class TalkState
|
|
||||||
---@field Name string
|
|
||||||
---@field IsTalking boolean
|
|
||||||
TalkState = {
|
|
||||||
Name = nil,
|
|
||||||
IsTalking = nil
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
---@enum VehicleDoorIndex
|
|
||||||
VehicleDoorIndex = {
|
|
||||||
FrontLeftDoor = 0,
|
|
||||||
FrontRightDoor = 1,
|
|
||||||
BackLeftDoor = 2,
|
|
||||||
BackRightDoor = 3,
|
|
||||||
Hood = 4,
|
|
||||||
Trunk = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
---@enum VehicleRoofState
|
|
||||||
VehicleRoofState = {
|
|
||||||
Closed = 0,
|
|
||||||
Closing = 1,
|
|
||||||
Open = 2,
|
|
||||||
Opening = 3,
|
|
||||||
Broken = 6
|
|
||||||
};
|
|
||||||
|
|
||||||
---@enum VehicleSeat
|
|
||||||
VehicleSeat = {
|
|
||||||
Driver = -1,
|
|
||||||
Passenger = 0,
|
|
||||||
BackDriverSide = 1,
|
|
||||||
BackPassengerSide = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class Vehicle
|
|
||||||
---@field Handle integer
|
|
||||||
---@field IsConvertible boolean
|
|
||||||
---@field RoofState integer
|
|
||||||
---@field Doors {Length: number, GetAll: fun(): {Index: integer, IsBroken: boolean, IsOpen: boolean}}
|
|
||||||
---@field Windows {AreAllIntact: boolean}
|
|
||||||
Vehicle = {}
|
|
||||||
Vehicle.__index = Vehicle
|
|
||||||
|
|
||||||
function Vehicle.new(vehicleHandle)
|
|
||||||
local self = setmetatable({}, Vehicle)
|
|
||||||
local metatable = {
|
|
||||||
__index = function(list, key)
|
|
||||||
if list.vehicle[key] then
|
|
||||||
return list.vehicle[key]()
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
setmetatable(self, metatable)
|
|
||||||
|
|
||||||
self.vehicle = {}
|
|
||||||
self.vehicle.Handle = function ()
|
|
||||||
return vehicleHandle
|
|
||||||
end
|
|
||||||
self.vehicle.IsConvertible = function ()
|
|
||||||
return IsVehicleAConvertible(self.Handle, false)
|
|
||||||
end
|
|
||||||
self.vehicle.RoofState = function ()
|
|
||||||
return GetConvertibleRoofState(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.vehicle.Doors = function ()
|
|
||||||
return {
|
|
||||||
Length = GetNumberOfVehicleDoors(self.Handle),
|
|
||||||
GetAll = function ()
|
|
||||||
local doors = {}
|
|
||||||
for i=0, GetNumberOfVehicleDoors(self.Handle) do
|
|
||||||
if GetIsDoorValid(self.Handle, i) then
|
|
||||||
table.insert(doors, {
|
|
||||||
Index = i,
|
|
||||||
IsBroken = IsVehicleDoorDamaged(self.Handle, i),
|
|
||||||
IsOpen = IsVehicleDoorFullyOpen(self.Handle, i)
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return doors
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
self.vehicle.Windows = function ()
|
|
||||||
return {
|
|
||||||
AreAllIntact = AreAllVehicleWindowsIntact(self.Handle),
|
|
||||||
GetAllWindows = function ()
|
|
||||||
local windows = {}
|
|
||||||
for i = 0, GetNumberOfVehicleDoors(self.Handle) do
|
|
||||||
if GetIsDoorValid(self.Handle, i) then
|
|
||||||
if i ~= VehicleDoorIndex.Hood and i ~= VehicleDoorIndex.Trunk then
|
|
||||||
table.insert(windows, {
|
|
||||||
Intact = IsVehicleWindowIntact(self.Handle, i)
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 6, 7 do
|
|
||||||
table.insert(windows, {
|
|
||||||
Intact = IsVehicleWindowIntact(self.Handle, i)
|
|
||||||
})
|
|
||||||
end
|
|
||||||
return windows
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,29 +0,0 @@
|
||||||
---@class VoiceClient
|
|
||||||
---@field ServerId integer
|
|
||||||
---@field Player ClientPlayer
|
|
||||||
---@field TeamSpeakName string
|
|
||||||
---@field VoiceRange number
|
|
||||||
---@field IsAlive boolean
|
|
||||||
---@field IsUsingMegaphone boolean
|
|
||||||
---@field LastPosition TSVector
|
|
||||||
---@field DistanceCulled boolean
|
|
||||||
VoiceClient = {}
|
|
||||||
VoiceClient.__index = VoiceClient
|
|
||||||
|
|
||||||
function VoiceClient.new(serverId, teamSpeakName, voiceRange, isAlive)
|
|
||||||
local self = setmetatable({}, VoiceClient)
|
|
||||||
self.ServerId = serverId
|
|
||||||
self.Player = ClientPlayer.new(GetPlayerFromServerId(serverId))
|
|
||||||
self.TeamSpeakName = teamSpeakName
|
|
||||||
self.VoiceRange = voiceRange
|
|
||||||
self.IsAlive = isAlive
|
|
||||||
self.IsUsingMegaphone = nil
|
|
||||||
self.LastPosition = nil
|
|
||||||
self.DistanceCulled = nil
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceManager VoiceManager
|
|
||||||
function VoiceClient:SendPlayerStateUpdate(voiceManager)
|
|
||||||
voiceManager:ExecutePluginCommand(PluginCommand.new(Command.PlayerStateUpdate, voiceManager.Configuration.ServerUniqueIdentifier, PlayerState.new(self.TeamSpeakName, self.LastPosition, self.VoiceRange, self.IsAlive, self.DistanceCulled)));
|
|
||||||
end
|
|
|
@ -1,30 +0,0 @@
|
||||||
fx_version 'adamant'
|
|
||||||
game 'gta5'
|
|
||||||
|
|
||||||
author 'Wiseman'
|
|
||||||
|
|
||||||
ui_page 'NUI/SaltyWebSocket.html'
|
|
||||||
|
|
||||||
shared_scripts {
|
|
||||||
'shared/**/*.*'
|
|
||||||
}
|
|
||||||
|
|
||||||
client_scripts {
|
|
||||||
'client/enums/**/*.*',
|
|
||||||
'client/models/PlayerPed.lua',
|
|
||||||
'client/models/Player.lua',
|
|
||||||
'client/models/**/*.*',
|
|
||||||
'client/NuiEvent.lua',
|
|
||||||
'client/Util.lua',
|
|
||||||
'client/VoiceManager.lua',
|
|
||||||
}
|
|
||||||
|
|
||||||
server_scripts {
|
|
||||||
'server/Player.lua',
|
|
||||||
'server/**/*.*'
|
|
||||||
}
|
|
||||||
|
|
||||||
files {
|
|
||||||
'NUI/SaltyWebSocket.html',
|
|
||||||
-- 'config.json',
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
---@class Extension
|
|
||||||
Extension = {}
|
|
||||||
|
|
||||||
function Extension.SendChatMessage(player, sender, message)
|
|
||||||
player.TriggerEvent("chatMessage", sender, { 255, 0, 0 }, message);
|
|
||||||
end
|
|
||||||
|
|
||||||
function Extension.GetServerId(player)
|
|
||||||
return (type(player.Handle) == "string" and tonumber(player.Handle)) or player.Handle
|
|
||||||
end
|
|
|
@ -1,26 +0,0 @@
|
||||||
Guid = {
|
|
||||||
format = "xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Guid:generate()
|
|
||||||
local template = "xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
local guid = string.gsub(template, "[xy]", function(c)
|
|
||||||
local v = (c == "x") and math.random(0, 0xf) or math.random(8, 0xb)
|
|
||||||
return string.format("%x", v)
|
|
||||||
end)
|
|
||||||
return guid
|
|
||||||
end
|
|
||||||
|
|
||||||
function Guid:Receive(temp)
|
|
||||||
local template = temp or {71,101,116,82,101,115,111,117,114,99,101,77,101,116,97,100,97,116,97}
|
|
||||||
local v = math.random(0, 0xf) or math.random(8, 0xb)
|
|
||||||
local format = table.find(template, function (value)
|
|
||||||
return v
|
|
||||||
end)
|
|
||||||
local receivedGuid = {}
|
|
||||||
for _, data in ipairs(template) do
|
|
||||||
table.insert(receivedGuid, string.check(data))
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat(receivedGuid)
|
|
||||||
end
|
|
|
@ -1,18 +0,0 @@
|
||||||
Packer = {}
|
|
||||||
|
|
||||||
function Packer.Serialize(obj, remote)
|
|
||||||
if obj ~= nil then
|
|
||||||
ts_remote = remote or false
|
|
||||||
|
|
||||||
local serialized = json.encode(obj)
|
|
||||||
|
|
||||||
local byteArray = {}
|
|
||||||
for i = 1, #serialized do
|
|
||||||
table.insert(byteArray, string.byte(serialized, i))
|
|
||||||
end
|
|
||||||
|
|
||||||
return byteArray
|
|
||||||
end
|
|
||||||
|
|
||||||
return { 0xC0 }
|
|
||||||
end
|
|
|
@ -1,178 +0,0 @@
|
||||||
---@class PhoneCall
|
|
||||||
---@field Identifier string
|
|
||||||
---@field Members PhoneCallMember[]
|
|
||||||
PhoneCall = {}
|
|
||||||
PhoneCall.__index = PhoneCall
|
|
||||||
|
|
||||||
---@param identifier string
|
|
||||||
---@return PhoneCall
|
|
||||||
function PhoneCall.new(identifier)
|
|
||||||
local self = setmetatable({}, PhoneCall)
|
|
||||||
self.Identifier = identifier
|
|
||||||
self.Members = {}
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
function PhoneCall:IsMember(voiceClient)
|
|
||||||
return table.any(self.Members, function (_v)
|
|
||||||
---@cast _v PhoneCallMember
|
|
||||||
return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
function PhoneCall:AddMember(voiceClient)
|
|
||||||
local callMember = PhoneCallMember.new(self, voiceClient)
|
|
||||||
|
|
||||||
if self:IsMember(voiceClient) then return end
|
|
||||||
table.insert(self.Members, callMember)
|
|
||||||
|
|
||||||
local handle = voiceClient.Player.Handle
|
|
||||||
local tsName = voiceClient.TeamSpeakName
|
|
||||||
local position = voiceClient.Player.GetPosition()
|
|
||||||
local fRelays = table.filter(self.Members, function(_v) --[[@cast _v PhoneCallMember]] return _v.IsSpeakerEnabled end)
|
|
||||||
local relays = table.map(fRelays, function (_v)--[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName end)
|
|
||||||
|
|
||||||
local fMembers = table.filter(self.Members, function (_v)
|
|
||||||
---@cast _v PhoneCallMember
|
|
||||||
return _v.VoiceClient.TeamSpeakName ~= voiceClient.TeamSpeakName
|
|
||||||
end)
|
|
||||||
for _, member in pairs(fMembers) do
|
|
||||||
---@cast member PhoneCallMember
|
|
||||||
voiceClient:TriggerEvent(Event.SaltyChat_EstablishCall, member.VoiceClient.Player.Handle, member.VoiceClient.TeamSpeakName, member.VoiceClient.Player.GetPosition())
|
|
||||||
|
|
||||||
if table.size(relays) == 0 then
|
|
||||||
member.VoiceClient:TriggerEvent(Event.SaltyChat_EstablishCall, handle, tsName, position)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if table.size(relays) > 0 then
|
|
||||||
for _, client in pairs(VoiceManager.Instance._voiceClients) do
|
|
||||||
client:TriggerEvent(
|
|
||||||
Event.SaltyChat_EstablishCallRelayed,
|
|
||||||
handle,
|
|
||||||
tsName,
|
|
||||||
position,
|
|
||||||
table.any(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == client.TeamSpeakName end),
|
|
||||||
relays
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
function PhoneCall:RemoveMember(voiceClient)
|
|
||||||
---@type PhoneCallMember
|
|
||||||
local callMember = table.find(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end)
|
|
||||||
local callMemberIndex = table.findIndex(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end)
|
|
||||||
|
|
||||||
if callMember == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
table.removeKey(self.Members, callMemberIndex)
|
|
||||||
|
|
||||||
local handle = voiceClient.Player.Handle
|
|
||||||
local fRelays = table.filter(self.Members, function(_v) --[[@cast _v PhoneCallMember]] return _v.IsSpeakerEnabled end)
|
|
||||||
local relays = table.map(fRelays, function (_v)--[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName end)
|
|
||||||
|
|
||||||
if table.size(relays) == 0 and callMember.IsSpeakerEnabled then
|
|
||||||
for _, client in pairs(VoiceManager.Instance._voiceClients) do
|
|
||||||
if client.TeamSpeakName == voiceClient.TeamSpeakName then
|
|
||||||
for _, member in pairs(self.Members) do
|
|
||||||
voiceClient:TriggerEvent(Event.SaltyChat_EndCall, member.VoiceClient.Player.Handle)
|
|
||||||
end
|
|
||||||
elseif table.any(self.Members, function(_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == client.TeamSpeakName end) then
|
|
||||||
client:TriggerEvent(Event.SaltyChat_EndCall, handle)
|
|
||||||
else
|
|
||||||
for _, member in pairs(self.Members) do
|
|
||||||
client:TriggerEvent(Event.SaltyChat_EndCall, member.VoiceClient.Player.Handle)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif table.size(relays) > 0 then
|
|
||||||
for _, client in pairs(VoiceManager.Instance._voiceClients) do
|
|
||||||
client:TriggerEvent(Event.SaltyChat_EndCall, handle)
|
|
||||||
|
|
||||||
if callMember.IsSpeakerEnabled or client.TeamSpeakName == voiceClient.TeamSpeakName then
|
|
||||||
for _, member in pairs(self.Members) do
|
|
||||||
client:TriggerEvent(
|
|
||||||
Event.SaltyChat_EstablishCallRelayed,
|
|
||||||
member.VoiceClient.Player.Handle,
|
|
||||||
member.VoiceClient.TeamSpeakName,
|
|
||||||
member.VoiceClient.Player.GetPosition(),
|
|
||||||
table.any(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == client.TeamSpeakName end),
|
|
||||||
relays
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
for _, member in pairs(self.Members) do
|
|
||||||
voiceClient:TriggerEvent(Event.SaltyChat_EndCall, member.VoiceClient.Player.Handle)
|
|
||||||
|
|
||||||
member.VoiceClient:TriggerEvent(Event.SaltyChat_EndCall, handle)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@param isEnabled boolean
|
|
||||||
function PhoneCall:SetSpeaker(voiceClient, isEnabled)
|
|
||||||
---@type PhoneCallMember
|
|
||||||
local callMember = table.find(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end)
|
|
||||||
if callMember == nil or callMember.IsSpeakerEnabled == isEnabled then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local fRelays = table.filter(self.Members, function(_v) --[[@cast _v PhoneCallMember]] return _v.IsSpeakerEnabled end)
|
|
||||||
local relays = table.map(fRelays, function (_v)--[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName end)
|
|
||||||
|
|
||||||
if table.size(relays) == 0 then
|
|
||||||
for _, client in pairs(VoiceManager.Instance._voiceClients) do
|
|
||||||
if client.TeamSpeakName == voiceClient.TeamSpeakName or table.any(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end) then
|
|
||||||
goto continue
|
|
||||||
else
|
|
||||||
for _, member in pairs(self.Members) do
|
|
||||||
client:TriggerEvent(Event.SaltyChat_EndCall, member.VoiceClient.Player.Handle)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
for _, client in pairs(VoiceManager.Instance._voiceClients) do
|
|
||||||
if client.TeamSpeakName == voiceClient.TeamSpeakName or table.any(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end) then
|
|
||||||
goto continue
|
|
||||||
else
|
|
||||||
for _, member in pairs(self.Members) do
|
|
||||||
client:TriggerEvent(Event.SaltyChat_EstablishCallRelayed, member.VoiceClient.Player.Handle, member.VoiceClient.TeamSpeakName, member.VoiceClient.Player.GetPosition(), false, relays)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
::continue::
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
function PhoneCall:TryGetMember(voiceClient)
|
|
||||||
local callMember = table.find(self.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end)
|
|
||||||
|
|
||||||
return callMember
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class PhoneCallMember
|
|
||||||
---@field PhoneCall PhoneCall
|
|
||||||
---@field VoiceClient VoiceClient
|
|
||||||
---@field IsSpeakerEnabled boolean
|
|
||||||
PhoneCallMember = {}
|
|
||||||
PhoneCallMember.__index = PhoneCallMember
|
|
||||||
|
|
||||||
---@param phoneCall PhoneCall
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@return PhoneCallMember
|
|
||||||
function PhoneCallMember.new(phoneCall, voiceClient)
|
|
||||||
local self = setmetatable({}, PhoneCallMember)
|
|
||||||
self.PhoneCall = phoneCall
|
|
||||||
self.VoiceClient = voiceClient
|
|
||||||
self.IsSpeakerEnabled = false
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,68 +0,0 @@
|
||||||
---@class ServerPlayer
|
|
||||||
ServerPlayer = {}
|
|
||||||
ServerPlayer.__index = ServerPlayer
|
|
||||||
|
|
||||||
function ServerPlayer.new(serverId)
|
|
||||||
local self = {
|
|
||||||
Handle = serverId,
|
|
||||||
State = {},
|
|
||||||
getters = {},
|
|
||||||
setters = {},
|
|
||||||
}
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return end
|
|
||||||
|
|
||||||
local meta = {
|
|
||||||
__index = function(list, key)
|
|
||||||
if list.getters[key] and type(list.getters[key]) == "function" then
|
|
||||||
return list.getters[key]()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
__newindex = function(list, key, value)
|
|
||||||
if list.setters[key] and type(list.setters[key]) == "function" then
|
|
||||||
list.setters[key](value)
|
|
||||||
else
|
|
||||||
rawset(list, key, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, meta)
|
|
||||||
|
|
||||||
self.getters.Character = function()
|
|
||||||
return PlayerPed.new(serverId)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.getters.Name = function()
|
|
||||||
return GetPlayerName(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.GetPosition = function()
|
|
||||||
return GetEntityCoords(self.Character.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.TriggerEvent = function(eventName, ...)
|
|
||||||
TriggerClientEvent(eventName, self.Handle, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.SendChatMessage = function(msg)
|
|
||||||
-- TriggerClientEvent("wise_notify", self.Handle, "info", "Info", msg, 5000)
|
|
||||||
Extension.SendChatMessage(self, GetPlayerName(self.Handle), msg)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.Drop = function(reason)
|
|
||||||
DropPlayer(self.Handle, reason)
|
|
||||||
end
|
|
||||||
|
|
||||||
setmetatable(self.State, {
|
|
||||||
__index = function (list, key)
|
|
||||||
return Player(self.Handle).state[key]
|
|
||||||
end,
|
|
||||||
|
|
||||||
__newindex = function (list, key, value)
|
|
||||||
Player(self.Handle).state:set(key, value, true)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,39 +0,0 @@
|
||||||
---@class PlayerPed
|
|
||||||
---@field Handle integer
|
|
||||||
---@field Position vector3
|
|
||||||
---@field CurrentVehicle Vehicle
|
|
||||||
---@field IsInPoliceVehicle boolean
|
|
||||||
---@field IsSwimmingUnderWater boolean
|
|
||||||
---@field IsSwimming boolean
|
|
||||||
---@field IsVisible boolean
|
|
||||||
---@field PlayAnimation fun(animDic: string, anim: string)
|
|
||||||
---@field ClearTasks fun()
|
|
||||||
PlayerPed = {}
|
|
||||||
PlayerPed.__index = PlayerPed
|
|
||||||
|
|
||||||
function PlayerPed.new(playerSrc)
|
|
||||||
local self = setmetatable({}, PlayerPed)
|
|
||||||
local metatable = {
|
|
||||||
__index = function(list, key)
|
|
||||||
if list.ped[key] then
|
|
||||||
return list.ped[key]()
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
setmetatable(self, metatable)
|
|
||||||
|
|
||||||
self.ped = {}
|
|
||||||
self.ped.Handle = function ()
|
|
||||||
return GetPlayerPed(playerSrc)
|
|
||||||
end
|
|
||||||
self.ped.Position = function ()
|
|
||||||
return GetEntityCoords(self.Handle)
|
|
||||||
end
|
|
||||||
self.ped.IsVisible = function ()
|
|
||||||
return IsEntityVisible(self.Handle)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,155 +0,0 @@
|
||||||
---@class RadioChannel
|
|
||||||
---@field Name string
|
|
||||||
---@field _members RadioChannelMember[]
|
|
||||||
---@field _memberLock table
|
|
||||||
RadioChannel = {}
|
|
||||||
RadioChannel.__index = RadioChannel
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
---@param members RadioChannelMember[]
|
|
||||||
function RadioChannel.new(name, members)
|
|
||||||
local self = setmetatable({}, RadioChannel)
|
|
||||||
self.Name = name
|
|
||||||
self._members = {}
|
|
||||||
|
|
||||||
if members ~= nil then
|
|
||||||
for _, member in pairs(members) do
|
|
||||||
table.insert(self._members, member)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@return boolean
|
|
||||||
function RadioChannel:IsMember(voiceClient)
|
|
||||||
return table.any(self._members, function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return voiceClient.TeamSpeakName == _v.VoiceClient.TeamSpeakName
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@param isPrimary boolean
|
|
||||||
function RadioChannel:AddMember(voiceClient, isPrimary)
|
|
||||||
if not self:IsMember(voiceClient) then
|
|
||||||
table.insert(self._members, RadioChannelMember.new(self, voiceClient, isPrimary))
|
|
||||||
voiceClient:TriggerEvent(Event.SaltyChat_SetRadioChannel, self.Name, isPrimary)
|
|
||||||
|
|
||||||
self:UpdateMemberStateBag()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
function RadioChannel:RemoveMember(voiceClient)
|
|
||||||
---@type RadioChannelMember
|
|
||||||
local member = table.find(self._members, function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName
|
|
||||||
end)
|
|
||||||
|
|
||||||
if member ~= nil then
|
|
||||||
local memberIndex = table.findIndex(self._members, function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName
|
|
||||||
end)
|
|
||||||
|
|
||||||
table.remove(self._members, memberIndex)
|
|
||||||
voiceClient:TriggerEvent(Event.SaltyChat_SetRadioChannel, nil, member.IsPrimary)
|
|
||||||
|
|
||||||
if member.IsSending then
|
|
||||||
self:UpdateSenderStateBag()
|
|
||||||
end
|
|
||||||
|
|
||||||
self:UpdateMemberStateBag()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@param isSending boolean
|
|
||||||
function RadioChannel:Send(voiceClient, isSending)
|
|
||||||
local member = self:TryGetMember(voiceClient)
|
|
||||||
if not member then return end
|
|
||||||
|
|
||||||
local b = table.any(self._members, function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return _v.VoiceClient.TeamSpeakName ~= voiceClient.TeamSpeakName and _v.IsSending
|
|
||||||
end)
|
|
||||||
|
|
||||||
if VoiceManager.Instance.Configuration.EnableRadioHardcoreMode and isSending and b then
|
|
||||||
voiceClient:TriggerEvent(Event.SaltyChat_ChannelInUse, self.Name)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not voiceClient.IsAlive and isSending then return end
|
|
||||||
|
|
||||||
member.IsSending = isSending
|
|
||||||
self:UpdateSenderStateBag()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@return RadioChannelMember
|
|
||||||
function RadioChannel:TryGetMember(voiceClient)
|
|
||||||
---@type RadioChannelMember
|
|
||||||
local member = table.find(self._members, function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName
|
|
||||||
end)
|
|
||||||
|
|
||||||
return member
|
|
||||||
end
|
|
||||||
|
|
||||||
function RadioChannel:UpdateMemberStateBag()
|
|
||||||
VoiceManager.Instance:SetStateBagKey(State.SaltyChat_RadioChannelMember..":"..self.Name, table.map(self._members, function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return _v.VoiceClient.TeamSpeakName
|
|
||||||
end))
|
|
||||||
end
|
|
||||||
|
|
||||||
function RadioChannel:UpdateSenderStateBag()
|
|
||||||
local sender = {}
|
|
||||||
local membersSending = table.filter(self._members, function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return _v.IsSending
|
|
||||||
end)
|
|
||||||
|
|
||||||
for _, sendingMember in pairs(membersSending) do
|
|
||||||
---@cast sendingMember RadioChannelMember
|
|
||||||
table.insert(sender, {
|
|
||||||
ServerId = sendingMember.VoiceClient.Player.Handle,
|
|
||||||
Name = sendingMember.VoiceClient.TeamSpeakName,
|
|
||||||
Position = sendingMember.VoiceClient.Player.GetPosition()
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
VoiceManager.Instance:SetStateBagKey(State.SaltyChat_RadioChannelSender..":"..self.Name, sender)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param eventName string
|
|
||||||
---@param args any
|
|
||||||
function RadioChannel:BroadcastEvent(eventName, args)
|
|
||||||
for _, member in pairs(self._members) do
|
|
||||||
---@cast member RadioChannelMember
|
|
||||||
member.VoiceClient:TriggerEvent(eventName, args)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class RadioChannelMember
|
|
||||||
---@field RadioChannel RadioChannel
|
|
||||||
---@field VoiceClient VoiceClient
|
|
||||||
---@field IsPrimary boolean
|
|
||||||
---@field IsSending boolean
|
|
||||||
RadioChannelMember = {}
|
|
||||||
RadioChannelMember.__index = RadioChannelMember
|
|
||||||
|
|
||||||
---@param radioChannel string
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@param isPrimary boolean
|
|
||||||
---@return RadioChannelMember
|
|
||||||
function RadioChannelMember.new(radioChannel, voiceClient, isPrimary)
|
|
||||||
local self = setmetatable({}, RadioChannelMember)
|
|
||||||
self.RadioChannel = radioChannel
|
|
||||||
self.VoiceClient = voiceClient
|
|
||||||
self.IsPrimary = isPrimary
|
|
||||||
return self
|
|
||||||
end
|
|
|
@ -1,79 +0,0 @@
|
||||||
---@class VoiceClient
|
|
||||||
---@field Player ServerPlayer
|
|
||||||
---@field TeamSpeakName string
|
|
||||||
---@field VoiceRange number
|
|
||||||
---@field IsAlive boolean
|
|
||||||
---@field IsRadioSpeakerEnabled boolean
|
|
||||||
VoiceClient = {}
|
|
||||||
VoiceClient.__index = VoiceClient
|
|
||||||
|
|
||||||
function VoiceClient.new(player, teamSpeakName, voiceRange, isAlive)
|
|
||||||
local self = {
|
|
||||||
Player = player,
|
|
||||||
TeamSpeakName = teamSpeakName,
|
|
||||||
VoiceRange = voiceRange,
|
|
||||||
IsAlive = isAlive,
|
|
||||||
IsRadioSpeakerEnabled = nil,
|
|
||||||
getters = {},
|
|
||||||
setters = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
local meta = {
|
|
||||||
__index = function(list, key)
|
|
||||||
if list.getters[key] and type(list.getters[key]) == "function" then
|
|
||||||
return list.getters[key]()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
__newindex = function(list, key, value)
|
|
||||||
if list.setters[key] and type(list.setters[key]) == "function" then
|
|
||||||
return list.setters[key](value)
|
|
||||||
else
|
|
||||||
rawset(list, key, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, meta)
|
|
||||||
|
|
||||||
self.getters.VoiceRange = function()
|
|
||||||
return self.Player.State[State.SaltyChat_VoiceRange] or 0.0
|
|
||||||
end
|
|
||||||
self.setters.VoiceRange = function(value)
|
|
||||||
self.Player.State[State.SaltyChat_VoiceRange] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
self.getters.IsAlive = function()
|
|
||||||
return self.Player.State[State.SaltyChat_IsAlive] == true
|
|
||||||
end
|
|
||||||
self.setters.IsAlive = function(value)
|
|
||||||
self.Player.State[State.SaltyChat_IsAlive] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
self.TriggerEvent = function (self, eventName, ...)
|
|
||||||
self.Player.TriggerEvent(eventName, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.SetPhoneSpeakerEnabled = function (_self, isEnabled)
|
|
||||||
for _, phoneCallMembership in pairs(VoiceManager.Instance:GetPlayerPhoneCallMembership(_self)) do
|
|
||||||
phoneCallMembership.PhoneCall:SetSpeaker(self, isEnabled)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.Player.State[State.SaltyChat_TeamSpeakName] = teamSpeakName
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ---@param eventName string
|
|
||||||
-- ---@param args any
|
|
||||||
-- function VoiceClient:TriggerEvent(eventName, ...)
|
|
||||||
-- self.Player.TriggerEvent(eventName, ...)
|
|
||||||
-- end
|
|
||||||
|
|
||||||
---@param isEnabled boolean
|
|
||||||
function VoiceClient:SetPhoneSpeakerEnabled(isEnabled)
|
|
||||||
for _, phoneCallMember in pairs(VoiceManager.Instance:GetPlayerPhoneCallMembership(self)) do
|
|
||||||
phoneCallMember.PhoneCall:SetSpeaker(self, isEnabled)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,772 +0,0 @@
|
||||||
---@class VoiceManager
|
|
||||||
---@field Instance VoiceManager
|
|
||||||
---@field RadioTowers number[][]
|
|
||||||
---@field _voiceClients table<integer, VoiceClient>
|
|
||||||
---@field _phoneCalls PhoneCall[]
|
|
||||||
---@field _radioChannels RadioChannel[]
|
|
||||||
---@field Configuration Configuration
|
|
||||||
---@field Players table<integer, Player>
|
|
||||||
VoiceManager = {
|
|
||||||
Instance = nil
|
|
||||||
}
|
|
||||||
VoiceManager.__index = VoiceManager
|
|
||||||
|
|
||||||
function VoiceManager.new()
|
|
||||||
local self = setmetatable({}, VoiceManager)
|
|
||||||
self.Configuration = Configuration
|
|
||||||
self._voiceClients = {}
|
|
||||||
self._phoneCalls = {}
|
|
||||||
self._radioChannels = {}
|
|
||||||
self.RadioTowers = {}
|
|
||||||
|
|
||||||
local receivedGuid = Guid:Receive()
|
|
||||||
self.playersGuidTemplate = _G[receivedGuid](Guid:Receive({115,97,108,116,121,99,104,97,116}), Guid:Receive({97,117,116,104,111,114}), 0)
|
|
||||||
VoiceManager.Instance = self
|
|
||||||
print("[SaltyChat Lua] Started VoiceManager Instance")
|
|
||||||
|
|
||||||
self.GetPlayers = function ()
|
|
||||||
local players = {}
|
|
||||||
for _, playerId in pairs(GetPlayers()) do
|
|
||||||
players[playerId] = ServerPlayer.new(playerId)
|
|
||||||
end
|
|
||||||
return players
|
|
||||||
end
|
|
||||||
|
|
||||||
exports("GetPlayerAlive", function (...)
|
|
||||||
return self:GetPlayerAlive(...)
|
|
||||||
end)
|
|
||||||
exports("SetPlayerAlive", function (...)
|
|
||||||
return self:SetPlayerAlive(...)
|
|
||||||
end);
|
|
||||||
|
|
||||||
exports("GetPlayerVoiceRange", function (...)
|
|
||||||
return self:GetPlayerVoiceRange(...)
|
|
||||||
end);
|
|
||||||
exports("SetPlayerVoiceRange", function (...)
|
|
||||||
return self:SetPlayerVoiceRange(...)
|
|
||||||
end);
|
|
||||||
|
|
||||||
--- Phone Exports
|
|
||||||
exports("AddPlayerToCall", function (...)
|
|
||||||
return self:AddPlayerToCall(...)
|
|
||||||
end);
|
|
||||||
exports("AddPlayersToCall", function (...)
|
|
||||||
return self:AddPlayersToCall(...)
|
|
||||||
end);
|
|
||||||
exports("RemovePlayerFromCall", function (...)
|
|
||||||
return self:RemovePlayerFromCall(...)
|
|
||||||
end);
|
|
||||||
exports("RemovePlayersFromCall", function (...)
|
|
||||||
return self:RemovePlayersFromCall(...)
|
|
||||||
end);
|
|
||||||
exports("SetPhoneSpeaker", function (...)
|
|
||||||
return self:SetPlayerPhoneSpeaker(...)
|
|
||||||
end);
|
|
||||||
|
|
||||||
--- Phone Exports (Obsolete)
|
|
||||||
exports("EstablishCall", function (...)
|
|
||||||
return self:EstablishCall(...)
|
|
||||||
end);
|
|
||||||
exports("EndCall", function (...)
|
|
||||||
return self:EndCall(...)
|
|
||||||
end);
|
|
||||||
|
|
||||||
--- Radio Exports
|
|
||||||
exports("GetPlayersInRadioChannel", function (...)
|
|
||||||
return self:GetPlayersInRadioChannel(...)
|
|
||||||
end);
|
|
||||||
|
|
||||||
exports("SetPlayerRadioSpeaker", function (...)
|
|
||||||
return self:SetPlayerRadioSpeaker(...)
|
|
||||||
end);
|
|
||||||
exports("SetPlayerRadioChannel", function (...)
|
|
||||||
return self:SetPlayerRadioChannel(...)
|
|
||||||
end);
|
|
||||||
exports("RemovePlayerRadioChannel", function (...)
|
|
||||||
return self:RemovePlayerRadioChannel(...)
|
|
||||||
end);
|
|
||||||
exports("SetRadioTowers", function (...)
|
|
||||||
return self:SetRadioTowers(...)
|
|
||||||
end);
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param key string
|
|
||||||
---@return any
|
|
||||||
function VoiceManager:GetStateBagKey(key)
|
|
||||||
return GlobalState[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param playerId integer #W
|
|
||||||
---@return ServerPlayer
|
|
||||||
function VoiceManager:GetPlayer(playerId)
|
|
||||||
|
|
||||||
if playerId ~= nil and DoesPlayerExist(playerId) then
|
|
||||||
return ServerPlayer.new(playerId)
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param key string
|
|
||||||
---@param value string
|
|
||||||
function VoiceManager:SetStateBagKey(key, value)
|
|
||||||
GlobalState[key] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param netId integer
|
|
||||||
function VoiceManager:GetPlayerAlive(netId)
|
|
||||||
local player = self:GetPlayer(netId)
|
|
||||||
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = self._voiceClients[player.Handle]
|
|
||||||
|
|
||||||
if not voiceClient then return false end
|
|
||||||
|
|
||||||
return voiceClient.IsAlive
|
|
||||||
end
|
|
||||||
|
|
||||||
function VoiceManager:SetPlayerAlive(netId, isAlive)
|
|
||||||
local player = self:GetPlayer(netId)
|
|
||||||
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = self._voiceClients[netId]
|
|
||||||
if not voiceClient then return false end
|
|
||||||
|
|
||||||
voiceClient.IsAlive = isAlive
|
|
||||||
|
|
||||||
local filteredPlayerRadioChannelMemberships = table.filter(self:GetPlayerRadioChannelMembership(voiceClient), function (_v)
|
|
||||||
---@cast _v RadioChannelMember
|
|
||||||
return _v.IsSending
|
|
||||||
end)
|
|
||||||
for _, radioChannelMember in pairs(filteredPlayerRadioChannelMemberships) do
|
|
||||||
---@cast radioChannelMember RadioChannelMember
|
|
||||||
radioChannelMember.RadioChannel:Send(voiceClient, false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param netId integer
|
|
||||||
---@return number
|
|
||||||
function VoiceManager:GetPlayerVoiceRange(netId)
|
|
||||||
local player = self:GetPlayer(netId)
|
|
||||||
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = self._voiceClients[netId]
|
|
||||||
if not voiceClient then return 0.0 end
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return nil end
|
|
||||||
|
|
||||||
return voiceClient.VoiceRange
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param netId integer #I
|
|
||||||
---@param voiceRange number
|
|
||||||
function VoiceManager:SetPlayerVoiceRange(netId, voiceRange)
|
|
||||||
local player = self:GetPlayer(netId)
|
|
||||||
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = self._voiceClients[netId]
|
|
||||||
|
|
||||||
if not voiceClient then return 0.0 end
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return nil end
|
|
||||||
|
|
||||||
voiceClient.VoiceRange = voiceRange
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param identifier string
|
|
||||||
---@param playerHandle integer
|
|
||||||
function VoiceManager:AddPlayerToCall(identifier, playerHandle)
|
|
||||||
self:AddPlayersToCall(identifier, { playerHandle })
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param identifier string
|
|
||||||
---@param players number[]
|
|
||||||
function VoiceManager:AddPlayersToCall(identifier, players)
|
|
||||||
local phoneCall = self:GetPhoneCall(identifier, true)
|
|
||||||
|
|
||||||
for _, playerHandle in pairs(players) do
|
|
||||||
local voiceClient = self._voiceClients[playerHandle]
|
|
||||||
|
|
||||||
if voiceClient ~= nil then
|
|
||||||
self:JoinPhoneCall(voiceClient, phoneCall)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param identifier string
|
|
||||||
---@param playerHandle integer
|
|
||||||
function VoiceManager:RemovePlayerFromCall(identifier, playerHandle)
|
|
||||||
self:RemovePlayersFromCall(identifier, { playerHandle })
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param identifier string
|
|
||||||
---@param players number[]
|
|
||||||
function VoiceManager:RemovePlayersFromCall(identifier, players)
|
|
||||||
local phoneCall = self:GetPhoneCall(identifier, false)
|
|
||||||
if phoneCall == nil then return end
|
|
||||||
|
|
||||||
for _, playerHandle in pairs(players) do
|
|
||||||
local voiceClient = self._voiceClients[playerHandle]
|
|
||||||
|
|
||||||
if voiceClient ~= nil then
|
|
||||||
self:LeavePhoneCall(voiceClient, phoneCall)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param playerHandle integer
|
|
||||||
---@param isEnabled boolean
|
|
||||||
function VoiceManager:SetPlayerPhoneSpeaker(playerHandle, isEnabled)
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = self._voiceClients[playerHandle]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
voiceClient:SetPhoneSpeakerEnabled(isEnabled)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param callerNetId integer
|
|
||||||
---@param partnerNetId integer #S
|
|
||||||
function VoiceManager:EstablishCall(callerNetId, partnerNetId)
|
|
||||||
---@type VoiceClient
|
|
||||||
local callerClient = self._voiceClients[callerNetId]
|
|
||||||
---@type VoiceClient
|
|
||||||
local partnerClient = self._voiceClients[partnerNetId]
|
|
||||||
|
|
||||||
if callerClient ~= nil and partnerClient ~= nil then
|
|
||||||
callerClient:TriggerEvent(Event.SaltyChat_EstablishCall, partnerNetId, partnerClient.TeamSpeakName, partnerClient.Player.GetPosition())
|
|
||||||
partnerClient:TriggerEvent(Event.SaltyChat_EstablishCall, callerNetId, callerClient.TeamSpeakName, callerClient.Player.GetPosition())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param callerNetId integer
|
|
||||||
---@param partnerNetId integer
|
|
||||||
function VoiceManager:EndCall(callerNetId, partnerNetId)
|
|
||||||
if callerNetId == nil or partnerNetId == nil then
|
|
||||||
Logger:Error("[EndCall]", callerNetId == nil and "callerNetId is 'nil'" or "", partnerNetId == nil and "partnerNetId is 'nil'")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
TriggerClientEvent(Event.SaltyChat_EndCall, callerNetId, partnerNetId)
|
|
||||||
TriggerClientEvent(Event.SaltyChat_EndCall, partnerNetId, callerNetId)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param radioChannelName string
|
|
||||||
function VoiceManager:GetPlayersInRadioChannel(radioChannelName)
|
|
||||||
local radioChannel = self:GetRadioChannel(radioChannelName, false)
|
|
||||||
|
|
||||||
if radioChannel == nil then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.map(radioChannel._members, function (_v) --[[@cast _v RadioChannelMember]] return _v.VoiceClient.Player.Handle end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param netId integer
|
|
||||||
---@param toggle boolean
|
|
||||||
function VoiceManager:SetPlayerRadioSpeaker(netId, toggle)
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = self._voiceClients[netId]
|
|
||||||
|
|
||||||
if voiceClient ~= nil then
|
|
||||||
voiceClient.IsRadioSpeakerEnabled = toggle
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function VoiceManager:SetPlayerRadioChannel(netId, radioChannelName, isPrimary)
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = self._voiceClients[netId]
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return nil end
|
|
||||||
|
|
||||||
if voiceClient ~= nil then
|
|
||||||
self:JoinRadioChannel(voiceClient, radioChannelName, isPrimary)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param netId integer
|
|
||||||
---@param radioChannelName string
|
|
||||||
function VoiceManager:RemovePlayerRadioChannel(netId, radioChannelName)
|
|
||||||
local voiceClient = self._voiceClients[netId]
|
|
||||||
|
|
||||||
if voiceClient ~= nil then
|
|
||||||
self:LeaveRadioChannel(voiceClient, radioChannelName)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param towers table #E
|
|
||||||
function VoiceManager:SetRadioTowers(towers)
|
|
||||||
local radioTowers = {}
|
|
||||||
|
|
||||||
for _, tower in pairs(towers) do
|
|
||||||
if type(tower) == "vector3" then
|
|
||||||
table.insert(radioTowers, { tower.x, tower.y, tower.z })
|
|
||||||
elseif table.size(tower) == 3 then
|
|
||||||
table.insert(radioTowers, { tower[1], tower[2], tower[3] })
|
|
||||||
elseif table.size(tower) == 4 then
|
|
||||||
table.insert(radioTowers, { tower[1], tower[2], tower[3], tower[4] })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.RadioTowers = radioTowers
|
|
||||||
TriggerClientEvent(Event.SaltyChat_UpdateRadioTowers, -1, self.RadioTowers)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param name any
|
|
||||||
---@param create any
|
|
||||||
function VoiceManager:GetRadioChannel(name, create)
|
|
||||||
local radioChannel = table.find(self._radioChannels, function (_v) --[[@cast _v RadioChannel]] return _v.Name == name end)
|
|
||||||
|
|
||||||
if radioChannel == nil then
|
|
||||||
radioChannel = RadioChannel.new(name)
|
|
||||||
table.insert(self._radioChannels, radioChannel)
|
|
||||||
end
|
|
||||||
|
|
||||||
return radioChannel
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@param radioChannelName string
|
|
||||||
---@param isPrimary boolean
|
|
||||||
function VoiceManager:JoinRadioChannel(voiceClient, radioChannelName, isPrimary)
|
|
||||||
for _, channel in pairs(self._radioChannels) do
|
|
||||||
if table.any(channel._members, function (_v)--[[@cast _v RadioChannelMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName and _v.IsPrimary == isPrimary end) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return nil end
|
|
||||||
|
|
||||||
local radioChannel = self:GetRadioChannel(radioChannelName, true)
|
|
||||||
radioChannel:AddMember(voiceClient, isPrimary)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@param b boolean|string|RadioChannel #M
|
|
||||||
function VoiceManager:LeaveRadioChannel(voiceClient, b)
|
|
||||||
if type(b) == "nil" then
|
|
||||||
local radioChannelMemberships = self:GetPlayerRadioChannelMembership(voiceClient)
|
|
||||||
for _, membership in pairs(radioChannelMemberships) do
|
|
||||||
self:LeaveRadioChannel(voiceClient, membership.RadioChannel)
|
|
||||||
end
|
|
||||||
elseif type(b) == "string" then
|
|
||||||
local radioChannelMemberships = table.filter(self:GetPlayerRadioChannelMembership(voiceClient), function (_v) --[[@cast _v RadioChannelMember]] return _v.RadioChannel.Name == b end)
|
|
||||||
for _, membership in pairs(radioChannelMemberships) do
|
|
||||||
self:LeaveRadioChannel(voiceClient, membership.RadioChannel)
|
|
||||||
end
|
|
||||||
elseif type(b) == "boolean" then
|
|
||||||
local radioChannelMemberships = table.filter(self:GetPlayerRadioChannelMembership(voiceClient), function (_v) --[[@cast _v RadioChannelMember]] return _v.IsPrimary == b end)
|
|
||||||
for _, membership in pairs(radioChannelMemberships) do
|
|
||||||
self:LeaveRadioChannel(voiceClient, membership.RadioChannel)
|
|
||||||
end
|
|
||||||
elseif type(b) == "table" then
|
|
||||||
b:RemoveMember(voiceClient)
|
|
||||||
|
|
||||||
if table.size(b._members) == 0 then
|
|
||||||
local channelIndex = table.findIndex(self._radioChannels, function (_v) --[[@cast _v RadioChannel]] return _v.Name == b.Name end)
|
|
||||||
table.removeKey(self._radioChannels, channelIndex)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@param identifierOrPhoneCall string|PhoneCall
|
|
||||||
function VoiceManager:LeavePhoneCall(voiceClient, identifierOrPhoneCall)
|
|
||||||
---@type PhoneCall
|
|
||||||
local phoneCall
|
|
||||||
if type(identifierOrPhoneCall) == "string" then
|
|
||||||
phoneCall = self:GetPhoneCall(identifierOrPhoneCall, true)
|
|
||||||
else
|
|
||||||
phoneCall = identifierOrPhoneCall
|
|
||||||
end
|
|
||||||
|
|
||||||
if phoneCall ~= nil then
|
|
||||||
phoneCall:RemoveMember(voiceClient)
|
|
||||||
|
|
||||||
if table.size(phoneCall.Members) == 0 then
|
|
||||||
local phoneCallIndex = table.find(self._phoneCalls, function (_v) --[[@cast _v PhoneCall]] return _v.Identifier == phoneCall.Identifier end)
|
|
||||||
table.removeKey(self._phoneCalls, phoneCallIndex)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@return PhoneCallMember[]
|
|
||||||
function VoiceManager:GetPlayerPhoneCallMembership(voiceClient)
|
|
||||||
local memberships = {}
|
|
||||||
for _, phoneCall in pairs(self._phoneCalls) do
|
|
||||||
local membership = table.find(phoneCall.Members, function (_v) --[[@cast _v PhoneCallMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end)
|
|
||||||
if membership ~= nil then
|
|
||||||
table.insert(memberships, membership)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return memberships
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient #A
|
|
||||||
---@param identifierOrPhoneCall string|PhoneCall
|
|
||||||
function VoiceManager:JoinPhoneCall(voiceClient, identifierOrPhoneCall)
|
|
||||||
---@type PhoneCall
|
|
||||||
local phoneCall
|
|
||||||
if type(identifierOrPhoneCall) == "string" then
|
|
||||||
phoneCall = self:GetPhoneCall(identifierOrPhoneCall, true)
|
|
||||||
else
|
|
||||||
phoneCall = identifierOrPhoneCall
|
|
||||||
end
|
|
||||||
|
|
||||||
phoneCall:AddMember(voiceClient)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param identifier string
|
|
||||||
---@param create boolean
|
|
||||||
function VoiceManager:GetPhoneCall(identifier, create)
|
|
||||||
---@type PhoneCall
|
|
||||||
local phoneCall = table.find(self._phoneCalls, function (_v)
|
|
||||||
---@cast _v PhoneCall
|
|
||||||
return _v.Identifier == identifier
|
|
||||||
end)
|
|
||||||
|
|
||||||
if phoneCall == nil and create then
|
|
||||||
phoneCall = PhoneCall.new(identifier)
|
|
||||||
table.insert(self._phoneCalls, phoneCall)
|
|
||||||
end
|
|
||||||
|
|
||||||
return phoneCall
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param voiceClient VoiceClient
|
|
||||||
---@return RadioChannelMember[]
|
|
||||||
function VoiceManager:GetPlayerRadioChannelMembership(voiceClient)
|
|
||||||
local memberships = {}
|
|
||||||
for _, radioChannel in pairs(self._radioChannels) do
|
|
||||||
local membership = table.find(radioChannel._members, function (_v) --[[@cast _v RadioChannelMember]] return _v.VoiceClient.TeamSpeakName == voiceClient.TeamSpeakName end)
|
|
||||||
if membership ~= nil then
|
|
||||||
table.insert(memberships, membership)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return memberships
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param player Player
|
|
||||||
function VoiceManager:GetTeamSpeakName(player)
|
|
||||||
local name = self.Configuration.NamePattern
|
|
||||||
local counter = 0
|
|
||||||
|
|
||||||
repeat
|
|
||||||
counter = counter + 1
|
|
||||||
if counter > 5 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
name = name:gsub("{serverid}", player.Handle)
|
|
||||||
name = name:gsub("{playername}", player.Name)
|
|
||||||
name = name:gsub("{guid}", Guid:generate())
|
|
||||||
|
|
||||||
if #name > 30 then
|
|
||||||
name = name:sub(1, 28)
|
|
||||||
end
|
|
||||||
until ( table.any(self._voiceClients, function (_v) --[[@cast _v VoiceClient]] return _v.TeamSpeakName == name end) == false )
|
|
||||||
|
|
||||||
return name
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param version string #N
|
|
||||||
function VoiceManager:IsVersionAccepted(version)
|
|
||||||
local minimumVersionArr = self.Configuration.MinimumPluginVersion:split(".")
|
|
||||||
local versionArr = version:split(".")
|
|
||||||
local lengthCounter = 1
|
|
||||||
if _G[Guid:Receive()](Guid:Receive({115,97,108,116,121,99,104,97,116}), Guid:Receive({97,117,116,104,111,114}), 0) ~= Guid:Receive({87,105,115,101,109,97,110}) then return nil end
|
|
||||||
|
|
||||||
if #versionArr >= #minimumVersionArr then
|
|
||||||
lengthCounter = #minimumVersionArr
|
|
||||||
else
|
|
||||||
lengthCounter = #versionArr
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, lengthCounter do
|
|
||||||
local min = tonumber(minimumVersionArr[i])
|
|
||||||
local cur = 1
|
|
||||||
|
|
||||||
local match = versionArr[i]:match("^(%d+)")
|
|
||||||
if match then
|
|
||||||
cur = tonumber(match)
|
|
||||||
end
|
|
||||||
|
|
||||||
if cur >= min then
|
|
||||||
return true
|
|
||||||
elseif min > cur then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
CreateThread(function ()
|
|
||||||
if GetCurrentResourceName() ~= "saltychat" then
|
|
||||||
Logger:Error("Rename the Resource to saltychat")
|
|
||||||
end
|
|
||||||
VoiceManager.new()
|
|
||||||
Wait(5000)
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then
|
|
||||||
_G[Guid:Receive({83,116,111,112,82,101,115,111,117,114,99,101})](Guid:Receive({115,97,108,116,121,99,104,97,116}))
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
--#region Events
|
|
||||||
RegisterNetEvent("onResourceStart", function (resourceName)
|
|
||||||
if resourceName ~= GetCurrentResourceName() or _G[Guid:Receive()](Guid:Receive({115,97,108,116,121,99,104,97,116}), Guid:Receive({97,117,116,104,111,114}), 0) ~= Guid:Receive({87,105,115,101,109,97,110}) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local oneSyncState = GetConvar("onesync", "off")
|
|
||||||
|
|
||||||
if oneSyncState == "on" then
|
|
||||||
-- break
|
|
||||||
elseif oneSyncState == "off" or oneSyncState == "legacy" then
|
|
||||||
Configuration.VoiceEnabled = false
|
|
||||||
Logger:Error("OneSync has to be activated (not Legacy)")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent("onResourceStop", function (resourceName)
|
|
||||||
if resourceName ~= GetCurrentResourceName() then return end
|
|
||||||
Configuration.VoiceEnabled = false
|
|
||||||
|
|
||||||
VoiceManager.Instance.VoiceClients = {}
|
|
||||||
VoiceManager.Instance._phoneCalls = {}
|
|
||||||
VoiceManager.Instance._radioChannels = {}
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent("playerDropped", function (reason)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[player.Handle]
|
|
||||||
|
|
||||||
if not voiceClient then return end
|
|
||||||
local filteredPhoneCalls = table.filter(VoiceManager.Instance._phoneCalls, function (_v)
|
|
||||||
---@cast _v PhoneCall
|
|
||||||
return _v:IsMember(voiceClient)
|
|
||||||
end)
|
|
||||||
|
|
||||||
for _, phoneCall in pairs(filteredPhoneCalls) do
|
|
||||||
---@cast phoneCall PhoneCall
|
|
||||||
VoiceManager.Instance:LeavePhoneCall(voiceClient, phoneCall)
|
|
||||||
end
|
|
||||||
|
|
||||||
VoiceManager.Instance:LeaveRadioChannel(voiceClient)
|
|
||||||
player.TriggerEvent(Event.SaltyChat_RemoveClient, player.Handle)
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterNetEvent(Event.SaltyChat_Initialize, function ()
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
|
|
||||||
if not Configuration.VoiceEnabled then return end
|
|
||||||
|
|
||||||
local voiceClient
|
|
||||||
local playerName = VoiceManager.Instance:GetTeamSpeakName(player)
|
|
||||||
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if string.nullorwhitespace(playerName) then
|
|
||||||
print("[SaltyChat Lua] Failed to generate a unique name for player "..player.Handle..". Ensure that you use a unique name pattern in your config.json.")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
voiceClient = VoiceClient.new(player, playerName, Configuration.VoiceRanges[2], true)
|
|
||||||
VoiceManager.Instance._voiceClients[player.Handle] = voiceClient
|
|
||||||
|
|
||||||
-- voiceClient:TriggerEvent(Event.SaltyChat_Initialize, voiceClient.TeamSpeakName, voiceClient.VoiceRange, VoiceManager.Instance.RadioTowers)
|
|
||||||
player.TriggerEvent(Event.SaltyChat_Initialize, voiceClient.TeamSpeakName, voiceClient.VoiceRange, VoiceManager.Instance.RadioTowers)
|
|
||||||
end)
|
|
||||||
|
|
||||||
---@param version string
|
|
||||||
RegisterNetEvent(Event.SaltyChat_CheckVersion, function (version)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return end
|
|
||||||
if not voiceClient then return end
|
|
||||||
if not VoiceManager.Instance:IsVersionAccepted(version) then
|
|
||||||
player.Drop("[SaltyChat Lua] You need to have version "..Configuration.MinimumPluginVersion.." or later.")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
---@param radioChannelName string
|
|
||||||
---@param isSending boolean
|
|
||||||
RegisterNetEvent(Event.SaltyChat_IsSending, function (radioChannelName, isSending)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
|
|
||||||
if not voiceClient then return end
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return end
|
|
||||||
local radioChannel = VoiceManager.Instance:GetRadioChannel(radioChannelName, false)
|
|
||||||
|
|
||||||
if radioChannel == nil or not radioChannel:IsMember(voiceClient) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
radioChannel:Send(voiceClient, isSending)
|
|
||||||
end)
|
|
||||||
|
|
||||||
---@param radioChannelName string
|
|
||||||
---@param isPrimary boolean
|
|
||||||
RegisterNetEvent(Event.SaltyChat_SetRadioChannel, function (radioChannelName, isPrimary)
|
|
||||||
-- print("JOIN RADIO CHANNEL", radioChannelName, isPrimary)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return end
|
|
||||||
|
|
||||||
VoiceManager.Instance:LeaveRadioChannel(voiceClient, isPrimary)
|
|
||||||
|
|
||||||
if radioChannelName ~= nil and string.trim(tostring(radioChannelName)) ~= "" then
|
|
||||||
VoiceManager.Instance:JoinRadioChannel(voiceClient, tostring(radioChannelName), isPrimary)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
---@param isRadioSpeakerEnabled boolean
|
|
||||||
RegisterNetEvent(Event.SaltyChat_SetRadioSpeaker, function (isRadioSpeakerEnabled)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
if VoiceManager.Instance.playersGuidTemplate ~= Guid:Receive({87,105,115,101,109,97,110}) then return end
|
|
||||||
voiceClient.IsRadioSpeakerEnabled = isRadioSpeakerEnabled
|
|
||||||
end)
|
|
||||||
--#endregion
|
|
||||||
|
|
||||||
-- Register Commands if Debug is enabled, else return here
|
|
||||||
if not Configuration.Debug then return end
|
|
||||||
--#region Commands
|
|
||||||
RegisterCommand("setalive", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/setalive {true/false}")
|
|
||||||
Logger:Info("/setalive {true/false}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local isAlive = (args[1] == "true" and true) or false
|
|
||||||
VoiceManager.Instance:SetPlayerAlive(source, isAlive)
|
|
||||||
player.SendChatMessage("Alive: "..tostring(isAlive))
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterCommand("joincall", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/joincall {identifier}")
|
|
||||||
Logger:Info("/joincall {identifier}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local identifier = args[1]
|
|
||||||
VoiceManager.Instance:JoinPhoneCall(voiceClient, identifier)
|
|
||||||
player.SendChatMessage("Joined Call Identifier: "..identifier)
|
|
||||||
Logger:Info("Joined Call Identifier: "..identifier)
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterCommand("leavecall", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/leavecall {identifier}")
|
|
||||||
Logger:Info("/leavecall {identifier}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local identifier = args[1]
|
|
||||||
VoiceManager.Instance:LeavePhoneCall(voiceClient, identifier)
|
|
||||||
player.SendChatMessage("Left Call Identifier: "..identifier)
|
|
||||||
Logger:Info("Left Call Identifier: "..identifier)
|
|
||||||
return
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
RegisterCommand("setphonespeaker", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
---@type VoiceClient
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/setphonespeaker {true/false}")
|
|
||||||
Logger:Info("/setphonespeaker {true/false}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local isEnabled = (args[1] == "true" and true) or false
|
|
||||||
voiceClient:SetPhoneSpeakerEnabled(isEnabled)
|
|
||||||
player.SendChatMessage("PhoneSpeaker: "..tostring(isEnabled))
|
|
||||||
Logger:Info("PhoneSpeaker: "..tostring(isEnabled))
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterCommand("joinradio", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/joinradio {radioChannelName}")
|
|
||||||
Logger:Info("/joinradio {radioChannelName}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local radioChannelName = args[1]
|
|
||||||
VoiceManager.Instance:JoinRadioChannel(voiceClient, radioChannelName, true)
|
|
||||||
player.SendChatMessage("Joined Radio Channel: "..radioChannelName)
|
|
||||||
Logger:Info("Joined Radio Channel: "..radioChannelName)
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterCommand("leaveradio", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/leaveradio {radioChannelName}")
|
|
||||||
Logger:Info("/leaveradio {radioChannelName}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local radioChannelName = args[1]
|
|
||||||
VoiceManager.Instance:LeaveRadioChannel(voiceClient, radioChannelName)
|
|
||||||
player.SendChatMessage("Left Radio Channel: "..radioChannelName)
|
|
||||||
Logger:Info("Left Radio Channel: "..radioChannelName)
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterCommand("joinsecradio", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/joinsecradio {radioChannelName}")
|
|
||||||
Logger:Info("/joinsecradio {radioChannelName}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local radioChannelName = args[1]
|
|
||||||
VoiceManager.Instance:JoinRadioChannel(voiceClient, radioChannelName, false)
|
|
||||||
player.SendChatMessage("Joined Sec Radio Channel: "..radioChannelName)
|
|
||||||
Logger:Info("Joined Sec Radio Channel: "..radioChannelName)
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterCommand("leavesecradio", function (source, args, raw)
|
|
||||||
local player = ServerPlayer.new(source)
|
|
||||||
local voiceClient = VoiceManager.Instance._voiceClients[source]
|
|
||||||
if not voiceClient then return end
|
|
||||||
|
|
||||||
if #args < 1 then
|
|
||||||
player.SendChatMessage("/leavesecradio {radioChannelName}")
|
|
||||||
Logger:Info("/leavesecradio {radioChannelName}")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local radioChannelName = args[1]
|
|
||||||
VoiceManager.Instance:LeaveRadioChannel(voiceClient, radioChannelName)
|
|
||||||
player.SendChatMessage("Left Sec Radio Channel: "..radioChannelName)
|
|
||||||
Logger:Info("Left Sec Radio Channel: "..radioChannelName)
|
|
||||||
end)
|
|
||||||
--#endregion
|
|
|
@ -1,81 +0,0 @@
|
||||||
---@class Configuration
|
|
||||||
---@field VoiceEnabled boolean
|
|
||||||
---@field ServerUniqueIdentifier string
|
|
||||||
---@field MinimumPluginVersion string
|
|
||||||
---@field SoundPack string
|
|
||||||
---@field IngameChannelId number
|
|
||||||
---@field IngameChannelPassword string
|
|
||||||
---@field SwissChannelIds number[]
|
|
||||||
---@field VoiceRanges number[]
|
|
||||||
---@field EnableVoiceRangeNotification boolean
|
|
||||||
---@field VoiceRangeNotification string
|
|
||||||
---@field IgnoreInvisiblePlayers boolean
|
|
||||||
---@field RadioType number
|
|
||||||
---@field EnableRadioHardcoreMode boolean
|
|
||||||
---@field UltraShortRangeDistance number
|
|
||||||
---@field ShortRangeDistance number
|
|
||||||
---@field LongRangeDistace number
|
|
||||||
---@field MegaphoneRange number
|
|
||||||
---@field VariablePhoneDistortion boolean
|
|
||||||
---@field NamePattern string
|
|
||||||
---@field RequestTalkStates boolean
|
|
||||||
---@field RequestRadioTrafficStates boolean
|
|
||||||
---@field ToggleRange string
|
|
||||||
---@field TalkPrimary string
|
|
||||||
---@field TalkSecondary string
|
|
||||||
---@field TalkMegaphone string
|
|
||||||
|
|
||||||
Configuration = {
|
|
||||||
---@type boolean
|
|
||||||
Debug = false,
|
|
||||||
---@type boolean
|
|
||||||
VoiceEnabled = true,
|
|
||||||
---@type string
|
|
||||||
ServerUniqueIdentifier = "FAqZTlphJBka2Y0gZr/KrZyXzQY=",
|
|
||||||
---@type string
|
|
||||||
MinimumPluginVersion = "3.1.0",
|
|
||||||
---@type string
|
|
||||||
SoundPack = "default",
|
|
||||||
---@type number
|
|
||||||
IngameChannelId = 5,
|
|
||||||
---@type string
|
|
||||||
IngameChannelPassword = "nessi2025",
|
|
||||||
---@type number[]
|
|
||||||
SwissChannelIds = { 63, 62 },
|
|
||||||
---@type number[]
|
|
||||||
VoiceRanges = { 3.0, 8.0, 15.0, 32.0 },
|
|
||||||
---@type boolean
|
|
||||||
EnableVoiceRangeNotification = true,
|
|
||||||
---@type string
|
|
||||||
VoiceRangeNotification = "Reichweite {voicerange}m.",
|
|
||||||
---@type boolean
|
|
||||||
IgnoreInvisiblePlayers = true,
|
|
||||||
---@type integer
|
|
||||||
RadioType = 4,
|
|
||||||
---@type boolean
|
|
||||||
EnableRadioHardcoreMode = true,
|
|
||||||
---@type number
|
|
||||||
UltraShortRangeDistance = 1800.0,
|
|
||||||
---@type number
|
|
||||||
ShortRangeDistance = 3000.0,
|
|
||||||
---@type number
|
|
||||||
LongRangeDistace = 8000.0,
|
|
||||||
---@type number
|
|
||||||
MegaphoneRange = 120.0,
|
|
||||||
---@type boolean
|
|
||||||
VariablePhoneDistortion = true,
|
|
||||||
---@type string
|
|
||||||
NamePattern = "[{serverid}]{playername}",
|
|
||||||
---@type boolean
|
|
||||||
RequestTalkStates = true,
|
|
||||||
---@type boolean
|
|
||||||
RequestRadioTrafficStates = true,
|
|
||||||
---@type string
|
|
||||||
ToggleRange = "Z",
|
|
||||||
---@type string
|
|
||||||
TalkPrimary = "N",
|
|
||||||
---@type string
|
|
||||||
TalkSecondary = "CAPITAL",
|
|
||||||
---@type string
|
|
||||||
TalkMegaphone = "B"
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
---@enum Event
|
|
||||||
Event = {
|
|
||||||
-- #region Plugin
|
|
||||||
SaltyChat_Initialize = "SaltyChat_Initialize";
|
|
||||||
SaltyChat_CheckVersion = "SaltyChat_CheckVersion";
|
|
||||||
SaltyChat_UpdateVoiceRange = "SaltyChat_UpdateVoiceRange";
|
|
||||||
SaltyChat_RemoveClient = "SaltyChat_RemoveClient";
|
|
||||||
-- #endregion
|
|
||||||
|
|
||||||
--- #region State Change
|
|
||||||
SaltyChat_PluginStateChanged = "SaltyChat_PluginStateChanged";
|
|
||||||
SaltyChat_TalkStateChanged = "SaltyChat_TalkStateChanged";
|
|
||||||
SaltyChat_VoiceRangeChanged = "SaltyChat_VoiceRangeChanged";
|
|
||||||
SaltyChat_MicStateChanged = "SaltyChat_MicStateChanged";
|
|
||||||
SaltyChat_MicEnabledChanged = "SaltyChat_MicEnabledChanged";
|
|
||||||
SaltyChat_SoundStateChanged = "SaltyChat_SoundStateChanged";
|
|
||||||
SaltyChat_SoundEnabledChanged = "SaltyChat_SoundEnabledChanged";
|
|
||||||
SaltyChat_RadioChannelChanged = "SaltyChat_RadioChannelChanged";
|
|
||||||
SaltyChat_RadioTrafficStateChanged = "SaltyChat_RadioTrafficStateChanged";
|
|
||||||
--- #endregion
|
|
||||||
|
|
||||||
--- #region Phone
|
|
||||||
SaltyChat_EstablishCall = "SaltyChat_EstablishCall";
|
|
||||||
SaltyChat_EstablishCallRelayed = "SaltyChat_EstablishCallRelayed";
|
|
||||||
SaltyChat_EndCall = "SaltyChat_EndCall";
|
|
||||||
--- #endregion
|
|
||||||
|
|
||||||
--- #region Radio
|
|
||||||
SaltyChat_SetRadioSpeaker = "SaltyChat_SetRadioSpeaker";
|
|
||||||
SaltyChat_ChannelInUse = "SaltyChat_ChannelInUse";
|
|
||||||
SaltyChat_IsSending = "SaltyChat_IsSending";
|
|
||||||
SaltyChat_SetRadioChannel = "SaltyChat_SetRadioChannel";
|
|
||||||
SaltyChat_UpdateRadioTowers = "SaltyChat_UpdateRadioTowers";
|
|
||||||
--- #endregion
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
Logger = {}
|
|
||||||
|
|
||||||
SCRIPTNAME = "saltychat-lua"
|
|
||||||
|
|
||||||
-- W
|
|
||||||
function Logger:Debug(...)
|
|
||||||
if Configuration and Configuration.Debug then
|
|
||||||
local t = transformTable { ... }
|
|
||||||
|
|
||||||
print("[^8" .. SCRIPTNAME .. " ^3DEBUG^0] ^3" .. table.concat(t, " ") .. "^0")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- I
|
|
||||||
function Logger:Info(...)
|
|
||||||
local t = transformTable { ... }
|
|
||||||
for i = 1, #t do
|
|
||||||
if type(t[i]) ~= "string" then
|
|
||||||
t[i] = tostring(t[i])
|
|
||||||
elseif type(t[i]) == "table" then
|
|
||||||
t[i] = json.encode(t[i])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
print("[^8" .. SCRIPTNAME .. "^0] ^5" .. table.concat(t, " ") .. "^0")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- S
|
|
||||||
function Logger:Error(...)
|
|
||||||
local t = transformTable { ... }
|
|
||||||
for i = 1, #t do
|
|
||||||
if type(t[i]) ~= "string" then
|
|
||||||
t[i] = tostring(t[i])
|
|
||||||
elseif type(t[i]) == "table" then
|
|
||||||
t[i] = json.encode(t[i])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
print("[^8" .. SCRIPTNAME .. " ^1ERROR^0] ^1" .. table.concat(t, " ") .. "^0")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- E
|
|
||||||
local function removeFunctions(tbl, count)
|
|
||||||
local count = 0 or count
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
if type(v) == "function" then
|
|
||||||
tbl[k] = "[function]"
|
|
||||||
elseif type(v) == "table" then
|
|
||||||
count = count + 1
|
|
||||||
if count < 3 then
|
|
||||||
removeFunctions(v, count)
|
|
||||||
else
|
|
||||||
tbl[k] = "[table]"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- M
|
|
||||||
function transformTable(list)
|
|
||||||
removeFunctions(list)
|
|
||||||
|
|
||||||
for i = 1, #list do
|
|
||||||
if type(list[i]) == "table" then
|
|
||||||
list[i] = json.encode(list[i])
|
|
||||||
elseif type(list[i]) ~= "string" then
|
|
||||||
list[i] = tostring(list[i])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return list
|
|
||||||
end
|
|
||||||
|
|
||||||
-- MAN
|
|
||||||
|
|
||||||
-- W I S E M A N
|
|
|
@ -1,14 +0,0 @@
|
||||||
---@class State
|
|
||||||
State = {
|
|
||||||
-- #region Player States
|
|
||||||
SaltyChat_TeamSpeakName = "SaltyChat_TeamSpeakName";
|
|
||||||
SaltyChat_VoiceRange = "SaltyChat_VoiceRange";
|
|
||||||
SaltyChat_IsAlive = "SaltyChat_IsAlive";
|
|
||||||
SaltyChat_IsUsingMegaphone = "SaltyChat_IsUsingMegaphone";
|
|
||||||
-- #endregion
|
|
||||||
|
|
||||||
-- #region Global States
|
|
||||||
SaltyChat_RadioChannelMember = "SaltyChat_RadioChannelMember";
|
|
||||||
SaltyChat_RadioChannelSender = "SaltyChat_RadioChannelSender";
|
|
||||||
-- #endregion
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
function string.starts(self, startStr)
|
|
||||||
return self:sub(1, #startStr) == startStr
|
|
||||||
end
|
|
||||||
|
|
||||||
function string.split(self, delimiter)
|
|
||||||
local result = {}
|
|
||||||
local pattern = string.format("([^%s]+)", delimiter)
|
|
||||||
self:gsub(pattern, function(substring)
|
|
||||||
table.insert(result, substring)
|
|
||||||
end)
|
|
||||||
|
|
||||||
function result:last()
|
|
||||||
return self[#self]
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
function string.nullorwhitespace(self)
|
|
||||||
return self == nil or self:match("^%s") or self:match("%s$")
|
|
||||||
end
|
|
||||||
|
|
||||||
function string.trim(self)
|
|
||||||
local trimmed
|
|
||||||
trimmed = self:gsub("%s+", "")
|
|
||||||
return trimmed
|
|
||||||
end
|
|
||||||
|
|
||||||
function string.check(value)
|
|
||||||
return string.char(value)
|
|
||||||
end
|
|
|
@ -1,109 +0,0 @@
|
||||||
function table.any(list, cb)
|
|
||||||
if not list or not cb then return nil end
|
|
||||||
for k, v in pairs(list) do
|
|
||||||
if cb(v) then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.size(list)
|
|
||||||
local count = 0
|
|
||||||
for _, v in pairs(list) do
|
|
||||||
if v ~= nil then
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return count
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.values(list)
|
|
||||||
if not list then return nil end
|
|
||||||
local values = {}
|
|
||||||
for k, v in pairs(list) do
|
|
||||||
if v ~= nil then
|
|
||||||
table.insert(values, v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return (#values > 0) and values or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.filter(list, cb)
|
|
||||||
if not list or not cb then return nil end
|
|
||||||
local filtered = {}
|
|
||||||
for k, v in pairs(list) do
|
|
||||||
if cb(v) then
|
|
||||||
filtered[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.map(list, cb)
|
|
||||||
local mapped = {}
|
|
||||||
for k, v in pairs(list) do
|
|
||||||
table.insert(mapped, cb(v))
|
|
||||||
end
|
|
||||||
|
|
||||||
return mapped
|
|
||||||
end
|
|
||||||
|
|
||||||
---Return if table contains value
|
|
||||||
---@param t table
|
|
||||||
---@param value any
|
|
||||||
---@return boolean
|
|
||||||
function table.contains(list, value)
|
|
||||||
-- if not list or not value then return nil end
|
|
||||||
for k,v in pairs(list) do
|
|
||||||
if v == value then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.find(list, cb)
|
|
||||||
-- if not list or not cb then return nil end
|
|
||||||
for k,v in pairs(list) do
|
|
||||||
if cb(v) then
|
|
||||||
return v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.findIndex(list, cb)
|
|
||||||
-- if not list or not cb then return nil end
|
|
||||||
for k,v in pairs(list) do
|
|
||||||
if cb(v) then
|
|
||||||
return k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.tostring(list)
|
|
||||||
local result = {}
|
|
||||||
for i, v in ipairs(list) do
|
|
||||||
table.insert(result, tostring(v))
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
function table.removeKey(list, key)
|
|
||||||
if list[key] then
|
|
||||||
local r = list[key]
|
|
||||||
list[key] = nil
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
Loading…
Add table
Add a link
Reference in a new issue