Custom event module


  • With the changed to coroutine.yield it is now possible to create Roblox like events.

    This is a WIP module aimed at proving value holders (similar to IntValue, CFrameValue) and custom events (similar to bindable events).

    The updated module can be found here.

    API

    	Module API
    	----------------------------------------------------------
    	
    	Returns a new ValueEventObject and caches it. Errors if duplicate name is used.
    	ValueEventObject NewValueEvent(string name, <string, number, boolean, Roblox data types> item, [boolean chkType])
    	
    	Returns the ValueEventObject for the given name or nil if not found.
    	ValueEventObject GetValueEvent(string name)
    	
    	Returns a new NewEvent and caches it. Errors if duplicate name is used. 
    	EventObject NewEvent(string name, [array argCheck])
    	
    	Returns the EventObject for the given name or nil if not found.
    	EventObject GetEvent(string name)
    		
    	ValueEventObject API
    	------------------------------------------------------
    	
    	Returns the current value.
    	Variant GetValue()
    	
    	Sets the current value, if chkType was set to true type checking is included and will error on incorrect type.
    	Run order for events are Changed, ChangedWait, Connect, Wait
    	Void SetValue([string, number, boolean, Roblox data types] newValue)
    	
    	Adds the given function onto the call list when the function SetValue() is used.
    	ConnectionObject Connect(function func)
    	
    	Adds the given function onto the call list when the function SetValue() is used and the value is not equal to the current value stored.
    	ConnectionObject ConnectChanged(function func)
    	
    	Yields the thread until the function SetValue() is used and returns the current value. (order defined in SetValue desc)
    	Variant Wait()
    	
    	Yields the thread until the function SetValue() and the value is not equal to the current value stored.
    	Variant ChangedWait()
    	
    	Locks the internal state, disconnects all ConnectionObject, resumes ChangedWait then Wait by passing the current value stored if resumeWait is true.
    	Any future function calls will cause an error excluding Destroy().
    	Void Destroy([boolean resumeWait])
    	
    	EventObject API
    	------------------------------------------------------
    	
    	Runs each function connected to this EventObject. Type checking arguments if set.
    	Void Fire(...)
    	
    	Adds the given function onto the call list when the function Fire() is used.
    	ConnectionObject Connect(function func)
    	
    	Yields the thread until the function Fire() is called passing back any arguments.
    	Variant Wait()
    	
    	Locks the internal state, disconnects all ConnectionObject and resumes waiting theads with no arguments if resumeWait is set to true.
    	Destroy([boolean resumeWait])
    		
    	ConnectionObject API
    	--------------------------------------------------
    	
    	Returns true if the connection is active in the call list.
    	boolean IsConnected()
    	
    	Removes this connection from the call list.
    	Voif Disconnect()
    

    Module code

    
    local mtLock = {__metatable = function() return "Metatable locked." end}
    local f = setmetatable({}, mtLock)
    local valueObj, eventObj = {}, {}
    
    local function isArray(tbl)
    	local i = 0
    	for _,_ in pairs(tbl) do
    		i = i + 1
    		if tbl[i] == nil then return false end
    	end
    	return true
    end
    
    local function mkConnectionObj(func, lst)
    	local conObj = setmetatable({}, mtLock)
    	local obj = {func, true}
    	
    	function conObj:IsConnected()
    		return obj[2]
    	end
    	
    	function conObj:Disconnect()
    		if obj[2] then
    			for i=1, #lst do
    				if lst[i][1] == func then
    					table.remove(lst, i)
    					break
    				end
    			end
    		end
    		
    		obj[2] = false
    	end
    	
    	lst[#lst+1] = obj
    	return conObj
    end
    
    local function isValidArgs(validArgs, args)
    	for i=1, #validArgs do
    		if typeof(args[i]) ~= validArgs[i] then
    			error("Expected arg #" .. tostring(i) .. " " .. validArgs[i] .. " but got " .. typeof(args[i]))
    		end
    	end
    end
    
    local function newEvent(name, argTable)
    	
    	local callList, waitList = {}, {}
    	local obj = setmetatable({}, mtLock)
    	local disable = false
    	
    	if argTable == nil then
    		function obj:Fire(...)
    			if disable then return error("Cannot use Fire after Destroy function call.") end
    			for i=1, #callList do callList[i][1](...) end
    			for i=1, #waitList do coroutine.resume(waitList[i], ...) end
    		end
    	else
    		function obj:Fire(...)
    			if disable then return error("Cannot use Fire after Destroy function call.") end
    			isValidArgs(argTable, {...})
    			
    			for i=1, #callList do callList[i][1](...) end
    			for i=1, #waitList do coroutine.resume(waitList[i], ...) end
    		end
    	end
    	
    	function obj:Connect(func)
    		if disable then return error("Cannot use Connect after Destroy function call.") end
    		if typeof(func) ~= "function" then error("Expected function but got " .. typeof(func)) end
    		
    		return mkConnectionObj(func,callList) 
    	end
    	
    	function obj:Wait()
    		if disable then return error("Cannot use Wait after Destroy function call.") end
    		local thread = coroutine.running()
    		local index = #waitList+1
    		waitList[index] = thread
    		
    		local res = {coroutine.yield()}
    		waitList[index] = nil
    		
    		return unpack(res)
    	end
    	
    	function obj:Destroy(resumeWait)
    		if disable then return end
    		disable = true
    		eventObj[name] = nil
    		
    		for i=1, #callList do callList[i][2] = false callList[i] = nil end
    		if resumeWait then
    			for i=1, #waitList do coroutine.resume(waitList[i]) end
    		end
    	end
    	
    	return obj
    end
    
    local function newValueObject(name, item, validate)
    	
    	local callList, changedCallList, waitList, changedWaitList = {}, {}, {}, {}
    	local obj = setmetatable({}, mtLock)
    	local val = item
    	local valType = typeof(val)
    	local disable = false
    	
    	function obj:GetValue()
    		if disable then error("Cannot use GetValue after Destroy function call.") end
    		return val
    	end
    	
    	if validate then
    		function obj:SetValue(newVal)
    			if disable then error("Cannot use SetValue after Destroy function call.") end
    			if typeof(newVal) ~= valType then error("Expected type " .. valType " but got " .. newVal) end
    			
    			if newVal ~= val then
    				for i=1, #changedCallList do changedCallList[i][1](newVal) end
    				for i=1, #changedWaitList do coroutine.resume(changedWaitList[i], newVal) end
    			end
    			
    			val = newVal
    			for i=1, #callList do callList[i][1](newVal) end
    			for i=1, #waitList do coroutine.resume(waitList[i], newVal) end
    		end
    	else
    		function obj:SetValue(newVal)
    			if disable then error("Cannot use SetValue after Destroy function call.") end
    				
    			if newVal ~= val then
    				for i=1, #changedCallList do changedCallList[i][1](newVal) end
    				for i=1, #changedWaitList do coroutine.resume(changedWaitList[i], newVal) end
    			end
    			
    			val = newVal
    			for i=1, #callList do callList[i][1](newVal) end
    			for i=1, #waitList do coroutine.resume(waitList[i], newVal) end
    		end
    	end
    	
    	function obj:Connect(func)
    		if disable then error("Cannot use Connect after Destroy function call.") end
    		if typeof(func) ~= "function" then error("Expected function but got " .. typeof(func)) end
    			
    		return mkConnectionObj(func, callList)
    	end
    		
    	function obj:ConnectChanged(func)
    		if disable then error("Cannot use ConnectChanged after Destroy function call.") end
    		if typeof(func) ~= "function" then error("Expected function but got " .. typeof(func)) end
    			
    		return mkConnectionObj(func, changedCallList)
    	end
    		
    	function obj:Wait()
    		if disable then error("Cannot use Wait after Destroy function call.") end
    		
    		local thread = coroutine.running()
    		local index, newVal = #waitList+1
    		waitList[index] = thread
    		
    		newVal = coroutine.yield()
    		waitList[index] = nil
    		
    		if disable then return val end
    		
    		return newVal
    	end
    		
    	function obj:ChangedWait()
    		if disable then error("Cannot use ChangedWait after Destroy function call.") end
    			
    		local thread = coroutine.running()
    		local index, newVal = #changedWaitList+1
    		changedWaitList[index] = thread
    		
    		newVal = coroutine.yield()
    		changedWaitList[index] = nil
    		
    		if disable then return val end
    		
    		return newVal
    	end
    	
    	function obj:Destroy(resumeWait)
    		if disable then return end
    		
    		disable = true
    		valueObj[name] = nil
    		
    		for i=1, #callList do callList[i][2] = false callList[i] = nil end
    		for i=1, #changedCallList do changedCallList[i][2] = false changedCallList[i] = nil end
    		
    		if resumeWait then
    			for i=1, #changedWaitList do coroutine.resume(changedWaitList[i], val) end
    			for i=1, #waitList do coroutine.resume(waitList[i], val) end
    		end
    	end
    	
    	return obj
    end
    
    function f:NewValueEvent(name, item, chkType)
    	
    	if typeof(name) ~= "string" then error("Name must be a string.") end
    	if typeof(item) == "table" 
    		or typeof(item) == "userdata" 
    		or typeof(item) == "function"
    		or typeof(item) == "thread"
    		or item == nil then error("Cannot use item type nil, table, userdata, thread or function.") end
    	if valueObj[name] then error(name .. " ValueEvent exists.") end
    	
    	local tmp = newValueObject(name, item, chkType and true or false)
    	valueObj[name] = tmp
    	return tmp
    end
    
    function f:GetValueEvent(name)
    	return valueObj[name]
    end
    
    function f:NewEvent(name, argCheck)
    	
    	if typeof(name) ~= "string" then error("Name must be a string.") end
    	if argCheck ~= nil then
    		if typeof(argCheck) ~= "table" or not isArray(argCheck) then error("Argument check list must be a array or nil.") end
    	end
    	if eventObj[name] then error(name .. " Event exists.") end
    	
    	local tmp = newEvent(name, argCheck)
    	eventObj[name] = tmp
    	return tmp
    end
    
    function f:GetEvent(name)
    	return eventObj[name]
    end
    
    return f
    

    Test script

    local customEvent = require(script.Parent)
    
    local test1 = true
    local test2 = true
    local test3 = true
    
    if test1 then
    	print("Value object test 1")
    	local intValue = customEvent:NewValueEvent("event1", 0, false)
    	
    	-- check set and get
    	print(intValue:GetValue())
    	intValue:SetValue(1)
    	print(intValue:GetValue())
    	
    	-- add function for event 
    	local con = intValue:Connect(function(newValue) print("function 1" , newValue) end)
    	local con2 = intValue:Connect(function(newValue) print("function 2" , newValue) end)
    		
    	--  set with events
    	intValue:SetValue(2)
    	
    	-- check connection state
    	print(con:IsConnected())
    	
    	-- disconnect event *2 
    	con:Disconnect()
    	con:Disconnect()
    	
    	-- check connection state
    	print(con:IsConnected())
    	
    	-- test set values
    	intValue:SetValue(3)
    	intValue:SetValue(4)
    	
    	-- test Wait 
    	delay(5, function() intValue:SetValue(5) end)
    	
    	print("Waiting")
    	print(intValue:Wait())
    	intValue:Destroy()
    	print(con2:IsConnected())
    end
    
    if test2 then
    -- test ConnectChanged
    	print()
    	print("Value object test 2")
    	
    	local intValue = customEvent:NewValueEvent("event1", 0, false)
    	
    	local con = intValue:ConnectChanged(function(newValue) print("function 1 new value", newValue) end)
    	intValue:Connect(function(newValue) print("function 1" , newValue) end)
    	local con2 = intValue:ConnectChanged(function(newValue) print("function 2 new value", newValue) end)
    		
    	intValue:SetValue(2)
    	intValue:SetValue(2)
    	
    	-- check connection state
    	print(con:IsConnected())
    	
    	-- disconnect event *2 
    	con:Disconnect()
    	con:Disconnect()
    	
    	print(con:IsConnected())
    	
    	-- test Wait 
    	delay(3, function() intValue:SetValue(2) end)
    	delay(5, function() intValue:SetValue(3) end)
    	
    	print("Waiting")
    	print(intValue:ChangedWait())
    	intValue:Destroy()
    	print(con2:IsConnected())
    	wait(5)
    end
    
    if test3 then
    	print()
    	print("Event test start")
    	
    	local event = customEvent:NewEvent("event1", {"string", "number"})
    	print("Event test start2")
    	local con1 = event:Connect(function(str, val) print("function 1", str, val) end)
    	local con2 = event:Connect(function(str, val) print("function 2", str, val) end)
    	
    	event:Fire("a", 1)
    	
    	print(con2:IsConnected())
    	con2:Disconnect()
    	
    	delay(3, function() event:Fire("b", 2) end)
    	
    	print("waiting")
    	print(event:Wait())
    end
    

    Output example
    alt text

    Feedback on this module is appreciated.


  • @kingdom5 This post is deleted!

    LOL JK

    Yeah this thing seems cool


  • I have a request, could you implement the example from the DevForum post of waiting for several events?

    I think a way to describe a series or pattern of events occuring within certain amounts of time of eachother would be super powerful. For example, an event for double key taps.


  • @kools https://devforum.roblox.com/t/custom-event-module/222220

    I am not sure how you would do that in an event since events do not store any previous state.

    I can take a look at creating such an event that would require you to fire it x amount of times before it will run. Thiniking about this it would need to store the time and keep yielding the the thread until it meets the limits set.

Log in to reply
 

Looks like your connection to Scripting Helpers was lost, please wait while we try to reconnect.