From 84925b334adceb7ed6cb47f7b6fa53d9db161821 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 16:54:23 +0100 Subject: [PATCH 1/8] chore(config): add Rust build artifacts to gitignore server/target/ excluded from version control. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bc32a5f60..85dc30c42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build and cache .cache/ +server/target/ # Database journal files (transient, not the DB itself) db/commonwealth.db-wal From f67caf1986699194ef7c9cca8ff21dc3edbc5947 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 16:54:40 +0100 Subject: [PATCH 2/8] feat(server): initialize Rust/bevy_ecs simulation server Server boilerplate epic (276) complete. Establishes the Rust simulation server foundation per D-020 (subprocess/IPC architecture). Structure: - bevy_ecs 0.18 + bevy_app 0.18, MessagePack serialization (rmp-serde) - SimulationPlugin with deterministic resources: SimulationTime (D-031), SimRng (D-030), InputQueue (D-010) - Core IPC types: ObserverSnapshot, PlayerInput, SimBridge trait (D-020) - CauseChain production component for provenance tracking (D-030) - SimulationTier types with LRU eviction support (D-026) - NPC 10-axis model components (D-024) - 15 tests: inline unit tests + integration smoke/serialization tests - make ci-server passes (clippy, fmt, build, test) Co-Authored-By: Claude Opus 4.6 --- server/.gitkeep | 0 server/Cargo.lock | 1301 ++++++++++++++++++++++++++++++++ server/Cargo.toml | 16 + server/src/bridge/mod.rs | 40 + server/src/bridge/types.rs | 56 ++ server/src/cause_chain.rs | 72 ++ server/src/lib.rs | 9 + server/src/main.rs | 31 + server/src/npc/mod.rs | 76 ++ server/src/perception/mod.rs | 16 + server/src/simulation/input.rs | 77 ++ server/src/simulation/mod.rs | 25 + server/src/simulation/rng.rs | 54 ++ server/src/simulation/tier.rs | 58 ++ server/src/simulation/time.rs | 117 +++ server/src/storyteller/mod.rs | 15 + server/tests/serialization.rs | 51 ++ server/tests/smoke.rs | 22 + 18 files changed, 2036 insertions(+) delete mode 100644 server/.gitkeep create mode 100644 server/Cargo.lock create mode 100644 server/Cargo.toml create mode 100644 server/src/bridge/mod.rs create mode 100644 server/src/bridge/types.rs create mode 100644 server/src/cause_chain.rs create mode 100644 server/src/lib.rs create mode 100644 server/src/main.rs create mode 100644 server/src/npc/mod.rs create mode 100644 server/src/perception/mod.rs create mode 100644 server/src/simulation/input.rs create mode 100644 server/src/simulation/mod.rs create mode 100644 server/src/simulation/rng.rs create mode 100644 server/src/simulation/tier.rs create mode 100644 server/src/simulation/time.rs create mode 100644 server/src/storyteller/mod.rs create mode 100644 server/tests/serialization.rs create mode 100644 server/tests/smoke.rs diff --git a/server/.gitkeep b/server/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/Cargo.lock b/server/Cargo.lock new file mode 100644 index 000000000..4bba5b658 --- /dev/null +++ b/server/Cargo.lock @@ -0,0 +1,1301 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bevy_app" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2271a0123a7cc355c3fe98754360c75aa84b29f2a6b1a9f8c00aac427570d174" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "cfg-if", + "ctrlc", + "downcast-rs", + "log", + "thiserror", + "variadics_please", +] + +[[package]] +name = "bevy_derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b6a05c31f54c83d681f1b8699bbaf581f06b25a40c9a6bb815625f731f5ba9" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_ecs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24637a7c8643cab493f4085cda6bde4895f0e0816699c59006f18819da2ca0b8" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags", + "bumpalo", + "concurrent-queue", + "derive_more", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eb14c18ca71e11c69fbae873c2db129064efac6d52e48d0127d37bfba1acfa8" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7272fca0bf30d8ca2571a803598856104b63e5c596d52850f811ed37c5e1e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "toml_edit", +] + +[[package]] +name = "bevy_platform" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b29ea749a8e85f98186ab662f607b885b97c804bb14cdb0cdf838164496d474" +dependencies = [ + "critical-section", + "foldhash", + "futures-channel", + "hashbrown", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "bevy_ptr" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f98cbc6d34bbdb58240b72ed1731931b4991a893b3a3238bb7c42ae054aa676" + +[[package]] +name = "bevy_reflect" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2a977e2b8dba65b6e9c11039c5f9ef108be428f036b3d1cac13ad86ec59f9c" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs", + "erased-serde", + "foldhash", + "glam", + "indexmap", + "serde", + "smallvec", + "smol_str", + "thiserror", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "067af30072b1611fda1a577f1cb678b8ea2c9226133068be808dd49aac30cef0" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_tasks" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990ffedd374dd2c4fe8f0fd4bcefd5617d1ee59164b6c3fcc356a69b48e26e8e" +dependencies = [ + "async-channel", + "async-executor", + "async-task", + "atomic-waker", + "bevy_platform", + "crossbeam-queue", + "derive_more", + "futures-lite", + "heapless", + "pin-project", +] + +[[package]] +name = "bevy_utils" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e258c44d869f9c41ac0f88a16815c67f2569eb9fff4716828a40273d127b6f84" +dependencies = [ + "bevy_platform", + "disqualified", + "thread_local", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "equivalent", + "serde", + "serde_core", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32", + "portable-atomic", + "stable_deref_trait", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "settled-reach-server" +version = "0.1.0" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bincode", + "rand", + "rand_chacha", + "rmp-serde", + "serde", + "thiserror", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "serde", + "thiserror", + "web-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 000000000..9b21c6d68 --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "settled-reach-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +bevy_ecs = "0.18" +bevy_app = "0.18" +serde = { version = "1", features = ["derive"] } +rmp-serde = "1" +bincode = "1" +rand = "0.9" +rand_chacha = "0.9" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/server/src/bridge/mod.rs b/server/src/bridge/mod.rs new file mode 100644 index 000000000..50619a4b1 --- /dev/null +++ b/server/src/bridge/mod.rs @@ -0,0 +1,40 @@ +// Bridge module - Client-server communication +// Implements D-020 subprocess/IPC architecture +// MessagePack serialization for Rust<->Godot communication + +use bevy_app::prelude::*; + +pub mod types; +pub use types::*; + +/// Error type for bridge operations +#[derive(Debug, thiserror::Error)] +pub enum BridgeError { + #[error("serialization error: {0}")] + Serialization(#[from] rmp_serde::encode::Error), + #[error("deserialization error: {0}")] + Deserialization(#[from] rmp_serde::decode::Error), + #[error("transport error: {0}")] + Transport(String), +} + +/// Abstracts transport layer (D-020) +/// Implemented by LocalBridge (stdio) and future NetworkBridge +pub trait SimBridge: Send + Sync { + /// Send an observer snapshot to the client + fn send_snapshot(&self, snapshot: &ObserverSnapshot) -> Result<(), BridgeError>; + + /// Receive player inputs from the client + fn receive_inputs(&self) -> Result, BridgeError>; +} + +/// Bridge plugin for client-server communication +/// Abstracts transport layer (LocalBridge/NetworkBridge) +pub struct BridgePlugin; + +impl Plugin for BridgePlugin { + fn build(&self, _app: &mut App) { + // Stub implementation - will be populated in phase 2 + tracing::debug!("BridgePlugin initialized"); + } +} diff --git a/server/src/bridge/types.rs b/server/src/bridge/types.rs new file mode 100644 index 000000000..c34466c8f --- /dev/null +++ b/server/src/bridge/types.rs @@ -0,0 +1,56 @@ +// Bridge type definitions +// ObserverSnapshot: data crossing the client-server boundary +// PlayerInput: semantic actions from client + +use serde::{Deserialize, Serialize}; + +/// The ONLY data structure crossing the client-server boundary (D-020) +/// Contains all information visible to the observer at a given tick +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ObserverSnapshot { + /// Simulation tick when this snapshot was produced + pub tick: u64, + /// All entities visible to the observer + pub entities: Vec, +} + +/// A visible entity in the simulation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VisibleEntity { + pub entity_id: u64, + pub x: f32, + pub y: f32, + pub z: i32, + pub kind: EntityKind, +} + +/// Category of visible entity +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EntityKind { + Npc, + Object, + Terrain, +} + +/// Semantic player actions, not raw key events (D-020) +/// Timestamped for deterministic processing +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlayerInput { + /// Tick when this input was issued + pub tick: u64, + /// The action to perform + pub action: PlayerAction, +} + +/// Player action variants +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PlayerAction { + MoveNorth, + MoveSouth, + MoveEast, + MoveWest, + Interact, + UsePerceptionMode(String), + Pause, + Unpause, +} diff --git a/server/src/cause_chain.rs b/server/src/cause_chain.rs new file mode 100644 index 000000000..456234cb9 --- /dev/null +++ b/server/src/cause_chain.rs @@ -0,0 +1,72 @@ +// Cause chain tracking system +// Production component for information provenance (D-030) +// Tracks monologue triggers, journal entries, debug causality + +use bevy_ecs::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct CauseChain { + pub causes: Vec, +} + +impl CauseChain { + pub fn new() -> Self { + Self { causes: Vec::new() } + } + pub fn with_cause(mut self, cause: Cause) -> Self { + self.causes.push(cause); + self + } + pub fn push(&mut self, cause: Cause) { + self.causes.push(cause); + } + pub fn latest(&self) -> Option<&Cause> { + self.causes.last() + } +} + +impl Default for CauseChain { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Cause { + pub tick: u64, + pub kind: CauseKind, + pub description: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum CauseKind { + Observed, + Heard, + Informed, + Inferred, + Background, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chain_preserves_order() { + let chain = CauseChain::new() + .with_cause(Cause { + tick: 1, + kind: CauseKind::Observed, + description: "Saw NPC".into(), + }) + .with_cause(Cause { + tick: 5, + kind: CauseKind::Inferred, + description: "NPC still inside".into(), + }); + assert_eq!(chain.causes.len(), 2); + assert_eq!(chain.latest().unwrap().tick, 5); + assert_eq!(chain.latest().unwrap().kind, CauseKind::Inferred); + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 000000000..aa7b2f9ee --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,9 @@ +// The Settled Reach - Simulation Server +// Rust/bevy_ecs simulation server for D-010 client-server architecture + +pub mod bridge; +pub mod cause_chain; +pub mod npc; +pub mod perception; +pub mod simulation; +pub mod storyteller; diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 000000000..b38940cf4 --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,31 @@ +// The Settled Reach - Simulation Server +// Entry point for standalone simulation binary + +use bevy_app::prelude::*; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +use settled_reach_server::bridge::BridgePlugin; +use settled_reach_server::simulation::SimulationPlugin; + +fn main() { + // Initialize tracing subscriber for logging + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "settled_reach_server=debug".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + tracing::info!("The Settled Reach - Simulation Server starting"); + + // Create the bevy App and add plugins + let mut app = App::new(); + app.add_plugins(SimulationPlugin); + app.add_plugins(BridgePlugin); + + // Run one update cycle + app.update(); + + tracing::info!("Simulation server update complete"); +} diff --git a/server/src/npc/mod.rs b/server/src/npc/mod.rs new file mode 100644 index 000000000..a365c29cc --- /dev/null +++ b/server/src/npc/mod.rs @@ -0,0 +1,76 @@ +// NPC module - NPC entity definitions and AI systems +// Implements D-024: 10-axis NPC model + CombatCapability component +// Background tier state machines for schedule, mood, relationships, job + +use bevy_ecs::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Component, Debug)] +pub struct Npc; + +// 7 essential axes (D-024) +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct Want { + pub description: String, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct Secret { + pub description: String, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct Relationships { + pub entries: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Relationship { + pub target_name: String, + pub kind: String, + pub trust_level: f32, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct ToleranceThreshold { + pub current_stress: f32, + pub threshold: f32, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct DailyRoutine { + pub description: String, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct InformationInventory { + pub known_facts: Vec, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct Contentment { + pub level: f32, +} + +// 3 supporting axes (D-024) +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct PersonalityTraits { + pub traits: Vec, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct TellSystem { + pub tells: Vec, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct SkillSet { + pub skills: Vec, + pub combat_trained: bool, +} + +#[derive(Component, Debug, Clone, Serialize, Deserialize)] +pub struct CombatCapability { + pub weapon_proficiency: f32, + pub combat_style: String, +} diff --git a/server/src/perception/mod.rs b/server/src/perception/mod.rs new file mode 100644 index 000000000..d31253691 --- /dev/null +++ b/server/src/perception/mod.rs @@ -0,0 +1,16 @@ +// Perception module - Information boundary system +// Implements D-010 principle 2: every piece of state is tagged with who knows it +// Generates ObserverSnapshot for client rendering + +use bevy_app::prelude::*; + +/// Perception system plugin +/// Manages information boundaries and observer snapshots +pub struct PerceptionPlugin; + +impl Plugin for PerceptionPlugin { + fn build(&self, _app: &mut App) { + // Stub implementation - will be populated in phase 2 + tracing::debug!("PerceptionPlugin initialized"); + } +} diff --git a/server/src/simulation/input.rs b/server/src/simulation/input.rs new file mode 100644 index 000000000..773992fbc --- /dev/null +++ b/server/src/simulation/input.rs @@ -0,0 +1,77 @@ +// Input processing system +// Timestamped player input events for deterministic simulation (D-010 principle 4) +// PlayerInput: semantic actions (MoveNorth, Interact, UsePerceptionMode) + +use crate::bridge::types::PlayerInput; +use bevy_ecs::prelude::*; +use std::collections::VecDeque; + +/// Queue of pending player inputs, ordered by tick +#[derive(Resource, Debug, Default)] +pub struct InputQueue { + queue: VecDeque, +} + +impl InputQueue { + /// Add a new input to the queue + pub fn push(&mut self, input: PlayerInput) { + self.queue.push_back(input); + } + + /// Drain all inputs for ticks <= the given tick + /// Returns inputs in FIFO order + pub fn drain_for_tick(&mut self, tick: u64) -> Vec { + let mut result = Vec::new(); + while let Some(front) = self.queue.front() { + if front.tick <= tick { + result.push(self.queue.pop_front().unwrap()); + } else { + break; + } + } + result + } + + /// Get the current queue length + pub fn len(&self) -> usize { + self.queue.len() + } + + /// Check if the queue is empty + pub fn is_empty(&self) -> bool { + self.queue.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bridge::types::PlayerAction; + + #[test] + fn drain_returns_inputs_up_to_tick() { + let mut queue = InputQueue::default(); + queue.push(PlayerInput { + tick: 1, + action: PlayerAction::MoveNorth, + }); + queue.push(PlayerInput { + tick: 2, + action: PlayerAction::MoveSouth, + }); + queue.push(PlayerInput { + tick: 5, + action: PlayerAction::Interact, + }); + let inputs = queue.drain_for_tick(3); + assert_eq!(inputs.len(), 2); + assert_eq!(queue.len(), 1); + } + + #[test] + fn drain_empty_queue_returns_empty() { + let mut queue = InputQueue::default(); + let inputs = queue.drain_for_tick(10); + assert!(inputs.is_empty()); + } +} diff --git a/server/src/simulation/mod.rs b/server/src/simulation/mod.rs new file mode 100644 index 000000000..b0cd793f4 --- /dev/null +++ b/server/src/simulation/mod.rs @@ -0,0 +1,25 @@ +// Simulation module - Core simulation plugin and systems +// Implements deterministic tick-based simulation (D-010 principle 4) + +use bevy_app::prelude::*; + +pub mod input; +pub mod rng; +pub mod tier; +pub mod time; + +/// Core simulation plugin +/// Manages simulation time, RNG, input processing, and tier transitions +pub struct SimulationPlugin; + +impl Plugin for SimulationPlugin { + fn build(&self, app: &mut App) { + // Initialize core simulation resources + app.init_resource::() + .insert_resource(rng::SimRng::new(0)) + .init_resource::() + .add_systems(Update, time::advance_tick); + + tracing::debug!("SimulationPlugin initialized"); + } +} diff --git a/server/src/simulation/rng.rs b/server/src/simulation/rng.rs new file mode 100644 index 000000000..c02650a1e --- /dev/null +++ b/server/src/simulation/rng.rs @@ -0,0 +1,54 @@ +// Simulation RNG system +// Injectable ChaCha RNG resource for deterministic replay (D-030) +// Ensures same seed produces same outcomes + +use bevy_ecs::prelude::*; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; + +/// Simulation RNG resource +/// ChaCha20 RNG with stored seed for deterministic replay +#[derive(Resource)] +pub struct SimRng { + pub rng: ChaCha20Rng, + seed: u64, +} + +impl SimRng { + /// Create a new SimRng with the given seed + pub fn new(seed: u64) -> Self { + Self { + rng: ChaCha20Rng::seed_from_u64(seed), + seed, + } + } + + /// Get the seed used to initialize this RNG + pub fn seed(&self) -> u64 { + self.seed + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::Rng; + + #[test] + fn same_seed_same_sequence() { + let mut rng1 = SimRng::new(42); + let mut rng2 = SimRng::new(42); + let vals1: Vec = (0..100).map(|_| rng1.rng.random()).collect(); + let vals2: Vec = (0..100).map(|_| rng2.rng.random()).collect(); + assert_eq!(vals1, vals2); + } + + #[test] + fn different_seed_different_sequence() { + let mut rng1 = SimRng::new(42); + let mut rng2 = SimRng::new(43); + let val1: u32 = rng1.rng.random(); + let val2: u32 = rng2.rng.random(); + assert_ne!(val1, val2); + } +} diff --git a/server/src/simulation/tier.rs b/server/src/simulation/tier.rs new file mode 100644 index 000000000..f002cf899 --- /dev/null +++ b/server/src/simulation/tier.rs @@ -0,0 +1,58 @@ +// Simulation tier system +// Implements D-026: Active/Background/State-saved/Ungenerated tiers +// Timestamp-based LRU eviction for simulation space management + +use bevy_ecs::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SimulationTier { + Active, + Background, + StateSaved, + Ungenerated, +} + +#[derive(Component, Debug, Clone)] +pub struct LastInteraction { + pub tick: u64, +} + +#[derive(Component, Debug, Clone)] +pub struct ScopeTag { + pub tags: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ScopeKind { + Neighborhood, + ActiveQuest, + Colleague, + KnownContact, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tier_can_be_added_and_queried() { + let mut world = bevy_ecs::world::World::new(); + let entity = world.spawn(SimulationTier::Active).id(); + assert_eq!( + *world.get::(entity).unwrap(), + SimulationTier::Active + ); + } + + #[test] + fn tier_can_transition() { + let mut world = bevy_ecs::world::World::new(); + let entity = world.spawn(SimulationTier::Active).id(); + world.entity_mut(entity).insert(SimulationTier::Background); + assert_eq!( + *world.get::(entity).unwrap(), + SimulationTier::Background + ); + } +} diff --git a/server/src/simulation/time.rs b/server/src/simulation/time.rs new file mode 100644 index 000000000..29b37d0f7 --- /dev/null +++ b/server/src/simulation/time.rs @@ -0,0 +1,117 @@ +// Simulation time system +// Implements D-031: 10 ticks = 1 game-minute, 4 day phases +// Injectable time resource for deterministic replay (D-030) + +use bevy_ecs::prelude::*; + +pub const TICKS_PER_GAME_MINUTE: u64 = 10; +pub const MINUTES_PER_PHASE: u64 = 360; +pub const MINUTES_PER_DAY: u64 = 1440; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DayPhase { + Morning, + Afternoon, + Evening, + Night, +} + +/// Simulation time resource +/// Tracks current tick and pause state for deterministic simulation +#[derive(Resource, Debug, Clone, Default)] +pub struct SimulationTime { + pub tick: u64, + pub paused: bool, +} + +impl SimulationTime { + pub fn game_minutes(&self) -> u64 { + self.tick / TICKS_PER_GAME_MINUTE + } + pub fn time_of_day_minutes(&self) -> u64 { + self.game_minutes() % MINUTES_PER_DAY + } + pub fn day_phase(&self) -> DayPhase { + let tod = self.time_of_day_minutes(); + match tod { + 0..360 => DayPhase::Morning, + 360..720 => DayPhase::Afternoon, + 720..1080 => DayPhase::Evening, + _ => DayPhase::Night, + } + } + pub fn day(&self) -> u64 { + self.game_minutes() / MINUTES_PER_DAY + } +} + +/// Advance the simulation tick if not paused +pub fn advance_tick(mut time: ResMut) { + if !time.paused { + time.tick += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tick_to_minute_conversion() { + let time = SimulationTime { + tick: 10, + paused: false, + }; + assert_eq!(time.game_minutes(), 1); + } + + #[test] + fn day_phase_boundaries() { + let time = SimulationTime { + tick: 0, + paused: false, + }; + assert_eq!(time.day_phase(), DayPhase::Morning); + let time = SimulationTime { + tick: 360 * TICKS_PER_GAME_MINUTE, + paused: false, + }; + assert_eq!(time.day_phase(), DayPhase::Afternoon); + let time = SimulationTime { + tick: 720 * TICKS_PER_GAME_MINUTE, + paused: false, + }; + assert_eq!(time.day_phase(), DayPhase::Evening); + let time = SimulationTime { + tick: 1080 * TICKS_PER_GAME_MINUTE, + paused: false, + }; + assert_eq!(time.day_phase(), DayPhase::Night); + } + + #[test] + fn pause_prevents_tick_advance() { + let mut world = bevy_ecs::world::World::new(); + world.insert_resource(SimulationTime { + tick: 0, + paused: true, + }); + let mut schedule = bevy_ecs::schedule::Schedule::default(); + schedule.add_systems(advance_tick); + schedule.run(&mut world); + assert_eq!(world.resource::().tick, 0); + } + + #[test] + fn unpause_allows_tick_advance() { + let mut world = bevy_ecs::world::World::new(); + world.insert_resource(SimulationTime { + tick: 0, + paused: false, + }); + let mut schedule = bevy_ecs::schedule::Schedule::default(); + schedule.add_systems(advance_tick); + schedule.run(&mut world); + assert_eq!(world.resource::().tick, 1); + } +} diff --git a/server/src/storyteller/mod.rs b/server/src/storyteller/mod.rs new file mode 100644 index 000000000..34a0fc9f7 --- /dev/null +++ b/server/src/storyteller/mod.rs @@ -0,0 +1,15 @@ +// Storyteller module - Rimworld-style storyteller system +// Event generation, pacing, hubris wall mechanics + +use bevy_app::prelude::*; + +/// Storyteller plugin +/// Manages narrative pacing and event generation +pub struct StorytellerPlugin; + +impl Plugin for StorytellerPlugin { + fn build(&self, _app: &mut App) { + // Stub implementation - will be populated in phase 2 + tracing::debug!("StorytellerPlugin initialized"); + } +} diff --git a/server/tests/serialization.rs b/server/tests/serialization.rs new file mode 100644 index 000000000..6551b76ac --- /dev/null +++ b/server/tests/serialization.rs @@ -0,0 +1,51 @@ +//! IPC serialization round-trip tests (D-030 Layer 1: fixture-based). + +use settled_reach_server::bridge::types::*; + +#[test] +fn observer_snapshot_roundtrip() { + let snapshot = ObserverSnapshot { + tick: 42, + entities: vec![VisibleEntity { + entity_id: 1, + x: 10.0, + y: 20.0, + z: 0, + kind: EntityKind::Npc, + }], + }; + + let bytes = rmp_serde::to_vec(&snapshot).expect("serialize"); + let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); + + assert_eq!(decoded.tick, 42); + assert_eq!(decoded.entities.len(), 1); + assert_eq!(decoded.entities[0].entity_id, 1); +} + +#[test] +fn player_input_roundtrip() { + let input = PlayerInput { + tick: 100, + action: PlayerAction::MoveNorth, + }; + + let bytes = rmp_serde::to_vec(&input).expect("serialize"); + let decoded: PlayerInput = rmp_serde::from_slice(&bytes).expect("deserialize"); + + assert_eq!(decoded.tick, 100); +} + +#[test] +fn empty_snapshot_roundtrip() { + let snapshot = ObserverSnapshot { + tick: 0, + entities: vec![], + }; + + let bytes = rmp_serde::to_vec(&snapshot).expect("serialize"); + let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); + + assert_eq!(decoded.tick, 0); + assert!(decoded.entities.is_empty()); +} diff --git a/server/tests/smoke.rs b/server/tests/smoke.rs new file mode 100644 index 000000000..d3504457e --- /dev/null +++ b/server/tests/smoke.rs @@ -0,0 +1,22 @@ +//! Smoke test: the simulation world boots and can run a single tick. + +use bevy_app::prelude::*; +use settled_reach_server::simulation::SimulationPlugin; +use settled_reach_server::simulation::time::SimulationTime; + +#[test] +fn world_boots_and_ticks() { + let mut app = App::new(); + app.add_plugins(SimulationPlugin); + + // Verify initial state + let time = app.world().resource::(); + assert_eq!(time.tick, 0); + + // Run one update cycle + app.update(); + + // Verify tick advanced + let time = app.world().resource::(); + assert_eq!(time.tick, 1); +} From 651995590d685934987785ac4098ba04a8b61eab Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 16:54:47 +0100 Subject: [PATCH 3/8] chore(db): mark server boilerplate epic and stories as done Tickets 276 (epic) and 278-286 (stories/tasks) updated to done status after make ci-server passed green. Co-Authored-By: Claude Opus 4.6 --- db/commonwealth.db | Bin 208896 -> 208896 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/db/commonwealth.db b/db/commonwealth.db index 3fe36633b57d1a7c39c8648f8056df16d0c112a3..569c76f2a58013d5be306c7af28849ea890eb835 100644 GIT binary patch delta 672 zcmZp8z|#OkTNoKXNV9P;a4|YD^3CAQ;4$Rp;Z$Z1W8-Jp&kPjY*r>+H)9lhD&c`6m z2U5N`7AI^wa;D6cmlkjSP&;bPbGj4Gk3xO|49gtqjbX4VuJ2IucuI zMYoH6X3R&@F@4n+Mm`RxLWb@8zc6|-3NtV;umf?EbCU=kgD6vAv##iLk#CIuAQs7f zXEb7m>ewFkopIw2M)vI-ADFf=aVs^+fi$=_bBazs`HATnAIN1HnduoRr71b7mD4YN zW)fDw;zEYz)FxSw6a8B&MYn(W%(MjR#F<~2^h98;YtC#VkN delta 651 zcmZp8z|#OkTNoKXNHZ`na4|YE@Xg@O;4$Rp;Z$Z1W8+}i&kPjY*r>|L)9l(L&c`6m z2UbQEFmJ<@8hknG_U_&5aC<%ybQmbPWv^3=OSJ&8&>gn+==9Kw1)8 zYQ?vUeP+x@)-rwN7e+pIh)SdF`@b-HGV(NeHHq*sh%yB>>ncx|{>J!k`mwK!!qZj1 zGipIp8dz)(`_8!W2P4CFjt@-Rn7Eajod&pZvu1Z2EddX3pu`KQpNc1C2Jf zGB9sWYmx;y!oRgreEWybOiN&nnDLcK530zxxu8h~tf-AseEY4hOlx3@y1p|RfE1Zo znOZhSG)aRL`L@Q3Z$I~)X&OvX#!n_a5r`tw7R4qhkfQjuaBsCh=sDNT|frS7e@;?sdH6WRXwH&Y)>B`{bO70ob0)x@kNut|a+sI8H;U0i(o zj=xNNOo*7!gM^cb$rfft0d`=VPUEO&n9d=<{9m$$Gn+GtGl0{T(~47{Q-xEKlaG^` z;|Isv?Fxd-XW5gvn8lgEG0G^1V9FwxG6<$Lf+>YyN+OsN2&OoKDK Date: Wed, 11 Feb 2026 16:55:04 +0100 Subject: [PATCH 4/8] chore(meta): update changelog Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d85ab34..f308897f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- Rust/bevy_ecs simulation server boilerplate (epic 276) — Cargo project, module structure, core ECS types, plugin scaffolding, deterministic simulation resources, test infrastructure +- SimulationTime resource with D-031 time system (10 ticks/game-minute, 4 day phases) +- SimRng deterministic RNG resource (ChaCha20, seeded for replay) +- InputQueue resource for timestamped semantic player actions +- ObserverSnapshot and PlayerInput IPC types with MessagePack serialization (D-020) +- SimBridge trait abstracting client-server transport +- CauseChain production component for information provenance tracking (D-030) +- SimulationTier types with LRU eviction support (D-026: Active/Background/StateSaved/Ungenerated) +- NPC 10-axis model components (D-024: 7 essential + 3 supporting + CombatCapability) +- Server test infrastructure: 11 inline unit tests + 4 integration tests (smoke + serialization round-trips) +- make ci-server pipeline verified green (clippy, fmt, build, test) - Round 18 v0.1 gap analysis workshop — 7 agents, 2 rounds, 4 tracks (concept proof, wow factor, missing systems, testability) - D-030: Testability architecture — 8 sub-decisions for ticket #214 (gdUnit4, hybrid Rust testing, CauseChain component, three-layer IPC testing) - D-031: Time system — 10 ticks = 1 game-minute, 4 day phases (Morning/Afternoon/Evening/Night), diegetic clock display From 9de1eb4aa334c0edb4056ad9beaa75c932572e70 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 17:25:00 +0100 Subject: [PATCH 5/8] docs(meta): add tea PR workflow to CLAUDE.md Document Gitea tea CLI usage for pull requests with all required flags for non-interactive execution. Adds Gitea to local services. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index dce2e577b..41a4d6f7d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,22 @@ db/connectors/qdrant-count Use conventional commits with project-specific scopes: `agents`, `skills`, `docs`, `briefings`, `discussions`, `schema`, `db`, `config`, `engine`, `simulation`, `client`, `ui`, `audio`, `assets`, `meta` +### Pull requests +**Use `tea` (Gitea CLI), not `gh` (GitHub CLI).** The remote is Gitea at `git.schweitz.internal`. + +Always provide all required flags to ensure non-interactive execution: +```bash +tea pr create \ + --repo jpmschweitzer/settled-reach \ + --login schweitz \ + --title "feat(scope): short description" \ + --description "PR body here" \ + --base main \ + --head branch-name +``` + ### Local services +- Gitea: `http://git.schweitz.internal` (login: `schweitz`) - Qdrant: `http://tower-of-joy:6333/` - Ollama: `http://tower-of-joy:11434/` (nomic-embed-text) - Collection: `commonwealth` (768 dimensions, cosine distance) From 6bda4a76209d3c7e9deae736230ebd542b20de76 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 17:53:01 +0100 Subject: [PATCH 6/8] fix(simulation): address PR #2 review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Cargo.toml edition 2024 → 2021 (Hoshe #1) - Switch test runner to cargo-nextest in Makefile (Hoshe #2, D-030) - Add debug_assert tick ordering enforcement in InputQueue::push (Hoshe #4) - Add DayPhase Serialize/Deserialize derives (Tyre #6) - Add day phase boundary comments clarifying half-open ranges (Hoshe #3) - Add phase duration adjustability comment (Tyre #5) - Document ObserverSnapshot planned fields as TODO (Tyre #1) - Document entity_id as wire-format ID, not ECS Entity (Tyre #2) - Change Relationship.target_name to target_id: u64 (Tyre #3) - Clarify single app.update() is intentional boilerplate (Hoshe #8, Tyre #7) Co-Authored-By: Claude Opus 4.6 --- Makefile | 2 +- server/Cargo.toml | 2 +- server/src/bridge/types.rs | 7 ++++++- server/src/main.rs | 2 +- server/src/npc/mod.rs | 3 ++- server/src/simulation/input.rs | 24 +++++++++++++++++++++++- server/src/simulation/time.rs | 26 +++++++++++++++++++++++++- 7 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index dceb5bfab..4a2878f20 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,7 @@ client: test: test-server test-client test-server: - cd server && cargo test + cd server && cargo nextest run test-client: @echo "Client tests run via gdUnit4 inside Godot." diff --git a/server/Cargo.toml b/server/Cargo.toml index 9b21c6d68..6afe63cbd 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "settled-reach-server" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] bevy_ecs = "0.18" diff --git a/server/src/bridge/types.rs b/server/src/bridge/types.rs index c34466c8f..f7d3811c6 100644 --- a/server/src/bridge/types.rs +++ b/server/src/bridge/types.rs @@ -5,7 +5,10 @@ use serde::{Deserialize, Serialize}; /// The ONLY data structure crossing the client-server boundary (D-020) -/// Contains all information visible to the observer at a given tick +/// Contains all information visible to the observer at a given tick. +/// +/// TODO: Planned fields — fog/visibility data, ambient sound events, +/// internal monologue triggers, HUD state (D-020 expansion). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ObserverSnapshot { /// Simulation tick when this snapshot was produced @@ -17,6 +20,8 @@ pub struct ObserverSnapshot { /// A visible entity in the simulation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VisibleEntity { + /// Wire-format entity identifier — NOT a bevy ECS Entity. + /// Stable across serialization for client-server boundary (D-020). pub entity_id: u64, pub x: f32, pub y: f32, diff --git a/server/src/main.rs b/server/src/main.rs index b38940cf4..6ddee05cf 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -24,7 +24,7 @@ fn main() { app.add_plugins(SimulationPlugin); app.add_plugins(BridgePlugin); - // Run one update cycle + // Single tick for smoke verification; real game loop in phase 2 app.update(); tracing::info!("Simulation server update complete"); diff --git a/server/src/npc/mod.rs b/server/src/npc/mod.rs index a365c29cc..5498ffae0 100644 --- a/server/src/npc/mod.rs +++ b/server/src/npc/mod.rs @@ -26,7 +26,8 @@ pub struct Relationships { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Relationship { - pub target_name: String, + /// Wire-format entity ID of the relationship target (scales to 10K+ NPCs) + pub target_id: u64, pub kind: String, pub trust_level: f32, } diff --git a/server/src/simulation/input.rs b/server/src/simulation/input.rs index 773992fbc..ea94108ab 100644 --- a/server/src/simulation/input.rs +++ b/server/src/simulation/input.rs @@ -13,8 +13,16 @@ pub struct InputQueue { } impl InputQueue { - /// Add a new input to the queue + /// Add a new input to the queue. + /// Inputs must be pushed in tick order for deterministic processing. + /// Panics in debug builds if tick ordering is violated. pub fn push(&mut self, input: PlayerInput) { + debug_assert!( + self.queue.back().is_none_or(|last| last.tick <= input.tick), + "InputQueue: tick ordering violated (last={}, new={})", + self.queue.back().map_or(0, |last| last.tick), + input.tick, + ); self.queue.push_back(input); } @@ -74,4 +82,18 @@ mod tests { let inputs = queue.drain_for_tick(10); assert!(inputs.is_empty()); } + + #[test] + #[should_panic(expected = "tick ordering violated")] + fn push_rejects_out_of_order_in_debug() { + let mut queue = InputQueue::default(); + queue.push(PlayerInput { + tick: 5, + action: PlayerAction::MoveNorth, + }); + queue.push(PlayerInput { + tick: 2, + action: PlayerAction::MoveSouth, + }); + } } diff --git a/server/src/simulation/time.rs b/server/src/simulation/time.rs index 29b37d0f7..753eefc1c 100644 --- a/server/src/simulation/time.rs +++ b/server/src/simulation/time.rs @@ -3,12 +3,14 @@ // Injectable time resource for deterministic replay (D-030) use bevy_ecs::prelude::*; +use serde::{Deserialize, Serialize}; pub const TICKS_PER_GAME_MINUTE: u64 = 10; pub const MINUTES_PER_PHASE: u64 = 360; pub const MINUTES_PER_DAY: u64 = 1440; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +// 4 equal 6-hour phases (adjustable — D-031 allows rebalancing phase durations) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum DayPhase { Morning, Afternoon, @@ -33,6 +35,7 @@ impl SimulationTime { } pub fn day_phase(&self) -> DayPhase { let tod = self.time_of_day_minutes(); + // Morning: [0, 360), Afternoon: [360, 720), Evening: [720, 1080), Night: [1080, 1440) match tod { 0..360 => DayPhase::Morning, 360..720 => DayPhase::Afternoon, @@ -114,4 +117,25 @@ mod tests { schedule.run(&mut world); assert_eq!(world.resource::().tick, 1); } + + #[test] + fn day_wraparound_at_midnight() { + // 1440 minutes = 1 full day, should wrap back to Morning + let time = SimulationTime { + tick: MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE, + paused: false, + }; + assert_eq!(time.day_phase(), DayPhase::Morning); + assert_eq!(time.time_of_day_minutes(), 0); + assert_eq!(time.day(), 1); + } + + #[test] + fn day_calculation() { + let time = SimulationTime { + tick: 3 * MINUTES_PER_DAY * TICKS_PER_GAME_MINUTE + 100, + paused: false, + }; + assert_eq!(time.day(), 3); + } } From 112910df9b9e1b8a358d2f429dd53fdfa996d31e Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 17:53:16 +0100 Subject: [PATCH 7/8] test(simulation): expand test coverage per PR #2 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add roundtrip tests for all PlayerAction variants (Hoshe #6) - Add roundtrip tests for all EntityKind variants (Hoshe #6) - Add day wraparound at midnight edge case test (Hoshe #7) - Add day() calculation test (Hoshe #7) - Add out-of-order input rejection test (Hoshe #7) - Total: 15 → 20 tests Co-Authored-By: Claude Opus 4.6 --- server/tests/serialization.rs | 55 +++++++++++++++++++++++++++++++++++ server/tests/smoke.rs | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/server/tests/serialization.rs b/server/tests/serialization.rs index 6551b76ac..568aa6618 100644 --- a/server/tests/serialization.rs +++ b/server/tests/serialization.rs @@ -49,3 +49,58 @@ fn empty_snapshot_roundtrip() { assert_eq!(decoded.tick, 0); assert!(decoded.entities.is_empty()); } + +/// All PlayerAction variants must survive MessagePack round-trip (D-030 Layer 1) +#[test] +fn all_player_action_variants_roundtrip() { + let actions = vec![ + PlayerAction::MoveNorth, + PlayerAction::MoveSouth, + PlayerAction::MoveEast, + PlayerAction::MoveWest, + PlayerAction::Interact, + PlayerAction::UsePerceptionMode("thermal".to_string()), + PlayerAction::Pause, + PlayerAction::Unpause, + ]; + + for action in actions { + let input = PlayerInput { + tick: 1, + action: action.clone(), + }; + let bytes = rmp_serde::to_vec(&input).expect("serialize"); + let decoded: PlayerInput = rmp_serde::from_slice(&bytes).expect("deserialize"); + assert_eq!(decoded.tick, 1); + // Verify the variant survived by re-serializing and comparing bytes + let re_bytes = rmp_serde::to_vec(&decoded).expect("re-serialize"); + assert_eq!(bytes, re_bytes, "round-trip mismatch for action variant"); + } +} + +/// All EntityKind variants must survive MessagePack round-trip (D-030 Layer 1) +#[test] +fn all_entity_kind_variants_roundtrip() { + let kinds = vec![EntityKind::Npc, EntityKind::Object, EntityKind::Terrain]; + + for (i, kind) in kinds.into_iter().enumerate() { + let entity = VisibleEntity { + entity_id: i as u64, + x: 0.0, + y: 0.0, + z: 0, + kind, + }; + let snapshot = ObserverSnapshot { + tick: 0, + entities: vec![entity], + }; + let bytes = rmp_serde::to_vec(&snapshot).expect("serialize"); + let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); + let re_bytes = rmp_serde::to_vec(&decoded).expect("re-serialize"); + assert_eq!( + bytes, re_bytes, + "round-trip mismatch for EntityKind variant" + ); + } +} diff --git a/server/tests/smoke.rs b/server/tests/smoke.rs index d3504457e..9420d4216 100644 --- a/server/tests/smoke.rs +++ b/server/tests/smoke.rs @@ -1,8 +1,8 @@ //! Smoke test: the simulation world boots and can run a single tick. use bevy_app::prelude::*; -use settled_reach_server::simulation::SimulationPlugin; use settled_reach_server::simulation::time::SimulationTime; +use settled_reach_server::simulation::SimulationPlugin; #[test] fn world_boots_and_ticks() { From 4afda9a6b8ae20687cf06ff410f9b13b59bc37bc Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 17:53:44 +0100 Subject: [PATCH 8/8] chore(meta): update changelog Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f308897f5..0486b8dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- InputQueue tick ordering enforcement via debug_assert (determinism guard) +- Serialization roundtrip tests for all PlayerAction and EntityKind variants (D-030 Layer 1) +- Edge case tests: day wraparound at midnight, day() calculation, out-of-order input rejection +- Test runner switched to cargo-nextest (D-030 requirement) - Rust/bevy_ecs simulation server boilerplate (epic 276) — Cargo project, module structure, core ECS types, plugin scaffolding, deterministic simulation resources, test infrastructure - SimulationTime resource with D-031 time system (10 ticks/game-minute, 4 day phases) - SimRng deterministic RNG resource (ChaCha20, seeded for replay) @@ -60,6 +64,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). - Rejected alternatives R-004 through R-010 documented (pure Bevy, pure Godot, GDExtension, C++ GDExtension, Fyrox, custom framework, protobuf) ### Fixed +- Cargo.toml edition 2024 → 2021 for broader toolchain compatibility +- Relationship.target_name: String → target_id: u64 for entity scalability (Tyre review) +- DayPhase enum now derives Serialize/Deserialize (consistent with other enums) - Replaced all absolute paths (macOS and Linux) with project-relative paths across round-16 docs - Removed duplicate ROUND-16-STATUS.md from project root (content already in round-16-session-notes.md) - Added relative-path convention to Qatux agent persona for cross-system consistency