Merge branch 'lang/foundations' into lang/uifree

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 23:33:48 +03:00
commit 978f128755
65 changed files with 29578 additions and 26064 deletions

View file

@ -0,0 +1,9 @@
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

@ -0,0 +1,9 @@
bump: minor
type: feature
**A `Job.parallel_for` worker may read a state but not change one.** Every pool thread runs the
worker at once, and the runtime hands each call the same state, so a worker that took a state as
`mut` raced on it with nothing to say so. The compiler now refuses it (`fn bump: a worker runs on
every core at once, so it may read CountState but not change it`): a result goes into `ctx`, a
shared count through a `Sync` handle kept in the state. A function's signature already says what it
touches, so this is the whole check - the same one a parallel scheduler needs to run two systems at
once. `examples/library/threads.ludic` keeps its total in the words it hands the worker.

View file

@ -19,7 +19,9 @@ started on first use) and the calling thread, so each index runs exactly once, i
on (`words`, `bytes` or a `pointer`). A worker computes on what it was handed and writes only its own on (`words`, `bytes` or a `pointer`). A worker computes on what it was handed and writes only its own
index's results: it must not `spawn`, `despawn`, `push` onto a list another thread can see, or use index's results: it must not `spawn`, `despawn`, `push` onto a list another thread can see, or use
Http or Audio. `spawn` and `despawn` on a worker stop the program with a located message. Shared Http or Audio. `spawn` and `despawn` on a worker stop the program with a located message. Shared
counters go through `Sync.add`, shared totals behind a `Sync.mutex`. counters go through `Sync.add`, shared totals behind a `Sync.mutex`. A worker may take states, but
only to read them: every thread runs it at once, so a worker that takes one as `mut` is refused at
compile time (a result goes into `ctx`, a count through a `Sync` handle kept in the state).
```ludic ```ludic
program Squares { program Squares {

View file

@ -9,17 +9,18 @@ program Threads {
state ThreadsState { state ThreadsState {
calls: int = 0 # an atomic handle, made on the main thread before any work calls: int = 0 # an atomic handle, made on the main thread before any work
lock: int = 0 # a mutex handle lock: int = 0 # a mutex handle
total: long = 0 # guarded by `lock`
} }
# out[i] = i * i: every index written exactly once, by whichever thread claimed it # out[i] = i * i: every index written exactly once, by whichever thread claimed it
function square(i: int, out: words) -> void { out[i] = i * i } function square(i: int, out: words) -> void { out[i] = i * i }
# count the call atomically, and add i to a shared total under the mutex # count the call atomically, and add i to a shared total under the mutex. A worker may read a
function tally(threads_st: mut ThreadsState, i: int, ctx: pointer) -> void { # state (the handles in it) but never change one - every thread runs it at once - so the total
# lives in what the worker is handed
function tally(threads_st: ThreadsState, i: int, total: words) -> void {
Sync.add(threads_st.calls, 1) Sync.add(threads_st.calls, 1)
Sync.lock(threads_st.lock) Sync.lock(threads_st.lock)
threads_st.total = threads_st.total + i total[0] = total[0] + i
Sync.unlock(threads_st.lock) Sync.unlock(threads_st.lock)
} }
@ -43,10 +44,11 @@ program Threads {
threads_st.calls = Sync.atomic() threads_st.calls = Sync.atomic()
threads_st.lock = Sync.mutex() threads_st.lock = Sync.mutex()
Job.parallel_for(N, fn tally, null) let total = words(1)
Job.parallel_for(N, fn tally, total)
if Sync.get(threads_st.calls) == N { print(4) } if Sync.get(threads_st.calls) == N { print(4) }
let want: long = N * (N - 1) / 2 let want = N * (N - 1) / 2
if threads_st.total == want { print(5) } if total[0] == want { print(5) }
let ran = words(N) let ran = words(N)
Job.parallel_for(N, fn placed, ran) Job.parallel_for(N, fn placed, ran)

View file

@ -50,6 +50,60 @@ import "ludic.base"
| `def Systems key { ... }`, `SYS_<KEY>`, `SYS_COUNT` | a system declared from any module; the list starts from these | | `def Systems key { ... }`, `SYS_<KEY>`, `SYS_COUNT` | a system declared from any module; the list starts from these |
| `core_init_all()`, `core_reset_all()`, `core_tick_all(t)`, `core_save_all() -> Val`, `core_load_all(v)` | the runner: in the order added, tick phase by phase | | `core_init_all()`, `core_reset_all()`, `core_tick_all(t)`, `core_save_all() -> Val`, `core_load_all(v)` | the runner: in the order added, tick phase by phase |
## Entities: `Table<T>` and its indexes
A mechanic that keeps many of something (Things on the ground, animals, drops) keeps them as rows
of a `Table<T>` in its state rather than as a list it scans. The table is data-oriented and
allocates nothing per query:
- **Rows are dense.** Hot data is columns - `tb.f[c]` (floats) and `tb.i[c]` (ints), one value per
row - and `tb.rec[row]` is a record `T` for everything cold. A removal moves the last row into
the gap (`tb_remove`), so a sweep over `0 .. tb_len(tb)` touches contiguous memory.
- **Handles go stale.** `tb_add` returns a handle: a 22-bit slot and a 9-bit generation. `tb_row(tb,
h)` is -1 once the entity is removed, even after the slot is reused. Keep handles, never rows.
- **Indexes are kept by the setters.** `tb_set_f`, `tb_set_xz`, `tb_set_i` (or writing `tb.f` /
`tb.i` directly and then `tb_refile`) stamp the row's change tick and refile it in every index
that reads that column, in O(1). An index is never a frame behind.
- `tb_grid(tb, cx, cz, gate, cell)` - a spatial hash over two float columns, gated by an int
column (a row is filed while it is non-zero: "active"). `tb_nearest(tb, g, x, z, maxr, mc, mv)`
searches rings outward and stops at the first ring that cannot hold anything nearer;
`tb_within(..., out)` fills a caller's `words`. Buckets double as the rows grow.
- `tb_index(tb, col, gate)` - a cached query: the rows holding each value of an int column (a
kind). `ix_rows(ix, v)`, `ix_count(ix, v)`, `ix_first(ix, v)`.
- `tb_nearest_of` / `tb_within_of` plan between the two: a rare kind is scanned from its own
list, a common one searched by rings.
- **Change detection.** `tb_advance(tb)` moves the table to its next tick; `tb_changed_since(tb,
tick, out)` and `tb_added_since` name the rows written since - what a save or a message needs to
send a delta instead of everything.
- **Observers.** `tb_on_add(tb, fn f)` / `tb_on_remove(tb, fn f)`: `f(handle)` (its states supplied,
like a system's) is told each handle as its row is made and before it goes.
- **Groups and chunks.** `tb_remove_all(tb, ix, v)` removes every row an index files under one
value - a kind, or a chunk of the world a stream lets go (`chunk_of(x, z, size)` packs a chunk's
cell into an int). An index keeps a small value (below 1024) in its own slot and gives a larger,
sparse one (a chunk's packed cell) a slot through a map, so each value costs one list.
- **Change detection is per 64-row block too**: `tb_changed_since` skips a block nobody wrote
since. A row moved into a removal's gap keeps its own tick; the removal is the observer's.
- **Parallel work reads, never writes, a state.** `Job.parallel_for(n, fn work, ctx)` over a
table's columns runs on every core; the compiler refuses a worker that takes a state as `mut`
(a result goes into `ctx`, a shared count through a `Sync` handle kept in the state).
- **Stable ids.** `IntMap` (`imap_new`, `imap_put`, `imap_get(m, k, none)`, `imap_del`) maps an
id kept in a save or a message to a handle without a scan.
Ludic frees nothing a safe program allocates, so a query that built a list per call leaked every
frame. Every question here writes into a buffer the caller keeps. What each costs, against a
`[]Record` list scanned (`M4 Pro`, one thread):
| | 10 000 | 100 000 | 1 000 000 |
| --- | --- | --- | --- |
| nearest, any | 0.37 us (list 30) | 1.5 us (list 307) | 7.2 us (list 3075) |
| nearest of a kind (1 in 40) | 1.4 us (list 5.8) | 3.3 us (list 56) | 12 us (list 864) |
| by id | 0.1 us (list 1.7) | 0.09 us (list 17) | 1.5 us (list 324) |
| within 30 m | 0.9 us | 1.5 us | 6.9 us |
| a move, refiled | 11 ns | 14 ns | 44 ns |
`tests/ecs_fuzz_test.ludic` holds the grid and the kind index against a scan through thousands of
random adds, removes, moves, kind changes and gate flips.
## A toy mechanic ## A toy mechanic
```ludic ```ludic

View file

@ -0,0 +1,79 @@
# ludic.base/ecs_grid.ludic - a spatial hash over two float columns: square cells hashed into
# buckets, each a doubly linked list of rows, so filing, moving and unfiling a row is O(1)
export property Grid {
cx: int = 0
cz: int = 1
gate: int = -1 # an int column: a row is filed only while it is non-zero (-1: always)
inv: float = 0.0625 # 1 / the cell's side
mask: int = 255 # buckets - 1, a power of two less one
head: words = null # bucket -> row + 1, 0 empty
next: words = null # row -> the next row in its bucket + 1
prev: words = null # row -> the previous row + 1
key: words = null # row -> its packed cell, -1 while not filed
count: int = 0
}
function grid_cell(g: Grid, v: float) -> int { return int(Math.floor(v * g.inv)) }
function grid_key(ix: int, iz: int) -> int { return ((ix & 32767) << 15) | (iz & 32767) }
function grid_bucket(g: Grid, ix: int, iz: int) -> int { return ((ix * 73856093) ^ (iz * 19349663)) & g.mask }
function grid_make(cx: int, cz: int, gate: int, cell: float, rows: int) -> Grid {
let g = new Grid
g.cx = cx
g.cz = cz
g.gate = gate
g.inv = 1.0 / cell
g.head = words(g.mask + 1)
g.next = words(0)
g.prev = words(0)
g.key = words(0)
for r in 0 .. rows { grid_grow(g) }
return g
}
# index a table spatially by columns cx and cz, in cells `cell` wide; its rows are filed at once
export function tb_grid<T>(tb: Table<T>, cx: int, cz: int, gate: int, cell: float) -> Grid {
let g = grid_make(cx, cz, gate, cell, tb.n)
push(tb.grids, g)
for r in 0 .. tb.n { tb_grid_refile(tb, g, r) }
return g
}
export function grid_count(g: Grid) -> int { return g.count }
function grid_grow(g: Grid) -> void {
push(g.next, 0)
push(g.prev, 0)
push(g.key, -1)
}
function grid_shrink(g: Grid) -> void {
List.pop(g.next)
List.pop(g.prev)
List.pop(g.key)
}
function grid_link(g: Grid, r: int, k: int, b: int) -> void {
let h = g.head[b]
g.next[r] = h
g.prev[r] = 0
if h > 0 { g.prev[h - 1] = r + 1 }
g.head[b] = r + 1
g.key[r] = k
g.count += 1
}
function grid_unlink(g: Grid, r: int) -> void {
let k = g.key[r]
if k < 0 { return }
let p = g.prev[r]
let n = g.next[r]
if p > 0 { g.next[p - 1] = n } else { g.head[grid_bucket(g, grid_key_x(k), grid_key_z(k))] = n }
if n > 0 { g.prev[n - 1] = p }
g.key[r] = -1
g.count -= 1
}
# a packed key back to its cell (sign-extended from 15 bits)
function grid_key_x(k: int) -> int { return ((k >> 15) & 32767) - (((k >> 29) & 1) * 32768) }
function grid_key_z(k: int) -> int { return (k & 32767) - (((k >> 14) & 1) * 32768) }

View file

@ -0,0 +1,40 @@
# ludic.base/ecs_grid_file.ludic - where a row is filed follows its columns: a row that has not
# left its cell is not touched, a moved one is relinked, and the buckets double as the rows grow
function grid_refile(g: Grid, r: int, x: float, z: float, on: bool) -> void {
if not on {
grid_unlink(g, r)
return
}
let ix = grid_cell(g, x)
let iz = grid_cell(g, z)
let k = grid_key(ix, iz)
if g.key[r] == k { return }
grid_unlink(g, r)
grid_link(g, r, k, grid_bucket(g, ix, iz))
if g.count > (g.mask + 1) * 2 { grid_rehash(g) }
}
# row `from` becomes row `to` (which is not filed): its neighbours and its bucket follow it
function grid_moved(g: Grid, from: int, to: int) -> void {
let k = g.key[from]
g.key[to] = k
if k < 0 { return }
let p = g.prev[from]
let n = g.next[from]
g.prev[to] = p
g.next[to] = n
if p > 0 { g.next[p - 1] = to + 1 } else { g.head[grid_bucket(g, grid_key_x(k), grid_key_z(k))] = to + 1 }
if n > 0 { g.prev[n - 1] = to + 1 }
g.key[from] = -1
}
# twice the buckets, every filed row linked again where its key now hashes
function grid_rehash(g: Grid) -> void {
g.mask = g.mask * 2 + 1
g.head = words(g.mask + 1)
g.count = 0
for r in 0 .. len(g.key) {
let k = g.key[r]
if k >= 0 { grid_link(g, r, k, grid_bucket(g, grid_key_x(k), grid_key_z(k))) }
}
}

View file

@ -0,0 +1,93 @@
# ludic.base/ecs_grid_query.ludic - the nearest row: rings of cells outward from the point, stopping
# at the first ring that cannot hold anything nearer. A match is an int column equal to a value
# (column -1: any), or a predicate on the row's record. A question: it writes nothing but locals.
const GRID_RINGS: int = 24 # past this many rings a scan of the filed rows is cheaper
const GRID_FAR: float = 1000000000000.0
# the nearest filed row to (x, z) within maxr (0: any distance) matching (mc, mv), or -1
export function tb_nearest<T>(tb: Table<T>, g: Grid, x: float, z: float, maxr: float, mc: int, mv: int) -> int {
if g.count == 0 { return -1 }
var mcol: words = null
if mc >= 0 { mcol = tb.i[mc] }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
var best = -1
var bd = grid_limit(maxr)
let cs = 1.0 / g.inv
let ix0 = grid_cell(g, x)
let iz0 = grid_cell(g, z)
var ring = 0
while ring <= GRID_RINGS {
if ring > 0 and grid_ring_past(ring, cs, bd) { return best }
let span = grid_ring_n(ring)
for k in 0 .. span {
best = grid_visit(g, xs, zs, mcol, mv, x, z, grid_ring_x(ix0, ring, k), grid_ring_z(iz0, ring, k), best, bd)
if best >= 0 { bd = grid_d2(xs, zs, best, x, z) }
}
ring += 1
}
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) < bd {
best = row
bd = grid_d2(xs, zs, row, x, z)
}
}
return best
}
function grid_limit(maxr: float) -> float {
if maxr > 0.0 { return maxr * maxr }
return GRID_FAR
}
# the nearest a point of ring `ring` can be is (ring - 1) cells: past the best, nothing there wins
function grid_ring_past(ring: int, cs: float, bd: float) -> bool {
let near = float(ring - 1) * cs
return near * near > bd
}
function grid_d2(xs: floats, zs: floats, row: int, x: float, z: float) -> float {
let dx = xs[row] - x
let dz = zs[row] - z
return dx * dx + dz * dz
}
# a ring's cells in a fixed order: 1 for ring 0, else 8 * ring round the square's edge
function grid_ring_n(ring: int) -> int {
if ring == 0 { return 1 }
return 8 * ring
}
function grid_ring_x(ix0: int, ring: int, k: int) -> int {
if ring == 0 { return ix0 }
let side = 2 * ring
if k < side { return ix0 - ring + k }
if k < 2 * side { return ix0 + ring }
if k < 3 * side { return ix0 + ring - (k - 2 * side) }
return ix0 - ring
}
function grid_ring_z(iz0: int, ring: int, k: int) -> int {
if ring == 0 { return iz0 }
let side = 2 * ring
if k < side { return iz0 - ring }
if k < 2 * side { return iz0 - ring + (k - side) }
if k < 3 * side { return iz0 + ring }
return iz0 + ring - (k - 3 * side)
}
# one cell: its bucket's rows that are really in it and match; the better of them and `best`
function grid_visit(g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, ix: int, iz: int, best: int, bd: float) -> int {
let k = grid_key(ix, iz)
var b = best
var d = bd
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k and (mcol == null or mcol[row] == mv) {
let d2 = grid_d2(xs, zs, row, x, z)
if d2 < d {
d = d2
b = row
}
}
r = g.next[row]
}
return b
}

View file

@ -0,0 +1,101 @@
# ludic.base/ecs_grid_where.ludic - the grid's questions put to the rows' records: the nearest one a
# predicate keeps, and every record within a radius into the caller's list. The predicate is called
# with the record only; it may read anything, and must change nothing the grid indexes.
export function tb_nearest_where<T>(tb: Table<T>, g: Grid, x: float, z: float, maxr: float, keep: fn(T) -> bool) -> int {
if g.count == 0 { return -1 }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
var best = -1
var bd = grid_limit(maxr)
let cs = 1.0 / g.inv
let ix0 = grid_cell(g, x)
let iz0 = grid_cell(g, z)
var ring = 0
while ring <= GRID_RINGS {
if ring > 0 and grid_ring_past(ring, cs, bd) { return best }
for k in 0 .. grid_ring_n(ring) {
best = grid_visit_where(tb, g, xs, zs, x, z, grid_ring_x(ix0, ring, k), grid_ring_z(iz0, ring, k), best, bd, keep)
if best >= 0 { bd = grid_d2(xs, zs, best, x, z) }
}
ring += 1
}
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and grid_d2(xs, zs, row, x, z) < bd and keep(tb.rec[row]) {
best = row
bd = grid_d2(xs, zs, row, x, z)
}
}
return best
}
function grid_visit_where<T>(tb: Table<T>, g: Grid, xs: floats, zs: floats, x: float, z: float, ix: int, iz: int, best: int, bd: float, keep: fn(T) -> bool) -> int {
let k = grid_key(ix, iz)
var b = best
var d = bd
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k {
let d2 = grid_d2(xs, zs, row, x, z)
if d2 < d and keep(tb.rec[row]) {
d = d2
b = row
}
}
r = g.next[row]
}
return b
}
# every filed record within r of (x, z) matching (mc, mv), appended to `out` (cleared first) - the
# caller's list, kept in the caller's state and reused, so a frame's question allocates nothing
export function tb_within_recs<T>(tb: Table<T>, g: Grid, x: float, z: float, r: float, mc: int, mv: int, out: []T) -> int {
List.clear(out)
if g.count == 0 { return 0 }
var mcol: words = null
if mc >= 0 { mcol = tb.i[mc] }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
let ix0 = grid_cell(g, x - r)
let ix1 = grid_cell(g, x + r)
let iz0 = grid_cell(g, z - r)
let iz1 = grid_cell(g, z + r)
let cells = (ix1 - ix0 + 1) * (iz1 - iz0 + 1)
if cells > g.count or cells < 0 {
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) }
}
return len(out)
}
for ix in ix0 .. ix1 + 1 {
for iz in iz0 .. iz1 + 1 { grid_recs_cell(tb, g, xs, zs, mcol, mv, x, z, r * r, ix, iz, out) }
}
return len(out)
}
function grid_recs_cell<T>(tb: Table<T>, g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, r2: float, ix: int, iz: int, out: []T) -> void {
let k = grid_key(ix, iz)
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r2 { push(out, tb.rec[row]) }
r = g.next[row]
}
}
# every record of kind v (an index over column ix.col) within r, into `out`: a rare kind from its list
export function tb_within_of_recs<T>(tb: Table<T>, g: Grid, ix: IntIndex, x: float, z: float, r: float, v: int, out: []T) -> int {
let n = ix_count(ix, v)
if n > PLAN_LIST_MAX { return tb_within_recs(tb, g, x, z, r, ix.col, v, out) }
List.clear(out)
if n == 0 { return 0 }
let rows = ix.lists[v]
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
for k in 0 .. n {
let row = rows[k]
if g.gate >= 0 and tb.i[g.gate][row] == 0 { continue }
if grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) }
}
return len(out)
}

View file

