How to make client's table read-only

Hi there, I currently have a stats replicator that passes new values to the client every time that value on the server gets altered. Here’s the code for it:

local plrConst = {}

local activeTbl = {}

local RunService = game:GetService("RunService")
local transferEvent = script.transfer
local setUpModule = require(script.setUpData)

local plrStatsTbl = {

}

local function printTable(tab, string)
	for i,v in pairs(tab) do
		if type(v) == "table" then
			print(string,i," : ",v,":::")
			printTable(v, string.."     ")
		else
			print(string,i," : ",v)
		end
	end
end

local function addChangedEvent(tbl)
	local events = {}

	local changedEvent = Instance.new("BindableEvent")
	
	rawset(tbl, "Changed", changedEvent.Event )
	
	local function getPropertyChangedSignal(key)
		local properyChangedEvent = Instance.new("BindableEvent")
		events[key] = events[key] or {}
		table.insert(events[key], properyChangedEvent)

		return properyChangedEvent.Event
	end

	rawset(tbl, "getPropertyChangedSignal", getPropertyChangedSignal)
	
	return changedEvent, events
end

function constructMetatbl(originalTbl, callback)
	local changedEvent, events 
	
	local proxy = setmetatable({}, {
		__index = function(self, key)
			return rawget(originalTbl, key)
		end,
		__newindex = function(self, key, v)
			if callback then
				callback(key, v)	
			end

			if changedEvent and events then
				changedEvent:Fire(key, v)

				if events[key] then
					for i, bindable in pairs(events[key]) do
						bindable:Fire(v)
					end				
				end				
			end
			
			return rawset(originalTbl, key, v)
		end,
	})
	
	changedEvent, events = addChangedEvent(proxy)
	
	return proxy	
end

function plrConst:addNewPlr(plr, dataTbl)
	if not activeTbl[plr] then
		local plrStatsProxy = constructMetatbl(plrStatsTbl, function(key, v)
			transferEvent:FireClient(plr, {tab = "plrStats", Key = key, Val = v})
		end)
		
		local dataProxy = constructMetatbl(dataTbl, function(key, v)
			transferEvent:FireClient(plr, {tab = "data", Key = key, Val = v})
		end)
		
		local newTbl = {
			plrStats = plrStatsProxy,
			data = dataProxy,
		}

		activeTbl[plr] = newTbl

		transferEvent:FireClient(plr, {newTbl = { -- send non-metatables cuz client can't recieve metatables
			plrStats = plrStatsTbl,
			data = dataTbl,
		}})
		
		setUpModule:setUp(plr, activeTbl[plr])
		
		return newTbl
	end
end

function constructReadOnly(originalTbl)
	local changedEvent, events 

	local proxy = setmetatable({}, {
		__index = function(self, key)
			return rawget(originalTbl, key)
		end,
		__newindex = function(self, key, v)
			if changedEvent and events then
				changedEvent:Fire(key, v)

				if events[key] then
					for i, bindable in pairs(events[key]) do
						bindable:Fire(v)
					end				
				end				
			end
			print(key, v)
			return nil
		end,
	})

	changedEvent, events = addChangedEvent(proxy)

	return proxy	
end

function plrConst:returnTbl(plr, key)
	if activeTbl[plr] then
		if RunService:IsServer() then
			return activeTbl[plr][key] or activeTbl[plr]
		else			
			return constructReadOnly(activeTbl[plr][key] or activeTbl[plr])		--constructReadOnly(activeTbl[plr][key] or activeTbl[plr]) --activeTbl[plr][key] or activeTbl[plr]			
		end
	end
end


if RunService:IsClient() then
	local plr = game.Players.LocalPlayer
	
	transferEvent.OnClientEvent:Connect(function(args)
		if args.newTbl then -- construct
			args.newTbl = {
				plrStats = constructMetatbl(args.newTbl.plrStats, function(key, v)
					--if serverTbl[plr] and serverTbl[plr][key] ~= v then
						--return error("Cannot set values on the client")						
					--end 
				end),
				data = constructMetatbl(args.newTbl.data, function(key, v)
					--if  serverTbl[plr] and serverTbl[plr][key] ~= v then
						--return error("Cannot set values on the client")
					--end
				end),
				
			}		
			
			--addChangedEvent(args.newTbl)
			
			activeTbl[plr] = args.newTbl or activeTbl[plr]
		end
		
		--printTable(args, "")
		if args.Key and args.tab then
			--print(args.Key, args.Val)
			activeTbl[plr][args.tab][args.Key] = args.Val
 		end
	end)
	
end

return plrConst

As you can see I hooked up a remote event in the metatable on the server, update the value to the client. However, doing so allow the client to change the values without restriction (The values of the client’s table of course), although this doesn’t replicate to the server, all of my stats checking on the client are based off of the client’s table, so this will bypass that validation.

I have tried returning a read only table in the returnTbl method as this is what the client uses to get their tables, however it doesn’t seem to work properly.

Basically I need to make the returnTbl return a read-only table while referencing to the original client’s table to get updated values from it. (Making the original client table read-only does work but this will completely malfunction the remote the server sends as it can’t update any values unless I pass the whole table to it, which is very excessive)

Any help is appreciated

1 Like

Would it be possible to create a read-only clone of the original table which is sent to the client? Doing checks on the client should only be for user feedback, everything else should preferably be made on the server.

Also, you have a unique structure to your module script. When I do OOP mine usually look like this:

local module = {}
module.__index = module

function module.new()
	local self = {}
	...
	setmetatable(self, module)
	return self
end


function module:ObjectFunction()
	...
end


function module.GlobalFunction()
	...
end

return module
2 Likes

Yes it would, in fact I have tried this method:

function plrConst:returnTbl(plr, key)
	if activeTbl[plr] then
		if RunService:IsServer() then
			return activeTbl[plr][key] or activeTbl[plr]
		else			
			return {
				plrStats = constructReadOnly(activeTbl[plr].plrStats),
				data = constructReadOnly(activeTbl[plr].data)
			}		
		end
	end
end

While it does make the table read-only I think it has one fatal flaw. By cloning it it loses its’ reference to the original table, thus, when I make changes to the original table, it doesn’t replicate to that cloned table.

1 Like

Use the table.freeze method

1 Like

I’ve set my table to a metatable already (To detect changes made to that table) and I don’t think you can freeze a metatable as it throws an error:


Thanks for the suggestion though!

1 Like

You’re going to need to define a custom setter function that informs the client of any changes made to the original table, when these changes occur, you can either send a new reference to the original table itself or send the new key & value pair for the client to insert into their copy of the table.

1 Like

I’m pretty sure my code above is exactly like what you’ve just said, I detect changes in the server, send a remote to the client, client updates their version of the table.

However the problem is that after the player has required this cloned table in another script (Using the returnTbl method), the player can modify that cloned table (This gets replicated to other local scripts aswell). Which is why I want to make the returned table read-only.

Then instead, send the table itself across the network (the client will receive a new reference to a new table). Use table.freeze if necessary or explicitly define the table’s __index and __newindex metamethods your self.