feat(ecs): observers and block change stamps on a Table

tb_on_add / tb_on_remove take a fn(int) (its states supplied, like a system's) told each handle as
its row is made and before it goes (ecs_hooks.ludic), so what follows a table - a drawing, a
message - does so without a scan. Every row's tick now also stamps its 64-row block, and
tb_changed_since / tb_added_since skip the blocks nobody wrote since: a delta over a large table
costs the blocks that moved. A row moved into a removal's gap keeps its own tick (it was not
written); a removal is told by the hook. ecs_test holds both. The ludic.wildlife table
(5ac3d48, written while builds were held) now builds and passes its 19 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 21:47:06 +03:00
parent 4a285903e1
commit 52270ab5d1
6 changed files with 103 additions and 7 deletions

View file

@ -10,6 +10,17 @@ program EcsTest {
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
@ -108,6 +119,41 @@ program EcsTest {
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
}
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() }