@ -0,0 +1,48 @@
# ludic.base/ecs_grid_within.ludic - every filed row within r of (x, z) matching (mc, mv), into
# `out` (cleared first), in no order; the count is returned. A radius wider than the rows are many
# is answered by one scan of them instead of by cells.
export function tb_within<T>(tb: Table<T>, g: Grid, x: float, z: float, r: float, mc: int, mv: int, out: words) -> int {
List.clear(out)
if g.count == 0 { return 0 }
var mcol: words = null
if mc >= 0 { mcol = tb.i[mc] }
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
let ix0 = grid_cell(g, x - r)
let ix1 = grid_cell(g, x + r)
let iz0 = grid_cell(g, z - r)
let iz1 = grid_cell(g, z + r)
let cells = (ix1 - ix0 + 1) * (iz1 - iz0 + 1)
if cells > g.count or cells < 0 {
grid_within_scan(g, xs, zs, mcol, mv, x, z, r * r, out)
return len(out)
}
for ix in ix0 .. ix1 + 1 {
for iz in iz0 .. iz1 + 1 { grid_within_cell(g, xs, zs, mcol, mv, x, z, r * r, ix, iz, out) }
}
return len(out)
}
function grid_within_cell(g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, r2: float, ix: int, iz: int, out: words) -> void {
let k = grid_key(ix, iz)
var r = g.head[grid_bucket(g, ix, iz)]
while r > 0 {
let row = r - 1
if g.key[row] == k and (mcol == null or mcol[row] == mv) {
let dx = xs[row] - x
let dz = zs[row] - z
if dx * dx + dz * dz <= r2 { push(out, row) }
}
r = g.next[row]
}
}
function grid_within_scan(g: Grid, xs: floats, zs: floats, mcol: words, mv: int, x: float, z: float, r2: float, out: words) -> void {
for row in 0 .. len(g.key) {
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) {
let dx = xs[row] - x
let dz = zs[row] - z
if dx * dx + dz * dz <= r2 { push(out, row) }
}
}
}

View file

@ -0,0 +1,20 @@
# ludic.base/ecs_group.ludic - a group is every row an index files under one value: a kind, or a
# chunk of the world, loaded and let go together. A chunk's number packs its two cell coordinates.
export function chunk_of(x: float, z: float, size: float) -> int {
let cx = int(Math.floor(x / size))
let cz = int(Math.floor(z / size))
return ((cx & 32767) << 15) | (cz & 32767)
}
# every row filed under v in ix removed (the observers told of each); how many went. The index's
# own list shrinks as each goes, so it is read from its end until it is empty.
export function tb_remove_all<T>(tb: Table<T>, ix: IntIndex, v: int) -> int {
var n = 0
let rows = ix_rows(ix, v)
while len(rows) > 0 {
let h = tb.ent[rows[len(rows) - 1]]
if not tb_remove(tb, h) { return n }
n += 1
}
return n
}

View file

@ -0,0 +1,18 @@
# ludic.base/ecs_hooks.ludic - observers on a table: told each handle as its row is made, and before
# it is removed, so what depends on a table (a drawing, a message) follows it without a scan
# an observer: a function handed an entity's handle (its states are supplied, like a system's)
export property TbHook {
run: fn(int) -> void = null
}
# f is told every handle added from here on, and every handle about to be removed
export function tb_on_add<T>(tb: Table<T>, f: fn(int) -> void) -> void {
let hk = new TbHook
hk.run = f
push(tb.on_add, hk)
}
export function tb_on_remove<T>(tb: Table<T>, f: fn(int) -> void) -> void {
let hk = new TbHook
hk.run = f
push(tb.on_remove, hk)
}

View file

@ -0,0 +1,83 @@
# ludic.base/ecs_index.ludic - a cached query: for each value of an int column (a kind), the rows
# holding it, kept current by the table's setters, so "every fire" is a list and "how many" a length
export property IntIndex {
col: int = 0
gate: int = -1 # an int column: a row is filed only while it is non-zero (-1: always)
lists: []words = null # value -> its rows
pos: words = null # row -> where it stands in its value's list, -1 not filed
val: words = null # row -> the slot of the value it is filed under
empty: words = null # what a value with no rows answers
sparse: IntMap = null # a value past IX_DENSE (a chunk's packed cell) to its list's slot
}
export function tb_index<T>(tb: Table<T>, col: int, gate: int) -> IntIndex {
let ix = new IntIndex
ix.col = col
ix.gate = gate
ix.lists = new []words
ix.pos = words(0)
ix.val = words(0)
ix.empty = words(0)
ix.sparse = imap_new()
for r in 0 .. tb.n { ix_grow(ix) }
push(tb.idx, ix)
for r in 0 .. tb.n { tb_ix_refile(tb, ix, r) }
return ix
}
# the rows filed under v - the index's own list: read it, never push to it
export function ix_rows(ix: IntIndex, v: int) -> words {
let s = ix_slot(ix, v, false)
if s < 0 { return ix.empty }
return ix.lists[s]
}
export function ix_count(ix: IntIndex, v: int) -> int { return len(ix_rows(ix, v)) }
# the first row filed under v, or -1
export function ix_first(ix: IntIndex, v: int) -> int {
let rows = ix_rows(ix, v)
if len(rows) == 0 { return -1 }
return rows[0]
}
function ix_grow(ix: IntIndex) -> void {
push(ix.pos, -1)
push(ix.val, -1)
}
function ix_shrink(ix: IntIndex) -> void {
List.pop(ix.pos)
List.pop(ix.val)
}
function ix_file(ix: IntIndex, r: int, v: int) -> void {
let s = ix_slot(ix, v, true)
if ix.pos[r] >= 0 and ix.val[r] == s { return }
ix_unfile(ix, r)
if s < 0 { return }
ix.pos[r] = len(ix.lists[s])
ix.val[r] = s
push(ix.lists[s], r)
}
# out of its list: the list's last row takes its place
function ix_unfile(ix: IntIndex, r: int) -> void {
let p = ix.pos[r]
if p < 0 { return }
let l = ix.lists[ix.val[r]]
let last = List.pop(l)
if last != r {
l[p] = last
ix.pos[last] = p
}
ix.pos[r] = -1
ix.val[r] = -1
}
# row `from` becomes row `to` (which is not filed)
function ix_moved(ix: IntIndex, from: int, to: int) -> void {
let p = ix.pos[from]
ix.pos[to] = p
ix.val[to] = ix.val[from]
if p >= 0 { ix.lists[ix.val[from]][p] = to }
ix.pos[from] = -1
ix.val[from] = -1
}

View file

@ -0,0 +1,24 @@
# ludic.base/ecs_index_slot.ludic - where a value's list lives: a small value (a kind) is its own
# slot; a large one (a chunk's packed cell) is given the next slot through a map, so a sparse
# value costs one list and not every list up to it
const IX_DENSE: int = 1024 # a value below this is its own slot; above, one is given it
# the slot value v's list is in, -1 when it has none (and `make` did not ask for one)
function ix_slot(ix: IntIndex, v: int, make: bool) -> int {
if v < 0 { return -1 }
if v < IX_DENSE {
if v >= len(ix.lists) {
if not make { return -1 }
while len(ix.lists) <= v { push(ix.lists, words(0)) }
}
return v
}
var s = imap_get(ix.sparse, v, -1)
if s < 0 and make {
while len(ix.lists) < IX_DENSE { push(ix.lists, words(0)) }
s = len(ix.lists)
push(ix.lists, words(0))
imap_put(ix.sparse, v, s)
}
return s
}

View file

@ -0,0 +1,79 @@
# ludic.base/ecs_map.ludic - IntMap: a key (0 or more) to a value, open addressing with linear
# probing; what turns a stable id kept in a save or a message into a handle without a scan
export property IntMap {
keys: words = null # -1 empty, -2 a key that was deleted
vals: words = null
mask: int = 63
n: int = 0
used: int = 0 # live and deleted slots: what makes probing long
}
export function imap_new() -> IntMap {
let m = new IntMap
imap_alloc(m, 64)
return m
}
function imap_alloc(m: IntMap, cap: int) -> void {
m.keys = words(cap)
m.vals = words(cap)
for i in 0 .. cap { m.keys[i] = -1 }
m.mask = cap - 1
m.n = 0
m.used = 0
}
function imap_slot(m: IntMap, k: int) -> int { return ((k * -1640531535) >> 7) & m.mask }
# the value under k, or `none` when there is none
export function imap_get(m: IntMap, k: int, none: int) -> int {
var s = imap_slot(m, k)
while m.keys[s] != -1 {
if m.keys[s] == k { return m.vals[s] }
s = (s + 1) & m.mask
}
return none
}
export function imap_put(m: IntMap, k: int, v: int) -> void {
if (m.used + 1) * 4 > (m.mask + 1) * 3 { imap_grow(m) }
var s = imap_slot(m, k)
var tomb = -1
while m.keys[s] != -1 {
if m.keys[s] == k {
m.vals[s] = v
return
}
if m.keys[s] == -2 and tomb < 0 { tomb = s }
s = (s + 1) & m.mask
}
if tomb >= 0 { s = tomb } else { m.used += 1 }
m.keys[s] = k
m.vals[s] = v
m.n += 1
}
export function imap_del(m: IntMap, k: int) -> bool {
var s = imap_slot(m, k)
while m.keys[s] != -1 {
if m.keys[s] == k {
m.keys[s] = -2
m.n -= 1
return true
}
s = (s + 1) & m.mask
}
return false
}
export function imap_len(m: IntMap) -> int { return m.n }
export function imap_clear(m: IntMap) -> void { imap_alloc(m, m.mask + 1) }
function imap_grow(m: IntMap) -> void {
let ok = m.keys
let ov = m.vals
var cap = m.mask + 1
if m.n * 2 > cap { cap = cap * 2 }
imap_alloc(m, cap)
for i in 0 .. len(ok) { if ok[i] >= 0 { imap_put(m, ok[i], ov[i]) } }
}

View file

@ -0,0 +1,46 @@
# ludic.base/ecs_plan.ludic - a query that has both a grid and a kind index picks the cheaper way:
# a rare kind's own list scanned, a common one found by rings with the kind as the match
const PLAN_LIST_MAX: int = 192 # a kind with at most this many rows is scanned from its list
export function tb_nearest_of<T>(tb: Table<T>, g: Grid, ix: IntIndex, x: float, z: float, maxr: float, v: int) -> int {
let n = ix_count(ix, v)
if n == 0 { return -1 }
if n > PLAN_LIST_MAX { return tb_nearest(tb, g, x, z, maxr, ix.col, v) }
let rows = ix.lists[v]
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
var best = -1
var bd = 1000000000000.0
if maxr > 0.0 { bd = maxr * maxr }
for k in 0 .. n {
let r = rows[k]
if g.gate >= 0 and tb.i[g.gate][r] == 0 { continue }
let dx = xs[r] - x
let dz = zs[r] - z
let d2 = dx * dx + dz * dz
if d2 < bd or (d2 == bd and best < 0) {
bd = d2
best = r
}
}
return best
}
# within r of (x, z), of kind v, into `out`
export function tb_within_of<T>(tb: Table<T>, g: Grid, ix: IntIndex, x: float, z: float, r: float, v: int, out: words) -> int {
let n = ix_count(ix, v)
if n > PLAN_LIST_MAX { return tb_within(tb, g, x, z, r, ix.col, v, out) }
List.clear(out)
if n == 0 { return 0 }
let rows = ix.lists[v]
let xs = tb.f[g.cx]
let zs = tb.f[g.cz]
for k in 0 .. n {
let row = rows[k]
if g.gate >= 0 and tb.i[g.gate][row] == 0 { continue }
let dx = xs[row] - x
let dz = zs[row] - z
if dx * dx + dz * dz <= r * r { push(out, row) }
}
return len(out)
}

View file

@ -0,0 +1,96 @@
# ludic.base/ecs_table.ludic - a Table<T>: entities as dense rows of hot columns (floats, words)
# and one record each for what is cold. A handle is a slot and a generation, so a stale one misses.
export const ECS_SLOT: int = 4194303 # the low 22 bits of a handle: its slot
export const ECS_GEN: int = 511 # the next 9: how many times the slot was reused
export const ECS_NONE: int = -1
export property Table<T> {
n: int = 0
rec: []T = null
ent: words = null # row -> handle
f: []floats = null # hot float columns, a value per row each
i: []words = null # hot int columns
added: words = null # row -> the tick it was added at
changed: words = null # row -> the tick a setter last wrote it at
blk: words = null # 64-row block -> the newest tick any row in it was added or written at
tick: int = 1
slot_gen: words = null # slot -> generation
slot_row: words = null # slot -> row, -1 while free
free: words = null
grids: []Grid = null
idx: []IntIndex = null
on_add: []TbHook = null # observers: told a handle as its row is made, and before it goes
on_remove: []TbHook = null
}
# a table of nf float and ni int columns, every value 0 in a new row
export function table_new<T>(nf: int, ni: int) -> Table<T> {
let tb = new Table<T>
tb.rec = new []T
tb.ent = words(0)
tb.f = new []floats
tb.i = new []words
for c in 0 .. nf { push(tb.f, floats(0)) }
for c in 0 .. ni { push(tb.i, words(0)) }
tb.added = words(0)
tb.changed = words(0)
tb.blk = words(0)
tb.slot_gen = words(0)
tb.slot_row = words(0)
tb.free = words(0)
tb.grids = new []Grid
tb.idx = new []IntIndex
tb.on_add = new []TbHook
tb.on_remove = new []TbHook
return tb
}
export function tb_len<T>(tb: Table<T>) -> int { return tb.n }
export function tb_rec<T>(tb: Table<T>, row: int) -> T { return tb.rec[row] }
export function tb_handle<T>(tb: Table<T>, row: int) -> int { return tb.ent[row] }
export function tb_tick<T>(tb: Table<T>) -> int { return tb.tick }
# the next tick: what is written from here on is newer than anything before
export function tb_advance<T>(tb: Table<T>) -> void { tb.tick += 1 }
# the row a handle names, or -1 when it was removed (or the slot has been reused since)
export function tb_row<T>(tb: Table<T>, h: int) -> int {
if h < 0 { return -1 }
let s = h & ECS_SLOT
if s >= len(tb.slot_gen) or tb.slot_gen[s] != ((h >> 22) & ECS_GEN) { return -1 }
return tb.slot_row[s]
}
export function tb_alive<T>(tb: Table<T>, h: int) -> bool { return tb_row(tb, h) >= 0 }
# a new row holding `r`, every column 0 and filed in no index until a setter says where it is
export function tb_add<T>(tb: Table<T>, r: T) -> int {
var s = 0
if len(tb.free) > 0 { s = List.pop(tb.free) } else {
s = len(tb.slot_gen)
push(tb.slot_gen, 0)
push(tb.slot_row, -1)
}
tb.slot_row[s] = tb.n
let h = s | (tb.slot_gen[s] << 22)
push(tb.rec, r)
push(tb.ent, h)
push(tb.added, tb.tick)
push(tb.changed, tb.tick)
for c in 0 .. len(tb.f) { push(tb.f[c], 0.0) }
for c in 0 .. len(tb.i) { push(tb.i[c], 0) }
tb.n += 1
for g in 0 .. len(tb.grids) { grid_grow(tb.grids[g]) }
for k in 0 .. len(tb.idx) { ix_grow(tb.idx[k]) }
tb_stamp(tb, tb.n - 1)
for k in 0 .. len(tb.on_add) { tb.on_add[k].run(h) }
return h
}
# the row's tick, and its block's: what change detection reads
function tb_stamp<T>(tb: Table<T>, row: int) -> void {
tb.changed[row] = tb.tick
let b = row >> 6
while len(tb.blk) <= b { push(tb.blk, 0) }
tb.blk[b] = tb.tick
}

View file

@ -0,0 +1,49 @@
# ludic.base/ecs_table_remove.ludic - removing a row moves the last one into its place, so the
# rows stay dense; every index is told, and the slot's generation moves on
export function tb_remove<T>(tb: Table<T>, h: int) -> bool {
if tb_row(tb, h) < 0 { return false }
for k in 0 .. len(tb.on_remove) { tb.on_remove[k].run(h) }
let r = tb_row(tb, h)
if r < 0 { return false }
for g in 0 .. len(tb.grids) { grid_unlink(tb.grids[g], r) }
for k in 0 .. len(tb.idx) { ix_unfile(tb.idx[k], r) }
let last = tb.n - 1
if r != last { tb_move(tb, last, r) }
tb_pop(tb)
let s = h & ECS_SLOT
tb.slot_row[s] = -1
tb.slot_gen[s] = (tb.slot_gen[s] + 1) & ECS_GEN
push(tb.free, s)
return true
}
function tb_move<T>(tb: Table<T>, from: int, to: int) -> void {
tb.rec[to] = tb.rec[from]
tb.ent[to] = tb.ent[from]
tb.added[to] = tb.added[from]
tb.changed[to] = tb.changed[from]
for c in 0 .. len(tb.f) { tb.f[c][to] = tb.f[c][from] }
for c in 0 .. len(tb.i) { tb.i[c][to] = tb.i[c][from] }
tb.slot_row[tb.ent[to] & ECS_SLOT] = to
let b = to >> 6
if tb.blk[b] < tb.changed[to] { tb.blk[b] = tb.changed[to] }
for g in 0 .. len(tb.grids) { grid_moved(tb.grids[g], from, to) }
for k in 0 .. len(tb.idx) { ix_moved(tb.idx[k], from, to) }
}
function tb_pop<T>(tb: Table<T>) -> void {
List.pop(tb.rec)
List.pop(tb.ent)
List.pop(tb.added)
List.pop(tb.changed)
for c in 0 .. len(tb.f) { List.pop(tb.f[c]) }
for c in 0 .. len(tb.i) { List.pop(tb.i[c]) }
for g in 0 .. len(tb.grids) { grid_shrink(tb.grids[g]) }
for k in 0 .. len(tb.idx) { ix_shrink(tb.idx[k]) }
tb.n -= 1
}
# every row gone; the slots keep their generations, so an old handle still misses
export function tb_clear<T>(tb: Table<T>) -> void {
while tb.n > 0 { tb_remove(tb, tb.ent[tb.n - 1]) }
}

View file

@ -0,0 +1,76 @@
# ludic.base/ecs_table_set.ludic - the setters: a write stamps the row's tick and refiles it in
# every index that reads that column, so an index is never a frame behind what it indexes
export function tb_f<T>(tb: Table<T>, c: int, row: int) -> float { return tb.f[c][row] }
export function tb_i<T>(tb: Table<T>, c: int, row: int) -> int { return tb.i[c][row] }
export function tb_set_f<T>(tb: Table<T>, c: int, row: int, v: float) -> void {
tb.f[c][row] = v
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) {
let gr = tb.grids[g]
if gr.cx == c or gr.cz == c { tb_grid_refile(tb, gr, row) }
}
}
# two float columns at once, filed once: a move
export function tb_set_xz<T>(tb: Table<T>, cx: int, cz: int, row: int, x: float, z: float) -> void {
tb.f[cx][row] = x
tb.f[cz][row] = z
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) {
let gr = tb.grids[g]
if gr.cx == cx or gr.cz == cz or gr.cx == cz or gr.cz == cx { tb_grid_refile(tb, gr, row) }
}
}
export function tb_set_i<T>(tb: Table<T>, c: int, row: int, v: int) -> void {
tb.i[c][row] = v
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) { if tb.grids[g].gate == c { tb_grid_refile(tb, tb.grids[g], row) } }
for k in 0 .. len(tb.idx) {
let ix = tb.idx[k]
if ix.col == c or ix.gate == c { tb_ix_refile(tb, ix, row) }
}
}
# a row written without a setter (tb.f / tb.i directly, for speed) is refiled everywhere by this
export function tb_refile<T>(tb: Table<T>, row: int) -> void {
tb_stamp(tb, row)
for g in 0 .. len(tb.grids) { tb_grid_refile(tb, tb.grids[g], row) }
for k in 0 .. len(tb.idx) { tb_ix_refile(tb, tb.idx[k], row) }
}
export function tb_mark<T>(tb: Table<T>, row: int) -> void { tb_stamp(tb, row) }
function tb_gate_on<T>(tb: Table<T>, gate: int, row: int) -> bool { return gate < 0 or tb.i[gate][row] != 0 }
function tb_grid_refile<T>(tb: Table<T>, g: Grid, row: int) -> void {
grid_refile(g, row, tb.f[g.cx][row], tb.f[g.cz][row], tb_gate_on(tb, g.gate, row))
}
function tb_ix_refile<T>(tb: Table<T>, ix: IntIndex, row: int) -> void {
if tb_gate_on(tb, ix.gate, row) { ix_file(ix, row, tb.i[ix.col][row]) } else { ix_unfile(ix, row) }
}
# change detection: the rows a setter wrote after `since`, into `out` (cleared first); no allocation
export function tb_changed_since<T>(tb: Table<T>, since: int, out: words) -> int {
List.clear(out)
for b in 0 .. len(tb.blk) {
if tb.blk[b] <= since { continue }
var end = b * 64 + 64
if end > tb.n { end = tb.n }
for r in b * 64 .. end { if tb.changed[r] > since { push(out, r) } }
}
return len(out)
}
export function tb_added_since<T>(tb: Table<T>, since: int, out: words) -> int {
List.clear(out)
for b in 0 .. len(tb.blk) {
if tb.blk[b] <= since { continue }
var end = b * 64 + 64
if end > tb.n { end = tb.n }
for r in b * 64 .. end { if tb.added[r] > since { push(out, r) } }
}
return len(out)
}

