From f6fb8ad6499f00390505f16071aefc16a833377b Mon Sep 17 00:00:00 2001 From: Reuh Date: Fri, 27 Dec 2019 18:54:30 +0100 Subject: [PATCH] uqt.signal --- backend/ctrulua.lua | 38 ++++++++----- backend/love.lua | 33 ++++++++--- init.lua | 22 ++------ input/backend/ctrulua.lua | 8 +++ input/backend/love.lua | 50 +++++++++------- input/input.lua | 55 +++++++++++------- scene/scene.lua | 21 ++++++- signal/backend/love.lua | 43 ++++++++++++++ signal/init.lua | 14 +++++ signal/signal.can | 116 ++++++++++++++++++++++++++++++++++++++ timer/timer.lua | 11 +++- 11 files changed, 331 insertions(+), 80 deletions(-) create mode 100644 signal/backend/love.lua create mode 100644 signal/init.lua create mode 100644 signal/signal.can diff --git a/backend/ctrulua.lua b/backend/ctrulua.lua index 3ca8e6b..f24aaa5 100644 --- a/backend/ctrulua.lua +++ b/backend/ctrulua.lua @@ -2,18 +2,30 @@ local uqt = require((...):match("^(.-ubiquitousse)%.")) local ctr = require("ctr") local gfx = require("ctr.gfx") -local function checkCompat(stuffName, expectedVersion, actualVersion) - if actualVersion ~= expectedVersion then - local txt = ("Ubiquitousse ctrµLua backend was made for %s %s but %s is used!\nThings may not work as expected.") - :format(stuffName, expectedVersion, actualVersion) - print(txt) - for _=0,300 do - gfx.start(gfx.TOP) - gfx.wrappedText(0, 0, txt, gfx.TOP_WIDTH) - gfx.stop() - gfx.render() - end +local madeForCtr = "v1.0" +local madeForUqt = "0.0.1" + +-- Check versions +local txt = "" + +if ctr.version ~= madeForCtr then + txt = txt .. ("Ubiquitousse ctrµLua backend was made for ctrµLua %s but %s is used!\n") + :format(madeForCtr, uqt.version) +end + +if uqt.version ~= madeForUqt then + txt = txt .. ("Ubiquitousse ctrµLua backend was made for Ubiquitousse %s but %s is used!\n") + :format(madeForUqt, uqt.version) +end + +-- Show warnings +if txt ~= "" then + txt = txt .. "Things may not work as expected.\n" + print(txt) + for _=0,300 do + gfx.start(gfx.TOP) + gfx.wrappedText(0, 0, txt, gfx.TOP_WIDTH) + gfx.stop() + gfx.render() end end -checkCompat("ctrµLua", "v1.0", ctr.version) -- not really a version, just get the latest build -checkCompat("Ubiquitousse", "0.0.1", uqt.version) diff --git a/backend/love.lua b/backend/love.lua index 8754772..ef320cc 100644 --- a/backend/love.lua +++ b/backend/love.lua @@ -1,11 +1,30 @@ local uqt = require((...):match("^(.-ubiquitousse)%.")) -local function checkCompat(stuffName, expectedVersion, actualVersion) - if actualVersion ~= expectedVersion then - local txt = ("Ubiquitousse Löve backend was made for %s %s but %s is used!\nThings may not work as expected.") - :format(stuffName, expectedVersion, actualVersion) - print(txt) +local madeForLove = { 11, "x", "x" } +local madeForUqt = "0.0.1" + +-- Check versions +local txt = "" + +local actualLove = { love.getVersion() } +for i, v in ipairs(madeForLove) do + if v ~= "x" then + if actualLove[i] ~= v then + txt = txt .. ("Ubiquitousse Löve backend was made for LÖVE %s.%s.%s but %s.%s.%s is used!\n") + :format(madeForLove[1], madeForLove[2], madeForLove[3], actualLove[1], actualLove[2], actualLove[3]) + break + end end end -checkCompat("Löve", "11.3.0", ("%s.%s.%s"):format(love.getVersion())) -checkCompat("Ubiquitousse", "0.0.1", uqt.version) + +if uqt.version ~= madeForUqt then + txt = txt .. ("Ubiquitousse Löve backend was made for Ubiquitousse %s but %s is used!\n") + :format(madeForUqt, uqt.version) +end + +-- Show warnings +if txt ~= "" then + txt = txt .. "Things may not work as expected.\n" + print(txt) + love.window.showMessageBox("Compatibility warning", txt, "warning") +end diff --git a/init.lua b/init.lua index 8f48c2b..6f5a7d1 100644 --- a/init.lua +++ b/init.lua @@ -64,33 +64,19 @@ local ubiquitousse ubiquitousse = { --- Ubiquitousse version. -- @impl ubiquitousse - version = "0.0.1", - - --- Should be called each time the game loop is ran; will update every loaded Ubiquitousse module that needs it. - -- @tparam number dt time since last call, in miliseconds - -- @impl mixed - update = function(dt) - if ubiquitousse.timer then ubiquitousse.timer.update(dt) end - if ubiquitousse.scene then ubiquitousse.scene.update(dt) end - if ubiquitousse.input then ubiquitousse.input.update(dt) end - end, - - --- Should be called each time the game expect a new frame to be drawn; will draw every loaded Ubiquitousse module that needs it - -- The screen is expected to be cleared since last frame. - -- @impl mixed - draw = function() - if ubiquitousse.scene then ubiquitousse.scene.draw() end - end + version = "0.0.1" } -- We're going to require modules requiring Ubiquitousse, so to avoid stack overflows we already register the ubiquitousse package package.loaded[p] = ubiquitousse -- Require external submodules -for _, m in ipairs{"asset", "ecs", "input", "scene", "timer", "util"} do +for _, m in ipairs{"signal", "asset", "ecs", "input", "scene", "timer", "util"} do local s, t = pcall(require, p.."."..m) if s then ubiquitousse[m] = t + elseif not t:match("^module [^n]+ not found") then + error(t) end end diff --git a/input/backend/ctrulua.lua b/input/backend/ctrulua.lua index 6829663..cccb8d9 100644 --- a/input/backend/ctrulua.lua +++ b/input/backend/ctrulua.lua @@ -1,5 +1,8 @@ local input = require((...):match("^(.-%.)backend").."input") +local loaded, signal = pcall(require, (...):match("^(.-)input").."signal") +if not loaded then signal = nil end + local gfx = require("ctr.gfx") local hid = require("ctr.hid") @@ -266,4 +269,9 @@ input.default.pointer:bind( input.default.confirm:bind("key.a") input.default.cancel:bind("key.b") +--- Register signals +if signal then + signal.event:replace("update", oUpdate, input.update) +end + return input diff --git a/input/backend/love.lua b/input/backend/love.lua index 158547e..833f151 100644 --- a/input/backend/love.lua +++ b/input/backend/love.lua @@ -1,5 +1,8 @@ local input = require((...):match("^(.-%.)backend").."input") +local loaded, signal = pcall(require, (...):match("^(.-)input").."signal") +if not loaded then signal = nil end + -- Config -- -- Use ScanCodes (layout independant input) instead of KeyConstants (layout dependant) for keyboard input @@ -13,24 +16,23 @@ local displayKeyConstant = true love.mouse.setVisible(false) -- Button detection --- FIXME love callbacks do something cleaner local buttonsInUse = {} local axesInUse = {} -function love.keypressed(key, scancode, isrepeat) +function input.keypressed(key, scancode, isrepeat) if useScancodes then key = scancode end buttonsInUse["keyboard."..key] = true end -function love.keyreleased(key, scancode) +function input.keyreleased(key, scancode) if useScancodes then key = scancode end buttonsInUse["keyboard."..key] = nil end -function love.mousepressed(x, y, button, istouch) +function input.mousepressed(x, y, button, istouch) buttonsInUse["mouse."..button] = true end -function love.mousereleased(x, y, button, istouch) +function input.mousereleased(x, y, button, istouch) buttonsInUse["mouse."..button] = nil end -function love.wheelmoved(x, y) +function input.wheelmoved(x, y) if y > 0 then buttonsInUse["mouse.wheel.up"] = true elseif y < 0 then @@ -42,17 +44,17 @@ function love.wheelmoved(x, y) buttonsInUse["mouse.wheel.left"] = true end end -function love.mousemoved(x, y, dx, dy) +function input.mousemoved(x, y, dx, dy) if dx ~= 0 then axesInUse["mouse.move.x"] = dx/love.graphics.getWidth() end if dy ~= 0 then axesInUse["mouse.move.y"] = dy/love.graphics.getHeight() end end -function love.gamepadpressed(joystick, button) +function input.gamepadpressed(joystick, button) buttonsInUse["gamepad.button."..joystick:getID().."."..button] = true end -function love.gamepadreleased(joystick, button) +function input.gamepadreleased(joystick, button) buttonsInUse["gamepad.button."..joystick:getID().."."..button] = nil end -function love.gamepadaxis(joystick, axis, value) +function input.gamepadaxis(joystick, axis, value) if value ~= 0 then axesInUse["gamepad.axis."..joystick:getID().."."..axis] = value else @@ -61,11 +63,7 @@ function love.gamepadaxis(joystick, axis, value) end -- Windows size -input.drawWidth, input.drawHeight = love.graphics.getWidth(), love.graphics.getHeight() -function love.resize(width, height) - input.drawWidth = width - input.drawHeight = height -end +input.getDrawWidth, input.getDrawHeight = love.graphics.getWidth, love.graphics.getHeight -- Update local oUpdate = input.update @@ -190,7 +188,7 @@ input.basicAxisDetector = function(id) end end -input.buttonsInUse = function(threshold) +input.buttonUsed = function(threshold) local r = {} threshold = threshold or 0.5 for b in pairs(buttonsInUse) do @@ -201,10 +199,10 @@ input.buttonsInUse = function(threshold) table.insert(r, b.."%"..(v < 0 and -threshold or threshold)) end end - return r + return unpack(r) end -input.axesInUse = function(threshold) +input.axisUsed = function(threshold) local r = {} threshold = threshold or 0.5 for b,v in pairs(axesInUse) do @@ -212,7 +210,7 @@ input.axesInUse = function(threshold) table.insert(r, b.."%"..threshold) end end - return r + return unpack(r) end input.buttonName = function(...) @@ -316,4 +314,18 @@ input.default.cancel:bind( "gamepad.button.1.b" ) +--- Register signals +if signal then + signal.event:bind("keypressed", input.keypressed) + signal.event:bind("keyreleased", input.keyreleased) + signal.event:bind("mousepressed", input.mousepressed) + signal.event:bind("mousereleased", input.mousereleased) + signal.event:bind("wheelmoved", input.wheelmoved) + signal.event:bind("mousemoved", input.mousemoved) + signal.event:bind("gamepadpressed", input.gamepadpressed) + signal.event:bind("gamepadreleased", input.gamepadreleased) + signal.event:bind("gamepadaxis", input.gamepadaxis) + signal.event:replace("update", oUpdate, input.update) +end + return input diff --git a/input/input.lua b/input/input.lua index 0d8a988..403cc19 100644 --- a/input/input.lua +++ b/input/input.lua @@ -1,5 +1,8 @@ --- ubiquitousse.input -- Depends on a backend. +-- Optional dependencies: ubiquitousse.signal (to bind to update signal in signal.event) +local loaded, signal = pcall(require, (...):match("^(.-)input").."signal") +if not loaded then signal = nil end -- TODO: some key selection helper? Will be backend-implemented, to account for all the possible input methods. -- TODO: some way to list all possible input / outputs, or make the *inUse make some separation between inputs indiscutitably in use and those who are incertain. @@ -40,8 +43,8 @@ local button_mt = { -- @treturn ButtonInput this ButtonInput object unbind = function(self, ...) for _, d in ipairs({...}) do - for i, detector in ipairs(self.detectors) do - if detector == d then + for i=#self.detectors, 1, -1 do + if self.detectors[i] == d then table.remove(self.detectors, i) break end @@ -270,7 +273,9 @@ local axis_mt = { self.val, self.raw, self.max = val, raw, max updated[self] = true end - end + end, + + --- LÖVE note: other callbacks that are defined in backend/love.lua and need to be called in the associated LÖVE callbacks. } axis_mt.__index = axis_mt @@ -382,9 +387,9 @@ local pointer_mt = { x = function(self) if self.grabbing == self then self:update() - return self.valX + (self.offsetX or self.width or input.drawWidth/2) + return self.valX + (self.offsetX or self.width or input.getDrawWidth()/2) else - return self.offsetX or self.width or input.drawWidth/2 + return self.offsetX or self.width or input.getDrawWidth()/2 end end, --- Returns the current Y value of the pointer. @@ -392,9 +397,9 @@ local pointer_mt = { y = function(self) if self.grabbing == self then self:update() - return self.valY + (self.offsetY or self.height or input.drawHeight/2) + return self.valY + (self.offsetY or self.height or input.getDrawHeight()/2) else - return self.offsetY or self.height or input.drawHeight/2 + return self.offsetY or self.height or input.getDrawHeight()/2 end end, @@ -418,9 +423,9 @@ local pointer_mt = { local magnitude = sqrt(x*x + y*y) cx, cy = cx / magnitude * width, cy / magnitude * height end - return cx + (self.offsetX or width or input.drawWidth/2), cy + (self.offsetY or height or input.drawHeight/2) + return cx + (self.offsetX or width or input.getDrawWidth()/2), cy + (self.offsetY or height or input.getDrawHeight()/2) else - return self.offsetX or width or input.drawWidth/2, self.offsetY or height or input.drawHeight/2 + return self.offsetX or width or input.getDrawWidth()/2, self.offsetY or height or input.getDrawHeight()/2 end end, @@ -445,7 +450,7 @@ local pointer_mt = { if not updated[self] then local x, y = self.valX, self.valY local xSpeed, ySpeed = self.xSpeed, self.ySpeed - local width, height = self.width or input.drawWidth/2, self.height or input.drawHeight/2 + local width, height = self.width or input.getDrawWidth()/2, self.height or input.getDrawHeight()/2 local newX, newY = x, y local maxMovX, maxMovY = 0, 0 -- the maxium axis movement in a direction (used to determine which axes have the priority) (absolute value) for _, pointer in ipairs(self.detectors) do @@ -642,16 +647,16 @@ input = { --- Returns a list of the buttons currently in use, identified by their string button identifier. -- This may also returns "axis threshold" buttons if an axis passes the threshold. - -- @treturn table buttons identifiers list - -- @treturn[opt=0.5] number threshold the threshold to detect axes as button + -- @tparam[opt=0.5] number threshold the threshold to detect axes as button + -- @treturn string,... buttons identifiers list -- @impl backend - buttonsInUse = function(threshold) end, + buttonUsed = function(threshold) end, --- Returns a list of the axes currently in use, identified by their string axis identifier - -- @treturn table axes identifiers list - -- @treturn[opt=0.5] number threshold the threshold to detect axes + -- @tparam[opt=0.5] number threshold the threshold to detect axes + -- @treturn string,... axes identifiers list -- @impl backend - axesInUse = function(threshold) end, + axisUsed = function(threshold) end, --- Returns a nice name for the button identifier. -- Can be locale-depedant and stuff, it's only for display. @@ -686,14 +691,14 @@ input = { cancel = nil -- Button: used to cancel something. Example binds: Escape, B button. }, - --- Draw area dimensions. + --- Get draw area dimensions. -- Used for pointers. -- @impl backend - drawWidth = 1, - drawHeight = 1, + getDrawWidth = function() return 1 end, + getDrawHeight = function() return 1 end, --- Update all the Inputs. - -- Should be called at every game update; called by ubiquitousse.update. + -- Should be called at every game update. If ubiquitousse.signal is available, will be bound to the "update" signal in signal.event. -- The backend can hook into this function to to its input-related updates. -- @tparam numder dt the delta-time -- @impl ubiquitousse @@ -701,6 +706,11 @@ input = { dt = newDt updated = {} end + + --- If you use LÖVE, note that in order to provide every feature (especially key detection), several callbacks functions will + -- need to be called on LÖVE events. See backend/love.lua. + -- If ubiquitousse.signal is available, these callbacks will be bound to signals in signal.event (with the same name as the LÖVE + -- callbacks, minux the "love."). } -- Create default inputs @@ -708,4 +718,9 @@ input.default.pointer = input.pointer() input.default.confirm = input.button() input.default.cancel = input.button() +-- Bind signals +if signal then + signal.event:bind("update", input.update) +end + return input diff --git a/scene/scene.lua b/scene/scene.lua index 5db7095..de6525e 100644 --- a/scene/scene.lua +++ b/scene/scene.lua @@ -1,5 +1,8 @@ --- ubiquitousse.scene -- Optional dependencies: ubiquitousse.timer (to provide each scene a timer registry) +-- Optional dependencies: ubiquitousse.signal (to bind to update and draw signal in signal.event) +local loaded, signal = pcall(require, (...):match("^(.-)scene").."signal") +if not loaded then signal = nil end local loaded, timer = pcall(require, (...):match("^(.-)scene").."timer") if not loaded then timer = nil end @@ -134,8 +137,16 @@ scene = setmetatable({ table.remove(scene.stack) end, + --- Pop all scenes. + -- @impl ubiquitousse + popAll = function() + while scene.current do + scene.pop() + end + end, + --- Update the current scene. - -- Should be called at every game update; called by ubiquitousse.update. + -- Should be called at every game update. If ubiquitousse.signal is available, will be bound to the "update" signal in signal.event. -- @tparam number dt the delta-time (milisecond) -- @param ... arguments to pass to the scene's update function after dt -- @impl ubiquitousse @@ -147,7 +158,7 @@ scene = setmetatable({ end, --- Draw the current scene. - -- Should be called every time the game is draw; called by ubiquitousse.draw. + -- Should be called every time the game is draw. If ubiquitousse.signal is available, will be bound to the "draw" signal in signal.event. -- @param ... arguments to pass to the scene's draw function -- @impl ubiquitousse draw = function(...) @@ -160,4 +171,10 @@ scene = setmetatable({ end }) +-- Bind signals +if signal then + signal.event:bind("update", scene.update) + signal.event:bind("draw", scene.draw) +end + return scene diff --git a/signal/backend/love.lua b/signal/backend/love.lua new file mode 100644 index 0000000..b93570c --- /dev/null +++ b/signal/backend/love.lua @@ -0,0 +1,43 @@ +local signal = require((...):match("^(.-%.)backend").."signal") + +function signal.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*1000) + end + else + love[callback] = function(dt) + event:emit(callback, dt*1000) + 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 + +return signal diff --git a/signal/init.lua b/signal/init.lua new file mode 100644 index 0000000..827b528 --- /dev/null +++ b/signal/init.lua @@ -0,0 +1,14 @@ +local signal + +local p = ... +if love then + signal = require(p..".backend.love") +elseif package.loaded["ctr"] then + error("NYI") +elseif package.loaded["libretro"] then + error("NYI") +else + error("no backend for ubiquitousse.signal") +end + +return signal \ No newline at end of file diff --git a/signal/signal.can b/signal/signal.can new file mode 100644 index 0000000..004160f --- /dev/null +++ b/signal/signal.can @@ -0,0 +1,116 @@ +--- ubiquitousse.signal + +let registry_mt = { + --- Map of signals to list of listeners. + -- @impl ubiquitousse + signals = {}, + + --- Bind one or several functions to a signal name. + -- @impl ubiquitousse + bind = :(name, fn, ...) + if not @signals[name] then + @signals[name] = {} + end + table.insert(@signals[name], fn) + if ... then + return @bind(name, ...) + end + end, + + --- Unbind one or several functions to a signal name. + -- @impl ubiquitousse + unbind = :(name, fn, ...) + if not @signals[name] then + return + end + for i=#@signals[name], 1, -1 do + if @signals[name] == fn then + table.remove(@signals[name], i) + end + end + if ... then + return @unbind(name, ...) + end + end, + + --- Remove every bound function to a signal name. + -- @impl ubiquitousse + unbindAll = :(name) + @signals[name] = nil + end, + + --- Replace a bound function with another function. + -- @impl ubiquitousse + replace = :(name, sourceFn, destFn) + if not @signals[name] then + @signals[name] = {} + end + for i, fn in ipairs(@signals[name]) do + if fn == sourceFn then + @signals[name][i] = destFn + break + end + end + end, + + --- Remove every bound function to every signal. + -- @impl ubiquitousse + clear = :() + @signals = {} + end, + + --- Emit a signal, i.e. call every function bound to it, with the given arguments. + -- @impl ubiquitousse + emit = :(name, ...) + if @signals[name] then + for _, fn in ipairs(@signals[name]) do + fn(...) + end + end + end +} +registry_mt.__index = registry_mt + +let signal = { + --- Creates and return a new SignalRegistry. + -- A SignalRegistry is a separate ubiquitousse.signal instance: its signals will be independant from other registries. + -- @impl ubiquitousse + new = () + return setmetatable({ signals = {} }, registry_mt) + end, + + --- Global SignalRegistry. + -- @impl ubiquitousse + signals = {}, + bind = (...) + return registry_mt.bind(signal, ...) + end, + unbind = (...) + return registry_mt.unbind(signal, ...) + end, + clear = (...) + return registry_mt.clear(signal, ...) + end, + emit = (...) + return registry_mt.emit(signal, ...) + end, + + --- SignalRegistry which will be used to bind signals that need to be called on game engine event. + -- 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. + -- Provided signals: + -- * update, should be called on every game update + -- * draw, should be called on every game draw + -- * for LÖVE, there are callbacks for every LÖVE callback function that need to be called on their corresponding LÖVE callback + -- @impl mixed + event = nil, + + --- Call this function to hook signal.event signals to the current backend. + -- For LÖVE, 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. + -- @impl backend + registerEvents = () end +} + +signal.event = signal.new() + +return signal diff --git a/timer/timer.lua b/timer/timer.lua index 3f2840b..a17ef44 100644 --- a/timer/timer.lua +++ b/timer/timer.lua @@ -1,5 +1,9 @@ --- ubiquitousse.timer -- Depends on a backend. +-- Optional dependencies: ubiquitousse.signal (to bind to update signal in signal.event) +local loaded, signal = pcall(require, (...):match("^(.-)timer").."signal") +if not loaded then signal = nil end + local ease = require((...):match("^.-timer")..".easing") local timer @@ -332,7 +336,7 @@ timer = { -- @impl ubiquitousse delayed = {}, lastTime = 0, - update = function(...) + update = function(...) -- If ubiquitousse.signal is available, will be bound to the "update" signal in signal.event. return registry_mt.update(timer, ...) end, run = function(...) @@ -346,4 +350,9 @@ timer = { end } +-- Bind signals +if signal then + signal.event:bind("update", timer.update) +end + return timer