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