Roblox Shadow Table

Hello, My first Community Resource. A RBLX Shadow Table.
I’ve always liked the idea when it comes to roblox and editing tables from meta tables, So I present to you a shadow table. This shadow-table is stack safe so your errors will be fine with the events. But handling a table index may cause an error. I’ve tried to make it as basic as possible.

So, What is a shadow table?
A shadow table is a table with certain methods that allow you to collect/handle information with ease.
For example,

shadowTable.example = "Hello, World!" -- Our normal index

shadowTableChnagedEvent = shadowTable.example.Changed.Event:Connect(function(oldValue, newValue)
	print("oldValue: " .. tostring(oldValue)) -- Before changed

	print("newValue: " .. tostring(newValue)) -- After changed
end)

The reason this might be useful. Is due to handling values such as statistical values inside of a module.
For most developers, They use TableData. Meaning they cache player data inside a table on the server and manage it there.

Using a shadow table opens up your table to multitude of opportunities. This shadow table is basic and easy to understand for scripters who understand a little about meta methods. But even if you don’t, Using it is quite easy.

Code Example

The code example below easily changes an index.
Once this index is destroyed, Which you can just set it to nil. It should deal with the aftermath, Helping to reduce the chance of a memory leak.

local shadowTableModule = require(game:GetService("ReplicatedStorage").ShadowTable)
local shadowTable = shadowTableModule.new()
local shadowTableChnagedEvent

shadowTable.example = "Hello, World!" -- Our normal index

shadowTable.example.Removed.Event:Connect(function()
	shadowTableChnagedEvent:Disconnect() -- Once we remove the index, We disconnect the event.
	-- NOTE; The bindable destroys itself once destroyed.
end)

print(shadowTable)

shadowTableChnagedEvent = shadowTable:GetElementChangedSignal("example"):Connect(function(oldValue, newValue)
	print("oldValue: " .. tostring(oldValue)) -- Before changed

	print("newValue: " .. tostring(newValue)) -- After changed
end)

--[[shadowTableChnagedEvent = shadowTable.example.Changed.Event:Connect(function(oldValue, newValue)
	print("oldValue: " .. tostring(oldValue)) -- Before changed

	print("newValue: " .. tostring(newValue)) -- After changed
end)]]--

shadowTable.example = "Bye, World!" -- Fire the changed event
shadowTable.example = nil -- Fire the removed event.
Module Code
local shadowTable = {}
local tableContent = {}
local tableManipulation = {}

--<<-------------------------------------------------------->>--

tableContent.__metatable = "The metatable is locked."

function tableContent:__index(index)
	local content = rawget(self, "content")
	local events = rawget(self, "events")
	
	if events[index] then return events[index] end
	return content[index]
end

function tableContent:__newindex(index, value)
	local content = rawget(self, "content")

	content[index] = value
end

function tableContent:__tostring(index)
	local content = rawget(self, "content")

	return tostring(content)
end

function tableContent:__call(...)
	local content = rawget(self, "content")
	
	return content(...)
end

function tableContent:__concat(value)
	local content = rawget(self, "content")
	
	return content .. value
end

function tableContent:__unm()
	local content = rawget(self, "content")
	
	return -content
end

function tableContent:__add(value)
	local content = rawget(self, "content")
	
	return content + value
end

function tableContent:__sub(value)
	local content = rawget(self, "content")
	
	return content - value
end

function tableContent:__mul(value)
	local content = rawget(self, "content")
	
	return content * value
end

function tableContent:__div(value)
	local content = rawget(self, "content")
	
	return content / value
end

function tableContent:__mod(value)
	local content = rawget(self, "content")
	
	return content % value
end

function tableContent:__pow(value)
	local content = rawget(self, "content")
	
	return content ^ value
end

function tableContent:__eq(value)
	local content = rawget(self, "content")
	
	return content == value
end

function tableContent:__lt(value)
	local content = rawget(self, "content")
	
	return content < value
end

function tableContent:__le(value)
	local content = rawget(self, "content")
	
	return content <= value
end

function tableContent:__len()
	local content = rawget(self, "content")
	
	return #content
end

--<<-------------------------------------------------------->>--

function shadowTable:__index(index)
	local apiCallback = rawget(self, index)

	if apiCallback then
		return apiCallback
	else
		local content = rawget(self, "content")
		
		return content[index]
	end
end

function shadowTable:__newindex(index, value)
	local content = rawget(self, "content")
	
	if content[index] then
		local previousValue = rawget(content[index], "content")
		local events = rawget(content[index], "events")
		
		rawset(content[index], "content", value)
		
		if value == nil then
			events.Removed:Fire(previousValue, value)

			events.Removed:Destroy()
			events.Changed:Destroy()		

			rawset(content, index, nil)
		else
			events.Changed:Fire(previousValue, value)
		end
	else
		local indexTable = {}
		
		indexTable.events = {Changed = Instance.new("BindableEvent"), Removed = Instance.new("BindableEvent")}
		indexTable.content = value
		
		content[index] = setmetatable(indexTable, tableContent)
	end
end

--<<-------------------------------------------------------->>--

function tableManipulation:replicateElements(table_1, table_2, setvalueCallback, methodClass)
	for index, value in pairs(table_1) do
		if setvalueCallback then
			setvalueCallback(methodClass, table_2, index, value)	
		else
			table_2[index] = value	
		end
	end
end

function tableManipulation:removeElements(table_1)
	local clone = {}
	
	for index, value in pairs(table_1) do
		clone[index] = value
		
		table_1[index] = nil
	end
	
	return clone
end

--<<-------------------------------------------------------->>--

return {
	["new"] = function(previousDataTable)
		local self__content = {}
		local self__raw = previousDataTable or {}
		
		tableManipulation:replicateElements(self__raw, self__content)
		tableManipulation:removeElements(self__raw)
		
		self__raw.content = {}
		
		tableManipulation:replicateElements(self__content, self__raw.content, shadowTable.__index, self__raw)
		tableManipulation:removeElements(self__content)

		function self__raw:GetElementChangedSignal(elementName)
			return self__raw[elementName].Changed.Event
		end
		
		setmetatable(self__content, {mode = "k"})
		return setmetatable(self__raw, shadowTable)
	end
}

Aside from that. I guess this concludes my first community resource.
If you have any problems or suggestions. Feel free to tell me what I can do to improve.

GitHub: GitHub - MatrixJack/RBLX_ShadowTable

2 Likes