Compare commits

..

No commits in common. "main" and "v0.16.2" have entirely different histories.

2137 changed files with 180783 additions and 709575 deletions

View file

@ -35,7 +35,7 @@ jobs:
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
git log --oneline -1 git log --oneline -1
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC. # See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV" echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV" echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
- name: Bootstrap x from the seed - name: Bootstrap x from the seed

View file

@ -48,7 +48,7 @@ jobs:
# LUDIC_CC so every clang invocation — the seed bootstrap, `ludic-dev build`, # LUDIC_CC so every clang invocation — the seed bootstrap, `ludic-dev build`,
# and each compiled test program — picks it up. Absolute path so it # and each compiled test program — picks it up. Absolute path so it
# still resolves if a step changes directory. # still resolves if a step changes directory.
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV" echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV" echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
- name: Bootstrap the toolchain from the IR seed (clang only) - name: Bootstrap the toolchain from the IR seed (clang only)

View file

@ -63,7 +63,7 @@ jobs:
# tiny C-free IR shim supplying the Darwin stdout/stderr globals over # tiny C-free IR shim supplying the Darwin stdout/stderr globals over
# glibc's, injected through LUDIC_CC. docs-gen is a pure CLI (no # glibc's, injected through LUDIC_CC. docs-gen is a pure CLI (no
# windowing), so the C-free bootstrap is all it needs. # windowing), so the C-free bootstrap is all it needs.
export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll"
export LUDIC_HOME="$(pwd)" export LUDIC_HOME="$(pwd)"
mkdir -p bin mkdir -p bin
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc

View file

@ -49,7 +49,7 @@ jobs:
git checkout "$TAG" git checkout "$TAG"
echo "TAG=$TAG" >> "$GITHUB_ENV" echo "TAG=$TAG" >> "$GITHUB_ENV"
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC. # See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV" echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV" echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
- name: The tag, VERSION and CHANGELOG must agree - name: The tag, VERSION and CHANGELOG must agree

1
.gitattributes vendored
View file

