diff --git a/packages/ludic.nav/README.md b/packages/ludic.nav/README.md index a889a2cf..0f6eba1f 100644 --- a/packages/ludic.nav/README.md +++ b/packages/ludic.nav/README.md @@ -48,8 +48,10 @@ import "ludic.nav" | `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go | | `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres | | `nav_path(kind, filter, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners | +| `nav_next_corner(kind, filter, sx, sy, sz, ex, ey, ez) -> bool` | the next corner of that way, read back as the nearest point is (a walker's one question a frame) | | `nav_straight(kind, filter, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable | | `nav_random_near(kind, filter, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed | +| `nav_asked()`, `nav_found()`, `nav_short()` | paths asked for, found and cut short since the meshes loaded (a game's measure of how often its walkers had a way) | | `nav_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter | ## Tests diff --git a/packages/ludic.nav/build.ludic b/packages/ludic.nav/build.ludic index 15fc5233..6ba8ef4e 100644 --- a/packages/ludic.nav/build.ludic +++ b/packages/ludic.nav/build.ludic @@ -47,8 +47,8 @@ export function nav_polygons(nav_st: NavState, kind: int) -> int { return nvc_polys(h) } -# a kind's mesh written to a file, and read back. Reading holds the file's bytes for good (a slice -# is never given back), so a mesh is loaded once per map, never per frame +# a kind's mesh written to a file, and read back - through the asset pack when it is in one - into the +# shim's own memory, which it frees once the tiles are made, so a map swap leaves nothing behind export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool { let h = nv_mesh(nav_st, kind) if h == null { return false } @@ -59,8 +59,14 @@ export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool return Fs.write_bytes(path, buf, n) } export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool { - if not Fs.exists(path) { return false } - let buf = Fs.read_bytes(path) - if buf == null or len(buf) == 0 { return false } - return nv_set(nav_st, kind, nvc_load(buf, len(buf))) + let f = file_open(path, "rb") + if f == null { return false } + file_seek(f, 0, 2) + let n = file_tell(f) + file_seek(f, 0, 0) + let buf = nvc_stage(n) + var got = 0 + if buf != null { got = file_read(f, buf, n) } + file_close(f) + return nv_set(nav_st, kind, nvc_load_staged(buf, got)) } diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib index 4ea3bedc..3941b8a7 100755 --- a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib +++ b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d2e4aa97130a76c41dfb6a6b93172e7cbab3fa6249035cc745c8eb162a8487a -size 176736 +oid sha256:10555b7baa5958700f0ce4599e85fc4fc18b9c92afbbacfa64f04b630f42f583 +size 176816 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.dll b/packages/ludic.nav/lib/windows-x64/ludicnav.dll index b5e76515..7a9c6695 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.dll +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:300a80b3d3edd283e7911a9899823cf21f1731ea1a41a1190223a0eced900972 -size 336896 +oid sha256:99671e7df79802010fbf132f736d1b5e2681f0cd72ab71e3aa6e7026b837c97c +size 337408 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.lib b/packages/ludic.nav/lib/windows-x64/ludicnav.lib index 46a507d1..9e1bdfe3 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.lib +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48f55cfffe5d3e99ae24b68a34b272bfc8ea693ab1f9d849e3e99f17797fee2b -size 3492 +oid sha256:c3e4f7ae19a6113829feffe2ed99e4a8f6b8fb2831c90115f489d1ecf646216a +size 3866 diff --git a/packages/ludic.nav/native.ludic b/packages/ludic.nav/native.ludic index 0d897d54..71d02ce1 100644 --- a/packages/ludic.nav/native.ludic +++ b/packages/ludic.nav/native.ludic @@ -13,3 +13,5 @@ extern function nvc_path(h: pointer, f: int, sx: float, sy: float, sz: float, ex extern function nvc_partial(h: pointer) -> int = "nav_partial" extern function nvc_raycast(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast" extern function nvc_random_near(h: pointer, f: int, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near" +extern function nvc_stage(size: int) -> pointer = "nav_stage" +extern function nvc_load_staged(buf: pointer, size: int) -> pointer = "nav_load_staged" diff --git a/packages/ludic.nav/native/shim/nav_shim.cpp b/packages/ludic.nav/native/shim/nav_shim.cpp index f31f049f..4d5b485f 100644 --- a/packages/ludic.nav/native/shim/nav_shim.cpp +++ b/packages/ludic.nav/native/shim/nav_shim.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include diff --git a/packages/ludic.nav/native/shim/nav_tiles.inl b/packages/ludic.nav/native/shim/nav_tiles.inl index 0b1b8ec4..347f7c55 100644 --- a/packages/ludic.nav/native/shim/nav_tiles.inl +++ b/packages/ludic.nav/native/shim/nav_tiles.inl @@ -100,3 +100,12 @@ NAV_SHIM void *nav_load(const unsigned char *buf, int size) { } return nav_open(mesh); } + +// a file read straight into the shim's own memory rather than a Ludic slice (which is never given +// back): nav_stage hands out the room, nav_load_staged parses it and frees it, whatever the outcome +NAV_SHIM unsigned char *nav_stage(int size) { return size > 0 ? static_cast(malloc(size)) : nullptr; } +NAV_SHIM void *nav_load_staged(unsigned char *buf, int size) { + void *h = buf ? nav_load(buf, size) : nullptr; + free(buf); + return h; +} diff --git a/packages/ludic.nav/query.ludic b/packages/ludic.nav/query.ludic index 7718c290..21e6ef6f 100644 --- a/packages/ludic.nav/query.ludic +++ b/packages/ludic.nav/query.ludic @@ -19,6 +19,16 @@ export function nav_random_near(nav_st: NavState, kind: int, filter: int, x: flo return nvc_random_near(h, filter, x, y, z, r, seed, nav_st.nv_near) == 1 } +# the next corner of the way from (sx, sy, sz) toward (ex, ey, ez) - the first after the start - read +# back as the nearest point is (nav_near_x / _z), so a walker's question and a goal share one answer +export function nav_next_corner(nav_st: mut NavState, kind: int, filter: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> bool { + if nav_path(nav_st, kind, filter, sx, sy, sz, ex, ey, ez) < 2 { return false } + nav_st.nv_near[0] = nav_st.nv_out[3] + nav_st.nv_near[1] = nav_st.nv_out[4] + nav_st.nv_near[2] = nav_st.nv_out[5] + return true +} + # a way from one point to another: its corners, first the start and last the end (or as near the end # as the mesh reaches - nav_partial says which); -1 when either end is off the mesh export function nav_path(nav_st: mut NavState, kind: int, filter: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int { @@ -26,9 +36,17 @@ export function nav_path(nav_st: mut NavState, kind: int, filter: int, sx: float nav_st.nv_count = 0 if h == null { return -1 } let n = nvc_path(h, filter, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS) + nav_st.nv_asked += 1 if n > 0 { nav_st.nv_count = n } + if n >= 2 { nav_st.nv_found += 1 } + if n > 0 and nvc_partial(h) == 1 { nav_st.nv_short += 1 } return n } + +# how many paths were asked for, found, and cut short (the end out of reach) since the meshes loaded +export function nav_asked(nav_st: NavState) -> int { return nav_st.nv_asked } +export function nav_found(nav_st: NavState) -> int { return nav_st.nv_found } +export function nav_short(nav_st: NavState) -> int { return nav_st.nv_short } export function nav_partial(nav_st: NavState, kind: int) -> bool { let h = nv_mesh(nav_st, kind) return h != null and nvc_partial(h) == 1 diff --git a/packages/ludic.nav/state.ludic b/packages/ludic.nav/state.ludic index 23a1ba8e..cf0f240c 100644 --- a/packages/ludic.nav/state.ludic +++ b/packages/ludic.nav/state.ludic @@ -42,6 +42,9 @@ export state NavState { nv_near: []float = floats(3) nv_cfg: []float = floats(7) # the configuration handed to the shim, filled in place per build nv_empty: []float = floats(1) # what the shim is handed for no cylinders or no footprints + nv_asked: int = 0 # paths asked for, found (two corners or more), and cut short + nv_found: int = 0 + nv_short: int = 0 } function nv_none() -> []pointer { let m = new []pointer @@ -66,4 +69,7 @@ export function nav_drop(nav_st: mut NavState, kind: int) -> void { nv_set(nav_s export function nav_reset(nav_st: mut NavState) -> void { for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) } nav_st.nv_count = 0 + nav_st.nv_asked = 0 + nav_st.nv_found = 0 + nav_st.nv_short = 0 } diff --git a/packages/ludic.nav/tests/nav_test.ludic b/packages/ludic.nav/tests/nav_test.ludic index e55bfe08..cfc66f08 100644 --- a/packages/ludic.nav/tests/nav_test.ludic +++ b/packages/ludic.nav/tests/nav_test.ludic @@ -76,6 +76,18 @@ program NavTest { expect_eq(nav_near_z(nav_st), z7) } + test "the next corner is the path's second, read back as the nearest point" (nav_st: mut NavState) { + expect(meadow(nav_st, NAV_PERSON, 0, true)) + let n = nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) + let cx = nav_corner_x(nav_st, 1) + let cz = nav_corner_z(nav_st, 1) + expect(n >= 3) + expect(nav_next_corner(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0)) + expect_near(nav_near_x(nav_st), cx, 0.0001) + expect_near(nav_near_z(nav_st), cz, 0.0001) + expect(not nav_next_corner(nav_st, NAV_PERSON, 0, -50.0, 0.0, -50.0, 35.0, 0.0, 20.0)) + } + test "a filter that dislikes a band of thicket walks round it; the plain one goes through" (nav_st: mut NavState) { expect(meadow(nav_st, NAV_PERSON, 3, false)) nav_area_cost(nav_st, NAV_PERSON, 1, 2, 20.0) diff --git a/packages/ludic.npc/README.md b/packages/ludic.npc/README.md index 31daab20..42263c87 100644 --- a/packages/ludic.npc/README.md +++ b/packages/ludic.npc/README.md @@ -87,10 +87,18 @@ export port NpcWorld { # every member has a default: a flat dry field at n next_id: fn() -> int, body_of: fn(int) -> int # an id across machines; a look seed's body taken: fn(string) -> bool # a name the place keeps born / gone: fn(NpcPerson) -> void # draw it, stop drawing it + way: fn(NpcPerson, x, z) -> bool, way_x, way_z # the next corner of its way toward (x, z) } export port NpcTalk { say: fn(NpcPerson, int) -> string, greeting: fn(NpcPerson) -> string } ``` +**A person walks by the world's way where the world has one.** `way` is asked for the next corner +toward where it is going - every half second, on reaching the corner, and at once for a new target - +and the person walks corner to corner, but begins its errand only at the target itself. With no +answer (the default) it walks straight and steps round what is in the way, as before; the step +round and the twenty seconds' give-up stay as the fallback. Maroon Lake binds `way` to its navmesh +by a person's taste. + ## API | | | diff --git a/packages/ludic.npc/defs.ludic b/packages/ludic.npc/defs.ludic index 905fee0b..18e3121a 100644 --- a/packages/ludic.npc/defs.ludic +++ b/packages/ludic.npc/defs.ludic @@ -47,6 +47,11 @@ export property NpcPerson { via_on: bool = false # a step round whatever is in the way vx: float = 0.0 vz: float = 0.0 + wx: float = 0.0 # the corner of its way it walks to now (NpcWorld.way) + wz: float = 0.0 + wtx: float = 0.0 # the target that corner was asked for: a new target asks again + wtz: float = 0.0 + way_t: float = 0.0 # seconds until it asks the way again detours: int = 0 fails: int = 0 stall: float = 0.0 # seconds walking without moving diff --git a/packages/ludic.npc/index.ludic b/packages/ludic.npc/index.ludic index ee478ac6..4cb6a00c 100644 --- a/packages/ludic.npc/index.ludic +++ b/packages/ludic.npc/index.ludic @@ -10,6 +10,7 @@ import "ground.ludic" import "spots.ludic" import "routine.ludic" import "walk.ludic" +import "way.ludic" import "act.ludic" import "names.ludic" import "census.ludic" diff --git a/packages/ludic.npc/port.ludic b/packages/ludic.npc/port.ludic index 3448ec87..908588a4 100644 --- a/packages/ludic.npc/port.ludic +++ b/packages/ludic.npc/port.ludic @@ -25,6 +25,11 @@ export port NpcWorld { taken: fn(string) -> bool = fn nw_free # a name the place already uses born: fn(NpcPerson) -> void = fn nw_nothing # a new one (or a slot reused): draw it gone: fn(NpcPerson) -> void = fn nw_nothing # left: stop drawing it + # (p, x, z): the next corner of this person's way toward (x, z) into way_x / _z; false, the + # straight line with its steps round (a navmesh's path by a person's taste, in Maroon Lake) + way: fn(NpcPerson, float, float) -> bool = fn nw_no_way + way_x: fn() -> float = fn nw_asked_x + way_z: fn() -> float = fn nw_asked_z } # what the story says through them: the words of a line whose `fill` is set ("" when it has nothing @@ -76,3 +81,4 @@ export function npc_near(npc_st: mut NpcState, x: float, z: float) -> float { } export function npc_near_i(npc_st: NpcState) -> int { return npc_st.np_near_i } +function nw_no_way(p: NpcPerson, x: float, z: float) -> bool { return false } diff --git a/packages/ludic.npc/tests/way_test.ludic b/packages/ludic.npc/tests/way_test.ludic new file mode 100644 index 00000000..22b726b4 --- /dev/null +++ b/packages/ludic.npc/tests/way_test.ludic @@ -0,0 +1,82 @@ +# way_test.ludic - a person walks by the world's way where the world has one: round a wall the +# straight line runs into, by the corners NpcWorld.way gives, and arrives only where it was going - +# never at a corner +import "ludic.npc" +import "ludic.base" +import "fake" +program NpcWayTest { + numbers float + bind NpcWorld { + ground: fn fk_ground + water: fn fk_water + allowed: fn fk_allowed + push: fn fk_push + pushed_x: fn fk_pushed_x + pushed_z: fn fk_pushed_z + clear: fn fk_clear + hour: fn fk_hour_now + day: fn fk_day_now + players: fn fk_players + player_on: fn fk_player_on + player_x: fn fk_x + player_z: fn fk_z + want: fn fk_wanted + extra: fn fk_extra_kind + taken: fn fk_taken + born: fn fk_on_born + gone: fn fk_on_gone + way: fn wt_way + way_x: fn wt_x + way_z: fn wt_z + } + bind NpcTalk { say: fn tk_say, greeting: fn tk_greeting } + + def NpcActs sit { } + def NpcActs fish { turns: false } + def NpcKinds from "kinds.lres" + def NpcNames from "names.lres" + def NpcLines from "lines.lres" + + function tk_say(p: NpcPerson, line: int) -> string { return "" } + function tk_greeting(p: NpcPerson) -> string { return "" } + function tk_start(npc_st: mut NpcState, p: NpcPerson) -> void { npc_start_near(npc_st, p, 0.0, 0.0, 0.0, 10.0) } + + state WayTestState { + wx: float = 0.0 + wz: float = 0.0 + asked: int = 0 + } + # a wall across x = 20 for |z| < 15 (a straight walk to (38, -25) meets it): the way goes round its + # north end, then on to the target - which is clear of where the fake place forbids stopping + function wt_way(way_test_st: mut WayTestState, p: NpcPerson, x: float, z: float) -> bool { + way_test_st.asked += 1 + way_test_st.wx = x + way_test_st.wz = z + if p.x < 19.0 and p.z < 16.0 { + way_test_st.wx = 20.0 + way_test_st.wz = 17.0 + } + return true + } + function wt_x(way_test_st: WayTestState) -> float { return way_test_st.wx } + function wt_z(way_test_st: WayTestState) -> float { return way_test_st.wz } + + test "it walks round the wall by the corners it is given and begins only where it was going" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState, way_test_st: WayTestState) { + fk_setup(npc_st, npc_tests_fake_st) + npc_tests_fake_st.fk_want = 1 + let p = npc_spawn(npc_st, NPCK_HIKER) + npc_set_at(p, 0.0, 0.0) + npc_go(npc_st, p, 38.0, -25.0, NPCA_SIT, 30.0) + var north = -100.0 + var k = 0 + while p.act == NPCA_WALK and k < 2000 { + npc_tick(npc_st, 0.05) + if p.x > 19.0 and p.x < 21.0 { north = Math.max(north, p.z) } + k += 1 + } + expect(north > 14.0) + expect_eq(p.act, NPCA_SIT) + expect(npc_d(p.x, p.z, 38.0, -25.0) < 1.3) + expect(way_test_st.asked > 5) + } +} diff --git a/packages/ludic.npc/walk.ludic b/packages/ludic.npc/walk.ludic index 230a7797..90758386 100644 --- a/packages/ludic.npc/walk.ludic +++ b/packages/ludic.npc/walk.ludic @@ -1,6 +1,6 @@ -# walk.ludic - walking to the target: turn toward it, step, slid round a trunk or a tent, never into -# the water and never up a scramble - step round it instead - and give up on a target that gets no -# closer for twenty seconds +# walk.ludic - walking to the target by the world's way where it has one (way.ludic): turn toward the +# next corner, step, slid round a trunk or a tent, never into the water and never up a scramble - step +# round it instead - and give up on a target that gets no closer for twenty seconds const NP_BODY_R: float = 0.35 # a person's width, for the still things export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void { @@ -10,12 +10,17 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void gx = p.vx gz = p.vz } - let dx = gx - p.x - let dz = gz - p.z - if Math.sqrt(dx * dx + dz * dz) < 1.2 { + if npc_d(p.x, p.z, gx, gz) < 1.2 { np_arrive(npc_st, p) return } + if not p.via_on { + np_way(p, dt) + gx = p.wx + gz = p.wz + } + let dx = gx - p.x + let dz = gz - p.z let dg = npc_d(p.x, p.z, p.tx, p.tz) if dg < p.best - 1.0 { p.best = dg @@ -79,28 +84,3 @@ function np_arrive(npc_st: mut NpcState, p: NpcPerson) -> void { p.stall = 0.0 if p.act != NPCA_WALK { np_fact(npc_st, NPC_F_ARRIVED, p, -1) } } - -function np_detour(p: NpcPerson, want: float) -> bool { - for k in 0 .. 8 { - let ang = want + float((k / 2 + 1) * (1 - 2 * (k % 2))) * 0.6 - let x = p.x - Math.sin(ang) * 6.0 - let z = p.z - Math.cos(ang) * 6.0 - if npc_line_ok(p.x, p.z, x, z) { - p.via_on = true - p.vx = x - p.vz = z - p.yaw = ang - return true - } - } - return false -} - -function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void { - p.fails += 1 - p.via_on = false - if p.fails >= 3 { - p.fails = 0 - npc_wander(npc_st, p) - } else { npc_plan(npc_st, p) } -} diff --git a/packages/ludic.npc/way.ludic b/packages/ludic.npc/way.ludic new file mode 100644 index 00000000..1108897f --- /dev/null +++ b/packages/ludic.npc/way.ludic @@ -0,0 +1,45 @@ +# way.ludic - where a person walks when the straight line will not do: the world's way (NpcWorld.way, +# a navmesh by a person's taste in Maroon Lake), and without one a step round what is in the way, +# and giving the target up after three tries + +# the next corner toward the target, asked again every half second, on reaching it, or for a new target +const NP_WAY_EVERY: float = 0.5 +function np_way(p: NpcPerson, dt: float) -> void { + p.way_t = p.way_t - dt + let fresh = p.wtx != p.tx or p.wtz != p.tz + if p.way_t > 0.0 and not fresh and npc_d(p.x, p.z, p.wx, p.wz) >= 1.0 { return } + p.way_t = NP_WAY_EVERY + p.wtx = p.tx + p.wtz = p.tz + p.wx = p.tx + p.wz = p.tz + if NpcWorld.way(p, p.tx, p.tz) { + p.wx = NpcWorld.way_x() + p.wz = NpcWorld.way_z() + } +} + +function np_detour(p: NpcPerson, want: float) -> bool { + for k in 0 .. 8 { + let ang = want + float((k / 2 + 1) * (1 - 2 * (k % 2))) * 0.6 + let x = p.x - Math.sin(ang) * 6.0 + let z = p.z - Math.cos(ang) * 6.0 + if npc_line_ok(p.x, p.z, x, z) { + p.via_on = true + p.vx = x + p.vz = z + p.yaw = ang + return true + } + } + return false +} + +function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void { + p.fails += 1 + p.via_on = false + if p.fails >= 3 { + p.fails = 0 + npc_wander(npc_st, p) + } else { npc_plan(npc_st, p) } +} diff --git a/packages/ludic.render3d/gltf.ludic b/packages/ludic.render3d/gltf.ludic index c991972a..2862f985 100644 --- a/packages/ludic.render3d/gltf.ludic +++ b/packages/ludic.render3d/gltf.ludic @@ -163,7 +163,11 @@ function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, no render3d_st.gltf_dir = dir let text = Fs.read_text(dir + "/" + file) if text == null { print(`gltf: cannot read {dir}/{file}`); return null } + # the last file's tree goes first (it is read only until the next load: ludic.anim's read.ludic), + # and the text once parsed - nothing gives memory back on its own (plan 23 of maroon-lake) + if render3d_st.gltf_doc != null { Json.free(render3d_st.gltf_doc) } render3d_st.gltf_doc = Json.parse(text) + free(text) let buffers = value_get(render3d_st.gltf_doc, "buffers") let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri")) render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb") diff --git a/packages/ludic.save/read.ludic b/packages/ludic.save/read.ludic index 454488b2..242c2625 100644 --- a/packages/ludic.save/read.ludic +++ b/packages/ludic.save/read.ludic @@ -6,8 +6,10 @@ export function save_parse(text: string, f: SaveFormat) -> SaveRead { if text == null { return read_new(SAVE_NONE, null) } if not save_text_intact(text) { return read_new(SAVE_CORRUPT, null) } let v = Json.parse(text) - if value_kind(v) != 6 { return read_new(SAVE_CORRUPT, null) } # 6 is an object - if f.is_one != null and not f.is_one(v) { return read_new(SAVE_CORRUPT, null) } + if value_kind(v) != 6 or (f.is_one != null and not f.is_one(v)) { # 6 is an object + if v != null { Json.free(v) } # a refused tree is let go + return read_new(SAVE_CORRUPT, null) + } return read_new(SAVE_OK, v) } diff --git a/packages/ludic.telemetry/queue.ludic b/packages/ludic.telemetry/queue.ludic index db920021..893720d7 100644 --- a/packages/ludic.telemetry/queue.ludic +++ b/packages/ludic.telemetry/queue.ludic @@ -92,6 +92,10 @@ function telemetry__read_obj(path: string) -> Val { let s = Fs.read_text(path) if s == null { return null } let v = Json.parse(s) - if v == null or Value.kind(v) != 6 { return null } + if v == null { return null } + if Value.kind(v) != 6 { + Json.free(v) + return null + } return v } diff --git a/packages/ludic.wildlife/README.md b/packages/ludic.wildlife/README.md index c9d5b67b..67cf0306 100644 --- a/packages/ludic.wildlife/README.md +++ b/packages/ludic.wildlife/README.md @@ -46,9 +46,18 @@ export port WildlifeWorld { # every member has a default: a flat dry mead next_id: fn() -> int # an animal's id across machines born: fn(WildAnimal) -> void # draw it placed: fn(WildRange, int) -> void # a range was set out: put its water and forage down + way: fn(WildAnimal, float, float) -> bool # the next corner of its way toward (x, z), into way_x / _z + goal: fn(WildAnimal, x, z, r, seed) -> bool # a spot about r from (x, z) it can walk to, into way_x / _z } ``` +**An animal walks by the world's way where the world has one.** Wandering and going to water or +forage face the next corner `way` gives (asked again every half second, or on reaching the corner), +and a wander's target is a spot `goal` says it can reach, seeded by the same two draws the old +target took - so binding them changes where an animal walks and never the order of the dice. With +no answer (the defaults) it walks straight and turns 75 degrees off water and a climb, as before. +Maroon Lake binds both to its navmesh, walked by the species' habitat's taste. + ## API | | | diff --git a/packages/ludic.wildlife/animal.ludic b/packages/ludic.wildlife/animal.ludic index eb3f52e4..56e92221 100644 --- a/packages/ludic.wildlife/animal.ludic +++ b/packages/ludic.wildlife/animal.ludic @@ -41,6 +41,9 @@ export property WildAnimal { home_z: float = 0.0 tx: float = 0.0 tz: float = 0.0 + wx: float = 0.0 # the corner of its way it is walking to now (WildlifeWorld.way) + wz: float = 0.0 + way_t: float = 0.0 # seconds until it asks the way again; 0 asks at once thirst: float = 0.0 # 0..100: over 60 it goes looking for water hunger: float = 0.0 scat_t: float = 0.0 # game minutes until it might leave sign diff --git a/packages/ludic.wildlife/ground.ludic b/packages/ludic.wildlife/ground.ludic index ae9339c0..26313853 100644 --- a/packages/ludic.wildlife/ground.ludic +++ b/packages/ludic.wildlife/ground.ludic @@ -31,7 +31,7 @@ export function wildlife_tick_ground(wildlife_st: mut WildlifeState, a: WildAnim } return } - wl_face(a, a.tx, a.tz, 2.0, dt) + wl_head(a, a.tx, a.tz, 2.0, dt) a.speed = s.walk wl_step(wildlife_st, a, dt) if wl_at_target(a, 1.5) or a.timer < 0.0 { diff --git a/packages/ludic.wildlife/index.ludic b/packages/ludic.wildlife/index.ludic index 0a65d1ed..37f48c0a 100644 --- a/packages/ludic.wildlife/index.ludic +++ b/packages/ludic.wildlife/index.ludic @@ -18,6 +18,7 @@ import "ranges.ludic" import "migrate.ludic" import "senses.ludic" import "steer.ludic" +import "way.ludic" import "lures.ludic" import "needs.ludic" import "ground.ludic" diff --git a/packages/ludic.wildlife/port.ludic b/packages/ludic.wildlife/port.ludic index 5fb77d1c..7c599b5c 100644 --- a/packages/ludic.wildlife/port.ludic +++ b/packages/ludic.wildlife/port.ludic @@ -46,6 +46,13 @@ export port WildlifeWorld { next_id: fn() -> int = fn wl_next_id born: fn(WildAnimal) -> void = fn wl_nothing # a new animal: draw it placed: fn(WildRange, int) -> void = fn wl_range_nothing # a range was set out + # (a, x, z): the next corner of this animal's way toward (x, z) into way_x / _z; false, the + # straight line (a navmesh's path by its habitat's taste, in Maroon Lake) + way: fn(WildAnimal, float, float) -> bool = fn wl_no_way + # (a, x, z, r, seed): a spot about r from (x, z) this animal can walk to, into way_x / _z + goal: fn(WildAnimal, float, float, float, int) -> bool = fn wl_no_goal + way_x: fn() -> float = fn wl_asked_x + way_z: fn() -> float = fn wl_asked_z } function wl_zero0() -> float { return 0.0 } function wl_zero1(p: int) -> float { return 0.0 } @@ -73,6 +80,8 @@ function wl_no_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { return fa function wl_no_lure(i: int, l: WildLure) -> bool { return false } function wl_nothing(a: WildAnimal) -> void { } function wl_range_nothing(r: WildRange, i: int) -> void { } +function wl_no_way(a: WildAnimal, x: float, z: float) -> bool { return false } +function wl_no_goal(a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool { return false } function wl_next_id(wildlife_st: mut WildlifeState) -> int { wildlife_st.wl_ids += 1 return wildlife_st.wl_ids diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index b561d5cc..4e73371e 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -1,5 +1,5 @@ # steer.ludic - turning toward a point at a rate, a step along the facing that turns away from water -# and a climb, and a target somewhere about home; a walker leaves a print every 2.8 m +# and a climb; a walker leaves a print every 2.8 m (the way there and a target about home: way.ludic) function wl_wrap(a: float) -> float { var r = a while r > 3.1415927 { r = r - WL_TAU } @@ -58,13 +58,6 @@ function wl_push(wildlife_st: mut WildlifeState, a: WildAnimal, s: WildSpecies, return hit } -function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void { - let ang = wl_rnd(wildlife_st) * WL_TAU - let d = wl_rnd(wildlife_st) * radius - a.tx = a.home_x + Math.cos(ang) * d - a.tz = a.home_z + Math.sin(ang) * d -} - function wl_at_target(a: WildAnimal, r: float) -> bool { let dx = a.tx - a.x let dz = a.tz - a.z diff --git a/packages/ludic.wildlife/tests/way_test.ludic b/packages/ludic.wildlife/tests/way_test.ludic new file mode 100644 index 00000000..b9624030 --- /dev/null +++ b/packages/ludic.wildlife/tests/way_test.ludic @@ -0,0 +1,106 @@ +# way_test.ludic - an animal walks by the world's way where the world has one: round a wall the +# straight line runs into (by the corners WildlifeWorld.way gives), and to a wander target the world +# says it can reach (goal); with no answer it walks straight, as before +import "ludic.wildlife" +import "ludic.base" +import "fake" +program WayTest { + numbers float + bind WildlifeWorld { + ground: fn fk_ground + water: fn fk_water + forest: fn fk_forest + sight: fn fk_sight + push: fn fk_push + pushed_x: fn fk_pushed_x + pushed_z: fn fk_pushed_z + players: fn fk_players + player_on: fn fk_player_on + player_x: fn fk_x + player_y: fn fk_y + player_z: fn fk_z + hidden: fn fk_is_hidden + noise: fn fk_noise_of + scent: fn fk_scent_of + stillness: fn fk_still_of + wind: fn fk_wind_of + wind_dir: fn fk_wind_to + winterness: fn fk_winter_of + about: fn fk_about_of + per_range: fn fk_one + lures: fn fk_count + lure: fn fk_lure + present: fn fk_present + born: fn fk_on_born + spot: fn fk_spot + way: fn wt_way + way_x: fn wt_x + way_z: fn wt_z + goal: fn wt_goal + } + + state WayTestState { + wx: float = 0.0 + wz: float = 0.0 + asked: int = 0 + } + # a wall across z = -40 for |x| < 30: the way goes round its east end, then on to the target + function wt_way(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float) -> bool { + way_test_st.asked += 1 + way_test_st.wx = x + way_test_st.wz = z + if a.z > -38.0 and a.x < 31.0 { + way_test_st.wx = 32.0 + way_test_st.wz = -40.0 + } + return true + } + # a wander target a known step from home, whatever the seed + function wt_goal(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool { + way_test_st.wx = x + 7.0 + way_test_st.wz = z + 3.0 + return true + } + function wt_x(way_test_st: WayTestState) -> float { return way_test_st.wx } + function wt_z(way_test_st: WayTestState) -> float { return way_test_st.wz } + + test "it walks round the wall by the corners it is given, and asks again as it goes" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = wildlife_make(wildlife_st, 0, 0.0, 0.0, false, 0.0, 0, 1.0, 1) + wildlife_tests_fake_st.fk_px[0] = 400.0 + wildlife_tests_fake_st.fk_pz[0] = 0.0 + wildlife_tests_fake_st.fk_hidden = true + a.state = WILD_WANDER + a.tx = 0.0 + a.tz = -100.0 + a.timer = 300.0 + var east = -100.0 + for k in 0 .. 3000 { + wildlife_tick(wildlife_st, 0.05, 0.0) + if a.z > -42.0 and a.z < -38.0 { east = Math.max(east, a.x) } + } + expect(east > 29.0) + expect(a.z < -60.0) + expect(way_test_st.asked > 10) + } + + test "a wander target is where the world says it can walk" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = wildlife_make(wildlife_st, 0, 5.0, 5.0, false, 0.0, 0, 1.0, 1) + wildlife_tests_fake_st.fk_px[0] = 400.0 + wildlife_tests_fake_st.fk_pz[0] = 0.0 + wildlife_tests_fake_st.fk_hidden = true + a.hunger = 0.0 + a.thirst = 0.0 + a.state = WILD_IDLE + a.timer = -1.0 + var k = 0 + while a.state != WILD_WANDER and k < 2000 { + wildlife_tick(wildlife_st, 0.05, 0.0) + k += 1 + } + expect_eq(a.state, WILD_WANDER) + expect_near(a.tx, a.home_x + 7.0, 0.001) + expect_near(a.tz, a.home_z + 3.0, 0.001) + } +} diff --git a/packages/ludic.wildlife/want.ludic b/packages/ludic.wildlife/want.ludic index 696d6005..ebe42f43 100644 --- a/packages/ludic.wildlife/want.ludic +++ b/packages/ludic.wildlife/want.ludic @@ -10,7 +10,7 @@ function wl_seek(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo a.timer = 2.0 return true } - wl_face(a, a.spot_x, a.spot_z, 2.0, dt) + wl_head(a, a.spot_x, a.spot_z, 2.0, dt) a.speed = wl_sp(wildlife_st, a.sp).walk a.tx = a.spot_x a.tz = a.spot_z @@ -44,6 +44,7 @@ function wl_drink(wildlife_st: WildlifeState, a: WildAnimal, dt: float, gh: floa let water = a.thirst > a.hunger if not WildlifeWorld.spot(a, water, wildlife_st.wl_spot_ask) { return false } a.spot = wildlife_st.wl_spot_ask.uid + a.way_t = 0.0 a.spot_x = wildlife_st.wl_spot_ask.x a.spot_z = wildlife_st.wl_spot_ask.z a.spot_water = wildlife_st.wl_spot_ask.water diff --git a/packages/ludic.wildlife/way.ludic b/packages/ludic.wildlife/way.ludic new file mode 100644 index 00000000..ddc00e31 --- /dev/null +++ b/packages/ludic.wildlife/way.ludic @@ -0,0 +1,36 @@ +# way.ludic - where an animal walks: by the world's path to where it is going (WildlifeWorld.way, a +# navmesh by its habitat's taste in Maroon Lake), to a target about home it can reach (goal), and +# straight, as before, where the world has no answer + +# face the way toward (tx, tz): the next corner of the world's path for it, asked again every +# WL_WAY_EVERY seconds or on reaching the corner; without a path, the straight line as before +const WL_WAY_EVERY: float = 0.5 +function wl_head(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> void { + a.way_t = a.way_t - dt + let cx = a.wx - a.x + let cz = a.wz - a.z + if a.way_t <= 0.0 or cx * cx + cz * cz < 1.0 { + a.way_t = WL_WAY_EVERY + a.wx = tx + a.wz = tz + if WildlifeWorld.way(a, tx, tz) { + a.wx = WildlifeWorld.way_x() + a.wz = WildlifeWorld.way_z() + } + } + wl_face(a, a.wx, a.wz, rate, dt) +} + +# somewhere about home: a spot the world says it can walk to when it knows, seeded by the same two +# draws the old target took, so the dice are the same whether it knows or not +function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void { + let ang = wl_rnd(wildlife_st) * WL_TAU + let d = wl_rnd(wildlife_st) * radius + a.tx = a.home_x + Math.cos(ang) * d + a.tz = a.home_z + Math.sin(ang) * d + a.way_t = 0.0 + if WildlifeWorld.goal(a, a.home_x, a.home_z, radius, int(ang * 100000.0) + int(d * 1000.0)) { + a.tx = WildlifeWorld.way_x() + a.tz = WildlifeWorld.way_z() + } +} diff --git a/runtime/native/namespaces.ludic b/runtime/native/namespaces.ludic index 7c24ea21..3f8627e4 100644 --- a/runtime/native/namespaces.ludic +++ b/runtime/native/namespaces.ludic @@ -478,10 +478,12 @@ namespace Value { alias as_int(value) = value_as_int alias as_float(value) = value_as_float alias as_str(value) = value_as_str + alias free(value) = value_free } namespace Json { alias encode(value) = json_encode alias parse(text) = json_parse + alias free(value) = json_free } namespace Xml { alias parse(text) = xml_parse diff --git a/runtime/native/value.ludic b/runtime/native/value.ludic index dab65b28..cc3e4c0b 100644 --- a/runtime/native/value.ludic +++ b/runtime/native/value.ludic @@ -166,9 +166,23 @@ function jp_skip_ws(p: JP) -> void { function json_parse(s: pointer) -> Val { let p = new JP p.s = s; p.i = 0; p.n = len(s) - return jp_value(p) + let v = jp_value(p) + free(p) + return v } +# Let a parsed tree go once what it describes is built: every node and its lists. Its strings stay - +# a caller may keep a name it read out of the tree - and a tree put together by hand, which may share +# a node or hold a literal, is not for this (Ludic has no collector; plan 23 of maroon-lake). +function value_free(v: Val) -> void { + if v == null { return } + for i in 0 .. len(v.kids) { value_free(v.kids[i]) } + free(v.kids) + free(v.keys) + free(v) +} +function json_free(v: Val) -> void { value_free(v) } + function jp_value(p: JP) -> Val { jp_skip_ws(p) if p.i >= p.n { return value_null() } @@ -182,9 +196,29 @@ function jp_value(p: JP) -> Val { return jp_number(p) } -# read a quoted string (cursor on the opening quote) -> the unescaped contents. +# read a quoted string (cursor on the opening quote) -> the unescaped contents. One allocation for a +# string with no escape in it: built a character at a time, every step was a string never freed function jp_string(p: JP) -> string { p.i += 1 # skip opening quote + let a = p.i + while p.i < p.n { + let c = p.s[p.i] + if c == '"' { + let out = p.s[a..p.i] + p.i += 1 + return out + } + if c == '\\' { + p.i = a + return jp_string_esc(p) + } + p.i += 1 + } + return p.s[a..p.n] +} + +# the same with escapes in it, the rare case, a character at a time +function jp_string_esc(p: JP) -> string { var out = "" while p.i < p.n { let c = p.s[p.i]