diff --git a/cat-paw/core/patterns/event/EventSystem.lua b/cat-paw/core/patterns/event/EventSystem.lua index 9e0ed02..8d5cc13 100644 --- a/cat-paw/core/patterns/event/EventSystem.lua +++ b/cat-paw/core/patterns/event/EventSystem.lua @@ -7,10 +7,14 @@ local Event = require "cat-paw.core.patterns.event.Event" ------------------------------ Constructor ------------------------------ local EventSystem = middleclass("EventSystem") function EventSystem:initialize() + self.rootSubscribers = setmetatable({}, {__mode = "kv"}) self.events = {} self.eventQueue = {} end +------------------------------ Constants ------------------------------ +EventSystem.ATTACH_TO_ALL = 0 + ------------------------------ Core API ------------------------------ function EventSystem:poll() while #self.eventQueue > 0 do @@ -20,6 +24,12 @@ function EventSystem:poll() end ------------------------------ Internals ------------------------------ +function EventSystem:_fetchAllCallbacks(target, code) + assert(code == EventSystem.ATTACH_TO_ALL, "`attach` got called with invalid code. Should be equal to EventSystem.ATTACH_TO_ALL") + --If subscriber is already attached to one or more specific events, will crash! + table.insert(self.rootSubscribers, target) +end + function EventSystem:_rawAttach(target, event, callback) assert(event:isSubclassOf(Event) or event == Event, "May only attach Event or a subclass of it.") assert(type(callback) == 'function', "Callback must be a function.") @@ -32,12 +42,25 @@ end function EventSystem:_fire(eventInstance) local event = eventInstance.class + + for _, target in pairs(self.rootSubscribers) do + local hierarchyPosition = event + while hierarchyPosition ~= Event.super do + if type(target[hierarchyPosition]) == 'function' then + target[hierarchyPosition](target, eventInstance) + end + --Go up one step in the hierarchy. + hierarchyPosition = hierarchyPosition.super + end + end + local t = {} for e, subscribers in pairs(self.events) do if event:isSubclassOf(e) or e == event then t[#t + 1] = {e, subscribers or {{}, {}}} end end + table.sort(t, function(a, b) -- Returns true when the first is less than the second. return a[1]:isSubclassOf(b[1]) @@ -52,30 +75,27 @@ end ------------------------------ API ------------------------------ EventSystem.attach = overload({ EventSystem, 'table', Event, - function (self, target, event) + function(self, target, event) + --print("case 1", self, target, event, event.class, event:isSubclassOf(Event), event == Event) self:_rawAttach(target, event, target[event]) end, EventSystem, 'table', 'table', - function (self, target, events) + function(self, target, events) + --print("case 2", self, target, events, events.class, events == Event) + --print(events[1].class, events[1] == Event, events[1]:isSubclassOf(Event)) for _, e in pairs(events) do self:_rawAttach(target, e, target[e]) end end, - + + EventSystem, 'table', 'number', + function(self, target, getAllCode) + self:_fetchAllCallbacks(target, getAllCode) + end, + EventSystem, 'table', Event, 'function', EventSystem._rawAttach, - - --Add 4th option, only pass the object. Iterate all its keys and for every; - -- key.subclassof(Event) and type(val) == 'function' call _rawAttach on it. - -- This would be very slow so it should cache the class of every object after the first - -- call, so that subsuquent calls would just read from the cache. - -- WARN: This does mean changes made to the object (or even class) after the first cache will - -- not be noticed. - --EventSystem, 'table', - --function (self, target) - -- - --end }) function EventSystem:queue(event) diff --git a/cat-paw/core/patterns/event/keyboard/EvTextEdit.lua b/cat-paw/core/patterns/event/keyboard/EvTextEdit.lua new file mode 100644 index 0000000..a29369b --- /dev/null +++ b/cat-paw/core/patterns/event/keyboard/EvTextEdit.lua @@ -0,0 +1,10 @@ +local middleclass = require "cat-paw.core.patterns.oop.middleclass" +local Event = require "cat-paw.core.patterns.event.Event" + +local EvTextEdit = middleclass("EvTextEdit", Event) +function EvTextEdit:initilize(text, start, length) + Event.initilize(self) + self.text, self.start, self.length = text, start, length +end + +return EvTextEdit \ No newline at end of file diff --git a/cat-paw/core/patterns/state/Fsm.lua b/cat-paw/core/patterns/state/Fsm.lua index d6f3c95..4631f85 100644 --- a/cat-paw/core/patterns/state/Fsm.lua +++ b/cat-paw/core/patterns/state/Fsm.lua @@ -30,7 +30,7 @@ function Fsm:goTo(id, ...) if self.currentState then self.currentState:leave(state) end local previous = self.currentState self.currentState = state - self.currentState:enter(previous, ...) + self.currentState.enter(self.currentState, previous, ...) return true end diff --git a/cat-paw/engine/AbstractGame.lua b/cat-paw/engine/AbstractGame.lua new file mode 100644 index 0000000..518b7b2 --- /dev/null +++ b/cat-paw/engine/AbstractGame.lua @@ -0,0 +1,96 @@ +local version = require "cat-paw.version" +local middleclass = require "libs.middleclass" + +local Fsm = require "cat-paw.core.patterns.state.Fsm" +local ApiHooks = require "cat-paw.hooks.LoveHooks" + +--local suit = require "libs.suit" +local Scheduler = require "cat-paw.core.timing.Scheduler" +local EventSystem = require "cat-paw.core.patterns.event.EventSystem" + +------------------------------ Constructor ------------------------------ +local AbstractGame = middleclass("AbstractGame", Fsm) +function AbstractGame:initialize(title, targetWindowW, targetWindowH) + Fsm.initialize(self) + self.title = title or "Untitled Game" + love.window.setTitle(title) + if targetWindowW == -1 and targetWindowH == -1 then + love.window.setFullscreen(true) + elseif targetWindowW > 0 and targetWindowH > 0 then + love.window.setMode(targetWindowW, targetWindowH) + else + error(string.format("Invalid window size. w/h must both be -1, for fullscreen," + .. "or positive. Current size: " .. targetWindowW .. ", " .. targetWindowH)) + end + self.windowW, self.windowH = love.window.getMode() + self:_printToolVersions() + + self.scheduler = Scheduler() + self.eventSystem = EventSystem() + ApiHooks.hookHandler(self) +end + +------------------------------ Constants ------------------------------ + +------------------------------ Core ------------------------------ +function AbstractGame:load(args) +end + +function AbstractGame:update(dt) + Fsm.update(self, dt) + + self.scheduler:update(dt) + self.eventSystem:poll() +end + +------------------------------ Other ------------------------------ +--Wrapper so AbstractGame can be directly passed to ApiHooks. Shouldn't be used anywhere else. +--If you want to queue stuff, use game:getEventSystem():queue(event) +--TODO: Find a better way to make this class and ApiHooks work nicely. +function AbstractGame:queue(...) + self.eventSystem:queue(...) +end + +------------------------------ Internals ------------------------------ +function AbstractGame:_printToolVersions() + print("Setting stdout's vbuf mode to 'no'. This is needed for some consoles to work properly.") + io.stdout:setvbuf("no") + print("============================================================") + print("Running Lua version: ", _VERSION) + if jit then + print("Running Luajit version: ", jit.version) + end + print("Running Love2d version: ", love.getVersion()) + print("Running CatPaw version: ", version) + print("\nCurrently using the following 3rd-party libraries (and possibly more):") + print("middleclass\tBy Kikito\tSingle inheritance OOP in Lua\t[MIT License]") + print("bump\t\tBy Kikito\tSimple platformer physics.\t[MIT License]") + print("suit\t\tBy vrld\t\tImGUIs for Lua/Love2D\t\t[MIT License]") + print("Huge thanks to (Kikito and vrld) for their wonderful contributions to the community; and for releasing their work under such open licenses!") + print("============================================================") + print("Game loaded: " .. self.title) + print(string.format("Set window size to: (%d, %d)", self:getWindowSize())) + print("============================================================") +end + +------------------------------ Getters / Setters ------------------------------ +function AbstractGame:getWindowW() return self.windowW end +function AbstractGame:getWindowH() return self.windowH end +function AbstractGame:getWindowSize() return self.windowW, self.windowH end + +function AbstractGame:setWindowSize(w, h) + self.windowW, self.windowH = w, h + love.window.setMode(w, h) +end + +--TODO: Service locator +function AbstractGame:getEventSystem() + return self.eventSystem +end + +function AbstractGame:getScheduler() + return self.scheduler +end + +return AbstractGame + diff --git a/cat-paw/engine/graphics/view/Camera.lua b/cat-paw/engine/graphics/view/Camera.lua new file mode 100644 index 0000000..e69de29 diff --git a/cat-paw/engine/graphics/view/VirtualWindow.lua b/cat-paw/engine/graphics/view/VirtualWindow.lua new file mode 100644 index 0000000..e69de29 diff --git a/cat-paw/hooks/LoveHooks.lua b/cat-paw/hooks/LoveHooks.lua new file mode 100644 index 0000000..3c28b0e --- /dev/null +++ b/cat-paw/hooks/LoveHooks.lua @@ -0,0 +1,110 @@ +local middleclass = require "libs.middleclass" + + +local EvKeyPress = require "cat-paw.core.patterns.event.keyboard.EvKeyPress" +local EvKeyRelease = require "cat-paw.core.patterns.event.keyboard.EvKeyRelease" +local EvTextInput = require "cat-paw.core.patterns.event.keyboard.EvTextInput" +local EvTextEdit = require "cat-paw.core.patterns.event.keyboard.EvTextEdit" + + +local EvMousePress = require "cat-paw.core.patterns.event.mouse.EvMousePress" +local EvMouseRelease = require "cat-paw.core.patterns.event.mouse.EvMouseRelease" +local EvMouseMove = require "cat-paw.core.patterns.event.mouse.EvMouseMove" +local EvMouseWheel = require "cat-paw.core.patterns.event.mouse.EvMouseWheel" + +local EvWindowFocus = require "cat-paw.core.patterns.event.os.EvWindowFocus" +local EvWindowResize = require "cat-paw.core.patterns.event.os.EvWindowResize" +local EvGameQuit = require "cat-paw.core.patterns.event.os.EvGameQuit" + +------------------------------ Constructor ------------------------------ +local LoveHooks = middleclass("LoveHooks") +function LoveHooks:initialize() + error("Attempting to initialize static class!" .. LoveHooks) +end + +------------------------------ API ------------------------------ +-- Can be any object (or even table) with `queue`, `load`, `tick`, and +-- `draw` methods. And optionally a `run` method. +-- `load` can be nil with no issues. `update` and `draw` can technically be null +-- but logic will not process nor will anything be drawn. +function LoveHooks.static.hookHandler(handler) + LoveHooks._hookLoveCallbacks(handler) +end + +------------------------------ Hooks ------------------------------ +---------LovEvents->Evsys +function LoveHooks.static._hookLoveCallbacks(handler) + local wrap = LoveHooks._loveCallbackWrapper + --Game + if handler.run then love.run = wrap(handler, handler.run) + else + love.load = wrap(handler, handler.load) + love.update = wrap(handler, handler.update) + love.draw = wrap(handler, handler.draw) + end + --Evsys + + love.keypressed = wrap(handler, LoveHooks._onKeyPressed) + love.keyreleased = wrap(handler, LoveHooks._onKeyReleased) + love.textinput = wrap(handler, LoveHooks._nTextInput) + love.texteditted = wrap(handler, LoveHooks._nTextEdit) + + love.mousepressed = wrap(handler, LoveHooks._onMousePressed) + love.mousereleased = wrap(handler, LoveHooks._onMouseReleased) + love.mousemoved = wrap(handler, LoveHooks._onMouseMoved) + love.wheelmoved = wrap(handler, LoveHooks._onMouseWheel) + + love.focus = wrap(handler, LoveHooks._onWindowFocus) + love.resize = wrap(handler, LoveHooks._onWindowResize) + love.quit = wrap(handler, LoveHooks._onGameQuit) +end + +------------------------------ Helpers ------------------------------ +function LoveHooks.static._loveCallbackWrapper(handler, f) + return f and function(...) -- wrapper or nil + return f(handler, ...) + end +end + +------------------------------ Evsys ------------------------------ +---------Keyboard +function LoveHooks.static._onKeyPressed(handler, k, code, isRepeat) + handler:queue(EvKeyPress(k, code, isRepeat)) +end +function LoveHooks.static._onKeyReleased(handler, k, code, isRepeat) + handler:queue(EvKeyRelease(k, code, isRepeat)) +end +function LoveHooks.static._onTextInput(handler, char) + handler:queue(EvTextInput(char)) +end + +function LoveHooks.static._onTextEdit(handler, text, start, length) + handler:queue(EvTextEdit(text, start, length)) +end + +---------Mouse +function LoveHooks.static._onMousePressed(handler, x, y, button, touch) + handler:queue(EvMousePress(x, y, button, touch)) +end +function LoveHooks.static._onMouseReleased(handler, x, y, button, touch) + handler:queue(EvMouseRelease(x, y, button, touch)) +end +function LoveHooks.static._onMouseMoved(handler, x, y, dx, dy, touch) + handler:queue(EvMouseMove(x, y, dx, dy, touch)) +end +function LoveHooks.static._onMouseWheel(handler, x, y) + handler:queue(EvMouseWheel(x, y)) +end + +---------OS +function LoveHooks.static._onWindowFocus(handler, focus) + handler:queue(EvWindowFocus(focus)) +end +function LoveHooks.static._onWindowResize(handler, w, h) + handler:queue(EvWindowResize(w, h)) +end +function LoveHooks.static._onGameQuit(handler) + handler:queue(EvGameQuit()) +end + +return LoveHooks diff --git a/cat-paw/main.lua b/cat-paw/main.lua deleted file mode 100644 index afecd88..0000000 --- a/cat-paw/main.lua +++ /dev/null @@ -1,28 +0,0 @@ ---TODO: Fix how Love2D handles this. -print("Setting stdout's vbuf to 'no'") -io.stdout:setvbuf('no') - -print("Running Lua version: ", _VERSION) -print("Running Love2d version: ", love.getVersion()) -print("Running CatPaw version: ", "dev-1.0.0", "\n") -print("Currently using the following 3rd-party libraries:") -print("middleclass\tBy Kikito\tSingle inheritance OOP in Lua\t[MIT License]") -print("bump\t\tBy Kikito\tSimple platformer physics.\t[MIT License]") -print("suit\t\tBy vrld\t\tImGUIs for Lua/Love2D\t\t[MIT License]") -print("Huge thanks to (Kikito and vrld) for their wonderful contributions to the community; and for releasing their work under such open licenses!") -print() - ---TODO: Make a test runner here. ---Should that just be a stand-alone program that launches and manages love- (test-) instances? - -function love.keypressed(key, scancode, isrepeat) - if key == 'escape' then - love.event.quit() - end -end - ---require "quick-tests.overload.all" ---require "quick-tests.instanceOfCheck.main" ---require "quick-tests.event.eventSystem" -require "quick-tests.component.objectFunctionality" ---require "quick-tests.uTable.all" diff --git a/cat-paw/version b/cat-paw/version new file mode 100644 index 0000000..89bf2c1 --- /dev/null +++ b/cat-paw/version @@ -0,0 +1 @@ +0.1.0-alpha \ No newline at end of file diff --git a/cat-paw/version.lua b/cat-paw/version.lua new file mode 100644 index 0000000..21afc0a --- /dev/null +++ b/cat-paw/version.lua @@ -0,0 +1,79 @@ +local version = {} + +------------------------------ Locals ------------------------------ + +--The chronological ordering of branches, used for comparing versions to check which is newer. +local branches = { + alpha = 1, + beta = 2, + prerelease = 3, + none = 10, +} + +--TODO: Find a way to properly just pass a relative path to `io.open`. +local VERSION_PATHS = { + "version", + "cat-paw/version", + "src/cat-paw/version", + "src/cat-paw/cat-paw/version", + "cat-paw/cat-paw/version", + "src/cat-paw/cat-paw/version", + "src/lib/cat-paw/version", + "src/libs/cat-paw/version", + "src/engine/cat-paw/version", +} +------------------------------ Constants ------------------------------ + +--Both set by `version._readVersionFromFile`. +version.VERSION_FILE_PATH = nil +version.VERSION_STRING = nil + +------------------------------ Internals ------------------------------ +function version._readVersionFromFile() + local f, msg, path + for i, p in ipairs(VERSION_PATHS) do + local succ + succ, f = pcall(io.open, p) + if f then + path = p + break + end + end + + if f then + version.VERSION_FILE_PATH = path + version.VERSION_STRING = f:read("*all") + f:close() + else + version.VERSION_FILE_PATH = "UNKNOWN" + version.VERSION_STRING = "UNKNOWN" + print("WARNING: Could not locate version file! Checked the following locations:\n" .. + table.concat(VERSION_PATHS,"\n")) + end +end + +------------------------------ Metamethods ------------------------------ +function version.__tostring() + return version.VERSION_STRING +end + +------------------------------ Quick Test ------------------------------ + +--[[ +print(version.getVersionString()) +version.HOTFIX = 0 +print(version.getVersionString()) +version.HOTFIX = 42 +print(version.getVersionString()) + +version.HOTFIX = 1.2 +print(version.getVersionString()) --Should fail due to invalid HOTFIX. +--]] + +------------------------------ Finalize & Returns ------------------------------ +setmetatable(version, version) + +--The version is expected to never change at runtime, so only computing this once on import should be fine. +version._readVersionFromFile() + +return version