Initial dummy template with hot reload logic

This commit is contained in:
Jeremy Penner 2023-10-22 18:28:46 -04:00
commit c2860399fa
12 changed files with 364 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
source
*.pdx

11
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "playdate",
"request": "launch",
"name": "Playdate: Debug",
"preLaunchTask": "${defaultBuildTask}"
}
]
}

32
.vscode/tasks.json vendored Normal file
View file

@ -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
}
}
]
}

32
Makefile Normal file
View file

@ -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

21
src/main.fnl Normal file
View file

@ -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)
)

32
src/meta/iter.fnl Normal file
View file

@ -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}

54
src/meta/proxy.fnl Normal file
View file

@ -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

80
src/meta/reload.fnl Normal file
View file

@ -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

57
src/meta/table.fnl Normal file
View file

@ -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

37
src/package.lua Normal file
View file

@ -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)

5
src/pdxinfo Normal file
View file

@ -0,0 +1,5 @@
name=Explosionface
author=Jeremy Penner
description=Keep the world from exploding!
bundleID=com.glorioustrainwrecks.explosionface
buildNumber=1

1
src/test.fnl Normal file
View file

@ -0,0 +1 @@
{:x :suppery :z 3}