Part 7
This commit is contained in:
parent
b0037c93fc
commit
f73db9e98f
2
resources/[voice]/pma-voice/.gitignore
vendored
Normal file
2
resources/[voice]/pma-voice/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.idea
|
||||
.vscode
|
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.
|
187
resources/[voice]/pma-voice/README.md
Normal file
187
resources/[voice]/pma-voice/README.md
Normal file
@ -0,0 +1,187 @@
|
||||
## PLEASE NOTE: Currently master branch has some breaking changes
|
||||
|
||||
If you previously used `voice_defaultPhoneVolume` you will instead need to use `voice_defaultCallVolume`
|
||||
If you previously used `voice_enablePhones` you will instead need to use `voice_enableCalls`
|
||||
|
||||
If you were previously using the state bag getter `Player(source).state.phone` you will instead need to use `Player(source).state.call`
|
||||
|
||||
# pma-voice
|
||||
A voice system designed around the use of FiveM/RedM internal mumble server.
|
||||
|
||||
## Support
|
||||
|
||||
Please report any issues you have in the GitHub [Issues](https://github.com/AvarianKnight/pma-voice/issues)
|
||||
|
||||
### NOTE: It is expected for servers to be on the latest recommended version, which you can find [here for Windows](https://runtime.fivem.net/artifacts/fivem/build_server_windows/master/) and [here for Linux](https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/).
|
||||
|
||||
# Compatibility Notice:
|
||||
|
||||
This script is not compatible with other voice systems (duh), that means if you have vMenus voice chat you will **have** to [disable](https://docs.vespura.com/vmenu/faq/#q-how-do-i-disable-voice-chat) it.
|
||||
|
||||
Please do not override `NetworkSetTalkerProximity`, `MumbleSetTalkerProximity`, `MumbleSetAudioInputDistance`, `MumbleSetAudioOutputDistance` or `NetworkSetVoiceActive` in any of your other scripts as there have been cases where it breaks pma-voice.
|
||||
|
||||
# Credits
|
||||
|
||||
- @Frazzle for mumble-voip (for which the concept came from)
|
||||
- @pichotm for pVoice (where the grid concept came from)
|
||||
|
||||
# FiveM/RedM Config
|
||||
|
||||
### NOTE: Only use one of the Audio options (don't enable 3d Audio & Native Audio at the same time), its also recommended to always use voice_useSendingRangeOnly.
|
||||
|
||||
You only need to add the convar **if** you're changing the value.
|
||||
|
||||
All of the configs here are set using `setr [voice_configOption] [boolean]`
|
||||
|
||||
Native audio will not work on RedM, you will have to use 3d audio.
|
||||
|
||||
| ConVar | Default | Description | Parameter(s) |
|
||||
|----------------------------|---------|---------------------------------------------------------------|--------------|
|
||||
| voice_useNativeAudio | false | **This will not work for RedM** Uses the games native audio, will add 3d sound, echo, reverb, and more. **Required for submixs** | boolean |
|
||||
| voice_use2dAudio | false | Uses 2d audio, will result in same volume sound no matter where they're at until they leave proximity. | boolean
|
||||
| voice_use3dAudio | false | Uses 3d audio | boolean |
|
||||
| voice_useSendingRangeOnly | false | Only allows you to hear people within your hear/send range, prevents people from connecting to your mumble server and trolling. | boolean |
|
||||
|
||||
# Config
|
||||
|
||||
### PLEASE NOTE: Any keybind changes only affect new players, if you want to change your key bind go to Key Bindings -> FiveM -> Look for keybinds under 'pma-voice'.
|
||||
|
||||
All of the config is done via ConVars in order to streamline the process.
|
||||
|
||||
The ints are used like a boolean to 0 would be false, 1 true.
|
||||
|
||||
All of the configs here are set using `setr [voice_configOption] [int]` OR `setr [voice_configOption] "[string]"`
|
||||
|
||||
#### Note: If a convar defaults to 1 (true) you don't have set it again unless you want to disable it.
|
||||
|
||||
### General Voice Settings
|
||||
|
||||
| ConVar | Default | Description | Parameter(s) |
|
||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
||||
| voice_enableUi | 1 | Enables the built in user interface | int |
|
||||
| voice_enableProximityCycle | 1 | Enables the usage of the F11 proximity key, if disabled players are stuck on the first proximity | int |
|
||||
| voice_defaultCycle | F11 | The default key to cycle the players proximity. You can find a list of valid keys [in the Cfx docs](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/) | string |
|
||||
| voice_defaultRadioVolume | 30 | The default volume to set the radio to (has to be between 1 and 100) *NOTE: Only new joins will have the new value, players that already joined will not.* | float |
|
||||
| voice_defaultCallVolume | 60 | The default volume to set the call to (has to be between 1 and 100) *NOTE: Only new joins will have the new value, players that already joined will not.* | float |
|
||||
| voice_defaultVoiceMode | 2 | Default proximity voice value when player joins server. (Voice Modes; 1:Whisper, 2:Normal, 3:Shouting) | int |
|
||||
|
||||
### Call & Radio
|
||||
|
||||
| ConVar | Default | Description | Parameter(s) |
|
||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
||||
| voice_enableRadios | 1 | Enables the radio sub-modules | int |
|
||||
| voice_enableCalls | 1 | Enables the call sub-modules | int |
|
||||
| voice_enableSubmix | 1 | Enables the submix which adds a radio/call style submix to their voice **NOTE: Submixs require native audio** | int |
|
||||
| voice_enableRadioAnim | 0 | Enables (grab shoulder mic) animation while talking on the radio. | int |
|
||||
| voice_defaultRadio | LMENU | The default key to use the radio. You can find a list of valid keys [in the FiveM docs](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/) | string |
|
||||
|
||||
### Sync
|
||||
|
||||
| ConVar | Default | Description | Parameter(s) |
|
||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
||||
| voice_refreshRate | 200 | How often the UI/Proximity is refreshed | int |
|
||||
|
||||
### External Server & Misc.
|
||||
| ConVar | Default | Description | Parameter(s) |
|
||||
|-------------------------|---------|--------------------------------------------------------------------|--------------|
|
||||
| voice_allowSetIntent | 1 | Whether or not to allow players to set their audio intents (you can see more [here](https://docs.fivem.net/natives/?_0x6383526B)) | int |
|
||||
| voice_externalAddress | none | The external address to use to connect to the mumble server | string |
|
||||
| voice_externalPort | 0 | The external port to use | int |
|
||||
| voice_debugMode | 0 | 1 for basic logs, 4 for verbose logs | int |
|
||||
| voice_externalDisallowJoin | 0 | Disables players being allowed to join the server, should only be used if you're using a FXServer as a external mumble server. | int |
|
||||
| voice_hideEndpoints | 1 | Hides the mumble address in logs *NOTE: You should only care to hide this for a external server.* | int |
|
||||
|
||||
|
||||
|
||||
### Aces
|
||||
|
||||
pma-voice comes with a built in /muteply (tgtPly) (duration) command, in order to allow your staff to use it you will have to grand them the ace!
|
||||
|
||||
Example:
|
||||
`add_ace group.superadmin command.muteply allow;`
|
||||
|
||||
This would only allow the superadmin group to mute players.
|
||||
|
||||
### Exports
|
||||
|
||||
#### Client
|
||||
|
||||
##### Setters
|
||||
|
||||
| Export | Description | Parameter(s) |
|
||||
|---------------------|-----------------------------|--------------|
|
||||
| [setVoiceProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any |
|
||||
| [setRadioChannel](docs/client-setters/setRadioChannel.md) | Set radio channel | int |
|
||||
| [setCallChannel](docs/client-setters/setCallChannel.md) | Set call channel | int |
|
||||
| [setRadioVolume](docs/client-setters/setRadioVolume.md) | Set radio volume for player | int |
|
||||
| [setCallVolume](docs/client-setters/setCallVolume.md) | Set call volume for player | int |
|
||||
| [addPlayerToRadio](docs/client-setters/setRadioChannel.md) | Set radio channel | int |
|
||||
| [addPlayerToCall](docs/client-setters/setCallChannel.md) | Set call channel | int |
|
||||
| [removePlayerFromRadio](docs/client-setters/removePlayerFromRadio.md) | Remove player from radio | |
|
||||
| [removePlayerFromCall](docs/client-setters/removePlayerFromCall.md) | Remove player from call | |
|
||||
|
||||
##### Toggles
|
||||
|
||||
| Export | Description | Parameter(s) |
|
||||
|---------------------|--------------------------------------------------------|--------------|
|
||||
| toggleMutePlayer | Toggles the selected player muted for the local client | int |
|
||||
|
||||
Supported from mumble-voip / toko-voip
|
||||
|
||||
| Export | Description | Parameter(s) |
|
||||
|-----------------------|--------------------------|--------------|
|
||||
| [SetMumbleProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any |
|
||||
| [SetTokoProperty](docs/client-setters/setVoiceProperty.md) | Set config options | string, any |
|
||||
| [SetRadioChannel](docs/client-setters/setRadioChannel.md) | Set radio channel | int |
|
||||
| [SetCallChannel](docs/client-setters/setCallChannel.md) | Set call channel | int |
|
||||
|
||||
#### Getters
|
||||
|
||||
The majority of setters are done through player states, while a small
|
||||
|
||||
|
||||
| State Bag | Description | Return Type |
|
||||
|---------------|--------------------------------------------------------------|--------------|
|
||||
| [proximity](docs/state-getters/stateBagGetters.md) | Returns a table with the mode index, distance, and mode name | table |
|
||||
| [radioChannel](docs/state-getters/stateBagGetters.md) | Returns the players current radio channel, or 0 for none | int |
|
||||
| [callChannel](docs/state-getters/stateBagGetters.md) | Returns the players current call channel, or 0 for none | int |
|
||||
|
||||
#### Events
|
||||
|
||||
These are events designed for third-party resource integration. These are emitted only to the current client.
|
||||
|
||||
| Event | Description | Event Params |
|
||||
|--------------------------|--------------------------------------------------------------|----------------|
|
||||
| [pma-voice:settingsCallback](docs/client-getters/events.md) | When emited it will return the current pma-voice settings. | cb(voiceSettings) |
|
||||
| [pma-voice:radioActive](docs/client-getters/events.md) | Triggered when the radio is activated / deactivated | boolean |
|
||||
| [pma-voice:setTalkingMode](docs/client-getters/events.md) | Triggered on proximity mode change with the voice mode id | int |
|
||||
|
||||
|
||||
#### Server
|
||||
|
||||
##### Setters
|
||||
|
||||
| Export | Description | Parameter(s) |
|
||||
|----------------------|--------------------------------------|--------------|
|
||||
| [setPlayerRadio](docs/server-setters/setPlayerRadio.md) | Sets the players radio channel | int, int |
|
||||
| [setPlayerCall](docs/server-setters/setPlayerCall.md) | Sets the players call channel | int, int |
|
||||
| [addChannelCheck](docs/server-setters/addChannelCheck.md) | Adds a channel check to the players radio channel | int, function |
|
||||
|
||||
|
||||
##### Getters
|
||||
|
||||
###### State Bags
|
||||
You can access the state with `Player(source).state['state bag here']`
|
||||
|
||||
| State Bag | Description | Return Type |
|
||||
|---------------|--------------------------------------------------------------|--------------|
|
||||
| [proximity](docs/state-getters/stateBagGetters.md) | Returns a table with the mode index, distance, and mode name | table |
|
||||
| [radioChannel](docs/state-getters/stateBagGetters.md) | Returns the players current radio channel, or 0 for none | int |
|
||||
| [callChannel](docs/state-getters/stateBagGetters.md) | Returns the players current call channel, or 0 for none | int |
|
||||
| [voiceIntent](docs/state-getters/stateBagGetters.md) | Returns the players current voice intent, either 'speech' or 'music' | string |
|
||||
|
||||
###### Exports
|
||||
|
||||
| Export | Description | Parameter(s) |
|
||||
|------------------------------|---------------------------------------------------|------|
|
||||
| [getPlayersInRadioChannel](docs/server-getters/getPlayersInRadioChannel.md) | Gets the current players in a radio channel | int |
|
10
resources/[voice]/pma-voice/TODO.md
Normal file
10
resources/[voice]/pma-voice/TODO.md
Normal file
@ -0,0 +1,10 @@
|
||||
## TODO
|
||||
- [ ] Ability to display radio members on the client.
|
||||
- [ ] Use commands to define voiceModes in shared.lua and only leave debug logs in shared.lua.
|
||||
- [ ] Convert the UI to React.
|
||||
- [ ] Multiple radio channels.
|
||||
|
||||
## DONE
|
||||
- [ x ] Implement a easy way to get the players current radio channel on the server.
|
||||
- [ x ] Add the ability to override proximity with exports.
|
||||
- [ x ] Rename everything that uses 'phone' to 'call' for consistency.
|
85
resources/[voice]/pma-voice/client/commands.lua
Normal file
85
resources/[voice]/pma-voice/client/commands.lua
Normal file
@ -0,0 +1,85 @@
|
||||
local wasProximityDisabledFromOverride = false
|
||||
disableProximityCycle = false
|
||||
RegisterCommand('setvoiceintent', function(source, args)
|
||||
if GetConvarInt('voice_allowSetIntent', 1) == 1 then
|
||||
local intent = args[1]
|
||||
if intent == 'speech' then
|
||||
MumbleSetAudioInputIntent(`speech`)
|
||||
elseif intent == 'music' then
|
||||
MumbleSetAudioInputIntent(`music`)
|
||||
end
|
||||
LocalPlayer.state:set('voiceIntent', intent, true)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterCommand('resetvoice', function() -- chat command, You can change it to your liking.
|
||||
NetworkClearVoiceChannel()
|
||||
NetworkSessionVoiceLeave()
|
||||
Wait(50)
|
||||
NetworkSetVoiceActive(false)
|
||||
MumbleClearVoiceTarget(2)
|
||||
Wait(1000)
|
||||
MumbleSetVoiceTarget(2)
|
||||
NetworkSetVoiceActive(true)
|
||||
end)
|
||||
|
||||
-- TODO: Better implementation of this?
|
||||
RegisterCommand('vol', function(_, args)
|
||||
if not args[1] then return end
|
||||
setVolume(tonumber(args[1]))
|
||||
end)
|
||||
|
||||
exports('setAllowProximityCycleState', function(state)
|
||||
type_check({state, "boolean"})
|
||||
disableProximityCycle = state
|
||||
end)
|
||||
|
||||
function setProximityState(proximityRange, isCustom)
|
||||
local voiceModeData = Cfg.voiceModes[mode]
|
||||
MumbleSetTalkerProximity(proximityRange + 0.0)
|
||||
LocalPlayer.state:set('proximity', {
|
||||
index = mode,
|
||||
distance = proximityRange,
|
||||
mode = isCustom and "Custom" or voiceModeData[2],
|
||||
}, true)
|
||||
sendUIMessage({
|
||||
-- JS expects this value to be - 1, "custom" voice is on the last index
|
||||
voiceMode = isCustom and #Cfg.voiceModes or mode - 1
|
||||
})
|
||||
end
|
||||
|
||||
exports("overrideProximityRange", function(range, disableCycle)
|
||||
type_check({range, "number"})
|
||||
setProximityState(range, true)
|
||||
if disableCycle then
|
||||
disableProximityCycle = true
|
||||
wasProximityDisabledFromOverride = true
|
||||
end
|
||||
end)
|
||||
|
||||
exports("clearProximityOverride", function()
|
||||
local voiceModeData = Cfg.voiceModes[mode]
|
||||
setProximityState(voiceModeData[1], false)
|
||||
if wasProximityDisabledFromOverride then
|
||||
disableProximityCycle = false
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterCommand('cycleproximity', function()
|
||||
-- Proximity is either disabled, or manually overwritten.
|
||||
if GetConvarInt('voice_enableProximityCycle', 1) ~= 1 or disableProximityCycle then return end
|
||||
local newMode = mode + 1
|
||||
|
||||
-- If we're within the range of our voice modes, allow the increase, otherwise reset to the first state
|
||||
if newMode <= #Cfg.voiceModes then
|
||||
mode = newMode
|
||||
else
|
||||
mode = 1
|
||||
end
|
||||
|
||||
setProximityState(Cfg.voiceModes[mode][1], false)
|
||||
TriggerEvent('pma-voice:setTalkingMode', mode)
|
||||
end, false)
|
||||
if gameVersion == 'fivem' then
|
||||
RegisterKeyMapping('cycleproximity', 'Skift talelængde', 'keyboard', GetConvar('voice_defaultCycle', 'F11'))
|
||||
end
|
42
resources/[voice]/pma-voice/client/events.lua
Normal file
42
resources/[voice]/pma-voice/client/events.lua
Normal file
@ -0,0 +1,42 @@
|
||||
function handleInitialState()
|
||||
local voiceModeData = Cfg.voiceModes[mode]
|
||||
MumbleSetTalkerProximity(voiceModeData[1] + 0.0)
|
||||
MumbleClearVoiceTarget(voiceTarget)
|
||||
MumbleSetVoiceTarget(voiceTarget)
|
||||
MumbleSetVoiceChannel(playerServerId)
|
||||
|
||||
while MumbleGetVoiceChannelFromServerId(playerServerId) ~= playerServerId do
|
||||
Wait(250)
|
||||
MumbleSetVoiceChannel(playerServerId)
|
||||
end
|
||||
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, playerServerId)
|
||||
|
||||
addNearbyPlayers()
|
||||
end
|
||||
|
||||
AddEventHandler('mumbleConnected', function(address, isReconnecting)
|
||||
logger.info('Connected to mumble server with address of %s, is this a reconnect %s', GetConvarInt('voice_hideEndpoints', 1) == 1 and 'HIDDEN' or address, isReconnecting)
|
||||
|
||||
logger.log('Connecting to mumble, setting targets.')
|
||||
-- don't try to set channel instantly, we're still getting data.
|
||||
local voiceModeData = Cfg.voiceModes[mode]
|
||||
LocalPlayer.state:set('proximity', {
|
||||
index = mode,
|
||||
distance = voiceModeData[1],
|
||||
mode = voiceModeData[2],
|
||||
}, true)
|
||||
|
||||
handleInitialState()
|
||||
|
||||
logger.log('Finished connection logic')
|
||||
end)
|
||||
|
||||
AddEventHandler('mumbleDisconnected', function(address)
|
||||
logger.info('Disconnected from mumble server with address of %s', GetConvarInt('voice_hideEndpoints', 1) == 1 and 'HIDDEN' or address)
|
||||
end)
|
||||
|
||||
-- TODO: Convert the last Cfg to a Convar, while still keeping it simple.
|
||||
AddEventHandler('pma-voice:settingsCallback', function(cb)
|
||||
cb(Cfg)
|
||||
end)
|
46
resources/[voice]/pma-voice/client/init/init.lua
Normal file
46
resources/[voice]/pma-voice/client/init/init.lua
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
AddEventHandler('onClientResourceStart', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then
|
||||
return
|
||||
end
|
||||
print('Starting script initialization')
|
||||
|
||||
-- Some people modify pma-voice and mess up the resource Kvp, which means that if someone
|
||||
-- joins another server that has pma-voice, it will error out, this will catch and fix the kvp.
|
||||
local success = pcall(function()
|
||||
local micClicksKvp = GetResourceKvpString('pma-voice_enableMicClicks')
|
||||
if not micClicksKvp then
|
||||
SetResourceKvp('pma-voice_enableMicClicks', "true")
|
||||
else
|
||||
if micClicksKvp ~= 'true' and micClicksKvp ~= 'false' then
|
||||
error('Invalid Kvp, throwing error for automatic fix')
|
||||
end
|
||||
micClicks = micClicksKvp
|
||||
end
|
||||
end)
|
||||
|
||||
if not success then
|
||||
logger.warn('Failed to load resource Kvp, likely was inappropriately modified by another server, resetting the Kvp.')
|
||||
SetResourceKvp('pma-voice_enableMicClicks', "true")
|
||||
micClicks = 'true'
|
||||
end
|
||||
sendUIMessage({
|
||||
uiEnabled = GetConvarInt("voice_enableUi", 1) == 1,
|
||||
voiceModes = json.encode(Cfg.voiceModes),
|
||||
voiceMode = mode - 1
|
||||
})
|
||||
|
||||
local radioChannel = LocalPlayer.state.radioChannel or 0
|
||||
local callChannel = LocalPlayer.state.callChannel or 0
|
||||
|
||||
-- Reinitialize channels if they're set.
|
||||
if radioChannel ~= 0 then
|
||||
setRadioChannel(radioChannel)
|
||||
end
|
||||
|
||||
if callChannel ~= 0 then
|
||||
setCallChannel(callChannel)
|
||||
end
|
||||
|
||||
print('Script initialization finished.')
|
||||
end)
|
298
resources/[voice]/pma-voice/client/init/main.lua
Normal file
298
resources/[voice]/pma-voice/client/init/main.lua
Normal file
@ -0,0 +1,298 @@
|
||||
local mutedPlayers = {}
|
||||
|
||||
-- we can't use GetConvarInt because its not a integer, and theres no way to get a float... so use a hacky way it is!
|
||||
local volumes = {
|
||||
-- people are setting this to 1 instead of 1.0 and expecting it to work.
|
||||
['radio'] = GetConvarInt('voice_defaultRadioVolume', 60) / 100,
|
||||
['call'] = GetConvarInt('voice_defaultCallVolume', 60) / 100,
|
||||
}
|
||||
|
||||
radioEnabled, radioPressed, mode = true, false, GetConvarInt('voice_defaultVoiceMode', 2)
|
||||
radioData = {}
|
||||
callData = {}
|
||||
submixIndicies = {}
|
||||
--- function setVolume
|
||||
--- Toggles the players volume
|
||||
---@param volume number between 0 and 100
|
||||
---@param volumeType string the volume type (currently radio & call) to set the volume of (opt)
|
||||
function setVolume(volume, volumeType)
|
||||
type_check({volume, "number"})
|
||||
local volume = volume / 100
|
||||
|
||||
if volumeType then
|
||||
local volumeTbl = volumes[volumeType]
|
||||
if volumeTbl then
|
||||
LocalPlayer.state:set(volumeType, volume, true)
|
||||
volumes[volumeType] = volume
|
||||
resyncVolume(volumeType, volume)
|
||||
else
|
||||
error(('setVolume got a invalid volume type %s'):format(volumeType))
|
||||
end
|
||||
else
|
||||
for volumeType, _ in pairs(volumes) do
|
||||
volumes[volumeType] = volume
|
||||
LocalPlayer.state:set(volumeType, volume, true)
|
||||
end
|
||||
resyncVolume("all", volume)
|
||||
end
|
||||
end
|
||||
|
||||
exports('setRadioVolume', function(vol)
|
||||
setVolume(vol, 'radio')
|
||||
end)
|
||||
exports('getRadioVolume', function()
|
||||
return volumes['radio']
|
||||
end)
|
||||
exports("setCallVolume", function(vol)
|
||||
setVolume(vol, 'call')
|
||||
end)
|
||||
exports('getCallVolume', function()
|
||||
return volumes['call']
|
||||
end)
|
||||
|
||||
|
||||
-- default submix incase people want to fiddle with it.
|
||||
-- freq_low = 389.0
|
||||
-- freq_hi = 3248.0
|
||||
-- fudge = 0.0
|
||||
-- rm_mod_freq = 0.0
|
||||
-- rm_mix = 0.16
|
||||
-- o_freq_lo = 348.0
|
||||
-- 0_freq_hi = 4900.0
|
||||
|
||||
if gameVersion == 'fivem' then
|
||||
local radioEffectId = CreateAudioSubmix('Radio')
|
||||
SetAudioSubmixEffectRadioFx(radioEffectId, 0)
|
||||
-- This is a GetHashKey on purpose, backticks break treesitter in nvim :|
|
||||
SetAudioSubmixEffectParamInt(radioEffectId, 0, GetHashKey('default'), 1)
|
||||
SetAudioSubmixOutputVolumes(
|
||||
radioEffectId,
|
||||
0,
|
||||
1.0 --[[ frontLeftVolume ]],
|
||||
0.25 --[[ frontRightVolume ]],
|
||||
0.0 --[[ rearLeftVolume ]],
|
||||
0.0 --[[ rearRightVolume ]],
|
||||
1.0 --[[ channel5Volume ]],
|
||||
1.0 --[[ channel6Volume ]]
|
||||
)
|
||||
AddAudioSubmixOutput(radioEffectId, 0)
|
||||
submixIndicies['radio'] = radioEffectId
|
||||
|
||||
local callEffectId = CreateAudioSubmix('Call')
|
||||
SetAudioSubmixOutputVolumes(
|
||||
callEffectId,
|
||||
1,
|
||||
0.10 --[[ frontLeftVolume ]],
|
||||
0.50 --[[ frontRightVolume ]],
|
||||
0.0 --[[ rearLeftVolume ]],
|
||||
0.0 --[[ rearRightVolume ]],
|
||||
1.0 --[[ channel5Volume ]],
|
||||
1.0 --[[ channel6Volume ]]
|
||||
)
|
||||
AddAudioSubmixOutput(callEffectId, 1)
|
||||
submixIndicies['call'] = callEffectId
|
||||
|
||||
-- Callback is expected to return data in an array, this is for compatibility sake with js, index 0 should be the name and index 1 should be the submixId
|
||||
-- the callback is sent the effectSlot it can register to, not sure if this is needed, but its here for safety
|
||||
exports("registerCustomSubmix", function(callback)
|
||||
local submixTable = callback()
|
||||
type_check({submixTable, "table"})
|
||||
local submixName, submixId = submixTable[1], submixTable[2]
|
||||
type_check({submixName, "string"}, {submixId, "number"})
|
||||
logger.info("Creating submix %s with submixId %s", submixName, submixId)
|
||||
submixIndicies[submixName] = submixId
|
||||
end)
|
||||
TriggerEvent("pma-voice:registerCustomSubmixes")
|
||||
end
|
||||
|
||||
--- export setEffectSubmix
|
||||
--- Sets a user defined audio submix for radio and phonecall effects
|
||||
---@param type string either "call" or "radio"
|
||||
---@param effectId number submix id returned from CREATE_AUDIO_SUBMIX
|
||||
exports("setEffectSubmix", function(type, effectId)
|
||||
type_check({type, "string"}, {effectId, "number"})
|
||||
if submixIndicies[type] then
|
||||
submixIndicies[type] = effectId
|
||||
end
|
||||
end)
|
||||
|
||||
function restoreDefaultSubmix(plyServerId)
|
||||
local submix = Player(plyServerId).state.submix
|
||||
local submixEffect = submixIndicies[submix]
|
||||
if not submix or not submixEffect then
|
||||
MumbleSetSubmixForServerId(plyServerId, -1)
|
||||
return
|
||||
end
|
||||
MumbleSetSubmixForServerId(plyServerId, submixEffect)
|
||||
end
|
||||
|
||||
-- used to prevent a race condition if they talk again afterwards, which would lead to their voice going to default.
|
||||
local disableSubmixReset = {}
|
||||
--- function toggleVoice
|
||||
--- Toggles the players voice
|
||||
---@param plySource number the players server id to override the volume for
|
||||
---@param enabled boolean if the players voice is getting activated or deactivated
|
||||
---@param moduleType string the volume & submix to use for the voice.
|
||||
function toggleVoice(plySource, enabled, moduleType)
|
||||
if mutedPlayers[plySource] then return end
|
||||
logger.verbose('[main] Updating %s to talking: %s with submix %s', plySource, enabled, moduleType)
|
||||
local distance = currentTargets[plySource]
|
||||
if enabled and (not distance or distance > 4.0) then
|
||||
MumbleSetVolumeOverrideByServerId(plySource, enabled and volumes[moduleType])
|
||||
if GetConvarInt('voice_enableSubmix', 1) == 1 and gameVersion == 'fivem' then
|
||||
if moduleType then
|
||||
disableSubmixReset[plySource] = true
|
||||
if submixIndicies[moduleType] then
|
||||
MumbleSetSubmixForServerId(plySource, submixIndicies[moduleType])
|
||||
end
|
||||
else
|
||||
restoreDefaultSubmix(plySource)
|
||||
end
|
||||
end
|
||||
elseif not enabled then
|
||||
if GetConvarInt('voice_enableSubmix', 1) == 1 and gameVersion == 'fivem' then
|
||||
-- garbage collect it
|
||||
disableSubmixReset[plySource] = nil
|
||||
SetTimeout(250, function()
|
||||
if not disableSubmixReset[plySource] then
|
||||
restoreDefaultSubmix(plySource)
|
||||
end
|
||||
end)
|
||||
end
|
||||
MumbleSetVolumeOverrideByServerId(plySource, -1.0)
|
||||
end
|
||||
end
|
||||
|
||||
local function updateVolumes(voiceTable, override)
|
||||
for serverId, talking in pairs(voiceTable) do
|
||||
if serverId == playerServerId then goto skip_iter end
|
||||
MumbleSetVolumeOverrideByServerId(serverId, talking and override or -1.0)
|
||||
::skip_iter::
|
||||
end
|
||||
end
|
||||
|
||||
--- resyncs the call/radio/etc volume to the new volume
|
||||
---@param volumeType any
|
||||
function resyncVolume(volumeType, newVolume)
|
||||
if volumeType == "all" then
|
||||
resyncVolume("radio", newVolume)
|
||||
resyncVolume("call", newVolume)
|
||||
elseif volumeType == "radio" then
|
||||
updateVolumes(radioData, newVolume)
|
||||
elseif volumeType == "call" then
|
||||
updateVolumes(callData, newVolume)
|
||||
end
|
||||
end
|
||||
|
||||
--- function playerTargets
|
||||
---Adds players voices to the local players listen channels allowing
|
||||
---Them to communicate at long range, ignoring proximity range.
|
||||
---@diagnostic disable-next-line: undefined-doc-param
|
||||
---@param targets table expects multiple tables to be sent over
|
||||
function playerTargets(...)
|
||||
local targets = {...}
|
||||
local addedPlayers = {
|
||||
[playerServerId] = true
|
||||
}
|
||||
|
||||
for i = 1, #targets do
|
||||
for id, _ in pairs(targets[i]) do
|
||||
-- we don't want to log ourself, or listen to ourself
|
||||
if addedPlayers[id] and id ~= playerServerId then
|
||||
logger.verbose('[main] %s is already target don\'t re-add', id)
|
||||
goto skip_loop
|
||||
end
|
||||
if not addedPlayers[id] then
|
||||
logger.verbose('[main] Adding %s as a voice target', id)
|
||||
addedPlayers[id] = true
|
||||
MumbleAddVoiceTargetPlayerByServerId(voiceTarget, id)
|
||||
end
|
||||
::skip_loop::
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- function playMicClicks
|
||||
---plays the mic click if the player has them enabled.
|
||||
---@param clickType boolean whether to play the 'on' or 'off' click.
|
||||
function playMicClicks(clickType)
|
||||
if micClicks ~= 'true' then return logger.verbose("Not playing mic clicks because client has them disabled") end
|
||||
-- TODO: Add customizable radio click volumes
|
||||
sendUIMessage({
|
||||
sound = (clickType and "audio_on" or "audio_off"),
|
||||
volume = (clickType and 0.1 or 0.03)
|
||||
})
|
||||
end
|
||||
|
||||
--- getter for mutedPlayers
|
||||
exports('getMutedPlayers', function()
|
||||
return mutedPlayers
|
||||
end)
|
||||
|
||||
--- toggles the targeted player muted
|
||||
---@param source number the player to mute
|
||||
function toggleMutePlayer(source)
|
||||
if mutedPlayers[source] then
|
||||
mutedPlayers[source] = nil
|
||||
MumbleSetVolumeOverrideByServerId(source, -1.0)
|
||||
else
|
||||
mutedPlayers[source] = true
|
||||
MumbleSetVolumeOverrideByServerId(source, 0.0)
|
||||
end
|
||||
end
|
||||
exports('toggleMutePlayer', toggleMutePlayer)
|
||||
|
||||
--- function setVoiceProperty
|
||||
--- sets the specified voice property
|
||||
---@param type string what voice property you want to change (only takes 'radioEnabled' and 'micClicks')
|
||||
---@param value any the value to set the type to.
|
||||
function setVoiceProperty(type, value)
|
||||
if type == "radioEnabled" then
|
||||
radioEnabled = value
|
||||
sendUIMessage({
|
||||
radioEnabled = value
|
||||
})
|
||||
elseif type == "micClicks" then
|
||||
local val = tostring(value)
|
||||
micClicks = val
|
||||
SetResourceKvp('pma-voice_enableMicClicks', val)
|
||||
end
|
||||
end
|
||||
exports('setVoiceProperty', setVoiceProperty)
|
||||
-- compatibility
|
||||
exports('SetMumbleProperty', setVoiceProperty)
|
||||
exports('SetTokoProperty', setVoiceProperty)
|
||||
|
||||
|
||||
-- cache their external servers so if it changes in runtime we can reconnect the client.
|
||||
local externalAddress = ''
|
||||
local externalPort = 0
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(500)
|
||||
-- only change if what we have doesn't match the cache
|
||||
if GetConvar('voice_externalAddress', '') ~= externalAddress or GetConvarInt('voice_externalPort', 0) ~= externalPort then
|
||||
externalAddress = GetConvar('voice_externalAddress', '')
|
||||
externalPort = GetConvarInt('voice_externalPort', 0)
|
||||
MumbleSetServerAddress(GetConvar('voice_externalAddress', ''), GetConvarInt('voice_externalPort', 0))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
if gameVersion == 'redm' then
|
||||
CreateThread(function()
|
||||
while true do
|
||||
if IsControlJustPressed(0, 0xA5BDCD3C --[[ Right Bracket ]]) then
|
||||
ExecuteCommand('cycleproximity')
|
||||
end
|
||||
if IsControlJustPressed(0, 0x430593AA --[[ Left Bracket ]]) then
|
||||
ExecuteCommand('+radiotalk')
|
||||
elseif IsControlJustReleased(0, 0x430593AA --[[ Left Bracket ]]) then
|
||||
ExecuteCommand('-radiotalk')
|
||||
end
|
||||
|
||||
Wait(0)
|
||||
end
|
||||
end)
|
||||
end
|
209
resources/[voice]/pma-voice/client/init/proximity.lua
Normal file
209
resources/[voice]/pma-voice/client/init/proximity.lua
Normal file
@ -0,0 +1,209 @@
|
||||
-- used when muted
|
||||
local disableUpdates = false
|
||||
local isListenerEnabled = false
|
||||
local plyCoords = GetEntityCoords(PlayerPedId())
|
||||
proximity = MumbleGetTalkerProximity()
|
||||
currentTargets = {}
|
||||
|
||||
function orig_addProximityCheck(ply)
|
||||
local tgtPed = GetPlayerPed(ply)
|
||||
local voiceRange = GetConvar('voice_useNativeAudio', 'false') == 'true' and proximity * 3 or proximity
|
||||
local distance = #(plyCoords - GetEntityCoords(tgtPed))
|
||||
return distance < voiceRange, distance
|
||||
end
|
||||
local addProximityCheck = orig_addProximityCheck
|
||||
|
||||
exports("overrideProximityCheck", function(fn)
|
||||
addProximityCheck = fn
|
||||
end)
|
||||
|
||||
exports("resetProximityCheck", function()
|
||||
addProximityCheck = orig_addProximityCheck
|
||||
end)
|
||||
|
||||
function addNearbyPlayers()
|
||||
if disableUpdates then return end
|
||||
-- update here so we don't have to update every call of addProximityCheck
|
||||
plyCoords = GetEntityCoords(PlayerPedId())
|
||||
proximity = MumbleGetTalkerProximity()
|
||||
currentTargets = {}
|
||||
MumbleClearVoiceTargetChannels(voiceTarget)
|
||||
if LocalPlayer.state.disableProximity then return end
|
||||
MumbleAddVoiceChannelListen(playerServerId)
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, playerServerId)
|
||||
|
||||
for source, _ in pairs(callData) do
|
||||
if source ~= playerServerId then
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, source)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local players = GetActivePlayers()
|
||||
for i = 1, #players do
|
||||
local ply = players[i]
|
||||
local serverId = GetPlayerServerId(ply)
|
||||
local shouldAdd, distance = addProximityCheck(ply)
|
||||
if shouldAdd then
|
||||
-- if distance then
|
||||
-- currentTargets[serverId] = distance
|
||||
-- else
|
||||
-- -- backwards compat, maybe remove in v7
|
||||
-- currentTargets[serverId] = 15.0
|
||||
-- end
|
||||
-- logger.verbose('Added %s as a voice target', serverId)
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, serverId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function setSpectatorMode(enabled)
|
||||
logger.info('Setting spectate mode to %s', enabled)
|
||||
isListenerEnabled = enabled
|
||||
local players = GetActivePlayers()
|
||||
if isListenerEnabled then
|
||||
for i = 1, #players do
|
||||
local ply = players[i]
|
||||
local serverId = GetPlayerServerId(ply)
|
||||
if serverId == playerServerId then goto skip_loop end
|
||||
logger.verbose("Adding %s to listen table", serverId)
|
||||
MumbleAddVoiceChannelListen(serverId)
|
||||
::skip_loop::
|
||||
end
|
||||
else
|
||||
for i = 1, #players do
|
||||
local ply = players[i]
|
||||
local serverId = GetPlayerServerId(ply)
|
||||
if serverId == playerServerId then goto skip_loop end
|
||||
logger.verbose("Removing %s from listen table", serverId)
|
||||
MumbleRemoveVoiceChannelListen(serverId)
|
||||
::skip_loop::
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('onPlayerJoining', function(serverId)
|
||||
if isListenerEnabled then
|
||||
MumbleAddVoiceChannelListen(serverId)
|
||||
logger.verbose("Adding %s to listen table", serverId)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('onPlayerDropped', function(serverId)
|
||||
if isListenerEnabled then
|
||||
MumbleRemoveVoiceChannelListen(serverId)
|
||||
logger.verbose("Removing %s from listen table", serverId)
|
||||
end
|
||||
end)
|
||||
|
||||
local listenerOverride = false
|
||||
exports("setListenerOverride", function(enabled)
|
||||
type_check({enabled, "boolean"})
|
||||
listenerOverride = enabled
|
||||
end)
|
||||
|
||||
-- cache talking status so we only send a nui message when its not the same as what it was before
|
||||
local lastTalkingStatus = false
|
||||
local lastRadioStatus = false
|
||||
local voiceState = "proximity"
|
||||
CreateThread(function()
|
||||
TriggerEvent('chat:addSuggestion', '/muteply', 'Muter spilleren med det angivede ID', {
|
||||
{ name = "id", help = "Spilleren der skal mutes" },
|
||||
{ name = "tid", help = "(Valgfrit) Tid i sekunder (default: 900)" }
|
||||
})
|
||||
while true do
|
||||
-- wait for mumble to reconnect
|
||||
while not MumbleIsConnected() do
|
||||
Wait(100)
|
||||
end
|
||||
-- Leave the check here as we don't want to do any of this logic
|
||||
if GetConvarInt('voice_enableUi', 1) == 1 then
|
||||
local curTalkingStatus = MumbleIsPlayerTalking(PlayerId()) == 1
|
||||
if lastRadioStatus ~= radioPressed or lastTalkingStatus ~= curTalkingStatus then
|
||||
lastRadioStatus = radioPressed
|
||||
lastTalkingStatus = curTalkingStatus
|
||||
sendUIMessage({
|
||||
usingRadio = lastRadioStatus,
|
||||
talking = lastTalkingStatus
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if voiceState == "proximity" then
|
||||
addNearbyPlayers()
|
||||
-- What a name, wowza
|
||||
local cam = GetConvarInt("voice_disableAutomaticListenerOnCamera", 0) ~= 1 and GetRenderingCam() or -1
|
||||
local isSpectating = NetworkIsInSpectatorMode() or cam ~= -1
|
||||
if not isListenerEnabled and (isSpectating or listenerOverride) then
|
||||
setSpectatorMode(true)
|
||||
elseif isListenerEnabled and not isSpectating and not listenerOverride then
|
||||
setSpectatorMode(false)
|
||||
end
|
||||
end
|
||||
|
||||
Wait(GetConvarInt('voice_refreshRate', 200))
|
||||
end
|
||||
end)
|
||||
|
||||
exports("setVoiceState", function(_voiceState, channel)
|
||||
if _voiceState ~= "proximity" and _voiceState ~= "channel" then
|
||||
logger.error("Didn't get a proper voice state, expected proximity or channel, got %s", _voiceState)
|
||||
end
|
||||
voiceState = _voiceState
|
||||
if voiceState == "channel" then
|
||||
type_check({channel, "number"})
|
||||
-- 65535 is the highest a client id can go, so we add that to the base channel so we don't manage to get onto a players channel
|
||||
channel = channel + 65535
|
||||
MumbleSetVoiceChannel(channel)
|
||||
while MumbleGetVoiceChannelFromServerId(playerServerId) ~= channel do
|
||||
Wait(250)
|
||||
end
|
||||
MumbleAddVoiceTargetChannel(voiceTarget, channel)
|
||||
elseif voiceState == "proximity" then
|
||||
handleInitialState()
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
AddEventHandler("onClientResourceStop", function(resource)
|
||||
if type(addProximityCheck) == "table" then
|
||||
local proximityCheckRef = addProximityCheck.__cfx_functionReference
|
||||
if proximityCheckRef then
|
||||
local isResource = string.match(proximityCheckRef, resource)
|
||||
if isResource then
|
||||
addProximityCheck = orig_addProximityCheck
|
||||
logger.warn('Reset proximity check to default, the original resource [%s] which provided the function restarted', resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
exports("addVoiceMode", function(distance, name)
|
||||
for i = 1, #Cfg.voiceModes do
|
||||
local voiceMode = Cfg.voiceModes[i]
|
||||
if voiceMode[2] == name then
|
||||
logger.verbose("Already had %s, overwritting instead", name)
|
||||
voiceMode[1] = distance
|
||||
return
|
||||
end
|
||||
end
|
||||
Cfg.voiceModes[#Cfg.voiceModes + 1] = {distance, name}
|
||||
end)
|
||||
|
||||
exports("removeVoiceMode", function(name)
|
||||
for i = 1, #Cfg.voiceModes do
|
||||
local voiceMode = Cfg.voiceModes[i]
|
||||
if voiceMode[2] == name then
|
||||
table.remove(Cfg.voiceModes, i)
|
||||
-- Reset our current range if we had it
|
||||
if mode == i then
|
||||
local newMode = Cfg.voiceModes[1]
|
||||
mode = 1
|
||||
setProximityState(newMode[i], false)
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end)
|
14
resources/[voice]/pma-voice/client/init/submix.lua
Normal file
14
resources/[voice]/pma-voice/client/init/submix.lua
Normal file
@ -0,0 +1,14 @@
|
||||
AddStateBagChangeHandler("submix", "", function(bagName, _, value)
|
||||
local tgtId = tonumber(bagName:gsub('player:', ''), 10)
|
||||
if not tgtId then return end
|
||||
logger.info("%s had their submix set to %s", tgtId, value)
|
||||
-- We got an invalid submix, discard we don't care about it
|
||||
if value and not submixIndicies[value] then return logger.warn("Player %s applied submix %s but it isn't valid", tgtId, value) end
|
||||
-- we don't want to reset submix if the player is talking on the radio
|
||||
if not value and not radioData[tgtId] and not callData[tgtId] then
|
||||
logger.info("Resetting submix for player %s", tgtId)
|
||||
MumbleSetSubmixForServerId(tgtId, -1)
|
||||
return
|
||||
end
|
||||
MumbleSetSubmixForServerId(tgtId, submixIndicies[value])
|
||||
end)
|
63
resources/[voice]/pma-voice/client/module/phone.lua
Normal file
63
resources/[voice]/pma-voice/client/module/phone.lua
Normal file
@ -0,0 +1,63 @@
|
||||
local callChannel = 0
|
||||
|
||||
RegisterNetEvent('pma-voice:syncCallData', function(callTable, channel)
|
||||
callData = callTable
|
||||
for tgt, _ in pairs(callTable) do
|
||||
if tgt ~= playerServerId then
|
||||
toggleVoice(tgt, true, 'call')
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('pma-voice:addPlayerToCall', function(plySource)
|
||||
toggleVoice(plySource, true, 'call')
|
||||
callData[plySource] = true
|
||||
end)
|
||||
|
||||
RegisterNetEvent('pma-voice:removePlayerFromCall', function(plySource)
|
||||
if plySource == playerServerId then
|
||||
for tgt, _ in pairs(callData) do
|
||||
if tgt ~= playerServerId then
|
||||
toggleVoice(tgt, false, 'call')
|
||||
end
|
||||
end
|
||||
callData = {}
|
||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
||||
playerTargets(radioPressed and radioData or {}, callData)
|
||||
else
|
||||
callData[plySource] = nil
|
||||
toggleVoice(plySource, false, 'call')
|
||||
if MumbleIsPlayerTalking(PlayerId()) then
|
||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
||||
playerTargets(radioPressed and radioData or {}, callData)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function setCallChannel(channel)
|
||||
if GetConvarInt('voice_enableCalls', 1) ~= 1 then return end
|
||||
TriggerServerEvent('pma-voice:setPlayerCall', channel)
|
||||
callChannel = channel
|
||||
sendUIMessage({
|
||||
callInfo = channel
|
||||
})
|
||||
end
|
||||
|
||||
exports('setCallChannel', setCallChannel)
|
||||
exports('SetCallChannel', setCallChannel)
|
||||
|
||||
exports('addPlayerToCall', function(_call)
|
||||
local call = tonumber(_call)
|
||||
if call then
|
||||
setCallChannel(call)
|
||||
end
|
||||
end)
|
||||
exports('removePlayerFromCall', function()
|
||||
setCallChannel(0)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('pma-voice:clSetPlayerCall', function(_callChannel)
|
||||
if GetConvarInt('voice_enableCalls', 1) ~= 1 then return end
|
||||
callChannel = _callChannel
|
||||
createCallThread()
|
||||
end)
|
209
resources/[voice]/pma-voice/client/module/radio.lua
Normal file
209
resources/[voice]/pma-voice/client/module/radio.lua
Normal file
@ -0,0 +1,209 @@
|
||||
local radioChannel = 0
|
||||
local radioNames = {}
|
||||
local disableRadioAnim = false
|
||||
|
||||
--- event syncRadioData
|
||||
--- syncs the current players on the radio to the client
|
||||
---@param radioTable table the table of the current players on the radio
|
||||
---@param localPlyRadioName string the local players name
|
||||
function syncRadioData(radioTable, localPlyRadioName)
|
||||
radioData = radioTable
|
||||
logger.info('[radio] Syncing radio table.')
|
||||
if GetConvarInt('voice_debugMode', 0) >= 4 then
|
||||
print('-------- RADIO TABLE --------')
|
||||
tPrint(radioData)
|
||||
print('-----------------------------')
|
||||
end
|
||||
for tgt, enabled in pairs(radioTable) do
|
||||
if tgt ~= playerServerId then
|
||||
toggleVoice(tgt, enabled, 'radio')
|
||||
end
|
||||
end
|
||||
sendUIMessage({
|
||||
radioChannel = radioChannel,
|
||||
radioEnabled = radioEnabled
|
||||
})
|
||||
if GetConvarInt("voice_syncPlayerNames", 0) == 1 then
|
||||
radioNames[playerServerId] = localPlyRadioName
|
||||
end
|
||||
end
|
||||
RegisterNetEvent('pma-voice:syncRadioData', syncRadioData)
|
||||
|
||||
--- event setTalkingOnRadio
|
||||
--- sets the players talking status, triggered when a player starts/stops talking.
|
||||
---@param plySource number the players server id.
|
||||
---@param enabled boolean whether the player is talking or not.
|
||||
function setTalkingOnRadio(plySource, enabled)
|
||||
toggleVoice(plySource, enabled, 'radio')
|
||||
radioData[plySource] = enabled
|
||||
playMicClicks(enabled)
|
||||
end
|
||||
RegisterNetEvent('pma-voice:setTalkingOnRadio', setTalkingOnRadio)
|
||||
|
||||
--- event addPlayerToRadio
|
||||
--- adds a player onto the radio.
|
||||
---@param plySource number the players server id to add to the radio.
|
||||
function addPlayerToRadio(plySource, plyRadioName)
|
||||
radioData[plySource] = false
|
||||
if GetConvarInt("voice_syncPlayerNames", 0) == 1 then
|
||||
radioNames[plySource] = plyRadioName
|
||||
end
|
||||
if radioPressed then
|
||||
logger.info('[radio] %s joined radio %s while we were talking, adding them to targets', plySource, radioChannel)
|
||||
playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
||||
else
|
||||
logger.info('[radio] %s joined radio %s', plySource, radioChannel)
|
||||
end
|
||||
end
|
||||
RegisterNetEvent('pma-voice:addPlayerToRadio', addPlayerToRadio)
|
||||
|
||||
--- event removePlayerFromRadio
|
||||
--- removes the player (or self) from the radio
|
||||
---@param plySource number the players server id to remove from the radio.
|
||||
function removePlayerFromRadio(plySource)
|
||||
if plySource == playerServerId then
|
||||
logger.info('[radio] Left radio %s, cleaning up.', radioChannel)
|
||||
for tgt, _ in pairs(radioData) do
|
||||
if tgt ~= playerServerId then
|
||||
toggleVoice(tgt, false, 'radio')
|
||||
end
|
||||
end
|
||||
sendUIMessage({
|
||||
radioChannel = 0,
|
||||
radioEnabled = radioEnabled
|
||||
})
|
||||
radioNames = {}
|
||||
radioData = {}
|
||||
playerTargets(MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
||||
else
|
||||
toggleVoice(plySource, false , 'radio')
|
||||
if radioPressed then
|
||||
logger.info('[radio] %s left radio %s while we were talking, updating targets.', plySource, radioChannel)
|
||||
playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
||||
else
|
||||
logger.info('[radio] %s has left radio %s', plySource, radioChannel)
|
||||
end
|
||||
radioData[plySource] = nil
|
||||
if GetConvarInt("voice_syncPlayerNames", 0) == 1 then
|
||||
radioNames[plySource] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
RegisterNetEvent('pma-voice:removePlayerFromRadio', removePlayerFromRadio)
|
||||
|
||||
--- function setRadioChannel
|
||||
--- sets the local players current radio channel and updates the server
|
||||
---@param channel number the channel to set the player to, or 0 to remove them.
|
||||
function setRadioChannel(channel)
|
||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||
type_check({channel, "number"})
|
||||
TriggerServerEvent('pma-voice:setPlayerRadio', channel)
|
||||
radioChannel = channel
|
||||
end
|
||||
|
||||
--- exports setRadioChannel
|
||||
--- sets the local players current radio channel and updates the server
|
||||
exports('setRadioChannel', setRadioChannel)
|
||||
-- mumble-voip compatability
|
||||
exports('SetRadioChannel', setRadioChannel)
|
||||
|
||||
--- exports removePlayerFromRadio
|
||||
--- sets the local players current radio channel and updates the server
|
||||
exports('removePlayerFromRadio', function()
|
||||
setRadioChannel(0)
|
||||
end)
|
||||
|
||||
--- exports addPlayerToRadio
|
||||
--- sets the local players current radio channel and updates the server
|
||||
---@param _radio number the channel to set the player to, or 0 to remove them.
|
||||
exports('addPlayerToRadio', function(_radio)
|
||||
local radio = tonumber(_radio)
|
||||
if radio then
|
||||
setRadioChannel(radio)
|
||||
end
|
||||
end)
|
||||
|
||||
--- exports toggleRadioAnim
|
||||
--- toggles whether the client should play radio anim or not, if the animation should be played or notvaliddance
|
||||
exports('toggleRadioAnim', function()
|
||||
disableRadioAnim = not disableRadioAnim
|
||||
TriggerEvent('pma-voice:toggleRadioAnim', disableRadioAnim)
|
||||
end)
|
||||
|
||||
-- exports disableRadioAnim
|
||||
--- returns whether the client is undercover or not
|
||||
exports('getRadioAnimState', function()
|
||||
return disableRadioAnim
|
||||
end)
|
||||
|
||||
--- check if the player is dead
|
||||
--- seperating this so if people use different methods they can customize
|
||||
--- it to their need as this will likely never be changed
|
||||
--- but you can integrate the below state bag to your death resources.
|
||||
--- LocalPlayer.state:set('isDead', true or false, false)
|
||||
function isDead()
|
||||
if LocalPlayer.state.isDead then
|
||||
return true
|
||||
elseif IsPlayerDead(PlayerId()) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
RegisterCommand('+radiotalk', function()
|
||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||
if isDead() or LocalPlayer.state.disableRadio then return end
|
||||
|
||||
if not radioPressed and radioEnabled then
|
||||
if radioChannel > 0 then
|
||||
logger.info('[radio] Start broadcasting, update targets and notify server.')
|
||||
playerTargets(radioData, MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
||||
TriggerServerEvent('pma-voice:setTalkingOnRadio', true)
|
||||
radioPressed = true
|
||||
playMicClicks(true)
|
||||
if GetConvarInt('voice_enableRadioAnim', 0) == 1 and IsPedInAnyVehicle(PlayerPedId(), false) and not disableRadioAnim then
|
||||
RequestAnimDict('random@arrests')
|
||||
while not HasAnimDictLoaded('random@arrests') do
|
||||
Wait(10)
|
||||
end
|
||||
TaskPlayAnim(PlayerPedId(), "random@arrests", "generic_radio_enter", 8.0, 2.0, -1, 50, 2.0, false, false, false)
|
||||
end
|
||||
CreateThread(function()
|
||||
TriggerEvent("pma-voice:radioActive", true)
|
||||
while radioPressed and not LocalPlayer.state.disableRadio do
|
||||
Wait(0)
|
||||
SetControlNormal(0, 249, 1.0)
|
||||
SetControlNormal(1, 249, 1.0)
|
||||
SetControlNormal(2, 249, 1.0)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end, false)
|
||||
|
||||
|
||||
RegisterCommand('-radiotalk', function()
|
||||
if (radioChannel > 0 or radioEnabled) and radioPressed then
|
||||
radioPressed = false
|
||||
MumbleClearVoiceTargetPlayers(voiceTarget)
|
||||
playerTargets(MumbleIsPlayerTalking(PlayerId()) and callData or {})
|
||||
TriggerEvent("pma-voice:radioActive", false)
|
||||
playMicClicks(false)
|
||||
if GetConvarInt('voice_enableRadioAnim', 0) == 1 then
|
||||
StopAnimTask(PlayerPedId(), "random@arrests", "generic_radio_enter", -4.0)
|
||||
end
|
||||
TriggerServerEvent('pma-voice:setTalkingOnRadio', false)
|
||||
end
|
||||
end, false)
|
||||
if gameVersion == 'fivem' then
|
||||
RegisterKeyMapping('+radiotalk', 'Talk over Radio', 'keyboard', GetConvar('voice_defaultRadio', 'LMENU'))
|
||||
end
|
||||
|
||||
--- event syncRadio
|
||||
--- syncs the players radio, only happens if the radio was set server side.
|
||||
---@param _radioChannel number the radio channel to set the player to.
|
||||
function syncRadio(_radioChannel)
|
||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||
logger.info('[radio] radio set serverside update to radio %s', radioChannel)
|
||||
radioChannel = _radioChannel
|
||||
end
|
||||
RegisterNetEvent('pma-voice:clSetPlayerRadio', syncRadio)
|
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
|
||||
exports ['pma-voice']:setRadioChannel(0)
|
||||
```
|
||||
|
||||
addPlayerToRadio is provided as a 'easier to read' alternative to setRadioChannel.
|
||||
|
||||
```lua
|
||||
-- Joins radio channel 1
|
||||
exports['pma-voice']:addPlayerToRadio(1)
|
||||
```
|
@ -0,0 +1,14 @@
|
||||
## setRadioVolume
|
||||
|
||||
## Description
|
||||
|
||||
Sets the local players radio channel volume
|
||||
|
||||
## Parameters
|
||||
|
||||
* **radioVolume**: the radio volume to set to between 0 - 100 percent
|
||||
|
||||
```lua
|
||||
-- sets the radio volume to 50 percent
|
||||
exports['pma-voice']:setRadioVolume(50)
|
||||
```
|
@ -0,0 +1,17 @@
|
||||
## setVoiceProperty | SetMumbleProperty | SetTokoProperty
|
||||
|
||||
## Description
|
||||
|
||||
Sets the voice property, currently the only use is to enable/disable radios and radio clicks.
|
||||
|
||||
## Parameters
|
||||
|
||||
* **property**: The property to set
|
||||
* **value**: The value to set the property to
|
||||
|
||||
```lua
|
||||
-- Enable the radio
|
||||
exports['pma-voice']:setVoiceProperty('radioEnabled', true)
|
||||
-- Disable radio clicks
|
||||
exports['pma-voice']:setVoiceProperty('micClicks', false)
|
||||
```
|
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
|
||||
```
|
70
resources/[voice]/pma-voice/fxmanifest.lua
Normal file
70
resources/[voice]/pma-voice/fxmanifest.lua
Normal file
@ -0,0 +1,70 @@
|
||||
game 'common'
|
||||
|
||||
fx_version 'cerulean'
|
||||
author 'AvarianKnight'
|
||||
description 'VOIP built using FiveM\'s built in mumble.'
|
||||
|
||||
dependencies {
|
||||
'/onesync',
|
||||
}
|
||||
|
||||
lua54 'yes'
|
||||
|
||||
shared_script 'shared.lua'
|
||||
|
||||
client_scripts {
|
||||
'client/utils/*',
|
||||
'client/init/proximity.lua',
|
||||
'client/init/init.lua',
|
||||
'client/init/main.lua',
|
||||
'client/init/submix.lua',
|
||||
'client/module/*.lua',
|
||||
'client/*.lua',
|
||||
}
|
||||
|
||||
server_scripts {
|
||||
'server/**/*.lua',
|
||||
'server/**/*.js'
|
||||
}
|
||||
|
||||
files {
|
||||
'ui/*.ogg',
|
||||
'ui/css/*.css',
|
||||
'ui/js/*.js',
|
||||
'ui/index.html',
|
||||
}
|
||||
|
||||
ui_page 'ui/index.html'
|
||||
|
||||
provides {
|
||||
'mumble-voip',
|
||||
'tokovoip',
|
||||
'toko-voip',
|
||||
'tokovoip_script'
|
||||
}
|
||||
|
||||
convar_category 'PMA-Voice' {
|
||||
"PMA-Voice Configuration Options",
|
||||
{
|
||||
{ "Use native audio", "$voice_useNativeAudio", "CV_BOOL", "false" },
|
||||
{ "Use 2D audio", "$voice_use2dAudio", "CV_BOOL", "false" },
|
||||
{ "Use sending range only", "$voice_useSendingRangeOnly", "CV_BOOL", "false" },
|
||||
{ "Enable UI", "$voice_enableUi", "CV_INT", "1" },
|
||||
{ "Enable F11 proximity key", "$voice_enableProximityCycle", "CV_INT", "1" },
|
||||
{ "Proximity cycle key", "$voice_defaultCycle", "CV_STRING", "F11" },
|
||||
{ "Voice radio volume", "$voice_defaultRadioVolume", "CV_INT", "30" },
|
||||
{ "Voice call volume", "$voice_defaultCallVolume", "CV_INT", "60" },
|
||||
{ "Enable radios", "$voice_enableRadios", "CV_INT", "1" },
|
||||
{ "Enable calls", "$voice_enableCalls", "CV_INT", "1" },
|
||||
{ "Enable submix", "$voice_enableSubmix", "CV_INT", "1" },
|
||||
{ "Enable radio animation", "$voice_enableRadioAnim", "CV_INT", "0" },
|
||||
{ "Radio key", "$voice_defaultRadio", "CV_STRING", "LMENU" },
|
||||
{ "UI refresh rate", "$voice_uiRefreshRate", "CV_INT", "200" },
|
||||
{ "Allow players to set audio intent", "$voice_allowSetIntent", "CV_INT", "1" },
|
||||
{ "External mumble server address", "$voice_externalAddress", "CV_STRING", "" },
|
||||
{ "External mumble server port", "$voice_externalPort", "CV_INT", "0" },
|
||||
{ "Voice debug mode", "$voice_debugMode", "CV_INT", "0" },
|
||||
{ "Disable players being allowed to join", "$voice_externalDisallowJoin", "CV_INT", "0" },
|
||||
{ "Hide server endpoints in logs", "$voice_hideEndpoints", "CV_INT", "1" },
|
||||
}
|
||||
}
|
140
resources/[voice]/pma-voice/server/main.lua
Normal file
140
resources/[voice]/pma-voice/server/main.lua
Normal file
@ -0,0 +1,140 @@
|
||||
voiceData = {}
|
||||
radioData = {}
|
||||
callData = {}
|
||||
|
||||
function defaultTable(source)
|
||||
handleStateBagInitilization(source)
|
||||
return {
|
||||
radio = 0,
|
||||
call = 0,
|
||||
lastRadio = 0,
|
||||
lastCall = 0
|
||||
}
|
||||
end
|
||||
|
||||
function handleStateBagInitilization(source)
|
||||
local plyState = Player(source).state
|
||||
if not plyState.pmaVoiceInit then
|
||||
plyState:set('radio', GetConvarInt('voice_defaultRadioVolume', 30), true)
|
||||
plyState:set('call', GetConvarInt('voice_defaultCallVolume', 60), true)
|
||||
plyState:set('submix', nil, true)
|
||||
plyState:set('proximity', {}, true)
|
||||
plyState:set('callChannel', 0, true)
|
||||
plyState:set('radioChannel', 0, true)
|
||||
plyState:set('voiceIntent', 'speech', true)
|
||||
-- We want to save voice inits because we'll automatically reinitalize calls and channels
|
||||
plyState:set('pmaVoiceInit', true, false)
|
||||
end
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
|
||||
local plyTbl = GetPlayers()
|
||||
for i = 1, #plyTbl do
|
||||
local ply = tonumber(plyTbl[i])
|
||||
voiceData[ply] = defaultTable(plyTbl[i])
|
||||
end
|
||||
|
||||
Wait(5000)
|
||||
|
||||
local nativeAudio = GetConvar('voice_useNativeAudio', 'false')
|
||||
local _3dAudio = GetConvar('voice_use3dAudio', 'false')
|
||||
local _2dAudio = GetConvar('voice_use2dAudio', 'false')
|
||||
local sendingRangeOnly = GetConvar('voice_useSendingRangeOnly', 'false')
|
||||
local gameVersion = GetConvar('gamename', 'fivem')
|
||||
|
||||
-- handle no convars being set (default drag n' drop)
|
||||
if
|
||||
nativeAudio == 'false'
|
||||
and _3dAudio == 'false'
|
||||
and _2dAudio == 'false'
|
||||
then
|
||||
if gameVersion == 'fivem' then
|
||||
SetConvarReplicated('voice_useNativeAudio', 'true')
|
||||
if sendingRangeOnly == 'false' then
|
||||
SetConvarReplicated('voice_useSendingRangeOnly', 'true')
|
||||
end
|
||||
logger.info('No convars detected for voice mode, defaulting to \'setr voice_useNativeAudio true\' and \'setr voice_useSendingRangeOnly true\'')
|
||||
else
|
||||
SetConvarReplicated('voice_use3dAudio', 'true')
|
||||
if sendingRangeOnly == 'false' then
|
||||
SetConvarReplicated('voice_useSendingRangeOnly', 'true')
|
||||
end
|
||||
logger.info('No convars detected for voice mode, defaulting to \'setr voice_use3dAudio true\' and \'setr voice_useSendingRangeOnly true\'')
|
||||
end
|
||||
elseif sendingRangeOnly == 'false' then
|
||||
logger.warn('It\'s recommended to have \'voice_useSendingRangeOnly\' set to true you can do that with \'setr voice_useSendingRangeOnly true\', this prevents players who directly join the mumble server from broadcasting to players.')
|
||||
end
|
||||
|
||||
if GetConvar('gamename', 'fivem') == 'rdr3' then
|
||||
if nativeAudio == 'true' then
|
||||
logger.warn("RedM doesn't currently support native audio, automatically switching to 3d audio. This also means that submixes will not work.")
|
||||
SetConvarReplicated('voice_useNativeAudio', 'false')
|
||||
SetConvarReplicated('voice_use3dAudio', 'true')
|
||||
end
|
||||
end
|
||||
|
||||
local radioVolume = GetConvarInt("voice_defaultRadioVolume", 30)
|
||||
local callVolume = GetConvarInt("voice_defaultCallVolume", 60)
|
||||
|
||||
-- When casted to an integer these get set to 0 or 1, so warn on these values that they don't work
|
||||
if
|
||||
radioVolume == 0 or radioVolume == 1 or
|
||||
callVolume == 0 or callVolume == 1
|
||||
then
|
||||
SetConvarReplicated("voice_defaultRadioVolume", 30)
|
||||
SetConvarReplicated("voice_defaultCallVolume", 60)
|
||||
for i = 1, 5 do
|
||||
Wait(5000)
|
||||
logger.warn("`voice_defaultRadioVolume` or `voice_defaultCallVolume` have their value set as a float, this is going to automatically be fixed but please update your convars.")
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerJoining', function()
|
||||
if not voiceData[source] then
|
||||
voiceData[source] = defaultTable(source)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("playerDropped", function()
|
||||
local source = source
|
||||
if voiceData[source] then
|
||||
local plyData = voiceData[source]
|
||||
|
||||
if plyData.radio ~= 0 then
|
||||
removePlayerFromRadio(source, plyData.radio)
|
||||
end
|
||||
|
||||
if plyData.call ~= 0 then
|
||||
removePlayerFromCall(source, plyData.call)
|
||||
end
|
||||
|
||||
voiceData[source] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
if GetConvarInt('voice_externalDisallowJoin', 0) == 1 then
|
||||
AddEventHandler('playerConnecting', function(_, _, deferral)
|
||||
deferral.defer()
|
||||
Wait(0)
|
||||
deferral.done('This server is not accepting connections.')
|
||||
end)
|
||||
end
|
||||
|
||||
-- only meant for internal use so no documentation
|
||||
function isValidPlayer(source)
|
||||
return voiceData[source]
|
||||
end
|
||||
exports('isValidPlayer', isValidPlayer)
|
||||
|
||||
function getPlayersInRadioChannel(channel)
|
||||
local returnChannel = radioData[channel]
|
||||
if returnChannel then
|
||||
return returnChannel
|
||||
end
|
||||
-- channel doesnt exist
|
||||
return {}
|
||||
end
|
||||
exports('getPlayersInRadioChannel', getPlayersInRadioChannel)
|
||||
exports('GetPlayersInRadioChannel', getPlayersInRadioChannel)
|
74
resources/[voice]/pma-voice/server/module/phone.lua
Normal file
74
resources/[voice]/pma-voice/server/module/phone.lua
Normal file
@ -0,0 +1,74 @@
|
||||
--- removes a player from the call for everyone in the call.
|
||||
---@param source number the player to remove from the call
|
||||
---@param callChannel number the call channel to remove them from
|
||||
function removePlayerFromCall(source, callChannel)
|
||||
logger.verbose('[call] Removed %s from call %s', source, callChannel)
|
||||
|
||||
callData[callChannel] = callData[callChannel] or {}
|
||||
for player, _ in pairs(callData[callChannel]) do
|
||||
TriggerClientEvent('pma-voice:removePlayerFromCall', player, source)
|
||||
end
|
||||
callData[callChannel][source] = nil
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
voiceData[source].call = 0
|
||||
end
|
||||
|
||||
--- adds a player to a call
|
||||
---@param source number the player to add to the call
|
||||
---@param callChannel number the call channel to add them to
|
||||
function addPlayerToCall(source, callChannel)
|
||||
logger.verbose('[call] Added %s to call %s', source, callChannel)
|
||||
-- check if the channel exists, if it does set the varaible to it
|
||||
-- if not create it (basically if not callData make callData)
|
||||
callData[callChannel] = callData[callChannel] or {}
|
||||
for player, _ in pairs(callData[callChannel]) do
|
||||
-- don't need to send to the source because they're about to get sync'd!
|
||||
if player ~= source then
|
||||
TriggerClientEvent('pma-voice:addPlayerToCall', player, source)
|
||||
end
|
||||
end
|
||||
callData[callChannel][source] = true
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
voiceData[source].call = callChannel
|
||||
TriggerClientEvent('pma-voice:syncCallData', source, callData[callChannel])
|
||||
end
|
||||
|
||||
--- set the players call channel
|
||||
---@param source number the player to set the call off
|
||||
---@param _callChannel number the channel to set the player to (or 0 to remove them from any call channel)
|
||||
function setPlayerCall(source, _callChannel)
|
||||
if GetConvarInt('voice_enableCalls', 1) ~= 1 then return end
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
local isResource = GetInvokingResource()
|
||||
local plyVoice = voiceData[source]
|
||||
local callChannel = tonumber(_callChannel)
|
||||
if not callChannel then
|
||||
-- only full error if its sent from another server-side resource
|
||||
if isResource then
|
||||
error(("'callChannel' expected 'number', got: %s"):format(type(_callChannel)))
|
||||
else
|
||||
return logger.warn("%s sent a invalid call, 'callChannel' expected 'number', got: %s", source,type(_callChannel))
|
||||
end
|
||||
end
|
||||
if isResource then
|
||||
-- got set in a export, need to update the client to tell them that their call
|
||||
-- changed
|
||||
TriggerClientEvent('pma-voice:clSetPlayerCall', source, callChannel)
|
||||
end
|
||||
|
||||
Player(source).state.callChannel = callChannel
|
||||
|
||||
if callChannel ~= 0 and plyVoice.call == 0 then
|
||||
addPlayerToCall(source, callChannel)
|
||||
elseif callChannel == 0 then
|
||||
removePlayerFromCall(source, plyVoice.call)
|
||||
elseif plyVoice.call > 0 then
|
||||
removePlayerFromCall(source, plyVoice.call)
|
||||
addPlayerToCall(source, callChannel)
|
||||
end
|
||||
end
|
||||
exports('setPlayerCall', setPlayerCall)
|
||||
|
||||
RegisterNetEvent('pma-voice:setPlayerCall', function(callChannel)
|
||||
setPlayerCall(source, callChannel)
|
||||
end)
|
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)
|
95
resources/[voice]/pma-voice/shared.lua
Normal file
95
resources/[voice]/pma-voice/shared.lua
Normal file
@ -0,0 +1,95 @@
|
||||
Cfg = {}
|
||||
|
||||
voiceTarget = 1
|
||||
|
||||
gameVersion = GetGameName()
|
||||
|
||||
-- these are just here to satisfy linting
|
||||
if not IsDuplicityVersion() then
|
||||
LocalPlayer = LocalPlayer
|
||||
playerServerId = GetPlayerServerId(PlayerId())
|
||||
end
|
||||
Player = Player
|
||||
Entity = Entity
|
||||
|
||||
if GetConvar('voice_useNativeAudio', 'false') == 'true' then
|
||||
-- native audio distance seems to be larger then regular gta units
|
||||
Cfg.voiceModes = {
|
||||
{1.5, "Hvisker"}, -- Whisper speech distance in gta distance units
|
||||
{3.0, "Normal"}, -- Normal speech distance in gta distance units
|
||||
{6.0, "Råber"} -- Shout speech distance in gta distance units
|
||||
}
|
||||
else
|
||||
Cfg.voiceModes = {
|
||||
{3.0, "Hvisker"}, -- Whisper speech distance in gta distance units
|
||||
{7.0, "Normal"}, -- Normal speech distance in gta distance units
|
||||
{15.0, "Råber"} -- Shout speech distance in gta distance units
|
||||
}
|
||||
end
|
||||
|
||||
logger = {
|
||||
log = function(message, ...)
|
||||
print((message):format(...))
|
||||
end,
|
||||
info = function(message, ...)
|
||||
if GetConvarInt('voice_debugMode', 0) >= 1 then
|
||||
print(('[info] ' .. message):format(...))
|
||||
end
|
||||
end,
|
||||
warn = function(message, ...)
|
||||
print(('[^1WARNING^7] ' .. message):format(...))
|
||||
end,
|
||||
error = function(message, ...)
|
||||
error((message):format(...))
|
||||
end,
|
||||
verbose = function(message, ...)
|
||||
if GetConvarInt('voice_debugMode', 0) >= 4 then
|
||||
print(('[verbose] ' .. message):format(...))
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
function tPrint(tbl, indent)
|
||||
indent = indent or 0
|
||||
for k, v in pairs(tbl) do
|
||||
local tblType = type(v)
|
||||
local formatting = string.rep(" ", indent) .. k .. ": "
|
||||
|
||||
if tblType == "table" then
|
||||
print(formatting)
|
||||
tPrint(v, indent + 1)
|
||||
elseif tblType == 'boolean' then
|
||||
print(formatting .. tostring(v))
|
||||
elseif tblType == "function" then
|
||||
print(formatting .. tostring(v))
|
||||
else
|
||||
print(formatting .. v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function types(args)
|
||||
local argType = type(args[1])
|
||||
for i = 2, #args do
|
||||
local arg = args[i]
|
||||
if argType == arg then
|
||||
return true, argType
|
||||
end
|
||||
end
|
||||
return false, argType
|
||||
end
|
||||
|
||||
--- does a type check and errors if an invalid type is sent
|
||||
---@param ... table a table with the variable being the first argument and the expected type being the second
|
||||
function type_check(...)
|
||||
local vars = {...}
|
||||
for i = 1, #vars do
|
||||
local var = vars[i]
|
||||
local matchesType, varType = types(var)
|
||||
if not matchesType then
|
||||
table.remove(var, 1)
|
||||
error(("Invalid type sent to argument #%s, expected %s, got %s"):format(i, table.concat(var, "|"), varType))
|
||||
end
|
||||
end
|
||||
end
|
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}
|
23
resources/[voice]/pma-voice/ui/index.html
Normal file
23
resources/[voice]/pma-voice/ui/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!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])+" [Distance] ",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] }} [Distance]
|
||||
</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",
|
||||
|
||||
}
|
2
resources/[voice]/qb-radio/.gitattributes
vendored
Normal file
2
resources/[voice]/qb-radio/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
674
resources/[voice]/qb-radio/LICENSE
Normal file
674
resources/[voice]/qb-radio/LICENSE
Normal file
@ -0,0 +1,674 @@
|
||||
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>.
|
67
resources/[voice]/qb-radio/README.md
Normal file
67
resources/[voice]/qb-radio/README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# qb-radio
|
||||
qb-radio Nopixel Inspired Radio for qb-core
|
||||
|
||||
# License
|
||||
|
||||
QBCore Framework
|
||||
Copyright (C) 2021 Joshua Eger
|
||||
|
||||
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/>
|
||||
|
||||
## Install Video
|
||||
- [Youtube](https://youtu.be/bNrmQMvVYno)
|
||||
|
||||
## Dependencies
|
||||
- [qb-core](https://github.com/qbcore-framework/qb-core)
|
||||
- [pma-voice](https://githubmate.com/repo/AvarianKnight/pma-voice)
|
||||
- [qb-radialmenu](https://github.com/qbcore-framework/qb-radialmenu) - Optional (Access to change channels 1 - 4)
|
||||
|
||||
## Features
|
||||
- Nopixel Inspired Radio
|
||||
- Radialmenu join channel option 1 - 6
|
||||
- Inventory Image [qb-radio/imgforinvenotry]
|
||||
|
||||
## RadialMenu Events
|
||||
Add the trigger to your qb-radialmenu > config.lua (Anywhere you want someone to have access to it) for example:
|
||||
```
|
||||
id = 'joinradio1',
|
||||
title = 'Channel 1',
|
||||
icon = 'radio',
|
||||
type = 'client',
|
||||
event = 'qb-radio:client:JoinRadioChannel1',
|
||||
shouldClose = true
|
||||
```
|
||||
Credits to MonkeyWhisper for creating the event
|
||||
|
||||
## Images
|
||||
- Google search for the image of the radio https://i.imgur.com/pgRGFIB.png
|
||||

|
||||
- Preview of the radio https://youtu.be/vMr-OFP8HzU
|
||||
|
||||
## Installation
|
||||
- Replace current qb-radio with this version if you want to use it
|
||||
|
||||
1/12/2022 (Radio Effects)
|
||||
- To add the new radio sounds go to `\pma-voice\ui`
|
||||
|
||||
Inventory image - replace radio.png in your current inventory image folder with this one.
|
||||
|
||||
## Discord
|
||||
- [Join Discord](https://discord.gg/T2xX5WwmEX)
|
||||
|
||||
## Support
|
||||
- [Ko-fi Link](https://ko-fi.com/trclassic)
|
||||
|
||||
## Original qb-radio
|
||||
- [qb-radio](https://github.com/qbcore-framework/qb-radio)
|
463
resources/[voice]/qb-radio/client.lua
Normal file
463
resources/[voice]/qb-radio/client.lua
Normal file
@ -0,0 +1,463 @@
|
||||
local QBCore = exports['qb-core']:GetCoreObject()
|
||||
local PlayerData = QBCore.Functions.GetPlayerData() -- Just for resource restart (same as event handler)
|
||||
local radioMenu = false
|
||||
local onRadio = false
|
||||
local RadioChannel = 0
|
||||
local RadioVolume = 50
|
||||
local hasRadio = false
|
||||
local radioProp = nil
|
||||
|
||||
local keybindControls = {
|
||||
["`"] = 243, ["ESC"] = 322, ["F1"] = 288, ["F2"] = 289, ["F3"] = 170, ["F5"] = 166, ["F6"] = 167, ["F7"] = 168, ["F8"] = 169, ["F9"] = 56, ["F10"] = 57, ["~"] = 243, ["1"] = 157, ["2"] = 158, ["3"] = 160, ["4"] = 164, ["5"] = 165, ["6"] = 159, ["7"] = 161, ["8"] = 162, ["9"] = 163, ["-"] = 84, ["="] = 83, ["BACKSPACE"] = 177, ["TAB"] = 37, ["Q"] = 44, ["W"] = 32, ["E"] = 38, ["R"] = 45, ["T"] = 245, ["Y"] = 246, ["U"] = 303, ["P"] = 199, ["["] = 39, ["]"] = 40, ["ENTER"] = 18, ["CAPS"] = 137, ["A"] = 34, ["S"] = 8, ["D"] = 9, ["F"] = 23, ["G"] = 47, ["H"] = 74, ["K"] = 311, ["L"] = 182, ["LEFTSHIFT"] = 21, ["Z"] = 20, ["X"] = 73, ["C"] = 26, ["V"] = 0, ["B"] = 29, ["N"] = 249, ["M"] = 244, [","] = 82, ["."] = 81, ["LEFTCTRL"] = 36, ["LEFTALT"] = 19, ["SPACE"] = 22, ["RIGHTCTRL"] = 70, ["HOME"] = 213, ["PAGEUP"] = 10, ["PAGEDOWN"] = 11, ["DELETE"] = 178, ["LEFT"] = 174, ["RIGHT"] = 175, ["TOP"] = 27, ["DOWN"] = 173, ["NENTER"] = 201, ["N4"] = 108, ["N5"] = 60, ["N6"] = 107, ["N+"] = 96, ["N-"] = 97, ["N7"] = 117, ["N8"] = 61, ["N9"] = 118
|
||||
}
|
||||
|
||||
--Function
|
||||
local function LoadAnimDic(dict)
|
||||
if not HasAnimDictLoaded(dict) then
|
||||
RequestAnimDict(dict)
|
||||
while not HasAnimDictLoaded(dict) do
|
||||
Wait(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SplitStr(inputstr, sep)
|
||||
if sep == nil then
|
||||
sep = "%s"
|
||||
end
|
||||
local t = {}
|
||||
for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
|
||||
t[#t+1] = str
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local function connecttoradio(channel)
|
||||
RadioChannel = channel
|
||||
if onRadio then
|
||||
exports["pma-voice"]:setRadioChannel(0)
|
||||
else
|
||||
onRadio = true
|
||||
exports["pma-voice"]:setVoiceProperty("radioEnabled", true)
|
||||
end
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
end
|
||||
|
||||
local function closeEvent()
|
||||
TriggerEvent("InteractSound_CL:PlayOnOne","click",0.6)
|
||||
end
|
||||
|
||||
local function leaveradio()
|
||||
closeEvent()
|
||||
RadioChannel = 0
|
||||
onRadio = false
|
||||
exports["pma-voice"]:setRadioChannel(0)
|
||||
exports["pma-voice"]:setVoiceProperty("radioEnabled", false)
|
||||
QBCore.Functions.Notify(Config.messages['you_leave'] , 'error')
|
||||
end
|
||||
|
||||
local function toggleRadioAnimation(pState)
|
||||
LoadAnimDic("cellphone@")
|
||||
if pState then
|
||||
TriggerEvent("attachItemRadio","radio01")
|
||||
TaskPlayAnim(PlayerPedId(), "cellphone@", "cellphone_text_read_base", 2.0, 3.0, -1, 49, 0, 0, 0, 0)
|
||||
radioProp = CreateObject(`prop_cs_hand_radio`, 1.0, 1.0, 1.0, 1, 1, 0)
|
||||
AttachEntityToEntity(radioProp, PlayerPedId(), GetPedBoneIndex(PlayerPedId(), 57005), 0.14, 0.01, -0.02, 110.0, 120.0, -15.0, 1, 0, 0, 0, 2, 1)
|
||||
else
|
||||
StopAnimTask(PlayerPedId(), "cellphone@", "cellphone_text_read_base", 1.0)
|
||||
ClearPedTasks(PlayerPedId())
|
||||
if radioProp ~= 0 then
|
||||
DeleteObject(radioProp)
|
||||
radioProp = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function toggleRadio(toggle)
|
||||
radioMenu = toggle
|
||||
SetNuiFocus(radioMenu, radioMenu)
|
||||
if radioMenu then
|
||||
toggleRadioAnimation(true)
|
||||
SendNUIMessage({type = "open"})
|
||||
else
|
||||
toggleRadioAnimation(false)
|
||||
SendNUIMessage({type = "close"})
|
||||
DeleteObject(radioProp)
|
||||
end
|
||||
end
|
||||
|
||||
local function IsRadioOn()
|
||||
return onRadio
|
||||
end
|
||||
|
||||
local function DoRadioCheck(PlayerItems)
|
||||
local _hasRadio = false
|
||||
|
||||
for _, item in pairs(PlayerItems) do
|
||||
if item.name == "radio" then
|
||||
_hasRadio = true
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
hasRadio = _hasRadio
|
||||
end
|
||||
|
||||
--Exports
|
||||
exports("IsRadioOn", IsRadioOn)
|
||||
|
||||
--Events
|
||||
|
||||
-- Handles state right when the player selects their character and location.
|
||||
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
|
||||
PlayerData = QBCore.Functions.GetPlayerData()
|
||||
DoRadioCheck(PlayerData.items)
|
||||
end)
|
||||
|
||||
-- Resets state on logout, in case of character change.
|
||||
RegisterNetEvent('QBCore:Client:OnPlayerUnload', function()
|
||||
DoRadioCheck({})
|
||||
PlayerData = {}
|
||||
leaveradio()
|
||||
end)
|
||||
|
||||
-- Handles state when PlayerData is changed. We're just looking for inventory updates.
|
||||
RegisterNetEvent('QBCore:Player:SetPlayerData', function(val)
|
||||
PlayerData = val
|
||||
DoRadioCheck(PlayerData.items)
|
||||
end)
|
||||
|
||||
-- Handles state if resource is restarted live.
|
||||
AddEventHandler('onResourceStart', function(resource)
|
||||
if GetCurrentResourceName() == resource then
|
||||
PlayerData = QBCore.Functions.GetPlayerData()
|
||||
DoRadioCheck(PlayerData.items)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:use', function()
|
||||
toggleRadio(not radioMenu)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:onRadioDrop', function()
|
||||
if RadioChannel ~= 0 then
|
||||
leaveradio()
|
||||
end
|
||||
end)
|
||||
|
||||
CreateThread(function()
|
||||
local OpenRadioBind = Config.keyBind.useRadio
|
||||
if IsControlPressed(1, keybindControls[OpenRadioBind]) then
|
||||
TriggerClientEvent('qb-radio:use')
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
-- NUI
|
||||
RegisterNUICallback('joinRadio', function(data, cb)
|
||||
local rchannel = tonumber(data.channel)
|
||||
if rchannel ~= nil then
|
||||
if rchannel <= Config.MaxFrequency and rchannel ~= 0 then
|
||||
if rchannel ~= RadioChannel then
|
||||
if Config.RestrictedChannels[rchannel] ~= nil then
|
||||
if Config.RestrictedChannels[rchannel][PlayerData.job.name] and PlayerData.job.onduty then
|
||||
connecttoradio(rchannel)
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['restricted_channel_error'], 'error')
|
||||
end
|
||||
else
|
||||
connecttoradio(rchannel)
|
||||
end
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['you_on_radio'] , 'error')
|
||||
end
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'] , 'error')
|
||||
end
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'] , 'error')
|
||||
end
|
||||
cb("ok")
|
||||
end)
|
||||
|
||||
RegisterNUICallback('leaveRadio', function(_, cb)
|
||||
if RadioChannel == 0 then
|
||||
QBCore.Functions.Notify(Config.messages['not_on_radio'], 'error')
|
||||
else
|
||||
leaveradio()
|
||||
end
|
||||
cb("ok")
|
||||
end)
|
||||
|
||||
RegisterNUICallback("volumeUp", function(_, cb)
|
||||
if RadioVolume <= 95 then
|
||||
RadioVolume = RadioVolume + 5
|
||||
QBCore.Functions.Notify(Config.messages["volume_radio"] .. RadioVolume, "success")
|
||||
exports["pma-voice"]:setRadioVolume(RadioVolume)
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages["decrease_radio_volume"], "error")
|
||||
end
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
RegisterNUICallback("volumeDown", function(_, cb)
|
||||
if RadioVolume >= 10 then
|
||||
RadioVolume = RadioVolume - 5
|
||||
QBCore.Functions.Notify(Config.messages["volume_radio"] .. RadioVolume, "success")
|
||||
exports["pma-voice"]:setRadioVolume(RadioVolume)
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages["increase_radio_volume"], "error")
|
||||
end
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
RegisterNUICallback("increaseradiochannel", function(_, cb)
|
||||
local newChannel = RadioChannel + 1
|
||||
connecttoradio(newChannel)
|
||||
QBCore.Functions.Notify(Config.messages["increase_decrease_radio_channel"] .. newChannel, "success")
|
||||
cb("ok")
|
||||
end)
|
||||
|
||||
RegisterNUICallback("decreaseradiochannel", function(_, cb)
|
||||
if not onRadio then return end
|
||||
local newChannel = RadioChannel - 1
|
||||
if newChannel >= 1 then
|
||||
connecttoradio(newChannel)
|
||||
QBCore.Functions.Notify(Config.messages["increase_decrease_radio_channel"] .. newChannel, "success")
|
||||
cb("ok")
|
||||
end
|
||||
end)
|
||||
RegisterCommand('openradio', function()
|
||||
if hasRadio then
|
||||
TriggerEvent('qb-radio:use')
|
||||
elseif not hasRadio then
|
||||
-- print('no radio')
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
RegisterKeyMapping('openradio', 'Open Radio', 'keyboard', Config.keyBind.openRadio)
|
||||
RegisterKeyMapping('Volup1', 'Turn Radio Up', 'keyboard', Config.keyBind.volUp1)
|
||||
RegisterKeyMapping('Radiovoldown', 'Turn Radio Down', 'keyboard', Config.keyBind.radioVolDown)
|
||||
|
||||
RegisterNUICallback('poweredOff', function(_, cb)
|
||||
leaveradio()
|
||||
cb("ok")
|
||||
end)
|
||||
|
||||
RegisterCommand('Volup1', function()
|
||||
TriggerEvent('Volup')
|
||||
end)
|
||||
|
||||
RegisterCommand('Radiovoldown', function()
|
||||
TriggerEvent('Voldown')
|
||||
end)
|
||||
|
||||
RegisterNetEvent('Volup', function()
|
||||
if RadioVolume <= 95 then
|
||||
RadioVolume = RadioVolume + 5
|
||||
QBCore.Functions.Notify(Config.messages["volume_radio"] .. RadioVolume, "success")
|
||||
exports["pma-voice"]:setRadioVolume(RadioVolume)
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages["decrease_radio_volume"], "error")
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('Voldown', function()
|
||||
if RadioVolume >= 0 then
|
||||
RadioVolume = RadioVolume - 5
|
||||
QBCore.Functions.Notify(Config.messages["volume_radio"] .. RadioVolume, "success")
|
||||
exports["pma-voice"]:setRadioVolume(RadioVolume)
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages["increase_radio_volume"], "error")
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNUICallback('escape', function(_, cb)
|
||||
toggleRadio(false)
|
||||
cb("ok")
|
||||
end)
|
||||
|
||||
--Main Thread
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(1000)
|
||||
if LocalPlayer.state.isLoggedIn and onRadio then
|
||||
if not hasRadio or PlayerData.metadata.isdead or PlayerData.metadata.inlaststand then
|
||||
if RadioChannel ~= 0 then
|
||||
leaveradio()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel1', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 1
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel2', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 2
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel3', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 3
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel4', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 4
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel5', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 5
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel6', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 5
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel7', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 5
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel8', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 5
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel9', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 5
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannel10', function(channel)
|
||||
QBCore.Functions.TriggerCallback('qb-radio:radiocheck', function(radio)
|
||||
if radio then
|
||||
local channel = 5
|
||||
exports["pma-voice"]:setRadioChannel(channel)
|
||||
if SplitStr(tostring(channel), ".")[2] ~= nil and SplitStr(tostring(channel), ".")[2] ~= "" then
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. ' MHz', 'success')
|
||||
else
|
||||
QBCore.Functions.Notify(Config.messages['joined_to_radio'] ..channel.. '.00 MHz', 'success')
|
||||
end
|
||||
elseif not radio then
|
||||
QBCore.Functions.Notify(Config.messages['invalid_radio'], 'error')
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('qb-radio:client:JoinRadioChannelToggle', function()
|
||||
leaveradio()
|
||||
end)
|
60
resources/[voice]/qb-radio/config.lua
Normal file
60
resources/[voice]/qb-radio/config.lua
Normal file
@ -0,0 +1,60 @@
|
||||
Config = {}
|
||||
|
||||
Config.keyBind = {
|
||||
useRadio = "ENTER",
|
||||
openRadio = "F7",
|
||||
volUp1 = "PAGEUP",
|
||||
radioVolDown = "PAGEDOWN"
|
||||
}
|
||||
|
||||
Config.RestrictedChannels = {
|
||||
[1] = {
|
||||
police = true,
|
||||
ambulance = true
|
||||
},
|
||||
[2] = {
|
||||
police = true,
|
||||
ambulance = true
|
||||
},
|
||||
[3] = {
|
||||
police = true,
|
||||
ambulance = true
|
||||
},
|
||||
[4] = {
|
||||
police = true,
|
||||
ambulance = true
|
||||
},
|
||||
[5] = {
|
||||
police = true
|
||||
},
|
||||
[6] = {
|
||||
police = true
|
||||
},
|
||||
[7] = {
|
||||
police = true
|
||||
},
|
||||
[8] = {
|
||||
police = true
|
||||
},
|
||||
[9] = {
|
||||
police = true,
|
||||
judge = true,
|
||||
court = true
|
||||
},
|
||||
}
|
||||
|
||||
Config.MaxFrequency = 500
|
||||
|
||||
Config.messages = {
|
||||
["not_on_radio"] = "You're not connected to a signal",
|
||||
["on_radio"] = "You're already connected to this signal",
|
||||
["joined_to_radio"] = "You're connected to: ",
|
||||
["restricted_channel_error"] = "You are not allow to connect to this signal!",
|
||||
["invalid_radio"] = "This frequency is not available.",
|
||||
["you_on_radio"] = "You're already connected to this channel",
|
||||
["you_leave"] = "You left the channel.",
|
||||
['volume_radio'] = 'New volume ',
|
||||
['decrease_radio_volume'] = 'The radio is already set to maximum volume',
|
||||
['increase_radio_volume'] = 'The radio is already set to the lowest volume',
|
||||
['increase_decrease_radio_channel'] = 'New channel ',
|
||||
}
|
19
resources/[voice]/qb-radio/fxmanifest.lua
Normal file
19
resources/[voice]/qb-radio/fxmanifest.lua
Normal file
@ -0,0 +1,19 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
description 'QB-Radio'
|
||||
version '1.0.0'
|
||||
|
||||
shared_script 'config.lua'
|
||||
|
||||
client_scripts {
|
||||
'client.lua'
|
||||
}
|
||||
|
||||
server_script 'server.lua'
|
||||
|
||||
ui_page('html/ui.html')
|
||||
|
||||
files {'html/ui.html', 'html/js/script.js', 'html/css/style.css', 'html/img/radio.png'}
|
||||
|
||||
lua54 'yes'
|
205
resources/[voice]/qb-radio/html/css/style.css
Normal file
205
resources/[voice]/qb-radio/html/css/style.css
Normal file
@ -0,0 +1,205 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Fira+Code&display=swap');
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100vh;
|
||||
display:none;
|
||||
}
|
||||
|
||||
.radio-container {
|
||||
position: absolute;
|
||||
bottom: -56vh;
|
||||
right: 5vh;
|
||||
}
|
||||
|
||||
.radio {
|
||||
width: 32vh;
|
||||
-moz-user-select: -moz-none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#submit {
|
||||
position: absolute;
|
||||
width: 3.5vh;
|
||||
height: 1.9vh;
|
||||
bottom: 45.8vh;
|
||||
left: 5.5vh;
|
||||
border-radius: 5px;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
#submit:hover::before {
|
||||
content: attr(title); display: block;
|
||||
position:relative;
|
||||
top:-20px;
|
||||
font-size:12px;
|
||||
color:white;
|
||||
background-color:rgb(255 0 0 / 0%);
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
#disconnect {
|
||||
position: absolute;
|
||||
width: 2.5vh;
|
||||
height: 4.0vh;
|
||||
bottom: 44.3vh;
|
||||
left: 9.5vh;
|
||||
border-radius: 5px;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
#disconnect:hover::before {
|
||||
content: attr(title); display: block;
|
||||
position:relative;
|
||||
top:-20px;
|
||||
font-size:12px;
|
||||
color:white;
|
||||
background-color:rgba(0, 0, 0, 0);
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
#volumeUp {
|
||||
position: absolute;
|
||||
width: 2.5vh;
|
||||
height: 4.0vh;
|
||||
bottom: 46.0vh;
|
||||
left: 13.0vh;
|
||||
border-radius: 5px;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
#volumeUp:hover::before {
|
||||
content: attr(title); display: block;
|
||||
position:relative;
|
||||
top:-20px;
|
||||
font-size:12px;
|
||||
color:white;
|
||||
background-color:rgba(0, 0, 0, 0);
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
#volumeDown {
|
||||
position: absolute;
|
||||
width: 2.5vh;
|
||||
height: 4.0vh;
|
||||
bottom: 46.0vh;
|
||||
left: 17.0vh;
|
||||
border-radius: 5px;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
#volumeDown:hover::before {
|
||||
content: attr(title); display: block;
|
||||
position:relative;
|
||||
top:-20px;
|
||||
font-size:12px;
|
||||
color:white;
|
||||
background-color:rgba(0, 0, 0, 0);
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
#increaseradiochannel {
|
||||
position: absolute;
|
||||
width: 3.5vh;
|
||||
height: 1.9vh;
|
||||
bottom: 15.2vh;
|
||||
left: 14.3vh;
|
||||
border-radius: 5px;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
#increaseradiochannel:hover::before {
|
||||
content: attr(title); display: block;
|
||||
position:relative;
|
||||
top:-20px;
|
||||
font-size:12px;
|
||||
color:white;
|
||||
background-color:rgba(0, 0, 0, 0);
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
#decreaseradiochannel {
|
||||
position: absolute;
|
||||
width: 3.5vh;
|
||||
height: 1.9vh;
|
||||
bottom: 11.2vh;
|
||||
left: 14.5vh;
|
||||
border-radius: 5px;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
#decreaseradiochannel:hover::before {
|
||||
content: attr(title); display: block;
|
||||
position:relative;
|
||||
top:-20px;
|
||||
font-size:12px;
|
||||
color:white;
|
||||
background-color:rgba(0, 0, 0, 0);
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
#poweredOff {
|
||||
position: absolute;
|
||||
width: 3.5vh;
|
||||
height: 2.9vh;
|
||||
bottom: 16.5vh;
|
||||
left: 19vh;
|
||||
border-radius: 5px;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
#poweredOff:hover::before {
|
||||
content: attr(title); display: block;
|
||||
position:relative;
|
||||
top:-20px;
|
||||
font-size:12px;
|
||||
color:white;
|
||||
background-color:rgba(0, 0, 0, 0);
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
.onoff button {
|
||||
padding: 40px 55px;
|
||||
opacity: .0;
|
||||
}
|
||||
|
||||
.channel {
|
||||
position: absolute;
|
||||
bottom: 24vh;
|
||||
left: 11.2vh;
|
||||
background: none;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.channel span {
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.channel input {
|
||||
text-align: center;
|
||||
border: none;
|
||||
font-size: 30px;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-family: 'Fira Code', monospace;
|
||||
width: 5vw;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
}
|
BIN
resources/[voice]/qb-radio/html/img/radio.png
Normal file
BIN
resources/[voice]/qb-radio/html/img/radio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
89
resources/[voice]/qb-radio/html/js/script.js
Normal file
89
resources/[voice]/qb-radio/html/js/script.js
Normal file
@ -0,0 +1,89 @@
|
||||
$(function() {
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type == "open") {
|
||||
QBRadio.SlideUp()
|
||||
}
|
||||
|
||||
if (event.data.type == "close") {
|
||||
QBRadio.SlideDown()
|
||||
}
|
||||
});
|
||||
|
||||
document.onkeyup = function (data) {
|
||||
if (data.which == 27) { // Escape key
|
||||
$.post('https://qb-radio/escape', JSON.stringify({}));
|
||||
QBRadio.SlideDown()
|
||||
} else if (data.which == 13) { // Enter key
|
||||
$.post('https://qb-radio/joinRadio', JSON.stringify({
|
||||
channel: $("#channel").val()
|
||||
}));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
QBRadio = {}
|
||||
|
||||
$(document).on('click', '#submit', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$.post('https://qb-radio/joinRadio', JSON.stringify({
|
||||
channel: $("#channel").val()
|
||||
}));
|
||||
});
|
||||
|
||||
$(document).on('click', '#disconnect', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$.post('https://qb-radio/leaveRadio');
|
||||
});
|
||||
|
||||
$(document).on('click', '#volumeUp', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$.post('https://qb-radio/volumeUp', JSON.stringify({
|
||||
channel: $("#channel").val()
|
||||
}));
|
||||
});
|
||||
|
||||
$(document).on('click', '#volumeDown', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$.post('https://qb-radio/volumeDown', JSON.stringify({
|
||||
channel: $("#channel").val()
|
||||
}));
|
||||
});
|
||||
|
||||
$(document).on('click', '#decreaseradiochannel', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$.post('https://qb-radio/decreaseradiochannel', JSON.stringify({
|
||||
channel: $("#channel").val()
|
||||
}));
|
||||
});
|
||||
|
||||
$(document).on('click', '#increaseradiochannel', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$.post('https://qb-radio/increaseradiochannel', JSON.stringify({
|
||||
channel: $("#channel").val()
|
||||
}));
|
||||
});
|
||||
|
||||
$(document).on('click', '#poweredOff', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
$.post('https://qb-radio/poweredOff', JSON.stringify({
|
||||
channel: $("#channel").val()
|
||||
}));
|
||||
});
|
||||
|
||||
QBRadio.SlideUp = function() {
|
||||
$(".container").css("display", "block");
|
||||
$(".radio-container").animate({bottom: "6vh",}, 250);
|
||||
}
|
||||
|
||||
QBRadio.SlideDown = function() {
|
||||
$(".radio-container").animate({bottom: "-110vh",}, 400, function(){
|
||||
$(".container").css("display", "none");
|
||||
});
|
||||
}
|
26
resources/[voice]/qb-radio/html/ui.html
Normal file
26
resources/[voice]/qb-radio/html/ui.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!doctype HTML>
|
||||
<html>
|
||||
<head>
|
||||
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="radio-container">
|
||||
<img src="./img/radio.png" alt="" class="radio">
|
||||
|
||||
<div class="channel">
|
||||
<input id="channel" type="number" name="channel" min="1" max="999" placeholder="1-999">
|
||||
</div>
|
||||
<div id="submit" class="left" title="CONN"></div>
|
||||
<div id="disconnect" class="left" title="DISC"></div>
|
||||
<div id="volumeDown" class="left" title="VOL-"></div>
|
||||
<div id="volumeUp" title="VOL+"></div>
|
||||
<div id="increaseradiochannel" class="left" title="NEXT"></div>
|
||||
<div id="decreaseradiochannel" class="left" title="PREV"></div>
|
||||
<div id="poweredOff" class="left" title="OFF"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/script.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
BIN
resources/[voice]/qb-radio/imgforinventory/radio.png
Normal file
BIN
resources/[voice]/qb-radio/imgforinventory/radio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
BIN
resources/[voice]/qb-radio/pma-voice mic_clicks/mic_click_on.ogg
Normal file
BIN
resources/[voice]/qb-radio/pma-voice mic_clicks/mic_click_on.ogg
Normal file
Binary file not shown.
23
resources/[voice]/qb-radio/server.lua
Normal file
23
resources/[voice]/qb-radio/server.lua
Normal file
@ -0,0 +1,23 @@
|
||||
local QBCore = exports['qb-core']:GetCoreObject()
|
||||
|
||||
QBCore.Functions.CreateUseableItem("radio", function(source)
|
||||
TriggerClientEvent('qb-radio:use', source)
|
||||
end)
|
||||
|
||||
QBCore.Functions.CreateCallback('qb-radio:radiocheck', function(source, cb)
|
||||
local Player = QBCore.Functions.GetPlayer(source)
|
||||
if Player ~= nil then
|
||||
if Player.Functions.GetItemByName("radio") ~= nil and not Player.PlayerData.metadata["isdead"] and not Player.PlayerData.metadata["inlaststand"] then
|
||||
cb(true)
|
||||
else
|
||||
cb(false)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
for channel, config in pairs(Config.RestrictedChannels) do
|
||||
exports['pma-voice']:addChannelCheck(channel, function(source)
|
||||
local Player = QBCore.Functions.GetPlayer(source)
|
||||
return config[Player.PlayerData.job.name]
|
||||
end)
|
||||
end
|
Loading…
Reference in New Issue
Block a user