mirror of
				https://github.com/Reuh/ubiquitousse.git
				synced 2025-10-27 17:19:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			334 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			334 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| --[[-- Simple signal / observer pattern implementation for Lua.
 | |
| 
 | |
| No dependency.
 | |
| Optional dependency: LÖVE to hook into LÖVE events.
 | |
| 
 | |
| The returned module also acts as a global `SignalRegistry`, so you can call the `:bind`, `:emit`, etc. methods directly on the module
 | |
| if you don't need to isolate your signals in separate registries.
 | |
| 
 | |
| @module signal
 | |
| @usage
 | |
| local signal = require("ubiquitousse.signal")
 | |
| 
 | |
| -- Bind a function to a "hit" signal
 | |
| signal:bind("hit", function(enemy)
 | |
| 	print(enemy.." was hit!")
 | |
| end)
 | |
| 
 | |
| -- Somewhere else in your code: will call every function bound to "hit" signal with "invader" argument
 | |
| signal:emit("hit", "invader")
 | |
| 
 | |
| -- We also provides a predefined SignalRegistry (signal.event) which emit signals on LÖVE callbacks
 | |
| -- You can initialize it with:
 | |
| signal.registerEvents()
 | |
| 
 | |
| signal.event:bind("update", function(dt) print("called every update") end)
 | |
| signal.event:bind("keypressed", function(key, scancode) print("pressed key "..key) end)
 | |
| -- etc., for every LÖVE callback
 | |
| --]]
 | |
| 
 | |
| let signal
 | |
| 
 | |
| --- Signal registry.
 | |
| --
 | |
| -- A SignalRegistry is a separate ubiquitousse.signal instance: its signals will be independant from other registries.
 | |
| -- @type SignalRegistry
 | |
