Metatable and Proxy table help

  1. What do you want to achieve?
    I’d like to listen to a tables changes using metatables
  2. What is the issue?
    I’m not sure how to listen to changes made in another module.
    From Programming in Lua : 13.4.4 :

Both __index and __newindex are relevant only when the index does not exist in the table. The only way to catch all accesses to a table is to keep it empty.

The table I’m trying to listen to arrives full, and the changes are being made in the module script. I dont want to use bindableEvents, but its beginning to look unavoidable

  1. What solutions have you tried so far?
    None really, I’ve read the syntax of metatables and im confuzzled

Basically, I have a modulescript that reads player inputs like WASD, Left/Right click, Shift,Tab,etc.
This module then exposes a table of booleans that indicate whether those buttons are pressed or not.

In my other script (where this table gets required), I’m using a renderStepped loop to move the player with a custom movement system, based on the bools in this table. However, I’d like to listen to changes in the table outside of the loop. This is where metatables come in. I’ve seen people use a proxy table type setup to listen to changes to a table, but in this case the changes to my table are being made in a different script, so I couldnt really follow the default setup. How would I go about setting up my metatable for this? Would this idea even work?

To re-iterate:
We are requiring a module. This module has a table. We are importing the table like so:
local t = module.Table (This works because tables are pass-by-reference)
How can I listen to changes to the table (in this script) with a metatable?

Apologies if im missing something really simple, I’ve only been looking into the syntax for a few hours now.

2 Likes

Proxies use a separate table or userdata. The reason for that is because __newindex doesn’t fire when reassigning a key. When you reference your table module, you would need to use your proxy table instead of your actual table, and even then you would need to hook up a callback via function or (custom) signals.

You can construct a proxy through a function and pass your callback. I personally would not recommend it, as it is more of a hassle than get/set functions.

--!strict

local function proxy<T, I, V>(t: T, callbackFunction: (key: I, newValue: V, oldValue: V) -> ()): T
	assert(typeof(t) == "table")
	
	local proxy: any = newproxy(true)
	local proxyMetatable: {[any]: any} = getmetatable(proxy)
	
	-- direct indexing back to original table
	proxyMetatable.__index = function(_: any, index: I): V
		return t[index] :: V
	end
	
	-- listen for new index on proxy and apply to original table
	proxyMetatable.__newindex = function(_: any, index: I, newValue: V): ()
		local oldValue: V = t[index] :: V
		t[index] = newValue
		
		callbackFunction(index, newValue, oldValue)
	end
	
	-- direct iteration to original table
	proxyMetatable.__iter = function(_: any): (typeof(next), T)
		return next, t
	end
	
	return proxy
end


-- example. myProxy will return the table you pass, getting full intellisense support.
-- the only caveat is that you would need to declare the types of index, newValue, and oldValue.
-- mixed tables you can just use any, any
local myProxy = proxy({
	Level = 1,
	Experience = 0
}, function(index: string, newValue: number, oldValue: number): ()
	print(`{index}: {oldValue} -> {newValue}`)
end)

myProxy.Level = 2
print(myProxy.Level)

myProxy.ExtraKey = 1
print(myProxy.ExtraKey)

for index: string, value: number in myProxy :: {[string]: number} do
	print(index, value)
end
2 Likes

The reason I’m trying to do it this way, is because the table changes on every key press, and I was hoping this way would be more performant than firing Bindable Events each time.
I dont think Get/Set functions apply here. I’m only setting the values inside the module (using CAS), and all elements are exposed through the table itself so Get() would be redundant.

Not a problem :slight_smile: all my indexes are strings, and all values are bools.

Here’s the passed table for reference:


InputModule.Cmds = {
	upMove = false;
	downMove = false;

	leftMove = false;
	rightMove = false;

	lastUp = false;
	lastLeft = false;

	jump = false;

	sprint = false;

	leftClick=false;
	rightClick=false;
	rightLastClick=false;

	dodge = false;


	QAbil=false;
	EAbil=false;
	Interact=false;

}

Thanks, I think this is what I was looking for, but I need to read your script a few times to understand it and be sure.

1 Like

Actually do me a favor and replace the __index with t instead of the function, as I believe it is more performant internally.

2 Likes

proxyMetatable.__index = t[index] :: V

like so?

1 Like
proxyMetatable.__index = t
2 Likes

Okay, this script is doing what its supposed to technically, I think what I asked for is just not possible in the engine.
Snippets from MovementScript (local script):

--Require InputModule
local IM = require(game:GetService("ReplicatedStorage"):WaitForChild("InputModule"))

--function proxy() is here


local Cmd = proxy(IM.Cmds, function(index: string, newValue: boolean, oldValue: boolean): ()
	print(`{index}: {oldValue} -> {newValue}`)
end)

Doing this^ detects changes made to Cmd locally, but not changes made to IM.Cmds from inside the module script(the behavior I’m looking for).

Last last question:
Could I wrap my table in some sort of Instance, and use one of the variations of .Changed() that Instance provides to listen to changes instead?
Thanks again for the help x)

1 Like

Again, you must be referencing the proxy when you want to detect changes, otherwise it wont trigger the proxy metatable. You can circumvent it by turning the Cmds itself directly into a proxy and hook a listener via a function (or use a BindableEvent/custom Signal class in reference to your Changed suggestion and fire it inside your proxy callback function).

I tried doing this (moving the proxy() function into InputModule, declaring Cmds as a proxy, and referencing the proxy in localscript), but the metatable is being fired inside InputModule and not the local script. I may as well just fire some Signals inside the CAS functions that set the booleans like you suggested x) . Unfortunately Roblox Studio went down a few minutes ago, but I’ll keep poking around later. Cheers!

Edit: One cool feature of this is that I can detect changes that were made to IM.Cmds inside the local script from the module script :smiley: Not exactly what I wanted, but a cool use case regardless

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.