From c2860399fa8d5283b8ea6fa1b3923f2fa24563c3 Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Sun, 22 Oct 2023 18:28:46 -0400 Subject: [PATCH] Initial dummy template with hot reload logic --- .gitignore | 2 ++ .vscode/launch.json | 11 +++++++ .vscode/tasks.json | 32 ++++++++++++++++++ Makefile | 32 ++++++++++++++++++ src/main.fnl | 21 ++++++++++++ src/meta/iter.fnl | 32 ++++++++++++++++++ src/meta/proxy.fnl | 54 ++++++++++++++++++++++++++++++ src/meta/reload.fnl | 80 +++++++++++++++++++++++++++++++++++++++++++++ src/meta/table.fnl | 57 ++++++++++++++++++++++++++++++++ src/package.lua | 37 +++++++++++++++++++++ src/pdxinfo | 5 +++ src/test.fnl | 1 + 12 files changed, 364 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 Makefile create mode 100644 src/main.fnl create mode 100644 src/meta/iter.fnl create mode 100644 src/meta/proxy.fnl create mode 100644 src/meta/reload.fnl create mode 100644 src/meta/table.fnl create mode 100644 src/package.lua create mode 100644 src/pdxinfo create mode 100644 src/test.fnl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8cbe73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +source +*.pdx diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0e19324 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "playdate", + "request": "launch", + "name": "Playdate: Debug", + "preLaunchTask": "${defaultBuildTask}" + } + ] + } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..077771d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,32 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "Fennel: Compile", + "command": "make source", + "problemMatcher": [] + }, + { + "type": "pdc", + "problemMatcher": ["$pdc-lua", "$pdc-external"], + "dependsOn": ["Fennel: Compile"], + "label": "Playdate: Build" + }, + { + "type": "playdate-simulator", + "problemMatcher": ["$pdc-external"], + "label": "Playdate: Run" + }, + { + "label": "Playdate: Build and Run", + "dependsOn": ["Playdate: Build", "Playdate: Run"], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..17695b7 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +# adapted from https://git.sr.ht/~nytpu/fennel-playdate-template/tree/master/item/Makefile + +rwildcard=$(foreach d,$(wildcard $(1:=/*)),$(call rwildcard,$d,$2) $(filter $(subst *,%,$2),$d)) + +PDXNAME = Explosionface +EXCLUDESOURCES = src/macros.fnl +SOURCEFILES = $(filter-out $(EXCLUDESOURCES),$(call rwildcard, src, *.fnl)) +OUTFILES = $(subst src,source,$(SOURCEFILES:.fnl=.lua) $(filter-out $(SOURCEFILES),$(call rwildcard, src, *))) + +all: $(PDXNAME).pdx + +source: $(OUTFILES) + +source/%.lua: src/%.fnl + mkdir -p $(shell dirname $@) + fennel -c $< > $@ + +source/%: src/% + mkdir -p $(shell dirname $@) + cp $< $@ + +$(PDXNAME).pdx: source + pdc source $@ + +clean: + rm -r $(PDXNAME).pdx + rm -r source + +run: $(PDXNAME).pdx + PlaydateSimulator $< + +.PHONY: all source clean diff --git a/src/main.fnl b/src/main.fnl new file mode 100644 index 0000000..bbe93c2 --- /dev/null +++ b/src/main.fnl @@ -0,0 +1,21 @@ +(import :CoreLibs/object) +(import :CoreLibs/graphics) +(import :CoreLibs/sprites) +(import :CoreLibs/timer) +(import :package) + +(local reload (require :meta.reload)) + +(local test (require :test)) +(local gfx playdate.graphics) + +(let [font (gfx.getSystemFont gfx.font.kVariantNormal)] + (fn _G.drawtext [text x y] (font:drawText text x y))) + +(fn playdate.update [] + (drawtext test.x 5 5) + (drawtext "Hello from Fennel!" 5 25) + (gfx.sprite.update) + (playdate.timer.updateTimers) + (reload.check) +) diff --git a/src/meta/iter.fnl b/src/meta/iter.fnl new file mode 100644 index 0000000..66299e7 --- /dev/null +++ b/src/meta/iter.fnl @@ -0,0 +1,32 @@ +(fn coro-iter [f ?start-arg] + (fn coro-next [co prev] + (if (= (coroutine.status co) :dead) nil + + ((fn [success ...] (if success ... (error ...))) + (coroutine.resume co prev)))) + (values coro-next (coroutine.create (fn [...] (f ...) nil)) ?start-arg)) + +(fn countiter [minmax ?max ?step] + (let [min (if ?max minmax 1) + max (or ?max minmax) + step (or ?step 1)] + (fn [_ iprev] + (let [i (if iprev (+ iprev step) min)] + (when (if (> step 0) (<= i max) (>= i max)) i))))) + +(fn reverse-ipairs [table] + (fn prev [tbl i-next] + (let [i (- i-next 1)] + (when (> i 0) + (values i (. tbl i))))) + (values prev table (+ (length table) 1))) + +(fn nextpacked [packed i-prev] + (let [i (+ i-prev 1)] + (when (<= i (or packed.n (length packed))) + (values i (. packed i))))) + +(fn ivalues [...] + (values nextpacked (table.pack ...) 0)) + +{: coro-iter : countiter : reverse-ipairs : nextpacked : ivalues} \ No newline at end of file diff --git a/src/meta/proxy.fnl b/src/meta/proxy.fnl new file mode 100644 index 0000000..00b9cb8 --- /dev/null +++ b/src/meta/proxy.fnl @@ -0,0 +1,54 @@ +; Proxy: An object with no backing data of its own, that intercepts property accesses +; from / to its backing table, possibly transforming them in the process. +; It should look, as much as possible, like a regular table, though they are subject +; to certain limitations: +; * table.* functions like `table.insert` do not work reliably or at all before Lua 5.3 +; * the `next` function always returns nil, because in order for the __index and __newindex +; metamethods to fire on every access, the raw table must not have any data in it +; * On Lua 5.1 proxies are implemented as userdata so that the __len metamethod functions. +; `type` is rewritten to return :table in this case, so `match` will continue to work. +; Proxies also implement the atom protocol by default. + +(local {: extend} (require :meta.table)) + +(local Proxy {}) + +(fn Proxy.proxy? [proxy] +"Returns true if an object is a proxy." + (match (getmetatable proxy) + {: target} true + _ false)) + +(fn Proxy.target [proxy] +"Returns the target object that the proxy is intercepting." + (. (getmetatable proxy) :target)) + +(fn Proxy.new-blank [metatable] +"Creates a new 'blank' table with the given `metatable`." + (setmetatable {} metatable)) + +(fn Proxy.target-next [t ?k] + (let [target (Proxy.target t) + k (next target ?k)] + (when k (values k (. t k))))) + +(fn Proxy.metatable [target ?override] +"Returns a 'default' metatable, overriding table metamethods to implement 'no-ops' so as to +appear as though it is the target object. The `?override` table, if provided, is merged into +the metatable, overriding any metamethods it may contain." + (extend {: target + :__index #(. (Proxy.target $1) $2) + :__newindex #(tset (Proxy.target $1) $2 $3) + :__pairs #(values Proxy.target-next $1 nil) + :__len #(length (Proxy.target $1)) + :__type :table + :get Proxy.target + :set #(tset (getmetatable $1) :target $2)} + (or ?override {}))) + +(fn Proxy.new [target ?mt-override] +"Creates a new proxy object targetting `target`, with behaviour customized in its metatable by +`?mt-override`." + (Proxy.new-blank (Proxy.metatable target ?mt-override))) + +Proxy \ No newline at end of file diff --git a/src/meta/reload.fnl b/src/meta/reload.fnl new file mode 100644 index 0000000..e8ca669 --- /dev/null +++ b/src/meta/reload.fnl @@ -0,0 +1,80 @@ +(local Proxy (require :meta.proxy)) +(local reload {}) + +(if playdate.isSimulator + (do + (fn reload.redirect-table [t target] + (each [k _ (pairs t)] (tset t k nil)) + (fn __reload [t new-val] + (let [mt (getmetatable t)] + (set mt.target new-val) + t)) + (setmetatable t (Proxy.metatable target {: __reload}))) + + (fn reload.copy-table-contents [src dst ?preprocess] + (each [k v (pairs src)] + (tset dst k (if ?preprocess (?preprocess v (. dst k)) v)))) + + (fn reload.replace-table [old new] + (reload.copy-table-contents new old #(reload.reload-value $2 $1)) + (reload.redirect-table new old) + old) + + (fn reload.reload-table [old new] + (let [mt (getmetatable old)] + ;; TODO: figure out how `new`'s metatable should influence `old`? + (if (and mt mt.__reload) (mt.__reload old new) + (= (type new) :table) (reload.replace-table old new) + new))) + + (fn reload.reload-value [old new] + (if (= (type old) :table) (reload.reload-table old new) + new)) + + (fn reload.reload [modname] + (let [old (. package.loaded modname) + _ (tset package.loaded modname nil) + new (reload.reload-value old (require modname))] + new)) + + (set package.watches {}) + + (fn reload.modtime [modname] + (playdate.file.modtime (package.pdzfilename modname))) + + (fn reload.modtime= [time1 time2] + (and (= time1.year time2.year) + (= time1.month time2.month) + (= time1.day time2.day) + (= time1.hour time2.hour) + (= time1.minute time2.minute) + (= time1.second time2.second))) + + (fn reload.watch [modname] + (when (and (= nil (. package.watches modname)) + (playdate.file.exists (package.pdzfilename modname))) + (tset package.watches modname {:modtime (reload.modtime modname)}))) + + (fn reload.check [] + (each [modname watch (pairs package.watches)] + (let [newmodtime (reload.modtime modname)] + (when (not (reload.modtime= watch.modtime newmodtime)) + (print (.. "Plugin reload: " modname)) + (set watch.modtime newmodtime) + (reload.reload modname))))) + + (reload.watch :meta.proxy) + (reload.watch :meta.iter) + (reload.watch :meta.table) + + (let [oldrequire _G.require] + (fn _G.require [modname] + (when (= nil (. package.loaded modname)) + (reload.watch modname)) + (oldrequire modname)))) + + (do + (fn reload.watch [modname]) + (fn reload.check []))) + +reload \ No newline at end of file diff --git a/src/meta/table.fnl b/src/meta/table.fnl new file mode 100644 index 0000000..81c423b --- /dev/null +++ b/src/meta/table.fnl @@ -0,0 +1,57 @@ +(local {: ivalues} (require :meta.iter)) +(local tbl {}) + +; Naming convention: Verb means mutation, noun / adjective means no mutation + +(fn tbl.ensure [t key ?default-value] + (match (. t key) + something something + nil (do (tset t key (or ?default-value {})) + (. t key)))) + +(fn tbl.extend [t ...] + (each [_ t-override (ivalues ...)] + (collect [k v (pairs t-override) &into t] k v)) + t) + +(fn tbl.defaults [t ...] + (each [_ t-default (ivalues ...)] + (collect [k v (pairs t-default) &into t] + (when (= (. t k) nil) (values k v)))) + t) + +(fn tbl.unset [t ...] + (each [_ k (ivalues ...)] + (tset t k nil)) + t) + +(fn tbl.clear [t ...] + (let [keys-to-keep (collect [_ k (ivalues ...)] k true)] + (each [k (pairs t)] + (when (not (. keys-to-keep k)) + (tset t k nil)))) + t) + +(fn tbl.deepclone [v] + (if (= (type v) :table) + (let [mt (getmetatable v)] + (if (and mt mt.__deepclone) + (mt.__deepclone v) + (collect [k child (pairs v)] k (tbl.deepclone child)))) + v)) + +(fn tbl.concat [t ...] + (each [_ t-concat (ivalues ...)] + (icollect [_ v (ipairs t-concat) &into t] v)) + t) + +(fn tbl.extended [...] + (tbl.extend {} ...)) + +(fn tbl.concatenated [...] + (tbl.concat [] ...)) + +(fn tbl.unpacked [packed ?i ?j] + (table.unpack packed ?i (or ?j packed.n))) + +tbl \ No newline at end of file diff --git a/src/package.lua b/src/package.lua new file mode 100644 index 0000000..161eb68 --- /dev/null +++ b/src/package.lua @@ -0,0 +1,37 @@ +_G.package = { loaded = {}, searchers = {}, preload = {} } + +function _G.require(modname) + local module = package.loaded[modname] + if module ~= nil then + return module + end + local searcherrors = {} + for _, searcher in ipairs(package.searchers) do + local loader, data = searcher(modname) + if type(loader) == "function" then + module = loader(data) + package.loaded[modname] = module + return module + elseif type(loader) == "string" then + table.insert(searcherrors, loader) + end + end + error("Unable to load module: " .. modname .. "\n" .. table.concat(searcherrors, "\n")) +end + +table.insert(package.searchers, function (modname) + return package.preload[modname] +end) + +function package.pdzfilename(modname) + return modname:gsub("%.", "/") .. ".pdz" +end + +table.insert(package.searchers, function (modname) + local filename = package.pdzfilename(modname) + if playdate.file.exists(filename) then + return playdate.file.run, filename + else + return filename .. " does not exist" + end +end) diff --git a/src/pdxinfo b/src/pdxinfo new file mode 100644 index 0000000..605da10 --- /dev/null +++ b/src/pdxinfo @@ -0,0 +1,5 @@ +name=Explosionface +author=Jeremy Penner +description=Keep the world from exploding! +bundleID=com.glorioustrainwrecks.explosionface +buildNumber=1 diff --git a/src/test.fnl b/src/test.fnl new file mode 100644 index 0000000..ddfc68c --- /dev/null +++ b/src/test.fnl @@ -0,0 +1 @@ +{:x :suppery :z 3} \ No newline at end of file