| let registry_mt = {
 | |
| 	--- Map of signals to list of listeners.
 | |
| 	-- @ftype {["name"]={fn,[fn]=1,...}}
 | |
| 	signals = {},
 | |
| 
 | |
| 	--- List of registries chained to this registry.
 | |
| 	-- @ftype { registry, ... }
 | |
| 	chained = {},
 | |
| 
 | |
| 	--- Bind a function to a signal name.
 | |
| 	-- @tparam string name the name of the signal
 | |
| 	-- @tparam function fn the function to bind to the signal
 | |
| 	bind = :(name, fn)
 | |
| 		assert(not @has(name, fn), ("function %s already bound to signal %s"):format(fn, name))
 | |
| 		if not @signals[name] then
 | |
| 			@signals[name] = {}
 | |
| 		end
 | |
| 		table.insert(@signals[name], fn)
 | |
| 		return @
 | |
| 	end,
 | |
| 
 | |
| 	--- Returns true if fn is bound to the signal.
 | |
| 	-- @tparam string name the name of the signal
 | |
| 	-- @tparam function fn the function
 | |
| 	has = :(name, fn)
 | |
| 		if not @signals[name] then
 | |
| 			return false
 | |
| 		end
 | |
| 		for _, f in ipairs(@signals[name]) do
 | |
| 			if f == fn then
 | |
| 				return true
 | |
| 			end
 | |
| 		end
 | |
| 		return false
 | |
| 	end,
 | |
| 
 | |
| 	--- Unbind a function from a signal name.
 | |
| 	-- @tparam string name the name of the signal
 | |
| 	-- @tparam function fn the function to unbind to the signal
 | |
| 	unbind = :(name, fn)
 | |
| 		if not @signals[name] then
 | |
| 			@signals[name] = {}
 | |
| 		end
 | |
| 		for i=#@signals[name], 1, -1 do
 | |
| 			local f = @signals[name][i]
 | |
| 			if f == fn then
 | |
| 				table.remove(@signals[name], i)
 | |
| 				return @
 | |
| 			end
 | |
| 		end
 | |
| 		error(("function %s not bound to signal %s"):format(fn, name))
 | |
| 	end,
 | |
| 	--- Unbind a function from every signal whose name match the pattern.
 | |
| 	-- @tparam string pat Lua pattern string
 | |
| 	-- @tparam function fn the function to unbind to the signals
 | |
| 	unbindPattern = :(pat, fn)
 | |
| 		return @_patternize("unbind", pat, fn)
 | |
| 	end,
 | |
| 
 | |
| 	--- Remove every bound function to a signal name.
 | |
| 	-- @tparam string name the name of the signal
 | |
| 	clear = :(name)
 | |
| 		@signals[name] = nil
 | |
| 	end,
 | |
| 	--- Remove every bound function to every signal whose name match the pattern.
 | |
| 	-- @tparam string pat Lua string pattern
 | |
| 	clearPattern = :(pat)
 | |
| 		return @_patternize("clear", pat)
 | |
| 	end,
 | |
| 
 | |
| 	--- Emit a signal, i.e. call every function bound to it, with the given arguments.
 | |
| 	-- @tparam string name the name of the signal
 | |
| 	-- @param ... arguments to pass to the functions bound to this signal
 | |
| 	emit = :(name, ...)
 | |
| 		if @signals[name] then
 | |
| 			for _, fn in ipairs(@signals[name]) do
 | |
| 				fn(...)
 | |
| 			end
 | |
| 		end
 | |
| 		for _, c in ipairs(@chained) do
 | |
| 			c:emit(name, ...)
 | |
| 		end
 | |
| 		return @
 | |
| 	end,
 | |
| 	--- Emit to every signal whose name match the pattern.
 | |
| 	-- @tparam string pat Lua pattern string
 | |
| 	-- @param ... arguments to pass to the functions bound to each signal
 | |
| 	emitPattern = :(pat, ...)
 | |
| 		return @_patternize("emit", pat, ...)
 | |
| 	end,
 | |
| 
 | |
| 	--- Chain another regsitry to this registry.
 | |
| 	-- I.e., after an event is emitted in this registry it will be automatically emitted in the other registry.
 | |
| 	-- Several registries can be chained to a single registry.
 | |
| 	-- @tparam SignalRegistry registry
 | |
| 	chain = :(registry)
 | |
| 		if not registry then
 | |
| 			registry = signal.new()
 | |
| 		end
 | |
| 		table.insert(@chained, registry)
 | |
| 		return registry
 | |
| 	end,
 | |
| 	--- Unchain a specific registry from the registry chaining list.
 | |
| 	-- Will error if the regsitry is not in the chaining list.
 | |
| 	-- @tparam SignalRegistry registry
 | |
| 	unchain = :(registry)
 | |
| 		for i=#@chained, 1, -1 do
 | |
| 			if @chained[i] == registry then
 | |
| 				table.remove(@chained, i)
 | |
| 				return @
 | |
| 			end
 | |
| 		end
 | |
| 		error("the givent registry is not chained with this registry")
 | |
| 	end,
 | |
| 
 | |
| 	_patternize = :(method, pat, ...)
 | |
| 		for name in pairs(@signals) do
 | |
| 			if name:match(pat) then
 | |
| 				@[method](@, name, ...)
 | |
| 			end
 | |
| 		end
 | |
| 	end
 | |
| }
 | |
| registry_mt.__index = registry_mt
 | |
| 
 | |
| --- Signal group.
 | |
| --
 | |
| -- A SignalGroup is a list of (registry, signal name, function) triplets.
 | |
| -- When the group is active, all of these triplets will bind the specified signal name to the specified function in the specified registry.
 | |
| -- When the group is paused, all of these triplets are unbound.
 | |
| --
 | |
| -- This can be used to maintain a list of signal bindings where every one should be either disabled or enabled at the same time.
 | |
| -- For example you may maintain a signal group of signals you want to be emitted when your game is running, and disabled when the game is paused
 | |
| -- (like inputs, update, simulation step, etc. signals).
 | |