View file

@ -8,3 +8,17 @@ import "rng.ludic"
import "save.ludic" import "save.ludic"
import "save_fields.ludic" import "save_fields.ludic"
import "system.ludic" import "system.ludic"
import "ecs_table.ludic"
import "ecs_hooks.ludic"
import "ecs_table_remove.ludic"
import "ecs_table_set.ludic"
import "ecs_grid.ludic"
import "ecs_grid_file.ludic"
import "ecs_grid_query.ludic"
import "ecs_grid_within.ludic"
import "ecs_grid_where.ludic"
import "ecs_index.ludic"
import "ecs_index_slot.ludic"
import "ecs_map.ludic"
import "ecs_plan.ludic"
import "ecs_group.ludic"

View file

@ -3,6 +3,8 @@
export property Queue<T> { export property Queue<T> {
items: []T = null items: []T = null
tag: QueueTag = null tag: QueueTag = null
none: []T = null # what draining an empty queue hands back, never pushed to
spare: []T = null # the list the last drain handed out: cleared and reused by the next
} }
# what a queue says about itself, whatever it holds: its name and how many facts wait in it # what a queue says about itself, whatever it holds: its name and how many facts wait in it
export property QueueTag { export property QueueTag {
@ -22,6 +24,8 @@ export function queue_new<T>(name: string) -> Queue<T> {
q.items = new []T q.items = new []T
q.tag = new QueueTag q.tag = new QueueTag
q.tag.name = name q.tag.name = name
q.none = new []T
q.spare = new []T
return q return q
} }
@ -30,18 +34,25 @@ export function q_push<T>(q: Queue<T>, v: T) -> void {
q.tag.pending = len(q.items) q.tag.pending = len(q.items)
} }
# the facts in the order they were pushed; the queue is empty afterwards # the facts in the order they were pushed; the queue is empty afterwards. Ludic frees nothing, so
# a drain allocates nothing: the queue keeps two lists and hands one out while the other fills, and
# an empty drain hands back one shared empty list. What a drain returns is good until the NEXT
# drain of the same queue - read it, never push to it, never keep it.
export function q_drain<T>(q: Queue<T>) -> []T { export function q_drain<T>(q: Queue<T>) -> []T {
if len(q.items) == 0 { return q.none }
let out = q.items let out = q.items
q.items = new []T List.clear(q.spare)
q.items = q.spare
q.spare = out
q.tag.pending = 0 q.tag.pending = 0
return out return out
} }
export function q_len<T>(q: Queue<T>) -> int { return len(q.items) } export function q_len<T>(q: Queue<T>) -> int { return len(q.items) }
# in place: the live list was never handed out (a drain gives its list away and starts another)
export function q_clear<T>(q: Queue<T>) -> void { export function q_clear<T>(q: Queue<T>) -> void {
q.items = new []T List.clear(q.items)
q.tag.pending = 0 q.tag.pending = 0
} }

View file

@ -0,0 +1,130 @@
# ecs_fuzz_test.ludic - thousands of random adds, removes, moves, kind changes and gate flips, and
# after every burst the grid and the kind index answer exactly what a scan of the rows answers
import "ludic.base"
program EcsFuzzTest {
numbers float
property Toy { n: int = 0 }
const CX: int = 0
const CZ: int = 1
const KIND: int = 0
const ON: int = 1
const KINDS: int = 6
state FuzzState {
seed: int = 12345
}
function rnd(fz: mut FuzzState, n: int) -> int {
fz.seed = (fz.seed * 1103515245 + 12345) & 2147483647
return (fz.seed >> 8) % n
}
function rpos(fz: mut FuzzState) -> float { return float(rnd(fz, 40000)) / 10.0 - 2000.0 }
function step(fz: mut FuzzState, tb: Table<Toy>) -> void {
let op = rnd(fz, 10)
if op < 4 or tb_len(tb) == 0 {
let toy = new Toy
toy.n = rnd(fz, 5)
let r = tb_row(tb, tb_add(tb, toy))
tb_set_xz(tb, CX, CZ, r, rpos(fz), rpos(fz))
tb_set_i(tb, KIND, r, rnd(fz, KINDS))
tb_set_i(tb, ON, r, rnd(fz, 4) % 3)
return
}
let r = rnd(fz, tb_len(tb))
if op < 6 { tb_remove(tb, tb_handle(tb, r)) } else if op < 8 {
tb_set_xz(tb, CX, CZ, r, tb_f(tb, CX, r) + float(rnd(fz, 200)) - 100.0, tb_f(tb, CZ, r) + float(rnd(fz, 200)) - 100.0)
} else if op < 9 { tb_set_i(tb, KIND, r, rnd(fz, KINDS)) } else { tb_set_i(tb, ON, r, 1 - tb_i(tb, ON, r)) }
}
# the scan's answer: the smallest squared distance among on rows matching, or -1
function brute_d2(tb: Table<Toy>, x: float, z: float, maxr: float, kind: int) -> float {
var best = -1.0
for r in 0 .. tb_len(tb) {
if tb_i(tb, ON, r) == 0 or (kind >= 0 and tb_i(tb, KIND, r) != kind) { continue }
let dx = tb_f(tb, CX, r) - x
let dz = tb_f(tb, CZ, r) - z
let d2 = dx * dx + dz * dz
if (maxr <= 0.0 or d2 <= maxr * maxr) and (best < 0.0 or d2 < best) { best = d2 }
}
return best
}
function keep_even(t: Toy) -> bool { return t.n % 2 == 0 }
function brute_even(tb: Table<Toy>, x: float, z: float, maxr: float) -> float {
var best = -1.0
for r in 0 .. tb_len(tb) {
if tb_i(tb, ON, r) == 0 or tb_rec(tb, r).n % 2 != 0 { continue }
let dx = tb_f(tb, CX, r) - x
let dz = tb_f(tb, CZ, r) - z
let d2 = dx * dx + dz * dz
if (maxr <= 0.0 or d2 < maxr * maxr) and (best < 0.0 or d2 < best) { best = d2 }
}
return best
}
function where_check(tb: Table<Toy>, g: Grid, ix: IntIndex, x: float, z: float, maxr: float, kind: int, recs: []Toy) -> void {
let want = brute_even(tb, x, z, maxr)
let row = tb_nearest_where(tb, g, x, z, maxr, fn keep_even)
if want < 0.0 { expect_eq(row, -1) } else {
let dx = tb_f(tb, CX, row) - x
let dz = tb_f(tb, CZ, row) - z
expect(dx * dx + dz * dz == want)
}
expect_eq(tb_within_recs(tb, g, x, z, 150.0, -1, 0, recs), brute_within(tb, x, z, 150.0, -1))
if kind >= 0 { expect_eq(tb_within_of_recs(tb, g, ix, x, z, 150.0, kind, recs), brute_within(tb, x, z, 150.0, kind)) }
}
function brute_within(tb: Table<Toy>, x: float, z: float, r: float, kind: int) -> int {
var n = 0
for i in 0 .. tb_len(tb) {
if tb_i(tb, ON, i) == 0 or (kind >= 0 and tb_i(tb, KIND, i) != kind) { continue }
let dx = tb_f(tb, CX, i) - x
let dz = tb_f(tb, CZ, i) - z
if dx * dx + dz * dz <= r * r { n += 1 }
}
return n
}
function check(fz: mut FuzzState, tb: Table<Toy>, g: Grid, ix: IntIndex, out: words, recs: []Toy) -> void {
for q in 0 .. 20 {
let x = rpos(fz)
let z = rpos(fz)
let kind = rnd(fz, KINDS + 1) - 1
var maxr = 0.0
if q % 2 == 1 { maxr = float(rnd(fz, 600)) }
var mc = KIND
if kind < 0 { mc = -1 }
let want = brute_d2(tb, x, z, maxr, kind)
let row = tb_nearest(tb, g, x, z, maxr, mc, kind)
if want < 0.0 { expect_eq(row, -1) } else {
expect(row >= 0)
let dx = tb_f(tb, CX, row) - x
let dz = tb_f(tb, CZ, row) - z
expect(dx * dx + dz * dz == want)
}
where_check(tb, g, ix, x, z, maxr, kind, recs)
let rad = float(rnd(fz, 400))
expect_eq(tb_within(tb, g, x, z, rad, mc, kind, out), brute_within(tb, x, z, rad, kind))
}
for k in 0 .. KINDS { expect_eq(ix_count(ix, k), brute_within(tb, 0.0, 0.0, 100000.0, k)) }
}
function fuzz_case(fz: mut FuzzState) -> void {
let tb: Table<Toy> = table_new(2, 2)
let g = tb_grid(tb, CX, CZ, ON, 16.0)
let ix = tb_index(tb, KIND, ON)
let out = words(0)
let recs = new []Toy
for burst in 0 .. 60 {
for s in 0 .. 150 { step(fz, tb) }
check(fz, tb, g, ix, out, recs)
}
tb_clear(tb)
expect_eq(grid_count(g), 0)
expect_eq(ix_count(ix, 1), 0)
}
test "the grid and the kind index agree with a scan through 9000 random changes" (fz: mut FuzzState) { fuzz_case(fz) }
}

View file

@ -0,0 +1,186 @@
# ecs_test.ludic - Table<T>: handles that go stale, dense rows after a removal, the setters keeping
# the grid and the kind index current, change ticks, and IntMap. Each case is a function because a
# generic call is not resolved inside a `test` body yet.
import "ludic.base"
program EcsTest {
numbers float
property Toy { name: string = "" }
const CX: int = 0
const CZ: int = 1
const KIND: int = 0
const ON: int = 1
state HookState {
added: int = 0
gone: int = 0
last: int = -1
}
function seen_add(hook_st: mut HookState, h: int) -> void {
hook_st.added += 1
hook_st.last = h
}
function seen_gone(hook_st: mut HookState, h: int) -> void { hook_st.gone += 1 }
function toy(name: string) -> Toy {
let t = new Toy
t.name = name
return t
}
function put(tb: Table<Toy>, name: string, x: float, z: float, kind: int) -> int {
let h = tb_add(tb, toy(name))
let r = tb_row(tb, h)
tb_set_xz(tb, CX, CZ, r, x, z)
tb_set_i(tb, KIND, r, kind)
tb_set_i(tb, ON, r, 1)
return h
}
function handles_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let a = put(tb, "a", 0.0, 0.0, 1)
let b = put(tb, "b", 1.0, 0.0, 1)
let c = put(tb, "c", 2.0, 0.0, 2)
expect_eq(tb_len(tb), 3)
expect(tb_remove(tb, a))
expect(not tb_alive(tb, a))
expect(not tb_remove(tb, a))
expect_eq(tb_len(tb), 2)
expect_eq(tb_rec(tb, tb_row(tb, c)).name, "c")
expect_eq(tb_rec(tb, tb_row(tb, b)).name, "b")
let d = put(tb, "d", 3.0, 0.0, 1)
expect(d != a)
expect_eq(d & ECS_SLOT, a & ECS_SLOT)
expect(not tb_alive(tb, a))
expect_eq(tb_rec(tb, tb_row(tb, d)).name, "d")
}
function grid_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let g = tb_grid(tb, CX, CZ, ON, 16.0)
let near = put(tb, "near", 5.0, 5.0, 1)
put(tb, "far", 500.0, -300.0, 1)
let fire = put(tb, "fire", -40.0, 2.0, 2)
expect_eq(tb_handle(tb, tb_nearest(tb, g, 0.0, 0.0, 0.0, -1, 0)), near)
expect_eq(tb_handle(tb, tb_nearest(tb, g, 0.0, 0.0, 0.0, KIND, 2)), fire)
expect_eq(tb_nearest(tb, g, 0.0, 0.0, 10.0, KIND, 2), -1)
tb_set_i(tb, ON, tb_row(tb, near), 0)
expect_eq(tb_handle(tb, tb_nearest(tb, g, 0.0, 0.0, 0.0, KIND, 1)), tb_handle(tb, tb_nearest(tb, g, 499.0, -300.0, 0.0, -1, 0)))
let out = words(0)
expect_eq(tb_within(tb, g, 0.0, 0.0, 50.0, -1, 0, out), 1)
tb_set_i(tb, ON, tb_row(tb, near), 1)
expect_eq(tb_within(tb, g, 0.0, 0.0, 50.0, -1, 0, out), 2)
tb_set_xz(tb, CX, CZ, tb_row(tb, fire), 900.0, 900.0)
expect_eq(tb_within(tb, g, 0.0, 0.0, 50.0, -1, 0, out), 1)
}
function index_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let ix = tb_index(tb, KIND, ON)
let a = put(tb, "a", 0.0, 0.0, 3)
put(tb, "b", 0.0, 0.0, 3)
put(tb, "c", 0.0, 0.0, 5)
expect_eq(ix_count(ix, 3), 2)
expect_eq(ix_count(ix, 5), 1)
expect_eq(ix_count(ix, 99), 0)
tb_set_i(tb, ON, tb_row(tb, a), 0)
expect_eq(ix_count(ix, 3), 1)
tb_remove(tb, a)
tb_set_i(tb, KIND, ix_first(ix, 3), 5)
expect_eq(ix_count(ix, 3), 0)
expect_eq(ix_count(ix, 5), 2)
}
function ticks_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let a = put(tb, "a", 0.0, 0.0, 1)
put(tb, "b", 0.0, 0.0, 1)
let seen = tb_tick(tb)
tb_advance(tb)
let out = words(0)
expect_eq(tb_changed_since(tb, seen, out), 0)
tb_set_f(tb, CX, tb_row(tb, a), 4.0)
expect_eq(tb_changed_since(tb, seen, out), 1)
expect_eq(out[0], tb_row(tb, a))
expect_eq(tb_added_since(tb, seen, out), 0)
}
function map_case() -> void {
let m = imap_new()
for k in 0 .. 5000 { imap_put(m, k * 7, k) }
expect_eq(imap_len(m), 5000)
expect_eq(imap_get(m, 700, -1), 100)
expect_eq(imap_get(m, 701, -1), -1)
for k in 0 .. 2500 { imap_del(m, k * 14) }
expect_eq(imap_len(m), 2500)
expect_eq(imap_get(m, 14, -1), -1)
expect_eq(imap_get(m, 21, -1), 3)
imap_put(m, 14, 9)
expect_eq(imap_get(m, 14, -1), 9)
}
function hooks_case(hook_st: HookState) -> void {
let tb: Table<Toy> = table_new(2, 2)
tb_on_add(tb, fn seen_add)
tb_on_remove(tb, fn seen_gone)
let a = put(tb, "a", 0.0, 0.0, 1)
expect_eq(hook_st.last, a)
put(tb, "b", 0.0, 0.0, 1)
tb_remove(tb, a)
tb_remove(tb, a)
expect_eq(hook_st.added, 2)
expect_eq(hook_st.gone, 1)
}
# 300 rows over five blocks; a change in one block is found, the untouched blocks skipped
function blocks_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
for i in 0 .. 300 { put(tb, "x", float(i), 0.0, 1) }
let seen = tb_tick(tb)
tb_advance(tb)
let out = words(0)
expect_eq(tb_changed_since(tb, seen, out), 0)
tb_set_f(tb, CX, 200, 9.0)
tb_set_f(tb, CX, 7, 9.0)
expect_eq(tb_changed_since(tb, seen, out), 2)
expect_eq(out[0], 7)
expect_eq(out[1], 200)
let before = tb_tick(tb)
tb_advance(tb)
tb_remove(tb, tb_handle(tb, 3))
expect_eq(tb_changed_since(tb, before, out), 0) # a row moved into a gap was not written
expect_eq(tb_changed_since(tb, seen, out), 2) # a removal is told by tb_on_remove
}
# values far apart (a chunk's packed cell) each take one list, not every list up to them
function sparse_case() -> void {
let tb: Table<Toy> = table_new(2, 2)
let ix = tb_index(tb, KIND, -1)
let a = chunk_of(9000.0, -9000.0, 256.0)
let b = chunk_of(-512.0, 700.0, 256.0)
expect(a > 100000 and b > 100000)
put(tb, "a", 0.0, 0.0, a)
put(tb, "b", 0.0, 0.0, b)
let c = put(tb, "c", 0.0, 0.0, a)
put(tb, "d", 0.0, 0.0, 3)
expect_eq(ix_count(ix, a), 2)
expect_eq(ix_count(ix, b), 1)
expect_eq(ix_count(ix, 3), 1)
expect(len(ix.lists) < 1100)
tb_set_i(tb, KIND, tb_row(tb, c), b)
expect_eq(ix_count(ix, a), 1)
expect_eq(ix_count(ix, b), 2)
expect_eq(tb_remove_all(tb, ix, b), 2)
expect_eq(ix_count(ix, b), 0)
expect_eq(tb_len(tb), 2)
}
test "a sparse value takes one list" () { sparse_case() }
test "observers hear each add, and a remove once" (hook_st: HookState) { hooks_case(hook_st) }
test "change detection skips the blocks nobody wrote" () { blocks_case() }
test "a removed handle misses, rows stay dense, a reused slot is a new handle" () { handles_case() }
test "the grid answers nearest and within, gated, after moves" () { grid_case() }
test "the kind index follows the setters, the gate and removals" () { index_case() }
test "change ticks name the rows written since" () { ticks_case() }
test "IntMap puts, finds, deletes and grows" () { map_case() }
}

View file

@ -67,7 +67,44 @@ program QueueTest {
expect_eq(len(core_undrained(tags)), 0) expect_eq(len(core_undrained(tags)), 0)
} }
# an empty drain allocates nothing and hands back the same empty list; a drained list is the
# caller's, untouched by what is pushed or cleared after it
function empty_case() -> void {
let q: Queue<int> = queue_new("quiet")
let a = q_drain(q)
let b = q_drain(q)
expect_eq(len(a), 0)
expect(a == b)
q_push(q, 5)
expect_eq(len(a), 0)
let got = q_drain(q)
q_push(q, 6)
q_clear(q)
expect_eq(len(got), 1)
expect_eq(got[0], 5)
expect_eq(q_len(q), 0)
expect_eq(len(q_drain(q)), 0)
}
test "a queue drains in the order it was pushed" () { fifo_case() } test "a queue drains in the order it was pushed" () { fifo_case() }
# two lists, reused: a drained list is the caller's until the next drain of that queue
function reuse_case() -> void {
let q: Queue<int> = queue_new("reused")
q_push(q, 1)
let a = q_drain(q)
q_push(q, 2)
expect_eq(a[0], 1)
let b = q_drain(q)
expect_eq(b[0], 2)
q_push(q, 3)
let c = q_drain(q)
expect(c == a)
expect_eq(len(c), 1)
expect_eq(c[0], 3)
}
test "a drain reuses the queue's two lists" () { reuse_case() }
test "an empty drain allocates nothing, and a drained list is the caller's" () { empty_case() }
test "a queue holds records" () { record_case() } test "a queue holds records" () { record_case() }
test "clear drops what is waiting" () { clear_case() } test "clear drops what is waiting" () { clear_case() }
test "a queue's element type is told by the queue alone" () { infer_case() } test "a queue's element type is told by the queue alone" () { infer_case() }

View file

