The game's renderer is one missing binding; generics is not the blocker
This commit is contained in:
parent
8d8346e58a
commit
523b80c388
483
PORTING.md
Normal file
483
PORTING.md
Normal file
@ -0,0 +1,483 @@
|
||||
# Porting siam-farmer
|
||||
|
||||
What the author's game needs from Flan that Flan does not have yet, measured against
|
||||
the code that exists today rather than against a plan.
|
||||
|
||||
`~/Development/siam-farmer` holds two implementations of the same game. The Clojure one
|
||||
(`src/*.clj`, ~1500 lines) is the complete thought. The Common Lisp one (`src/engine.lisp`,
|
||||
`src/game.lisp`, `src/tilemap-blob.lisp`) is the author working the same design through a
|
||||
compiled, non-lazy, manually-managed host. **Where the two differ is the most reliable
|
||||
evidence in this report**: every place the Lisp port abandoned a Clojure construct is a
|
||||
place Flan does not have to supply one, and the author has already paid the cost of
|
||||
finding that out.
|
||||
|
||||
**Coverage.** I read, in full: `src/engine.clj`, `src/game.clj`, `src/rl.clj`,
|
||||
`src/sprite_atlas.clj`, `src/tilemap_blob.clj`, `src/util.clj`, `src/check.clj`,
|
||||
`src/watch.clj`, `src/editor.clj`, and the whole Lisp port — `src/engine.lisp`,
|
||||
`src/game.lisp`, `src/tilemap-blob.lisp`. On the Flan side: `vendor/raylib/raylib.flan`
|
||||
and its `link`/`headers`/`build-web.sh`, `sand.flan`, `lib/prelude.ml`'s export surface,
|
||||
`lib/check.ml`'s container and global rules, `runtime/flan_rt.c`'s trap path,
|
||||
`lib/dev.ml`'s request gating, and the relevant `test/programs/*.flan`. I did **not** read
|
||||
`MEMORY.org` or `TODO.org` in siam-farmer, its Emacs side (`watch.el`, `cider-error.el`)
|
||||
beyond what the Clojure sources say about them, or most of Flan's `plan.org`/`BUILT.md`
|
||||
outside the FFI sections. Everything below points at a file. Nothing below is
|
||||
hypothetical.
|
||||
|
||||
---
|
||||
|
||||
## 1. The one thing that cannot be written at all
|
||||
|
||||
### `DrawTexturePro` is not bound
|
||||
|
||||
Every tile in this game is drawn by one call:
|
||||
|
||||
- `src/game.clj`, `draw-tile!` — `rl/draw-texture-pro!*` with a source rect into the
|
||||
atlas, a destination rect on the grid, an origin, a rotation and a tint.
|
||||
- `src/game.lisp`, `draw-tile` — the same call, same six arguments.
|
||||
- `src/game.clj`, `draw-game` — again for the palette overlay, once per palette cell.
|
||||
|
||||
It is not in `vendor/raylib/raylib.flan`. I grepped for both spellings across
|
||||
`vendor/`, `lib/`, `test/` and `tools/`; there is no `DrawTexturePro` and no
|
||||
`draw-texture-pro` anywhere in the tree.
|
||||
|
||||
Nothing else in the hand-written 172 does the job. `DrawTextureRec` takes a source
|
||||
rect and a position but no scale, and this game draws 16px tiles at 4x. `DrawTextureEx`
|
||||
takes a scale but no source rect, and this game draws one cell out of an atlas. The two
|
||||
together are exactly the two halves `DrawTexturePro` puts in one call, and neither half
|
||||
is usable alone.
|
||||
|
||||
**Blocker.** Not "awkward" — the renderer of this game cannot be expressed. The grid
|
||||
draw, the palette, and the tile riding the cursor are all of the drawing there is.
|
||||
|
||||
**Cost to fix: one line.** The function's signature mentions only `Texture2D`,
|
||||
`Rectangle`, `Vector2` and `Color`, all four of which `raylib.flan` already describes, so
|
||||
it is a `declare-c` beside `draw-texture-rec` and nothing else.
|
||||
|
||||
**Why it did not already land.** It is importable — with `FLAN_RAYLIB_H` exported it
|
||||
comes in with the other 256. But `vendor/raylib/headers` says the header is opt-in on
|
||||
purpose, so that a build needs libraylib linkable and *not* raylib-devel installed. That
|
||||
property is worth keeping, and it means the default build of this game has no renderer.
|
||||
The general rule this exposes is worth more than the one line: **a function the game
|
||||
calls every frame should be hand-written and checked against the header, not left to the
|
||||
import.** The import widens the surface; it should not be load-bearing.
|
||||
|
||||
Three more the same way, none of them blockers, all one line each:
|
||||
|
||||
| Missing | Used at | Verdict |
|
||||
|---|---|---|
|
||||
| `ImageFromImage` | `sprite_atlas.clj` `auto-select-tiles`; `tilemap-blob.lisp` `tile-subimages` | Annoyance — see §3 |
|
||||
| `IsWindowReady` | `engine.clj` `run-game!`, `engine.lisp` `run-game` — the "window already open" guard | Annoyance; a `defvar bool` does the same thing |
|
||||
| `SetTextureFilter` | declared in `rl.clj`, called nowhere | Not needed. Point is raylib's default |
|
||||
| `UpdateTexture` | declared in `rl.clj`, called nowhere | Not needed |
|
||||
| `SetClipboardText` | `sprite_atlas.clj`, in a `comment` block only | Not needed |
|
||||
| `GetMouseWheelMoveV` | declared in `rl.clj`; the atlas tool uses the scalar form, which *is* bound | Not needed |
|
||||
|
||||
And one enum member, not a function: **`raylib.flan`'s `Key` has no `left-shift`.** Both
|
||||
`game.clj`'s `handle-game-input` and `game.lisp`'s do shift+1..5 to pick the tilemap, via
|
||||
`KEY_LEFT_SHIFT` (340). The `defenum` carries a deliberate subset — the keys `sand.flan`
|
||||
uses — and this is one member short of what this game needs. One entry.
|
||||
|
||||
---
|
||||
|
||||
## 2. What looks like a gap and is not
|
||||
|
||||
This is the half of the report that should stop work rather than start it. Each of these
|
||||
is a Clojure habit the Lisp port already dropped, or a Flan answer that is simply spelled
|
||||
differently.
|
||||
|
||||
### Persistent maps as game state — already abandoned
|
||||
|
||||
`game.clj` threads an immutable map through `update-as->` (`util.clj`), `cond->`, `assoc`,
|
||||
`dissoc`, `reduce`-with-`reduced`. `check.clj` exists only to police the shape of that map
|
||||
at runtime, with `malli`, including `{:optional true}` keys because `dissoc :editor-mode`
|
||||
is how the editor closes.
|
||||
|
||||
`game.lisp` threw all of it away. The state is a `defclass` with fixed slots; `update-game`
|
||||
mutates through accessors and returns nothing; `drag-mode` is a slot holding `nil` rather
|
||||
than a key that comes and goes.
|
||||
|
||||
In Flan that is a `defstruct` mutated through a `(Ptr Game)`. `check.clj` does not get
|
||||
ported — it disappears, because the compiler is already doing it and does it earlier. No
|
||||
feature is needed.
|
||||
|
||||
### `reduce` with `reduced` for early exit — `break` covers it
|
||||
|
||||
Three sites: `set-random-tile!`'s weighted pick, `handle-game-input`'s number-key scan,
|
||||
`update-game`'s palette hit test. All three are "walk until something matches, then stop".
|
||||
Flan has `break` with loop labels. `game.lisp` had already rewritten all three as `loop …
|
||||
return`.
|
||||
|
||||
**`loop`/`recur` and tail calls are not needed by this game.** Nothing here recurses.
|
||||
|
||||
### Escaping closures and capture — needed once, and trivially avoided
|
||||
|
||||
I checked every function-valued thing in both implementations:
|
||||
|
||||
- `run-game!`'s `:init`/`:update`/`:draw`/`:unload`/`:watch-fn` — `#'` vars in Clojure,
|
||||
bare symbols in Lisp. Top-level names, no environment. Flan's `(Fn [...] R)` takes them
|
||||
as-is, and `fn-values.flan` shows exactly this shape.
|
||||
- `do-neighbors!`'s `do-fn` — non-escaping, called and dropped.
|
||||
- `auto-tile!` passes `do-neighbors!` a literal `(fn [l r c] … game …)`. **That one
|
||||
captures `game`.** `fn-capture.flan` refuses it by name.
|
||||
|
||||
The fix is to give the callback the parameter: `(Fn [(Ptr Game) i32 i32 i32])`, and
|
||||
`do-neighbors` forwards the game it was handed. One extra parameter. Note also that
|
||||
`game.lisp`'s `do-neighbors` is currently dead code — `auto-tile` calls `tile-bitset`
|
||||
directly — so even the one capture is in a path the port has not revived.
|
||||
|
||||
**Escaping closures are not on this game's path.** Nothing stores a function with an
|
||||
environment; nothing needs to.
|
||||
|
||||
### `Result`/`try` — nothing uses that discipline
|
||||
|
||||
Neither implementation returns errors as values. Both use the host's exception or
|
||||
condition system throughout. Porting this game exercises `signal`/`error`/`restart-case`
|
||||
and never wants a `Result`.
|
||||
|
||||
### `handler-case` — one site, and `handler-bind` already covers it
|
||||
|
||||
`engine.clj`'s `reload-config!` is `(try … (catch Exception e (println e)))`. That is
|
||||
"log it and carry on", which is what a `handler-bind` clause returning normally does.
|
||||
|
||||
### `Handle` and pools — nothing to pool
|
||||
|
||||
The texture cache is five entries keyed by path (`engine.clj` `load-texture-once`,
|
||||
`engine.lisp` `*texture-cache*`). There are no entities yet, no spawning, no freeing
|
||||
mid-frame. Handles and pools have no customer in this code.
|
||||
|
||||
### User-written allocators, structural typing — no customer either
|
||||
|
||||
Nothing in either implementation parameterises over allocation, and nothing depends on a
|
||||
type's shape rather than its name.
|
||||
|
||||
### Reading `.edn` / `.data` files at runtime — `vendor:edn` already does it
|
||||
|
||||
`engine.lisp` `read-data`, `game.lisp` `load-tileset` and `read-bitmask` read plists out
|
||||
of `assets/*.data` at startup, and `reload-config!` re-reads the config while the game
|
||||
runs. `vendor/edn/edn.flan` is a 561-line tokenizer with `int-of` / `text=?` /
|
||||
`keyword=?` / `expect`, and `test/programs/edn.flan` includes a hand-written struct
|
||||
reader as the worked example.
|
||||
|
||||
So this is writable today. It costs a hand-written reader per schema — two here, the
|
||||
tileset (`:texture-path`, `:selected-cells`) and the bitmask table — because `(read-edn T
|
||||
bytes)` is not built. Call it ~80 lines, once, at load time. See §3 for whether it should
|
||||
be written at all.
|
||||
|
||||
---
|
||||
|
||||
## 3. Real friction, with what the workaround costs
|
||||
|
||||
Ordered by how much of the game it touches.
|
||||
|
||||
### Globals cannot hold a `Vec` or a `Map`
|
||||
|
||||
`lib/check.ml:4039`, `no_move_only_global`. Both hosts keep the texture cache in a global
|
||||
(`engine.clj`'s `texture-cache` atom, `engine.lisp`'s `*texture-cache*` hash table), and
|
||||
`engine.lisp` keeps `*buffers*` there too.
|
||||
|
||||
`sand.flan`'s idiom — everything in `defvar` fixed arrays, the loop functions taking no
|
||||
state — works because nothing it holds is move-only. This game's state is not like that:
|
||||
`tilesets`, `src-rects`, `dst-rects` and the bitmask table all want a `Vec` or a `Map`.
|
||||
|
||||
**Annoyance.** Two workarounds, both cheap, and the second is better than what either
|
||||
host does:
|
||||
|
||||
1. Hold the state in a `let` in `main` and pass `(Ptr Game)` to update and draw. Costs
|
||||
one parameter on three functions.
|
||||
2. For the texture cache specifically, a `[8 CacheEntry]` fixed array with a count beats a
|
||||
`(Map string Texture2D)` outright — the game loads five textures, and a linear scan of
|
||||
five path comparisons at load time is not worth a hash table. This is a case where the
|
||||
restriction pushes toward the right answer.
|
||||
|
||||
### A `Vec` cannot hold a move-only element, and a `Map` cannot hold a move-only value
|
||||
|
||||
`lib/check.ml:412` and `:317`. `game.lisp`'s shape is `tilesets` = a vector of `tileset`
|
||||
structs, each of which owns `tiles`, `src-rects` and `dst-rects` vectors. That is
|
||||
`(Vec Tileset)` where `Tileset` owns three `Vec`s, and it is refused — recursive teardown
|
||||
waits on `drop`, which `NEXT.md:484` records as deferred, not built.
|
||||
|
||||
**Annoyance.** The fix is at the *inner* level, not the outer one: making the outer
|
||||
container a fixed array changes nothing, because `[4 Tileset]` where `Tileset` owns `Vec`s
|
||||
is still move-only transitively (`lib/check.ml:3831`). Make `tiles`, `src-rects` and
|
||||
`dst-rects` fixed arrays with counts, and then nothing in the state is move-only at all.
|
||||
This game has exactly one tileset and about twenty palette cells. Cost: the palette's capacity becomes a
|
||||
constant instead of growing. That is not a loss at this size.
|
||||
|
||||
Flag it anyway, because the next thing this game grows — several tilesets, several
|
||||
atlases — grows straight into it.
|
||||
|
||||
### Generics: an annoyance here, not the blocker the plan says it is
|
||||
|
||||
I looked for every place this game changes element type, because that is the case the
|
||||
project has queued generics for.
|
||||
|
||||
- `game.clj` `compute-tileset-rects` / `game.lisp` the same — `[r c]` pairs to
|
||||
`Rectangle`s. Twenty elements, once, at load.
|
||||
- `game.clj` `init-game`'s `mapv` over four `[keyword path]` pairs to textures. Four
|
||||
elements, once.
|
||||
- The prelude is monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so
|
||||
anything over `[r c]` pairs has no helper at all.
|
||||
|
||||
All three are `dotimes` with a `push`. Five lines each, at load time, run once.
|
||||
|
||||
**The reusable-engine argument does not survive either.** `engine.clj` is shared by
|
||||
`game.clj` and `sprite_atlas.clj` with two different state types, which looks like it
|
||||
forces `run-game` to be generic over the state. It does not: with the state in the game's
|
||||
own storage and the engine taking only `(Fn [] Unit)` callbacks, the engine never names
|
||||
the state type. `sand.flan` is already written that way. The frame rollback is the one
|
||||
piece that needs to touch the state, and the game can supply `snapshot`/`restore` as two
|
||||
more callbacks.
|
||||
|
||||
**This verdict is conditional, and the condition is one decision.** Both it and the
|
||||
globals workaround above hang on the same pivot, so it is worth stating once, flatly:
|
||||
*nothing in this game's state needs to be move-only.* Make the inner collections fixed
|
||||
arrays with counts, and then the state fits in `defvar` globals, the engine takes only
|
||||
`(Fn [] Unit)` callbacks and never names a state type, and generics stays off the critical
|
||||
path. Keep `Vec`s in a let-bound `Game` struct passed as `(Ptr Game)` instead, and
|
||||
`run-game` names `Game`, two programs with two state types need two engines, and generics
|
||||
is back on the critical path immediately. The recommendation is the first branch.
|
||||
|
||||
**On that branch: generics is worth building, and it is not what is standing between Flan
|
||||
and this game.** Saying otherwise would send the work in the wrong direction. If the goal is
|
||||
siam-farmer running, generics is not the next thing.
|
||||
|
||||
### A bad index ends the process, and the frame loop is built on surviving one
|
||||
|
||||
This is the finding with the widest consequences, and it is about the dev loop rather
|
||||
than the language.
|
||||
|
||||
Both hosts survive a bad frame and keep the window open:
|
||||
|
||||
- `engine.clj` `run-game!` snapshots every `IntGrid` at the top of the frame, catches
|
||||
`Throwable`, restores the grids, and parks in a draw-only loop until the error is
|
||||
cleared.
|
||||
- `engine.lisp` `run-frame` wraps update and draw in a `restart-case` offering
|
||||
`retry-frame`, `skip-frame` and `reinit`, each of which rolls the grids *and* a MOP
|
||||
shallow-copy of the state object back.
|
||||
|
||||
Both files carry a deliberate blow-up on the `E` key (`game.clj` `handle-game-input`,
|
||||
`game.lisp` the same) whose only purpose is exercising that path.
|
||||
|
||||
Flan has the shape already — `sand.flan`'s main loop is `restart-case` around
|
||||
`(agent/poll)` and `(game-update)` with a `continue` restart, which is `skip-frame` under
|
||||
another name. Two things are missing behind it:
|
||||
|
||||
1. **The rollback.** `sand.flan`'s `continue` abandons the frame but restores nothing.
|
||||
This game's grids are mutated in place by `set-tile`/`delete-tile`, so an abandoned
|
||||
frame leaves half-written state. In Flan this is *easier* than in either host — fixed
|
||||
arrays are values, so `(set backup grid)` is the whole of `snapshot!` and there is no
|
||||
`sb-mop:class-slots` walk to write. It just has to be written.
|
||||
|
||||
2. **A bounds failure is not a condition.** `runtime/flan_rt.c` `flan_bounds_fail` prints
|
||||
and calls `rt_die`, which is `exit(134)`. No handler runs, no restart is offered, and
|
||||
`lib/dev.ml` answers every subsequent request with "the program exited; restart flan
|
||||
dev".
|
||||
|
||||
That is not theoretical for this game. `game.clj`'s `update-game` computes `row` and
|
||||
`col` straight from the mouse position and indexes the grid with them, with no bounds
|
||||
check anywhere. **`game.lisp` added `in-bounds-p` and calls it in `update-drag`** — the
|
||||
author hit this and fixed it in the port. But the Clojure version survived the bug
|
||||
because a bad frame was recoverable, and under Flan the same bug is the end of the
|
||||
session.
|
||||
|
||||
**Not a blocker for writing the game** — the game can be written, and the check can be
|
||||
written. **It is a hole in "never restarting"**, and this game walks into it by the most
|
||||
ordinary route there is: a mouse coordinate one pixel outside the window.
|
||||
|
||||
Worth separating honestly: the `(+ 1 :asdf)` blow-up both hosts carry is a *type* error,
|
||||
and Flan rejects it at compile time. Part of that rollback machinery is answering a
|
||||
dynamic-language problem and is not a gap. The out-of-bounds path is the part that
|
||||
survives the port, and it is the part that matters.
|
||||
|
||||
### The watch buffer has no equivalent
|
||||
|
||||
`watch.clj` is a pull-based watch: the game drops a snapshot of labelled values into an
|
||||
atom once per frame, and Emacs polls it on its own timer, so the watch rate is decoupled
|
||||
from the frame rate and nothing is pushed over nREPL. `spy` records one value per label;
|
||||
`spy-num` keeps count/min/max/last/mean in a `double-array` so it survives being called
|
||||
thousands of times a frame without allocating. `game.clj`'s `watch-game` publishes the
|
||||
mouse position and the whole state minus the three big collections every frame.
|
||||
|
||||
Flan's inspector is **stopped-only**: `lib/dev.ml` `globals_op` refuses a running program
|
||||
by name — "globals are read against a stopped stack, and the stack is what decides which
|
||||
of them to show". That is the right design for a break loop and it is not what this is.
|
||||
Watching a value while the game plays at 30fps is a different tool.
|
||||
|
||||
**Annoyance, and a heavily-used one.** The author built this deliberately (the docstring
|
||||
argues the design), built the Emacs side for it, and built a second numeric variant for
|
||||
hot loops. A game that stops to be inspected is not the same workflow as a game that
|
||||
shows you a number while you drag the mouse.
|
||||
|
||||
Whether `flan dev`'s `eval` against a running program could serve it — it does not gate on
|
||||
`Running` the way `globals_op` does — I did not test, and should not be assumed.
|
||||
|
||||
### Offline image tooling
|
||||
|
||||
`sprite_atlas.clj` `auto-select-tiles` carves the atlas into 16x16 subimages and keeps the
|
||||
ones that are not fully transparent. `tilemap_blob.clj` / `tilemap-blob.lisp` carve a
|
||||
tileset the same way and sample eight edge pixels per tile to compute an autotiling
|
||||
bitmask. Both go through `ImageFromImage`, which is not bound.
|
||||
|
||||
**Not a gap, mostly a rewrite.** `GetImageColor` *is* bound, and `Image.data` is a
|
||||
`(Ptr u8)`, so both jobs read better against the whole image with computed coordinates
|
||||
than against a subimage per tile — the bitmask sampler in particular is eight
|
||||
`get-image-color` calls at offsets derived from the tile's origin, and never needs a
|
||||
subimage at all. `ImageCrop` is bound but mutates in place and there is no `ImageCopy`, so
|
||||
the literal transcription is the one that does not work.
|
||||
|
||||
`sprite_atlas.clj` also opens a `java.awt.FileDialog` to choose where to save. Nothing in
|
||||
Flan or in raylib 6.0 offers that. Workaround: write to a fixed path under `assets/`.
|
||||
Tool-only, and the tool is run by the author.
|
||||
|
||||
### String formatting
|
||||
|
||||
`sprite_atlas.clj`'s HUD draws `(format " X: %d" (int mx))` every frame. Flan has
|
||||
`append-i64!`, `concat`, `join` and `format-f64`, and no printf. Cost: a few lines and a
|
||||
reusable buffer per HUD string, since doing it naively allocates per frame. Small, and
|
||||
only the atlas tool draws text today.
|
||||
|
||||
### `reload-config!` may not want porting at all
|
||||
|
||||
`engine.clj` re-reads `game-config.edn` from disk while the game runs so the config can be
|
||||
edited live. Flan's answer is compile-time embedding plus `flan dev` recompiling into the
|
||||
running process, which reaches the same place with no file watcher, no runtime parser, and
|
||||
no window where the file on disk and the value in memory disagree. **This is a case where
|
||||
porting the mechanism would be porting the workaround.**
|
||||
|
||||
---
|
||||
|
||||
## 4. What Flan does better than either host
|
||||
|
||||
Worth recording, because these are places the game gets shorter rather than longer.
|
||||
|
||||
- **Conditions and restarts, natively.** `engine.clj` reaches them by printing a
|
||||
`SIAM-REPORT-EXCEPTION` marker line, stashing the throwable in an atom, and having
|
||||
`cider-error.el` watch the REPL output and rethrow it over nREPL so it lands in a
|
||||
stacktrace buffer. That is a marker line, an atom and an Emacs Lisp
|
||||
handler simulating what `engine.lisp` gets from `restart-case` in eight lines. Flan is on the Lisp side of that, with
|
||||
typed restarts, and `dev-break.flan` shows the editor driving them.
|
||||
|
||||
- **Frame snapshot is an assignment.** `engine.lisp` walks `sb-mop:class-slots` to
|
||||
shallow-copy the state object, with a comment explaining that it copies *into* the
|
||||
existing object so a REPL reference stays live. In Flan the state struct and the fixed
|
||||
arrays are values; `(set backup state)` is the whole thing, and there is no aliasing
|
||||
question to answer.
|
||||
|
||||
- **`check.clj` does not get ported.** 56 lines of malli schema, a `:closed true` map, and
|
||||
a compile-time flag to switch it off in release — all to catch a typo'd key. A struct
|
||||
field is checked by the compiler, earlier, for free, and the release flag has nothing to
|
||||
switch off.
|
||||
|
||||
- **Asset paths stop being a hazard.** `engine.lisp` `resource-path` exists because raylib
|
||||
resolves paths against the process working directory, which under SLY is wherever the
|
||||
inferior Lisp started, and a miss is silent — `LoadTexture` hands back a texture with id
|
||||
0. The function resolves against the ASDF system directory and `probe-file`s it, and
|
||||
`load-texture-once` checks for id 0 afterwards anyway. Flan's compile-time embedding of
|
||||
a file or a directory, plus the bound `LoadImageFromMemory` and `LoadTextureFromImage`,
|
||||
removes the failure mode instead of detecting it.
|
||||
|
||||
- **`num-keys` indexing.** `game.lisp`'s key loop reads `(aref *num-keys* n)` over a
|
||||
literal vector of keywords. Flan's `defenum Key` resolves a keyword against its members
|
||||
at compile time and a typo is an error there.
|
||||
|
||||
---
|
||||
|
||||
## 5. The raylib binding, checked against what the game actually links
|
||||
|
||||
siam-farmer vendors raylib **6.0** (`vendor/raylib-6.0/include/raylib.h`, `RAYLIB_VERSION
|
||||
"6.0"`). Flan pins **5.5** — `vendor/raylib/link` names `libraylib.so.550` and
|
||||
`build-web.sh` pins the web archive to tag 5.5, both with comments explaining that two
|
||||
targets built from different raylibs would disagree about layouts silently.
|
||||
|
||||
**I diffed every struct on this game's path against the 6.0 header: `Vector2`, `Color`,
|
||||
`Rectangle`, `Texture`/`Texture2D`, `Image`, `Font`, `GlyphInfo`. All seven are byte-for-byte
|
||||
identical to `raylib.flan`'s `defstruct`s.** Field order, field count and scalar widths all
|
||||
match.
|
||||
|
||||
Of the C names `rl.clj` declares, I checked nine directly against the vendored header —
|
||||
`DrawTexturePro`, `ImageFromImage`, `GetImageColor`, `CheckCollisionPointRec`,
|
||||
`DrawTextureEx`, `SetTextureFilter`, `IsWindowReady`, `SetClipboardText` and
|
||||
`GetMouseWheelMove` — and all nine exist in 6.0 under the same name with the same
|
||||
signature. **I did not sweep the full ~50.** Those nine are the ones on the game's own
|
||||
path; the remainder is unchecked and should not be read as verified.
|
||||
|
||||
**Enum values, the other half of what `link` says `raylib.flan` carries.** `KEY_ZERO`=48
|
||||
through `KEY_NINE`=57, `KEY_SPACE`=32, `KEY_E`=69, `KEY_LEFT_SHIFT`=340,
|
||||
`MOUSE_BUTTON_LEFT`/`RIGHT`/`MIDDLE`=0/1/2, and `LOG_WARNING` as the fifth
|
||||
`TraceLogLevel` member (4) are all unchanged in 6.0 and all match `raylib.flan` — except
|
||||
that `left-shift` is absent from the `Key` `defenum` entirely, which is the §1 note, not a
|
||||
skew.
|
||||
|
||||
So the version skew is not a correctness problem for this game today. It is still worth
|
||||
closing, because the whole point of `headers` is that a silent disagreement is the failure
|
||||
mode, and "I checked by hand once" is exactly the state `headers` was built to replace.
|
||||
|
||||
The binding's real gap against `rl.clj` is §1: `DrawTexturePro`, then `ImageFromImage` and
|
||||
`IsWindowReady`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ranked: what to build next
|
||||
|
||||
Split into two tiers, because a one-line binding fix and a month of language work should
|
||||
not compete for the same slot.
|
||||
|
||||
### Tier 0 — bindings. Hours, not weeks. Do these first.
|
||||
|
||||
1. **`declare-c draw-texture-pro`.** Unblocks the entire renderer. Without it this game
|
||||
has no draw call. One line, plus an acceptance case that makes raylib compute with the
|
||||
source rect so a permuted `Rectangle` goes red.
|
||||
|
||||
2. **`left-shift` in the `Key` `defenum`.** Shift+1..5 picks the tilemap in both
|
||||
implementations, and the member is not there. One entry, and without it that input is
|
||||
unwritable.
|
||||
|
||||
3. **`declare-c image-from-image` and `window-ready?`.** The atlas tool and the engine's
|
||||
reentrancy guard. Two lines.
|
||||
|
||||
4. **Decide the rule the first item exposes.** A function the game calls every frame
|
||||
should be hand-written and header-checked, not left to the opt-in import. Worth writing
|
||||
down, because the next game-shaped program will find the next `DrawTexturePro`.
|
||||
|
||||
### Tier 1 — language and tooling, in the order that unblocks the most of this game
|
||||
|
||||
4. **A bounds failure should stop the program, not end it.** Route `flan_bounds_fail`
|
||||
through the condition machinery so it reaches the break loop with the restarts that are
|
||||
on the stack, instead of `exit(134)`. This is the single largest gap between what Flan
|
||||
promises and what this game would experience, and the game reaches it through the most
|
||||
ordinary path in it — a mouse coordinate outside the window, which `game.lisp` had to
|
||||
add `in-bounds-p` to survive. Everything else here is a workaround with a known cost;
|
||||
this one ends the session.
|
||||
|
||||
5. **A watch for a running program.** `watch.clj` + `watch.el` + `spy` + `spy-num` is a
|
||||
tool the author built deliberately and uses constantly, and the stopped-stack inspector
|
||||
is a different tool for a different moment. Given that the dev loop is the priority,
|
||||
this outranks every language feature below it. The numeric accumulator for hot loops is
|
||||
the part that is least obvious and most valuable.
|
||||
|
||||
6. **Frame rollback in the engine pattern.** Not a language feature — the pieces are all
|
||||
there (`restart-case`, struct assignment, fixed arrays as values). What is missing is
|
||||
the worked example showing `snapshot`/`restore` callbacks alongside the `continue`
|
||||
restart, the way `sand.flan` is the worked example for the loop. A page of code and a
|
||||
test program, and the "never restarting" claim gets materially stronger.
|
||||
|
||||
7. **`drop`, or recursive teardown.** Unblocks `(Vec T)` where `T` owns a `Vec`, which is
|
||||
`game.lisp`'s tileset shape exactly. Fixed arrays sidestep it at this size, so this is
|
||||
about the version of the game with several atlases, not this one. `NEXT.md:484` already
|
||||
has it deferred with reasons; nothing here argues with that, only records a customer.
|
||||
|
||||
8. **Generics.** Real, and worth building — but for this game it is five-line loops at
|
||||
load time, and the reusable-engine argument for it dissolves once the state lives in the
|
||||
game rather than in the engine. **Do not sequence it ahead of items 4–6 on this game's
|
||||
account.**
|
||||
|
||||
9. **`(read-edn T bytes)`.** `vendor:edn` already makes the asset readers writable; this
|
||||
removes ~80 lines of hand-written cursor walking for two schemas. Convenience, and it
|
||||
competes with compile-time embedding, which may be the better answer for both files
|
||||
anyway.
|
||||
|
||||
**Not ranked, because this game does not need them:** escaping closures and capture (one
|
||||
site, fixed by one parameter), `Handle` and pools (nothing to pool), `Result`/`try`
|
||||
(neither host uses that discipline), `handler-case` (one site, `handler-bind` covers it),
|
||||
`loop`/`recur` and tail calls (nothing recurses), user-written allocators, structural
|
||||
typing.
|
||||
Loading…
x
Reference in New Issue
Block a user