I can’t think of a way to do this with a single proxy table, but if you use empty tables as proxies instead of using userdatas created with newproxy, you’ll only need one metatable for the whole nested table. You can then make a proxy table hierarchy that is used when detecting changes. If you store data about the parent and child relationships of the proxy tables, you can even find the whole key/index path from the top table to the changed key/index in a descendant table.
I wrote some code that does the things mentioned above. It’s meant to be a class in a modulescript. If a new table, which can be nested too, is added somewhere inside the nested table, it automatically gets its own proxies in the proxy hierarchy. References to proxies for removed tables are cleaned up.
I’ve tested the code a little but not much so it’s possible that there are problems that I haven’t noticed. For example, I’m not sure if I did the reference clean up properly, so there might be memory leaks.
I’ll go to sleep soon, so if you have any questions, I probably won’t reply before tomorrow.
local NestedTableWithChangeDetection = {}
local SPECIAL_DATA_INDEX = "_CHANGE_DETECTION_DATA"
-- This is similar to Instance:GetFullName(). The first key in this string is a key in the main table
local function getHierarchyString(k, proxyTable, hierarchy)
local parentProxies, keys = hierarchy.parentProxies, hierarchy.keys
-- tostring in case the key is something else than a string
local hierarchyString = tostring(k)
local childProxy = proxyTable
while parentProxies[childProxy] ~= nil do
hierarchyString = tostring(keys[childProxy]) .. "." .. hierarchyString
childProxy = parentProxies[childProxy]
end
return hierarchyString
end
local function clearProxyData(proxyTable, hierarchy)
local keys, parentProxies, childProxies, dataTables = hierarchy.keys, hierarchy.parentProxies, hierarchy.childProxies, hierarchy.dataTables
local proxyTablesToRemove = {proxyTable}
local currentParents = {proxyTable}
while #currentParents > 0 do
local newParents = {}
for _, parent in ipairs(currentParents) do
for _, child in ipairs(parent) do
table.insert(newParents, child)
table.insert(proxyTablesToTemove, child)
end
end
currentParents = newParents
end
childProxies[ parentProxies[proxyTable] ][ keys[proxyTable] ] = nil
for _, proxy in ipairs(proxyTablesToRemove) do
keys[proxy], parentProxies[proxy], childProxies[proxy], dataTables[proxy] = nil, nil, nil, nil
end
end
-- last three are only use when adding a new table to an existing nested table
local function createProxyData(dataTable, meta, existingHierarchy, proxyParent, key)
-- keys: key of a proxy in its parent's childProxies tables
-- childProxies tables are used instead of proxy[childKey] = childProxy so that changes to childKey's value are detected by newindex
local hierarchy = existingHierarchy or {
keys = {},
parentProxies = {},
childProxies = {},
dataTables = {}
}
local keys, parentProxies, childProxies, dataTables = hierarchy.keys, hierarchy.parentProxies, hierarchy.childProxies, hierarchy.dataTables
local proxyMain = {}
childProxies[proxyMain] = {}
dataTables[proxyMain] = dataTable
local currentParents = {dataTable}
local currentProxyParents = {proxyMain}
while #currentParents > 0 do
local newParents, newProxyParents = {}, {}
for i, parent in ipairs(currentParents) do
local proxyParent = currentProxyParents[i]
-- v is a child table or a value
for k, v in pairs(parent) do
if type(v) == "table" then
local vProxy = {}
dataTables[vProxy] = v
childProxies[proxyParent][k] = vProxy
parentProxies[vProxy] = proxyParent
keys[vProxy] = k
childProxies[vProxy] = {}
table.insert(newParents, v)
table.insert(newProxyParents, vProxy)
setmetatable(vProxy, meta)
end
end
end
currentParents = newParents
currentProxyParents = newProxyParents
end
if existingHierarchy then
keys[proxyMain] = key
parentProxies[proxyMain] = proxyParent
childProxies[proxyParent][key] = proxyMain
else
dataTable[SPECIAL_DATA_INDEX] = {
hierarchy = hierarchy
}
end
setmetatable(proxyMain, meta)
return proxyMain
end
-- constructor
-- if onChangeFunction has parameters they should be for hierarchy string, old value and new value.
function NestedTableWithChangeDetection.new(data, onChangeFunction)
-- Create the metatable. This will be set as the metatable of every table in the nested proxy table.
local meta = {}
function meta:__newindex(k, newVal)
local specialData = data[SPECIAL_DATA_INDEX]
local hierarchy = specialData.hierarchy
local dataTable = hierarchy.dataTables[self]
if dataTable[k] ~= newVal then
local oldVal = dataTable[k]
if type(oldVal) == "table" then
clearProxyData(hierarchy.childProxies[self][k], hierarchy)
end
if type(newVal) == "table" then
createProxyData(newVal, meta, hierarchy, self, k)
end
local hierarchyString = getHierarchyString(k, self, hierarchy)
dataTable[k] = newVal
onChangeFunction(hierarchyString, oldVal, newVal)
end
end
function meta:__index(k)
local specialData = data[SPECIAL_DATA_INDEX]
local hierarchy = specialData.hierarchy
local dataVal = hierarchy.dataTables[self][k]
if type(dataVal) == "table" then
return hierarchy.childProxies[self][k]
end
return dataVal
end
-- Create hierarchy data in the above hierarchy table.
return createProxyData(data, meta)
end
return NestedTableWithChangeDetection
Edit: Here’s an example of using it. I also made some other small edits.
local NestedTableWithChangeDetection = require(--[[modulescript]])
local data = {
Inventory = {
Apple = 5,
Knife = 2,
Gun = 0
},
Effects = {
Fire = true,
Sparkles = false
}
}
local function printChange(hierarchyString, oldVal, newVal)
print(hierarchyString .. " changed from " .. tostring(oldVal) .. " to " .. tostring(newVal) .. ".")
end
local changeDetectData = NestedTableWithChangeDetection.new(data, printChange)
changeDetectData.Inventory.Apple = 6
-- output: Inventory.Apple changed from 5 to 6.