@ -30,6 +30,7 @@ property Actor {
cast_hidden: bool = false, cast_hidden: bool = false,
casts: bool = true, casts: bool = true,
id: int = 0, id: int = 0,
row: int = -1, # where it stands in ac_actors, so it leaves in O(1); -1 off the stage
cutout: bool = false, # alpha-tested (a flame's cards) cutout: bool = false, # alpha-tested (a flame's cards)
emissive: float = 0.0, # float bits: self-lit strength emissive: float = 0.0, # float bits: self-lit strength
skin: Skin, # this instance's own pose (skin_clone); null: the model's skin: Skin, # this instance's own pose (skin_clone); null: the model's
@ -82,11 +83,27 @@ function actor_init(render3d_st: mut Render3dState) -> void {
render3d_st.ac_out_cut = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n") render3d_st.ac_out_cut = ac_prog_new(render3d_st, "skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n")
render3d_st.ac_actors = new []Actor render3d_st.ac_actors = new []Actor
} }
# off the stage: the last actor takes its row, so the list stays dense and nothing is rebuilt
# (it was a new list of every other actor per removal, never freed)
function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void { function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void {
if render3d_st.ac_actors == null { return } let l = render3d_st.ac_actors
let keep = new []Actor if l == null or a == null { return }
for i in 0 .. len(render3d_st.ac_actors) { if render3d_st.ac_actors[i].id != a.id { push(keep, render3d_st.ac_actors[i]) } } let r = a.row
render3d_st.ac_actors = keep if r < 0 or r >= len(l) or l[r].id != a.id { return }
let last = List.pop(l)
if last.id != a.id {
l[r] = last
last.row = r
}
a.row = -1
}
# back on the stage (an actor kept across a world swap)
function actor_keep(render3d_st: mut Render3dState, a: Actor) -> void {
if a == null { return }
if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor }
a.row = len(render3d_st.ac_actors)
push(render3d_st.ac_actors, a)
} }
# colour one named part of the model (a material name from the file) # colour one named part of the model (a material name from the file)
function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> void { function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> void {
@ -114,8 +131,7 @@ function actor_new(render3d_st: mut Render3dState, model: Model) -> Actor {
render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id
if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 } if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 }
a.cull = 450.0 a.cull = 450.0
if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor } actor_keep(render3d_st, a)
push(render3d_st.ac_actors, a)
return a return a
} }
@ -396,6 +412,8 @@ function ac_caster_count(render3d_st: mut Render3dState, a: Actor) -> void {
# every actor off the stage at once, for a world being replaced. Actors own no GL objects; # every actor off the stage at once, for a world being replaced. Actors own no GL objects;
# their models belong to whoever loaded them. # their models belong to whoever loaded them.
function actor_clear_all(render3d_st: mut Render3dState) -> void { function actor_clear_all(render3d_st: mut Render3dState) -> void {
render3d_st.ac_actors = new []Actor if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor }
for i in 0 .. len(render3d_st.ac_actors) { render3d_st.ac_actors[i].row = -1 }
List.clear(render3d_st.ac_actors)
outline_clear(render3d_st) outline_clear(render3d_st)
} }

View file

@ -0,0 +1,34 @@
# chunks.ludic - the Things of one chunk of the world, counted and let go together: what a streamed
# world asks when a chunk is loaded or dropped. A chunk is chunk_of(x, z, TH_CHUNK_M).
export function things_chunk(x: float, z: float) -> int { return chunk_of(x, z, TH_CHUNK_M) }
export function things_chunk_count(things_st: ThingsState, chunk: int) -> int { return ix_count(things_st.th.chunks, chunk) }
# every Thing standing in the chunk removed for good, the port and the facts told of each as
# thing_remove tells them; how many went
export function things_drop_chunk(things_st: mut ThingsState, chunk: int) -> int {
var n = 0
let rows = ix_rows(things_st.th.chunks, chunk)
while len(rows) > 0 {
thing_remove(things_st, things_st.th.tab.rec[rows[len(rows) - 1]])
n += 1
}
return n
}
# change detection: the tick now, the table moving on to the next - a consumer keeps what this
# returned and asks things_changed(since: it) for what was written after
export function things_now(things_st: mut ThingsState) -> int {
let t = tb_tick(things_st.th.tab)
tb_advance(things_st.th.tab)
return t
}
# every Thing a verb wrote after `since`, into the caller's list (untouched 64-row blocks skipped)
export function things_changed(things_st: ThingsState, since: int, rows: words, out: []Thing) -> int {
List.clear(out)
let tb = things_st.th.tab
let n = tb_changed_since(tb, since, rows)
for k in 0 .. n { push(out, tb.rec[rows[k]]) }
return n
}

View file

@ -45,14 +45,22 @@ export function thing_is_personal(t: Thing) -> bool { return thing_kind_ok(t.kin
# set down by someone rather than grown by the world: the kind says, and the port has the last word # set down by someone rather than grown by the world: the kind says, and the port has the last word
export function thing_is_put_down(t: Thing) -> bool { return ThingsWorld.put_down(t) } export function thing_is_put_down(t: Thing) -> bool { return ThingsWorld.put_down(t) }
# every active Thing whose kind ticks, `hours` of the world's clock on # every active Thing whose kind ticks, `hours` of the world's clock on: only the kinds that tick
export function things_tick(things_st: ThingsState, hours: float) -> void { # are visited, through the kind index, from a list of handles taken first - a tick may hide one
let all = things_all(things_st) export function things_tick(things_st: mut ThingsState, hours: float) -> void {
for i in 0 .. len(all) { let hs = things_st.th_hs
let t = all[i] List.clear(hs)
if not t.active or not thing_kind_ok(t.kind) { continue } let tb = things_st.th.tab
let k = ThingKinds[t.kind] for k in 0 .. TH_COUNT {
if k.tick != null { k.tick(t, hours) } if ThingKinds[k].tick == null { continue }
let rows = ix_rows(things_st.th.kinds, k)
for i in 0 .. len(rows) { push(hs, tb.ent[rows[i]]) }
}
for i in 0 .. len(hs) {
let r = tb_row(tb, hs[i])
if r < 0 { continue }
let t = tb.rec[r]
if t.active { ThingKinds[t.kind].tick(t, hours) }
} }
} }

View file

@ -6,6 +6,10 @@ import "ludic.base"
import "kinds.ludic" import "kinds.ludic"
import "port.ludic" import "port.ludic"
import "store.ludic" import "store.ludic"
import "verbs.ludic"
import "moves.ludic"
import "queries.ludic" import "queries.ludic"
import "near.ludic"
import "chunks.ludic"
import "dispatch.ludic" import "dispatch.ludic"
import "system.ludic" import "system.ludic"

View file

@ -19,6 +19,8 @@ export property Thing {
sz: float = 0.0 sz: float = 0.0
syaw: float = 0.0 syaw: float = 0.0
shared: bool = false # the game copies it to others (a party) shared: bool = false # the game copies it to others (a party)
ent: int = -1 # its handle in the table, -1 once removed
tab: Table<Thing> = null # the table holding it, so a verb on the Thing alone keeps the indexes
} }
export property ThingKind { export property ThingKind {

View file

@ -0,0 +1,55 @@
# moves.ludic - a Thing moved, shown or hidden: the record and its row together, so the grid, the
# kind indexes and the chunk index never answer for where it was or what it was
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
thing_move_to(t, x, ThingsWorld.ground(x, z), z, yaw)
}
export function thing_move_to(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
thing_place_at(t, x, y, z, yaw)
ThingsWorld.moved(t)
}
# moved without telling the port: a stand-in the game draws itself, or nothing draws
export function thing_place_at(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
t.x = x
t.y = y
t.z = z
t.yaw = yaw
let r = th_row(t)
if r >= 0 { th_moved(t.tab, r, x, z) }
}
export function thing_set_active(t: Thing, on: bool) -> void {
if t == null { return }
thing_set_on(t, on)
ThingsWorld.shown(t)
}
export function thing_hide(t: Thing) -> void { thing_set_active(t, false) }
export function thing_show(t: Thing) -> void { thing_set_active(t, true) }
# active or not without telling the port
export function thing_set_on(t: Thing, on: bool) -> void {
if t == null { return }
t.active = on
let r = th_row(t)
if r < 0 { return }
var v = 0
if on { v = 1 }
tb_set_i(t.tab, TH_ON, r, v)
}
# where it stands on the ground plane, without telling the port (its height is the caller's)
export function thing_set_xz(t: Thing, x: float, z: float) -> void {
if t == null { return }
t.x = x
t.z = z
let r = th_row(t)
if r >= 0 { th_moved(t.tab, r, x, z) }
}
# where a row now stands: its columns, its cell in the grid and its chunk
function th_moved(tb: Table<Thing>, r: int, x: float, z: float) -> void {
tb_set_xz(tb, TH_X, TH_Z, r, x, z)
let c = chunk_of(x, z, TH_CHUNK_M)
if tb.i[TH_CHUNK][r] != c { tb_set_i(tb, TH_CHUNK, r, c) }
}

View file

@ -0,0 +1,35 @@
# near.ludic - the neighbourhood questions a frame asks many times: what is around a point, of any
# kind or of one, into a list the CALLER keeps (in its own state, reused every frame), from the grid
# or the kind's own list - never a walk of every Thing, and nothing allocated or written here
export function things_near(things_st: ThingsState, x: float, z: float, r: float, out: []Thing) -> int {
return tb_within_recs(things_st.th.tab, things_st.th.grid, x, z, r, -1, 0, out)
}
export function things_near_of(things_st: ThingsState, kind: int, x: float, z: float, r: float, out: []Thing) -> int {
return tb_within_of_recs(things_st.th.tab, things_st.th.grid, things_st.th.kinds, x, z, r, kind, out)
}
# the nearest active Thing within maxr (0: any distance) that `keep` says yes to, or null
export function thing_nearest_where(things_st: ThingsState, x: float, z: float, maxr: float, keep: fn(Thing) -> bool) -> Thing {
let r = tb_nearest_where(things_st.th.tab, things_st.th.grid, x, z, maxr, keep)
if r < 0 { return null }
return things_st.th.tab.rec[r]
}
# every one of a kind, active or not, counted and walked without a list
export function things_count_all_of(things_st: ThingsState, kind: int) -> int { return ix_count(things_st.th.every, kind) }
export function thing_of_kind_all(things_st: ThingsState, kind: int, i: int) -> Thing {
let rows = ix_rows(things_st.th.every, kind)
if i < 0 or i >= len(rows) { return null }
return things_st.th.tab.rec[rows[i]]
}
# the nearest active one of a kind (below 0: any) strictly within maxr of (x, z), or null
export function thing_nearest_within(things_st: ThingsState, kind: int, x: float, z: float, maxr: float) -> Thing {
let tb = things_st.th.tab
var r = -1
if kind < 0 { r = tb_nearest(tb, things_st.th.grid, x, z, maxr, -1, 0) } else { r = tb_nearest_of(tb, things_st.th.grid, things_st.th.kinds, x, z, maxr, kind) }
if r < 0 { return null }
return tb.rec[r]
}

View file

@ -1,38 +1,43 @@
# queries.ludic - where the Things are. Only active Things answer, except thing_by_uid and # queries.ludic - where the Things are, answered by the table's indexes rather than a scan. Only
# things_each_all, which ask about everything that exists. A kind below 0 is any kind. # active Things answer, except thing_by_uid and things_each_all, which ask about everything that
# exists. A kind below 0 is any kind.
function th_of(t: Thing, kind: int) -> bool { return kind < 0 or t.kind == kind } function th_of(t: Thing, kind: int) -> bool { return kind < 0 or t.kind == kind }
# the first active one of a kind (and of that id, unless id < 0), in the order placed # the first-placed active one of a kind (and of that id, unless id < 0)
export function thing_find(things_st: ThingsState, kind: int, id: int) -> Thing { export function thing_find(things_st: ThingsState, kind: int, id: int) -> Thing {
let all = things_all(things_st) let tb = things_st.th.tab
for i in 0 .. len(all) { var best: Thing = null
let t = all[i] if kind < 0 {
if t.active and th_of(t, kind) and (id < 0 or t.id == id) { return t } for r in 0 .. tb_len(tb) { best = th_earlier(best, tb.rec[r], id) }
return best
} }
return null let rows = ix_rows(things_st.th.kinds, kind)
for k in 0 .. len(rows) { best = th_earlier(best, tb.rec[rows[k]], id) }
return best
}
# the one of the two placed first that is active and has the id asked for
function th_earlier(best: Thing, t: Thing, id: int) -> Thing {
if not t.active or (id >= 0 and t.id != id) { return best }
if best == null or t.uid < best.uid { return t }
return best
} }
export function thing_by_uid(things_st: ThingsState, uid: int) -> Thing { export function thing_by_uid(things_st: ThingsState, uid: int) -> Thing {
let all = things_all(things_st) let r = tb_row(things_st.th.tab, imap_get(things_st.th_uids, uid, -1))
for i in 0 .. len(all) { if all[i].uid == uid { return all[i] } } if r < 0 { return null }
return null return things_st.th.tab.rec[r]
} }
# the nearest active one of a kind to (x, z), or null # the nearest active one of a kind to (x, z), or null
export function thing_nearest(things_st: ThingsState, kind: int, x: float, z: float) -> Thing { export function thing_nearest(things_st: ThingsState, kind: int, x: float, z: float) -> Thing {
var best: Thing = null let tb = things_st.th.tab
var bd = 0.0 var r = -1
let all = things_all(things_st) if kind < 0 { r = tb_nearest(tb, things_st.th.grid, x, z, 0.0, -1, 0) } else {
for i in 0 .. len(all) { r = tb_nearest_of(tb, things_st.th.grid, things_st.th.kinds, x, z, 0.0, kind)
let t = all[i]
if not t.active or not th_of(t, kind) { continue }
let d = thing_dist2(t, x, z)
if best == null or d < bd {
best = t
bd = d
} }
} if r < 0 { return null }
return best return tb.rec[r]
} }
# the squared ground distance from a Thing to (x, z) # the squared ground distance from a Thing to (x, z)
@ -42,14 +47,13 @@ export function thing_dist2(t: Thing, x: float, z: float) -> float {
return dx * dx + dz * dz return dx * dx + dz * dz
} }
# every active one within r of (x, z), nearest first # every active one within r of (x, z), nearest first, in a new list: for a question asked now and
# then - a frame's question is things_near, into a list the caller keeps
export function things_within(things_st: ThingsState, x: float, z: float, r: float) -> []Thing { export function things_within(things_st: ThingsState, x: float, z: float, r: float) -> []Thing {
let found = new []Thing
things_near(things_st, x, z, r, found)
let out = new []Thing let out = new []Thing
let all = things_all(things_st) for k in 0 .. len(found) { th_insert(out, found[k], x, z) }
for i in 0 .. len(all) {
let t = all[i]
if t.active and thing_dist2(t, x, z) <= r * r { th_insert(out, t, x, z) }
}
return out return out
} }
@ -63,18 +67,44 @@ function th_insert(out: []Thing, t: Thing, x: float, z: float) -> void {
out[j] = t out[j] = t
} }
# every active one of a kind, in the order placed # every active one of a kind, first placed first
export function things_each(things_st: ThingsState, kind: int) -> []Thing { export function things_each(things_st: ThingsState, kind: int) -> []Thing {
let out = new []Thing return th_listed(things_st, ix_rows(things_st.th.kinds, kind))
let all = things_all(things_st)
for i in 0 .. len(all) { if all[i].active and th_of(all[i], kind) { push(out, all[i]) } }
return out
} }
# every one of a kind, active or not # every one of a kind, active or not
export function things_each_all(things_st: ThingsState, kind: int) -> []Thing { export function things_each_all(things_st: ThingsState, kind: int) -> []Thing {
let out = new []Thing return th_listed(things_st, ix_rows(things_st.th.every, kind))
let all = things_all(things_st) }
for i in 0 .. len(all) { if th_of(all[i], kind) { push(out, all[i]) } }
return out # how many active ones of a kind there are, without a list
export function things_count_of(things_st: ThingsState, kind: int) -> int {
return ix_count(things_st.th.kinds, kind)
}
function th_listed(things_st: ThingsState, rows: words) -> []Thing {
let out = new []Thing
for k in 0 .. len(rows) { push(out, things_st.th.tab.rec[rows[k]]) }
return th_by_uid_order(out)
}
function th_by_uid_order(xs: []Thing) -> []Thing {
for i in 1 .. len(xs) {
let t = xs[i]
var j = i
while j > 0 and xs[j - 1].uid > t.uid {
xs[j] = xs[j - 1]
j -= 1
}
xs[j] = t
}
return xs
}
# the i-th active one of a kind, 0 .. things_count_of(kind) - 1, in the index's order (which a
# removal or a hide changes: walk it within a frame, never keep an i)
export function thing_of_kind(things_st: ThingsState, kind: int, i: int) -> Thing {
let rows = ix_rows(things_st.th.kinds, kind)
if i < 0 or i >= len(rows) { return null }
return things_st.th.tab.rec[rows[i]]
} }

View file

@ -1,10 +1,42 @@
# store.ludic - the Things, and the only verbs that make, remove, move, show and hide one; each # store.ludic - the Things, rows of a ludic.base Table: where each stands and whether it is active
# placing and removing is a fact, and the port is told at once so the game can draw it # are columns a grid and two kind indexes are kept over, and only the verbs (verbs.ludic) make,
# move, show, hide and remove one; each placing and removing is a fact the port hears at once
export const TH_X: int = 0 # float columns
export const TH_Z: int = 1
export const TH_KIND: int = 0 # int columns
export const TH_ON: int = 1 # 1 while active
export const TH_CHUNK: int = 2 # the chunk of the world it stands in (chunk_of, TH_CHUNK_M)
export const TH_CHUNK_M: float = 256.0
const TH_CELL: float = 16.0
# the table and the indexes kept over it, made together
export property ThingsTable {
tab: Table<Thing> = null
grid: Grid = null
kinds: IntIndex = null # active Things by kind
every: IntIndex = null # every Thing by kind, active or not
chunks: IntIndex = null # every Thing by the chunk it stands in: loaded and let go together
}
export state ThingsState { export state ThingsState {
th_list: []Thing = new []Thing th: ThingsTable = th__new()
th_uids: IntMap = imap_new()
th_uid: int = 0 th_uid: int = 0
th_facts: Queue<ThingFact> = th_facts__new() th_facts: Queue<ThingFact> = th_facts__new()
th_marked: int = -1 th_hs: words = words(0) # things_tick's handles, taken before any tick runs
}
function th_facts__new() -> Queue<ThingFact> { return queue_new("things.facts") }
function th__new() -> ThingsTable {
let th = new ThingsTable
let tb: Table<Thing> = table_new(2, 3)
th.tab = tb
th.grid = tb_grid(tb, TH_X, TH_Z, TH_ON, TH_CELL)
th.kinds = tb_index(tb, TH_KIND, TH_ON)
th.every = tb_index(tb, TH_KIND, -1)
th.chunks = tb_index(tb, TH_CHUNK, -1)
return th
} }
export function things_facts(things_st: ThingsState) -> Queue<ThingFact> { export function things_facts(things_st: ThingsState) -> Queue<ThingFact> {
@ -24,88 +56,37 @@ function th_fact(things_st: ThingsState, what: int, t: Thing) -> void {
} }
# whether a world has been set out (things_clear) since the program began # whether a world has been set out (things_clear) since the program began
export function things_ready(things_st: ThingsState) -> bool { return things_st.th_list != null } export function things_ready(things_st: ThingsState) -> bool { return things_st.th.tab != null }
# every Thing, in the order placed; the list is the package's, never to be pushed to # every Thing, one per row - the table's own list: read it, never push to it. A removal moves the
export function things_all(things_st: ThingsState) -> []Thing { # last Thing into the removed one's place, so the order is the table's, not the order placed.
return things_st.th_list export function things_all(things_st: ThingsState) -> []Thing { return things_st.th.tab.rec }
}
export function things_count(things_st: ThingsState) -> int { return len(things_all(things_st)) } export function things_count(things_st: ThingsState) -> int { return tb_len(things_st.th.tab) }
# a new world: every Thing forgotten, quietly (the game has already dropped its drawings) # a new world: every Thing forgotten, quietly (the game has already dropped its drawings)
export function things_clear(things_st: mut ThingsState) -> void { things_st.th_list = new []Thing } export function things_clear(things_st: mut ThingsState) -> void {
let tb = things_st.th.tab
export function thing_spawn(things_st: mut ThingsState, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing { for r in 0 .. tb_len(tb) { tb.rec[r].ent = -1 }
return th_place(things_st, new Thing, kind, look, x, y, z, yaw, id) tb_clear(tb)
imap_clear(things_st.th_uids)
} }
function th_place(things_st: mut ThingsState, t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing { # the row a Thing stands in, or -1 when it is not in the table any more
t.kind = kind function th_row(t: Thing) -> int {
t.look = look if t == null or t.tab == null { return -1 }
t.x = x return tb_row(t.tab, t.ent)
t.y = y
t.z = z
t.yaw = yaw
t.id = id
things_st.th_uid += 1
t.uid = things_st.th_uid
push(things_all(things_st), t)
ThingsWorld.placed(t)
th_fact(things_st, THING_PLACED, t)
return t
} }
# a Thing on the ground (the port's), set into it by `sink`, used from `reach` # the columns say what the record says: a Thing's fields written around the verbs are caught here
export function thing_put(things_st: mut ThingsState, kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing { export function things_verify(things_st: ThingsState) -> int {
let t = new Thing let tb = things_st.th.tab
t.reach = reach var bad = 0
return th_place(things_st, t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id) for r in 0 .. tb_len(tb) {
let t = tb.rec[r]
var on = 0
if t.active { on = 1 }
if tb.f[TH_X][r] != t.x or tb.f[TH_Z][r] != t.z or tb.i[TH_KIND][r] != t.kind or tb.i[TH_ON][r] != on or tb.i[TH_CHUNK][r] != chunk_of(t.x, t.z, TH_CHUNK_M) or t.ent != tb.ent[r] { bad += 1 }
} }
return bad
# gone for good
export function thing_remove(things_st: mut ThingsState, t: Thing) -> void {
if t == null { return }
things_st.th_marked = t.uid
things_drop(things_st, fn th_is_marked)
things_st.th_marked = -1
} }
function th_is_marked(things_st: ThingsState, t: Thing) -> bool { return t.uid == things_st.th_marked }
# every Thing `gone` says so is removed for good
export function things_drop(things_st: mut ThingsState, gone: fn(Thing) -> bool) -> void {
let keep = new []Thing
let all = things_all(things_st)
for i in 0 .. len(all) {
let t = all[i]
if gone(t) {
ThingsWorld.removed(t)
th_fact(things_st, THING_REMOVED, t)
} else {
push(keep, t)
}
}
things_st.th_list = keep
}
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
thing_move_to(t, x, ThingsWorld.ground(x, z), z, yaw)
}
export function thing_move_to(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
t.x = x
t.y = y
t.z = z
t.yaw = yaw
ThingsWorld.moved(t)
}
export function thing_set_active(t: Thing, on: bool) -> void {
if t == null { return }
t.active = on
ThingsWorld.shown(t)
}
export function thing_hide(t: Thing) -> void { thing_set_active(t, false) }
export function thing_show(t: Thing) -> void { thing_set_active(t, true) }
function th_facts__new() -> Queue<ThingFact> { return queue_new("things.facts") }

View file

@ -155,4 +155,64 @@ program ThingsTest {
expect_eq(t.owner, 0) expect_eq(t.owner, 0)
expect_eq(things_test_st.back, 1) expect_eq(things_test_st.back, 1)
} }
test "removals keep the indexes true: nearest, find, uid and the columns agree" (things_st: mut ThingsState) {
things_clear(things_st)
var keep: Thing = null
for i in 0 .. 400 {
let t = put(things_st, TH_BRANCH + (i % 2), float(i) * 3.0 - 600.0, float(i % 17) * 5.0)
if i == 250 { keep = t }
}
let gone = thing_find(things_st, TH_BRANCH, -1)
let uid = gone.uid
thing_remove(things_st, gone)
expect(thing_by_uid(things_st, uid) == null)
expect(thing_by_uid(things_st, keep.uid) == keep)
expect_eq(things_count(things_st), 399)
expect_eq(things_count_of(things_st, TH_BRANCH), 199)
thing_move(keep, 5000.0, 5000.0, 0.0)
expect(thing_nearest(things_st, -1, 4990.0, 4990.0) == keep)
thing_hide(keep)
expect(thing_nearest(things_st, -1, 4990.0, 4990.0) != keep)
expect_eq(things_verify(things_st), 0)
keep.x = 1.0
expect_eq(things_verify(things_st), 1)
}
test "a chunk's Things are counted and let go together; a move carries a Thing to its new chunk" (things_st: mut ThingsState, things_test_st: ThingsTestState) {
things_clear(things_st)
put(things_st, TH_BRANCH, 10.0, 10.0)
put(things_st, TH_BRANCH, 20.0, 30.0)
let far = put(things_st, TH_BRANCH, 600.0, 10.0)
let here = things_chunk(10.0, 10.0)
expect_eq(things_chunk_count(things_st, here), 2)
expect_eq(things_chunk_count(things_st, things_chunk(600.0, 10.0)), 1)
thing_move(far, 15.0, 15.0, 0.0)
expect_eq(things_chunk_count(things_st, here), 3)
facts(things_st)
let gone = things_test_st.gone
expect_eq(things_drop_chunk(things_st, here), 3)
expect_eq(things_count(things_st), 0)
expect_eq(things_test_st.gone, gone + 3)
expect_eq(len(facts(things_st)), 3)
expect_eq(things_verify(things_st), 0)
}
test "change detection names what a verb wrote since the tick a consumer kept" (things_st: mut ThingsState) {
things_clear(things_st)
let a = put(things_st, TH_BRANCH, 1.0, 0.0)
let b = put(things_st, TH_BRANCH, 2.0, 0.0)
let seen = things_now(things_st)
let rows = words(0)
let out = new []Thing
expect_eq(things_changed(things_st, seen, rows, out), 0)
thing_hide(b)
expect_eq(things_changed(things_st, seen, rows, out), 1)
expect(out[0] == b)
let later = things_now(things_st)
thing_move(a, 9.0, 9.0, 0.0)
expect_eq(things_changed(things_st, later, rows, out), 1)
expect(out[0] == a)
expect_eq(things_changed(things_st, seen, rows, out), 2)
}
} }

