286 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Lua
		
	
			
		
		
	
	
			286 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Lua
		
	
-- EventLib - An event library in pure lua (uses standard coroutine library)
 | 
						|
-- License: WTFPL
 | 
						|
-- Author: Elijah Frederickson
 | 
						|
-- Version: 1.0
 | 
						|
-- Copyright (C) 2012 LoDC
 | 
						|
-- 
 | 
						|
-- Description:
 | 
						|
-- ROBLOX has an event library in its RbxUtility library, but it isn't pure Lua. 
 | 
						|
-- It originally used a BoolValue but now uses BindableEvent. I wanted to write
 | 
						|
-- it in pure Lua, so here it is. It also contains some new features.
 | 
						|
-- 
 | 
						|
-- 
 | 
						|
-- API:
 | 
						|
-- 
 | 
						|
-- EventLib
 | 
						|
--   new([name])
 | 
						|
--     aliases: CreateEvent
 | 
						|
--     returns: the event, with a metatable __index for the EventLib table
 | 
						|
--   Connect(event, func)
 | 
						|
--     aliases: connect
 | 
						|
--     returns: a Connection
 | 
						|
--   Disconnect(event, [func])
 | 
						|
--     aliases: disconnect
 | 
						|
--     returns: the index of [func]
 | 
						|
--     notes: if [func] is nil, it removes all connections
 | 
						|
--   DisconnectAll(event)
 | 
						|
--     notes: calls Disconnect()
 | 
						|
--   Fire(event, ... <args>)
 | 
						|
--     aliases: Simulate, fire
 | 
						|
--     notes: resumes all :wait() first
 | 
						|
--   Wait(event)
 | 
						|
--     aliases: wait
 | 
						|
--     returns: the Fire() arguments
 | 
						|
--     notes: blocks the thread until Fire() is called
 | 
						|
--   ConnectionCount(event)
 | 
						|
--     returns: the number of current connections
 | 
						|
--   Spawn(func)
 | 
						|
--     aliases: spawn
 | 
						|
--     returns: the result of func
 | 
						|
--     notes: runs func in a separate coroutine/thread
 | 
						|
--   Destroy(event)
 | 
						|
--     aliases: destroy, Remove, remove
 | 
						|
--     notes: renders the event completely useless
 | 
						|
--   WaitForCompletion(event)
 | 
						|
--     notes: blocks current thread until the current event Fire() is done
 | 
						|
--       If a connected function calls WaitForCompletion, it will hang forever
 | 
						|
--   IsWaiting(event)
 | 
						|
--     returns: if the event has any waiters
 | 
						|
--   WaitForWaiters(event)
 | 
						|
--     notes: waits for all waiters to finish. Called in Fire() to make sure that all
 | 
						|
--       the waiting threads are done before settings self.args to nil
 | 
						|
--   
 | 
						|
-- Event
 | 
						|
--   [All EventLib functions]
 | 
						|
--   EventName
 | 
						|
--     Property, defaults to "<Unknown Event>"
 | 
						|
--   <Private fields>
 | 
						|
--     handlers, waiter, args, waiters, executing, 
 | 
						|
-- 
 | 
						|
-- Connection
 | 
						|
--   Disconnect
 | 
						|
--     aliases: disconnect
 | 
						|
--     returns: the result of [Event].Disconnect
 | 
						|
-- 
 | 
						|
-- Basic usage (there are some tests on the bottom):
 | 
						|
--  local EventLib = require'EventLib' 
 | 
						|
-- For ROBLOX use: repeat wait() until _G.EventLib local EventLib = _G.EventLib
 | 
						|
--  
 | 
						|
--  local event = EventLib:new()
 | 
						|
--  local con = event:Connect(function(...) print(...) end)
 | 
						|
--  event:Fire("test") --> 'test' is print'ed
 | 
						|
--  con:disconnect()
 | 
						|
--  event:Fire("test") --> nothing happens: no connections
 | 
						|
-- 
 | 
						|
-- Supported versions/implementations of Lua:
 | 
						|
-- Lua 5.1, 5.2
 | 
						|
-- SharpLua 2
 | 
						|
-- MetaLua
 | 
						|
-- RbxLua (automatically registers if it detects ROBLOX)
 | 
						|
 | 
						|
