0.0.1
This commit is contained in:
151
resources/[standalone]/pma-voice/server/main.lua
Normal file
151
resources/[standalone]/pma-voice/server/main.lua
Normal file
@@ -0,0 +1,151 @@
|
||||
voiceData = {}
|
||||
radioData = {}
|
||||
callData = {}
|
||||
|
||||
local mappedChannels = {}
|
||||
function firstFreeChannel()
|
||||
for i = 1, 2048 do
|
||||
if not mappedChannels[i] then
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
function defaultTable(source)
|
||||
handleStateBagInitilization(source)
|
||||
return {
|
||||
radio = 0,
|
||||
call = 0,
|
||||
lastRadio = 0,
|
||||
lastCall = 0
|
||||
}
|
||||
end
|
||||
|
||||
function handleStateBagInitilization(source)
|
||||
local plyState = Player(source).state
|
||||
if not plyState.pmaVoiceInit then
|
||||
plyState:set('radio', GetConvarInt('voice_defaultRadioVolume', 30), true)
|
||||
plyState:set('call', GetConvarInt('voice_defaultCallVolume', 60), true)
|
||||
plyState:set('submix', nil, true)
|
||||
plyState:set('proximity', {}, true)
|
||||
plyState:set('callChannel', 0, true)
|
||||
plyState:set('radioChannel', 0, true)
|
||||
plyState:set('voiceIntent', 'speech', true)
|
||||
-- We want to save voice inits because we'll automatically reinitalize calls and channels
|
||||
plyState:set('pmaVoiceInit', true, false)
|
||||
end
|
||||
|
||||
local assignedChannel = firstFreeChannel()
|
||||
plyState:set('assignedChannel', assignedChannel, true)
|
||||
if assignedChannel ~= 0 then
|
||||
mappedChannels[assignedChannel] = source
|
||||
logger.verbose('[reuse] Assigned %s to channel %s', source, assignedChannel)
|
||||
else
|
||||
logger.error('[reuse] Failed to find a free channel for %s', source)
|
||||
end
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
local plyTbl = GetPlayers()
|
||||
for i = 1, #plyTbl do
|
||||
local ply = tonumber(plyTbl[i])
|
||||
voiceData[ply] = defaultTable(ply)
|
||||
end
|
||||
|
||||
Wait(5000)
|
||||
|
||||
local nativeAudio = GetConvar('voice_useNativeAudio', 'not-set')
|
||||
local _3dAudio = GetConvar('voice_use3dAudio', 'not-set')
|
||||
local _2dAudio = GetConvar('voice_use2dAudio', 'not-set')
|
||||
local sendingRangeOnly = GetConvar('voice_useSendingRangeOnly', 'not-set')
|
||||
local gameVersion = GetConvar('gamename', 'fivem')
|
||||
|
||||
-- handle no convars being set (default drag n' drop)
|
||||
if
|
||||
nativeAudio == 'not-set'
|
||||
and _3dAudio == 'not-set'
|
||||
and _2dAudio == 'not-set'
|
||||
then
|
||||
SetConvarReplicated('voice_useNativeAudio', 'true')
|
||||
logger.info('No voice mod detected, defaulting to \'setr voice_useNativeAudio true\'')
|
||||
elseif sendingRangeOnly == 'true' then
|
||||
logger.warn(
|
||||
"It's recommended to have 'voice_useSendingRangeOnly' set to false, you can do that with 'setr voice_useSendingRangeOnly false', this makes clients ignore the position information sent from the client.")
|
||||
end
|
||||
|
||||
local radioVolume = GetConvarInt("voice_defaultRadioVolume", 30)
|
||||
local callVolume = GetConvarInt("voice_defaultCallVolume", 60)
|
||||
|
||||
-- When casted to an integer these get set to 0 or 1, so warn on these values that they don't work
|
||||
if
|
||||
radioVolume == 0 or radioVolume == 1 or
|
||||
callVolume == 0 or callVolume == 1
|
||||
then
|
||||
SetConvarReplicated("voice_defaultRadioVolume", 30)
|
||||
SetConvarReplicated("voice_defaultCallVolume", 60)
|
||||
for i = 1, 5 do
|
||||
Wait(5000)
|
||||
logger.warn(
|
||||
"`voice_defaultRadioVolume` or `voice_defaultCallVolume` have their value set as a float, this is going to automatically be fixed but please update your convars.")
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('playerJoining', function()
|
||||
if not voiceData[source] then
|
||||
voiceData[source] = defaultTable(source)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("playerDropped", function()
|
||||
local source = source
|
||||
local mappedChannel = Player(source).state.assignedChannel
|
||||
|
||||
if voiceData[source] then
|
||||
local plyData = voiceData[source]
|
||||
|
||||
if plyData.radio ~= 0 then
|
||||
removePlayerFromRadio(source, plyData.radio)
|
||||
end
|
||||
|
||||
if plyData.call ~= 0 then
|
||||
removePlayerFromCall(source, plyData.call)
|
||||
end
|
||||
|
||||
voiceData[source] = nil
|
||||
end
|
||||
|
||||
if mappedChannel then
|
||||
mappedChannels[mappedChannel] = nil
|
||||
logger.verbose('[reuse] Unassigned %s from channel %s', source, mappedChannel)
|
||||
end
|
||||
end)
|
||||
|
||||
if GetConvarInt('voice_externalDisallowJoin', 0) == 1 then
|
||||
AddEventHandler('playerConnecting', function(_, _, deferral)
|
||||
deferral.defer()
|
||||
Wait(0)
|
||||
deferral.done('This server is not accepting connections.')
|
||||
end)
|
||||
end
|
||||
|
||||
-- only meant for internal use so no documentation
|
||||
function isValidPlayer(source)
|
||||
return voiceData[source]
|
||||
end
|
||||
|
||||
exports('isValidPlayer', isValidPlayer)
|
||||
|
||||
function getPlayersInRadioChannel(channel)
|
||||
local returnChannel = radioData[channel]
|
||||
if returnChannel then
|
||||
return returnChannel
|
||||
end
|
||||
-- channel doesnt exist
|
||||
return {}
|
||||
end
|
||||
|
||||
exports('getPlayersInRadioChannel', getPlayersInRadioChannel)
|
||||
exports('GetPlayersInRadioChannel', getPlayersInRadioChannel)
|
||||
76
resources/[standalone]/pma-voice/server/module/phone.lua
Normal file
76
resources/[standalone]/pma-voice/server/module/phone.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
--- removes a player from the call for everyone in the call.
|
||||
---@param source number the player to remove from the call
|
||||
---@param callChannel number the call channel to remove them from
|
||||
function removePlayerFromCall(source, callChannel)
|
||||
logger.verbose('[call] Removed %s from call %s', source, callChannel)
|
||||
|
||||
callData[callChannel] = callData[callChannel] or {}
|
||||
for player, _ in pairs(callData[callChannel]) do
|
||||
TriggerClientEvent('pma-voice:removePlayerFromCall', player, source)
|
||||
end
|
||||
callData[callChannel][source] = nil
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
voiceData[source].call = 0
|
||||
end
|
||||
|
||||
--- adds a player to a call
|
||||
---@param source number the player to add to the call
|
||||
---@param callChannel number the call channel to add them to
|
||||
function addPlayerToCall(source, callChannel)
|
||||
logger.verbose('[call] Added %s to call %s', source, callChannel)
|
||||
-- check if the channel exists, if it does set the varaible to it
|
||||
-- if not create it (basically if not callData make callData)
|
||||
callData[callChannel] = callData[callChannel] or {}
|
||||
for player, _ in pairs(callData[callChannel]) do
|
||||
-- don't need to send to the source because they're about to get sync'd!
|
||||
if player ~= source then
|
||||
TriggerClientEvent('pma-voice:addPlayerToCall', player, source)
|
||||
end
|
||||
end
|
||||
callData[callChannel][source] = true
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
voiceData[source].call = callChannel
|
||||
TriggerClientEvent('pma-voice:syncCallData', source, callData[callChannel])
|
||||
end
|
||||
|
||||
--- set the players call channel
|
||||
---@param source number the player to set the call off
|
||||
---@param _callChannel number the channel to set the player to (or 0 to remove them from any call channel)
|
||||
function setPlayerCall(source, _callChannel)
|
||||
if GetConvarInt('voice_enableCalls', 1) ~= 1 then return end
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
local isResource = GetInvokingResource()
|
||||
local plyVoice = voiceData[source]
|
||||
local callChannel = tonumber(_callChannel)
|
||||
if not callChannel then
|
||||
-- only full error if its sent from another server-side resource
|
||||
if isResource then
|
||||
error(("'callChannel' expected 'number', got: %s"):format(type(_callChannel)))
|
||||
else
|
||||
return logger.warn("%s sent a invalid call, 'callChannel' expected 'number', got: %s", source,
|
||||
type(_callChannel))
|
||||
end
|
||||
end
|
||||
if isResource then
|
||||
-- got set in a export, need to update the client to tell them that their call
|
||||
-- changed
|
||||
TriggerClientEvent('pma-voice:clSetPlayerCall', source, callChannel)
|
||||
end
|
||||
|
||||
Player(source).state.callChannel = callChannel
|
||||
|
||||
if callChannel ~= 0 and plyVoice.call == 0 then
|
||||
addPlayerToCall(source, callChannel)
|
||||
elseif callChannel == 0 then
|
||||
removePlayerFromCall(source, plyVoice.call)
|
||||
elseif plyVoice.call > 0 then
|
||||
removePlayerFromCall(source, plyVoice.call)
|
||||
addPlayerToCall(source, callChannel)
|
||||
end
|
||||
end
|
||||
|
||||
exports('setPlayerCall', setPlayerCall)
|
||||
|
||||
RegisterNetEvent('pma-voice:setPlayerCall', function(callChannel)
|
||||
setPlayerCall(source, callChannel)
|
||||
end)
|
||||
193
resources/[standalone]/pma-voice/server/module/radio.lua
Normal file
193
resources/[standalone]/pma-voice/server/module/radio.lua
Normal file
@@ -0,0 +1,193 @@
|
||||
|
||||
local msgpack_pack_args = msgpack.pack_args
|
||||
|
||||
local radioChecks = {}
|
||||
|
||||
--- checks if the player can join the channel specified
|
||||
--- @param source number the source of the player
|
||||
--- @param radioChannel number the channel they're trying to join
|
||||
--- @return boolean if the user can join the channel
|
||||
function canJoinChannel(source, radioChannel)
|
||||
if radioChecks[radioChannel] then
|
||||
return radioChecks[radioChannel](source)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- adds a check to the channel, function is expected to return a boolean of true or false
|
||||
---@param channel number the channel to add a check to
|
||||
---@param cb function the function to execute the check on
|
||||
function addChannelCheck(channel, cb)
|
||||
local channelType = type(channel)
|
||||
local cbType = type(cb)
|
||||
if channelType ~= "number" then
|
||||
error(("'channel' expected 'number' got '%s'"):format(channelType))
|
||||
end
|
||||
if cbType ~= 'table' or not cb.__cfx_functionReference then
|
||||
error(("'cb' expected 'function' got '%s'"):format(cbType))
|
||||
end
|
||||
radioChecks[channel] = cb
|
||||
logger.info("%s added a check to channel %s", GetInvokingResource(), channel)
|
||||
end
|
||||
|
||||
exports('addChannelCheck', addChannelCheck)
|
||||
|
||||
--- removes any check for the channel
|
||||
---@param channel number the channel to add a check to
|
||||
function removeChannelCheck(channel)
|
||||
local channelType = type(channel)
|
||||
if channelType ~= "number" then
|
||||
error(("'channel' expected 'number' got '%s'"):format(channelType))
|
||||
end
|
||||
radioChecks[channel] = nil
|
||||
logger.info("%s removed check for channel %s", GetInvokingResource(), channel)
|
||||
end
|
||||
|
||||
exports("removeChannelCheck", removeChannelCheck)
|
||||
|
||||
local function radioNameGetter_orig(source)
|
||||
return GetPlayerName(source)
|
||||
end
|
||||
local radioNameGetter = radioNameGetter_orig
|
||||
|
||||
--- triggers an event for all of the players in the table while only doing msgpack
|
||||
--- serialization once
|
||||
local function triggerEventForRadioChannel(eventName, radioTbl, ...)
|
||||
local payload = msgpack_pack_args(...)
|
||||
for player, _ in pairs(radioTbl) do
|
||||
TriggerClientEventInternal(eventName, player, payload, payload:len())
|
||||
end
|
||||
end
|
||||
|
||||
--- adds a check to the channel, function is expected to return a boolean of true or false
|
||||
---@param cb function the function to execute the check on
|
||||
function overrideRadioNameGetter(channel, cb)
|
||||
local cbType = type(cb)
|
||||
if cbType == 'table' and not cb.__cfx_functionReference then
|
||||
error(("'cb' expected 'function' got '%s'"):format(cbType))
|
||||
end
|
||||
radioNameGetter = cb
|
||||
logger.info("%s added a check to channel %s", GetInvokingResource(), channel)
|
||||
end
|
||||
|
||||
exports('overrideRadioNameGetter', overrideRadioNameGetter)
|
||||
|
||||
--- adds a player to the specified radion channel
|
||||
---@param source number the player to add to the channel
|
||||
---@param radioChannel number the channel to set them to
|
||||
---@return boolean wasAdded if the player was successfuly added to the radio channel, or if it failed.
|
||||
function addPlayerToRadio(source, radioChannel)
|
||||
if not canJoinChannel(source, radioChannel) then
|
||||
-- remove the player from the radio client side
|
||||
TriggerClientEvent("pma-voice:radioChangeRejected", source)
|
||||
TriggerClientEvent('pma-voice:removePlayerFromRadio', source, source)
|
||||
return false
|
||||
end
|
||||
logger.verbose('[radio] Added %s to radio %s', source, radioChannel)
|
||||
|
||||
-- check if the channel exists, if it does set the varaible to it
|
||||
-- if not create it (basically if not radiodata make radiodata)
|
||||
radioData[radioChannel] = radioData[radioChannel] or {}
|
||||
local plyName = radioNameGetter(source)
|
||||
triggerEventForRadioChannel('pma-voice:addPlayerToRadio', radioData[radioChannel], source, plyName)
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
voiceData[source].radio = radioChannel
|
||||
radioData[radioChannel][source] = false
|
||||
TriggerClientEvent('pma-voice:syncRadioData', source, radioData[radioChannel],
|
||||
GetConvarInt("voice_syncPlayerNames", 0) == 1 and plyName)
|
||||
return true
|
||||
end
|
||||
|
||||
--- removes a player from the specified channel
|
||||
---@param source number the player to remove
|
||||
---@param radioChannel number the current channel to remove them from
|
||||
function removePlayerFromRadio(source, radioChannel)
|
||||
logger.verbose('[radio] Removed %s from radio %s', source, radioChannel)
|
||||
radioData[radioChannel] = radioData[radioChannel] or {}
|
||||
triggerEventForRadioChannel('pma-voice:removePlayerFromRadio', radioData[radioChannel], source)
|
||||
radioData[radioChannel][source] = nil
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
voiceData[source].radio = 0
|
||||
end
|
||||
|
||||
-- TODO: Implement this in a way that allows players to be on multiple channels
|
||||
--- sets the players current radio channel
|
||||
---@param source number the player to set the channel of
|
||||
---@param _radioChannel number the radio channel to set them to (or 0 to remove them from radios)
|
||||
function setPlayerRadio(source, _radioChannel)
|
||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
local isResource = GetInvokingResource()
|
||||
local plyVoice = voiceData[source]
|
||||
local radioChannel = tonumber(_radioChannel)
|
||||
if not radioChannel then
|
||||
-- only full error if its sent from another server-side resource
|
||||
if isResource then
|
||||
error(("'radioChannel' expected 'number', got: %s"):format(type(_radioChannel)))
|
||||
else
|
||||
return logger.warn("%s sent a invalid radio, 'radioChannel' expected 'number', got: %s", source,
|
||||
type(_radioChannel))
|
||||
end
|
||||
end
|
||||
if isResource then
|
||||
-- got set in a export, need to update the client to tell them that their radio
|
||||
-- changed
|
||||
TriggerClientEvent('pma-voice:clSetPlayerRadio', source, radioChannel)
|
||||
end
|
||||
if radioChannel ~= 0 then
|
||||
if plyVoice.radio > 0 then
|
||||
removePlayerFromRadio(source, plyVoice.radio)
|
||||
end
|
||||
local wasAdded = addPlayerToRadio(source, radioChannel)
|
||||
Player(source).state.radioChannel = wasAdded and radioChannel or 0
|
||||
elseif radioChannel == 0 then
|
||||
removePlayerFromRadio(source, plyVoice.radio)
|
||||
Player(source).state.radioChannel = 0
|
||||
end
|
||||
end
|
||||
|
||||
exports('setPlayerRadio', setPlayerRadio)
|
||||
|
||||
RegisterNetEvent('pma-voice:setPlayerRadio', function(radioChannel)
|
||||
setPlayerRadio(source, radioChannel)
|
||||
end)
|
||||
|
||||
--- syncs the player talking across all radio members
|
||||
---@param talking boolean sets if the palyer is talking.
|
||||
function setTalkingOnRadio(talking)
|
||||
if GetConvarInt('voice_enableRadios', 1) ~= 1 then return end
|
||||
voiceData[source] = voiceData[source] or defaultTable(source)
|
||||
local plyVoice = voiceData[source]
|
||||
local radioTbl = radioData[plyVoice.radio]
|
||||
if radioTbl then
|
||||
radioTbl[source] = talking
|
||||
logger.verbose('[radio] Set %s to talking: %s on radio %s', source, talking, plyVoice.radio)
|
||||
triggerEventForRadioChannel('pma-voice:setTalkingOnRadio', radioTbl, source, talking)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('pma-voice:setTalkingOnRadio', setTalkingOnRadio)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource)
|
||||
for channel, cfxFunctionRef in pairs(radioChecks) do
|
||||
local functionRef = cfxFunctionRef.__cfx_functionReference
|
||||
local functionResource = string.match(functionRef, resource)
|
||||
if functionResource then
|
||||
radioChecks[channel] = nil
|
||||
logger.warn('Channel %s had its radio check removed because the resource that gave the checks stopped',
|
||||
channel)
|
||||
end
|
||||
end
|
||||
|
||||
if type(radioNameGetter) == "table" then
|
||||
local radioRef = radioNameGetter.__cfx_functionReference
|
||||
if radioRef then
|
||||
local isResource = string.match(radioRef, resource)
|
||||
if isResource then
|
||||
radioNameGetter = radioNameGetter_orig
|
||||
logger.warn(
|
||||
'Radio name getter is resetting to default because the resource that gave the cb got turned off')
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
27
resources/[standalone]/pma-voice/server/mute.js
Normal file
27
resources/[standalone]/pma-voice/server/mute.js
Normal file
@@ -0,0 +1,27 @@
|
||||
let mutedPlayers = {}
|
||||
// this is implemented in JS due to Lua's lack of a ClearTimeout
|
||||
// muteply instead of mute because mute conflicts with rp-radio
|
||||
RegisterCommand('muteply', (source, args) => {
|
||||
const mutePly = parseInt(args[0])
|
||||
const duration = parseInt(args[1]) || 900
|
||||
if (mutePly && exports[GetCurrentResourceName()].isValidPlayer(mutePly)) {
|
||||
const isMuted = !MumbleIsPlayerMuted(mutePly);
|
||||
Player(mutePly).state.muted = isMuted;
|
||||
MumbleSetPlayerMuted(mutePly, isMuted);
|
||||
emit('pma-voice:playerMuted', mutePly, source, isMuted, duration);
|
||||
// since this is a toggle, if theres a mutedPlayers entry it can be assumed
|
||||
// that they're currently muted, so we'll clear the timeout and unmute
|
||||
if (mutedPlayers[mutePly]) {
|
||||
clearTimeout(mutedPlayers[mutePly]);
|
||||
delete mutedPlayers[mutePly];
|
||||
MumbleSetPlayerMuted(mutePly, isMuted)
|
||||
Player(mutePly).state.muted = isMuted;
|
||||
return;
|
||||
}
|
||||
mutedPlayers[mutePly] = setTimeout(() => {
|
||||
MumbleSetPlayerMuted(mutePly, !isMuted)
|
||||
Player(mutePly).state.muted = !isMuted;
|
||||
delete mutedPlayers[mutePly]
|
||||
}, duration * 1000)
|
||||
}
|
||||
}, true)
|
||||
Reference in New Issue
Block a user