View file

@ -0,0 +1,69 @@
# verbs.ludic - the only ways a Thing is made, removed, moved, shown or hidden: each writes the
# record and the table's columns together, so the grid and the kind indexes never fall behind
export function thing_spawn(things_st: mut ThingsState, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
return th_place(things_st, new Thing, kind, look, x, y, z, yaw, id)
}
function th_place(things_st: mut ThingsState, t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
t.kind = kind
t.look = look
t.x = x
t.y = y
t.z = z
t.yaw = yaw
t.id = id
things_st.th_uid += 1
t.uid = things_st.th_uid
let tb = things_st.th.tab
t.tab = tb
t.ent = tb_add(tb, t)
th_columns(t)
imap_put(things_st.th_uids, t.uid, t.ent)
ThingsWorld.placed(t)
th_fact(things_st, THING_PLACED, t)
return t
}
# the record's x, z, kind and active into its row, refiled once
function th_columns(t: Thing) -> void {
let r = th_row(t)
if r < 0 { return }
let tb = t.tab
tb.f[TH_X][r] = t.x
tb.f[TH_Z][r] = t.z
tb.i[TH_KIND][r] = t.kind
var on = 0
if t.active { on = 1 }
tb.i[TH_ON][r] = on
tb.i[TH_CHUNK][r] = chunk_of(t.x, t.z, TH_CHUNK_M)
tb_refile(tb, r)
}
# a Thing on the ground (the port's), set into it by `sink`, used from `reach`
export function thing_put(things_st: mut ThingsState, kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing {
let t = new Thing
t.reach = reach
return th_place(things_st, t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id)
}
# gone for good
export function thing_remove(things_st: mut ThingsState, t: Thing) -> void {
if th_row(t) < 0 { return }
ThingsWorld.removed(t)
th_fact(things_st, THING_REMOVED, t)
imap_del(things_st.th_uids, t.uid)
tb_remove(t.tab, t.ent)
t.ent = -1
}
# every Thing `gone` says so is removed for good; from the last row down, so the row a removal
# moves into place has already been asked
export function things_drop(things_st: mut ThingsState, gone: fn(Thing) -> bool) -> void {
let tb = things_st.th.tab
var r = tb_len(tb) - 1
while r >= 0 {
let t = tb.rec[r]
if gone(t) { thing_remove(things_st, t) }
r -= 1
}
}

View file

@ -1,10 +1,11 @@
# call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there # call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there
# after; gone when its player leaves # after; gone when its player leaves
function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw: float) -> Vehicle { function ve_add(vehicles_st: VehiclesState, kind: int, owner: int, x: float, z: float, yaw: float) -> Vehicle {
if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null } if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null }
let v = new Vehicle let v = new Vehicle
v.nid = VehicleWorld.next_id() v.nid = VehicleWorld.next_id()
v.kind = kind v.kind = kind
v.owner = owner
v.yaw = yaw v.yaw = yaw
if kind == VEHICLE_HORSE { if kind == VEHICLE_HORSE {
v.seat_h = 1.35 v.seat_h = 1.35
@ -14,7 +15,7 @@ function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw:
v.seat_h = 0.30 v.seat_h = 0.30
} }
ve_put(v, x, z) ve_put(v, x, z)
push(ve_all(vehicles_st), v) ve_file(vehicles_st.ve, v)
ve_settle(v) ve_settle(v)
VehicleWorld.made(v) VehicleWorld.made(v)
return v return v
@ -51,9 +52,8 @@ export function vehicle_call_for(vehicles_st: VehiclesState, pid: int, kind: int
var yaw = vehicles_st.ve_dock_yaw var yaw = vehicles_st.ve_dock_yaw
if kind == VEHICLE_HORSE { yaw = vehicles_st.ve_rail_yaw } if kind == VEHICLE_HORSE { yaw = vehicles_st.ve_rail_yaw }
if v == null { if v == null {
let made = ve_add(vehicles_st, kind, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k), yaw) let made = ve_add(vehicles_st, kind, pid, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k), yaw)
if made == null { return false } if made == null { return false }
made.owner = pid
if pid == 0 { ve_apply_kept(vehicles_st, made) } if pid == 0 { ve_apply_kept(vehicles_st, made) }
return true return true
} }
@ -68,10 +68,10 @@ export function vehicle_call_for(vehicles_st: VehiclesState, pid: int, kind: int
# a player left: their boat and horse go with them, and their places are free # a player left: their boat and horse go with them, and their places are free
export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> void { export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> void {
if pid == 0 { return } if pid == 0 { return }
let keep = new []Vehicle let rows = ix_rows(vehicles_st.ve.owners, pid)
let l = ve_all(vehicles_st) while len(rows) > 0 {
for i in 0 .. len(l) { let v = ve_all(vehicles_st)[rows[len(rows) - 1]]
if l[i].owner != pid { push(keep, l[i]) } else { ve_gone(l[i]) } ve_gone(v)
ve_unfile(vehicles_st.ve, v)
} }
vehicles_st.ve_list = keep
} }

View file

@ -5,6 +5,7 @@ module ludic_vehicles uses ludic_base
numbers float numbers float
import "ludic.base" import "ludic.base"
import "kinds.ludic" import "kinds.ludic"
import "table.ludic"
import "ports.ludic" import "ports.ludic"
import "state.ludic" import "state.ludic"
import "places.ludic" import "places.ludic"

View file

@ -19,6 +19,8 @@ export property Vehicle {
rider: int = -1 # the player on it, -1 nobody rider: int = -1 # the player on it, -1 nobody
home_x: float = 0.0 # a horse is tied where its home is home_x: float = 0.0 # a horse is tied where its home is
home_z: float = 0.0 home_z: float = 0.0
ent: int = -1 # its handle in the table
tab: Table<Vehicle> = null
} }
export const VEHICLE_MOUNTED: int = 0 export const VEHICLE_MOUNTED: int = 0

View file

@ -45,7 +45,7 @@ export state VehiclesState {
ve_ids: int = 0 ve_ids: int = 0
ve_sx: float = 0.0 ve_sx: float = 0.0
ve_sz: float = 0.0 ve_sz: float = 0.0
ve_list: []Vehicle = new []Vehicle ve: VehicleTable = ve__new()
ve_cur: Vehicle = null ve_cur: Vehicle = null
ve_dock_x: float = 0.0 ve_dock_x: float = 0.0
ve_dock_z: float = 0.0 ve_dock_z: float = 0.0

View file

@ -18,7 +18,7 @@ function ve_say(vehicles_st: VehiclesState, what: int, v: Vehicle, n: float) ->
q_push(vehicle_facts(vehicles_st), f) q_push(vehicle_facts(vehicles_st), f)
} }
function ve_all(vehicles_st: VehiclesState) -> []Vehicle { return vehicles_st.ve_list } function ve_all(vehicles_st: VehiclesState) -> []Vehicle { return vehicles_st.ve.tab.rec }
# every vehicle in the valley, to draw and to aim at (change one only through the verbs) # every vehicle in the valley, to draw and to aim at (change one only through the verbs)
export function vehicles_all(vehicles_st: VehiclesState) -> []Vehicle { return ve_all(vehicles_st) } export function vehicles_all(vehicles_st: VehiclesState) -> []Vehicle { return ve_all(vehicles_st) }
@ -32,8 +32,9 @@ export function vehicle_ride_kind(vehicles_st: VehiclesState) -> int {
export function vehicle_current(vehicles_st: VehiclesState) -> Vehicle { return vehicles_st.ve_cur } export function vehicle_current(vehicles_st: VehiclesState) -> Vehicle { return vehicles_st.ve_cur }
export function vehicle_of(vehicles_st: VehiclesState, pid: int, kind: int) -> Vehicle { export function vehicle_of(vehicles_st: VehiclesState, pid: int, kind: int) -> Vehicle {
let rows = ix_rows(vehicles_st.ve.owners, pid)
let l = ve_all(vehicles_st) let l = ve_all(vehicles_st)
for i in 0 .. len(l) { if l[i].kind == kind and l[i].owner == pid { return l[i] } } for k in 0 .. len(rows) { if l[rows[k]].kind == kind { return l[rows[k]] } }
return null return null
} }

View file

@ -2,13 +2,14 @@
# and the horse's hay); where they lie is not saved - they are called again. The boats' hulls are # and the horse's hay); where they lie is not saved - they are called again. The boats' hulls are
# read back and pushed by the wind in vehicles_tick, which the game calls where the world is run # read back and pushed by the wind in vehicles_tick, which the game calls where the world is run
export function vehicles_reset(vehicles_st: mut VehiclesState) -> void { export function vehicles_reset(vehicles_st: mut VehiclesState) -> void {
if vehicles_st.ve_list != null { let l = ve_all(vehicles_st)
for i in 0 .. len(vehicles_st.ve_list) { for i in 0 .. len(l) {
let v = vehicles_st.ve_list[i] let v = l[i]
if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) } if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) }
v.ent = -1
} }
} tb_clear(vehicles_st.ve.tab)
vehicles_st.ve_list = new []Vehicle imap_clear(vehicles_st.ve.nids)
vehicles_st.ve_cur = null vehicles_st.ve_cur = null
vehicles_st.ve_have_dock = false vehicles_st.ve_have_dock = false
vehicles_st.ve_swell_said = 0.0 vehicles_st.ve_swell_said = 0.0

View file

@ -0,0 +1,45 @@
# table.ludic - the vehicles, rows of a ludic.base Table: kind and owner as columns, each player's
# own by index, and a nid to its handle. Where one is stays on its record: a boat moves every frame
# and there are never more than two a player to search.
export const VE_KIND: int = 0 # int columns
export const VE_OWNER: int = 1
export property VehicleTable {
tab: Table<Vehicle> = null
owners: IntIndex = null # every vehicle by the player it belongs to
nids: IntMap = null
}
function ve__new() -> VehicleTable {
let w = new VehicleTable
let tb: Table<Vehicle> = table_new(0, 2)
w.tab = tb
w.owners = tb_index(tb, VE_OWNER, -1)
w.nids = imap_new()
return w
}
# a new row for v, its kind and owner from its record
function ve_file(w: VehicleTable, v: Vehicle) -> void {
v.tab = w.tab
v.ent = tb_add(w.tab, v)
let r = tb_row(w.tab, v.ent)
w.tab.i[VE_KIND][r] = v.kind
w.tab.i[VE_OWNER][r] = v.owner
tb_refile(w.tab, r)
imap_put(w.nids, v.nid, v.ent)
}
function ve_unfile(w: VehicleTable, v: Vehicle) -> void {
imap_del(w.nids, v.nid)
tb_remove(w.tab, v.ent)
v.ent = -1
}
# the vehicle a message names, or null
export function vehicle_by_nid(vehicles_st: VehiclesState, nid: int) -> Vehicle {
let tb = vehicles_st.ve.tab
let r = tb_row(tb, imap_get(vehicles_st.ve.nids, nid, -1))
if r < 0 or tb.rec[r].nid != nid { return null }
return tb.rec[r]
}

View file

@ -344,4 +344,23 @@ program VehiclesTest {
expect_near(vehicle_find(vehicles_st, VEHICLE_HORSE).food, 77.0, 0.01) expect_near(vehicle_find(vehicles_st, VEHICLE_HORSE).food, 77.0, 0.01)
expect(not vehicle_find(vehicles_st, VEHICLE_BOAT).owned) expect(not vehicle_find(vehicles_st, VEHICLE_BOAT).owned)
} }
test "the table: a nid finds its vehicle, each player's own by the owner index, and a release takes both" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {
fresh(vehicles_st, vehicles_test_st)
vehicle_call_for(vehicles_st, 5, VEHICLE_HORSE, 0)
vehicle_call_for(vehicles_st, 5, VEHICLE_BOAT, 0)
vehicle_call_for(vehicles_st, 6, VEHICLE_BOAT, 1)
let b = vehicle_of(vehicles_st, 6, VEHICLE_BOAT)
expect(b != null)
expect(vehicle_by_nid(vehicles_st, b.nid) == b)
expect(vehicle_of(vehicles_st, 6, VEHICLE_HORSE) == null)
let h = vehicle_of(vehicles_st, 5, VEHICLE_HORSE)
vehicles_release(vehicles_st, 5)
expect_eq(len(vehicles_all(vehicles_st)), 1)
expect(vehicle_by_nid(vehicles_st, h.nid) == null)
expect(vehicle_of(vehicles_st, 6, VEHICLE_BOAT) == b)
vehicles_reset(vehicles_st)
expect_eq(len(vehicles_all(vehicles_st)), 0)
expect(vehicle_by_nid(vehicles_st, b.nid) == null)
}
} }

View file

@ -64,4 +64,6 @@ export property WildAnimal {
sus_z: float = 0.0 sus_z: float = 0.0
spook: float = 0.0 # game minutes of extra wariness after being flushed spook: float = 0.0 # game minutes of extra wariness after being flushed
seen: float = 0.0 # the alert it has already reacted to seen: float = 0.0 # the alert it has already reacted to
ent: int = -1 # its handle in the table
tab: Table<WildAnimal> = null # the table holding it, so a verb on the animal alone keeps the indexes
} }

View file

@ -3,13 +3,13 @@
export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void { export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void {
if wildlife_st.wl_own != null { return } if wildlife_st.wl_own != null { return }
wildlife_st.wl_own = wildlife_all(wildlife_st) wildlife_st.wl_own = wildlife_st.wl
wildlife_st.wl_list = new []WildAnimal wildlife_st.wl = wl__new()
} }
export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void { export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void {
if wildlife_st.wl_own == null { return } if wildlife_st.wl_own == null { return }
wildlife_st.wl_list = wildlife_st.wl_own wildlife_st.wl = wildlife_st.wl_own
wildlife_st.wl_own = null wildlife_st.wl_own = null
} }

View file

@ -8,7 +8,9 @@ import "animal.ludic"
import "places.ludic" import "places.ludic"
import "port.ludic" import "port.ludic"
import "facts.ludic" import "facts.ludic"
import "table.ludic"
import "store.ludic" import "store.ludic"
import "near.ludic"
import "guest.ludic" import "guest.ludic"
import "record.ludic" import "record.ludic"
import "habitat.ludic" import "habitat.ludic"

View file

@ -0,0 +1,15 @@
# near.ludic - the animals around a point, living ones only, into a list the CALLER keeps (in its own
# state, reused every frame): from the grid, or a species' own list when it is rare. Nothing is
# allocated or written here.
export function wildlife_near(wildlife_st: WildlifeState, x: float, z: float, r: float, out: []WildAnimal) -> int {
let w = wildlife_st.wl
return tb_within_recs(w.tab, w.grid, x, z, r, -1, 0, out)
}
export function wildlife_near_of(wildlife_st: WildlifeState, sp: int, x: float, z: float, r: float, out: []WildAnimal) -> int {
let w = wildlife_st.wl
return tb_within_of_recs(w.tab, w.grid, w.species, x, z, r, sp, out)
}
# how many of a species are alive, legends included, without a walk
export function wildlife_alive_of(wildlife_st: WildlifeState, sp: int) -> int { return ix_count(wildlife_st.wl.species, sp) }

View file

