From 44511cde752028b6c444568923d6142e2345a15b Mon Sep 17 00:00:00 2001 From: Serkyo Date: Tue, 7 Jul 2026 18:53:24 +0200 Subject: [PATCH] feat(server): stream chunks around a player entity via ECS --- Cargo.lock | 500 +++++++++++++++++++++++++++++- crates/server/Cargo.toml | 1 + crates/server/src/main.rs | 53 +++- crates/server/src/player.rs | 18 ++ crates/server/src/world_server.rs | 136 ++++++-- 5 files changed, 669 insertions(+), 39 deletions(-) create mode 100644 crates/server/src/player.rs diff --git a/Cargo.lock b/Cargo.lock index f093e97..e0b0a95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -109,11 +109,48 @@ dependencies = [ "raw-window-metal", ] +[[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-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +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" @@ -121,6 +158,168 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bevy_ecs" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4654b9b3535fa7813d2878a50e1b5ad80a361dc1c913b96ded57667f65d70a01" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.11.1", + "bumpalo", + "concurrent-queue", + "derive_more", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror 2.0.18", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macro_logic" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0b98a74141ce8dce4654c60020ebbaefab32646f42af6bbae55f282ae65ff7" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc958a194d226d618404e0fea99a3c8b4146207a00a3ba07fa712bf71607240" +dependencies = [ + "bevy_ecs_macro_logic", + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746a19912c6dc1bbe79188778573e8a253d5832c696b2fcb95578c17b29ff7ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "toml_edit", +] + +[[package]] +name = "bevy_platform" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3120670bb2308980f723477c9d82476fcda31f08be9b256c3f0acdee82a65094" +dependencies = [ + "critical-section", + "foldhash", + "futures-channel", + "futures-lite", + "hashbrown 0.16.1", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", + "windows-sys 0.61.2", +] + +[[package]] +name = "bevy_ptr" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b511e89f7078fd21fdea77061a0ca3e737297c7011bb10c073e0aecb17c9fea6" + +[[package]] +name = "bevy_reflect" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92abd59cbba1524c1847e9a50f74d94e6b7836c80a8edfa9c47e59f7fd81021d" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs 2.0.2", + "erased-serde", + "foldhash", + "glam 0.32.1", + "indexmap", + "serde", + "smallvec", + "smol_str", + "thiserror 2.0.18", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fa4e6b97fd2d582dd5ed96762a70d04505a477c833cf4f69d016dcd03d2d04" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_tasks" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa34e6b9b804851ec08a41c0346c859d82e2fa98c906d74b965ee61bbbb688e4" +dependencies = [ + "async-executor", + "async-task", + "atomic-waker", + "bevy_platform", + "crossbeam-queue", + "derive_more", + "futures-lite", + "heapless", + "web-task", +] + +[[package]] +name = "bevy_utils" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aceea6c7ffdd568f866d5ca4dfe50c33440c54f7bc230c13a9ba7ab0c91aed1" +dependencies = [ + "bevy_platform", + "disqualified", + "indexmap", + "thread_local", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -132,6 +331,9 @@ name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] [[package]] name = "block" @@ -174,6 +376,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -293,6 +501,16 @@ 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]] @@ -335,6 +553,12 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -354,6 +578,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -366,12 +599,41 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[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 = "dispatch" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + [[package]] name = "dlib" version = "0.5.3" @@ -387,6 +649,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dpi" version = "0.1.2" @@ -405,6 +673,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[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 = "errno" version = "0.3.14" @@ -427,6 +706,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[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 = "foreign-types" version = "0.5.0" @@ -454,12 +745,40 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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.32" @@ -496,10 +815,21 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "glam" version = "0.27.0" @@ -515,6 +845,15 @@ version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" +[[package]] +name = "glam" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" +dependencies = [ + "serde_core", +] + [[package]] name = "gpu-allocator" version = "0.28.0" @@ -531,12 +870,43 @@ dependencies = [ "windows", ] +[[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 = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "portable-atomic", + "stable_deref_trait", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -550,7 +920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.0", ] [[package]] @@ -623,7 +993,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] @@ -765,6 +1135,12 @@ dependencies = [ "rand_xorshift", ] +[[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" @@ -1072,6 +1448,12 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1130,6 +1512,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "presser" version = "0.3.1" @@ -1178,6 +1575,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.6" @@ -1415,6 +1818,7 @@ dependencies = [ name = "server" version = "0.1.0" dependencies = [ + "bevy_ecs", "glam 0.27.0", "rayon", "serde_json", @@ -1471,6 +1875,15 @@ 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" @@ -1511,6 +1924,21 @@ 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 = "strict-num" version = "0.1.1" @@ -1699,6 +2127,12 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[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.24" @@ -1711,12 +2145,41 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[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.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "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" @@ -1804,7 +2267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", - "downcast-rs", + "downcast-rs 1.2.1", "rustix 1.1.4", "scoped-tls", "smallvec", @@ -1916,6 +2379,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-task" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538" +dependencies = [ + "async-task", + "cfg-if", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -1926,6 +2401,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "serde", + "web-sys", +] + [[package]] name = "winapi-util" version = "0.1.11" diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 0f8436b..ae4aad7 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -9,6 +9,7 @@ authors.workspace = true workspace = true [dependencies] +bevy_ecs = "0.19" glam = "0.27" rayon = "1.12.0" serde_json = "1.0.149" diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 69742b5..aa7c210 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -5,16 +5,43 @@ //! The server handles the authoritative game simulation, including world //! management, physics, and combat. +/// Entity components describing players and other world-streaming anchors. +pub mod player; /// Authoritative chunk storage and generation logic for the server. pub mod world_server; +use std::collections::HashSet; use std::fs; +use bevy_ecs::prelude::{Query, ResMut, Schedule, With, World}; use glam::Vec3; use shared::generator::{VoxelGenerator, WorldGenConfig}; use shared::world::{ChunkPos, EntityPos}; use tracing::{debug, info}; +use player::{Player, Position, ViewDistance}; +use world_server::{ServerWorld, cylinder_chunks}; + +/// Streaming system: loads and unloads chunks so that the resident set matches the union of the cylinders around every player anchor. +fn stream_chunks( + anchors: Query<(&Position, &ViewDistance), With>, + mut world: ResMut, +) { + // Desired set is the union of every anchor's cylinder; a chunk survives as long as it lies within any one player's view. + let mut desired = HashSet::new(); + for (position, view) in &anchors { + cylinder_chunks(position.0.chunk, view.0, &mut desired); + } + + let stats = world.reconcile(&desired); + info!( + loaded = stats.loaded, + unloaded = stats.unloaded, + resident = stats.resident, + "streaming reconcile" + ); +} + fn main() { tracing_subscriber::fmt() .with_env_filter( @@ -44,10 +71,28 @@ fn main() { let generator = VoxelGenerator::new(worldgen_config, seed); - let mut world = world_server::ServerWorld::new(generator); + let mut world = World::new(); + world.insert_resource(ServerWorld::new(generator)); - // Hardcoded spawn stand-in until a real player entity exists; the chunk anchor drives streaming. - let player = EntityPos::new(ChunkPos::new(0, 0, 0), Vec3::ZERO); + // Spawn a single dummy player anchor at the world origin. + world.spawn(( + Player, + Position(EntityPos::new(ChunkPos::new(0, 0, 0), Vec3::ZERO)), + ViewDistance(4), + )); - world.load_around(player, 4); + // A schedule is one tick's worth of systems; running it advances the world. + let mut schedule = Schedule::default(); + schedule.add_systems(stream_chunks); + + // Drive several ticks, marching the dummy one chunk along +X between each. The manual movement stands in for network-driven player input and exists only to exercise load/unload as the anchor moves. + for step in 0..5 { + info!(step, "tick"); + schedule.run(&mut world); + + let mut movers = world.query_filtered::<&mut Position, With>(); + for mut position in movers.iter_mut(&mut world) { + position.0.chunk.x += 1; + } + } } diff --git a/crates/server/src/player.rs b/crates/server/src/player.rs new file mode 100644 index 0000000..fae25a4 --- /dev/null +++ b/crates/server/src/player.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +//! Entity components describing players and other world-streaming anchors. + +use bevy_ecs::prelude::Component; +use shared::world::EntityPos; + +/// Marker component identifying an entity as a player. +#[derive(Component)] +pub struct Player; + +/// Authoritative world-space position of an entity. +#[derive(Component)] +pub struct Position(pub EntityPos); + +/// Chunk-streaming radius, in chunks, of the cylinder loaded around an entity. +#[derive(Component)] +pub struct ViewDistance(pub i32); diff --git a/crates/server/src/world_server.rs b/crates/server/src/world_server.rs index f2fb406..6b6e1f5 100644 --- a/crates/server/src/world_server.rs +++ b/crates/server/src/world_server.rs @@ -2,16 +2,31 @@ //! Authoritative chunk storage and generation logic for the server. +use bevy_ecs::prelude::Resource; use rayon::prelude::*; use shared::{ generator::VoxelGenerator, - world::{Chunk, ChunkPos, EntityPos}, + world::{Chunk, ChunkPos}, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; + +/// Outcome of a single streaming reconcile pass, for logging and tests. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct StreamStats { + /// Number of chunks generated and inserted this pass. + pub loaded: usize, + /// Number of chunks evicted this pass. + pub unloaded: usize, + /// Number of chunks resident after the pass. + pub resident: usize, +} /// The server's authoritative representation of the world. +#[derive(Resource)] pub struct ServerWorld { + /// Deterministic voxel source used to generate missing chunks on demand. generator: VoxelGenerator, + /// Currently resident chunks, keyed by chunk-space position. chunks: HashMap, } @@ -32,42 +47,103 @@ impl ServerWorld { .or_insert_with(|| self.generator.generate_chunk(pos)) } - /// Generates and caches all chunks in a cylindrical region around the given position. - pub fn load_around(&mut self, center: EntityPos, radius: i32) { - // The chunk anchor is the exact streaming key; no lossy world-to-chunk conversion is needed. - let center = center.chunk; + /// Number of chunks currently resident in memory. + #[must_use] + pub fn loaded_count(&self) -> usize { + self.chunks.len() + } - // Phase 1: collect the positions inside the cylinder that are not yet resident - let mut pending = Vec::new(); - for x in center.x - radius..=center.x + radius { - for z in center.z - radius..=center.z + radius { - for y in center.y - radius / 2..=center.y + radius / 2 { - let dx = x - center.x; - let dz = z - center.z; - - if (dx * dx + dz * dz) <= radius * radius { - let pos = ChunkPos::new(x, y, z); - if !self.chunks.contains_key(&pos) { - pending.push(pos); - } - } - } - } + /// Reconciles resident chunks against a desired set: generates the chunks present in `desired` but not resident, and evicts resident chunks absent from `desired`. + pub fn reconcile(&mut self, desired: &HashSet) -> StreamStats { + // Evict resident chunks that no anchor wants any more. + let stale: Vec = self + .chunks + .keys() + .filter(|pos| !desired.contains(pos)) + .copied() + .collect(); + for pos in &stale { + self.chunks.remove(pos); } - // Phase 2: generate the missing chunks in parallel - let generator = &self.generator; - let generated: Vec<(ChunkPos, Chunk)> = pending - .into_par_iter() - .map(|pos| (pos, generator.generate_chunk(pos))) + // Collect the desired positions that are not yet resident. + let pending: Vec = desired + .iter() + .filter(|pos| !self.chunks.contains_key(pos)) + .copied() .collect(); - // Phase 3: move the finished chunks into the map - let count = generated.len(); + // Generate the missing chunks in parallel + let generator = &self.generator; + let generated: Vec<(ChunkPos, Chunk)> = pending + .par_iter() + .map(|&pos| (pos, generator.generate_chunk(pos))) + .collect(); + + let loaded = generated.len(); for (pos, chunk) in generated { self.chunks.insert(pos, chunk); } - tracing::info!(chunks = count, center = ?center, radius, "generated chunk cylinder"); + StreamStats { + loaded, + unloaded: stale.len(), + resident: self.chunks.len(), + } + } +} + +/// Inserts every chunk position inside the streaming cylinder around `center` into `out`. +pub fn cylinder_chunks( + center: ChunkPos, + radius: i32, + out: &mut HashSet, +) { + for x in center.x - radius..=center.x + radius { + for z in center.z - radius..=center.z + radius { + let dx = x - center.x; + let dz = z - center.z; + + // Keep only the columns whose XZ distance falls within the disc. + if dx * dx + dz * dz <= radius * radius { + for y in center.y - radius / 2..=center.y + radius / 2 { + out.insert(ChunkPos::new(x, y, z)); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{ChunkPos, cylinder_chunks}; + use std::collections::HashSet; + + #[test] + fn cylinder_is_symmetric_and_bounded() { + let mut set = HashSet::new(); + cylinder_chunks(ChunkPos::new(0, 0, 0), 2, &mut set); + + // Center column is always included. + assert!(set.contains(&ChunkPos::new(0, 0, 0))); + // A corner outside the disc (dx=2, dz=2 -> 8 > 4) is excluded. + assert!(!set.contains(&ChunkPos::new(2, 0, 2))); + // An axis cell at exactly the radius is included (dx=2, dz=0 -> 4 == 4). + assert!(set.contains(&ChunkPos::new(2, 0, 0))); + // Vertical extent is radius/2 = 1, so y=2 is out of range. + assert!(!set.contains(&ChunkPos::new(0, 2, 0))); + assert!(set.contains(&ChunkPos::new(0, 1, 0))); + } + + #[test] + fn cylinder_translates_with_center() { + let mut origin = HashSet::new(); + cylinder_chunks(ChunkPos::new(0, 0, 0), 3, &mut origin); + + let mut shifted = HashSet::new(); + cylinder_chunks(ChunkPos::new(10, 0, -5), 3, &mut shifted); + + // Shape is translation-invariant: same count regardless of center. + assert_eq!(origin.len(), shifted.len()); } }