Detect changes in a nested table OOP

Hi, I’m currently making a data store system using OOP. I want to detect changes in a nested table (or the player data)
Module script:

local PlrData = {}
PlrData.__index = PlrData

local transferData = require(script.Parent.transferData)

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


function PlrData:newData() -- Create new data if the player is new
	local newData = {
		PlrStats = {
			Strength = 1,
			Agility = 1,
			Magic = 1,
			Vitality = 1,
			Defense = 1,
		},	
	}
	return newData
end

local SavingFor = {} 

function PlrData.init(plr)
	local plrTable = {}
	
	local self = setmetatable({}, PlrData)
	self.dataStore = game:GetService("DataStoreService"):GetDataStore("test")
	self.key = plr.UserId .. "'s Data"
	self.table = plrTable
	
	local mt = setmetatable(PlrData, {
		__index = function(t, k)
			if type(self[k]) == "table" then
				-- if I change the strength of the player, it would not print out anything because it doesnt detect any changes to self.table
			end
			return self[k]
		end,
		__newindex = function(t, k, v)
			self[k] = v
			return self[k]
		end,
	})
	
	return mt
end

function PlrData:GetData(plr) -- return the data table of the according plr
	return self.table[plr.UserId]
end

function PlrData:LoadData(plr) -- load the data of the according player
	local Data
	
	local suc, err = pcall(function()
		Data = self.dataStore:GetAsync(self.key)
	end)

	if not suc then 
		return warn(err)
 	end
	
	self.table[plr.UserId] = Data or self:newData()
end

function PlrData:SaveData(plr)
	if not SavingFor[plr] then                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
		SavingFor[plr] = true
		
		local plrId = plr.UserId
		
		if self.table[plrId] then
			local suc, err = pcall(function()
				self.dataStore:SetAsync(self.key, self.table[plrId])
			end)
			
			if not suc then
				return warn("Data wasn't saved for " .. plr.Name .. ".")	
			end
		end
		
		SavingFor[plr] = nil
	end
end

function PlrData:UpdateData(plr, newDataTbl)
	self.table[plr.UserId] = newDataTbl
	printTable(self.table[plr.UserId], "")
end

return PlrData

So basically I want to detect any changes made inside this table

plrID = {
    PlrStats = {
			Strength = 1,
			Agility = 1,
			Magic = 1,
			Vitality = 1,
			Defense = 1,
   },	
}

While returning local PlrData = {} PlrData.__index = PlrData. So far I have only made it to detect changes to the first hierarchy of the PlrData. I want it to detect changes to the stats in the self.table itself, like whenever I change the Strength, etc.
I have looked at other posts but the code seems very confusing and hard to implement into OOP
Any help is appreciated!

After a while of digging and scrambling, I have managed to come up with this:


local function getNestedTables(t)
	local mt = {
		__index = function(self, k)
			return rawget(t, k)
		end,
		__newindex = function(self, k, v)
			if type(v) == "table" then
				print(k, v)
				return rawset(t, k, getNestedTables(v))
			else
				print(k, v)
				return rawset(t, k, v)
			end
		end,
	}

	return setmetatable({}, mt)
end

function PlrData.init(plr)	
	local plrTable = {}
	
	local self = setmetatable({}, PlrData)
	self.dataStore = game:GetService("DataStoreService"):GetDataStore("test")
	self.key = plr.UserId .. "'s Data"
	self.table = plrTable
	
	local mt = setmetatable(PlrData, {
		__index = function(t, k)
			if type(rawget(self, k)) == "table" then
				local tbl = getNestedTables(self[k])
			end
			return rawget(self, k)
		end,
		__newindex = function(t, k, v)
			print(k, v)
			return rawset(self, k, v)
		end,
	})

	return mt
end

I don’t know what’s wrong with it as it’s not working. It makes sense to me.

I’m pretty sure those metamethods only invoke when you index the table in which the metatable is attached to (from my knowledge, don’t play around with metatables a whole lot.)

Is it a solution to instead use a module to handle updating changes to specified fields in the player’s data, then executing callbacks to do what you need to do?

1 Like

Hi, thanks for your reply, I basically created a recursion to create metamethods and set the according metatables if the value is a table. So I don’t know why the __index is still not being fired.

Yes, it is a viable solution to create a setter, but it feels “hacky” and I want to stick to using metatables in order to get used to it.

Is there any reason why you cant modify the __newindex of the player data table instead?

-- on phone, mind my coding
plrID.__newindex = function(tbl, key, value)
      if tbl == rawget(plrID, PlrStats) then
            warn(tostring(key).." stat being modified to "..tostring(value)
      end
      rawset(tbl, key, value)
end

I don’t know if invoking it like plrID[1][PlrStats] will invoke the __newindex/index method on the plrID table or the PlrStats table.

Either way you should be able to modify it, this is just one of my solutions. I’m not too experienced with OOP/metatables either.

1 Like

Hi, Thanks for your reply,

It’s because I need to return the PlrData, or else I wouldn’t be able to use the module and its’ metamethods (or functions).
Edit: So I just added a for loop and somehow it worked

local function getNestedTables(t)	
	for i, v in pairs(t) do
		if type(v) == "table" then
			t[i] = getNestedTables(v)
		end
	end
	
	local mt = {
		__index = function(self, k)
			return rawget(t, k)
		end,
		__newindex = function(self, k, v)
			if type(v) == "table" then
				print(k, v)
				return rawset(t, k, getNestedTables(v))
			else
				print(k, v)
				return rawset(t, k, v)
			end
		end,
	}
	
	return setmetatable({}, mt)
end

It worked for the first time, after I rejoin the place the data gets “corrupted” and when I print the table using this

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

It doesn’t print out the whole table
image
So my take on this issue is… I have absolutely zero idea… This is getting way too out of hand and unnecessary for no reason at all, so I might have to resort to what @C_Sharper has suggested.

After a while I have decided to come back to this, so I have modified the code to purely metatables, no OOP this time:

local dummy = {
	PlrStats = {
		Strength = 1,
		Agility = 1,
		Magic = 1,
		Vitality = 1,
		Defense = 1,
	},	
}	
	
function getNestedTable(t)
	local mt = setmetatable({}, {
		__index = t,
		__newindex = function(self, k, v)
			if type(v) == "table" then
				print(k)
				return rawset(t, k, getNestedTable(v))
			else
				print(k)
				if v == "Defense" then
					print(v)
				end
				return rawset(t, k, v)
			end
		end,
	})
	for k, v in pairs(t) do
		if type(v) == "table" then
			getNestedTable(v)
			print(v)
		end
	end
	return mt
end

local plrStats = getNestedTable(dummy)
printTable(plrStats, "")

And yes, the problem still occurs, no idea why. Only prints out
image