@ -4,6 +4,7 @@ export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: flo
wl_seed_scan(wildlife_st, dt) wl_seed_scan(wildlife_st, dt)
let all = wildlife_all(wildlife_st) let all = wildlife_all(wildlife_st)
for i in 0 .. len(all) { wl_tick_one(wildlife_st, all[i], dt, gh) } for i in 0 .. len(all) { wl_tick_one(wildlife_st, all[i], dt, gh) }
wildlife_refile(wildlife_st)
} }
function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void { function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void {

View file

@ -34,7 +34,7 @@ export property WildSpecies {
export state WildlifeState { export state WildlifeState {
wl_facts: Queue<WildFact> = wl_facts__new() wl_facts: Queue<WildFact> = wl_facts__new()
wl_own: []WildAnimal = null wl_own: WildTable = null # a guest's own animals, put away whole while it draws the host's
wl_px: float = 0.0 wl_px: float = 0.0
wl_pz: float = 0.0 wl_pz: float = 0.0
wl_ask: WildLure = new WildLure wl_ask: WildLure = new WildLure
@ -53,7 +53,7 @@ export state WildlifeState {
wl_fed_any: bool = false wl_fed_any: bool = false
wl_near_i: int = 0 wl_near_i: int = 0
wl_species: []WildSpecies = new []WildSpecies wl_species: []WildSpecies = new []WildSpecies
wl_list: []WildAnimal = new []WildAnimal wl: WildTable = wl__new()
wl_keys: int = 0 wl_keys: int = 0
wl_dice: Rng = rng_new(97) wl_dice: Rng = rng_new(97)
wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed

View file

@ -1,14 +1,20 @@
# store.ludic - the animals, their own dice, and the verbs that make and take one away # store.ludic - the animals, their own dice, and the verbs that make and take one away
const WL_TAU: float = 6.2831853 const WL_TAU: float = 6.2831853
# every animal, one per row - the table's own list: read it, never push to it
export function wildlife_all(wildlife_st: WildlifeState) -> []WildAnimal { export function wildlife_all(wildlife_st: WildlifeState) -> []WildAnimal {
return wildlife_st.wl_list return wildlife_st.wl.tab.rec
} }
export function wildlife_count_all(wildlife_st: WildlifeState) -> int { return len(wildlife_all(wildlife_st)) } export function wildlife_count_all(wildlife_st: WildlifeState) -> int { return tb_len(wildlife_st.wl.tab) }
# a new valley: every animal forgotten, quietly (the game has already let its drawings go) # a new valley: every animal forgotten, quietly (the game has already let its drawings go)
export function wildlife_clear(wildlife_st: mut WildlifeState) -> void { wildlife_st.wl_list = new []WildAnimal } export function wildlife_clear(wildlife_st: mut WildlifeState) -> void {
let tb = wildlife_st.wl.tab
for r in 0 .. tb_len(tb) { tb.rec[r].ent = -1 }
tb_clear(tb)
imap_clear(wildlife_st.wl.nids)
}
function wl_rng(wildlife_st: WildlifeState) -> Rng { function wl_rng(wildlife_st: WildlifeState) -> Rng {
return wildlife_st.wl_dice return wildlife_st.wl_dice
@ -40,7 +46,7 @@ export function wildlife_make(wildlife_st: mut WildlifeState, sp: int, x: float,
a.scale = scale a.scale = scale
a.legend = legend a.legend = legend
a.state = WILD_IDLE a.state = WILD_IDLE
push(wildlife_all(wildlife_st), a) wl_file(wildlife_st.wl, a)
WildlifeWorld.born(a) WildlifeWorld.born(a)
return a return a
} }
@ -67,7 +73,7 @@ export function wildlife_new(wildlife_st: mut WildlifeState, sp: int, x: float,
# gone for good: not drawn, not ticked # gone for good: not drawn, not ticked
export function wildlife_remove(a: WildAnimal) -> void { export function wildlife_remove(a: WildAnimal) -> void {
if a == null { return } if a == null { return }
a.alive = false wildlife_set_alive(a, false)
a.shown = false a.shown = false
} }
@ -75,15 +81,16 @@ export function wildlife_remove(a: WildAnimal) -> void {
export function wildlife_hold(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_hold = on } export function wildlife_hold(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_hold = on }
export function wildlife_by_nid(wildlife_st: WildlifeState, nid: int) -> WildAnimal { export function wildlife_by_nid(wildlife_st: WildlifeState, nid: int) -> WildAnimal {
let all = wildlife_all(wildlife_st) let tb = wildlife_st.wl.tab
for i in 0 .. len(all) { if all[i].nid == nid { return all[i] } } let r = tb_row(tb, imap_get(wildlife_st.wl.nids, nid, -1))
return null if r < 0 or tb.rec[r].nid != nid { return null }
return tb.rec[r]
} }
# how many of a species are alive, legends apart # how many of a species are alive, legends apart
export function wildlife_count(wildlife_st: WildlifeState, sp: int) -> int { export function wildlife_count(wildlife_st: WildlifeState, sp: int) -> int {
var n = 0 var n = 0
let all = wildlife_all(wildlife_st) let rows = ix_rows(wildlife_st.wl.species, sp)
for i in 0 .. len(all) { if all[i].alive and all[i].sp == sp and not all[i].legend { n += 1 } } for k in 0 .. len(rows) { if not wildlife_st.wl.tab.rec[rows[k]].legend { n += 1 } }
return n return n
} }

View file

@ -0,0 +1,91 @@
# table.ludic - the animals, rows of a ludic.base Table: where each stands, its species and whether
# it lives are columns, with a grid over the living, the living by species, and a nid to its handle.
# The AI moves an animal by its record; wildlife_refile files every move once, after the tick.
export const WA_X: int = 0 # float columns
export const WA_Z: int = 1
export const WA_SP: int = 0 # int columns
export const WA_ALIVE: int = 1 # 1 while alive
const WA_CELL: float = 32.0
export property WildTable {
tab: Table<WildAnimal> = null
grid: Grid = null
species: IntIndex = null # the living, by species
nids: IntMap = null
}
function wl__new() -> WildTable {
let w = new WildTable
let tb: Table<WildAnimal> = table_new(2, 2)
w.tab = tb
w.grid = tb_grid(tb, WA_X, WA_Z, WA_ALIVE, WA_CELL)
w.species = tb_index(tb, WA_SP, WA_ALIVE)
w.nids = imap_new()
return w
}
function wl_row(a: WildAnimal) -> int {
if a == null or a.tab == null { return -1 }
return tb_row(a.tab, a.ent)
}
# a new row for `a`, its columns from its record
function wl_file(w: WildTable, a: WildAnimal) -> void {
a.tab = w.tab
a.ent = tb_add(w.tab, a)
let r = wl_row(a)
let tb = w.tab
tb.f[WA_X][r] = a.x
tb.f[WA_Z][r] = a.z
tb.i[WA_SP][r] = a.sp
var on = 0
if a.alive { on = 1 }
tb.i[WA_ALIVE][r] = on
tb_refile(tb, r)
if a.nid != 0 { imap_put(w.nids, a.nid, a.ent) }
}
# alive or not, the record and the row together (a copy the game is handed says so too)
export function wildlife_set_alive(a: WildAnimal, on: bool) -> void {
if a == null { return }
a.alive = on
let r = wl_row(a)
if r < 0 { return }
var v = 0
if on { v = 1 }
tb_set_i(a.tab, WA_ALIVE, r, v)
}
# every row whose record moved since it was filed, refiled: once after a tick that moves them all,
# and by the game after it moves the copies a guest is handed
export function wildlife_refile(wildlife_st: mut WildlifeState) -> void {
let tb = wildlife_st.wl.tab
let xs = tb.f[WA_X]
let zs = tb.f[WA_Z]
for r in 0 .. tb_len(tb) {
let a = tb.rec[r]
if xs[r] != a.x or zs[r] != a.z { tb_set_xz(tb, WA_X, WA_Z, r, a.x, a.z) }
}
}
# after a refile, the columns say what the records say; the count of rows that do not
export function wildlife_verify(wildlife_st: WildlifeState) -> int {
let tb = wildlife_st.wl.tab
var bad = 0
for r in 0 .. tb_len(tb) {
let a = tb.rec[r]
var on = 0
if a.alive { on = 1 }
if tb.f[WA_X][r] != a.x or tb.f[WA_Z][r] != a.z or tb.i[WA_SP][r] != a.sp or tb.i[WA_ALIVE][r] != on or a.ent != tb.ent[r] { bad += 1 }
}
return bad
}
# the nearest living animal of a species (below 0: any) within maxr (0: any distance), or null
export function wildlife_nearest(wildlife_st: WildlifeState, sp: int, x: float, z: float, maxr: float) -> WildAnimal {
let w = wildlife_st.wl
var r = -1
if sp < 0 { r = tb_nearest(w.tab, w.grid, x, z, maxr, -1, 0) } else { r = tb_nearest_of(w.tab, w.grid, w.species, x, z, maxr, sp) }
if r < 0 { return null }
return w.tab.rec[r]
}

View file

@ -230,4 +230,33 @@ program WildlifeTest {
expect_near(r.x, r.wx, 0.001) expect_near(r.x, r.wx, 0.001)
expect(r.x != x0 or r.wx == r.sx) expect(r.x != x0 or r.wx == r.sx)
} }
test "the table: a nid finds its animal, the living count by species, the nearest, and a refile" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) {
fk_setup(wildlife_st, wildlife_tests_fake_st)
wildlife_clear(wildlife_st)
let a = wildlife_make(wildlife_st, 0, 10.0, 0.0, false, 0.0, 0, 1.0, 41)
let b = wildlife_make(wildlife_st, 0, 90.0, 0.0, false, 0.0, 0, 1.0, 42)
let c = wildlife_make(wildlife_st, 1, 30.0, 0.0, true, 0.0, 0, 1.0, 43)
expect(wildlife_by_nid(wildlife_st, 42) == b)
expect(wildlife_by_nid(wildlife_st, 7) == null)
expect_eq(wildlife_count(wildlife_st, 0), 2)
expect_eq(wildlife_count(wildlife_st, 1), 0)
expect(wildlife_nearest(wildlife_st, -1, 0.0, 0.0, 0.0) == a)
expect(wildlife_nearest(wildlife_st, 1, 0.0, 0.0, 0.0) == c)
let near = new []WildAnimal
expect_eq(wildlife_near(wildlife_st, 0.0, 0.0, 40.0, near), 2)
expect_eq(wildlife_near_of(wildlife_st, 0, 0.0, 0.0, 200.0, near), 2)
expect_eq(wildlife_alive_of(wildlife_st, 1), 1)
wildlife_remove(a)
expect_eq(wildlife_count(wildlife_st, 0), 1)
expect(wildlife_nearest(wildlife_st, 0, 0.0, 0.0, 0.0) == b)
b.x = -5.0
wildlife_refile(wildlife_st)
expect(wildlife_nearest(wildlife_st, -1, 0.0, 0.0, 20.0) == b)
expect_eq(wildlife_verify(wildlife_st), 0)
expect_eq(wildlife_count_all(wildlife_st), 3)
wildlife_clear(wildlife_st)
expect_eq(wildlife_count_all(wildlife_st), 0)
expect(wildlife_by_nid(wildlife_st, 42) == null)
}
} }

View file

@ -136,9 +136,11 @@ function emit_component_access(prop: pointer, meth: pointer, e: Node) -> Val {
let lt = emit_bind(`icmp slt i32 {i0}, {ec}`) let lt = emit_bind(`icmp slt i32 {i0}, {ec}`)
emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n") emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n")
emit(body); emit(":\n") emit(body); emit(":\n")
let ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {i0}`) let eb1 = ecs_base("L_alive")
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb1}, i32 {i0}`)
let al = emit_bind(`load i32, ptr {ap}`) let al = emit_bind(`load i32, ptr {ap}`)
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, i32 {i0}`) let eb2 = ecs_base(`H_{prop}`)
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb2}, i32 {i0}`)
let hv = emit_bind(`load i8, ptr {hp}`) let hv = emit_bind(`load i8, ptr {hp}`)
let hz = emit_bind(`zext i8 {hv} to i32`) let hz = emit_bind(`zext i8 {hv} to i32`)
let both = emit_bind(`and i32 {al}, {hz}`) let both = emit_bind(`and i32 {al}, {hz}`)
@ -154,15 +156,19 @@ function emit_component_access(prop: pointer, meth: pointer, e: Node) -> Val {
if len(e.kids) != 1 { perr(`{prop}.{meth} takes one argument: the entity`) } if len(e.kids) != 1 { perr(`{prop}.{meth} takes one argument: the entity`) }
let ev = emit_expr(e.kids[0]) let ev = emit_expr(e.kids[0])
if (meth == "of") { if (meth == "of") {
let slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_{prop}], ptr @S_{prop}, i32 0, i32 {ev.code}`) let eb3 = ecs_base(`S_{prop}`)
let slot = emit_bind(`getelementptr inbounds %Cmp_{prop}, ptr {eb3}, i32 {ev.code}`)
return val(slot, prop) return val(slot, prop)
} }
let in_range = emit_bind(`icmp ult i32 {ev.code}, {me}`) let cap = emit_bind("load i32, ptr @L_cap")
let in_range = emit_bind(`icmp ult i32 {ev.code}, {cap}`)
let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`) let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`)
let ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {idx}`) let eb4 = ecs_base("L_alive")
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb4}, i32 {idx}`)
let alive = emit_bind(`load i32, ptr {ap}`) let alive = emit_bind(`load i32, ptr {ap}`)
let is_alive = emit_bind(`icmp ne i32 {alive}, 0`) let is_alive = emit_bind(`icmp ne i32 {alive}, 0`)
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, i32 {idx}`) let eb5 = ecs_base(`H_{prop}`)
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb5}, i32 {idx}`)
let hv = emit_bind(`load i8, ptr {hp}`) let hv = emit_bind(`load i8, ptr {hp}`)
let carries = emit_bind(`icmp ne i8 {hv}, 0`) let carries = emit_bind(`icmp ne i8 {hv}, 0`)
let ok1 = emit_bind(`and i1 {in_range}, {is_alive}`) let ok1 = emit_bind(`and i1 {in_range}, {is_alive}`)
@ -372,7 +378,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
return emit_alias_or_fail("App", meth, e) return emit_alias_or_fail("App", meth, e)
} }
if (ns == "Pool") { if (ns == "Pool") {
if (meth == "capacity") { return val(itoa(MAX_ENT), "int") } # max entities if (meth == "capacity") { return val(emit_bind("load i32, ptr @L_cap"), "int") } # the stores' size now: they grow
if (meth == "reserved") { return val(emit_bind("load i32, ptr @L_entc"), "int") } # slots ever allocated (high-water) if (meth == "reserved") { return val(emit_bind("load i32, ptr @L_entc"), "int") } # slots ever allocated (high-water)
if (meth == "free") { return val(emit_bind("load i32, ptr @L_freen"), "int") } # recycled slots ready for reuse if (meth == "free") { return val(emit_bind("load i32, ptr @L_freen"), "int") } # recycled slots ready for reuse
if (meth == "live") { # currently alive = reserved - free if (meth == "live") { # currently alive = reserved - free
@ -407,7 +413,8 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
let at = emit_expr(e.kids[1]) let at = emit_expr(e.kids[1])
let ent = emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`) let ent = emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_Position], ptr @S_Position, i32 0, i32 {ent}`) let eb6 = ecs_base(`S_Position`)
let slot = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {eb6}, i32 {ent}`)
let xa = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "x"))}`) let xa = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "x"))}`)
let ya = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "y"))}`) let ya = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "y"))}`)
let ax = vec_x(at.code) let ax = vec_x(at.code)

View file

@ -61,6 +61,7 @@ function emit_main(d: Node) -> void {
# its framebuffer allocated. Headless it only allocates — no window, no output — # its framebuffer allocated. Headless it only allocates — no window, no output —
# so a non-rendering entry game is unchanged. # so a non-rendering entry game is unchanged.
emit(" call void @L_init_runtime()\n") emit(" call void @L_init_runtime()\n")
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") } if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") }
emit(" call void @L_init_globals()\n") emit(" call void @L_init_globals()\n")
emit_block(d.a) emit_block(d.a)
@ -149,6 +150,7 @@ function emit_test_runner() -> void {
emit(" ret i32 0\n") emit(" ret i32 0\n")
emit(`{lrun0}:\n`) emit(`{lrun0}:\n`)
emit(" call void @L_init_runtime()\n") emit(" call void @L_init_runtime()\n")
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
if has_ecs() { emit(" call void @rt_init()\n") } if has_ecs() { emit(" call void @rt_init()\n") }
emit(" call void @L_init_globals()\n") emit(" call void @L_init_globals()\n")
i = 0 i = 0

View file

@ -185,4 +185,11 @@ function check_worker_ref(e: Node) -> void {
var ok = len(d.kids) == s + 2 and llty(d.ty) == "void" var ok = len(d.kids) == s + 2 and llty(d.ty) == "void"
if ok { ok = llty(d.kids[s].ty) == "i32" and llty(d.kids[s + 1].ty) == "ptr" } if ok { ok = llty(d.kids[s].ty) == "i32" and llty(d.kids[s + 1].ty) == "ptr" }
if not ok { perr(`fn {e.s}: a worker function takes (i: int, ctx: pointer) and returns nothing`) } if not ok { perr(`fn {e.s}: a worker function takes (i: int, ctx: pointer) and returns nothing`) }
# every thread runs it at once: a state it is handed is read by all of them, so none may change one
var k = 0
while k < s {
let p = d.kids[k]
if p.uns == 1 { perr(`fn {e.s}: a worker runs on every core at once, so it may read {p.ty} but not change it ({p.s}: mut {p.ty}); write results into ctx, shared counts through Sync.add`) }
k += 1
}
} }

View file