--[[
 | 
						|
Issues:
 | 
						|
- None, but see [Todo 1]
 | 
						|
 | 
						|
Todo:
 | 
						|
- fix Wait() for non-roblox clients without a wait function...
 | 
						|
 | 
						|
Changelog:
 | 
						|
 | 
						|
v1.0
 | 
						|
- Initial version
 | 
						|
 | 
						|
]]
 | 
						|
 | 
						|
local _M = { }
 | 
						|
_M._VERSION = "1.0"
 | 
						|
_M._M = _M
 | 
						|
_M._AUTHOR = "Elijah Frederickson"
 | 
						|
_M._COPYRIGHT = "Copyright (C) 2012 LoDC"
 | 
						|
 | 
						|
local function spawn(f)
 | 
						|
    return coroutine.resume(coroutine.create(function()
 | 
						|
        f()
 | 
						|
    end))
 | 
						|
end
 | 
						|
_M.Spawn = spawn
 | 
						|
_M.spawn = spawn
 | 
						|
 | 
						|
function _M:new(name)
 | 
						|
    assert(self ~= nil and type(self) == "table" and self == _M, "Invalid EventLib table (make sure you're using ':' not '.')")
 | 
						|
    local s = { }
 | 
						|
    s.handlers = { }
 | 
						|
    s.waiter = false
 | 
						|
    s.args = nil
 | 
						|
    s.waiters = 0
 | 
						|
    s.EventName = name or "<Unknown Event>"
 | 
						|
    s.executing = false
 | 
						|
    return setmetatable(s, { __index = self })
 | 
						|
end
 | 
						|
_M.CreateEvent = _M.new
 | 
						|
 | 
						|
function _M:Connect(handler)
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    assert(type(handler) == "function", "Invalid handler. Expected function got " .. type(handler))
 | 
						|
    assert(self.handlers, "Invalid Event")
 | 
						|
    table.insert(self.handlers, handler)
 | 
						|
    local t = { }
 | 
						|
    t.Disconnect = function()
 | 
						|
        return self:Disconnect(handler)
 | 
						|
    end
 | 
						|
    t.disconnect = t.Disconnect
 | 
						|
    return t
 | 
						|
end
 | 
						|
_M.connect = _M.Connect
 | 
						|
 | 
						|
function _M:Disconnect(handler)
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    assert(type(handler) == "function" or type(handler) == "nil", "Invalid handler. Expected function or nil, got " .. type(handler))
 | 
						|
    if not handler then
 | 
						|
        self.handlers = { }
 | 
						|
    else
 | 
						|
        for k, v in pairs(self.handlers) do
 | 
						|
            if v == handler then
 | 
						|
                self.handlers[k] = nil
 | 
						|
                return k
 | 
						|
            end
 | 
						|
        end
 | 
						|
    end
 | 
						|
end
 | 
						|
_M.disconnect = _M.Disconnect
 | 
						|
 | 
						|
function _M:DisconnectAll()
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    self:Disconnect()
 | 
						|
end
 | 
						|
 | 
						|
function _M:Fire(...)
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    self.args = { ... }
 | 
						|
    self.executing = true
 | 
						|
    --[[
 | 
						|
    if self.waiter then
 | 
						|
        self.waiter = false
 | 
						|
        for k, v in pairs(self.waiters) do
 | 
						|
            coroutine.resume(v)
 | 
						|
        end
 | 
						|
    end]]
 | 
						|
    self.waiter = false
 | 
						|
    local i = 0
 | 
						|
assert(self.handlers, "no handler table")
 | 
						|
    for k, v in pairs(self.handlers) do
 | 
						|
        i = i + 1
 | 
						|
        spawn(function() 
 | 
						|
            v(unpack(self.args)) 
 | 
						|
            i = i - 1
 | 
						|
            if i == 0 then self.executing = false end
 | 
						|
        end)
 | 
						|
    end
 | 
						|
    self:WaitForWaiters()
 | 
						|
    self.args = nil
 | 
						|
    --self.executing = false
 | 
						|
end
 | 
						|
_M.Simulate = _M.Fire
 | 
						|
_M.fire = _M.Fire
 | 
						|
 | 
						|
