hi, I want complete control on what client sends to server while not yielding any threads forever. I created a fake remote function and just wondering if this is alright (maybe there is a better way) and if I missed anything. (I don’t know which category would have been better.)
basically client fires back to server to the open data index after server requests data
replicatedStorage.ClientRemotes.BetterInvoke.OnServerEvent:Connect(function(client: Player,time:string,response:any)
if type(time) ~= "string" or betterFunctions.clientResponses[client][time] ~= "Open" then
client:Kick("Attempted to manipulate data.")
return
end
betterFunctions.clientResponses[client][time] = response --is this alright?
end)
the fake remote function handler:
functions.clientResponses = {}
functions.invokeClient = function(client:Player?,...)
local RemoteFunction: RemoteEvent = ReplicatedStorage.ClientRemotes.BetterInvoke --My goal is to use BetterInvoke remote event as remote function. So I don't mess with yielding.
if client and client:IsA("Player") then --switch to early return for less indent
local start = os.clock()
local playerData
local args = {...} --I can't send it into pcall otherwise
local _, Error = pcall(function()
local maxIndex = 20
local totalIndex = 0
local function deepCopy(possibleTable:any?) --I don't want to change the data, so I create a copy. I forgot why tho.
local clonedData
if type(possibleTable) == "table" then
clonedData = {}
for k, v in pairs(possibleTable) do
totalIndex += 1
if totalIndex > maxIndex then --don't want clients to send big tables.
if client then
client:Kick()
end
return
end
if type(v) == "table" then
v = deepCopy(v)
end
clonedData[k] = v
end
else
clonedData = possibleTable
end
return clonedData
end
functions.clientResponses[client][tostring(start)] = "Open"
RemoteFunction:FireClient(client,tostring(start),table.unpack(args))
playerData = deepCopy(functions.clientResponses[client][tostring(start)])
if playerData == "Open" then
repeat
print("waiting") --for test (usually prints 3 times which is same as remote functions)
task.wait()
if not functions.clientResponses[client] then
return
end
playerData = deepCopy(functions.clientResponses[client][tostring(start)])
until playerData ~= "Open" or os.clock() - start > 2
end
end)
if functions.clientResponses[client] then
functions.clientResponses[client][tostring(start)] = nil
end
if Error then --mainly because player might just left the game
warn(Error)
return
elseif playerData == "Open" then
playerData = nil
warn("Failed to receive information") --for test
end
return playerData --might exist, might not exist. Will be checked if it fits the required data type and if there is manipulations.
end
end
An example of getting data from a different module;
func.latestMousePositions = {}
func.getMousePos = function(player:Player)
if not player:IsA("Player") then
return
end
local mousePos:Vector3? = betterFunctions.invokeClient(player,"Give Mouse Position")
if mousePos and (typeof(mousePos) ~= "Vector3" or mousePos.X ~= mousePos.X or mousePos.Y ~= mousePos.Y or mousePos.Z ~= mousePos.Z) then
player:Kick()
return
elseif not mousePos then
if func.latestMousePositions[player] then
return func.latestMousePositions[player]
else
return
end
else
func.latestMousePositions[player] = mousePos
end
return mousePos --which will run a function if it exists - stop it if it does not.
end
Also yes, on player added - removing the player table instance gets added - removed.
Use guard clauses and newlines because this looks, not to be mean, unreadable
Instead of making all of this take inspiration from one of my old modules
It shouldn’t be used in production though, but you can see how it works and adapt it to your codebase that way.
Your approach for invoking remote events while maintaining control and avoiding thread yielding is valid. However, a few things to consider:
Data Size: Be cautious with the data size limitation, as handling large data client-side can impact performance.
Deep Copy: Your deep copy function is necessary for data integrity, but it can be optimized for better performance.
Client Kicking: Kicking clients for data manipulation is a good security measure, but provide clear error messages.
Polling: Polling for data isn’t very efficient. Consider asynchronous handling with promises or callbacks.
Cleanup: Ensure your data cleanup logic is robust and doesn’t leave dangling data.
Error Handling: Enhance error messages and server-side logging for debugging.
Documentation: Add comments and documentation for clarity.
Your approach can work, but it’s complex and almost unreadable. Evaluate if the trade-offs align with your game’s requirements and explore other communication patterns if needed.
I added kick message, tried to make it more clear. Also deepcopy is just 1 line, i don’t think i will use tables a lot (or big tables), but still client can manipulate so I set maxIndex to something like 5 (I can improve it later). Also I can make it more robust with setting default values like default timeLimit.
So what I understand is I just need to make it more clear to read? I mean I don’t see any way to stop polling, bindable event comes to my mind but they don’t have Event:Wait(), would be glad if you can tell me how can I stop it.
Also @HexadecimalLiker I checked the repo, i mean I was already invoking in task.spawn() like your invoke function but the client can still yield that thread forever while you can continue. That was the reason for this remote event invoking. Am I wrong that they can yield that thread forever?
Here is uh more clear(?) code;
functions.clientResponses = {}
functions.invokeClient = function(client:Player?,...)
if not client or not client:IsA("Player") then
return
end
local RemoteFunction: RemoteEvent = ReplicatedStorage.ClientRemotes.BetterInvoke --My goal is to use BetterInvoke remote event as remote function. So I don't mess with yielding.
local start = os.clock()
local playerData
--can't send args into pcall, so I put args into a table and move that into pcall.
local args = {...}; local _, Error = pcall(function()
local maxIndex = 5
local currentIndex = 0
--I don't want to change the data, so I create a copy. Also for data to exist after data point deletion.
local function deepCopy(possibleTable:any?)
local clonedData
if type(possibleTable) == "table" then --there is no functions for this yet so don't worry about it, see the else statement.
clonedData = {}
for key, value in pairs(possibleTable) do
currentIndex += 1
if currentIndex > maxIndex then --received data is bigger than max data size.
if client then
client:Kick("Tried sending data that exceeds the limits.")
end
return
end
if type(value) == "table" then --found a table index, will deep copy it aswell.
value = deepCopy(value)
end
clonedData[key] = value
end
else
clonedData = possibleTable
end
return clonedData
end
--init
functions.clientResponses[client][tostring(start)] = "Open" --opens a data point
RemoteFunction:FireClient(client,tostring(start),table.unpack(args)) --orders player to send back data
--demands data in given time limit
local timeLimit = 2
repeat
--print("waiting") --for test
task.wait()
if not functions.clientResponses[client] then
return
end
playerData = deepCopy(functions.clientResponses[client][tostring(start)])
until playerData ~= "Open" or os.clock() - start > timeLimit
end)
--data received or time limit passed, does some checks before returning data.
if functions.clientResponses[client] then --client might left the game, which means data point erased already.
functions.clientResponses[client][tostring(start)] = nil --close the open data point
end
if Error then --mainly because player might just left the game
warn(Error)
return
elseif playerData == "Open" then --player failed to send data back, might be connection issue etc.
playerData = nil --we don't want to send string values to server when client fails to send data
--warn("Failed to receive information") --for test
end
return playerData --might exist, might not exist. Will be checked if it fits the required data type and if there is manipulations.
end