@ -56,9 +56,12 @@ function emit_ecs_storage() -> void {
if len(g_scenes) > 0 { emith("@L_scene = internal global i32 0\n") } # active base scene id if len(g_scenes) > 0 { emith("@L_scene = internal global i32 0\n") } # active base scene id
emith("@L_entc = internal global i32 0\n") emith("@L_entc = internal global i32 0\n")
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emith(`@L_alive = internal global [{me} x i32] zeroinitializer\n`) # the per-entity stores are heap blocks L_grow makes and doubles: @L_cap slots each, from MAX_ENT
emith(`@L_kind = internal global [{me} x i32] zeroinitializer\n`) emith("@L_cap = internal global i32 0\n")
emith(`@L_freelist = internal global [{me} x i32] zeroinitializer\n`) emith("@L_snapcap = internal global i32 0\n") # a snapshot's slot count, read before its stores
emith("@L_alive = internal global ptr null\n")
emith("@L_kind = internal global ptr null\n")
emith("@L_freelist = internal global ptr null\n")
emith("@L_freen = internal global i32 0\n") emith("@L_freen = internal global i32 0\n")
# NETWORKING role registers (N3/N5): a runtime sets these; offline they hold the # NETWORKING role registers (N3/N5): a runtime sets these; offline they hold the
# single-player default — @L_role=1 (this peer is the authority), local id 0. # single-player default — @L_role=1 (this peer is the authority), local id 0.
@ -68,7 +71,7 @@ function emit_ecs_storage() -> void {
emith("@L_role = internal global i32 1\n") # 1 = server/authority (offline default) emith("@L_role = internal global i32 1\n") # 1 = server/authority (offline default)
emith("@L_localid = internal global i32 0\n") # this peer's id emith("@L_localid = internal global i32 0\n") # this peer's id
} }
if net_has_owned() { emith(`@L_owner_arr = internal global [{me} x i32] zeroinitializer\n`) } if net_has_owned() { emith("@L_owner_arr = internal global ptr null\n") }
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let c = prog[i] let c = prog[i]
@ -76,8 +79,8 @@ function emit_ecs_storage() -> void {
# header). Every property in an ECS program is a component today; a property # header). Every property in an ECS program is a component today; a property
# used only via `new` would not need these, but no such program mixes the two. # used only via `new` would not need these, but no such program mixes the two.
if c.kind == N_COMP { if c.kind == N_COMP {
emith(`@S_{c.s} = internal global [{me} x %Cmp_{c.s}] zeroinitializer\n`) emith(`@S_{c.s} = internal global ptr null\n`)
emith(`@H_{c.s} = internal global [{me} x i8] zeroinitializer\n`) emith(`@H_{c.s} = internal global ptr null\n`)
} }
# one enabled-flag global per model and per handler (default enabled) # one enabled-flag global per model and per handler (default enabled)
if c.kind == N_ARCH { emith(`@ME_{c.s} = internal global i32 1\n`) } if c.kind == N_ARCH { emith(`@ME_{c.s} = internal global i32 1\n`) }
@ -92,21 +95,25 @@ function emit_ecs_storage() -> void {
# L_reset(e): clear every has-flag and the archetype kind for entity e # L_reset(e): clear every has-flag and the archetype kind for entity e
function emit_ecs_allocator() -> void { function emit_ecs_allocator() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit_ecs_grow()
emit("define void @L_reset(i32 %e) {\nentry:\n") emit("define void @L_reset(i32 %e) {\nentry:\n")
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_COMP { if prog[i].kind == N_COMP {
let hn = `%h{itoa(i)}` let hn = `%h{itoa(i)}`
emit(" "); emit(hn); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(prog[i].s); emit(", i32 0, i32 %e\n") let eb1 = ecs_base(`H_{prog[i].s}`)
emit(" "); emit(hn); emit(" = getelementptr inbounds i8, ptr "); emit(eb1); emit(", i32 %e\n")
emit(" store i8 0, ptr "); emit(hn); emit("\n") emit(" store i8 0, ptr "); emit(hn); emit("\n")
} }
i += 1 i += 1
} }
emit(" %k = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n") let eb2 = ecs_base("L_kind")
emit(" %k = getelementptr inbounds i32, ptr "); emit(eb2); emit(", i32 %e\n")
emit(" store i32 0, ptr %k\n") emit(" store i32 0, ptr %k\n")
# N3: reset an @Owned entity's network owner to -1 (unowned) on alloc/free # N3: reset an @Owned entity's network owner to -1 (unowned) on alloc/free
if net_has_owned() { if net_has_owned() {
emit(" %ow = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n") let eb3 = ecs_base("L_owner_arr")
emit(" %ow = getelementptr inbounds i32, ptr "); emit(eb3); emit(", i32 %e\n")
emit(" store i32 -1, ptr %ow\n") emit(" store i32 -1, ptr %ow\n")
} }
emit(" ret void\n}\n\n") emit(" ret void\n}\n\n")
@ -118,29 +125,100 @@ function emit_ecs_allocator() -> void {
emit("reuse:\n") emit("reuse:\n")
emit(" %fn1 = sub i32 %fn, 1\n") emit(" %fn1 = sub i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n") emit(" store i32 %fn1, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn1\n") let eb4 = ecs_base("L_freelist")
emit(" %fp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %fn1\n")
emit(" %re = load i32, ptr %fp\n") emit(" %re = load i32, ptr %fp\n")
emit(" br label %done\n") emit(" br label %done\n")
emit("fresh:\n") emit("fresh:\n")
emit(" %ec = load i32, ptr @L_entc\n") emit(" %ec = load i32, ptr @L_entc\n")
emit(" %ec1 = add i32 %ec, 1\n") emit(" %ec1 = add i32 %ec, 1\n")
emit(" call void @L_grow(i32 %ec1)\n") # a slot past the stores doubles them first
emit(" store i32 %ec1, ptr @L_entc\n") emit(" store i32 %ec1, ptr @L_entc\n")
emit(" br label %done\n") emit(" br label %done\n")
emit("done:\n") emit("done:\n")
emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n") emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n")
emit(" call void @L_reset(i32 %e)\n") emit(" call void @L_reset(i32 %e)\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") let eb5 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %e\n")
emit(" store i32 1, ptr %ap\n") emit(" store i32 1, ptr %ap\n")
emit(" ret i32 %e\n}\n\n") emit(" ret i32 %e\n}\n\n")
emit("define void @L_free_entity(i32 %e) {\nentry:\n") emit("define void @L_free_entity(i32 %e) {\nentry:\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") let eb6 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb6); emit(", i32 %e\n")
emit(" store i32 0, ptr %ap\n") emit(" store i32 0, ptr %ap\n")
emit(" call void @L_reset(i32 %e)\n") emit(" call void @L_reset(i32 %e)\n")
emit(" %fn = load i32, ptr @L_freen\n") emit(" %fn = load i32, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn\n") let eb7 = ecs_base("L_freelist")
emit(" %fp = getelementptr inbounds i32, ptr "); emit(eb7); emit(", i32 %fn\n")
emit(" store i32 %e, ptr %fp\n") emit(" store i32 %e, ptr %fp\n")
emit(" %fn1 = add i32 %fn, 1\n") emit(" %fn1 = add i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n") emit(" store i32 %fn1, ptr @L_freen\n")
emit(" ret void\n}\n\n") emit(" ret void\n}\n\n")
} }
# the base of a per-entity store, loaded where it is indexed (a store moves when L_grow doubles it)
function ecs_base(store: pointer) -> pointer {
let r = `%ecsb{itoa(ll_t)}` # its own prefix: raw functions name blocks t0, t1, ...
ll_t += 1
emit(` {r} = load ptr, ptr @{store}\n`)
return r
}
# L_grow(need): every per-entity store holds at least `need` slots - doubled from MAX_ENT as the
# entities outgrow them, the new slots zero, the mods' registered stores with them. Nothing is
# ever moved back down: an entity's slot is its handle for its whole life.
function emit_ecs_grow() -> void {
emit("define void @L_grow(i32 %need) {\nentry:\n %cap = load i32, ptr @L_cap\n")
emit(" %ok = icmp sle i32 %need, %cap\n br i1 %ok, label %done, label %grow\ngrow:\n")
emit(` %c0 = icmp eq i32 %cap, 0\n %start = select i1 %c0, i32 {itoa(MAX_ENT)}, i32 %cap\n br label %dbl\n`)
emit("dbl:\n %nc = phi i32 [ %start, %grow ], [ %nc2, %dbl2 ]\n %enough = icmp sge i32 %nc, %need\n")
emit(" br i1 %enough, label %apply, label %dbl2\ndbl2:\n %nc2 = mul i32 %nc, 2\n br label %dbl\n")
emit("apply:\n %cap64 = sext i32 %cap to i64\n %nc64 = sext i32 %nc to i64\n")
emit_grow_store("L_alive", "4", 0)
emit_grow_store("L_kind", "4", 1)
emit_grow_store("L_freelist", "4", 2)
if net_has_owned() { emit_grow_store("L_owner_arr", "4", 3) }
var k = 4
var i = 0
while i < len(prog) {
if prog[i].kind == N_COMP {
let sz = `%gs{itoa(k)}`
emit(` {sz} = ptrtoint ptr getelementptr (%Cmp_{prog[i].s}, ptr null, i32 1) to i64\n`)
emit_grow_store(`S_{prog[i].s}`, sz, k)
emit_grow_store(`H_{prog[i].s}`, "1", k + 1)
k += 2
}
i += 1
}
emit_grow_dyn()
emit("fin:\n store i32 %nc, ptr @L_cap\n br label %done\ndone:\n ret void\n}\n\n")
}
# one store to nc slots of `esz` bytes: reallocated (8 bytes spare, so a field-less tag is never
# a zero-byte block), its new tail zeroed
function emit_grow_store(store: pointer, esz: pointer, k: int) -> void {
let n = itoa(k)
emit(` %gp{n} = load ptr, ptr @{store}\n %gb{n} = mul i64 %nc64, {esz}\n %gr{n} = add i64 %gb{n}, 8\n`)
emit(` %gn{n} = call ptr @realloc(ptr %gp{n}, i64 %gr{n})\n %go{n} = mul i64 %cap64, {esz}\n`)
emit(` %gt{n} = getelementptr i8, ptr %gn{n}, i64 %go{n}\n %gz{n} = sub i64 %gr{n}, %go{n}\n`)
emit(` %gm{n} = call ptr @memset(ptr %gt{n}, i32 0, i64 %gz{n})\n store ptr %gn{n}, ptr @{store}\n`)
}
# the stores a mod registered at run time (ludic_register_prop), grown the same way
function emit_grow_dyn() -> void {
emit(" br label %dynh\ndynh:\n %di = phi i32 [ 0, %apply ], [ %di1, %dynb ]\n %dcn = load i32, ptr @dyn_count\n")
emit(" %dmore = icmp slt i32 %di, %dcn\n br i1 %dmore, label %dynb, label %fin\ndynb:\n")
emit(" %dfp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %di\n %dnf = load i32, ptr %dfp\n")
emit(" %dnf4 = mul i32 %dnf, 4\n %desz = sext i32 %dnf4 to i64\n")
emit_grow_slot("dynS", "%desz", "s")
emit_grow_slot("dynH", "1", "h")
emit(" %di1 = add i32 %di, 1\n br label %dynh\n")
}
function emit_grow_slot(table: pointer, esz: pointer, t: pointer) -> void {
emit(` %d{t}p = getelementptr inbounds [32 x ptr], ptr @{table}, i32 0, i32 %di\n %d{t}o = load ptr, ptr %d{t}p\n`)
emit(` %d{t}b = mul i64 %nc64, {esz}\n %d{t}r = add i64 %d{t}b, 8\n %d{t}n = call ptr @realloc(ptr %d{t}o, i64 %d{t}r)\n`)
emit(` %d{t}f = mul i64 %cap64, {esz}\n %d{t}t = getelementptr i8, ptr %d{t}n, i64 %d{t}f\n %d{t}z = sub i64 %d{t}r, %d{t}f\n`)
emit(` %d{t}m = call ptr @memset(ptr %d{t}t, i32 0, i64 %d{t}z)\n store ptr %d{t}n, ptr %d{t}p\n`)
}

View file

