From 2046b5e1e0d5c80994cb5fd5e2f4e739ab142706 Mon Sep 17 00:00:00 2001 From: the lemons Date: Wed, 29 Mar 2023 01:47:25 -0500 Subject: evloop refactor --- evloop.lua | 201 +++++++++++++++++++++++++++++++--------------------------- game/gfx.lua | 7 +- game/init.lua | 69 +++++++++----------- main.lua | 67 ++++++++++++++++++-- 4 files changed, 197 insertions(+), 147 deletions(-) diff --git a/evloop.lua b/evloop.lua index 302a29a..637d266 100644 --- a/evloop.lua +++ b/evloop.lua @@ -1,133 +1,144 @@ -local M = {} -M.__index = M +-- module implementing nestable asynchronous event loop objects. the module +-- itself is an event loop object, representing the main event loop. -local queue = {} +local M -local function resume_task(task, e) - if coroutine.status(task.co) == "dead" then - return false +local function new(...) + local new = setmetatable({}, M) + new.event_queue = {} + new.tasks = {} + for i = 1, select("#", ...) do + new:wrap(select(i, ...)) end + return new +end - if task.filter then - for _, f in ipairs(task.filter) do - if f == e[1] then - goto resume - end - end - if e[1] == "update" and - task.timeout and love.timer.getTime() >= task.timeout then - e = {} - goto resume - end - return true - end +M = new() +M.__index = M - ::resume:: - local ok, ret, timeout = coroutine.resume(task.co, e) - if not ok then - io.stderr:write(debug.traceback(task.co, ret)..'\n\n') - error "error in event loop" - end - task.filter = ret - task.timeout = timeout and love.timer.getTime() + timeout - return coroutine.status(task.co) ~= "dead" +M.new = new + +-- create a new task in the event loop. +function M:wrap(fn, ...) + table.insert(self.event_queue, {coroutine.create(fn), ...}) end -local function create_task(fn) - local task = {} - task.co = coroutine.create(fn) - resume_task(task) - return task +-- send an event: evloop:queue(event, args...) +function M:queue(...) + assert(type((...))) + table.insert(self.event_queue, {...}) end -function M.poll(timeout, ...) - local filter = {...} +local function event_filter(timeout, ...) + local filter = {} + for i = 1, select("#", ...) do + filter[select(i, ...)] = true + end if type(timeout) == "string" then - table.insert(filter, timeout) + filter[timeout] = true timeout = nil end - return unpack( - coroutine.yield((timeout or #filter > 0) and filter, timeout)) -end - -function M.sleep(secs) - M.poll(secs) + return filter, timeout end -function M.queue(...) - table.insert(queue, {...}) +-- poll for an event: evloop.poll([timeout,] [events...]) +-- returns: event, args... +function M.poll(...) + local filter, timeout = event_filter(...) + local e + local t = 0 + repeat + e = coroutine.yield(true) + if e[1] == "timer" then + t = t + e[2] + end + if timeout and t >= timeout then + return + end + until filter[e[1]] or (not timeout and not next(filter)) + return unpack(e) end -function M.await_any(...) - local tasks = {...} - for i, t in ipairs(tasks) do - tasks[i] = create_task(t) - end - while true do - local e = coroutine.yield() - for _, t in ipairs(tasks) do - if not resume_task(t, e) then return end +-- like evloop.poll but returns an iterator +function M.events(...) + local filter, timeout = event_filter(...) + local t = 0 + local function iter() + if timeout and t >= timeout then + t = t - timeout + return false + end + local e = coroutine.yield(true) + if e[1] == "timer" then + t = t + e[2] + end + if (timeout or next(filter)) and not filter[e[1]] then + return iter() + else + return unpack(e) end end + return iter +end + +function M.sleep(secs) + M.poll(secs) +end + +function M:quit(...) + self:queue("quit", ...) end function M.debug_sleep(secs) - M.paused = true local t = 0 + M.queue "debug.pause" while t < secs do - local _, dt = M.poll "debug.update" + local _, dt = M.poll "debug.timer" t = t + dt end - M.paused = false + M.queue "debug.unpause" end -local function poll_love() - love.event.pump() - local es = love.event.poll() - while true do - local e = {es()} - if not e[1] then break end - if love.handlers[e[1]] then - love.handlers[e[1]](unpack(e, 2)) - end - table.insert(queue, e) +local function resume_task(co, ...) + if coroutine.status(co) == "dead" then + return false end -end -function M.mainloop(start) - local main = create_task(function() - start() - end) + local ok, ret = coroutine.resume(co, ...) + if not ok then + -- TODO: make this better somehow + io.stderr:write(debug.traceback(co, ret)..'\n\n') + error "error in event loop" + end - return function() - if not M.paused then poll_love() end + return coroutine.status(co) ~= "dead" +end - local q = queue - queue = {} - for i, e in ipairs(q) do - if e[1] == "quit" then - if not love.quit or not love.quit() then - return e[2] or 0 +-- run each task in the event loop at once +function M:run() + while true do + ::send_events:: + local q = self.event_queue + self.event_queue = {} + for _, e in ipairs(q) do + if type(e[1]) == "thread" then + -- start a new task + if resume_task(e[1], unpack(e, 2)) then + self.tasks[e[1]] = true + end + elseif e[1] == "quit" then + return unpack(e, 2) + else + for task in pairs(self.tasks) do + self.tasks[task] = resume_task(task, e) or nil end end - if not resume_task(main, e) then return 0 end end - - local dt = love.timer.step() - if not resume_task( - main, {M.paused and "debug.update" or "update", dt}) then - return 0 + if #self.event_queue == 0 then + if not next(self.tasks) then return end + assert(coroutine.running(), "event queue depleted!") + table.insert(self.event_queue, coroutine.yield(true)) end - - ::graphics:: - if love.graphics and love.graphics.isActive() then - love.graphics.clear(love.graphics.getBackgroundColor()) - require "viewport".origin() - if not resume_task(main, {"draw", dt}) then return 0 end - love.graphics.present() - end - - love.timer.sleep(0.001) end end diff --git a/game/gfx.lua b/game/gfx.lua index a5a05a8..93bb3b0 100644 --- a/game/gfx.lua +++ b/game/gfx.lua @@ -164,13 +164,10 @@ function M:draw(dt) end end -function M:loop() - local function loop() - local _, dt = evloop.poll "draw" +function M:run() + for _, dt in evloop.events "draw" do self:draw(dt) - return loop() end - return loop end return M diff --git a/game/init.lua b/game/init.lua index 085b0e8..a7fd64f 100644 --- a/game/init.lua +++ b/game/init.lua @@ -41,16 +41,15 @@ function M.new(params) end function M:input_loop() - local function loop() + for e, key in evloop.events("keypressed", "keyreleased") do -- TODO: interface with a remappable input system (it should generate -- its own events) - local e, key = evloop.poll("keypressed", "keyreleased") if not self.piece then - return loop() + goto continue end - local moved if e == "keypressed" then + local moved if key == "left" then moved = self.piece:move(0, -1) elseif key == "right" then @@ -80,24 +79,23 @@ function M:input_loop() local tmp = self.hold self.hold = self.piece.poly self.piece = tmp:drop(self.field) - evloop.queue "game.lock_cancel" + evloop:queue "game.lock_cancel" end self.can_hold = false ::bypass:: end - end - if moved then - if self.infinity then - evloop.queue "game.lock_cancel" - elseif not self.piece:can_move(-1, 0) then - evloop.queue "game.lock" + if moved then + if self.infinity then + evloop:queue "game.lock_cancel" + elseif not self.piece:can_move(-1, 0) then + evloop:queue "game.lock" + end end end - return loop() + ::continue:: end - return loop end function M:on_rotated() @@ -114,7 +112,7 @@ function M:place_piece() if not self.piece:can_move(-1, 0) then self.piece:place() self.stats.pieces = self.stats.pieces + 1 - evloop.queue "game.lock_cancel" + evloop:queue "game.lock_cancel" local cleared = self.field:remove_cleared() if cleared > 0 then self.combo = self.combo + 1 @@ -123,11 +121,11 @@ function M:place_piece() local sound = ({"tspinsingle","tspindouble","tspintriple"})[cleared] sfx.play(sound) end - evloop.queue "game.line_clear" + evloop:queue "game.line_clear" else self.combo = -1 end - evloop.queue "game.piece_placed" + evloop:queue "game.piece_placed" self:next_piece() return true else @@ -142,43 +140,34 @@ function M:next_piece() end function M:lock_loop() - local function loop() - evloop.poll "game.lock" + for _ in evloop.events "game.lock" do local e = evloop.poll(self.lock_delay, "game.lock_cancel") - if e then - return loop() - else + if not e then self:place_piece() end - return loop() end - return loop end function M:gravity_loop() - local function loop() - evloop.sleep(self.gravity_delay) + for _ in evloop.events(self.gravity_delay) do if self.piece and not self.piece:move(-1, 0) then - evloop.queue "game.lock" + evloop:queue "game.lock" end - if self.piece then - return loop() + if not self.piece then + self.loop:quit() end end - return loop end -function M:loop() - local function loop() - self:next_piece() - evloop.await_any( - self:input_loop(), - self:gravity_loop(), - self:lock_loop(), - self.gfx:loop() - ) - end - return loop +function M:run() + self:next_piece() + self.loop = evloop.new( + function() self:input_loop() end, + function() self:gravity_loop() end, + function() self:lock_loop() end, + function() self.gfx:run() end + ) + return self.loop:run() end return M diff --git a/main.lua b/main.lua index 586ccdf..2957bda 100644 --- a/main.lua +++ b/main.lua @@ -1,16 +1,69 @@ local evloop = require "evloop" local game = require "game" -local game_obj +local function main() + evloop.poll "load" + local game_obj = game.new {} + evloop.poll "loaded" + game_obj:run() + evloop:quit() +end + +local function update() + evloop:queue("load", love.arg.parseGameArguments(arg), arg) + evloop:queue "loaded" + evloop.poll "loaded" + local paused = false + for e in evloop.events("update", "debug.pause", "debug.unpause") do + if e == "debug.pause" then + paused = true + elseif e == "debug.unpause" then + paused = false + end + if not paused then + love.event.pump() + local es = love.event.poll() + while true do + local e = {es()} + if not e[1] then break end + if love.handlers[e[1]] then + love.handlers[e[1]](unpack(e, 2)) + end + evloop:queue(unpack(e)) + end + end + + local dt = love.timer.step() + evloop:queue(not paused and "timer" or "debug.timer", dt) -function love.load() - game_obj = game.new {} + if love.graphics and love.graphics.isActive() then + love.graphics.clear(love.graphics.getBackgroundColor()) + require "viewport".origin() + evloop:queue "draw" + evloop:queue "draw_complete" + evloop.poll "draw_complete" + love.graphics.present() + end + + love.timer.sleep(0.001) + end end +evloop:wrap(update) +evloop:wrap(main) + +local run = coroutine.wrap(function() evloop:run() end) + function love.run() - if love.load then love.load(love.arg.parseGameArguments(arg), arg) end if love.timer then love.timer.step() end - return evloop.mainloop(function() - game_obj:loop()() - end) + return function() + local val = run({"update"}) + if val == true then + return + elseif val == nil then + return 0 + else + return val + end + end end -- cgit v1.2.3