| --
 | |
| -- @type SignalGroup
 | |
| let group_mt = {
 | |
| 	--- Indicate if the signal group if currently paused or not.
 | |
| 	-- @ftype boolean
 | |
| 	paused = false,
 | |
| 
 | |
| 	--- List of triplets in the group.
 | |
| 	-- @ftype { {registry, "signal name", function}, ... }
 | |
| 	binds = {},
 | |
| 
 | |
| 	--- Bind a function to a signal name in the given registry.
 | |
| 	-- This handles binding the function on its own; you do not need to call `SignalRegistry:bind` manually.
 | |
| 	-- If the group is paused, this will not bind the function immediately but only on the next time this group is resumed (as expected).
 | |
| 	-- @tparam SignalRegistry registry to bind the signal in
 | |
| 	-- @tparam string name the name of the signal
 | |
| 	-- @tparam function fn the function to bind to the signal
 | |
| 	bind = :(registry, name, fn)
 | |
| 		table.insert(@binds, { registry, name, fn })
 | |
| 		if not @paused then registry:bind(name, fn) end
 | |
| 	end,
 | |
| 
 | |
| 	--- Remove every bound triplet in the group.
 | |
| 	clear = :()
 | |
| 		if not @paused then
 | |
| 			for _, b in ipairs(@binds) do
 | |
| 				b[1]:unbind(b[2], b[3])
 | |
| 			end
 | |
| 		end
 | |
| 		@binds = {}
 | |
| 	end,
 | |
| 
 | |
| 	--- Pause the group.
 | |
| 	-- The signals bound to this group will be disabled in their given registries.
 | |
| 	pause = :()
 | |
| 		assert(not @paused, "event group is already paused")
 | |
| 		@paused = true
 | |
| 		for _, b in ipairs(@binds) do
 | |
| 			b[1]:unbind(b[2], b[3])
 | |
| 		end
 | |
| 	end,
 | |
| 
 | |
| 	--- Resume the group.
 | |
| 	-- The signals bound to this group will be enabled in their given registries.
 | |
| 	resume = :()
 | |
| 		assert(@paused, "event group is not paused")
 | |
| 		@paused = false
 | |
| 		for _, b in ipairs(@binds) do
 | |
| 			b[1]:bind(b[2], b[3])
 | |
| 		end
 | |
| 	end
 | |
| }
 | |
| group_mt.__index = group_mt
 | |
| 
 | |
| --- Module.
 | |
| --
 | |
| -- @section module
 | |
| 
 | |
