Is there a way to check if a table variable was changed?

I’m pretty new to this forum and I’m an amateur but I wanted to know if there was a way to tell if a value was changed inside of a table. I created a data store script and I have a looped save but I feel like I’m doing it wrong. So instead I wanted to only save the data if it was changed and not loop the saving.

Here’s what I’m working with right now:

>  while wait(5) do
	for i, player in pairs(game.Players:GetPlayers())do
		_G.PlayerData[player.userId] = dataStore:GetAsync(player.userId, _G.PlayerData[player.userId])	
 		updateEvent:FireClient(player, _G.PlayerData[player.userId])
 	end	
 end

I works and all but I’m not sure if it’ll eventually lag the server or any other good stuff. Like I said, I’m still an amateur. :sweat_smile: I appreciate you taking the time to check this, thank you.

3 Likes

Indirect access.

local tab = {1,2,3}--your table

local proxy = newproxy(true)--the proxy userdata you will use for access and new values from now on
local meta = getmetatable(proxy)
meta.__index = tab
meta.__newindex = function(self,i,v1)
  if tab[i]~=v1 then
    tab[i]=v1
    print("Changed")
  end
end

proxy.test = 1 -->Changed
print(proxy.test)  -->1
10 Likes

Shouldn’t you use rawset to avoid a C stack overflow, or does that not happen in the way you set it up? I’m thinking that because you have __newindex, when you set the value tab[i] = v1, this invokes metamethods as well. Even if you do a check with the not equal operator, it’s still getting called.

1 Like

That won’t work and there won’t be an overflow since they are both different objects with different types.

Proxy is a userdata, so no rawset. Tab is a table without a metatable, so no metamethods called when setting a new value.

1 Like

Data stores have an OnUpdate callback, where you can connect a function to the value whenever it changes, so you could just do something like this:

local dsConnections = {}

-- connects a callback to anyone already in the server
-- this is probably only needed for studio testing
for _, player in ipairs(game.Players:GetPlayers()) do
	dsConnections[player.UserId] = dataStore:OnUpdate(player.UserId, function(dat)
		updateEvent:FireClient(player, dat)
	end)
end

-- connects a callback to any player who joins the server
game.Players.PlayerAdded:Connect(function(player)
	
	-- There should never already be a callback connected before a player joins, 
	-- but it never hurts to be careful
	local connection = dsConnections[player.UserId]
	if connection then connection:Disconnect() end
	
	dsConnections[player.UserId] = dataStore:OnUpdate(player.UserId, function(dat)
		updateEvent:FireClient(player, dat)
	end)
end)

-- disconnects the callback to any leaving player
game.Players.PlayerRemoving:Connect(function(player)
	local connection = dsConnections[player.UserId]
	if connection then connection:Disconnect() end
	
	dsConnections[player.UserId] = nil
end)

Otherwise, with normal tables, the approach I go for is creating custom Set and Get methods for my table, and editing a storage table inside it using those functions. Then I could add custom functions inside the Set function, since this would (or is supposed to) be the only way to change data inside the Storage table:

local Module = {
	Storage = {}
}

function Module:Get(i)
	return self.Storage[i]
end

function Module:Set(i, v)
	-- check if self.Storage[i] ~= v
	-- if so, set a flag true, or fire a remote event

	self.Storage[i] = v
end
2 Likes

This is how I like to do it, because it lets you track nearly everything the table is doing. You can replace the prints with whatever you want to do when that action happens.

Thank you to @nooneisback for helping me write this for Lua 5.1!

local function TableTracker(Table)
	local proxy = newproxy(true); local meta = getmetatable(proxy)
	
	meta.__index = function(_, key)
		print(tostring(Table).." - access to element ".. tostring(key))
		return Table[key]
	end
	meta.__newindex = function(_, key, value)
		print(tostring(Table).." - update to element ".. tostring(key) .. ' to ' .. tostring(value))
		Table[key] = value
	end
	meta.__len = function()
		print(tostring(Table).." - access to table length")
		return #Table
	end
	return proxy
end

Then, when you create the table you want to track, do

local tbl = TableTracker({"124","hagfal", "foo"})

and then it behaves like this:

tbl[2] = 4 -- prints "table:XXXXXXXX - update to element 2 to 4"

local var = tbl[1] -- prints "table:XXXXXXXX - access to element 1"

This works for dictionaries and arrays. When it prints element i it would print element key for dictionaries.

This is best way I know of to properly track a table.

8 Likes

You forgot that tables in Lua 5.1 don’t support the __len metamethod. You’ll have to use userdatas for that.

Oh shoot
Thanks for letting me know!

Care to provide an example of how to do this properly?

local function TableTracker(Table)
	local proxy = newproxy(true); local meta = getmetatable(proxy)
	
	meta.__index = function(_, key)
		print(tostring(Table).." - access to element ".. tostring(key))
		return Table[key]
	end
	meta.__newindex = function(_, key, value)
		print(tostring(Table).." - update to element ".. tostring(key) .. ' to ' .. tostring(value))
		Table[key] = value
	end
	--had to remove pairs as it only supports tables
	meta.__len = function()
		print(tostring(Table).." - access to table length")
		return #Table
	end
	return proxy
end
2 Likes

Thanks! I’ll go update my reply.

You could implement a replacement for __pairs as a custom method that returns the same thing, but I can’t really do much about large code chunks from where I am right now.

1 Like

Hold up - are you using the userdata solely to proxy metamethods to update the table and create an update call? That’s an interesting way to use them. And I assume that raw methods are for if you set a metatable directly to a table and have other metamethods (e.g. __index and __newindex) involved.

That’s why the function to create them is called newproxy. They are just empty userdatas that you can do anything you want with, but that is what they are meant for. And as you said, rawset is used for if you don’t want to use indirect access, but a single table.

1 Like

Thank you! That helps a lot, hopefully I do it right this time!

1 Like

What is a proxy? I’ve heard of them, but don’t fully understand

Basically empty userdatas. Their main use was the __len metamethod for the # operator, since tables don’t have access to it, as well as them being slightly faster and better at memory efficiency than empty tables when used for indirect access; however, that’s pretty much it.

So they’re essentially tables with unique functions? You mentioned empty tables. are you even able to store data in a proxy?

Nope, you cannot. Userdatum is the most primitive object available. In vanilla Lua, it is nothing more than a reserved space in your memory that serves as an interface for Lua and C functions. A proxy is just a userdatum with an empty metatable.

In other words, you can use them if you don’t need to store any data, but do require metamethods.