diff --git a/packages/ludic.base/ecs_grid_file.ludic b/packages/ludic.base/ecs_grid_file.ludic index 4d9a79fc..2bdee04b 100644 --- a/packages/ludic.base/ecs_grid_file.ludic +++ b/packages/ludic.base/ecs_grid_file.ludic @@ -38,3 +38,9 @@ function grid_rehash(g: Grid) -> void { if k >= 0 { grid_link(g, r, k, grid_bucket(g, grid_key_x(k), grid_key_z(k))) } } } + +# buckets enough for `rows` filed rows before a rehash, made now: a rehash in play makes a new +# bucket array and nothing gives the old one back +export function grid_reserve(g: Grid, rows: int) -> void { + while (g.mask + 1) * 2 < rows { grid_rehash(g) } +} diff --git a/packages/ludic.things/README.md b/packages/ludic.things/README.md index 134acb83..cfdbb742 100644 --- a/packages/ludic.things/README.md +++ b/packages/ludic.things/README.md @@ -47,6 +47,11 @@ def ThingKinds lure { name_of: fn bait_name, tick: fn bait_wear, put_down: true, | `put_down` | set down by someone rather than grown by the world: a restock leaves it (the port has the last word) | | `keeps` | keeps its `used` through a restock | +**A removed Thing's record is placed again.** Ludic frees nothing, and a world places and takes +away Things all day, so `thing_remove` keeps the record and `thing_put` / `thing_spawn` make the +oldest one again - set back as new, with a new `uid` - once 32 are waiting. Hold a Thing across a +removal only by its `uid` (`thing_by_uid`), never by the record. + ## The port Every member has a default, so any may be left out of the `bind`: @@ -73,6 +78,7 @@ the actor the moment the Thing exists (a caller often scales or tints it on the | --- | --- | | `thing_spawn(kind, look, x, y, z, yaw, id) -> Thing`, `thing_put(kind, look, x, z, yaw, sink, reach, id) -> Thing` | a new Thing, at a height or on the ground set into it by `sink` | | `thing_remove(t)`, `things_drop(gone: fn(Thing) -> bool)`, `things_clear()` | gone for good (told to the port); a new world, quietly | +| `things_reserve(n)`, `things_spare_count()` | room in the grid for `n` Things, made once at the start; how many removed records wait to be placed again | | `thing_move(t, x, z, yaw)`, `thing_move_to(t, x, y, z, yaw)` | on the ground there, or at a height | | `thing_hide(t)`, `thing_show(t)`, `thing_set_active(t, on)` | `active`, told to the port | | `things_all()`, `things_count()`, `things_ready()`, `thing_by_uid(uid)` | every Thing, in the order placed; whether a world was ever set out | diff --git a/packages/ludic.things/index.ludic b/packages/ludic.things/index.ludic index 28a38b7b..76661a7e 100644 --- a/packages/ludic.things/index.ludic +++ b/packages/ludic.things/index.ludic @@ -13,3 +13,4 @@ import "near.ludic" import "chunks.ludic" import "dispatch.ludic" import "system.ludic" +import "spare.ludic" diff --git a/packages/ludic.things/spare.ludic b/packages/ludic.things/spare.ludic new file mode 100644 index 00000000..e3bf70ad --- /dev/null +++ b/packages/ludic.things/spare.ludic @@ -0,0 +1,64 @@ +# spare.ludic - a removed Thing's record is made again rather than a new one: Ludic frees nothing, +# and the world places and takes away Things all day (a sign an animal leaves, a bed, a drop). +# Oldest first, and only once TH_SPARE_LAG are waiting, so anything still holding one sees its +# uid change long after the removal; everything else about it is set back as new +const TH_SPARE_LAG: int = 32 + +function th_spare_put(things_st: mut ThingsState, t: Thing) -> void { push(things_st.th_spare, t) } + +# a record for a Thing about to be placed: the oldest spare past the lag, or a new one +function th_record(things_st: mut ThingsState) -> Thing { + let l = things_st.th_spare + if len(l) - things_st.th_spare_at <= TH_SPARE_LAG { return new Thing } + let t = l[things_st.th_spare_at] + things_st.th_spare_at += 1 + if things_st.th_spare_at >= 64 { th_spare_pack(things_st) } + return th_clean(t) +} + +# the waiting ones moved to the front, in place +function th_spare_pack(things_st: mut ThingsState) -> void { + let l = things_st.th_spare + let at = things_st.th_spare_at + let n = len(l) - at + for i in 0 .. n { l[i] = l[i + at] } + for i in 0 .. at { List.pop(l) } + things_st.th_spare_at = 0 +} + +function th_clean(t: Thing) -> Thing { + t.uid = 0 + t.kind = 0 + t.id = 0 + t.look = "" + t.x = 0.0 + t.y = 0.0 + t.z = 0.0 + t.yaw = 0.0 + t.owner = -1 + t.active = true + t.reach = 0.0 + t.timer = 0.0 + t.used = 0 + t.n = 0 + t.sx = 0.0 + t.sz = 0.0 + t.syaw = 0.0 + t.shared = false + t.ent = -1 + t.tab = null + return t +} + +# room for `n` Things in the grid before it has to grow: a world that will place more in play +# than it placed at the start sizes it once, at the start +export function things_reserve(things_st: mut ThingsState, n: int) -> void { grid_reserve(things_st.th.grid, n) } + +export function things_spare_count(things_st: ThingsState) -> int { return len(things_st.th_spare) - things_st.th_spare_at } + +# a placed Thing's slot in the table: reused as Things come and go, so a list indexed by it is as +# long as the most Things there have been at once; -1 once it is removed +export function thing_slot(t: Thing) -> int { + if t == null or t.ent < 0 { return -1 } + return t.ent & ECS_SLOT +} diff --git a/packages/ludic.things/store.ludic b/packages/ludic.things/store.ludic index 164e591b..4ec3fe18 100644 --- a/packages/ludic.things/store.ludic +++ b/packages/ludic.things/store.ludic @@ -24,6 +24,8 @@ export state ThingsState { th_uid: int = 0 th_facts: Queue = th_facts__new() th_hs: words = words(0) # things_tick's handles, taken before any tick runs + th_spare: []Thing = new []Thing # removed records, made again oldest first (spare.ludic) + th_spare_at: int = 0 } function th_facts__new() -> Queue { return queue_new("things.facts") } diff --git a/packages/ludic.things/tests/things_test.ludic b/packages/ludic.things/tests/things_test.ludic index 11438c80..a0bb5433 100644 --- a/packages/ludic.things/tests/things_test.ludic +++ b/packages/ludic.things/tests/things_test.ludic @@ -215,4 +215,30 @@ program ThingsTest { expect(out[0] == a) expect_eq(things_changed(things_st, seen, rows, out), 2) } + test "a removed Thing's record is placed again once enough wait, as new, with a new uid" (things_st: mut ThingsState) { + things_reset(things_st) + let first = thing_put(things_st, TH_LURE, "lure", 1.0, 1.0, 0.0, 0.0, 2.0, 7) + first.used = 5 + first.owner = 3 + thing_hide(first) + let old_uid = first.uid + thing_remove(things_st, first) + for i in 0 .. 32 { thing_remove(things_st, thing_put(things_st, TH_BRANCH, "branch", float(i), 0.0, 0.0, 0.0, 1.5, 0)) } + expect_eq(things_spare_count(things_st), 33) + let again = thing_put(things_st, TH_SIGN, "sign", 4.0, 4.0, 0.0, 0.0, 2.0, 1) + let ap: pointer = again + let fp: pointer = first + expect(ap == fp) + expect(again.uid != old_uid) + expect_eq(again.kind, TH_SIGN) + expect_eq(again.used, 0) + expect_eq(again.owner, -1) + expect(again.active) + expect_eq(thing_nearest(things_st, TH_SIGN, 4.0, 4.0).uid, again.uid) + expect_eq(things_verify(things_st), 0) + for i in 0 .. 500 { thing_remove(things_st, thing_put(things_st, TH_BRANCH, "branch", 0.0, float(i), 0.0, 0.0, 1.5, 0)) } + expect(things_spare_count(things_st) <= 33) + things_reserve(things_st, 5000) + expect_eq(things_verify(things_st), 0) + } } diff --git a/packages/ludic.things/verbs.ludic b/packages/ludic.things/verbs.ludic index f43e6080..043cd6d0 100644 --- a/packages/ludic.things/verbs.ludic +++ b/packages/ludic.things/verbs.ludic @@ -1,7 +1,7 @@ # 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) + return th_place(things_st, th_record(things_st), 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 { @@ -41,7 +41,7 @@ function th_columns(t: Thing) -> void { # 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 + let t = th_record(things_st) t.reach = reach return th_place(things_st, t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id) } @@ -54,6 +54,7 @@ export function thing_remove(things_st: mut ThingsState, t: Thing) -> void { imap_del(things_st.th_uids, t.uid) tb_remove(t.tab, t.ent) t.ent = -1 + th_spare_put(things_st, t) } # every Thing `gone` says so is removed for good; from the last row down, so the row a removal