| signal = {
 | |
| 	--- Creates and return a new SignalRegistry.
 | |
| 	-- @treturn SignalRegistry
 | |
| 	new = ()
 | |
| 		return setmetatable({ signals = {}, chained = {} }, registry_mt)
 | |
| 	end,
 | |
| 
 | |
| 	--- Creates and return a new SignalGroup.
 | |
| 	-- @treturn SignalGroup
 | |
| 	group = ()
 | |
| 		return setmetatable({ binds = {} }, group_mt)
 | |
| 	end,
 | |
| 
 | |
| 	-- Global SignalRegistry.
 | |
| 	signals = {},
 | |
| 	bind = (...)
 | |
| 		return registry_mt.bind(signal, ...)
 | |
| 	end,
 | |
| 	has = (...)
 | |
| 		return registry_mt.has(signal, ...)
 | |
| 	end,
 | |
| 	unbind = (...)
 | |
| 		return registry_mt.unbind(signal, ...)
 | |
| 	end,
 | |
| 	unbindPattern = (...)
 | |
| 		return registry_mt.unbindPattern(signal, ...)
 | |
| 	end,
 | |
| 	clear = (...)
 | |
| 		return registry_mt.clear(signal, ...)
 | |
| 	end,
 | |
| 	clearPattern = (...)
 | |
| 		return registry_mt.clearPattern(signal, ...)
 | |
| 	end,
 | |
| 	emit = (...)
 | |
| 		return registry_mt.emit(signal, ...)
 | |
| 	end,
 | |
| 	emitPattern = (...)
 | |
| 		return registry_mt.emitPattern(signal, ...)
 | |
| 	end,
 | |
| 
 | |
| 	--- `SignalRegistry` which will be used to bind signals that need to be called on LÖVE events; other ubiquitousse modules may bind to this registry
 | |
| 	-- if avaible.
 | |
| 	--
 | |
| 	-- For example, every ubiquitousse module with a "update" function will bind it to the "update" signal in the registry;
 | |
| 	-- you can then call this signal on each game update to update every ubiquitousse module easily.
 | |
| 	--
 | |
| 	-- You will need to call `registerEvents` for the signal to be called on LÖVE callbacks automatically (otherwise you will have to emit the events
 | |
| 	-- from the LÖVE callbacks manually).
 | |
| 	--
 | |
| 	-- List of signals available: "displayrotated", "draw", "load", "lowmemory", "quit", "update",
 | |
| 	-- "directorydropped", "filedropped", "focus", "mousefocus", "resize", "visible",
 | |
| 	-- "keypressed", "keyreleased", "textedited", "textinput",
 | |
| 	-- "mousemoved", "mousepressed", "mousereleased", "wheelmoved",
 | |
| 	-- "gamepadaxis", "gamepadpressed", "gamepadreleased",
 | |
| 	-- "joystickadded", "joystickaxis", "joystickhat", "joystickpressed", "joystickreleased", "joystickremoved",
 | |
| 	-- "touchmoved", "touchpressed", "touchreleased".
 | |
| 	--
 | |
| 	-- @ftype SignalRegistry
 | |
| 	event = nil,
 | |
| 
 | |
| 	--- Call this function to hook `signal.event` signals to LÖVE events.
 | |
| 	-- This means overriding every existing LÖVE callback. If a callback is already defined, the new one will call the old function along with the signal:emit.
 | |
| 	-- @require love
 | |
| 	registerEvents = ()
 | |
| 		local callbacks = { -- everything except run, errorhandler, threaderror
 | |
| 			"displayrotated", "draw", "load", "lowmemory", "quit", "update",
 | |
| 			"directorydropped", "filedropped", "focus", "mousefocus", "resize", "visible",
 | |
| 			"keypressed", "keyreleased", "textedited", "textinput",
 | |
| 			"mousemoved", "mousepressed", "mousereleased", "wheelmoved",
 | |
| 			"gamepadaxis", "gamepadpressed", "gamepadreleased",
 | |
| 			"joystickadded", "joystickaxis", "joystickhat", "joystickpressed", "joystickreleased", "joystickremoved",
 | |
| 			"touchmoved", "touchpressed", "touchreleased"
 | |
| 		}
 | |
| 		local event = signal.event
 | |
| 		for _, callback in ipairs(callbacks) do
 | |
| 			if callback == "update" then
 | |
| 				if love[callback] then
 | |
| 					local old = love[callback]
 | |
| 					love[callback] = function(dt)
 | |
| 						old(dt)
 | |
| 						event:emit(callback, dt)
 | |
| 					end
 | |
| 				else
 | |
| 					love[callback] = function(dt)
 | |
| 						event:emit(callback, dt)
 | |
| 					end
 | |
| 				end
 | |
| 			else
 | |
| 				if love[callback] then
 | |
| 					local old = love[callback]
 | |
| 					love[callback] = function(...)
 | |
| 						old(...)
 | |
| 						event:emit(callback, ...)
 | |
| 					end
 | |
| 				else
 | |
| 					love[callback] = function(...)
 | |
| 						event:emit(callback, ...)
 | |
| 					end
 | |
| 				end
 | |
| 			end
 | |
| 		end
 | |
| 	end
 | |
| }
 | |
| 
 | |
| signal.event = signal.new()
 | |
| 
 | |
| return signal
 |