I wouldn’t even use RemoteFunction:InvokeClient()
. Not only is there the yielding problem, but there’s also the problem that it may error. Here’s an alternative using remote events. I haven’t tested it but I believe it’ll work with some edits if it doesn’t work yet.
Server code (in a module script):
local Players = game:GetService("Players")
local InvokeClientServerModule = {}
InvokeClientServerModule.timeoutReturnValue = setmetatable({}, {tostring = function() return "TIMEOUT_RETURN_VALUE" end})
InvokeClientServerModule.playerLeftReturnValue = setmetatable({}, {tostring = function() return "PLAYER_LEFT_RETURN_VALUE" end})
InvokeClientServerModule.successReturnValue = setmetatable({}, {tostring = function() return "SUCCESS_RETURN_VALUE" end})
local data = {}
local function resumeCoroutine(playerRemoteEventData, invokeNumber, coroutineToResume, ...)
playerRemoteData.howManyReceivedOrTimedOut += 1
playerRemoteData.waitingCoroutineForEachInvoke[invokeNumber] = nil
if playerRemoteData.howManyReceivedOrTimedOut == playerRemoteData.howManySent then
-- This is done so that there's no unnecessary event connection.
playerRemoteData.connection:Disconnect()
playerRemoteData.connection = nil
end
coroutine.resume(coroutineToResume, ...)
end
local function doesPlayerRemoteEventDataExist(player, remoteEvent)
local dataForPlayer = data[player]
if dataForPlayer == nil then
return false
end
return dataForPlayer[remoteEvent] ~= nil
end
local function resumeCoroutineIfTimedOut(player, remoteEvent, invokeNumber)
if not doesPlayerRemoteDataExist(player, remoteEvent) then
-- Player has left the game before timeout.
return
end
local playerRemoteData = data[player][remoteEvent]
local waitingCoroutine = playerRemoteData.waitingCoroutineForEachInvoke[invokeNumber]
if waitingCoroutine == nil then
-- The client has sent a value before timeout.
return
end
-- InvokeClientServerModule.invoke will now return nil because no value was received from the client before timeout.
resumeCoroutine(playerRemoteEventData, invokeNumber, waitingCoroutine, InvokeClientServerModule.timeoutReturnValue)
end
local function handleOnServerEvent(player, remoteEvent, invokeNumber, ...)
if not doesPlayerRemoteDataExist(player, remoteEvent) then
-- exploiter or mistake in code (or maybe the player can have left before than the sent data was received by the server)
return
end
if invokeNumber > playerRemoteData.howManySent then
-- exploiter or mistake in code
end
local waitingCoroutine = playerRemoteData.waitingCoroutineForEachInvoke[invokeNumber]
if waitingCoroutine == nil then
-- Timeout has been reached or the client fired the remote twice with the same invoke number because of either a mistake in code or an exploiter.
return
end
resumeCoroutine(playerRemoteEventData, invokeNumber, waitingCoroutine, InvokeClientServerModule.successReturnValue, ...)
end
local function setupPlayerRemoteEventDataIfNecessary(player, remoteEvent)
local dataForPlayer = data[player]
if dataForPlayer == nil then
dataForPlayer = {}
data[player] = dataForPlayer
end
local playerRemoteData = dataForPlayer[remoteEvent]
if playerRemoteData == nil then
playerRemoteData = {
howManySent = 0
howManyReceivedOrTimedOut = 0
waitingCoroutineForEachInvoke = {}
}
dataForPlayer[remoteEvent] = playerRemoteData
end
end
function InvokeClientServerModule.invoke(player, remoteEvent, timeout, ...)
setupPlayerRemoteEventDataIfNecessary(player, remoteEvent)
local playerRemoteEventData = data[player][remoteEvent]
local invokeNumber = playerRemoteEventData.howManySent + 1
playerRemoteEventData.howManySent = invokeNumber
remoteEvent:FireClient(player, invokeNumber, ...)
if playerRemoteEventData.connection == nil then
-- There were no invokes waiting for response before this one was made so there was no connection and thus a new connection needs to be made.
playerRemoteEventData.connection = remoteEvent.OnServerEvent:Connect(function(player, invokeNumber, ...)
handleOnServerEvent(player, remoteEvent, invokeNumber, ...)
end)
end
playerRemoteEventData.waitingCoroutineForEachInvoke[invokeNumber] = coroutine.running()
task.delay(timeout, resumeCoroutineIfTimedOut, player, remoteEvent, invokeNumber)
return coroutine.yield()
end
local function onPlayerRemoving(player)
local playerData = data[player]
if playerData == nil then
-- This player hasn't been invoked at all.
return
end
data[player] = nil
for _, playerRemoteData in playerData do
for invokeNumber, waitingCoroutine in waitingCoroutineForEachInvoke do
resumeCoroutine(playerRemoteEventData, invokeNumber, waitingCoroutine, InvokeClientServerModule.playerLeftReturnValue)
end
end
end
Players.PlayerRemoving:Connect(onPlayerRemoving)
return InvokeClientServerModule
The client function that is connected to RemoteEvent.OnClientEvent
should have invokeNumber
as its first parameter. and the client should give invoke number as the first argument to FireServer
.
local function handleClientInvoke(invokeNumber, --[[possibly other parameters after invokeNumber]])
--[[
do something
--]]
remoteEvent:FireServer(invokeNumber, --[[possibly other arguments after invokeNumber]])
end
remoteEvent.OnClientEvent:Connect(handleClientInvoke)
Edit June 18:
I realised I had defined timeoutReturnValue
and playerLeftReturnValue
as local variables. They must be stored as member variables of InvokeClientServerModule
so that other modulescripts or scripts using the function can check the reason for resuming the thread. I’ve fixed this now.
I also added InvokeClientServerModule.successReturnValue
because I realised that it’s probably a better idea that the first value returned by InvokeClientServerModule.invoke
is always a value that explicitly tells the reason for continuing the thread. With the earlier code, the first return value was either
-
timeoutReturnValue
,
-
playerLeftReturnValue
or
- the first one of the values received from the client.
With the current code, the first value is either
-
InvokeClientServerModule.timeoutReturnValue
,
-
InvokeClientServerModule.playerLeftReturnValue
or
-
InvokeClientServerModule.successReturnValue
.
In the case of the first return value being InvokeClientServerModule.successReturnValue
, the other return values are values received from the client. In the other two cases, there are no other return values.