diff --git a/[editor]/editor_gui/client/browser/objects.xml b/[editor]/editor_gui/client/browser/objects.xml
index 42385acf5..26e91f490 100644
--- a/[editor]/editor_gui/client/browser/objects.xml
+++ b/[editor]/editor_gui/client/browser/objects.xml
@@ -760,6 +760,7 @@
+
diff --git a/[editor]/editor_main/client/colpatch/patches.col b/[editor]/editor_main/client/colpatch/patches.col
index 0029a8534..4d11e0233 100644
Binary files a/[editor]/editor_main/client/colpatch/patches.col and b/[editor]/editor_main/client/colpatch/patches.col differ
diff --git a/[editor]/editor_main/client/colpatch/placement.list b/[editor]/editor_main/client/colpatch/placement.list
index bc3ee7e22..0047d49d0 100644
--- a/[editor]/editor_main/client/colpatch/placement.list
+++ b/[editor]/editor_main/client/colpatch/placement.list
@@ -5626,4 +5626,5 @@
16284,0,511.97656,1077.24219,36.76563,0.00000,-0.00000,0.00000
16286,0,557.28125,1123.08594,26.72656,0.00000,-0.00000,0.00000
16498,0,829.40625,694.06250,10.67188,-0.00000,-0.00000,20.73494
-16291,0,671.07813,1119.28125,37.03125,0.00000,-0.00000,0.00000
\ No newline at end of file
+16291,0,671.07813,1119.28125,37.03125,0.00000,-0.00000,0.00000
+5993,0,1110.89844,-1328.81250,13.85156,0.00000,0.00000,0.00000
\ No newline at end of file
diff --git a/[gamemodes]/[play]/play/play_config.lua b/[gamemodes]/[play]/play/play_config.lua
index 0530b42bd..818de52a8 100644
--- a/[gamemodes]/[play]/play/play_config.lua
+++ b/[gamemodes]/[play]/play/play_config.lua
@@ -1,13 +1,13 @@
playerSkins = {
- 0, 1, 2, 7, 9, 10, 11, 12, 13, 14,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32,
33, 34, 35, 36, 37, 38, 39, 40, 41,
- 43, 44, 45, 46, 47, 48, 49, 50, 51,
+ 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 59, 60,
- 61, 62, 63, 64, 66, 67, 68, 69, 70,
+ 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 75, 76, 77, 78, 79, 80,
- 81, 82, 83, 84, 85, 87, 88, 89, 90,
+ 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99,
100, 101, 102, 103, 104, 105, 106, 107, 108,
109, 110, 111, 112, 113, 114, 115, 116, 117,
@@ -27,9 +27,9 @@ playerSkins = {
238, 239, 240, 241, 242, 243, 244, 245, 246,
247, 248, 249, 250, 251, 252, 253, 254, 255,
256, 257, 258, 259, 260, 261, 262, 263, 264,
- 265, 266, 267, 268, 269, 270, 271, 272, 274,
+ 265, 266, 267, 268, 269, 270, 271, 272, 273, 274,
275, 276, 277, 278, 279, 280, 281, 282, 283,
- 284, 285, 286, 287, 288, 290, 291, 292, 293,
+ 284, 285, 286, 287, 288, 289, 290, 291, 292, 293,
294, 295, 296, 297, 298, 299, 300, 301, 302,
303, 304, 305, 306, 307, 308, 309, 310, 311,
312
@@ -100,4 +100,4 @@ vehicleSpawns = {
{541, 2024.7091064453, 1499.3103027344, 10.520312309265, 270.00003051758},
{429, 2025.9349365234, 1490.2434082031, 10.5703125, 270.00003051758},
{429, 2025.6474609375, 1494.671875, 10.5703125, 270.00003051758}
-}
\ No newline at end of file
+}
diff --git a/[gameplay]/voice_local/client.lua b/[gameplay]/voice_local/client.lua
index ea20fb9d7..f5f8dc3c7 100644
--- a/[gameplay]/voice_local/client.lua
+++ b/[gameplay]/voice_local/client.lua
@@ -1,155 +1,146 @@
-local fMinDistance = 0
-local fMaxDistance = 25
-local mathexp = math.exp
+-- Remote events:
+addEvent("voice_local:onClientPlayerVoiceStart", true)
+addEvent("voice_local:onClientPlayerVoiceStop", true)
+addEvent("voice_local:updateSettings", true)
+
+-- Only starts handling player voices after receiving the settings from the server
+local initialWaiting = true
+
local streamedPlayers = {}
-local fDistDiff = fMinDistance - fMaxDistance
+local localPlayerTalking = false
local sx, sy = guiGetScreenSize()
-local nx, ny = sx / 1920, sy / 1080
-local width = 108 * nx
-local height = 180 * ny
-local xOffset = 75 * nx
-local yOffset = 50 * ny
-local halfWidth = width / 2
-local halfHeight = height / 2
+local devSX, devSY = sx / 1920, sy / 1080
+local iconWidth = 108 * devSX
+local iconHalfWidth = iconWidth / 2
+local iconHeight = 180 * devSY
+local iconHalfHeight = iconHeight / 2
+local iconTexture = dxCreateTexture("icon.png", "dxt5", true, "clamp")
+
+local function drawTalkingIcon(player, camDistToPlayer)
+ local boneX, boneY, boneZ = getPedBonePosition(player, 8)
+ local screenX, screenY = getScreenFromWorldPosition(boneX, boneY, boneZ + 0.4)
+ if screenX and screenY then
+ local factor = 1 / camDistToPlayer
+ dxDrawImage(
+ screenX - iconHalfWidth * factor,
+ screenY - iconHalfHeight * factor,
+ iconWidth * factor,
+ iconHeight * factor,
+ iconTexture, 0, 0, 0, -1, false
+ )
+ end
+end
+
+local function handlePreRender()
+ local debugY = 50
+ local maxDistance = settings.maxVoiceDistance.value
+ local cameraX, cameraY, cameraZ = getCameraMatrix()
+ local localPlayerX, localPlayerY, localPlayerZ = getElementPosition(localPlayer)
+ for player, talking in pairs(streamedPlayers) do
+ local otherPlayerX, otherPlayerY, otherPlayerZ = getElementPosition(player)
+ local realDistanceToPlayer = getDistanceBetweenPoints3D(localPlayerX, localPlayerY, localPlayerZ, otherPlayerX, otherPlayerY, otherPlayerZ)
+ local playerVolume
+ if (realDistanceToPlayer >= maxDistance) then
+ playerVolume = 0.0
+ else
+ playerVolume = (1.0 - (realDistanceToPlayer / maxDistance)^2)
+ end
-local icon = dxCreateTexture("icon.png", "dxt5", true, "clamp")
+ -- Voice voume is usually unfortunately very low, resulting in players
+ -- barely hearing others if we set the player voice volume to 1.0
+ -- So we need to increase it to like 6.0 to make it audible
+ playerVolume = playerVolume * settings.voiceSoundBoost.value
-function preRender()
- local x, y, z, lx, ly, lz = getCameraMatrix()
+ setSoundVolume(player, playerVolume)
- for player, talking in pairs(streamedPlayers) do
- if player and isElement(player) and getElementType(player) == "player" then
- local x1, y1, z1 = getElementPosition(player)
- local fDistance = getDistanceBetweenPoints3D(x, y, z, x1, y1, z1)
-
- local fVolume
- if (fDistance <= fMinDistance) then
- fVolume = 100
- elseif (fDistance >= fMaxDistance) then
- fVolume = 0.0
- else
- fVolume = mathexp(-(fDistance - fMinDistance) * (5.0 / fDistDiff)) * 100
- end
-
- if isLineOfSightClear(x, y, z, x1, y1, z1, false, false, false, false, true, true, true, localPlayer) then
- setSoundVolume(player, fVolume)
- setSoundEffectEnabled(player, "compressor", false)
- else
- setSoundVolume(player, fVolume * 2)
- setSoundEffectEnabled(player, "compressor", true)
- end
-
- if talking and isLineOfSightClear(x, y, z, x1, y1, z1, false, false, false, false, true, true, true, localPlayer) then
- local boneX, boneY, boneZ = getPedBonePosition(player, 8)
- local screenX, screenY = getScreenFromWorldPosition(boneX, boneY, boneZ + 0.5)
- if screenX and screenY and fDistance < fMaxDistance then
- fDistance = 1 / fDistance
- dxDrawImage(screenX - halfWidth * fDistance, screenY - halfHeight * fDistance, width * fDistance, height * fDistance, icon, 0, 0, 0, -1, false)
- end
- end
+ if DEBUG_MODE then
+ dxDrawRectangle(20, debugY - 5, 300, 25, tocolor(0, 0, 0, 200))
+ dxDrawText(("%s | Distance: %.2f | Voice Volume: %.2f"):format(getPlayerName(player), realDistanceToPlayer, playerVolume), 30, debugY)
+ debugY = debugY + 15
end
- end
-end
-addEventHandler("onClientPreRender", root, preRender)
-function onStart()
- local x, y, z = getElementPosition(localPlayer)
- for _, player in ipairs(getElementsWithinRange(x, y, z, 250, "player")) do
- if streamedPlayers[player] == nil then
- streamedPlayers[player] = false
+ if talking and (settings.showTalkingIcon.value == true)
+ and realDistanceToPlayer < maxDistance
+ and isLineOfSightClear(cameraX, cameraY, cameraZ, otherPlayerX, otherPlayerY, otherPlayerZ, false, false, false, false, true, true, true, localPlayer) then
+ drawTalkingIcon(player, getDistanceBetweenPoints3D(cameraX, cameraY, cameraZ, otherPlayerX, otherPlayerY, otherPlayerZ))
end
end
- triggerServerEvent("voice:setPlayerBroadcast", resourceRoot, localPlayer, streamedPlayers)
+ if localPlayerTalking and (settings.showTalkingIcon.value == true) then
+ drawTalkingIcon(localPlayer, getDistanceBetweenPoints3D(cameraX, cameraY, cameraZ, localPlayerX, localPlayerY, localPlayerZ))
+ end
end
-addEventHandler("onClientResourceStart", resourceRoot, onStart, false)
-function playerJoin()
- if getElementType(source) == "player" then
- if streamedPlayers[source] == nil then
- streamedPlayers[source] = false
- triggerServerEvent("voice:addToPlayerBroadcast", resourceRoot, localPlayer, source)
+addEventHandler("onClientResourceStart", resourceRoot, function()
+ for _, player in pairs(getElementsByType("player", root, true)) do
+ if player ~= localPlayer and streamedPlayers[player] == nil then
+ setSoundVolume(player, 0)
+ streamedPlayers[player] = false
end
end
-end
-addEventHandler("onClientPlayerJoin", root, playerJoin)
+ triggerServerEvent("voice_local:setPlayerBroadcast", localPlayer, streamedPlayers)
+end, false)
-function playerQuit()
+-- Handle remote/other player quit
+addEventHandler("onClientPlayerQuit", root, function()
if streamedPlayers[source] ~= nil then
streamedPlayers[source] = nil
- setSoundVolume(source, 0)
- triggerServerEvent("voice:removePlayerBroadcast", resourceRoot, localPlayer, source)
+ triggerServerEvent("voice_local:removeFromPlayerBroadcast", localPlayer, source)
end
-end
-addEventHandler("onClientPlayerQuit", root, playerQuit)
+end)
-- Code considers this event's problem @ "Note" box: https://wiki.multitheftauto.com/wiki/OnClientElementStreamIn
-- It should be modified if said behavior ever changes
-function streamIn()
- if (isElement(source) and getElementType(source) == "player" and isPedDead(source) == false) then
- if streamedPlayers[source] == nil then
- streamedPlayers[source] = false
- triggerServerEvent("voice:addToPlayerBroadcast", resourceRoot, localPlayer, source)
- end
- end
-end
-addEventHandler("onClientElementStreamIn", root, streamIn)
-
--- To ensure table integrity, stream out & local player death into 2 separate, safer events
--- See the "Note" box at https://wiki.multitheftauto.com/wiki/OnClientElementStreamIn for reason why
--- Stream out event
-function streamOut()
- if (source ~= localPlayer and isElement(source) and getElementType(source) == "player") then
- if streamedPlayers[source] ~= nil then
- streamedPlayers[source] = nil
- setSoundVolume(source, 0)
- triggerServerEvent("voice:removePlayerBroadcast", resourceRoot, localPlayer, source)
- end
- end
-end
-addEventHandler("onClientElementStreamOut", root, streamOut)
-
--- Local player death event
-function onWasted()
- if streamedPlayers[localPlayer] ~= nil then
- streamedPlayers[localPlayer] = nil
- setSoundVolume(localPlayer, 0)
- triggerServerEvent("voice:removePlayerBroadcast", resourceRoot, localPlayer, localPlayer)
- end
-end
-addEventHandler("onClientPlayerWasted", localPlayer, onWasted)
+addEventHandler("onClientElementStreamIn", root, function()
+ if source == localPlayer then return end
+ if not (isElement(source) and getElementType(source) == "player") then return end
-function resourceStop()
- triggerServerEvent("voice:removePlayerBroadcasts", resourceRoot, localPlayer, streamedPlayers)
- streamedPlayers = {}
-end
-addEventHandler("onClientResourceStop", resourceRoot, resourceStop, false)
+ if isPedDead(source) then return end
-function voiceStart(source)
- if (isElement(source) and getElementType(source) == "player") then
- if streamedPlayers[source] ~= nil then
- streamedPlayers[source] = true
- end
+ if streamedPlayers[source] == nil then
+ setSoundVolume(source, 0)
+ streamedPlayers[source] = false
+ triggerServerEvent("voice_local:addToPlayerBroadcast", localPlayer, source)
end
-end
-addEvent("voice_cl:onClientPlayerVoiceStart", true)
-addEventHandler("voice_cl:onClientPlayerVoiceStart", resourceRoot, voiceStart)
+end)
+addEventHandler("onClientElementStreamOut", root, function()
+ if source == localPlayer then return end
+ if not (isElement(source) and getElementType(source) == "player") then return end
-function voiceStop(source)
- if (isElement(source) and getElementType(source) == "player") then
- if streamedPlayers[source] ~= nil then
- streamedPlayers[source] = false
- end
+ if streamedPlayers[source] ~= nil then
+ setSoundVolume(source, 0)
+ streamedPlayers[source] = nil
+ triggerServerEvent("voice_local:removeFromPlayerBroadcast", localPlayer, source)
end
-end
-addEvent("voice_cl:onClientPlayerVoiceStop", true)
-addEventHandler("voice_cl:onClientPlayerVoiceStop", resourceRoot, voiceStop)
+end)
-function table.empty(a)
- if type(a) ~= "table" then
- return false
+-- Update player talking status (for displaying)
+addEventHandler("voice_local:onClientPlayerVoiceStart", root, function(player)
+ if not (isElement(player) and getElementType(player) == "player") then return end
+
+ if player == localPlayer then
+ localPlayerTalking = true
+ elseif streamedPlayers[player] ~= nil then
+ streamedPlayers[player] = true
+ end
+end)
+addEventHandler("voice_local:onClientPlayerVoiceStop", root, function(player)
+ if not (isElement(player) and getElementType(player) == "player") then return end
+
+ if player == localPlayer then
+ localPlayerTalking = false
+ elseif streamedPlayers[player] ~= nil then
+ streamedPlayers[player] = false
end
+end)
- return next(a) == nil
-end
\ No newline at end of file
+-- Load the settings received from the server
+addEventHandler("voice_local:updateSettings", localPlayer, function(settingsFromServer)
+ settings = settingsFromServer
+
+ if initialWaiting then
+ addEventHandler("onClientPreRender", root, handlePreRender, false)
+ initialWaiting = false
+ end
+end, false)
diff --git a/[gameplay]/voice_local/meta.xml b/[gameplay]/voice_local/meta.xml
index 0b584185b..0a9a0b43c 100644
--- a/[gameplay]/voice_local/meta.xml
+++ b/[gameplay]/voice_local/meta.xml
@@ -1,8 +1,17 @@
-
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/[gameplay]/voice_local/server.lua b/[gameplay]/voice_local/server.lua
index b3ad7da07..6e5a9054f 100644
--- a/[gameplay]/voice_local/server.lua
+++ b/[gameplay]/voice_local/server.lua
@@ -1,72 +1,128 @@
+-- Remote events:
+addEvent("voice_local:setPlayerBroadcast", true)
+addEvent("voice_local:addToPlayerBroadcast", true)
+addEvent("voice_local:removeFromPlayerBroadcast", true)
+
local broadcasts = {}
-function setPlayerBroadcast(thePlayer, players)
- broadcasts[thePlayer] = {}
+for settingName, settingData in pairs(settings) do
+ if settingData then
+ settings[settingName].value = get("*"..settingData.key)
+ end
+end
- for player, _ in pairs(players) do
- table.insert(broadcasts[thePlayer], player)
+addEventHandler("onPlayerResourceStart", root, function(res)
+ if res == resource then
+ triggerClientEvent(source, "voice_local:updateSettings", source, settings)
end
+end)
- setPlayerVoiceBroadcastTo(thePlayer, broadcasts[thePlayer])
+-- Don't let the player talk to anyone as soon as they join
+addEventHandler("onPlayerJoin", root, function()
+ setPlayerVoiceBroadcastTo(source, {})
+end)
+
+addEventHandler("onPlayerQuit", root, function()
+ broadcasts[source] = nil
+end)
+
+-- Anti-cheat
+-- Prevents clients from wanting to broadcast their voice to players that are really too far away
+local function canPlayerBeWithinOtherPlayerStreamDistance(player, otherPlayer)
+ local maxDist = tonumber(getServerConfigSetting("ped_syncer_distance")) or 100
+ if (not isElement(player)) or (not isElement(otherPlayer)) then
+ return false
+ end
+ if getElementType(player) ~= "player" or getElementType(otherPlayer) ~= "player" then
+ return false
+ end
+ if getElementInterior(player) ~= getElementInterior(otherPlayer)
+ or getElementDimension(player) ~= getElementDimension(otherPlayer) then
+ return false
+ end
+ local px, py, pz = getElementPosition(player)
+ local opx, opy, opz = getElementPosition(otherPlayer)
+ return getDistanceBetweenPoints3D(px, py, pz, opx, opy, opz) <= maxDist
end
-addEvent("voice:setPlayerBroadcast", true)
-addEventHandler("voice:setPlayerBroadcast", resourceRoot, setPlayerBroadcast)
-function addToPlayerBroadcast(thePlayer, player)
- if not broadcasts[thePlayer] then
- broadcasts[thePlayer] = {}
+addEventHandler("voice_local:setPlayerBroadcast", root, function(players)
+ if not client then return end
+ if type(players) ~= "table" then return end
+ broadcasts[client] = {client}
+
+ for player, _ in pairs(players) do
+ if player ~= client then
+ if canPlayerBeWithinOtherPlayerStreamDistance(client, player) then
+ table.insert(broadcasts[client], player)
+ else
+ iprint(eventName, "ignoring", getPlayerName(player))
+ end
+ end
end
+ setPlayerVoiceBroadcastTo(client, broadcasts[client])
+end)
- table.insert(broadcasts[thePlayer], player)
- setPlayerVoiceBroadcastTo(thePlayer, broadcasts[thePlayer])
-end
-addEvent("voice:addToPlayerBroadcast", true)
-addEventHandler("voice:addToPlayerBroadcast", resourceRoot, addToPlayerBroadcast)
+addEventHandler("voice_local:addToPlayerBroadcast", root, function(player)
+ if not client then return end
+ if not (isElement(player) and getElementType(player) == "player") then return end
-function removePlayerBroadcasts(thePlayer, players)
- if not broadcasts[thePlayer] then
+ if not broadcasts[client] then
+ broadcasts[client] = {client}
+ end
+
+ if not canPlayerBeWithinOtherPlayerStreamDistance(client, player) then
+ iprint(eventName, "ignoring", getPlayerName(player))
return
end
- for _, player in ipairs(players) do
- for i, broadcast in ipairs(broadcasts[thePlayer]) do
- if player == broadcast then
- table.remove(broadcasts[thePlayer], i)
- end
+ -- Prevent duplicates
+ for _, broadcast in pairs(broadcasts[client]) do
+ if player == broadcast then
+ return
end
end
- setPlayerVoiceBroadcastTo(thePlayer, broadcasts[thePlayer])
-end
-addEvent("voice:removePlayerBroadcasts", true)
-addEventHandler("voice:removePlayerBroadcasts", resourceRoot, removePlayerBroadcasts)
+ table.insert(broadcasts[client], player)
+ setPlayerVoiceBroadcastTo(client, broadcasts[client])
+end)
+
+addEventHandler("voice_local:removeFromPlayerBroadcast", root, function(player)
+ if not client then return end
+ if not (isElement(player) and getElementType(player) == "player") then return end
-function removePlayerBroadcast(thePlayer, player)
- if not broadcasts[thePlayer] then
+ if not broadcasts[client] then
return
end
- for i, broadcast in ipairs(broadcasts[thePlayer]) do
- if player == broadcast then
- table.remove(broadcasts[thePlayer], i)
+ for i, broadcast in pairs(broadcasts[client]) do
+ if player~=client and player == broadcast then
+ table.remove(broadcasts[client], i)
+ break
end
end
- setPlayerVoiceBroadcastTo(thePlayer, broadcasts[thePlayer])
-end
-addEvent("voice:removePlayerBroadcast", true)
-addEventHandler("voice:removePlayerBroadcast", resourceRoot, removePlayerBroadcast)
+ setPlayerVoiceBroadcastTo(client, broadcasts[client])
+end)
-function playerVoiceStart()
- for _, listener in ipairs(broadcasts[source]) do
- triggerClientEvent(listener, "voice_cl:onClientPlayerVoiceStart", resourceRoot, source)
+addEventHandler("onPlayerVoiceStart", root, function()
+ if not broadcasts[source] then
+ -- Somehow if the system still hasn't loaded the player, prevent them from talking
+ cancelEvent()
+ return
end
-end
-addEventHandler("onPlayerVoiceStart", root, playerVoiceStart)
+ triggerClientEvent(broadcasts[source], "voice_local:onClientPlayerVoiceStart", source, source)
+end)
-function playerVoiceStop()
- for _, listener in ipairs(broadcasts[source]) do
- triggerClientEvent(listener, "voice_cl:onClientPlayerVoiceStop", resourceRoot, source)
+addEventHandler("onPlayerVoiceStop", root, function()
+ if not broadcasts[source] then
+ return
end
-end
-addEventHandler("onPlayerVoiceStop", root, playerVoiceStop)
\ No newline at end of file
+ triggerClientEvent(broadcasts[source], "voice_local:onClientPlayerVoiceStop", source, source)
+end)
+
+-- Cancel resource start if voice is not enabled on the server
+addEventHandler("onResourceStart", resourceRoot, function()
+ if not isVoiceEnabled() then
+ cancelEvent(true, " setting is not enabled on this server")
+ end
+end, false)
diff --git a/[gameplay]/voice_local/shared.lua b/[gameplay]/voice_local/shared.lua
new file mode 100644
index 000000000..cf88fbe71
--- /dev/null
+++ b/[gameplay]/voice_local/shared.lua
@@ -0,0 +1,7 @@
+settings = {
+ showTalkingIcon = {key="show_talking_icon", value=nil},
+ maxVoiceDistance = {key="max_voice_distance", value=nil},
+ voiceSoundBoost = {key="voice_sound_boost", value=nil},
+}
+
+DEBUG_MODE = false