From 81581132adb0a4dcd89dcd25efe6c3566515d57c Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Sun, 11 Oct 2026 17:29:17 +0200 Subject: [PATCH] feat(sim): integrate Picard actors for living entities Give people and animals persistent Picard actors and document runtime boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 420 ++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + README.md | 2 + src/world.rs | 209 ++++++++++++++++++++++++- 4 files changed, 626 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65352fa..e0456fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "any_spawner" version = "0.3.0" @@ -46,6 +55,21 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "async-lock" version = "3.4.2" @@ -197,6 +221,17 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "better_default" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b9a5040dce49a7642c97ccb1ae59567098967b5d52c29773f1299a42d23bb39" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "bitflags" version = "2.13.2" @@ -255,6 +290,19 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "codee" version = "0.3.5" @@ -288,11 +336,18 @@ version = "0.15.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e9fbc53afcec54441422f1c596cd054655b32e70e1cbac956651b7b175a404" dependencies = [ + "async-trait", "convert_case 0.6.0", + "json5", "pathdiff", + "ron", + "rust-ini", + "serde-untagged", "serde_core", + "serde_json", "toml", "winnow", + "yaml-rust2", ] [[package]] @@ -311,6 +366,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "const-str" version = "1.1.0" @@ -371,6 +446,18 @@ dependencies = [ "convert_case 0.11.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -401,6 +488,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.8" @@ -435,6 +531,12 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -445,6 +547,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.11.1" @@ -496,6 +612,15 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -527,6 +652,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -539,6 +678,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -756,9 +915,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -837,6 +998,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -874,6 +1041,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "heck" version = "0.5.0" @@ -1011,6 +1187,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.3.0" @@ -1187,6 +1387,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c" +dependencies = [ + "serde", + "ucd-trie", +] + [[package]] name = "konst" version = "0.2.20" @@ -1492,6 +1702,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "musli" version = "0.0.124" @@ -1654,6 +1870,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "parking" version = "2.2.1" @@ -1710,6 +1936,41 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "picard" +version = "0.4.2" +source = "git+https://github.com/picard-rs/picard?rev=50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5#50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5" +dependencies = [ + "better_default", + "chrono", + "config", + "crossbeam-channel", + "dashmap", + "futures", + "getrandom 0.3.4", + "gloo-timers", + "picard-macros", + "rand 0.9.5", + "serde", + "slog", + "slog-scope", + "slog-stdlog", + "thiserror 2.0.21", + "uuid", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "picard-macros" +version = "0.2.0" +source = "git+https://github.com/picard-rs/picard?rev=50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5#50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -2071,6 +2332,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "rsa" version = "0.9.10" @@ -2198,6 +2473,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.28" @@ -2311,6 +2596,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde 0.4.10", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -2498,6 +2795,7 @@ version = "0.1.0" dependencies = [ "axum", "include_dir", + "picard", "rand 0.8.8", "rune", "serde", @@ -2519,6 +2817,40 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slog" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3b8565691b22d2bdfc066426ed48f837fc0c5f2c8cad8d9718f7f99d6995c1" +dependencies = [ + "anyhow", + "erased-serde 0.3.31", + "rustversion", + "serde_core", +] + +[[package]] +name = "slog-scope" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b76cf645c92e7850d5a1c9205ebf2864bd32c0ab3e978e6daad51fedf7ef54" +dependencies = [ + "arc-swap", + "lazy_static", + "slog", +] + +[[package]] +name = "slog-stdlog" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6706b2ace5bbae7291d3f8d2473e2bfab073ccd7d03670946197aec98471fa3e" +dependencies = [ + "log", + "slog", + "slog-scope", +] + [[package]] name = "slotmap" version = "1.1.1" @@ -2596,7 +2928,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink", + "hashlink 0.10.0", "indexmap", "log", "memchr", @@ -2921,6 +3253,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -3157,12 +3498,24 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "ui" version = "0.1.0" @@ -3479,6 +3832,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -3516,6 +3879,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3531,6 +3929,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3706,6 +4113,17 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "550a2b930b62486a393c52d5c3b84bff264b28aa437ed64694d31e93b1757af7" +[[package]] +name = "yaml-rust2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36710ce3a279cfce8465dbab826f161675a262950b922cb2c3663852dfe9eb0" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.11.1", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 37d855a..d95d28e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "any", "sqlite", "postgres"] } rand = "0.8" +picard = { git = "https://github.com/picard-rs/picard", rev = "50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5" } [build-dependencies] wasm-bindgen-cli-support = "=0.2.129" diff --git a/README.md b/README.md index 3ffc38e..35dfefd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A browser-based world simulation with a Rust server and a Leptos/Wasm UI. The server runs the simulation and exposes its state over HTTP and WebSockets. You can edit terrain and resources, manage people and animals, and customize agent behavior with the included Rune script. +Living simulation entities (people, rabbits, and wolves) run as Picard actors. The world coordinates terrain, shared resources, and actor messages; Rune behavior scripts remain on the simulation thread because Rune's VM is not `Send`. + The UI is built automatically as part of `cargo run` and embedded in the server binary. There is no separate manual UI build step. ## Getting Started diff --git a/src/world.rs b/src/world.rs index 009d2ed..f9429bf 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,8 +1,17 @@ +use picard::actors::{ + Actor, ActorFactoryArgs, ActorRef, ActorRefFactory, ActorReference, ActorSystem, + Context as ActorContext, Sender, Tell, +}; use rune::termcolor::Buffer; use rune::{Any, Context, Diagnostics, Module, Source, Sources, Vm}; use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; use std::fmt::Write; -use std::sync::Arc; +use std::sync::{ + Arc, + mpsc::{SyncSender, sync_channel}, +}; +use std::time::Duration; pub const W: usize = 96; pub const H: usize = 64; @@ -148,8 +157,10 @@ fn load_script(src: &str) -> Result { Ok(vm) } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Person { + #[serde(skip)] + actor_id: u64, id: u32, x: usize, y: usize, @@ -167,14 +178,61 @@ impl Person { } } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Animal { + #[serde(skip)] + actor_id: u64, x: usize, y: usize, energy: f32, dead: bool, } +#[derive(Clone, Debug)] +enum EntityKind { + Person(Person), + Rabbit(Animal), + Wolf(Animal), +} + +#[derive(Clone, Debug)] +struct EntityState { + id: u64, + kind: EntityKind, +} + +#[derive(Debug)] +enum EntityMessage { + Read(SyncSender), + Write(EntityState, SyncSender<()>), +} + +struct EntityActor { + state: EntityState, +} + +impl ActorFactoryArgs for EntityActor { + fn create_args(state: EntityState) -> Self { + Self { state } + } +} + +impl Actor for EntityActor { + type Msg = EntityMessage; + + fn recv(&mut self, _ctx: &ActorContext, msg: Self::Msg, _sender: Sender) { + match msg { + EntityMessage::Read(reply) => { + let _ = reply.send(self.state.clone()); + } + EntityMessage::Write(state, reply) => { + self.state = state; + let _ = reply.send(()); + } + } + } +} + pub struct World { seed: u64, tiles: Vec, @@ -182,6 +240,9 @@ pub struct World { people: Vec, rabbits: Vec, wolves: Vec, + actor_system: ActorSystem, + entities: HashMap>, + next_actor_id: u64, houses: Vec<(usize, usize)>, home: (usize, usize), food: u32, @@ -260,6 +321,9 @@ impl World { people: vec![], rabbits: vec![], wolves: vec![], + actor_system: ActorSystem::new().expect("failed to start Picard actor system"), + entities: HashMap::new(), + next_actor_id: 1, houses: vec![], home: (W / 2, H / 2), food: 4, @@ -296,6 +360,7 @@ impl World { for _ in 0..60 { if let Some(p) = w.random_land() { w.rabbits.push(Animal { + actor_id: 0, x: p.0, y: p.1, energy: 70.0, @@ -307,6 +372,7 @@ impl World { if let Some(p) = w.random_land() { if cheb(p, w.home) > 15 { w.wolves.push(Animal { + actor_id: 0, x: p.0, y: p.1, energy: 70.0, @@ -315,9 +381,104 @@ impl World { } } } + w.sync_entities(); w } + fn read_entities(actors: &HashMap>) -> Vec { + let replies: Vec<_> = actors + .values() + .map(|actor| { + let (reply, result) = sync_channel(1); + actor.tell(EntityMessage::Read(reply), None); + result + }) + .collect(); + let mut states: Vec<_> = replies + .into_iter() + .map(|reply| { + reply + .recv_timeout(Duration::from_secs(5)) + .expect("Picard entity actor did not respond to read") + }) + .collect(); + states.sort_by_key(|state| state.id); + states + } + + fn load_entities(&mut self) { + let states = Self::read_entities(&self.entities); + self.people.clear(); + self.rabbits.clear(); + self.wolves.clear(); + for state in states { + match state.kind { + EntityKind::Person(person) => self.people.push(person), + EntityKind::Rabbit(animal) => self.rabbits.push(animal), + EntityKind::Wolf(animal) => self.wolves.push(animal), + } + } + } + + fn sync_entities(&mut self) { + for person in &mut self.people { + if person.actor_id == 0 { + person.actor_id = self.next_actor_id; + self.next_actor_id += 1; + } + } + for animal in self.rabbits.iter_mut().chain(self.wolves.iter_mut()) { + if animal.actor_id == 0 { + animal.actor_id = self.next_actor_id; + self.next_actor_id += 1; + } + } + + let states = self + .people + .iter() + .cloned() + .map(|person| EntityState { + id: person.actor_id, + kind: EntityKind::Person(person), + }) + .chain(self.rabbits.iter().cloned().map(|animal| EntityState { + id: animal.actor_id, + kind: EntityKind::Rabbit(animal), + })) + .chain(self.wolves.iter().cloned().map(|animal| EntityState { + id: animal.actor_id, + kind: EntityKind::Wolf(animal), + })); + let mut active = HashSet::new(); + for state in states { + active.insert(state.id); + if let Some(actor) = self.entities.get(&state.id) { + let (reply, result) = sync_channel(1); + actor.tell(EntityMessage::Write(state, reply), None); + result + .recv_timeout(Duration::from_secs(5)) + .expect("Picard entity actor did not respond to write"); + } else { + let id = state.id; + let name = format!("entity-{id}"); + let actor = self + .actor_system + .actor_of_args::(&name, state) + .expect("failed to create Picard entity actor"); + self.entities.insert(id, actor); + } + } + self.entities.retain(|id, actor| { + if active.contains(id) { + true + } else { + actor.stop(); + false + } + }); + } + fn find_land(&self, kind: u8) -> Option<(usize, usize)> { let c = (W / 2, H / 2); let mut best: Option<(usize, usize)> = None; @@ -346,6 +507,7 @@ impl World { fn spawn_person(&mut self) { let (x, y) = self.home; self.people.push(Person { + actor_id: 0, id: self.next_id, x, y, @@ -436,6 +598,7 @@ impl World { } pub fn step(&mut self) { + self.load_entities(); self.tick += 1; self.regrow(); if self.tick % 2 == 0 { @@ -454,6 +617,7 @@ impl World { self.rabbits.retain(|a| !a.dead); self.wolves.retain(|a| !a.dead); self.population(); + self.sync_entities(); } fn regrow(&mut self) { @@ -483,6 +647,7 @@ impl World { if self.rabbits.len() < 15 { if let Some(p) = self.random_land() { self.rabbits.push(Animal { + actor_id: 0, x: p.0, y: p.1, energy: 70.0, @@ -494,6 +659,7 @@ impl World { if let Some(p) = self.random_land() { if cheb(p, self.home) > 15 { self.wolves.push(Animal { + actor_id: 0, x: p.0, y: p.1, energy: 70.0, @@ -677,6 +843,7 @@ impl World { if a.energy > 80.0 && self.rabbits.len() < 120 && self.rng.chance(0.01) { a.energy -= 30.0; self.rabbits.push(Animal { + actor_id: 0, x: a.x, y: a.y, energy: 50.0, @@ -715,6 +882,7 @@ impl World { if a.energy > 90.0 && self.wolves.len() < 10 && self.rng.chance(0.02) { a.energy -= 40.0; self.wolves.push(Animal { + actor_id: 0, x: a.x, y: a.y, energy: 50.0, @@ -800,13 +968,17 @@ impl World { return Err("world snapshot has invalid terrain dimensions".into()); } let vm = load_script(&saved.script)?; - Ok(Self { + let mut world = Self { seed: saved.seed.unwrap_or(fallback_seed), tiles: saved.tiles, res: saved.res, people: saved.people, rabbits: saved.rabbits, wolves: saved.wolves, + actor_system: ActorSystem::new() + .map_err(|e| format!("failed to start Picard actor system: {e}"))?, + entities: HashMap::new(), + next_actor_id: 1, houses: saved.houses, home: saved.home, food: saved.food, @@ -822,7 +994,9 @@ impl World { paused: saved.paused, speed: saved.speed, terrain_rev: saved.terrain_rev, - }) + }; + world.sync_entities(); + Ok(world) } fn rect(t: &[&str], i: usize) -> Option<(usize, usize, usize, usize)> { @@ -838,6 +1012,7 @@ impl World { /// Applies one whitespace-separated edit command; malformed commands are ignored. pub fn apply(&mut self, cmd: &str) { + self.load_entities(); let t: Vec<&str> = cmd.split_whitespace().collect(); let num = |i: usize| t.get(i).and_then(|s| s.parse::().ok()); match t.first().copied() { @@ -923,6 +1098,7 @@ impl World { } left -= 1; let a = Animal { + actor_id: 0, x, y, energy: 70.0, @@ -985,6 +1161,7 @@ impl World { } _ => {} } + self.sync_entities(); } fn paint_terrain(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, kind: u8) { @@ -1093,6 +1270,12 @@ impl World { } } +impl Drop for World { + fn drop(&mut self) { + let _ = self.actor_system.shutdown(); + } +} + #[cfg(test)] mod tests { use super::{DAY, DEFAULT_SCRIPT, World}; @@ -1136,4 +1319,20 @@ mod tests { let world = World::new(42); assert_ne!(world.regenerated_seed(), world.seed()); } + + #[test] + fn picard_actors_own_living_entity_state() { + let mut world = World::new(42); + let person_id = world.people[0].id; + + world.apply(&format!("person {person_id} hunger 73")); + world.people[0].hunger = 1.0; + world.load_entities(); + + assert_eq!(world.people[0].hunger, 73.0); + assert_eq!( + world.entities.len(), + world.people.len() + world.rabbits.len() + world.wolves.len() + ); + } }