Sending a Dictionary to a Client, but Hiding Some Fields

Often, you want to not disclose information to the client. If you send the location of a secret to the client, a hacker can detect the location easily. So, you might want to send “all the information about an area” to the client, but not some fields. It can be awkward - do you keep two separate ‘area’ dictionaries - like area_server and area_client? Instead, I thought of labelling all properties with “s_” if they are server-only.

areaData = {	-- Any fields starting with "s_" should not go to client.
	title = "Cemetary",
	s_key = {3,7} --client shouldn't know this location
}

-- remote function, called by client when it needs something from server. It passes name of query as pParams.q.
rf.OnServerInvoke = function(plr,pParams)
	if pParams.q=="getAreaData" then
		return areaData -- send the client all the information about the area (but will also include secret fields, that we don't want client to know yet).
	end

So, I’ve labelled server-only fields with “s_” in the areaData dictionary. To filter them out before sending to client:

function retClientSafeDict(pDict)
	if pDict==nil then return nil; end;
	local lClientDict = deepCopy(pDict)	-- a func to copy the dict. Next: REMOVE any properties that start with s_. (Don't want to do this on live dict, so we made a copy first)
	local function recurse(pClientDict)
		for propName,propVal in pairs(pClientDict) do
			if typeof(propName)~="number" and propName:sub(1,2)=="s_" then -- Do not send to client. (if it was a number, it's a List member and doesn't have a 'name' to check just [1][2][3]etc)
				pClientDict[propName] = nil
			elseif type(propVal)=="table" then -- need to recurse and check each child
				recurse(propVal)
			end
		end
	end
	recurse(lClientDict)	
	return lClientDict
end

Now, in my script that sends areaData to the client, it does this:

rf.OnServerInvoke = function(plr,pParams) -- remote function, called by client when it needs something from server
	if pParams.q=="getAreaData" then
		return retClientSafeDict(areaData) -- send the client area info (but WITHOUT the server-only data)
	end

I no longer have to worry about ways of managing information about ‘areaData’ in a safe way. I just label properties in the dict with s_ and voila, they don’t routinely get sent to the client. It’s immediately been so useful, and means I can keep information more simple in code.

1 Like

This looks like a pretty interesting way of filtering out server and client data without using unnecessary data. I am curious if there’s any way you can do this with metatables to simplify it even more, but this does look pretty good. I’d love to see what you use this for specifically, and some more example code on how you use it. Good luck.

Example, in ServerScriptService, there’s a monsterTypes module. When monsters spawn, the monster details are passed to the client. The client doesn’t get a copy of the module, because, I don’t want to reveal to the client what all the monster types are, and what their properties are. So they get passed one by one as required. When I pass monster data, I want some fields to stay on the server and not go to the client. Those fields are prepended with “s_”.

ServerScriptService.monsterTypes:

skeleton_archer = {
	name = 	"Skeleton Archer",
	health = 	8,
        [... etc ...]
	s_drops={ [list] } -- list of possible drops when plr kills monster. don't reveal this list to client.
}

And in the remote function listener on the server, the client asks ‘what monster spawns next?’

rf.OnServerInvoke = function(plr,pParams) -- remote function, called by client when it needs something from server
	if pParams.q=="spawnNextMonster" then
		-- randomly pick monster etc
		local lMonster = monsterTypes.skeleton_archer -- but this contains data that's not for client          
		return retClientSafeDict(lMonster ) -- send the client area info (but WITHOUT the server-only data)
	end

On the client:

local lNextMonsterData = RS.rf:InvokeServer({q="spawnNextMonster"}) -- server will only pass me the things I need to know

(There might be fancy ways to do this with metatables (as you ask), but… although I use them for classes, I don’t know how to use them imaginatively for other purposes.)

1 Like

there is a fancy metatable way to do this, but it wouldn’t be useful as you would have to pass the data and metadata to the client regardless. But, the basic concept is as follows. Module goes into replicated storage:

local dataTable = {a=1,b=2,s_c=3,d=4}

return setmetatable({},{ --returning a proxy table
	__index = function(i)
		if game:GetService("RunService"):IsServer() then
			return dataTable[i]
		else
			if tostring(i):sub(1,2)~="s_" then
				return dataTable[i]
			end
		end
	end,
}) 

Obviously, issue here is module has to be replicated to begin with. This concept could be applied to nested tables as well too, with recursion. But still a cool concept to toy around with if youre just interested in the functionality side of things.

Sidenote, all table declarations are shallow by default but table.copy is a given deepcopy function. no need for manual implementation.