@ -93,7 +93,8 @@ function emit_countdown_system() -> void {
emit(" %n = load i32, ptr @L_entc\n br label %loop\nloop:\n") emit(" %n = load i32, ptr @L_entc\n br label %loop\nloop:\n")
emit(" %i = phi i32 [ 0, %entry ], [ %i1, %next ]\n") emit(" %i = phi i32 [ 0, %entry ], [ %i1, %next ]\n")
emit(" %go = icmp slt i32 %i, %n\n br i1 %go, label %body, label %done\nbody:\n") emit(" %go = icmp slt i32 %i, %n\n br i1 %go, label %body, label %done\nbody:\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n") let eb1 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb1); emit(", i32 %i\n")
emit(" %al = load i32, ptr %ap\n %alive = icmp ne i32 %al, 0\n br i1 %alive, label %c0, label %next\n") emit(" %al = load i32, ptr %ap\n %alive = icmp ne i32 %al, 0\n br i1 %alive, label %c0, label %next\n")
var k = 0 var k = 0
var i = 0 var i = 0
@ -106,12 +107,14 @@ function emit_countdown_system() -> void {
if any { if any {
let sk = itoa(k); let nk = itoa(k + 1) let sk = itoa(k); let nk = itoa(k + 1)
emit("c"); emit(sk); emit(":\n") emit("c"); emit(sk); emit(":\n")
emit(" %h"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(d.s); emit(", i32 0, i32 %i\n") let eb2 = ecs_base(`H_{d.s}`)
emit(" %h"); emit(sk); emit(" = getelementptr inbounds i8, ptr "); emit(eb2); emit(", i32 %i\n")
emit(" %hv"); emit(sk); emit(" = load i8, ptr %h"); emit(sk); emit("\n") emit(" %hv"); emit(sk); emit(" = load i8, ptr %h"); emit(sk); emit("\n")
emit(" %has"); emit(sk); emit(" = icmp ne i8 %hv"); emit(sk); emit(", 0\n") emit(" %has"); emit(sk); emit(" = icmp ne i8 %hv"); emit(sk); emit(", 0\n")
emit(" br i1 %has"); emit(sk); emit(", label %t"); emit(sk); emit(", label %c"); emit(nk); emit("\n") emit(" br i1 %has"); emit(sk); emit(", label %t"); emit(sk); emit(", label %c"); emit(nk); emit("\n")
emit("t"); emit(sk); emit(":\n") emit("t"); emit(sk); emit(":\n")
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(d.s); emit("], ptr @S_"); emit(d.s); emit(", i32 0, i32 %i\n") let eb3 = ecs_base(`S_{d.s}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(d.s); emit(", ptr "); emit(eb3); emit(", i32 %i\n")
f = 0 f = 0
while f < len(d.kids) { while f < len(d.kids) {
if (d.kids[f].ty == "countdown") { if (d.kids[f].ty == "countdown") {
@ -474,7 +477,8 @@ function emit_world_despawn_fn() -> void {
emit("define void @fn_world_despawn(i32 %e) {\nentry:\n") emit("define void @fn_world_despawn(i32 %e) {\nentry:\n")
if len(g_ondespawn) > 0 { if len(g_ondespawn) > 0 {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n") let eb4 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %e\n")
emit(" %k = load i32, ptr %kp\n") emit(" %k = load i32, ptr %kp\n")
var i = 0 var i = 0
while i < len(g_ondespawn) { while i < len(g_ondespawn) {
@ -508,10 +512,14 @@ function emit_despawn_all_fn() -> void {
emit("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n") emit("loop:\n %i = phi i32 [ 0, %entry ], [ %i1, %cont ]\n")
emit(" %n = load i32, ptr @L_entc\n %go = icmp slt i32 %i, %n\n") emit(" %n = load i32, ptr @L_entc\n %go = icmp slt i32 %i, %n\n")
emit(" br i1 %go, label %body, label %fin\n") emit(" br i1 %go, label %body, label %fin\n")
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %i\n") emit("body:\n")
let eb5 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %i\n")
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n") emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n")
emit(" br i1 %isa, label %do, label %cont\n") emit(" br i1 %isa, label %do, label %cont\n")
emit("do:\n %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %i\n") emit("do:\n")
let eb6 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb6); emit(", i32 %i\n")
emit(" %k = load i32, ptr %kp\n") emit(" %k = load i32, ptr %kp\n")
var i = 0 var i = 0
while i < len(g_ondespawn) { while i < len(g_ondespawn) {
@ -608,7 +616,8 @@ function emit_event_fns() -> void {
emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n") emit(" %n = load i32, ptr @evN_"); emit(en); emit("\n")
emit(" %lo = icmp sge i32 %tok, 0\n %hi = icmp slt i32 %tok, %n\n %ok = and i1 %lo, %hi\n") emit(" %lo = icmp sge i32 %tok, 0\n %hi = icmp slt i32 %tok, %n\n %ok = and i1 %lo, %hi\n")
emit(" br i1 %ok, label %do, label %skip\n") emit(" br i1 %ok, label %do, label %skip\n")
emit("do:\n %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %tok\n") emit("do:\n")
emit(" %slot = getelementptr inbounds ["); emit(cap); emit(" x ptr], ptr @evL_"); emit(en); emit(", i32 0, i32 %tok\n")
emit(" store ptr null, ptr %slot\n br label %skip\n") emit(" store ptr null, ptr %slot\n br label %skip\n")
emit("skip:\n ret void\n}\n\n") emit("skip:\n ret void\n}\n\n")

View file

@ -25,7 +25,8 @@ function emit_query(st: Node) -> void {
emit(body); emit(":\n"); g_term = false emit(body); emit(":\n"); g_term = false
let i1 = emit_bind(`load i32, ptr {ip}`) let i1 = emit_bind(`load i32, ptr {ip}`)
# alive? # alive?
let ap = nreg(); emit(" "); emit(ap); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 "); emit(i1); emit("\n") let eb1 = ecs_base("L_alive")
let ap = nreg(); emit(" "); emit(ap); emit(" = getelementptr inbounds i32, ptr "); emit(eb1); emit(", i32 "); emit(i1); emit("\n")
let al = emit_bind(`load i32, ptr {ap}`) let al = emit_bind(`load i32, ptr {ap}`)
let alc = emit_bind(`icmp ne i32 {al}, 0`) let alc = emit_bind(`icmp ne i32 {al}, 0`)
let ka = lbl("qa") let ka = lbl("qa")
@ -40,14 +41,16 @@ function emit_query(st: Node) -> void {
let ak = find_arch_id(tm.s) let ak = find_arch_id(tm.s)
var ok: pointer = "0" var ok: pointer = "0"
if ak > 0 { if ak > 0 {
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(i1); emit("\n") let eb2 = ecs_base("L_kind")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds i32, ptr "); emit(eb2); emit(", i32 "); emit(i1); emit("\n")
let kv = emit_bind(`load i32, ptr {kp}`) let kv = emit_bind(`load i32, ptr {kp}`)
let kok = emit_bind(`icmp eq i32 {kv}, {itoa(ak)}`) let kok = emit_bind(`icmp eq i32 {kv}, {itoa(ak)}`)
let mev = emit_bind(`load i32, ptr @ME_{tm.s}`) # model-enabled flag let mev = emit_bind(`load i32, ptr @ME_{tm.s}`) # model-enabled flag
let meok = emit_bind(`icmp ne i32 {mev}, 0`) let meok = emit_bind(`icmp ne i32 {mev}, 0`)
ok = emit_bind(`and i1 {kok}, {meok}`) ok = emit_bind(`and i1 {kok}, {meok}`)
} else { } else {
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n") let eb3 = ecs_base(`H_{tm.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb3); emit(", i32 "); emit(i1); emit("\n")
let hv = emit_bind(`load i8, ptr {hp}`) let hv = emit_bind(`load i8, ptr {hp}`)
ok = emit_bind(`icmp ne i8 {hv}, 0`) ok = emit_bind(`icmp ne i8 {hv}, 0`)
} }
@ -66,7 +69,8 @@ function emit_query(st: Node) -> void {
if tm.ival == 0 and find_arch_id(tm.s) == 0 { if tm.ival == 0 and find_arch_id(tm.s) == 0 {
if vi < len(st.kids) { if vi < len(st.kids) {
let slot = nreg() let slot = nreg()
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(tm.s); emit("], ptr @S_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n") let eb4 = ecs_base(`S_{tm.s}`)
emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(tm.s); emit(", ptr "); emit(eb4); emit(", i32 "); emit(i1); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.kids[vi].s, vslot, tm.s) loc_push(st.kids[vi].s, vslot, tm.s)

View file

@ -46,16 +46,23 @@ function emit_io(fn2: pointer, p: pointer, bytes: pointer) -> void {
function emit_snapshot_blocks(fn2: pointer) -> void { function emit_snapshot_blocks(fn2: pointer) -> void {
g_iok = 0 g_iok = 0
g_off = "0" g_off = "0"
let me = itoa(MAX_ENT) # the stores grow, so a snapshot says how many slots it holds first; a load grows to it before
# reading them back
let reading = (fn2 == "fread") or (g_snap_mode == "load")
if not reading { emit(" %sncap0 = load i32, ptr @L_cap\n store i32 %sncap0, ptr @L_snapcap\n") }
emit_io(fn2, "@L_snapcap", "4")
emit(" %sncap = load i32, ptr @L_snapcap\n")
if reading { emit(" call void @L_grow(i32 %sncap)\n") }
emit(" %sncap64 = sext i32 %sncap to i64\n")
emit_io(fn2, "@L_entc", "4") emit_io(fn2, "@L_entc", "4")
emit_io(fn2, "@L_freen", "4") emit_io(fn2, "@L_freen", "4")
emit(" %nalive = mul i64 "); emit(me); emit(", 4\n") emit(" %nalive = mul i64 %sncap64, 4\n")
emit_io(fn2, "@L_alive", "%nalive") emit_io(fn2, snap_base("L_alive", 0), "%nalive")
emit_io(fn2, "@L_freelist", "%nalive") emit_io(fn2, snap_base("L_freelist", 1), "%nalive")
emit_io(fn2, "@L_kind", "%nalive") emit_io(fn2, snap_base("L_kind", 2), "%nalive")
# N3: an @Owned world snapshots its per-entity owners too, so rollback/replication # N3: an @Owned world snapshots its per-entity owners too, so rollback/replication
# round-trips ownership (like @L_kind). Gated, so non-@Owned snapshots are unchanged. # round-trips ownership (like @L_kind). Gated, so non-@Owned snapshots are unchanged.
if net_has_owned() { emit_io(fn2, "@L_owner_arr", "%nalive") } if net_has_owned() { emit_io(fn2, snap_base("L_owner_arr", 3), "%nalive") }
var i = 0 var i = 0
while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, `@g_{prog[i].s}`, "4") }; i += 1 } while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, `@g_{prog[i].s}`, "4") }; i += 1 }
var ci = 0 var ci = 0
@ -63,16 +70,25 @@ function emit_snapshot_blocks(fn2: pointer) -> void {
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_COMP { if prog[i].kind == N_COMP {
let c = prog[i].s let c = prog[i].s
let csz1 = `%csz1_{itoa(ci)}`
let csz = `%csz{itoa(ci)}` let csz = `%csz{itoa(ci)}`
emit(" "); emit(csz); emit(" = ptrtoint ptr getelementptr (%Cmp_"); emit(c); emit(", ptr null, i32 "); emit(me); emit(") to i64\n") emit(" "); emit(csz1); emit(" = ptrtoint ptr getelementptr (%Cmp_"); emit(c); emit(", ptr null, i32 1) to i64\n")
emit_io(fn2, `@S_{c}`, csz) emit(" "); emit(csz); emit(" = mul i64 %sncap64, "); emit(csz1); emit("\n")
emit_io(fn2, `@H_{c}`, me) emit_io(fn2, snap_base(`S_{c}`, 4 + ci * 2), csz)
emit_io(fn2, snap_base(`H_{c}`, 5 + ci * 2), "%sncap64")
ci += 1 ci += 1
} }
i += 1 i += 1
} }
} }
# a store's base, loaded after any grow the snapshot made
function snap_base(store: pointer, k: int) -> pointer {
let r = `%snb{itoa(k)}`
emit(` {r} = load ptr, ptr @{store}\n`)
return r
}
function emit_snapshot() -> void { function emit_snapshot() -> void {
g_snap_mode = "file" # seed (module ptr inits are null) g_snap_mode = "file" # seed (module ptr inits are null)
emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n") emith("@.sav_path = private unnamed_addr constant [10 x i8] c\"ludic.sav\\00\"\n")

View file

@ -15,10 +15,12 @@ function emit_init_component(e: pointer, comp: pointer, rec: Node) -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let c = find_comp(comp) let c = find_comp(comp)
if (c == null) { perr(`spawn: unknown property {comp}`) } if (c == null) { perr(`spawn: unknown property {comp}`) }
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n") let eb1 = ecs_base(`H_{comp}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb1); emit(", i32 "); emit(e); emit("\n")
emit(" store i8 1, ptr "); emit(hp); emit("\n") emit(" store i8 1, ptr "); emit(hp); emit("\n")
let slot = nreg() let slot = nreg()
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(comp); emit("], ptr @S_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n") let eb2 = ecs_base(`S_{comp}`)
emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(eb2); emit(", i32 "); emit(e); emit("\n")
# defaults # defaults
var f = 0 var f = 0
while f < len(c.kids) { while f < len(c.kids) {
@ -71,7 +73,8 @@ function emit_bind_props(model: Node, e: pointer) -> void {
while c < len(model.kids) { while c < len(model.kids) {
let pname = model.kids[c].s let pname = model.kids[c].s
let slot = nreg() let slot = nreg()
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(pname); emit("], ptr @S_"); emit(pname); emit(", i32 0, i32 "); emit(e); emit("\n") let eb3 = ecs_base(`S_{pname}`)
emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(pname); emit(", ptr "); emit(eb3); emit(", i32 "); emit(e); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(pname, vslot, pname) loc_push(pname, vslot, pname)
@ -146,7 +149,8 @@ function emit_spawn(st: Node) -> pointer {
if (find_prefab(st.s) != null) and (ak == 0) { perr(`prefab {st.s}: unknown model {model}`) } if (find_prefab(st.s) != null) and (ak == 0) { perr(`prefab {st.s}: unknown model {model}`) }
if ak > 0 { if ak > 0 {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(e); emit("\n") let eb4 = ecs_base("L_kind")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 "); emit(e); emit("\n")
emit(" store i32 "); emit(itoa(ak)); emit(", ptr "); emit(kp); emit("\n") emit(" store i32 "); emit(itoa(ak)); emit(", ptr "); emit(kp); emit("\n")
let arch = find_arch(model) let arch = find_arch(model)
var c = 0 var c = 0
@ -179,7 +183,8 @@ function emit_despawn(st: Node) -> void {
# @OnDespawn: dispatch on the entity's kind and run the matching model's hook # @OnDespawn: dispatch on the entity's kind and run the matching model's hook
if len(g_ondespawn) > 0 { if len(g_ondespawn) > 0 {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(v.code); emit("\n") let eb5 = ecs_base("L_kind")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 "); emit(v.code); emit("\n")
let kind = emit_bind(`load i32, ptr {kp}`) let kind = emit_bind(`load i32, ptr {kp}`)
var i = 0 var i = 0
while i < len(g_ondespawn) { while i < len(g_ondespawn) {
@ -205,7 +210,8 @@ function emit_despawn(st: Node) -> void {
function emit_attach(st: Node) -> void { function emit_attach(st: Node) -> void {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb6 = ecs_base(`H_{st.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb6); emit(", i32 "); emit(ev.code); emit("\n")
let cur = emit_bind(`load i8, ptr {hp}`) let cur = emit_bind(`load i8, ptr {hp}`)
let isnew = emit_bind(`icmp eq i8 {cur}, 0`) let isnew = emit_bind(`icmp eq i8 {cur}, 0`)
let doit = lbl("attach"); let done = lbl("attdone") let doit = lbl("attach"); let done = lbl("attdone")
@ -223,7 +229,8 @@ function emit_attach(st: Node) -> void {
function emit_detach(st: Node) -> void { function emit_detach(st: Node) -> void {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb7 = ecs_base(`H_{st.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb7); emit(", i32 "); emit(ev.code); emit("\n")
let cur = emit_bind(`load i8, ptr {hp}`) let cur = emit_bind(`load i8, ptr {hp}`)
let here = emit_bind(`icmp ne i8 {cur}, 0`) let here = emit_bind(`icmp ne i8 {cur}, 0`)
let doit = lbl("detach"); let done = lbl("detdone") let doit = lbl("detach"); let done = lbl("detdone")
@ -233,7 +240,8 @@ function emit_detach(st: Node) -> void {
let hb = ondetach_body(st.s) # @OnDetach reads the outgoing value let hb = ondetach_body(st.s) # @OnDetach reads the outgoing value
if (hb != null) { if (hb != null) {
let save = nloc let save = nloc
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(st.s); emit("], ptr @S_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb8 = ecs_base(`S_{st.s}`)
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(st.s); emit(", ptr "); emit(eb8); emit(", i32 "); emit(ev.code); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.s, vslot, st.s) loc_push(st.s, vslot, st.s)
@ -266,13 +274,15 @@ function emit_toggle(st: Node) -> void {
if (st.a != null) { if (st.a != null) {
let ev = emit_expr(st.a) let ev = emit_expr(st.a)
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb9 = ecs_base(`H_{st.s}`)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds i8, ptr "); emit(eb9); emit(", i32 "); emit(ev.code); emit("\n")
emit(" store i8 "); emit(val); emit(", ptr "); emit(hp); emit("\n") emit(" store i8 "); emit(val); emit(", ptr "); emit(hp); emit("\n")
var hb: Node = null var hb: Node = null
if st.ival == 1 { hb = onenable_body(st.s) } else { hb = ondisable_body(st.s) } if st.ival == 1 { hb = onenable_body(st.s) } else { hb = ondisable_body(st.s) }
if (hb != null) { if (hb != null) {
let save = nloc let save = nloc
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(st.s); emit("], ptr @S_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n") let eb10 = ecs_base(`S_{st.s}`)
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds %Cmp_"); emit(st.s); emit(", ptr "); emit(eb10); emit(", i32 "); emit(ev.code); emit("\n")
let vslot = emit_alloca("ptr") let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n") emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.s, vslot, st.s) loc_push(st.s, vslot, st.s)

View file

@ -96,7 +96,8 @@ function emit_world_table() -> void {
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n") emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n") emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
emit("g"); emit(sk); emit(":\n") emit("g"); emit(sk); emit(":\n")
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb1 = ecs_base(`S_{cn}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb1); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}` let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
@ -139,7 +140,8 @@ function emit_world_table() -> void {
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n") emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n") emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
emit("g"); emit(sk); emit(":\n") emit("g"); emit(sk); emit(":\n")
emit(" %s"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb2 = ecs_base(`S_{cn}`)
emit(" %s"); emit(sk); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb2); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}` let ft = llty(c.kids[fj].ty); let fk = `{sk}_{itoa(fj)}`
@ -182,7 +184,8 @@ function emit_world_table() -> void {
emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n") emit(" %m"); emit(sk); emit(" = icmp eq i32 %p, "); emit(sk); emit("\n")
emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n") emit(" br i1 %m"); emit(sk); emit(", label %g"); emit(sk); emit(", label %gn"); emit(sk); emit("\n")
emit("g"); emit(sk); emit(":\n") emit("g"); emit(sk); emit(":\n")
emit(" %hp"); emit(sk); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(cn); emit(", i32 0, i32 %e\n") let eb3 = ecs_base(`H_{cn}`)
emit(" %hp"); emit(sk); emit(" = getelementptr inbounds i8, ptr "); emit(eb3); emit(", i32 %e\n")
emit(" %hv"); emit(sk); emit(" = load i8, ptr %hp"); emit(sk); emit("\n") emit(" %hv"); emit(sk); emit(" = load i8, ptr %hp"); emit(sk); emit("\n")
emit(" %hr"); emit(sk); emit(" = zext i8 %hv"); emit(sk); emit(" to i32\n ret i32 %hr"); emit(sk); emit("\n") emit(" %hr"); emit(sk); emit(" = zext i8 %hv"); emit(sk); emit(" to i32\n ret i32 %hr"); emit(sk); emit("\n")
emit("gn"); emit(sk); emit(":\n") emit("gn"); emit(sk); emit(":\n")
@ -201,10 +204,10 @@ function emit_world_table() -> void {
# zeroed. The returned id works with get/set/has/attach exactly like a built-in. # zeroed. The returned id works with get/set/has/attach exactly like a built-in.
emit("define i32 @ludic_register_prop(ptr %name, i32 %nfields) {\nentry:\n") emit("define i32 @ludic_register_prop(ptr %name, i32 %nfields) {\nentry:\n")
emit(" %dc = load i32, ptr @dyn_count\n %full = icmp slt i32 %dc, 32\n br i1 %full, label %do, label %rej\n") emit(" %dc = load i32, ptr @dyn_count\n %full = icmp slt i32 %dc, 32\n br i1 %full, label %do, label %rej\n")
emit("do:\n %nf4 = mul i32 %nfields, 4\n %sz = mul i32 %nf4, "); emit(me); emit("\n %szl = sext i32 %sz to i64\n") emit("do:\n %rcap = load i32, ptr @L_cap\n %nf4 = mul i32 %nfields, 4\n %sz = mul i32 %nf4, %rcap\n %szl = sext i32 %sz to i64\n")
emit(" %buf = call ptr @malloc(i64 %szl)\n call ptr @memset(ptr %buf, i32 0, i64 %szl)\n") emit(" %buf = call ptr @malloc(i64 %szl)\n call ptr @memset(ptr %buf, i32 0, i64 %szl)\n")
emit(" %sp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %dc\n store ptr %buf, ptr %sp\n") emit(" %sp = getelementptr inbounds [32 x ptr], ptr @dynS, i32 0, i32 %dc\n store ptr %buf, ptr %sp\n")
emit(" %hbuf = call ptr @malloc(i64 "); emit(me); emit(")\n call ptr @memset(ptr %hbuf, i32 0, i64 "); emit(me); emit(")\n") emit(" %hcap = sext i32 %rcap to i64\n %hbuf = call ptr @malloc(i64 %hcap)\n call ptr @memset(ptr %hbuf, i32 0, i64 %hcap)\n")
emit(" %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dc\n store ptr %hbuf, ptr %hp\n") emit(" %hp = getelementptr inbounds [32 x ptr], ptr @dynH, i32 0, i32 %dc\n store ptr %hbuf, ptr %hp\n")
emit(" %fp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %dc\n store i32 %nfields, ptr %fp\n") emit(" %fp = getelementptr inbounds [32 x i32], ptr @dynF, i32 0, i32 %dc\n store i32 %nfields, ptr %fp\n")
emit(" %np = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %dc\n store ptr %name, ptr %np\n") emit(" %np = getelementptr inbounds [32 x ptr], ptr @dynName, i32 0, i32 %dc\n store ptr %name, ptr %np\n")
@ -222,7 +225,8 @@ function emit_world_table() -> void {
# identifies each entity's model, then reads/writes it with get/set/has above. # identifies each entity's model, then reads/writes it with get/set/has above.
emit("define i32 @ludic_entity_count() {\nentry:\n %n = load i32, ptr @L_entc\n ret i32 %n\n}\n\n") emit("define i32 @ludic_entity_count() {\nentry:\n %n = load i32, ptr @L_entc\n ret i32 %n\n}\n\n")
emit("define i32 @ludic_kind(i32 %e) {\nentry:\n") emit("define i32 @ludic_kind(i32 %e) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n") let eb4 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %e\n")
emit(" %k = load i32, ptr %kp\n ret i32 %k\n}\n\n") emit(" %k = load i32, ptr %kp\n ret i32 %k\n}\n\n")
emit("define i32 @ludic_model_id(ptr %name) {\nentry:\n") emit("define i32 @ludic_model_id(ptr %name) {\nentry:\n")
k = 0; i = 0 k = 0; i = 0
@ -287,7 +291,9 @@ function emit_world_table() -> void {
emit(" %n = load i32, ptr @L_entc\n br label %loop\n") emit(" %n = load i32, ptr @L_entc\n br label %loop\n")
emit("loop:\n %e = phi i32 [ %from, %entry ], [ %e1, %cont ]\n") emit("loop:\n %e = phi i32 [ %from, %entry ], [ %e1, %cont ]\n")
emit(" %go = icmp slt i32 %e, %n\n br i1 %go, label %body, label %none\n") emit(" %go = icmp slt i32 %e, %n\n br i1 %go, label %body, label %none\n")
emit("body:\n %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") emit("body:\n")
let eb5 = ecs_base("L_alive")
emit(" %ap = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %e\n")
emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n br i1 %isa, label %chk, label %cont\n") emit(" %al = load i32, ptr %ap\n %isa = icmp ne i32 %al, 0\n br i1 %isa, label %chk, label %cont\n")
emit("chk:\n %h = call i32 @ludic_has(i32 %e, i32 %p)\n %hit = icmp ne i32 %h, 0\n br i1 %hit, label %hitb, label %cont\n") emit("chk:\n %h = call i32 @ludic_has(i32 %e, i32 %p)\n %hit = icmp ne i32 %h, 0\n br i1 %hit, label %hitb, label %cont\n")
emit("hitb:\n ret i32 %e\n") emit("hitb:\n ret i32 %e\n")
@ -446,6 +452,7 @@ function emit_game_main() -> void {
emit(" store i32 %argc, ptr @L_argc\n") emit(" store i32 %argc, ptr @L_argc\n")
emit(" store ptr %argv, ptr @L_argv\n") emit(" store ptr %argv, ptr @L_argv\n")
emit(" call void @L_init_runtime()\n") emit(" call void @L_init_runtime()\n")
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") } if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") }
emit(" call void @L_init_globals()\n") emit(" call void @L_init_globals()\n")
emit_calls_for_phase("Start") emit_calls_for_phase("Start")

View file

@ -121,7 +121,8 @@ function emit_net_serialize(m: Node) -> void {
let cn = m.kids[ci].s let cn = m.kids[ci].s
let c = find_comp(cn) let c = find_comp(cn)
if (c != null) { if (c != null) {
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb1 = ecs_base(`S_{cn}`)
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb1); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
if c.kids[fj].ival == 1 { if c.kids[fj].ival == 1 {
@ -154,7 +155,8 @@ function emit_net_apply(m: Node) -> void {
let cn = m.kids[ci].s let cn = m.kids[ci].s
let c = find_comp(cn) let c = find_comp(cn)
if (c != null) { if (c != null) {
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(cn); emit("], ptr @S_"); emit(cn); emit(", i32 0, i32 %e\n") let eb2 = ecs_base(`S_{cn}`)
let s = nreg(); emit(" "); emit(s); emit(" = getelementptr inbounds %Cmp_"); emit(cn); emit(", ptr "); emit(eb2); emit(", i32 %e\n")
var fj = 0 var fj = 0
while fj < len(c.kids) { while fj < len(c.kids) {
if c.kids[fj].ival == 1 { if c.kids[fj].ival == 1 {
@ -181,7 +183,8 @@ function emit_net_dispatch() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define i32 @ludic_serialize(i32 %e, ptr %buf) {\nentry:\n") emit("define i32 @ludic_serialize(i32 %e, ptr %buf) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n") let eb3 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb3); emit(", i32 %e\n %k = load i32, ptr %kp\n")
var k = 0 var k = 0
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
@ -198,7 +201,8 @@ function emit_net_dispatch() -> void {
emit(" ret i32 0\n}\n\n") emit(" ret i32 0\n}\n\n")
emit("define void @ludic_apply(i32 %e, ptr %buf, i32 %len) {\nentry:\n") emit("define void @ludic_apply(i32 %e, ptr %buf, i32 %len) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n") let eb4 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb4); emit(", i32 %e\n %k = load i32, ptr %kp\n")
k = 0; i = 0 k = 0; i = 0
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) { if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
@ -216,7 +220,8 @@ function emit_net_dispatch() -> void {
# ludic_sync_size(e): the replicated byte count for the entity's model — a # ludic_sync_size(e): the replicated byte count for the entity's model — a
# constant per kind, so a runtime can size a buffer before serialize. # constant per kind, so a runtime can size a buffer before serialize.
emit("define i32 @ludic_sync_size(i32 %e) {\nentry:\n") emit("define i32 @ludic_sync_size(i32 %e) {\nentry:\n")
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n %k = load i32, ptr %kp\n") let eb5 = ecs_base("L_kind")
emit(" %kp = getelementptr inbounds i32, ptr "); emit(eb5); emit(", i32 %e\n %k = load i32, ptr %kp\n")
k = 0; i = 0 k = 0; i = 0
while i < len(prog) { while i < len(prog) {
if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) { if prog[i].kind == N_ARCH and net_model_syncs(prog[i]) {
@ -239,12 +244,15 @@ function emit_net_dispatch() -> void {
function emit_net_owner() -> void { function emit_net_owner() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emit("define i32 @L_owner(i32 %e) {\nentry:\n") emit("define i32 @L_owner(i32 %e) {\nentry:\n")
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %v = load i32, ptr %p\n ret i32 %v\n}\n\n") let eb6 = ecs_base("L_owner_arr")
emit(" %p = getelementptr inbounds i32, ptr "); emit(eb6); emit(", i32 %e\n %v = load i32, ptr %p\n ret i32 %v\n}\n\n")
emit("define void @L_set_owner(i32 %e, i32 %id) {\nentry:\n") emit("define void @L_set_owner(i32 %e, i32 %id) {\nentry:\n")
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n store i32 %id, ptr %p\n ret void\n}\n\n") let eb7 = ecs_base("L_owner_arr")
emit(" %p = getelementptr inbounds i32, ptr "); emit(eb7); emit(", i32 %e\n store i32 %id, ptr %p\n ret void\n}\n\n")
# is_owner(e): does the local peer own e? owner(e) == local_id(). # is_owner(e): does the local peer own e? owner(e) == local_id().
emit("define i32 @L_is_owner(i32 %e) {\nentry:\n") emit("define i32 @L_is_owner(i32 %e) {\nentry:\n")
emit(" %p = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n %o = load i32, ptr %p\n") let eb8 = ecs_base("L_owner_arr")
emit(" %p = getelementptr inbounds i32, ptr "); emit(eb8); emit(", i32 %e\n %o = load i32, ptr %p\n")
emit(" %lid = load i32, ptr @L_localid\n %eq = icmp eq i32 %o, %lid\n %r = zext i1 %eq to i32\n ret i32 %r\n}\n\n") emit(" %lid = load i32, ptr @L_localid\n %eq = icmp eq i32 %o, %lid\n %r = zext i1 %eq to i32\n ret i32 %r\n}\n\n")
} }

View file

@ -147,7 +147,7 @@ function red_check(i: int) -> void {
function act_src() -> pointer { function act_src() -> pointer {
var src = "export state LudicActions {\n kinds: []int = new []int\n vals: []pointer = new []pointer\n head: int = 0\n depth: int = 0\n}\n" var src = "export state LudicActions {\n kinds: []int = new []int\n vals: []pointer = new []pointer\n head: int = 0\n depth: int = 0\n}\n"
src = src + "export function ludic_act_push(q: mut LudicActions, k: int, v: pointer) -> void {\n push(q.kinds, k)\n push(q.vals, v)\n}\n" src = src + "export function ludic_act_push(q: mut LudicActions, k: int, v: pointer) -> void {\n push(q.kinds, k)\n push(q.vals, v)\n}\n"
src = src + "export function drain_actions(q: mut LudicActions) -> void {\n if q.depth > 0 { return }\n q.depth = 1\n var pass = 0\n var round_end = len(q.kinds)\n" src = src + "export function drain_actions(q: mut LudicActions) -> void {\n if q.depth > 0 or len(q.kinds) == 0 { return }\n q.depth = 1\n var pass = 0\n var round_end = len(q.kinds)\n"
src = src + " while q.head < len(q.kinds) {\n if q.head == round_end {\n pass += 1\n round_end = len(q.kinds)\n" src = src + " while q.head < len(q.kinds) {\n if q.head == round_end {\n pass += 1\n round_end = len(q.kinds)\n"
src = src + ` if pass >= {itoa(ACTION_PASSES)} {{ ludic_act_runaway(q.kinds[q.head]) }}\n }}\n` src = src + ` if pass >= {itoa(ACTION_PASSES)} {{ ludic_act_runaway(q.kinds[q.head]) }}\n }}\n`
src = src + " let k = q.kinds[q.head]\n let v = q.vals[q.head]\n q.head += 1\n" src = src + " let k = q.kinds[q.head]\n let v = q.vals[q.head]\n q.head += 1\n"
@ -165,7 +165,7 @@ function act_src() -> pointer {
} }
a += 1 a += 1
} }
src = src + " }\n q.kinds = new []int\n q.vals = new []pointer\n q.head = 0\n q.depth = 0\n}\n" src = src + " }\n List.clear(q.kinds)\n List.clear(q.vals)\n q.head = 0\n q.depth = 0\n}\n" # in place: a drain a phase, seven a frame, allocates nothing
src = src + "export function ludic_act_runaway(k: int) -> void {\n var name = \"?\"\n" src = src + "export function ludic_act_runaway(k: int) -> void {\n var name = \"?\"\n"
a = 0 a = 0
while a < len(g_act_names) { while a < len(g_act_names) {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff