feat(bundle): ship a game as a macOS .app, with a splash it controls

`ludic build` produces a program. Double-clicked it opens a Terminal window, it
wears the generic executable icon, it calls itself whatever the file is called,
and it carries none of its assets. `ludic bundle` produces an application.

Everything it needs is in package.ludic, so the command takes no arguments: an
Info.plist and PkgInfo from `app` lines, an .icns built by sips and iconutil at
all ten sizes macOS asks for from a single source PNG, the asset pack in
Contents/Resources, and an ad-hoc signature - which is not optional on Apple
silicon, where an unsigned binary is killed rather than warned about. The bundle
identifier falls back to the package path reversed, so a project that never
thinks about it still gets a defensible one instead of two apps sharing a key
Launch Services hangs the Dock, saved state and permissions off.

A bundled game is moved to ~/Library/Application Support/<name> before main,
because Finder starts a .app with its working directory at "/" where no save
could ever be written. Reads come out of the pack, writes land somewhere real
and per-user, and the game's save code needs no change and no platform
knowledge.

The splash is the other half of looking like an application. A game that loads
165 MB spends a visible moment doing it with nothing on screen, which from the
outside is indistinguishable from a launch that failed. splash_show puts a
borderless window up from the same constructor that mounts the pack - before
main, so it appears while the process is still starting rather than after the
slow part it exists to cover - and reads the artwork out of the pack like any
other asset. It turns the run loop enough times to be mapped and composited
there and then; once composited the backing store survives a busy main thread,
so it stays up for the whole load.

Nothing hides it automatically. Only the game knows when its first real frame is
ready, and a splash that vanishes before that leaves the same black gap it was
covering, so the game calls App.splash_hide(). Headless there is no splash and
the call lowers to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-10 16:57:27 +03:00
parent ce5bf0fe0e
commit be74b4de6f
14 changed files with 34667 additions and 33621 deletions

12
changes/asset-packs.md Normal file
View file

@ -0,0 +1,12 @@
bump: minor
type: feat
**Asset packs.** `ludic pack` writes every asset a game opens into one `.lpak`
beside the binary, and the runtime mounts it before `main`. Nothing about how a
game is written changes: the pack is spliced in at `file_open`, the one place
every asset comes through, so `gltf_load`, `tex_load`, `Audio.load`,
`Fs.read_text` and the renderer's own shader loads all find it without knowing
it exists. `Fs.exists` and `Fs.size` consult the packs too. Entries are stored
rather than compressed and the pack is `mmap`'d, so 165 MB of terrain costs one
syscall at startup and pages in only what is touched. Several packs can be
mounted in order, and a later one shadows an earlier one - which is how a patch
replaces individual files without rewriting the base pack. See `docs/SHIPPING.md`.

9
changes/boot-splash.md Normal file
View file

@ -0,0 +1,9 @@
bump: minor
type: feat
**A boot splash**, customised by the game. `app splash` in `package.ludic` names
the artwork and `app splash_bg` the colour behind it; the runtime raises a
borderless window from a constructor that runs before `main`, reading the image
out of the asset pack, so it is on screen while the process is still starting
rather than after the slow part it exists to cover. Nothing hides it
automatically - only the game knows when its first real frame is ready, so the
game calls `App.splash_hide()`.

9
changes/macos-bundle.md Normal file
View file

@ -0,0 +1,9 @@
bump: minor
type: feat
**`ludic bundle`** turns a built game into a real macOS `.app`: `Info.plist` and
`PkgInfo` from the manifest, an `.icns` built from one source PNG at every size
macOS asks for, the asset pack in `Contents/Resources`, and an ad-hoc signature
so it launches on Apple silicon. Everything comes from `app` lines in
`package.ludic`, so the command takes no arguments. A bundled game is also moved
to `~/Library/Application Support/<name>` at startup, because Finder starts a
`.app` with its working directory at `/` where no save could ever be written.