@ -1 +0,0 @@
packages/*/lib/** filter=lfs diff=lfs merge=lfs -text

6
.gitignore vendored
View file

@ -26,7 +26,6 @@ tools/editors/vscode/node_modules/
tools/editors/vscode/*.vsix tools/editors/vscode/*.vsix
tools/editors/jetbrains/.gradle/ tools/editors/jetbrains/.gradle/
tools/editors/jetbrains/build/ tools/editors/jetbrains/build/
tools/editors/jetbrains/.kotlin/
# IntelliJ plugin SDK sandbox (tools/editors/jetbrains) # IntelliJ plugin SDK sandbox (tools/editors/jetbrains)
.intellijPlatform/ .intellijPlatform/
@ -55,8 +54,3 @@ __pycache__/
assets/polyhaven/hdri/ assets/polyhaven/hdri/
assets/polyhaven/textures/ assets/polyhaven/textures/
assets/polyhaven/models/ assets/polyhaven/models/
# `ludic run` beside an example writes its binary into a build/ there
examples/**/build/
# a package native/build.sh writes its objects under the package (phase 15)
packages/*/build/

View file

@ -7,195 +7,6 @@ change type. Preview the next one with `ludic dev release --dry-run`.
A released section may carry a hand-written summary paragraph above its groups A released section may carry a hand-written summary paragraph above its groups
(v0.2.0 has one); the generated bullets below it are not edited by hand. (v0.2.0 has one); the generated bullets below it are not edited by hand.
## v0.22.0 — 2026-09-18
### Features
- **Every key on the keyboard is bindable** — the F-row, Home/End/PageUp/PageDown,
Insert/Delete, Caps Lock and the numpad reach a game as codes 132-166.
Until now the platform gave them no code at all, so a game's rebinding screen could
not take one and nothing said why. Windows asked the active layout what they type and
got nothing back (`w_vk_char` answers 0 for a key with no character); macOS let them
fall through to `charactersIgnoringModifiers`, which reports `NSF1FunctionKey` and its
neighbours at 0xF704 and up - outside the 256-bit held set either way.
- **The codes are the same on both platforms**: 132-143 are F1-F12, 144-149 are Home,
End, PageUp, PageDown, Insert and Delete, 150 is Caps Lock, 152-161 are the numpad
digits and 162-166 its `*`, `+`, `-`, `.` and `/`. The numpad's Enter is Enter.
- **`Key.F1`, `Key.Home`, `Key.Numpad0`** and the rest fold at compile time like the
other named keys.
- **`Input.key_label`** names them - "F1", "Home", "CapsLock", "Num 0", "Num /" - and
does *not* ask the layout, because a key that types nothing is called the same thing
on every layout.
## v0.21.1 — 2026-09-17
### Fixes
- A windowed game opens under its package's `app name`. `ludic build`, `ludic run` and
`ludic bundle` pass it to the compiler (`ludicc --title <name>`), so `game_title()` - the
window's first title - is the name the player knows rather than the `program` name, which
showed for up to two seconds before the renderer retitled the window. Without an `app name`
it is the `program` name, as before.
- `Crypto.random_bytes`, `Crypto.random_hex`, `Crypto.random_u32` and `Uuid.*` draw from the
system CSPRNG on Windows (`RtlGenRandom`, advapi32). They read `/dev/urandom`, which Windows
does not have, and every byte came back zero.
## v0.21.0 — 2026-09-17
### Features
- **A launcher's runtime** — start a program, step aside while it runs, and download large
files with a progress bar.
- **`Process.*`** — `Process.spawn(path, args)` starts a program directly (no shell) with this
process's environment and working directory and returns at once; `Process.poll(h)` is `-1`
while it runs, then its exit code (`128 + signal` for a signal death, `137` after
`Process.kill`); `Process.free` lets a handle go. `posix_spawn` on macOS
(`runtime/native/process.ll`); `CreateProcessW` on Windows (`process_win.ll`), with each
argument quoted by the MSVC rules and no console window. Linked only into a program that
uses `Process.*`.
- **`Http.save_to(h, path)`** — before `Http.send`, streams the response body straight into a
file (written at `path` itself, created on send) instead of memory; `Http.text` of such a
request is empty and `Http.body_len` is the bytes written. **`Http.received(h)`** and
**`Http.expected(h)`** (the `Content-Length`, or `-1`) are live while it is pending. macOS
streams through an `NSURLSession` with a delegate built at run time, Windows through the
WinHTTP read loop. Freeing a pending request now cancels it and parks its slot until the
worker has let go, rather than freeing what the worker is still writing.
- **`App.window_hide()` / `App.window_show()`** — take the game's window off the screen and
bring it back without closing it or its GL / Vulkan surface; the run goes on while it is
hidden. No-ops headless and before there is a window.
### Fixes
- A game's window takes the title it asks for. The runtime opens a game's window before `main`,
titled with the `program` name, and `Gl.open` / render3d's `gl_open` and `gvk_open` attached
to it without passing their title on, so a `program Valley` that opened "Maroon Lake" showed
"Valley". `win_set_title` (cocoa.ll `setTitle:`, win32.ll `SetWindowTextW`, UTF-8 to UTF-16)
now retitles it, and a second `win_open` on macOS retitles the open window instead of making
another, as it already did on Windows.
## v0.20.0 — 2026-09-17
### Features
- The JetBrains plugin (1.6.0) has a Ludic tool window for the package, like the Gradle one:
- an overview of the package (what it is, entry, version, dependencies, scripts, hooks, app);
- its commands, scripts and hooks, run by double-click, with each command's hooks shown;
- its dependencies against package.lock.ludic: the locked version, whether it is fetched
and linked, the indirect ones, and Fetch, Update, Verify, Vendor, Add and Remove;
- the app preview the `app` lines describe, and the asset roots with missing ones marked.
The bar over package.ludic now appears only when something needs doing (fetch, or no
toolchain), instead of carrying every command.
## v0.19.0 — 2026-09-17
### Features
- `Udp.*`: polled IPv4 datagrams - the transport under a game's own netcode. `Udp.open(port)`
binds a non-blocking socket (port 0 picks one, `Udp.port` says which), `Udp.send` sends one
datagram to an address and port, and `Udp.recv` drains what arrived without ever blocking;
`Udp.from_ip` / `Udp.from_port` name the sender. `Udp.ip` / `Udp.ip_text` convert addresses,
`Udp.resolve` looks a host name up and `Udp.local_ip` is the address a player on the same
network would dial. BSD sockets on macOS (`runtime/native/udp.ll`), Winsock on Windows
(`udp_win.ll`, linked with ws2_32), linked only into a program that uses `Udp.*`.
## v0.18.1 — 2026-09-16
### Fixes
- The release build links libm on Linux, which `float`/`double` code needs (glibc keeps
`sinf`, `sqrtf` and the rest there). 0.18.0's release job stopped on this before
publishing, so 0.18.1 is the first published build with float types, barrel imports,
package scripts and hooks, and the JetBrains 1.5.0 support.
## v0.18.0 — 2026-09-16
### Features
- Run a single test by name: a compiled test program takes the test's exact name as its
first argument (`./tests "adds"`), and `ludic test` gains `--test NAME` to pass it through.
A name that matches no test is reported and fails the run. `ludic test --verbose` (`-v`)
prints every test's `ok`/`FAIL` line, framed by `RUN <file>` … `PASS|FAIL <file>`, which is
what editor test views parse.
- The JetBrains plugin covers much more of the IDE:
- Run configurations for `ludic run`/`build`/`test` (plus any other `ludic` command), with
gutter run buttons on `program` and on every `test "name"` block.
- Test results appear in the IDE's test tree, with navigation back to each test block.
- `file.ludic:line` locations are clickable in every console.
- Smart indentation on Enter and when typing a closing bracket; quotes pair up.
- A Color Scheme page and a Code Style page.
- Live templates, New Ludic File templates, and a Ludic project in File | New | Project.
- A Tools | Ludic menu, and a warning when the toolchain is missing.
- `package.ludic` is treated as a package manifest:
- a banner offers Fetch, Update, Verify, Run, Test, Build and Bundle, and warns when the lock is
missing or older than the manifest;
- gutter actions on `package` and `require` lines;
- completion and quick documentation for directives and `app` keys;
- its own icon.
- `script` and `hook` lines complete and are validated; each script has a run button, the
package banner has script shortcuts, and Tools | Ludic | Run Script… lists them all.
- The toolchain's and the project's packages are listed under External Libraries.
- The Structure view, parameter info and code-block navigation now come from `ludic-lsp`.
- The plugin is now 1.5.0 and requires IntelliJ Platform 2024.2 or newer and LSP4IJ 0.21.
- Fixed: Enter no longer inserts an extra `}`, and Comment Line no longer writes two spaces.
- `float` and `double`: IEEE floating point with ordinary operators, so renderer math reads
`a * b + c` instead of `f_add(f_mul(a, b), c)`.
- Decimal literals take their type from context (`let s: float = 0.1` is exactly 0.1), and
stay `fixed` elsewhere, so existing programs keep their meaning.
- `int`/`long` promote; `float(x)`, `double(x)`, `int(x)`, `long(x)` and `fixed(x)` convert.
- `floats(n)` / `doubles(n)` buffers; float fields, globals, constants and parameters.
- `Math.*` computes in float when given one; `string`/`print`/interpolation write the
shortest round-tripping decimal; `float_bits` / `float_from_bits` expose the IEEE pattern.
- `@deterministic` code may not use floats (it is now checked).
- `import "camp"` imports a directory through its barrel, `camp/index.ludic` (a fragment
listing the directory's own imports). A directory without one is a clear error. Packages
resolve the same way: `import "ludic.render3d"` reads `ludic.render3d/index.ludic`.
- `ludic-lsp` navigates like the compiler resolves:
- Package imports (`import "ludic.render3d/r3d.ludic"`) resolve from `ludic_modules/` and the
toolchain's `packages/`, so their names hover, complete and jump. Import strings are links.
- `var` locals are tracked (declaration, usages, rename).
- Hover shows the declared or inferred type of locals, parameters and globals.
- Hover and go-to-definition on built-ins, built-in types and namespaces show the reference
page, with a link to the online docs. Release archives now ship `docs/language` for this.
- Go to type declaration (`textDocument/typeDefinition`) is supported.
- Signature help carries per-parameter ranges and follows named arguments.
- Completion inside a call offers the parameter names not given yet, `Namespace.` offers its
members, and results are ranked: locals first, then globals, built-ins and keywords.
- `package.ludic` gains `entry`, scripts and lifecycle hooks:
- `entry "src/game.ludic"` names the program `ludic run`/`build`/`bundle` compile. Without
it, the one file under `src/` that declares a `program` is used.
- `script "dev" "ludic run --headless"` defines a command: `ludic dev` (or `ludic script dev`)
runs it with any extra arguments, and `ludic scripts` lists them.
- `hook before|after <command> "…"` wraps `build`, `run`, `test`, `bundle`, `pack`, `get`, …
or a script. A failing `before` hook stops the command; `after` hooks run only on success.
- Quoted manifest values accept `\"` and `\\` escapes.
### Fixes
- **`==` / `!=` on records is identity again** — only two strings compare by content.
Every pointer-typed comparison used to be lowered to a C-string content compare,
so two distinct records (properties, slices, enums) compared their bytes up to
the first zero byte: `a == b` could be true for different objects that shared a
leading field, depending on layout. References now compare by identity
(`icmp eq ptr`); `string` (and untyped `pointer`/`pointers` text) still compares by content,
and comparing a `string` with a non-string reference is a compile error.
## v0.17.0 — 2026-09-16
### Features
- **Renderer switches stay out of shipped games** — every `R3D_*` environment switch in
`ludic.render3d` (debug views, feature kills, file writers, hardware fakes, the Windows
feature overrides) now goes through one gate: a headless build honours them as before, a
windowed build only when `R3D_DEV` is set to anything but `0`. See `docs/SHIPPING.md`.
### Fixes
- **A lake can be the water that mirrors the world** — a reflecting body is clipped to its
ellipse like any other unless it is an unbounded sea, so a map whose reflection belongs to
its lake no longer floods every hollow in the survey with that lake's level; and the
terrain's wet shore, forest and scree gates read the carved lake's line inside its outline
(`u_lake`), the rule the grass already used, so a lake above the sea keeps its wet bank and
no forest is painted down its bed.
`terrain_lake_carve(false)` keeps a lake's line, outline and shore but skips carving its
bed, for a height map that already carries a shaped one.
## v0.16.2 — 2026-09-15 ## v0.16.2 — 2026-09-15
### Fixes ### Fixes

View file

@ -25,7 +25,7 @@
> ```bash > ```bash
> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic > # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic
> mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc > mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
> bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev > bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
> bin/ludic-dev build # the whole toolchain into bin/ > bin/ludic-dev build # the whole toolchain into bin/
> # (ludicc, ludic, ludic-fmt, ludic-lsp) > # (ludicc, ludic, ludic-fmt, ludic-lsp)
> bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly > bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly
@ -73,9 +73,6 @@ point at a different LLVM toolchain if you have one.
| a windowed native executable | `ludicc game.ludic -o build/game` | | a windowed native executable | `ludicc game.ludic -o build/game` |
| a headless executable | `ludicc game.ludic --headless -o build/game` | | a headless executable | `ludicc game.ludic --headless -o build/game` |
| the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` | | the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` |
| the schema an editor reads (records, registries and their entries, consts) | `ludicc src.ludic --emit-schema schema.json` |
| every error, as a JSON array on stdout | `ludicc src.ludic --check --diagnostics=json` |
| the same, with an unsaved buffer on stdin standing for one of its files | `ludicc src.ludic --check --diagnostics=json --stdin-file lib/a.ludic < buf` |
| a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` | | a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` |
| a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` | | a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` |
| an object file † | `ludicc src.ludic -c -o src.o` | | an object file † | `ludicc src.ludic -c -o src.o` |
@ -353,7 +350,6 @@ The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept:
--windowed force a windowed (Cocoa) build --windowed force a windowed (Cocoa) build
--headless force a headless build (stdin input, out.ppm output) --headless force a headless build (stdin input, out.ppm output)
--emit-llvm stop at LLVM IR — write it and exit, no clang --emit-llvm stop at LLVM IR — write it and exit, no clang
--check every check a build makes (types, modules, uses, layers, ports, binds); write nothing
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error --fmt lex + parse only; exit 0 if it parses, 1 on a parse error
(the check-docs gate; canonical formatting not yet restored) (the check-docs gate; canonical formatting not yet restored)
--save-temps keep the intermediate .ll --save-temps keep the intermediate .ll

View file

@ -19,7 +19,7 @@ From a clean checkout, one line lifts the toolchain off the seed:
```bash ```bash
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
``` ```
That gives you `bin/ludic-dev`, the contributor tool: it replaces every That gives you `bin/ludic-dev`, the contributor tool: it replaces every

File diff suppressed because it is too large Load diff

View file

@ -144,7 +144,6 @@ cached in a content-addressed store:
```bash ```bash
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/ ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
ludic get # install from package.ludic, write the lock ludic get # install from package.ludic, write the lock
ludic remove git.workshopsoft.io/user/pkg # the inverse of add
ludic verify # check locked packages against the store ludic verify # check locked packages against the store
``` ```

View file

@ -1 +1 @@
0.22.0 0.16.2

View file

@ -1,8 +0,0 @@
bump: patch
type: fix
`Actor.cast_hidden` separates being DRAWN from CASTING. `ac_visible` rejected any hidden
actor from the shadow pass as well as the scene pass, so a game that hides the player's
own body - first person, or a viewfinder held to the eye - lost that player's shadow
entirely. An actor hidden because the camera is inside its head is still standing in the
sun; set this on it and it keeps its shadow. Everything else still stops casting when it
is hidden.

View file

@ -1,17 +0,0 @@
bump: patch
type: fix
A leaf is not matte.
Foliage roughness was pinned to 1.0 and grazing Fresnel switched off, so nothing green in
the game had a highlight anywhere: the glint off waxy leaves and wet needles, which is
most of what makes a real stand look alive rather than painted, was simply absent.
The reason it was switched off is real. A crown is card quads, and at a grazing angle the
card's normal is a lie, so a plain specular lobe frosted whole crowns white against the
sky. So the sheen comes back as its own term gated on exactly that: it fades out as the
card turns edge-on, which is where its normal stops meaning anything. A tight lobe for the
glint, a weak wide one for the waxy rim, and nothing at all at the angles that frosted.
Thin-leaf translucency reaches further with it - a backlit stand glows for as far as you
can see it, not 140 m - and a dense crown passes 0.45 of it rather than 0.3. The distance
cap that stops a two-pixel clump card becoming a lime disc stays.

View file

@ -1,28 +0,0 @@
bump: minor
type: feat
Anti-aliasing that exists, and a lens for the viewfinder.
THE SHIPPING DEFAULT HAD NO ANTI-ALIASING AT ALL. The temporal resolve was removed (for
good reasons - it reprojected water through the surface plane and dragged the mirror image
behind the camera), the setting's first option went on saying "Temporal", and MSAA defaults
to one sample. Every machine without DLSS - which is every Mac - drew a frame full of grass
blades and needle cards with nothing smoothing a single edge.
FXAA now, in the sharpen pass, because that pass already reads this pixel's neighbourhood
and runs last on the LDR image. It has no history, so it cannot drag or smear a reflection.
Measured on edge pixels: 25.5% less single-pixel staircase.
One trap worth recording. The unsharp mask's delta is computed from the RAW image and only
then applied to the anti-aliased colour. Taking the centre from the FXAA result and the
neighbours from the raw texture measures a difference that is half smoothing and half
signal, so the mask sharpens exactly the edges FXAA just softened - measured, that first
version was 29% WORSE than no anti-aliasing at all.
And depth of field, for the photo mode: a disc of taps whose radius is the pixel's circle
of confusion, signed so the two sides of the focal plane differ and normalised by the focus
distance, because a lens focused at two metres throws a background out far harder than one
focused at two hundred. A tap only counts if it is at least as out of focus as the pixel
it is blurring into, which is what keeps a sharp foreground from haloing into a blurred
background. It runs between the scene and the bloom so a blurred highlight still blooms,
and the whole pass is skipped when the aperture is shut - in ordinary play there is no lens
and this never draws.

View file

@ -1,11 +0,0 @@
bump: minor
type: feature
**Actions and reducers.** `action PickUp { item: int }` is a typed record of something that
happened; `reducer Bag on PickUp(b: mut Bag, a: PickUp) { ... }`, in the module that owns the state,
says what it means for that one state - a reducer takes exactly its state and the action, and a
second state is refused; `dispatch PickUp { item: 7 }` queues one from anywhere. The queue is drained
at the end of every phase of the frame loop, after every phase of ludic.base's `core_tick_all`, and
where a program calls `drain_actions()`: in dispatch order, each action's reducers in the order of
their states' names, an action a reducer dispatches queued behind (a queue still growing after 64
rounds stops the program, naming the action). `ludic deps` reports `widest_function` - the most
states any function or entry point of the program takes - and `--check` ratchets it.

View file

@ -1,15 +0,0 @@
bump: minor
type: feat
An aspen leaf hangs on a flattened stalk and turns in air a spruce never feels, and the
kit had no way to say so. `layer_flutter(l, v)` gives a scatter layer a per-leaf tremble:
the vertex stage offsets each leaf by a phase taken from its own place on the card, so
neighbouring leaves are never in step, and writes the result out as a varying the
fragment stage uses to flash the leaf's pale underside as it turns. The flash is the part
that reads - a still frame of a tremble is a still frame of nothing. One uniform, one
varying, no extra pass, and every other layer leaves it at zero.
Also fixes the sway itself, which was measured in METRES: `hgt * hgt * 0.35` is right for
a 40 cm flower and puts ten metres of sideways into a 14 m trunk, so every tall tree in
the valley stood bent over like a fishing rod. It is a fraction of the model's own height
now, so the tip moves a few per cent of the tree whatever the tree is and the base does
not move at all.

View file

@ -1,9 +0,0 @@
bump: minor
type: feature
**`ludic.anim` carries ozz-animation (0.17.0, MIT) as a native library**, the second package to do
so after `ludic.physics`. Its skeleton and each clip are built at LOAD from the numbers the package
already reads - a skin's parents and rest pose, a clip's flattened channels - so there is no bake
step and no new file. `anim_oz_skel(sk)`, `anim_oz_clip(c)`, `anim_oz_ctx(s)` and
`anim_oz_sample(ctx, clip, t, rot, pos, n)` are the first step (Maroon Lake's phase 19.1): a sampled
rotation agrees with `anim_mix` to within 1e-4 a component. `anim_play` is unchanged. The library is
built by `native/build.sh` from the pinned release, and on Windows it imports KERNEL32 alone.

View file

@ -1,6 +0,0 @@
bump: minor
type: feature
**`@Asset(kind, map)`: a path under each map's directory.** A field whose file lives in the map's own folder
(a grass kind's density picture, `ground/blades.png`) says so, and `ludicc --check` looks for it in every
map - a @PerMap row's in its map, a game-wide row's in all of them - refusing a map that lacks it unless the
field is `@Asset(kind, map, optional)`. The schema marks the attribute `"scope": "map"`.

View file

@ -1,7 +0,0 @@
bump: patch
type: fix
**An attribute before `export` is kept.** `@ToClients export event E`, `@Sync export property P`,
`@Owned export model M` and the rest lost the attributes written in front of `export`: the parser read
them, then parsed the declaration afresh and forgot them - so an exported remote event was silently
local; and `export @ToClients event` was refused outright. Attributes and `export` now read in
either order into the same declaration.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**A bake's inputs can follow the data.** `bake_expand(inputs, map)` gives a Bakes row's inputs as they are
hashed: `{map}` put as the map's key, and each input with a `*` put as the paths it matches, sorted as whole
paths byte by byte, dot-names left out, a glob matching nothing gone. `bake_maps(first_input)` is the maps a
`{map}` row covers: each directory under assets/maps holding its first input (`bake_maps_in` under another
root). A runner hashes `bake_inputs_hash(bake_expand(row.inputs, map))`, the same path the check takes, so
the two cannot disagree on stale; a game's Python tools are its checked twin.

View file

@ -1,11 +0,0 @@
bump: minor
type: feature
**Bakes you can look at, and three more of the renderer's textures read from one.** `ludic.lab` writes
raw 8-bit pixels (1 to 4 channels) as a PNG (`lab_png_write`, `png_write.ludic`, importable alone with its
own `LabPngState`) and turns float textures into honest previews (`png_convert.ludic`: R32F min..max as
grey, RG16F as red and green x255, HDR RGBA16F as x/(1+x) then sRGB). `ludic.render3d` takes three bakes
the game names (`bake_load.ludic`) and makes each as before when one is missing or stale: an impostor's
atlases as BC7 with their baked mips, through the compressed upload (`impostor_from_baked`,
`impostor_fill_bc7`; a fog opening past its cards reads the bake again instead of painting), the sky's image-based light at the start yaw per prefilter width (`sky_baked_in`; any other
turn of the sky is convolved), and the grass carpet (`carpet_from_baked`, `carpet_bytes`).
`impostor_from_bytes` returns null, keeping nothing, when the bytes are not the impostor's shape.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**Textures a game baked at build time are read before the PNG.** `png_decode` first takes
`assets/baked/png/<path>.tex` (a game's `ludic bake` output: the samples, ready to upload), and a cut-out
load (`tex_load_ex` with dilate) first takes `assets/baked/cutouts/<path>.bc7` (padded as `tex_dilate`
pads, then BC7 with its mips) - so a boot decodes, pads and converts nothing it can take ready-made. Both
are ludic.base's baked form, read by hand (baked_tex.ludic: the "LBAK" header, the key and version); a
missing or stale one falls back to the PNG as before. `tex_load_dds_at` reads a .dds at an offset.

View file

@ -1,5 +0,0 @@
bump: minor
type: feature
**`bind Purse { money: g_money }` - a port member bound to a variable.** A member that takes nothing
may name a global instead of a function; the compiler writes the getter in the bind's file, so the
one-line wrapper is gone. A member that takes something is refused a variable.

View file

@ -1,7 +0,0 @@
bump: patch
type: fix
**`ludic build` keeps its LLVM IR out of the project.** The intermediate `.ll` was written beside
the binary (`build/<name>.ll`) and deleted after linking, so a project's tree held one for the
length of every build, and two builds at once deleted each other's - which surfaced as a
`clang: no such file` that read exactly like a compile error. It now goes to the run's own
temporary directory and goes with it. `--save-temps` still keeps it at `build/<name>.ll`.

View file

@ -1,9 +0,0 @@
bump: patch
type: fix
**A function named like a built-in a call always takes is refused.** `function words(st, k)` compiled,
and every call to it became the built-in `words(n)` - n zeroed ints, with a pointer for n - and LLVM
refused the IR far from the cause. A top-level function whose name a call always takes as the
compiler's own (`words`, `keep`, `print`, `save`, `load`, `key`, ...; the table is
`selfhost/check/check_builtins.ludic`, held to `emit_call` by `ludic-dev syntax --check`) is now an
error at its declaration, as `run` already was. Every other built-in (`buffer`, `floats`, `double`,
...) yields to a function the program declares, in the checker as it already did in codegen.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**`ludic build --check` / `ludicc --check`: check without building.** The parse, the type checker
and the module rules (`export`, `uses`, layers, ports, registries) run, and nothing is emitted or
linked - about three seconds on Maroon Lake where a build takes about a minute. In this mode the
checker asks the module rules at each reference it resolves, since the emitter that usually asks
them does not run.

View file

@ -1,9 +0,0 @@
bump: minor
type: feat
**Check an unsaved buffer.** `ludic build --check --diagnostics=json --stdin-file <path>` (and
`ludicc --check --stdin-file <path>`) checks the program as usual, but wherever the compiler would open
`<path>` - the entry, an import reached through a barrel, a component's `.xml` / `.lss`, an `.lres` -
it reads the text on stdin instead, so an editor's diagnostics follow typing without a save. Paths are
matched after normalising both (separators, relative to the working directory, `.` / `..` folded).
Diagnostics carry the file's usual name with lines and columns in the buffer; a `<path>` the program
never opens is reported as one warning.

View file

@ -1,7 +0,0 @@
bump: patch
type: fix
**A chunked table's keys are unique across its map, and not interned.** A row's id is `(map, key)`, so a
tree moved into another chunk keeps it, and `ludicc --check` refuses a key written in two of a map's
chunk files, naming both. The keys are no longer interned: interning every key a player walked past would
have filled the bounded intern table and kept them all for good. A chunk slot keeps its keys in its own
buffers, rewritten in place when the slot is refilled; `intern(row.key)` keeps one past `_out`.

View file

@ -1,9 +0,0 @@
bump: patch
type: feat
Things sit ON the ground rather than hovering over it. The screen-space GI pass takes
a second, much tighter set of taps (a 0.40 m radius that grows with distance, with a
range check so a far surface behind a near one cannot darken it) and folds the result
into the ambient occlusion it already had. The wide radius answers "how enclosed is
this", which a trunk meeting grass barely registers; the tight one answers "is
something touching here", which is the shadow the eye looks for to place an object.
It is eight taps on a buffer the pass had already bound.

View file

@ -1,9 +0,0 @@
bump: minor
type: feature
**Namespaces are declared in Ludic.** `alias meth(labels) = target` in a `namespace` block makes
`Ns.meth(...)` a call to `target`, taking named arguments by those labels; with no label list the
target's own parameter names are the labels. The engine's 41 table-driven namespaces - `Http`,
`Udp`, `Process`, `Json`, `Value`, `Screen`, `Input`, `Audio`, `World`, `Tiled` and the rest, 438
methods - moved out of the compiler into `runtime/native/namespaces.ludic`, and a package owns an
API the same way. The code a program compiles to is unchanged byte for byte, and the checker now
checks an alias call's arguments against its target.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**`def Recipes from "recipes.lres"` - a game fills a package's open registry from its own resource
file.** The entries are checked against the registry's record as the file is read, with errors at
the resource file's line, and they are defs of the module that wrote the line: the registry must be
open to it, and they sit in the stable order (the declaring module's entries, then other modules'
by name, and file order within a file).

View file

@ -1,134 +0,0 @@
bump: minor
type: feature
**Default parameters, components, views and templates.** A parameter can have a default (`pad: float = 8.0`).
A call leaves out what it does not change, and may pass its first arguments by position and the
rest by name.
A `view` declaration is the one bridge between a program and its UI. It names the fields a
template may read, the functions it may ask and the `on` events it may send, and it writes
`view_<name>() -> UiView`.
A `component Name { prop, state, fields, functions, on events }` declaration beside `Name.xml` and
`Name.lss` is a UI component. Its template and styles are compiled in (with `@import` inlined), each
mounted instance keeps its own props and state, styles are scoped to it, and a parent's `class`,
`style` and `id` land on its root.
`ludic.ui` is now a template runtime. Screens and components are XML files loaded at run time,
with:
- `{expression}` bindings;
- `<if>`, `<else>` and `<each>`;
- props, `<slot/>` and per-instance `<state>`;
- `on-press` actions that send events, `set` state or `emit` to the component's user;
- component libraries (`export="true"`, `<import src as>`);
- HTML's elements (`div`, `p`, `h1`-`h6`, `ul`/`li`, `img`, `hr`, ...), with a default stylesheet;
- HTML's attributes: `id`, `class`, `style`, `hidden`, `disabled` and `onclick`, with any other
attribute kept for selectors;
- the CSS box model (padding and margin in 1-4 values, borders, `px` and `%`) and flex layout
(`flex-grow`, `justify-content`, `align-items`/`align-self`, `flex-wrap`, min and max sizes)
under CSS's property names;
- stylesheets, in a `<style>` or an `.lss` file (a Ludic StyleSheet) that others import and that
can `@import` more;
- CSS's selectors: `#id`, compound classes, `[attr=value]`, descendant and `>` combinators,
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
specificity.
- more CSS: custom properties and `var()`, `position` with insets and `z-index`, `em`/`rem`/`vw`/`vh`,
`@media`, wrapping text and ellipsis, `overflow`, `+`/`~`, `:nth-child(an+b)`, `:checked`, `:active`;
- more React: keyed lists, `<let>`, `<provide>` context, `<fragment>`, named slots, `on-mount` and
`on-unmount`;
- native elements a program draws itself (`ui_native`, `ui_fire`), and form controls;
- errors with file and line, hot reload (`ui_reload`), and an inspector-style dump.
The runtime is a UI framework, not only a template engine:
- it takes input itself: focus and keyboard navigation, the pointer, scroll boxes, `autofocus`;
- it has built-in controls (button, checkbox, radio, range, select, text, key), styled as CSS
parts;
- `ludic.ui/render3d.ludic` is a render3d backend, with textures, atlases, nine-slices, clipping
and scale;
- more CSS: `rgba()`/`#rrggbbaa`, `border-radius`, `outline`, `box-shadow`, `background-image`,
`border-image`, group `opacity`, `@keyframes` / `animation` / `transition`;
- HTML mixed content, and boolean attributes;
- `popover` (a top layer that keeps the pointer and keys, with light dismissal), `title` tooltips,
and `<progress>` / `<meter>`;
- importing `ludic.ui/render3d.ludic` installs the backend, and atlases take rows;
- hooks for the program's language, sounds and clock.
What a game's screens found missing, now in `ludic.ui`:
- `<input type="number" min max step>`: typed digits, Enter or leaving it commits them clamped, the
arrows step it;
- `<input type="key">` listens for any key (Tab and the arrows included) once Enter or a click starts
it; Esc stops it, Backspace clears it, `shown` names the value, and `ui_capturing()` tells the host;
- `note="..."` under any control's label (`.ui-note`); a range's `decimals`, `format="percent"` and
`unit`; a track laid out as a row, with the range's fill as tall as it;
- popovers anchored beside an element (`anchor="id"`, or a bare `anchor` for the element before it,
`placement`), flipped to the other side and kept on the screen;
- `flex-shrink` (a scroll box in a column takes the room its siblings leave), `flex: grow shrink`,
`order`, and text in a row wrapping in the room its siblings leave;
- `calc()` over px, %, em, rem, vw, vh and `var()`; `width: 0` and `height: 0` mean 0;
- `text-shadow`; tooltips of several lines; `ui_opacity()` for a native's draw;
- `border-image` drawn as painted with no background colour, tinted by one, and not at all under
`transparent`; a picture file drawn untinted (an atlas cell still takes `color`);
- the render3d backend loads a picture again when its file changes (`ui_image_reload`), draws a path
with a drive letter as a path, and slices a nine-slice by its texture's own width and height;
- a component root that is itself a component takes every user's class, style and id, and the
sheets that style it are weighed together by specificity;
- a component's event may be called `set`; a `string` prop given a number reads it as text; two
components of one name are an error naming both files;
- the scrollbar is `.ui-scrollbar` and `.ui-thumb`: a press on the thumb holds it where it was taken,
a press on the track jumps the thumb's middle there, and neither presses what is under the bar;
- a popover's own controls take its presses whatever lies under it, a press outside only closes it,
and while one is up the scroll boxes outside it do not take the pointer;
- the first gamepad moves the focus (d-pad, left stick), steps ranges and selects, and presses (A)
and goes back (B); a held direction, on the pad or the arrow keys, repeats after 0.42 s and then
every 0.11 s on the ui clock (`UiInput.held_*`, `pad_a`, `pad_b` for a host);
- pointer events: `on-pointerdown` / `pointermove` / `pointerup` / `drag` / `wheel` with `event.x`,
`y`, `dx`, `dy`, `button` and `wheel`, and `ui_native_input(tag, fn)` for a native; a press captures
the pointer until release; the pointer hits the topmost element in painting order, and
`pointer-events: none` lets it through;
- `on-down` and `on-up` on a button (the pointer, Enter or A), with `:active` true while it is held
there rather than whenever the pointer is down over it;
- an anchored popover's `align="start|center|end"`, and `within="id"` (by default the nearest
scroll box around it) for the bounds it is flipped against and kept inside;
- `text-fit: shrink MIN` shrinks a line to its box, then cuts it with an ellipsis; `line-height`;
an `em` reads the font size the element ends with (a `font-size` later in the rule, or in a later
rule), not the one it had so far;
- `min()`, `max()` and `clamp()`, in `calc()` or on their own; `top` / `right` / `bottom` / `left`
as a percentage or a `calc()` of one, of the containing block;
- a nine-slice's corners are clamped to half the box in each direction on its own and cut on whole
pixels (`ui_nine_cuts`), so a small key cap has no seam;
- `scroll-top="{px}"` holds a scroll box at an offset, with `on-scroll` when the player moves it;
`ui_scroll_set(id, px)` moves one once;
- `linear-gradient(...)` backgrounds; `aspect-ratio`; `object-fit` for pictures (the renderer's
`image_w` / `image_h`) and `ui_object_fit` for natives;
- `translate="no"` keeps an element's text as written; a title of several lines is translated whole,
else line by line;
- `<input type="key">` takes a mouse button (`UI_MOUSE_LEFT` / `RIGHT` / `MIDDLE`, 256-258) and is
`:capturing` while it listens;
- a `title` shows for the keyboard's focus too, after the same half second; a focus ring drawn
through a renderer with no `rect` no longer crashes;
- a component with no stylesheet of its own reads a theme's `:root` variables from around it (it
did; now a test says so);
- `ui_scale()` and `ui_box("id")`, the scale and an element's laid-out box, for a host;
- `on-submit` on a text field (Enter or A; the focus and text stay unless `clear-on-submit`);
- `zoom` on any element, and a length over a length in `calc()` is a plain number;
- `on-hold` every frame a button is held, with `event.dt` and `event.t`;
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
frame rate).
render3d gains `tex_width` / `tex_height`, and the XML reader keeps text runs among elements in
order (`mixed`).
A `view` field set to a literal or a named function's result needs no type.
Screens are drawn through a registered renderer. `Value` gains a float kind.
Also:
- A program's function named like one of the runtime's is refused; it had been silently taking the
runtime's own calls. So is one named like a compiler built-in (`run`, `exit`, `free`, `fill`,
...): every call to a program's own `run` compiled into C's `system()`, and clang failed on the IR.
- An index is evaluated before the slice's elements are read. A `xs[f()]` whose `f` grew `xs`
read stale memory.
- A runtime error names the file its expression is in, not the program's.
Two declarations with one name (a package's private global and a program's, say) are reported as
such before type checking. They used to surface as a page of type errors about the wrong type.

View file

@ -1,6 +0,0 @@
bump: patch
type: feature
**`ludic deps --writes` warns about a write through a local alias.** `let t = thing_cur` and then
`t.used = 1` writes another module's record just as `thing_cur.used = 1` does; a local bound straight
from another module's global (or from such a local) is now followed within its function and each
write through it listed as a warning. A reference that arrives from a function's result is not.

View file

@ -1,9 +0,0 @@
bump: patch
type: fix
**`ludic deps`: a reach counts every state apart, however the states are numbered.** The reach and
write-reach bitsets packed 60 states to a word, but an `int` is 32 bits, so `1 << 45` came back as bit
13 and states 32 apart shared a bit: a function taking both counted one, fewer than it takes, and the
counts (`widest_reach`, `widest_write_reach`, `--reach`, `--wreach`) rose and fell with how a program's
states happened to be numbered. The sets now hold 30 to a word. On Maroon Lake `widest_reach` goes
58 -> 76 and `widest_write_reach` 54 -> 64 - the real numbers, which the old count hid.
`examples/state/reach_wide.ludic` (40 states, S00 and S32 taken together) holds it.

View file

@ -1,10 +0,0 @@
bump: minor
type: feat
**`ludic deps` sees through fn values, and lists the widest functions.** A step list or a registry of
fn values takes no state and still reaches every state its steps take; `widest_reach` is the most
states any function can come to - by a call, a `fn f` it writes, or a global holding fn values it
reads - reported beside `widest_function` with how many of them it does not take itself
(`the widest reach: app_boot (src/app/boot.ludic:30), 72 states (72 through calls and fn values it
does not take)`). `--widest N` lists the N functions that take the most states with what each
reaches; `--reach N` orders them by reach. A baseline written before this has no `widest_reach` and
does not hold it until it is rewritten.

View file

@ -1,7 +0,0 @@
bump: minor
type: feat
**`ludic deps` says what a function can come to CHANGE** (`widest_write_reach`, and `--wreach N`
lists the functions by it): the states it reaches as `mut`, through calls, `fn` values and step
lists. Reach itself is sharper: `Port.member()` reaches that member's binding only, and
`Registry[i].field` (or a local holding `Registry[i]`) reaches that field only - a question asked of
a port or a table that also holds verbs no longer reaches the verbs.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**`ludic.devlink`: the interface's verbs.** A `DevlinkUi` port, every member defaulting to "not offered":
`ui_screen` (the screen's root class and its components), `ui_model "<Class>" "<out>"` and `ui_tree "<out>"`
(the game writes a component's model or the whole tree to a file, no `..`, and the answer names it, so a
datagram stays small) and `ui_override "<path>" "<file>"` (a template or stylesheet read from another file
and reloaded keeping state; `""` clears one, `"" ""` all). Answered at once; nothing made per frame.

View file

@ -1,9 +0,0 @@
bump: minor
type: feature
**`ludic.devlink`: an editor's live link into a running dev build** (protocol v1, frozen with Ludic
Studio). Loopback UDP through the `DevlinkNet` port, one request a frame parsed in place from one fixed
8 KB buffer and answered into another; every verb a `DevlinkWorld` member defaulting to "not offered":
`ping`, `hello` (the build's schema hash as 16 hex digits), `cam_get` / `cam_set` / `cam_release`, `goto`,
`map_load`, `time`, `weather`, `shot`, `pause` / `resume` / `step`, the slow three answered later by id.
A socket is opened only when `enabled()` says so (a game binds `dev_tools`), a sender off this machine is
dropped, and a connected co-op session refuses everything but `ping` and `hello`.

View file

@ -1,8 +0,0 @@
bump: patch
type: performance
**Cut-out edge padding runs on every core.** `tex_dilate`'s passes hand their rows, sixteen at a time, to
`Job.parallel_for`: within a pass a row writes only its own still-masked texels and reads only
neighbours the mask already let go, so the bytes are the ones the single-threaded loop made. The worker
is handed plain buffers in a `DilateJob` and makes nothing. `tex_dilate_bytes` is the slice-taking
form (safe_api.ludic), and `examples/rendering/dilate.ludic` holds the result against the old loop
(prints DILATE OK). It was 206 ms of the main thread in a Maroon Lake boot.

View file

@ -1,10 +0,0 @@
bump: patch
type: fix
**A dispatched action no longer allocates a record each time.** `dispatch A { ... }` made a fresh
record for the queue, and Ludic frees nothing, so a system dispatching every frame (an input's
`Move`, a frame's time) grew the program by a record a frame. The queue now keeps a list per
action: a dispatch takes the next one (making one only when all are queued), fills every field
as `new` would - given, or its default - and `drain_actions()` hands them all back once the queue
is empty. A reducer reads its action only during the drain, so nothing sees a record after it is
reused; keep what must last in the state, not the action. `ludic.base`'s `actions_test` holds a
reused record getting its defaults back.

View file

@ -1,8 +0,0 @@
bump: minor
type: feat
**A name is defined once, for every kind of declaration.** Two functions with one name were
already an error; two `var`s or `const`s (or an enum and a const), or two `property` / `event`
records, kept the first definition silently. A game lost months to it: two files both said
`KEY_LEFT`, one meaning an arrow key's code and one a binding slot, and the menus read the slot.
They are now an error that names both files and lines. `examples/rejected/` holds the two cases,
checked by a new `reject_case` in the test runner (an example the compiler must refuse).

View file

@ -1,9 +0,0 @@
bump: minor
type: feature
**The built-in ECS grows.** Every component was a fixed array of 1024 slots, so a game past 1024
entities could not have them (and until the last release silently corrupted memory trying). The
per-entity stores are heap blocks now, doubled by `L_grow` as entities outgrow them, the new slots
zero: 100 000 entities spawn and query. `Prop.has` bounds against the live capacity and
`Pool.capacity` answers it. A snapshot (`save`/`load`, `world_save`/`world_load`) records its slot
count first and a load grows to it before reading the stores back, so a snapshot's size follows the
world's instead of a fixed 1024. A mod's registered components grow with the rest.

View file

@ -1,31 +0,0 @@
bump: minor
type: feat
**A schema for editors, and every error as JSON.** `ludicc --emit-schema out.json` (and `ludic
schema [file] [-o FILE]`) writes what the compiler resolved once the program type-checks: every
record with its fields' types, defaults, doc comments and places; every registry with its record,
prefix, resource file, openness and its entries in their final order after the open-registry merge
(key, constant, index, file:line:col of the entry and of each field value, and which file brought
which entries in); every const; and every function a `fn` value can name, with the `fn_type` a field sees (its states stripped).
Deterministic, `"schema_version": 1`. Fields and registries carry editor attributes on the existing
`@` syntax - `@Ref(Registry)`, `@OneOf(PREFIX_)`, `@Range(lo, hi)`, `@Unit("m/s")`, `@Asset("gltf")`,
`@Color`, `@Node(field)`, `@Clip(field)`, `@Material(field)`, `@Tint(SLOT)`, `@Derived`, `@Text`, `@Multiline`, `@Key`, and `@AppendOnly` / `@ByKey` on
a registry - which change nothing but go into the schema; `@Ref` naming no registry is an error, and
so is `@Node` / `@Clip` naming a field that is not a glTF (`@Asset("gltf")`, or an `@Ref` to one), and
a listing `@OneOf` (`@OneOf(A, B)`, not a prefix `@OneOf(P_)`) naming a constant that does not exist, and `@Tint` naming no constant. A field may now carry several attributes. `ludicc --check
--diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array
of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column.
`Build.schema_hash()` answers FNV-1a 64 of the program's own schema (the bytes `ludic schema` prints),
computed only when a program names it, and 0 under `ludicc --release`, which `ludic bundle` now passes.
A target no part of the program declares (an `@Ref` registry, an `@Tint` or listed `@OneOf` constant) is
a warning and `"unresolved": true` in the schema, so a package can name the game's registry; a name of
another kind is an error. `@OneOf` on a string field takes words, and every registry row's value is
checked against them.
The schema has a `components` list: each UI component's module, place, doc, template and stylesheet
paths, its `props` and `state` (type, default as written, place, doc), `states_read` (the states its
header names, apart from its model), `derived` fields with their types, and the `functions` and
`events` its template calls with their parameters (states and instance stripped), and the
registered native tags its template uses. A `natives` list gives every `ui_native` /
`ui_native_input` call with a literal tag: the tag, the function called, its handler and its place.

View file

@ -1,6 +0,0 @@
bump: patch
type: fix
**A function named like an engine namespace method's target is refused where that method is
called.** `Random.range` is `rng_range`, so a package's own `rng_range(a, b, c)` silently took
every `Random.range(1, 6)` (and the checker then asked for its third argument). It is now an error
naming the function, the namespace method and the call.

View file

@ -1,5 +0,0 @@
bump: patch
type: fix
**`expect_eq` on strings compares their text.** It lowered to an integer compare of two pointers,
which the IR refused; now two strings with the same text are equal (a null only to a null), and a
failure prints both: `expect_eq failed (got "camp", want "lake")`.

View file

@ -1,5 +0,0 @@
bump: patch
type: fix
**`expect_eq` and `expect_near` take floats.** On a float or a double they compared with an integer
instruction, and the build failed in clang ("defined with type 'float' but expected 'i32'"); they
compare as floats now (the wider kind of the two) and a failure prints the numbers.

View file

@ -1,12 +0,0 @@
bump: minor
type: feat
**`ludic fmt` for editors.** `ludic fmt --lint --json` prints the violations `--lint` reports as one JSON
array on stdout, `[{"file", "line", "col", "rule", "message"}]` ordered by file, line and column (the
summary on stderr, `--lint`'s exit status, and the baseline never rewritten). `ludic fmt -` formats
stdin to stdout under the project found from the working directory (the nearest `package.ludic`
upwards), and refuses a buffer that does not read as Ludic - an open string, a bracket never closed or
closed by the wrong one - with exit 2 and `<name>:<line>:<col>: error: ...` on stderr.
`--stdin-name <path>` makes the buffer that file: the project is found from its directory, and
`ludic fmt - --lint --json --stdin-name <path>` judges it against that file's baseline and `lint
paths`, reporting it under the name given. Hooks around `fmt` and `get` read nothing from stdin and
write to stderr when the command's stdout is a program's (`--json`, `-`).

View file

@ -1,5 +0,0 @@
bump: minor
type: feature
**`friend module lab of fishing, data` - a friend of some modules, not all.** A scoped friend sees
the private names of the modules it names and only the exports of every other; `friend module lab`
alone still sees everything.

View file

@ -1,9 +0,0 @@
bump: minor
type: feat
**Functions are values (L2).** `fn(int, float) -> bool` is a type, `fn name` is any top-level
function's value (it used to be only a thread worker's address), and a call through a local, a
global, a record field, a slice element, a parameter or a result of a function type is an
indirect call. Two different function types do not mix, a call through one checks its argument
count, and a value may be `null`. A registry can hold behaviour and a package can take
callbacks. `Job.parallel_for` still checks that its worker takes (int, pointer) and returns
nothing. `ludic-dev selfhost-build` now says why it failed instead of exiting 1 silently.

View file

@ -1,5 +0,0 @@
bump: patch
type: feature
**The blades' density window can be filled without a GPU.** grass_density.ludic's gb_frame is split: gb_window_fill
fills the GB_TILES x GB_TILES window of density tiles round the camera's (zeros off the map) and sets its corner,
and gb_frame sends it. The same bytes as before; a test reads gb_win after gb_window_fill.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**Generic records and functions.** `property Pool<T> { items: []T }`, `function first<T>(xs: []T)
-> T` and `function map<T, U>(xs: []T, f: fn(T) -> U) -> []U`; a type writes an instance as
`Pool<Thing>`, nested as deep as needed. A call's type arguments come from its arguments, or from
the declared type its result is written into, and are refused with the parameter named when
neither says. Each instance is compiled once as an ordinary record or function. `ludic-fmt` keeps
`Pool<Thing>` together while still spacing `a < b`.

View file

@ -1,13 +0,0 @@
bump: minor
type: feat
**`ludic.render3d`: painted ground layers grow solid things, with ids, and the trample is data.**
`ground_fill`'s candidate is its own function, `ground_candidate` (pure `gf_*` steps with the density read
between them, into a caller-held `GroundCand`), and `ground_fill` draws exactly its answers - the same
operations in the same order as before, so every cover layer grows bit for bit what it did. A layer with
`solid: true` is filled at `step0` with band 0's hashes whatever the camera, a far band drawing a stable
subset; each thing has an int id from (layer, chunk, cell) (`ground_solid_id`), and `ground_solid_list` /
`ground_solid_at` answer a chunk's things or one by id for physics, the nav bake and saves.
`r3d_ground_clearing(x, z, r_in, r_out, floor)` hands the trample over as discs the editor can see, beside
the `r3d_on_ground_trample` callback, which still works. `tests/ground_fill_test.ludic` holds all of it to
`tests/ground_fill_golden.json` (written by `tests/gen/ground_fill_golden.ludic`), the file the studio's
TypeScript generator is tested against.

View file

@ -1,6 +0,0 @@
bump: patch
type: fix
**`Http.text` and `Http.header` return copies.** They handed back the handle's own buffer (and on macOS the
response object's string), which `Http.free` then released: a text read before the free and used after it
was garbage or empty - maroon-lake's map list wrote a 0-byte maps.json. Each call now returns a string that
is the caller's to keep. Read a body once per response.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**English left is an error (phase 26.9).** Under a `lang` line, a template's own words, a text
attribute's, a quoted choice that reads as words and a `@Text` row still holding English now refuse
the build, where they were warnings; `ludic deps` still counts them as `english_left`. Hole counts and
undescribed splits stay warnings. ludic.ui's own words - the key field's "Right click", "Middle
click", "Left click" and "press a key..." - are keys (`ui_tk(ui_st, k"ui.right_click", plain)`,
`ui.*` in the program's `.po`), with their plain English for a program that binds no translator.

View file

@ -1,9 +0,0 @@
bump: patch
type: fix
**Text keys below a row, padding, and `tr`'s cast.** A `@Text Key` in a record nested in a registry
row (and in each item of a list of them) is filled with its derived key, `<registry>.<row>.<field>.<i>.<field>`,
as a top-level one is, and a `@Text []Key` a row leaves out takes `<...>.0`, `.1`, ... for as many as
the source `.po` has - so no `.lres` spells a key. `field: null` is no text. `trf` / `trn`'s trailing
`""` arguments are padding and not counted against the English's holes. And `string(x)` of a string or
a `Key` is no allocation to the escape analysis: it is `x` itself, so a `tr(key)` that returns it
passes `arena strict` (a template's lone hole still copies).

View file

@ -1,9 +0,0 @@
bump: minor
type: change
**ludic.i18n draws plain text as it is: the English path is gone (phase 26.9).** `L` makes a key, a
key glued into text, or a line bracketed inside another; anything else - a player's name, a chat
line, a number - is drawn as it is, in every language, so a player named "Settings" stays
"Settings". Removed with it: the lookup of English words (exact lines, patterns with holes, a
paragraph a sentence at a time, padding), `Ln` (use `trn(kn"...")`), `i18n_pattern_count`, and an
English argument's own lookup inside a key's hole. A language `.po` is read for its keys and
plurals only. A game still on English msgids draws them untranslated until they are keys.

View file

@ -1,10 +0,0 @@
bump: minor
type: feature
**`ludic.i18n`: keys (phase 26).** A key names what a text is for, and `en.po` says it in English like any
other language. A key is a string with a marker byte (`I18N_KEY`, `I18N_PLURAL`), its arguments after
byte 31, so the code that makes text never takes `I18nState`: `tr(k)`, `trf(k, a, b, c, d)` and
`trn(k, n, a, b, c)` build it, and `L` makes it into text where it is drawn - the language in use, else
en.po (read the first time a key is asked for), else the key itself, `[[key]]` in a developer's build
(`i18n_loud`). Holes take their arguments in the language's order, a key argument made first; plural
keys go by each language's rule. A string with no marker takes the old English path, so a game can
move over a file at a time.

View file

@ -1,6 +0,0 @@
bump: patch
type: fix
**A key used only in a listener counts as used.** The key check walked the program's declarations but
not the bodies kept beside them - `@On` listeners, the lifecycle hooks, tests, computed fields and
scenes - so a key used only there was never checked against en.po and `ludic schema` listed it
unused. They are walked now, as every other pass walks them.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**Inflate in a caller-owned context.** `ZInflate` holds everything one inflate works in - the bit reader,
the RFC's length and distance tables, and every Huffman table and scratch list a block builds - made once
by `z_inflate_new()` and rebuilt in place, so an inflate allocates nothing and each thread that inflates
holds a context of its own. `z_inflate_in(z, src, len, out, cap)`, `z_uncompress_in` (zlib) and
`z_gunzip_in` (gzip) take the context; `z_inflate` / `z_uncompress` / `z_gunzip` keep their signatures,
working in the one context `RtInflateState` holds, so no caller changes.

View file

@ -1,6 +0,0 @@
bump: patch
type: fix
**The most negative int is emitted as itself.** The compiler's `itoa` negated a negative value before
taking its digits, and `-(-2147483648)` overflows back to itself, so a literal such as `0x80000000` in an
int (`b & 0x80000000`) was written into the IR as a bare `-` and the build failed in LLVM. The digits are
now taken off the value as it is. Needs a reseed to reach `bin/ludicc`.

View file

@ -1,6 +0,0 @@
bump: patch
type: fix
**`Json.parse` decodes an escaped string in one pass.** A string with escapes in it was joined a character
at a time, and every shorter copy was kept: a long escaped text took gigabytes (ui-preview's story model
reached 6.5 GB in three seconds). It is now decoded into one buffer sized from the text. ui-preview's
protocol-v1.md states how an `@import` path is resolved and which key a `file` override answers to.

View file

@ -1,5 +0,0 @@
bump: patch
type: fix
**`Json.parse` decodes `\uXXXX`.** An escaped code point is UTF-8 now - a surrogate pair joined into one, a
lone surrogate or bad hex as U+FFFD - where the backslash was dropped and the hex kept as text ("iu015f"),
and `\t`, `\r`, `\b` and `\f` are the characters they name. Python's `json.dump` writes non-ASCII this way.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**`Json.write_file(value, path)` saves without keeping anything.** It writes the text
`Json.encode` would give straight to the file (through `path.tmp` and a rename), built in a buffer
the runtime keeps, so a game that saves often holds nothing more for it. `Json.encode` itself is
built the same way and makes only the string it returns: it used to join its text with `+`, keeping
every piece and every number's text. `examples/lang/json_saves.ludic`: 1000 saves, 0 bytes.

View file

@ -1,3 +0,0 @@
bump: patch
type: feature
**ludic.lab: 16-bit PNGs** — `lab_png_write16_from(st, path, w, h, channels, px, at)` writes 16-bit samples (two bytes each, most significant first) the way `lab_png_write_from` writes 8-bit ones, so a test can write a height map render3d's decoder reads back as R16; `lp_png_bytes(stride, h)` is the size of one.

View file

@ -1,39 +0,0 @@
bump: minor
type: feat
Three things a crown and a hand light were getting wrong.
**An aspen's trunk quaked with its leaves.** `layer_flutter`'s mask was the vertex's
HEIGHT alone — anything more than 1.2 m up trembled — so on a ten metre aspen the whole
bole moved, and a tree that is supposed to shiver read as a column of cloth. What
separates a leaf from a bole is not height, it is distance from the model's own centre
line: the leaves are out at the edge of the crown and the trunk is on the axis. The mask
is radial now, and small plants keep the old one, because a flower is all leaf.
**Foliage in the camera's face.** A third-person camera walks into a crown and the branch
between it and the body is an opaque wall a metre from the lens; in first person it is the
whole screen. `#define NEAR_FADE` dithers tree foliage out inside arm's length of the
camera — in the depth PREPASS, so the lit pass never sees those pixels either and the
equal-depth optimisation is untouched. `gl_Position.w` is the view depth for a perspective
projection, so it costs one varying and no uniform. Blades, cards and flowers are left
alone on purpose: they live at the player's feet, they are always that close, and fading
them opens a hole in the meadow. Nor is the BOLE ever faded: a trunk and its needle cards
are one mesh drawn by one program, so the first cut dissolved the trunk into a dither
pattern as you walked up to it - a solid tree you can see through reads as a fault, where
a branch getting out of your way reads as the camera being polite. The mask is the same
signal the quake uses. The shadow pass keeps every leaf, or a tree would stop shading the
ground it stands on as you walked up to it.
**A hand light had the reach of a candle, whatever it was meant to be.** `handLight`'s
falloff was an inverse square windowed off between 26 and 6 metres, with both numbers
hard-coded: two per cent of its own near field by ten metres out, so carrying a torch at
night lit your boots and nothing else. `daylight_hand` takes a `reach` in metres now and
the falloff is a gentle power out to it — about nine tenths at a metre, half at half the
reach, a tenth at nine tenths of it, nothing past it. A pool of light with a gradient in
it rather than a hotspot with a cliff.
**And the wheel on macOS did nothing, or everything.** `scrollingDeltaY` is a double, and
the Cocoa event pump truncated it to an int PER EVENT before accumulating: a trackpad or a
Magic Mouse sends a stream of fractions of a line, every one of which truncated to zero, so
the wheel was dead; a notched mouse sends three to ten lines at once, so it jumped. The
fraction is accumulated now, a precise delta is scaled from points to notches, and the
remainder carries to the next frame - one gesture, one step, on both kinds of mouse.

View file

@ -1,8 +0,0 @@
bump: patch
type: fix
**A template literal inside another's `{…}` hole, and integer literals past 2^31 - 1.** The first
crashed the compiler (the outer literal ended at the inner one's backtick); a hole is now read as
code, so strings, chars and templates inside it are taken whole. The second was read as a wrapped
negative int; a decimal literal past `2147483647`, or a hex one of more than eight digits, is now a
`long` with its value, and giving one to an `int` is refused. Eight hex digits or fewer are still a
32-bit pattern.

View file

@ -1,35 +0,0 @@
bump: minor
type: feat
The air and the light are the hour's, not one constant apiece.
Aerial perspective is a curve now. A low sun shines through far more air than a high
one and shines ALONG the ground rather than down onto it, so density, height falloff
and forward scatter all ride the sun's own elevation, and overcast thickens the air
while flattening the scatter. The term that was missing entirely is distance
DESATURATION: a surface is pulled toward its own luminance faster than the fog itself
arrives. Without it, blending a saturated ridge toward a saturated blue noon sky left
a saturated ridge, and a midday frame had a mountain three kilometres off reading as
vividly as a bench two metres from the camera.
The grade is the hour's too. White balance, the shadows' floor, contrast and
saturation were nine literals bound at the draw; daylight.ludic writes them now.
Noon is the case worth naming: direct sun is warm-white and the only thing filling a
midday shadow is a blue sky, so noon gets a cool balance over a blue-lifted shadow
with hard contrast between it and the lit ground, and dawn and dusk get the reverse.
Gain is deliberately left alone - the grade is `c * gain + lift * (1 - c)`, so it
warms the whole frame rather than the highlights.
The visible sky is relit. It is one HDRI turned on its axis so its sun sits where the
hour wants it, and turning a photograph does not change what colour it was taken at -
so a sunset had a mid-morning blue overhead. An analytic sky supplies the chroma while
the photograph keeps the luminance, so the cloud stays where it is and goes orange at
dusk, and the zenith goes deep blue at noon, with no second sky shipped. It fades out
under the horizon, where the night's own tint, stars and moon take over, and eases off
under heavy cloud.
And the ground bounce follows the ground: the colour a surface is filled with from
below is the map's now, crossing from meadow to rock at its treeline, instead of one
green constant everywhere including above the scree.
R3D_NOAIR=1 restores all of it to what it was, so a before-and-after comes from one
binary at one hour. Measured at 400 frames: no cost on either backend.

View file

@ -1,32 +0,0 @@
bump: minor
type: feat
Light you can see: sun shafts, and mist that lies in the valley.
Everything before this made the air a COLOUR APPLIED TO A SURFACE. Nothing put light in
the space between surfaces, so a basin at dawn had no shafts, no pooled mist and no rays
off a ridge at any hour, however the fog was tuned.
A half-resolution march from the camera to the depth buffer, asking the SAME shadow the
rest of the frame asks - the cascades, the baked height-field shadow and the cloud mask -
so a shaft is cast by the actual trees and the actual ridge, and a passing cloud dims its
own rays. Henyey-Greenstein scattering, because real air throws light forward, which is
why a low sun fills a valley when you look into it and does almost nothing when you look
away. Density and a separate ground-hugging MIST layer ride the sun's elevation from
daylight.ludic, so the mist forms in the cold at either end of the day and burns off by
mid-morning.
It composites into the HDR scene with the bloom pyramid's own tent upsample under ONE/ONE
blending - which is exactly what was wanted and already existed - and before bloom, so a
shaft blooms like the bright thing in the air it is. Into post_hdr and not post_scene:
post_scene is the copy the water refracts, and shafts added there would sit under the lake.
The tuning that mattered was the SKY term. It is added at every step, so at 0.06 it
accumulated into a flat grey wash that lifted lit and shadowed air by the same amount -
which is the contrast a shaft is made of. The valley came out as one pale sheet with no
rays in it. At 0.012 the sun's term dominates and there is light to see rather than fog.
R3D_NOVOL=1 switches it off for an A/B. Off in Settings skips the pass whole rather than
marching once, because a feature is off when it costs nothing.
Measured, 400 frames at 07:00: OpenGL 7.1 -> 7.3 s, Vulkan 7.2 -> 7.4 s. Backends agree
to 0.08/255.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**`ludic-fmt` enforces a project's style.** `lint` lines in `package.ludic` state it - one statement
per line, file, function and comment-block limits (a file's opening comment its own) - and
`ludic-fmt --check <files>` and `ludic-fmt --lint` (the project's paths) fail what breaks them, at the
line. A baseline file is the ratchet that lets a rule arrive in an existing codebase: the counts a
file had may stay but not grow, and they are lowered as they are fixed.

View file

@ -1,7 +0,0 @@
bump: patch
type: fix
**A `return` inside an `@On` listener ends that listener, not the whole dispatch.** Listeners are
compiled into one `@ev_<E>` function, and `return` branched to its exit: every listener declared
after the one that returned, and every foreign listener, never heard the event. The shape it
broke is the common one - a listener per kind that returns early for every kind but its own -
where only the first-declared kind was ever answered. `examples/events/answer.ludic` holds it.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**`ludic deps` - the module graph as the compiler resolved it.** It compiles the program with
`LUDIC_DEPS` set, and the compiler records every reference its visibility pass sees (from module,
to module) and every assignment to another module's global. It prints the modules, dependencies,
largest cycle, cross-module writes and globals written from outside; `--graph`, `--dot`, `--writes`,
`--uses MOD`, `--check FILE` and `--baseline FILE`.

View file

@ -1,9 +0,0 @@
bump: minor
type: feature
**ludic.lab - the visual lab as a package.** A scene is an entry in the open registry `LabScenes`,
shown on a plate: a flat lit disc, a small sky of the package's own with the sun at one hour, and a
frame clock. Headless, each of its cameras (`lab_shot`, `lab_shot_at`) settles and is written as
`build/lab/<scene>/<shot>.png`; in a window N and P step through them. `tools/lab/run.sh` and
`sheet.py` make a contact sheet. ludic.render3d gains `r3d_plate_mode(true)` - no terrain, grass or
water is made - and `r3d_sky_path`; the example scene takes about 120 MB resident. `quit()` in a
program with no frame loop no longer fails to link.

View file

@ -1,35 +0,0 @@
bump: minor
type: feat
**A state machine as data: `@Machine(Record.field)`** (phase 27.1). A registry marked
`@Machine(Deer.mood)` is the transitions of a machine over that enum field of the records a state's
`Table<Deer>` holds: its record has `from` and `to` (the enum's variants), `on: string` (an action's
name, `""` for a transition the tick asks), `guard: fn(Row<Deer>, reads...) -> bool` and
`enter: fn(Row<Deer>, reads...) -> void`; the states are the enum's variants and the start is the
field's default. Its rows are data - an `.lres` or `def`s - so the studio edits the machine as a graph.
- **The compiler writes the machine.** For each action an `on` names, a row reducer in the
registry's file (`reducer Deer in Herd.deer on Spook`, beside any the program writes, after them):
the row's state, the first transition from it on that action whose guard passes, the field set,
`enter` run. When a row leaves a state on a guard alone, `state DeerStepsMachine` (the kept row
view) and `deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...)`, one transition a row
a tick, called from the program's system. Guards and enters are called by name; nothing allocates.
- **The table is the whole machine.** The field is written by nothing else: an assignment to it or
a `machine` block's `become` over it outside the written code is a type error (`Deer.mood is the
machine DeerSteps's (@Machine(Deer.mood)) - it changes only by a transition in its table`); a new
row takes its state in its `new`. A guard and an enter take the row first, are the record's
module's, and keep a row reducer's rules (`r.rec` and `r.h` only, a `@Column` field only through a
`@RowVerb`); a guard writes nothing through its row.
- **The graph is checked**, each an error naming its row: a state never reached from the start, a
state with no way out, an `on` naming no action or an action with no `@Target`, a self-transition
with no guard, and two ways out of a state on one trigger behind an unguarded first. Also refused:
`@Machine` on anything but a registry, a field that is not a plain enum with a default, a
`@Column` field, no table (or two) of the record, a transitions record of another shape, and a
machine outside its table's state's module.
- **`ludic schema`'s code section** gains `machines` (`registry`, `record`, `field`, `enum`, `table`,
`start`, `states`, `actions`, `tick`, `module`, `at`); the registry carries `@Machine` among its
attributes. `ludic deps` names a machine's reducer `reducer Deer in Herd.deer on Spook (machine
DeerSteps)`. vocab: `@Machine`; docs `annot-machine`; LANGUAGE.md "A machine as data".
- Examples `actions/machine` (with `deer_steps.lres`); rejects for an unreachable state, no way out,
an unknown action, an action with no row, a self-transition with no guard, an ambiguous trigger, a
guard that writes, the field written by hand, `@Machine` off a registry and a field that is no
enum. The `machine` block stays for a machine that is only code.

View file

@ -1,5 +0,0 @@
bump: minor
type: feature
**Map-scoped tables: `@PerMap` and `@Chunked(n)` registries** — a registry whose rows live in each map's directory and are read when the map loads, or a chunk at a time.
`@PerMap @ByKey registry Props of PropRow from "props.lres"` reads `<maps root>/<map>/props.lres` (the root is `package.ludic`'s new `maps "assets/maps"` line), and `@PerMap @Chunked(64) registry Instances of InstRow from "instances/{cx}_{cz}.lres"` one file per chunk. The compiler writes the table's `state` and its verbs in the declaring module (`props_load`, `props_clear`, `props_find`, `props_path`; `instances_in`, `_out`, `_slot`, `_find`, `_clear`, `_path`) and a typed fill over a small runtime `.lres` reader (`runtime/native/lres.ludic`, spliced on demand): a row names any int or float constant by name (resolved at load), a `fn` value by name, nested records and lists. Rows, their lists and the records in them are pooled and refilled in place, and `_find` is an allocation-free hash, so a load allocates nothing past the table's high water and a frame may bring a chunk in. `@Ref(T)` into a map table is a string key, checked against the same map. `ludicc --check` type-checks every map directory's tables against their records with file:line:col diagnostics (`--no-maps` to skip). The schema gives each registry a `"scope"` (`"map"` / `"game"`) and `"chunk"`, and a map `@Ref` `"scope": "map"`; `@Unit` now has canonical ASCII spellings (`deg`, `m/s2`, `N.m`, ...), listed as the schema's `"units"`, and any other spelling is a warning naming the right one.

View file

@ -1,7 +0,0 @@
bump: patch
type: fix
**`ludic build --check` reads the maps only for the package's entry.** A partial program - a unit test, a
molecule, a bake's runner - lacks the game's constants, so checking every map's tables against it refused
rows the game reads fine (`TH_*`, `NPCK_*` unknown). The maps are now checked with the `entry` package.ludic
names (as `ludic build --check` with no file, and tests/pack.sh, do); `--maps` checks them anyway, `--no-maps`
never. `ludicc` itself is unchanged: it reads them unless told `--no-maps`.

View file

@ -1,10 +0,0 @@
bump: patch
type: fix
**A `Math.*` call has a type the checker knows.** `Math.max`, `Math.sqrt` and the rest are computed
inline by the emitter, and the checker gave every one of them the unknown type, which agrees with
everything - so `dispatch Notify { a1: Math.max(5, n) }` put an int into a string field, passed L4
and failed in LLVM (`%t63 defined with type i32 but expected ptr`). The checker now gives each the
type the emitter does: a float or double first argument is that type (`sign` an int); otherwise
`min`, `max`, `abs` and `clamp` keep the first argument's type, `sign`, `floor`, `ceil`, `round`,
`posmod`, `wrap` and `ping_pong` are ints and the rest fixed. The bare `min` / `max` / `abs` /
`clamp` builtins the same. Where the arguments cannot be told, the result stays unknown.

View file

@ -1,10 +0,0 @@
bump: minor
type: feature
**A loaded model can be let go of: `model_release(model)`.** Its meshes are freed at once, and
each texture once no other model uses it - the glTF loader shares textures between models (one
path loaded once, a LOD chain borrowing its LOD0's material), so every cached texture now counts
the primitives using it, and the last one frees it and forgets it, with any remembered material
that named it. A game can unload a species' kit or a map's models when nothing of them is left,
and loading one again afterwards is a fresh load. A scatter layer takes room for what it holds
instead of its whole capacity up front (it doubles as it fills, to its cap), which in Maroon Lake
was 0.4 GB of never-filled instance arrays. `examples/rendering/release.ludic` is the check.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**Layers: `module flow in layer app uses base, items`.** The modules of one layer use each other
freely and may go round; anything outside the layer is still held to the module's `uses` (a
layered module with no `uses` reaches nothing outside its layer), and a cycle is refused unless it
stays inside one layer. `ludic deps` shows the layers and counts the largest cycle without the edges
inside them, printing the count with them beside it.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**Modules.** `module NAME` in a barrel makes its directory a module, and a module's
declarations are private to it unless they say `export`: a function, global, record or event of
another module used without one is an error naming the module and where to mark it. `friend
module NAME` sees everything (a test harness), a file in no module is public, and a package keeps
its own module. `LUDIC_VIS_REPORT=1` lists every violation instead of stopping, so an existing
program can be given its exports by a script before the rule applies to it.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**`module fishing uses base, data` - a module declares what it may reach, and the compiler holds it
to that.** A reference from a module that says `uses` into a module it does not name is refused,
exported or not, with the use and the fix in the message ("fishing uses items.inv_add (...): add
'uses items' to fishing's module line, or take it through a port"). A module with no `uses` keeps
the old rule, a package's module is always usable, a friend is not held to it, a cycle in the
declared graph is refused, and `LUDIC_VIS_REPORT=1` lists the violations as `uses:` lines.

View file

@ -1,10 +0,0 @@
bump: patch
type: fix
**A frame that draws more than any before no longer grows the heap on the Mac.** MoltenVK's command
pooling kept every command object a frame had ever recorded - about 650 bytes for each draw beyond the
busiest frame so far, for as long as the game ran. render3d turns it off
(`MVK_CONFIG_USE_COMMAND_POOLING=0`, unless the environment already says otherwise) before the first
Vulkan call; the objects are made and freed with their command buffer, at no measured cost.
`examples/rendering/steady.ludic` ramps a frame from 20 to 200 actors and fails on what pooling left.
A buffer written through its mapping is also read once at the start of the next frame's commands,
so MoltenVK makes its Metal buffer then rather than at its first draw, however much later that is.

View file

@ -1,10 +0,0 @@
bump: minor
type: feat
**A package can carry a native library.** `native "<target>" "<path>"` in a package's
`package.ludic` names a C/C++ library per target (`macos-arm64`, `windows-x64`, ...). The
compiler records the libraries of every package a program imports, and every link - `ludicc -o`,
`ludic build`, `ludic test`, `ludic bundle` - links them: on macOS with an rpath to the package
and to `Contents/Frameworks`, where `ludic bundle` places and signs them; on Windows through the
import library, the `.dll` copied beside the executable. `tools/native/lib.sh` builds a library
from a pinned, checksummed source; `ludic.nativeecho` is the worked example, and
`packages/README.md` says what a shim may pass across.

View file

@ -1,6 +0,0 @@
bump: patch
type: feat
**A native library's licence ships with it.** `ludic bundle` copies each linked package's
`native/LICENSE*` files beside the `.exe` on Windows and into `Contents/Resources` on macOS, so
Jolt Physics' MIT notice travels with every copy of the game. `tools/native/lib.sh` passes lld-link
`-implib`, which Git Bash leaves alone (it rewrote `/implib:` into a Windows path).

View file

@ -1,4 +0,0 @@
bump: patch
type: fix
**The foliage's near-fade programs have SPIR-V.** The three `NEAR_FADE` variants were missing
from `variants.list`, so on Vulkan the trees' and cards' programs failed to build.

View file

@ -1,13 +0,0 @@
bump: minor
type: feat
**A `numbers float` file is strict about where its numbers come from.** Two rules, both found
converting a renderer and a game from float bits in an `int` to real floats:
- A decimal literal takes the kind of what it meets. `factor == 0.0` against a `fixed` parameter,
`half(3.0)` into a `fixed` one and `var z: fixed = -1.5` are `fixed` literals there, rather
than a `fixed` and a `float` that do not mix.
- A computed integer never becomes a float implicitly. `count < limit` with an `int` on one side
and a `float` on the other is an error that asks for `float(x)`: in a file whose numbers are
floats, an `int` meeting one is almost always raw bits, and promoting it compares the bits as a
number. It caught a uniform fed an integer as float bits, and a test comparing a tint's bits
with its value. Integer constants still promote, and `float(x)` is always allowed.

View file

@ -1,9 +0,0 @@
bump: minor
type: feat
**`numbers float` — a module whose decimal literals are `float`.** A bare `1.5` is `fixed`
unless a float is in sight, which is the right default for deterministic code and the wrong
one for a renderer or a game whose numbers are all IEEE: `Math.sqrt(2.0)` came out in `fixed`,
and every literal needed a `float(...)` round it. A file that says `numbers float` (at the top,
or inside a `program` block) gets float literals, and passes the mode on to every non-runtime
file it imports, so one line in a barrel or an entry file covers a whole package. Runtime files
under `runtime/` are never switched, so the engine's own fixed-point code keeps its meaning.

View file

@ -1,11 +0,0 @@
bump: patch
type: fix
**One MoltenVK in a process.** On a Mac with the Vulkan SDK installed, the runtime opened the SDK's loader
(/usr/local/lib/libvulkan.1.dylib) before MoltenVK, and the loader loaded the SDK's own MoltenVK as its
driver - beside ludic.render3d's, which every program is linked against: two copies, each with its pools,
and the program drew through the SDK's ("MVKBlockObserver is implemented in both"). MoltenVK is opened
first now (dlopen hands back the linked copy), and the loader only when a layer is asked for
(VK_INSTANCE_LAYERS, VK_LOADER_LAYERS_ENABLE, R3D_VK_LOADER) - then pinned to ludic.render3d's MoltenVK
through VK_DRIVER_FILES and lib/macos-arm64/MoltenVK_icd.json, unless the caller named a driver.
examples/rendering/steady.ludic's stream round warms up over 300 cells and takes the least of three
windows of 150, as the frame round does: one sample of it read +75 KB on a run whose twin read -5 KB.

View file

@ -1,25 +0,0 @@
bump: minor
type: feat
One wind, and everything in the valley is moved by it.
The grass had a gust built from two sines and the trees had NO gust term at all - just a
per-instance wobble - so the meadow rippled and the canopy above it swayed to an unrelated
rhythm and nothing ever crossed the valley. A real gust is a WAVE: you watch it come over
the grass, it reaches you, and it goes on into the trees behind you.
wind.glsl is prepended to EVERY stage, so the grass's vertex shader, the crown's vertex
shader and the water's fragment shader read the same field at the same world position and
cannot drift apart. No uniforms are declared in it on purpose - u_time and u_wind already
exist in several of those files and redeclaring them is a compile error in whichever stage
includes both - so the caller passes what it already has.
Two numbers mattered. The gust's WAVELENGTH has to fit inside a view or nothing is ever
seen to travel: the first cut was 209 m crest to crest, longer than the meadow you can see,
so the whole frame sat in one phase and read as everything breathing together. It is 74 m
now and a front crosses a normal view in a couple of seconds. And `stiff` lets one field
move a blade a long way and a bole hardly at all.
R3D_DEBUG_WIND=1 paints the field on the ground, which is the only way to SEE a gust in a
still: two shots a moment apart show the fronts and show them moving.
Measured: no cost. 400 frames, GL 6.9 s, VK 7.2 s.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**`export open registry Items of Item` - a registry other modules may add to, in a stable order.**
A `def` from another module into a registry that is not open is refused, and a def now goes
through visibility like any reference (the registry exported, its module in the definer's `uses`).
An open registry's index order is the declaring module's entries first, then every other module's
by module name, each in the order it is read - independent of the order a barrel imports them in.

View file

@ -1,5 +0,0 @@
bump: patch
type: fix
**`Os.platform()` and `Os.arch()` allocate nothing after the first call.** Each call malloc'd an
8 KB `uname` buffer and let it go, and a game asks the platform every frame in places (a launcher's
wait, an update panel, a renderer's present). The buffer is made once and kept.

View file

@ -1,8 +0,0 @@
bump: patch
type: fix
**A name that meets a package's export says whose it is; ludic.ui's `UiAct` is its own.** A game's
`UiAct` collided with ludic.ui's, which no program uses - it is private to ludic.ui now, so the game
may have one. A real clash - a type named like one a package exports - is still refused, and the
message names the package and the way out: `'UiNode' is defined twice (...): ludic_ui exports it, and
exported names are one namespace - rename this one, or declare it without export inside a module of
your own`.

View file

@ -1,8 +0,0 @@
bump: minor
type: feat
**`ludic.physics`: Jolt Physics as a package.** Shapes (box, sphere, capsule, cylinder, a
boulder's dome, a heightfield, a mesh), still, kinematic and dynamic bodies that can be removed
for real, rays, a foot's landing height, a push out of what a body stands in, overlaps, buoyancy
against a `PhysWater` port with its current, and hard contacts and splashes as facts - stepped at a
fixed 1/60 s, deterministic across machines. Jolt v5.6.0 is built here from its pinned tag
(`native/build.sh`) through the phase-15 route.

View file

@ -1,7 +0,0 @@
bump: patch
type: fix
**A validation run sees the game's MoltenVK.** With a layer asked for, the loader was pinned to
ludic.render3d's MoltenVK only when the shell named no driver - but the SDK's setup-env.sh exports both
VK_DRIVER_FILES and VK_ICD_FILENAMES at the SDK's own MoltenVK, so a shell with one left set loaded that
one beside ours. The pin now overwrites VK_DRIVER_FILES and clears VK_ICD_FILENAMES; R3D_VK_ANY_DRIVER=1
keeps the shell's driver for a run that means to try another.

View file

@ -1,10 +0,0 @@
bump: minor
type: feat
**`ludic remove <module>` and `ludic get --json`.** `remove` is the inverse of `add`: the `require`
leaves `package.ludic`, the lock keeps what the remaining requires still reach (read from the store's
copies of each package's manifest, no network), and each package leaving the lock loses the
`ludic_modules/` link `add` made - never the shared store entry. A module that is not required is
refused; source still importing a removed package is a warning. `ludic get --json` prints what the run
changed in the lock as one JSON object on stdout - `{"added", "removed", "changed", "unchanged"}`, an
entry as the lock records it (`name`, `version`, `hash`, `kind`, `provides`), a change as `{name, from,
to, from_hash, to_hash}` - with the resolver's own lines on stderr.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**Ports: `port Clock { now: fn() -> int, day: fn() -> int = fn first_day }` in a module and
`bind Clock { now: fn game_hours }` where the program is put together.** A module asks for what it
needs through `Clock.now()` without naming - or `uses`-ing - the module that answers. A port used
and never bound, a bind that leaves out a member with no default, a member the port does not have
and a second bind are refused; the binder must see the (exported) port.

View file

@ -1,7 +0,0 @@
bump: minor
type: feature
**A module's private records and events are its own.** A `property` or `event` a module does not
export no longer collides with the same spelling in another module: each private one is compiled
under its module's name and its module's types, `new`, `emit` and `@On` follow it. Exported ones are
one namespace, and two events of one spelling are now refused (the first used to take the other's
emits silently). Generic records, entity components and component or view records stay global.

View file

@ -1,8 +0,0 @@
bump: minor
type: feature
**A module's private names are its own.** A function, var or const a module does not export no
longer collides with the same spelling in another module, in a file in no module or in the
runtime: each private one is compiled under its module's name (`seed$shop`) and its module's
references follow it. Exported names are still one namespace, and two exports of one name are
still refused. Records and events still share one namespace. A program with no such clash compiles
exactly as before.

View file

@ -1,8 +0,0 @@
bump: patch
type: fix
**`ludic migrate state --prune` keeps a state declared after a plain parameter, and a parameter named twice is refused.**
A function like `home_keep(r: Records, save_st: mut Save)` was taken to lack `save_st`, and the
prune gave it a second one at the front (`home_keep(save_st: mut Save, r: Records, save_st: mut
Save)`) - which `ludic build --check` let through and clang then refused. The migration now counts a
state anywhere in the list as declared, and the checker refuses a function that names two parameters
alike (`add names two parameters n`).

View file

@ -1,10 +0,0 @@
bump: minor
type: feat
**A queue keeps its own count, so a package verb takes only its own state; `ludic migrate state --prune`.**
ludic.base's `Queue<T>` carries its name and pending count itself: `queue_new(name)`, `q_push(q, v)`,
`q_drain(q)`, `q_clear(q)` take no `BaseState`, and neither does any package verb that pushes a fact -
`wallet_earn(wallet_st, n)`, `inv_add(inventory_st, ...)` - so a reducer on a package's state can call
its own package's verbs. `core_undrained(tags)` names the queues given to it (`q_tag(q)`) that still
hold facts. `ludic migrate state --prune` takes out each state parameter a function no longer uses
(nor anything it calls) and the argument that fills it, including an argument for a parameter the
callee has dropped: run it over a game to follow this change.

Some files were not shown because too many files have changed in this diff Show more