function _M:Wait()
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    self.waiter = true
 | 
						|
    self.waiters = self.waiters + 1
 | 
						|
    --[[
 | 
						|
    local c = coroutine.create(function()
 | 
						|
        coroutine.yield()
 | 
						|
        return unpack(self.args)
 | 
						|
    end)
 | 
						|
    
 | 
						|
    table.insert(self.waiters, c)
 | 
						|
    coroutine.resume(c)
 | 
						|
    ]]
 | 
						|
    
 | 
						|
    while self.waiter or not self.args do if wait then wait() end end
 | 
						|
    self.waiters = self.waiters - 1
 | 
						|
    return unpack(self.args)
 | 
						|
end
 | 
						|
_M.wait = _M.Wait
 | 
						|
 | 
						|
function _M:ConnectionCount()
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    return #self.handlers
 | 
						|
end
 | 
						|
 | 
						|
function _M:Destroy()
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    self:DisconnectAll()
 | 
						|
    for k, v in pairs(self) do
 | 
						|
        self[k] = nil
 | 
						|
    end
 | 
						|
    setmetatable(self, { })
 | 
						|
end
 | 
						|
_M.destroy = _M.Destroy
 | 
						|
_M.Remove = _M.Destroy
 | 
						|
_M.remove = _M.Destroy
 | 
						|
 | 
						|
function _M:WaitForCompletion()
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    while self.executing do if wait then wait() end end
 | 
						|
    while self.waiters > 0 do if wait then wait() end end
 | 
						|
end
 | 
						|
 | 
						|
function _M:IsWaiting()
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    return self.waiter or self.waiters > 0
 | 
						|
end
 | 
						|
 | 
						|
function _M:WaitForWaiters()
 | 
						|
    assert(self ~= nil and type(self) == "table", "Invalid Event (make sure you're using ':' not '.')")
 | 
						|
    while self.waiters > 0 do if wait then wait() end end
 | 
						|
end
 | 
						|
 | 
						|
-- Tests
 | 
						|
if false then
 | 
						|
    local e = _M:new("test")
 | 
						|
    local f = function(...) print("| Fired!", ...) end
 | 
						|
    local e2 = e:connect(f)
 | 
						|
    e:fire("arg1", 5, { })
 | 
						|
    -- Would work in a ROBLOX Script, but not on Lua 5.1...
 | 
						|
    if script ~= nil and failhorribly then
 | 
						|
        spawn(function() print("Wait() results", e:wait()) print"|- done waiting!" end)
 | 
						|
    end
 | 
						|
    e:fire(nil, "x")
 | 
						|
    print("Disconnected events index:", e:disconnect(f))
 | 
						|
    print("Couldn't disconnect an already disconnected handler?", e2:disconnect()==nil)
 | 
						|
    print("Connections:", e:ConnectionCount())
 | 
						|
    assert(e:ConnectionCount() == 0 and e:ConnectionCount() == #e.handlers)
 | 
						|
    e:connect(f)
 | 
						|
    e:connect(function() print"Throwing error... " error("...") end)
 | 
						|
    e:fire("Testing throwing an error...")
 | 
						|
    e:disconnect()
 | 
						|
    e:Simulate()
 | 
						|
    f("plain function call")
 | 
						|
    assert(e:ConnectionCount() == 0)
 | 
						|
    
 | 
						|
    if wait then
 | 
						|
        e:connect(function() wait(2, true) print'fired after waiting' end)
 | 
						|
        e:Fire()
 | 
						|
        e:WaitForCompletion()
 | 
						|
        print'Done!'
 | 
						|
    end
 | 
						|
    
 | 
						|
    local failhorribly = false
 | 
						|
    if failhorribly then -- causes an eternal loop in the WaitForCompletion call
 | 
						|
        e:connect(function() e:WaitForCompletion() print'done with connected function' end)
 | 
						|
        e:Fire()
 | 
						|
        print'done'
 | 
						|
    end
 | 
						|
    
 | 
						|
    e:Destroy()
 | 
						|
    assert(not e.EventName and not e.Fire and not e.Connect)
 | 
						|
end
 | 
						|
 | 
						|
if shared and Instance then -- ROBLOX support
 | 
						|
    shared.EventLib = _M 
 | 
						|
    _G.EventLib = _M
 | 
						|
end
 | 
						|
 | 
						|
return _M
 |