diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 19524a591..2c5b0b5fe 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -8,27 +8,27 @@ jobs: - job: 'CI' strategy: matrix: - #linux-stable: - # rustup_toolchain: stable - # image_name: 'ubuntu-16.04' + linux-stable: + rustup_toolchain: stable + image_name: 'ubuntu-16.04' linux-beta: rustup_toolchain: beta image_name: 'ubuntu-16.04' linux-nightly: rustup_toolchain: nightly-2019-09-28 image_name: 'ubuntu-16.04' - #windows-stable: - # rustup_toolchain: stable - # image_name: 'windows-latest' - windows-beta: - rustup_toolchain: beta-gnu + windows-stable: + rustup_toolchain: stable image_name: 'windows-latest' - windows-nightly: - rustup_toolchain: nightly-2019-09-28 - image_name: 'ubuntu-16.04' - #apple-stable: - # rustup_toolchain: stable - # image_name: 'macOS-10.13' + #windows-beta: + # rustup_toolchain: beta-gnu + # image_name: 'windows-latest' + #windows-nightly: + # rustup_toolchain: nightly-2019-09-28 + # image_name: 'ubuntu-16.04' + apple-stable: + rustup_toolchain: stable + image_name: 'macOS-10.13' #apple-beta: # rustup_toolchain: beta # image_name: 'macos-latest' diff --git a/.gitignore b/.gitignore index c132a9511..85d7f7da6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ **/*.rs.bk **/.idea feather.toml -massif.out* \ No newline at end of file +massif.out* +.cargo diff --git a/Cargo.lock b/Cargo.lock index fe183b46b..460225941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,11 @@ dependencies = [ "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "as-slice" version = "0.1.0" @@ -99,11 +104,6 @@ dependencies = [ "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "atom" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "atty" version = "0.2.13" @@ -146,6 +146,19 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bit-set" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bit-vec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bit-vec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.2.0" @@ -180,6 +193,11 @@ name = "bumpalo" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bumpalo" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "byteorder" version = "1.3.2" @@ -242,6 +260,15 @@ dependencies = [ "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "chashmap" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "chrono" version = "0.4.9" @@ -379,6 +406,14 @@ dependencies = [ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-channel" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "crossbeam-deque" version = "0.7.1" @@ -409,6 +444,14 @@ dependencies = [ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-queue" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "crossbeam-utils" version = "0.6.6" @@ -418,6 +461,16 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "crossbeam-utils" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "csv" version = "1.1.1" @@ -438,6 +491,15 @@ dependencies = [ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ctor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ctrlc" version = "3.1.3" @@ -585,6 +647,8 @@ dependencies = [ "nalgebra-glm 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", "strum 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -632,15 +696,16 @@ dependencies = [ name = "feather-server" version = "0.5.0" dependencies = [ + "ahash 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "bitvec 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "chashmap 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "derive-new 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)", "derive_deref 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "feather-blocks 0.5.0", @@ -653,10 +718,13 @@ dependencies = [ "heapless 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "hematite-nbt 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "humantime-serde 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "inventory 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "legion 0.2.1 (git+https://github.com/TomGillen/legion?rev=0f67adc237af35799df173f31a2c238b3d8010a2)", + "lock_api 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mojang-api 0.3.0 (git+https://github.com/caelunshun/mojang-api-rs?rev=6525e910ad53953fa16028f0fce74b1a19855733)", - "multimap 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mojang-api 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "multimap 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "nalgebra 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", "nalgebra-glm 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "ncollide3d 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -672,17 +740,16 @@ dependencies = [ "rsa-der 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", - "shrev 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "simdeez 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "simdnoise 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "simple_logger 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", - "specs 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", "strum 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "thread_local 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-executor 0.2.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tonks 0.1.0 (git+https://github.com/feather-rs/tonks?rev=ccdecae02e11fc6694bd252d48cce5794499736f)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -778,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -844,6 +911,14 @@ dependencies = [ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "generic-array" version = "0.12.3" @@ -870,6 +945,16 @@ dependencies = [ "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ghost" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "h2" version = "0.2.0-alpha.3" @@ -949,12 +1034,11 @@ dependencies = [ ] [[package]] -name = "hibitset" -version = "0.6.2" +name = "hermit-abi" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "atom 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1056,6 +1140,26 @@ dependencies = [ "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "inventory" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "ghost 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "inventory-impl 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "inventory-impl" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "iovec" version = "0.1.2" @@ -1103,6 +1207,25 @@ dependencies = [ "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "legion" +version = "0.2.1" +source = "git+https://github.com/TomGillen/legion?rev=0f67adc237af35799df173f31a2c238b3d8010a2#0f67adc237af35799df173f31a2c238b3d8010a2" +dependencies = [ + "bit-set 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "derivative 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "downcast-rs 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "paste 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "libc" version = "0.2.62" @@ -1229,13 +1352,13 @@ dependencies = [ [[package]] name = "mojang-api" -version = "0.3.0" -source = "git+https://github.com/caelunshun/mojang-api-rs?rev=6525e910ad53953fa16028f0fce74b1a19855733#6525e910ad53953fa16028f0fce74b1a19855733" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "num-bigint 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.10.0-alpha.0 (git+https://github.com/seanmonstar/reqwest?rev=5b55aee1a9ddf785f82d9086c8befc50db268cb8)", + "reqwest 0.10.0-alpha.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1255,6 +1378,14 @@ dependencies = [ "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "multimap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "nalgebra" version = "0.18.1" @@ -1444,9 +1575,10 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1490,6 +1622,23 @@ name = "ordermap" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "owning_ref" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parking_lot" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot_core 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "parking_lot" version = "0.9.0" @@ -1500,6 +1649,17 @@ dependencies = [ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "parking_lot_core" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "parking_lot_core" version = "0.6.2" @@ -1850,7 +2010,7 @@ dependencies = [ "crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1900,8 +2060,8 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.10.0-alpha.0" -source = "git+https://github.com/seanmonstar/reqwest?rev=5b55aee1a9ddf785f82d9086c8befc50db268cb8#5b55aee1a9ddf785f82d9086c8befc50db268cb8" +version = "0.10.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2078,23 +2238,6 @@ name = "sha1" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "shred" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "hashbrown 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "mopa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "shrev" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "simdeez" version = "0.6.4" @@ -2151,22 +2294,6 @@ name = "sourcefile" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "specs" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "derivative 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "hashbrown 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "hibitset 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "shred 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "shrev 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tuple_utils 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "spin" version = "0.5.2" @@ -2177,6 +2304,11 @@ name = "stable_deref_trait" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "stream-cipher" version = "0.3.2" @@ -2325,7 +2457,7 @@ dependencies = [ "futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", "futures-sink-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-codec 0.2.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-executor 0.2.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-fs 0.2.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2361,7 +2493,7 @@ dependencies = [ "futures-core-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-sync 0.2.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", "tracing 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2416,7 +2548,7 @@ dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-codec 0.2.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2467,6 +2599,39 @@ dependencies = [ "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tonks" +version = "0.1.0" +source = "git+https://github.com/feather-rs/tonks?rev=ccdecae02e11fc6694bd252d48cce5794499736f#ccdecae02e11fc6694bd252d48cce5794499736f" +dependencies = [ + "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bit-set 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "derivative 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "hashbrown 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "inventory 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "legion 0.2.1 (git+https://github.com/TomGillen/legion?rev=0f67adc237af35799df173f31a2c238b3d8010a2)", + "mopa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tonks-macros 0.1.0 (git+https://github.com/feather-rs/tonks?rev=ccdecae02e11fc6694bd252d48cce5794499736f)", +] + +[[package]] +name = "tonks-macros" +version = "0.1.0" +source = "git+https://github.com/feather-rs/tonks?rev=ccdecae02e11fc6694bd252d48cce5794499736f#ccdecae02e11fc6694bd252d48cce5794499736f" +dependencies = [ + "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tower-make" version = "0.3.0-alpha.2a" @@ -2516,11 +2681,6 @@ name = "try-lock" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "tuple_utils" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "typenum" version = "1.11.2" @@ -2832,18 +2992,21 @@ dependencies = [ "checksum approx 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08abcc3b4e9339e33a3d0a5ed15d84a687350c05689d825e0f6655eef9e76a94" "checksum approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" "checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba" +"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" "checksum as-slice 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "293dac66b274fab06f95e7efb05ec439a6b70136081ea522d270bc351ae5bb27" -"checksum atom 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3c86699c3f02778ec07158376991c8f783dd1f2f95c579ffaf0738dc984b2fe2" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" "checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" "checksum backtrace 0.3.38 (registry+https://github.com/rust-lang/crates.io-index)" = "690a62be8920ccf773ee00ef0968649b0e724cda8bd5b12286302b4ae955fdf5" "checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +"checksum bit-set 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e84c238982c4b1e1ee668d136c510c67a13465279c0cb367ea6baf6310620a80" +"checksum bit-vec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f59bbe95d4e52a6398ec21238d31577f2b28a9d86807f06ca59d191d8440d0bb" "checksum bitflags 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8a606a02debe2813760609f57a64a2ffd27d9fdf5b2f133eaca0b248dd92cdd2" "checksum bitvec 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a993f74b4c99c1908d156b8d2e0fb6277736b0ecbd833982fd1241d39b2766a6" "checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" "checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245" "checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708" +"checksum bumpalo 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c2636342f603744010322b36bc245a35d1a9eecadc29fbbb532be90415bdc35d" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" "checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" @@ -2853,6 +3016,7 @@ dependencies = [ "checksum cfb8 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1b310afa67a25a8d5189eacaf5b14418c8dc3d8bcc5755619d89cab87871260d" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum cgmath 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "64a4b57c8f4e3a2e9ac07e0f6abc9c24b6fc9e1b54c3478cfb598f3d0023e51c" +"checksum chashmap 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff41a3c2c1e39921b9003de14bf0439c7b63a9039637c291e1a64925d8ddfa45" "checksum chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" @@ -2866,12 +3030,16 @@ dependencies = [ "checksum criterion-plot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eccdc6ce8bbe352ca89025bee672aa6d24f4eb8c53e3a8b5d1bc58011da072a2" "checksum crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2d818a4990769aac0c7ff1360e233ef3a41adcb009ebb2036bf6915eb0f6b23c" "checksum crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa" +"checksum crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c" "checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71" "checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" +"checksum crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfd6515864a82d2f877b42813d4553292c6659498c9a2aa31bab5a15243c2700" "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" "checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d" "checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c" +"checksum ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc" "checksum ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c7dfd2d8b4c82121dfdff120f818e09fc4380b0b7e17a742081a89b94853e87f" "checksum derivative 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "942ca430eef7a3806595a6737bc388bf51adb888d3fc0dd1b50f1c170167ee3a" "checksum derive-new 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)" = "71f31892cd5c62e414316f2963c5689242c43d8e7bbcaaeca97e5e28c95d91d9" @@ -2902,9 +3070,11 @@ dependencies = [ "checksum futures-select-macro-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "df2ae43560eb10b5e50604c53bead6c9c75eade7081390cd3cce66e1582958f7" "checksum futures-sink-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "86f148ef6b69f75bb610d4f9a2336d4fc88c4b5b67129d1a340dd0fd362efeec" "checksum futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "5ce968633c17e5f97936bd2797b6e38fb56cf16a7422319f7ec2e30d3c470e8d" +"checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" "checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" "checksum generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd" "checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571" +"checksum ghost 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a36606a68532b5640dc86bb1f33c64b45c4682aad4c50f3937b317ea387f3d6" "checksum h2 0.2.0-alpha.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0f107db1419ef8271686187b1a5d47c6431af4a7f4d98b495e7b7fc249bb0a78" "checksum hash32 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "12d790435639c06a7b798af9e1e331ae245b7ef915b92f70a39b4cf8c00686af" "checksum hash32-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebc0efbd154a17cddc3616d83faef479c0076d871a2143c157b310cc7ca799a2" @@ -2912,7 +3082,7 @@ dependencies = [ "checksum heapless 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f339aa7d51777fc0af6aa7cbeb277dfc6e6c029cbdeda48d0fbb92c2337f0e69" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hematite-nbt 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "98b407a33bb1715a4cf0276edfe8df52352c55b2a3703c5079adedf398b92932" -"checksum hibitset 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "47e7292fd9f7fe89fa35c98048f2d0a69b79ed243604234d18f6f8a1aa6f408d" +"checksum hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7" "checksum http 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "372bcb56f939e449117fb0869c2e8fd8753a8223d92a172c6e808cf123a5b6e4" "checksum http-body 0.2.0-alpha.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1f3aef6f3de2bd8585f5b366f3f550b5774500b4764d00cf00f903c95749eec3" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" @@ -2922,12 +3092,15 @@ dependencies = [ "checksum hyper-tls 0.4.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "47cb3975f80cc809efe5dfcc52b73c9b281fde33f2df35a2e5f79f35e384ae7f" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" "checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3" +"checksum inventory 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f4cece20baea71d9f3435e7bbe9adf4765f091c5fe404975f844006964a71299" +"checksum inventory-impl 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2869bf972e998977b1cb87e60df70341d48e48dca0823f534feb91ea44adaf9" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum js-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)" = "2cc9a97d7cec30128fd8b28a7c1f9df1c001ceb9b441e2b755e24130a6b43c79" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum legion 0.2.1 (git+https://github.com/TomGillen/legion?rev=0f67adc237af35799df173f31a2c238b3d8010a2)" = "" "checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" "checksum libm 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" "checksum lock_api 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f8912e782533a93a167888781b836336a6ca5da6175c05944c86cf28c31104dc" @@ -2943,9 +3116,10 @@ dependencies = [ "checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" "checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" -"checksum mojang-api 0.3.0 (git+https://github.com/caelunshun/mojang-api-rs?rev=6525e910ad53953fa16028f0fce74b1a19855733)" = "" +"checksum mojang-api 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f4f301933426b04d4557b52f7715a1e62dd9c43bc64625c645cbb50de70d5692" "checksum mopa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915" "checksum multimap 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de234f818d54830a7103b9be18ad0861d75aeb5e3c89759bc3f9a004cc39cfa3" +"checksum multimap 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97b1a404699e1fa9fa1665e045c1dd23e6663e8e9ff883faa349c90e71aa7ce9" "checksum nalgebra 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aaa9fddbc34c8c35dd2108515587b8ce0cab396f17977b8c738568e4edb521a2" "checksum nalgebra-glm 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7a4cd007520d46d2ca24002ddf538fce40ea2a34ed0ffcd3e2ae0b6ba18811d3" "checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" @@ -2963,13 +3137,16 @@ dependencies = [ "checksum num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2885278d5fe2adc2f75ced642d52d879bffaceb5a2e0b1d4309ffdfb239b454" "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" -"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" +"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72" "checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" "checksum openssl 0.10.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2f372b2b53ce10fb823a337aaa674e3a7d072b957c6264d0f4ff0bd86e657449" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-sys 0.9.50 (registry+https://github.com/rust-lang/crates.io-index)" = "2c42dcccb832556b5926bc9ae61e8775f2a61e725ab07ab3d1e7fcf8ae62c3b6" "checksum ordermap 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a86ed3f5f244b372d6b1a00b72ef7f8876d0bc6a78a4c9985c53614041512063" +"checksum owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" +"checksum parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e" "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +"checksum parking_lot_core 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "4db1a8ccf734a7bce794cc19b3df06ed87ab2f3907036b693c68f56b4d4537fa" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" "checksum paste 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "423a519e1c6e828f1e73b720f9d9ed2fa643dce8a7737fb43235ce0b41eeaa49" "checksum paste-impl 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "4214c9e912ef61bf42b81ba9a47e8aad1b2ffaf739ab162bf96d1e011f54e6c5" @@ -3016,7 +3193,7 @@ dependencies = [ "checksum regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "92b73c2a1770c255c240eaa4ee600df1704a38dc3feaa6e949e7fcd4f8dc09f9" "checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" -"checksum reqwest 0.10.0-alpha.0 (git+https://github.com/seanmonstar/reqwest?rev=5b55aee1a9ddf785f82d9086c8befc50db268cb8)" = "" +"checksum reqwest 0.10.0-alpha.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3d75dbf305ed1eb54d3c8564e3b746012166b40ec0841381df92b50a2052db71" "checksum rgb 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2089e4031214d129e201f8c3c8c2fe97cd7322478a0d1cdf78e7029b0042efdb" "checksum rsa 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6ad8d3632f6745bb671c8637e2aa44015537c5e384789d2ea3235739301ed1e0" "checksum rsa-der 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1170c86c683547fa781a0e39e6e281ebaedd4515be8a806022984f427ea3d44d" @@ -3035,8 +3212,6 @@ dependencies = [ "checksum serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)" = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2" "checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" -"checksum shred 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d15d46c92f8c0aed110a132f3c68a8cdd390048f51fa547c89dc571ba1e01191" -"checksum shrev 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b5752e017e03af9d735b4b069f53b7a7fd90fefafa04d8bd0c25581b0bff437f" "checksum simdeez 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4204ae48b2a871f428dc20426f005be250413fa6263e6f3d93094a36b29504dc" "checksum simdnoise 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "86a9e4c1c3369eab7105ac7e1582a601942fed0a63877cb2e1afcf57f34ed7b3" "checksum simple_asn1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618" @@ -3045,9 +3220,9 @@ dependencies = [ "checksum slotmap 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "759fd553261805f128e2900bf69ab3d034260bc338caf7f0ee54dbf035c85acd" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" "checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" -"checksum specs 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4943fde8c5d3d14c3d19d2a4c7abbd7b626c270a19e6cd35252294a48feb698c" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +"checksum static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" "checksum stream-cipher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" @@ -3075,13 +3250,14 @@ dependencies = [ "checksum tokio-timer 0.3.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b97c1587fe71018eb245a4a9daa13a5a3b681bbc1f7fdadfe24720e141472c13" "checksum tokio-tls 0.3.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)" = "566b4086589c7eebb86aa625d302ab80720ef2aa088649dcae18ec4d754cbd16" "checksum toml 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c7aabe75941d914b72bf3e5d3932ed92ce0664d49d8432305a8b547c37227724" +"checksum tonks 0.1.0 (git+https://github.com/feather-rs/tonks?rev=ccdecae02e11fc6694bd252d48cce5794499736f)" = "" +"checksum tonks-macros 0.1.0 (git+https://github.com/feather-rs/tonks?rev=ccdecae02e11fc6694bd252d48cce5794499736f)" = "" "checksum tower-make 0.3.0-alpha.2a (registry+https://github.com/rust-lang/crates.io-index)" = "316d47dd40cde4ac5d88110eaf9a10a4e2a68612d9c056cd2aa24e37dcb484cd" "checksum tower-service 0.3.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "63ff37396cd966ce43bea418bfa339f802857495f797dafa00bea5b7221ebdfa" "checksum tracing 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c21ff9457accc293386c20e8f754d0b059e67e325edf2284f04230d125d7e5ff" "checksum tracing-attributes 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3ff978fd9c9afe2cc9c671e247713421c6406b3422305cbdce5de695d3ab4c3c" "checksum tracing-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "528c8ebaaa16cdac34795180b046c031775b0d56402704d98c096788f33d646a" "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" -"checksum tuple_utils 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44834418e2c5b16f47bedf35c28e148db099187dd5feee6367fb2525863af4f1" "checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" "checksum unicase 2.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2e2e6bd1e59e56598518beb94fd6db628ded570326f0a98c679a304bd9f00150" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" diff --git a/Cargo.toml b/Cargo.toml index 4c9bd66dd..d9d83091f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,7 @@ members = [ "codegen", "generator", "util/rand-legacy", -] \ No newline at end of file +] + +[profile.dev] +opt-level = 1 diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index ab6f68331..ed4a27415 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -210,6 +210,10 @@ pub fn derive_packet(_item: TokenStream) -> TokenStream { PacketType::#ident } + fn ty_sized() -> PacketType where Self: Sized { + PacketType::#ident + } + fn box_clone(&self) -> Box { Box::new((*self).clone()) } diff --git a/core/Cargo.toml b/core/Cargo.toml index 1e0549076..b13027a10 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,7 +20,7 @@ log = "0.4" serde = { version = "1.0", features = ["derive"] } num-traits = "0.2" num-derive = "0.3" -hashbrown = { version = "0.6", features = ["serde"] } +hashbrown = { version = "0.6", features = ["serde", "rayon"] } hematite-nbt = "0.4" byteorder = "1.3" nalgebra-glm = "0.4" @@ -34,3 +34,5 @@ tokio = "=0.2.0-alpha.6" failure = "0.1" bitvec = "0.15" multimap = "0.6" +parking_lot = "0.9.0" +rayon = "1.2.0" diff --git a/core/src/network/mod.rs b/core/src/network/mod.rs index 732b62454..a34d9d5e2 100644 --- a/core/src/network/mod.rs +++ b/core/src/network/mod.rs @@ -1,7 +1,9 @@ +use crate::Packet; + pub mod codec; pub mod mctypes; pub mod packet; -pub fn cast_packet(packet: &dyn packet::Packet) -> &P { - packet.as_any().downcast_ref().unwrap() +pub fn cast_packet(packet: Box) -> P { + *packet.into_any().downcast().unwrap() } diff --git a/core/src/network/packet/implementation.rs b/core/src/network/packet/implementation.rs index ed9261cfd..72a7a5ca0 100644 --- a/core/src/network/packet/implementation.rs +++ b/core/src/network/packet/implementation.rs @@ -138,6 +138,13 @@ impl Packet for Handshake { PacketType::Handshake } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::Handshake + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -197,6 +204,13 @@ impl Packet for EncryptionResponse { PacketType::EncryptionResponse } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::EncryptionResponse + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -302,6 +316,13 @@ impl Packet for PluginMessageServerbound { PacketType::PluginMessageServerbound } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::PluginMessageServerbound + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -355,6 +376,13 @@ impl Packet for UseEntity { PacketType::UseEntity } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::UseEntity + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -479,6 +507,13 @@ impl Packet for PlayerDigging { PacketType::PlayerDigging } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::PlayerDigging + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -528,6 +563,13 @@ impl Packet for EntityAction { PacketType::EntityAction } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::EntityAction + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -672,6 +714,13 @@ impl Packet for AnimationServerbound { PacketType::AnimationServerbound } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::AnimationServerbound + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -741,6 +790,13 @@ impl Packet for PlayerBlockPlacement { PacketType::PlayerBlockPlacement } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::PlayerBlockPlacement + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -783,6 +839,13 @@ impl Packet for EncryptionRequest { PacketType::EncryptionRequest } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::EncryptionRequest + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -906,6 +969,13 @@ impl Packet for SpawnPlayer { PacketType::SpawnPlayer } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::SpawnPlayer + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -931,6 +1001,13 @@ impl Packet for AnimationClientbound { PacketType::AnimationClientbound } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::AnimationClientbound + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -960,6 +1037,13 @@ impl Packet for Statistics { PacketType::Statistics } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::Statistics + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1037,6 +1121,13 @@ impl Packet for BossBar { PacketType::BossBar } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::BossBar + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1158,6 +1249,13 @@ impl Packet for WindowItems { PacketType::WindowItems } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::WindowItems + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1203,6 +1301,13 @@ impl Packet for PluginMessageClientbound { PacketType::PluginMessageClientbound } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::PluginMessageClientbound + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1277,6 +1382,13 @@ impl Packet for Explosion { PacketType::Explosion } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::Explosion + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1387,6 +1499,13 @@ impl Packet for ChunkData { PacketType::ChunkData } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::ChunkData + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1509,7 +1628,14 @@ impl Packet for CombatEvent { } fn ty(&self) -> PacketType { - unimplemented!() + PacketType::CombatEvent + } + + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::CombatEvent } fn box_clone(&self) -> Box { @@ -1532,8 +1658,8 @@ impl Default for CombatEventType { #[derive(AsAny, new, Clone)] pub struct PlayerInfo { - action: PlayerInfoAction, - uuid: Uuid, + pub action: PlayerInfoAction, + pub uuid: Uuid, } impl Packet for PlayerInfo { @@ -1581,6 +1707,13 @@ impl Packet for PlayerInfo { PacketType::PlayerInfo } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::PlayerInfo + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1656,6 +1789,13 @@ impl Packet for DestroyEntities { PacketType::DestroyEntities } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::DestroyEntities + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } @@ -1707,6 +1847,13 @@ impl Packet for PacketEntityMetadata { PacketType::EntityMetadata } + fn ty_sized() -> PacketType + where + Self: Sized, + { + PacketType::EntityMetadata + } + fn box_clone(&self) -> Box { box_clone_impl!(self); } diff --git a/core/src/network/packet/mod.rs b/core/src/network/packet/mod.rs index d8bcc87ec..538d5cb30 100644 --- a/core/src/network/packet/mod.rs +++ b/core/src/network/packet/mod.rs @@ -10,10 +10,26 @@ pub trait AsAny { fn as_any(&self) -> &dyn Any; } -pub trait Packet: AsAny + Send + Sync { +pub trait IntoAny { + fn into_any(self: Box) -> Box; +} + +impl IntoAny for T +where + T: Any, +{ + fn into_any(self: Box) -> Box { + self + } +} + +pub trait Packet: AsAny + IntoAny + Send + Sync + Any { fn read_from(&mut self, buf: &mut Cursor<&[u8]>) -> Result<(), failure::Error>; fn write_to(&self, buf: &mut BytesMut); fn ty(&self) -> PacketType; + fn ty_sized() -> PacketType + where + Self: Sized; /// Returns a clone of this packet in a dynamic box. fn box_clone(&self) -> Box; diff --git a/core/src/save/player_data.rs b/core/src/save/player_data.rs index ca331ce2c..d9cae5f0b 100644 --- a/core/src/save/player_data.rs +++ b/core/src/save/player_data.rs @@ -8,8 +8,10 @@ use crate::inventory::{ use crate::ItemStack; use feather_items::Item; use std::fs; -use std::io::{Read, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; +use tokio::io::AsyncReadExt; +use tokio::prelude::AsyncRead; use uuid::Uuid; /// Represents the contents of a player data file. @@ -92,14 +94,16 @@ impl InventorySlot { } } -fn load_from_file(reader: R) -> Result { - nbt::from_gzip_reader::<_, PlayerData>(reader) +async fn load_from_file(mut reader: R) -> Result { + let mut buf = vec![]; + reader.read(&mut buf).await?; + nbt::from_gzip_reader(buf.as_slice()) } -pub fn load_player_data(world_dir: &Path, uuid: Uuid) -> Result { +pub async fn load_player_data(world_dir: &Path, uuid: Uuid) -> Result { let file_path = file_path(world_dir, uuid); - let file = File::open(file_path)?; - let data = load_from_file(file)?; + let file = tokio::fs::File::open(file_path).await?; + let data = load_from_file(file).await?; Ok(data) } diff --git a/core/src/world/mod.rs b/core/src/world/mod.rs index 907b0c218..2192e447d 100644 --- a/core/src/world/mod.rs +++ b/core/src/world/mod.rs @@ -2,9 +2,12 @@ use crate::world::block::*; use crate::world::chunk::Chunk; use glm::{DVec3, Vec3}; use hashbrown::HashMap; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use rayon::iter::ParallelIterator; use std::fmt; use std::fmt::{Display, Formatter}; use std::ops::{Add, Sub}; +use std::sync::Arc; pub mod block; #[allow(clippy::cast_lossless)] @@ -219,6 +222,17 @@ impl Display for ChunkPosition { } } +impl Add for ChunkPosition { + type Output = ChunkPosition; + + fn add(self, rhs: ChunkPosition) -> Self::Output { + ChunkPosition { + x: self.x + rhs.x, + z: self.z + rhs.z, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Hash32, Default, new)] pub struct BlockPosition { pub x: i32, @@ -247,97 +261,78 @@ impl Add for BlockPosition { } } -pub struct ChunkMap { - chunk_map: HashMap, -} +pub type ChunkMapInner = HashMap>>; + +/// The chunk map. +/// +/// This struct stores all the chunks on the server, +/// so it allows access to blocks and lighting data. +/// +/// Chunks are internally wrapped in `Arc`, +/// allowing multiple systems to access different parts +/// of the world in parallel. Mutable access to this +/// type is only required for inserting and removing +/// chunks. +pub struct ChunkMap(ChunkMapInner); impl ChunkMap { + /// Creates a new chunk map with no chunks. pub fn new() -> Self { - Self { - chunk_map: HashMap::new(), - } + Self(HashMap::new()) } - pub fn inner(&self) -> &HashMap { - &self.chunk_map + /// Retrieves a handle to the chunk at the given + /// position, or `None` if it is not loaded. + pub fn chunk_at(&self, pos: ChunkPosition) -> Option> { + self.0.get(&pos).map(|lock| lock.read()) } - pub fn inner_mut(&mut self) -> &mut HashMap { - &mut self.chunk_map + /// Retrieves a handle to the chunk at the given + /// position, or `None` if it is not loaded. + pub fn chunk_at_mut(&self, pos: ChunkPosition) -> Option> { + self.0.get(&pos).map(|lock| lock.write()) } - /// Retrieves the chunk at the specified location. - /// If the chunk is not loaded, `None` will be returned. - pub fn chunk_at(&self, pos: ChunkPosition) -> Option<&Chunk> { - if let Some(chunk) = self.chunk_map.get(&pos) { - return Some(chunk); - } - - None - } - - /// Retrieves the block at the specified - /// location. If the chunk in which the block - /// exists is not laoded, `None` is returned. + /// Retrieves the block at the given position, + /// or `None` if its chunk is not loaded. pub fn block_at(&self, pos: BlockPosition) -> Option { - if pos.y > 255 || pos.y < 0 { - return None; - } - - let chunk_pos = pos.chunk_pos(); - - if let Some(chunk) = self.chunk_at(chunk_pos) { - let rpos = chunk_relative_pos(pos); - Some(chunk.block_at(rpos.0, rpos.1, rpos.2)) - } else { - None - } + let (x, y, z) = chunk_relative_pos(pos); + self.chunk_at(pos.chunk_pos()) + .map(|chunk| chunk.block_at(x, y, z)) } /// Sets the block at the given position. - /// If the chunk in which the position resides - /// does not exist, `Err` is returned. In all - /// other cases, `Ok` is returned. /// - /// Note that on the server side, calling this function - /// does not broadcast the update in any way. As such, - /// the according function should be called instead. - pub fn set_block_at(&mut self, pos: BlockPosition, block: Block) -> Result<(), ()> { - if pos.y > 255 || pos.y < 0 { - return Err(()); - } + /// Returns `true` if the block was set, or `false` + /// if its chunk was not loaded and thus no operation + /// was performed. + pub fn set_block_at(&self, pos: BlockPosition, block: Block) -> bool { + let (x, y, z) = chunk_relative_pos(pos); - let chunk_pos = pos.chunk_pos(); - - if let Some(chunk) = self.chunk_map.get_mut(&chunk_pos) { - let (x, y, z) = chunk_relative_pos(pos); - chunk.set_block_at(x, y, z, block); - Ok(()) - } else { - Err(()) - } + self.chunk_at_mut(pos.chunk_pos()) + .map(|mut chunk| chunk.set_block_at(x, y, z, block)) + .is_some() } - /// Sets the chunk at the given location. - pub fn set_chunk_at(&mut self, pos: ChunkPosition, chunk: Chunk) { - self.chunk_map.insert(pos, chunk); + /// Returns an iterator over chunks. + pub fn iter_chunks(&self) -> impl IntoIterator>> { + self.0.values() } - /// Removes the chunk at the given location, - /// effectively unloading it. - pub fn unload_chunk_at(&mut self, pos: ChunkPosition) -> Option { - self.chunk_map.remove(&pos) + /// Returns a parallel iterator over chunks. + pub fn par_iter_chunks(&self) -> impl ParallelIterator>> { + self.0.par_values() } - /// Returns an immutable reference to the internal map. - pub fn chunks(&self) -> &HashMap { - &self.chunk_map + /// Inserts a new chunk into the chunk map. + pub fn insert(&mut self, chunk: Chunk) { + self.0 + .insert(chunk.position(), Arc::new(RwLock::new(chunk))); } - /// Returns a mutable reference to the internal - /// map. - pub fn chunks_mut(&mut self) -> &mut HashMap { - &mut self.chunk_map + /// Removes the chunk at the given position, returning `true` if it existed. + pub fn remove(&mut self, pos: ChunkPosition) -> bool { + self.0.remove(&pos).is_some() } } diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 65b2df87f..000000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -beta diff --git a/server/Cargo.toml b/server/Cargo.toml index 1071f1790..c39e4e0ed 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -13,65 +13,92 @@ name = "feather-server" path = "src/main.rs" [dependencies] +# Feather crates feather-blocks = { path = "../blocks" } feather-core = { path = "../core" } feather-item-block = { path = "../item_block" } +feather-codegen = { path = "../codegen" } + +# Core ECS + systems +legion = { git = "https://github.com/TomGillen/legion", rev = "0f67adc237af35799df173f31a2c238b3d8010a2" } +tonks = { git = "https://github.com/feather-rs/tonks", rev = "ccdecae02e11fc6694bd252d48cce5794499736f", features = ["system-registry"] } +# tonks = { path = "../../../dev/tonks", features = ["system-registry"] } + +# Concurrency/threading crossbeam = "0.7" +rayon = "1.2" +parking_lot = "0.9" +lock_api = "0.3" +thread_local = "1.0" + +# Netorking/IO +tokio = "=0.2.0-alpha.6" +tokio-executor = "=0.2.0-alpha.6" +futures-preview = { version = "=0.3.0-alpha.19", features = ["async-await"] } +bytes = "0.4" +mojang-api = "0.4" + +# Crypto +rsa = "0.1" +rsa-der = "0.2" +# Match RSA git master +num-bigint = { version = "0.4", features = ["rand", "i128", "u64_digit", "prime", "zeroize"], package = "num-bigint-dig" } + +# Hash functions +ahash = "0.2" +fnv = "1.0" +base64 = "0.10" + +# Math and physics +nalgebra-glm = "0.4" +nalgebra = "0.18" +ncollide3d = "0.20" + +# Other data structures +hashbrown = { version = "0.6", features = ["rayon"] } +bitvec = "0.15" +bitflags = "1.2" +heapless = "0.5" +uuid = { version = "0.7", features = ["v4"] } +multimap = "0.7" +smallvec = "0.6" +chashmap = "2.2" + +# Logging log = "0.4" simple_logger = "1.3" -uuid = { version = "0.7", features = ["v4"] } -derive-new = "0.5" + +# Serialization/deserialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.5" -rsa = "0.1" -# Match RSA git master -num-bigint = { version = "0.4", features = ["rand", "i128", "u64_digit", "prime", "zeroize"], package = "num-bigint-dig" } -rsa-der = "0.2" +hematite-nbt = "0.4" + +# RNGs rand = "0.7" rand_xorshift = "0.2" rand-legacy = { path = "../util/rand-legacy" } -bytes = "0.4" -hashbrown = { version = "0.6", features = ["rayon"] } -mojang-api = { git = "https://github.com/caelunshun/mojang-api-rs", rev = "6525e910ad53953fa16028f0fce74b1a19855733" } -multimap = "0.6" -hematite-nbt = "0.4" -specs = { version = "0.15", features = ["storage-event-control"] } -rayon = "1.2" -shrev = "1.1" + +# Other failure = "0.1" num-derive = "0.3" num-traits = "0.2" -smallvec = "0.6" lazy_static = "1.4" -nalgebra-glm = "0.4" -nalgebra = "0.18" -ncollide3d = "0.20" derive_deref = "1.1" -feather-codegen = { path = "../codegen" } -bitflags = "1.2" -fnv = "1.0" -base64 = "0.10" bumpalo = "2.6" -thread_local = "1.0" -parking_lot = "0.9" -heapless = "0.5" strum = "0.16" simdnoise = "3.1" simdeez = "0.6" -bitvec = "0.15" -tokio = "=0.2.0-alpha.6" -tokio-executor = "=0.2.0-alpha.6" -futures-preview = { version = "=0.3.0-alpha.19", features = ["async-await"] } humantime-serde = "0.1" ctrlc = "3.1" +inventory = "0.1" [dev-dependencies] -criterion = "0.3.0" +criterion = "0.3" [[bench]] name = "worldgen" harness = false [features] -nightly = ["specs/nightly", "parking_lot/nightly"] +nightly = ["hashbrown/nightly", "parking_lot/nightly"] diff --git a/server/src/blocks/falling.rs b/server/src/blocks/falling.rs deleted file mode 100644 index d048f9b13..000000000 --- a/server/src/blocks/falling.rs +++ /dev/null @@ -1,70 +0,0 @@ -use shrev::ReaderId; -use specs::shrev::EventChannel; -use specs::{Builder, Entities, LazyUpdate, Read, System, Write}; - -use feather_core::world::ChunkMap; - -use feather_blocks::{Block, BlockExt}; - -use crate::blocks::{BlockNotifyEvent, BlockUpdateCause, BlockUpdateEvent}; -use crate::entity::{falling_block, PositionComponent, VelocityComponent}; -use feather_core::Position; - -/// This system listens to `BlockNotifyEvent`s. -#[derive(Default)] -pub struct FallingBlockCreationSystem { - reader: Option>, -} - -impl<'a> System<'a> for FallingBlockCreationSystem { - type SystemData = ( - Read<'a, EventChannel>, - Write<'a, EventChannel>, - Write<'a, ChunkMap>, - Read<'a, LazyUpdate>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (events, mut block_update, mut chunk_map, lazy, entities) = data; - - // Process events - for event in events.read(&mut self.reader.as_mut().unwrap()) { - match event.block { - Block::Sand | Block::RedSand | Block::Gravel => { - let mut below = event.pos; - below.y -= 1; - - if !chunk_map.block_at(below).unwrap_or(Block::Air).is_solid() { - chunk_map.set_block_at(event.pos, Block::Air).unwrap(); - - let update_event = BlockUpdateEvent { - cause: BlockUpdateCause::FallingBlock, - pos: event.pos, - old_block: event.block, - new_block: Block::Air, - }; - - block_update.single_write(update_event); - - let mut entity_pos: Position = event.pos.world_pos(); - // Center position on block - entity_pos.x += 0.5; - entity_pos.z += 0.5; - - falling_block::create(&lazy, &entities, event.block, entity_pos) - .with(PositionComponent { - current: entity_pos, - previous: entity_pos, - }) - .with(VelocityComponent::default()) - .build(); - } - } - _ => (), - } - } - } - - setup_impl!(reader); -} diff --git a/server/src/blocks/mod.rs b/server/src/blocks/mod.rs deleted file mode 100644 index 67d43a4f0..000000000 --- a/server/src/blocks/mod.rs +++ /dev/null @@ -1,128 +0,0 @@ -mod falling; - -pub use falling::FallingBlockCreationSystem; - -use shrev::{EventChannel, ReaderId}; -use specs::{DispatcherBuilder, Entity, Read, System, Write}; - -use feather_blocks::Block; -use feather_core::world::{BlockPosition, ChunkMap}; - -use crate::systems::{BLOCK_FALLING_CREATION, BLOCK_UPDATE_PROPAGATE}; -use hashbrown::HashSet; - -lazy_static! { - /// List of block types that need to be notified - /// of adjacent block updates. - static ref BLOCKS_TO_NOTIFY: HashSet = { - let mut set = HashSet::new(); - // Falling blocks - set.insert(Block::Sand); - set.insert(Block::RedSand); - set.insert(Block::Gravel); - set - }; -} - -/// Event triggered when a block is updated. -/// -/// This event is triggered *after* the block is updated -/// in the chunk map. -#[derive(Debug, Clone)] -pub struct BlockUpdateEvent { - /// The cause of this block update event. - pub cause: BlockUpdateCause, - /// The location of the block which was updated. - pub pos: BlockPosition, - /// The block which was previously at the position. - pub old_block: Block, - /// The new block at the position. - pub new_block: Block, -} - -/// The possible causes of a block update event. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum BlockUpdateCause { - /// Indicates that a player updated the block. - Player(Entity), - /// Indicates that a falling block updated the block. - FallingBlock, - /// A test block update caused, used for unit testing. - Test, -} - -#[derive(Debug, Clone)] -pub struct BlockNotifyEvent { - pub block: Block, - pub pos: BlockPosition, - pub notified_by: BlockPosition, -} - -/// System for propagating block update -/// events to surrounding blocks. -/// -/// This system listens to `BlockUpdateEvent`s. -#[derive(Default)] -pub struct BlockUpdatePropagateSystem { - reader: Option>, -} - -impl<'a> System<'a> for BlockUpdatePropagateSystem { - type SystemData = ( - Read<'a, EventChannel>, - Read<'a, ChunkMap>, - Write<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (events, chunk_map, mut notify) = data; - - // Process events - for event in events.read(&mut self.reader.as_mut().unwrap()) { - let mut notify_events: Vec = Vec::new(); - - for x in -1..=1 { - for y in -1..=1 { - for z in -1..=1 { - let mut adjacent = event.pos; - adjacent.x += x; - adjacent.y += y; - adjacent.z += z; - let block = chunk_map.block_at(adjacent); - if let Some(block) = block { - if BLOCKS_TO_NOTIFY.contains(&block) { - notify_events.push(BlockNotifyEvent { - block, - pos: adjacent, - notified_by: event.pos, - }); - } - } - } - } - } - - // Notify all adjacent blocks at once - notify.drain_vec_write(&mut notify_events); - } - } - - setup_impl!(reader); -} - -pub fn init_logic(_dispatcher: &mut DispatcherBuilder) { - // TODO -} - -pub fn init_handlers(dispatcher: &mut DispatcherBuilder) { - dispatcher.add( - BlockUpdatePropagateSystem::default(), - BLOCK_UPDATE_PROPAGATE, - &[], - ); - dispatcher.add( - FallingBlockCreationSystem::default(), - BLOCK_FALLING_CREATION, - &[BLOCK_UPDATE_PROPAGATE], - ); -} diff --git a/server/src/broadcasters/animation.rs b/server/src/broadcasters/animation.rs new file mode 100644 index 000000000..a239e3806 --- /dev/null +++ b/server/src/broadcasters/animation.rs @@ -0,0 +1,21 @@ +use crate::entity::EntityId; +use crate::player::PlayerAnimationEvent; +use crate::state::State; +use feather_core::network::packet::implementation::AnimationClientbound; +use legion::query::Read; +use tonks::{PreparedWorld, Query}; + +/// Broadcasts animations. +#[event_handler] +fn broadcast_animation( + event: &PlayerAnimationEvent, + state: &State, + _query: &mut Query>, + world: &mut PreparedWorld, +) { + let packet = AnimationClientbound { + entity_id: world.get_component::(event.player).unwrap().0, + animation: event.animation, + }; + state.broadcast_entity_update(event.player, packet, Some(event.player)); +} diff --git a/server/src/broadcasters/entity_creation.rs b/server/src/broadcasters/entity_creation.rs new file mode 100644 index 000000000..88751b5d7 --- /dev/null +++ b/server/src/broadcasters/entity_creation.rs @@ -0,0 +1,75 @@ +use crate::chunk_logic::ChunkHolders; +use crate::entity::{CreationPacketCreator, EntityCreateEvent, SpawnPacketCreator}; +use crate::network::Network; +use crate::player::PlayerJoinEvent; +use crate::state::State; +use feather_core::Position; +use legion::query::Read; +use rayon::prelude::*; +use tonks::{PreparedWorld, Query, QueryAccessor}; + +/// When an entity is created and has a `CreationPacketCreator` and/or `SpawnPacketCreator`, +/// broadcasts the packets to all online clients. +#[event_handler] +fn broadcast_entity_creation( + events: &[EntityCreateEvent], + state: &State, + accessor1: &QueryAccessor>, + accessor2: &QueryAccessor>, + _query: &mut Query>, + world: &mut PreparedWorld, + holders: &ChunkHolders, +) { + events.par_iter().for_each(|event: &EntityCreateEvent| { + if let Some(accessor) = accessor1.find(event.entity) { + if let Some(packet_creator) = accessor.get_component::(world) { + let packet = packet_creator.get(&accessor, world); + state.broadcast_global_boxed(packet, None); + } + } + + if let Some(accessor) = accessor2.find(event.entity) { + if let Some(packet_creator) = accessor.get_component::(world) { + let packet = packet_creator.get(&accessor, world); + state.broadcast_entity_update_boxed(event.entity, packet, Some(event.entity)); + } + } + + // Register entity sends + let chunk = world + .get_component::(event.entity) + .unwrap() + .chunk_pos(); + for entity in holders.holders_for(chunk).unwrap_or(&[]) { + state.register_entity_send(event.entity, *entity); + } + }); +} + +/// Wehn a player joins, sends existing entities to the player. +/// +/// This only handles init packets (PlayerInfo, etc.)—spawn packets +/// are handled by the view update mechanism. +#[event_handler] +fn broadcast_existing_entities( + events: &[PlayerJoinEvent], + accessor: &QueryAccessor>, + query: &mut Query>, + _query2: &mut Query>, + world: &mut PreparedWorld, + state: &State, +) { + // TODO: change to par_iter when legion implements immutable queries + events.iter().for_each(|event: &PlayerJoinEvent| { + // Send init packets for all entities with a `CreationPacketCreator`. + let network = world.get_component::(event.player).unwrap(); + + query.par_entities_for_each_immutable(world, |(entity, packet_creator)| { + if let Some(accessor) = accessor.find(entity) { + let packet = packet_creator.get(&accessor, world); + network.send_boxed(packet); + state.register_entity_send(entity, event.player); + } + }); + }); +} diff --git a/server/src/broadcasters/entity_deletion.rs b/server/src/broadcasters/entity_deletion.rs new file mode 100644 index 000000000..e5848c921 --- /dev/null +++ b/server/src/broadcasters/entity_deletion.rs @@ -0,0 +1,46 @@ +use crate::chunk_logic::ChunkHolders; +use crate::entity::EntityDeleteEvent; +use crate::network::Network; +use crate::player::Player; +use crate::state::State; +use feather_core::network::packet::implementation::{ + DestroyEntities, PlayerInfo, PlayerInfoAction, +}; +use legion::query::Read; +use rayon::prelude::*; +use tonks::{PreparedWorld, Query}; + +/// Broadcasts when an entity is deleted. +#[event_handler] +fn broadcast_entity_deletion( + events: &[EntityDeleteEvent], + holders: &ChunkHolders, + _query: &mut Query<(Read, Read)>, + world: &mut PreparedWorld, + state: &State, +) { + events.par_iter().for_each(|event: &EntityDeleteEvent| { + if let Some(pos) = event.position { + let chunk = pos.chunk_pos(); + + for entity in holders.holders_for(chunk).unwrap_or(&[]) { + if let Some(network) = world.get_component::(*entity) { + network.send(DestroyEntities { + entity_ids: vec![event.id.0], + }); + state.register_entity_unload(event.entity, *entity); + } + } + } + + // If entity was a player, broadcast PlayerInfo with delete status + if world.get_component::(event.entity).is_some() { + let packet = PlayerInfo { + action: PlayerInfoAction::RemovePlayer, + uuid: event.uuid, + }; + + state.broadcast_global(packet, None); + } + }); +} diff --git a/server/src/broadcasters/keepalive.rs b/server/src/broadcasters/keepalive.rs new file mode 100644 index 000000000..ddf748ab3 --- /dev/null +++ b/server/src/broadcasters/keepalive.rs @@ -0,0 +1,12 @@ +use crate::state::State; +use crate::{TickCount, TPS}; +use feather_core::network::packet::implementation::KeepAliveClientbound; + +/// Broadcasts keepalives every second. +#[system] +fn broadcast_keepalive(state: &State, tick: &TickCount) { + if tick.0 % TPS == 0 { + let packet = KeepAliveClientbound { keep_alive_id: 0 }; + state.broadcast_global(packet, None); + } +} diff --git a/server/src/broadcasters/mod.rs b/server/src/broadcasters/mod.rs new file mode 100644 index 000000000..7da636247 --- /dev/null +++ b/server/src/broadcasters/mod.rs @@ -0,0 +1,11 @@ +//! Systems which broadcast packets based on events. +//! +//! There are two types of broadcasters: +//! * Those which broadcast packets to all online clients through `State::broadcast_global()`. +//! * Those which broadcast packets to all clients who can see a given entity through `State::broadcast_entity_update()`. + +mod animation; +pub mod entity_creation; +pub mod entity_deletion; +pub mod keepalive; +pub mod movement; diff --git a/server/src/broadcasters/movement.rs b/server/src/broadcasters/movement.rs new file mode 100644 index 000000000..c0d3be05b --- /dev/null +++ b/server/src/broadcasters/movement.rs @@ -0,0 +1,152 @@ +//! Broadcasting of movement updates. + +use crate::chunk_logic::ChunkHolders; +use crate::entity::{EntityId, EntityMoveEvent}; +use crate::network::Network; +use crate::util::{calculate_relative_move, degrees_to_stops}; +use feather_core::network::packet::implementation::{ + EntityHeadLook, EntityLook, EntityLookAndRelativeMove, EntityRelativeMove, +}; +use feather_core::{Packet, Position}; +use hashbrown::HashMap; +use legion::entity::Entity; +use legion::query::{Read, Write}; +use rayon::prelude::*; +use smallvec::SmallVec; +use tonks::{PreparedWorld, Query}; + +/// Component containing the last sent positions of all entities for a given client. +/// This component is used to determine +/// the relative movement for an entity. +#[derive(Default)] +pub struct LastKnownPositions(pub HashMap); + +/// System to broadcast when an entity moves. +#[event_handler] +fn broadcast_move( + events: &[EntityMoveEvent], + _query: &mut Query<( + Read, + Read, + Write, + Read, + )>, + world: &mut PreparedWorld, + chunk_holders: &ChunkHolders, +) { + events.iter().for_each(|event: &EntityMoveEvent| { + // Find position of entity. + let pos = *world.get_component::(event.entity).unwrap(); + + // Find clients which can see the entity. + let chunk = pos.chunk_pos(); + let clients = chunk_holders.holders_for(chunk).unwrap_or(&[]); + + let entity_id = world.get_component::(event.entity).unwrap().0; + + // For each client, send the position update relative to the client's last known + // position for the entity. If no `LastKnownPositions` entry exists for the entity, + // then the entity has not yet been sent to the client, so we do not send a position + // update. (When an entity is spawned on a client, the `LastKnownPositions` entry + // is inserted with the starting position.) + clients.iter().copied().for_each(|client: Entity| { + // Don't sent player's position to themself + if client == event.entity { + return; + } + + if !world.is_alive(client) { + return; + } + + let mut last_known_positions = + unsafe { world.get_component_mut_unchecked::(client) }.unwrap(); + if let Some(old_pos) = last_known_positions.0.get_mut(&event.entity) { + let packets = packets_for_movement_update(entity_id, *old_pos, pos); + + let network = world.get_component::(client).unwrap(); + + packets.into_iter().for_each(|packet| { + network.send_boxed(packet); + }); + + // Update last known position. + *old_pos = pos; + } + }); + }); +} + +/// Returns the packet needed to notify a client +/// of a position update, from the old position to the new one. +#[allow(clippy::float_cmp)] +fn packets_for_movement_update( + entity_id: i32, + old_pos: Position, + new_pos: Position, +) -> SmallVec<[Box; 2]> { + if old_pos == new_pos { + return smallvec![]; + } + + let mut packets = smallvec![]; + + let has_moved = old_pos.x != new_pos.x || old_pos.y != new_pos.y || old_pos.z != new_pos.z; + let has_looked = old_pos.pitch != new_pos.pitch + || old_pos.yaw != new_pos.yaw + || old_pos.on_ground != new_pos.on_ground; + + if has_moved { + let (rx, ry, rz) = calculate_relative_move(old_pos, new_pos); + + if (rx == 0 && ry == 0 && rz == 0) && !has_looked { + // Because of floating point errors, + // the physics system may trigger an + // event when the distance moved is minuscule, + // which causes jittering on the client. + // Don't send the packet if it has no effect. + return smallvec![]; + } + + if has_looked { + let packet: Box = Box::new(EntityLookAndRelativeMove::new( + entity_id, + rx, + ry, + rz, + degrees_to_stops(new_pos.yaw), + degrees_to_stops(new_pos.pitch), + new_pos.on_ground, + )); + packets.push(packet); + } else { + let packet: Box = Box::new(EntityRelativeMove::new( + entity_id, + rx, + ry, + rz, + new_pos.on_ground, + )); + packets.push(packet); + } + } else { + let packet: Box = Box::new(EntityLook::new( + entity_id, + degrees_to_stops(new_pos.yaw), + degrees_to_stops(new_pos.pitch), + new_pos.on_ground, + )); + packets.push(packet); + } + + // Entity Head Look also needs to be sent if the entity turned its head + if has_looked { + let packet: Box = Box::new(EntityHeadLook::new( + entity_id, + degrees_to_stops(new_pos.yaw), + )); + packets.push(packet); + } + + packets +} diff --git a/server/src/chunk_entities.rs b/server/src/chunk_entities.rs new file mode 100644 index 000000000..75a2bd88d --- /dev/null +++ b/server/src/chunk_entities.rs @@ -0,0 +1,112 @@ +use crate::entity::{EntityCreateEvent, EntityDeleteEvent, EntityMoveEvent, PreviousPosition}; +use crate::state::State; +use feather_core::{ChunkPosition, Position}; +use hashbrown::HashMap; +use legion::entity::Entity; +use legion::query::Read; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; +use rayon::prelude::*; +use tonks::{PreparedWorld, Query}; + +static EMPTY_VEC: Vec = Vec::new(); + +/// Stores which entities belong to every given chunk. +/// +/// This data structure can be used to accelerate certain +/// operations, such as querying for entities +/// within some distance of a position. In addition, +/// it can be used to send all entities in a chunk +/// to a player. +/// +/// This structure is internally stored in `State`, using +/// a `RwLock` for concurrent access. (TODO: remove lock.) +/// +/// Do note that the information in this structure is not necessarily up to date, +/// although a best effort is made to update the data. +#[derive(Resource)] +pub struct ChunkEntities(RwLock>>); + +impl ChunkEntities { + pub fn new() -> Self { + Self(RwLock::new(HashMap::new())) + } + + /// Returns a slice of entities in the given chunk. + pub fn entities_in_chunk(&self, chunk: ChunkPosition) -> MappedRwLockReadGuard<[Entity]> { + let map = self.0.read(); + + RwLockReadGuard::map(map, move |map| { + if let Some(vec) = map.get(&chunk) { + vec.as_slice() + } else { + &EMPTY_VEC + } + }) + } +} + +impl Default for ChunkEntities { + fn default() -> Self { + Self::new() + } +} + +/// System to update ChunkEntities when entities move into new chunks. +#[event_handler] +fn chunk_entities_handle_movement( + events: &[EntityMoveEvent], + state: &State, + _query: &mut Query<(Read, Read)>, + world: &mut PreparedWorld, +) { + events.par_iter().for_each(|event| { + let old_pos = world + .get_component::(event.entity) + .unwrap() + .0; + let new_pos = *world.get_component::(event.entity).unwrap(); + + let old_chunk = old_pos.chunk_pos(); + let new_chunk = new_pos.chunk_pos(); + + if old_chunk != new_chunk { + // Update chunk entities + let mut map = state.chunk_entities.0.write(); + map.entry(new_chunk) + .or_insert_with(|| vec![]) + .push(event.entity); + map.entry(old_chunk).and_modify(|vec| { + vec.remove_item(&event.entity); + }); + } + }) +} + +#[event_handler] +fn chunk_entities_insert( + event: &EntityCreateEvent, + state: &State, + _query: &mut Query>, + world: &mut PreparedWorld, +) { + if let Some(position) = world.get_component::(event.entity) { + let chunk = position.chunk_pos(); + let mut map = state.chunk_entities.0.write(); + + map.entry(chunk) + .or_insert_with(|| vec![]) + .push(event.entity); + } +} + +#[event_handler] +fn chunk_entities_remove(event: &EntityDeleteEvent, state: &State) { + if let Some(position) = event.position { + let chunk = position.chunk_pos(); + let mut map = state.chunk_entities.0.write(); + + map.entry(chunk) + .or_insert_with(|| vec![]) + .remove_item(&event.entity); + } +} diff --git a/server/src/chunk_logic.rs b/server/src/chunk_logic.rs index 2212903ef..134a31482 100644 --- a/server/src/chunk_logic.rs +++ b/server/src/chunk_logic.rs @@ -3,36 +3,31 @@ //! //! Also handles unloading chunks when unused. use crossbeam::channel::{Receiver, Sender}; -use shrev::{EventChannel, ReaderId}; -use specs::{ - Component, DispatcherBuilder, Entity, Read, ReadExpect, ReadStorage, System, World, Write, -}; use std::sync::atomic::{AtomicU32, Ordering}; -use feather_core::world::{ChunkMap, ChunkPosition}; +use feather_core::world::ChunkPosition; use rayon::prelude::*; -use crate::config::Config; -use crate::entity::EntityDestroyEvent; -use crate::systems::{CHUNK_HOLD_REMOVE, CHUNK_LOAD, CHUNK_OPTIMIZE, CHUNK_UNLOAD}; -use crate::worldgen::WorldGenerator; -use crate::{chunkworker, current_time_in_millis, TickCount, TPS}; +use crate::entity::EntityDeleteEvent; +use crate::state::State; +use crate::{chunk_worker, current_time_in_millis, TickCount, TPS}; use feather_core::entity::EntityData; use feather_core::Chunk; use hashbrown::HashSet; +use legion::entity::Entity; +use legion::query::Read; use multimap::MultiMap; -use specs::storage::BTreeStorage; use std::collections::VecDeque; -use std::path::Path; use std::sync::Arc; +use tonks::{PreparedWorld, Query, Trigger}; /// A handle for interacting with the chunk /// worker thread. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Resource)] pub struct ChunkWorkerHandle { - pub sender: Sender, - pub receiver: Receiver, + pub sender: Sender, + pub receiver: Receiver, } /// Event which is triggered when a chunk is loaded. @@ -48,84 +43,35 @@ pub struct ChunkLoadFailEvent { pub pos: ChunkPosition, } -/// Event which is triggered when a chunk is unloaded. -#[derive(Clone)] -pub struct ChunkUnloadEvent { - /// The chunk which was unloaded. - pub chunk: Arc, -} - /// System for receiving loaded chunks from the chunk worker thread. -pub struct ChunkLoadSystem; - -impl<'a> System<'a> for ChunkLoadSystem { - type SystemData = ( - Write<'a, ChunkMap>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - ReadExpect<'a, ChunkWorkerHandle>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut chunk_map, mut load_events, mut fail_events, handle) = data; - - while let Ok(reply) = handle.receiver.try_recv() { - if let chunkworker::Reply::LoadedChunk(pos, result) = reply { - match result { - Ok((chunk, entities)) => { - chunk_map.set_chunk_at(pos, chunk); - - // Trigger event - let event = ChunkLoadEvent { pos, entities }; - load_events.single_write(event); - - trace!("Loaded chunk at {:?}", pos); - } - Err(err) => { - warn!("Failed to load chunk at {:?}: {}", pos, err); - let event = ChunkLoadFailEvent { pos }; - fail_events.single_write(event); - } +#[system] +fn chunk_load_system( + state: &State, + handle: &ChunkWorkerHandle, + fail_events: &mut Trigger, +) { + while let Ok(reply) = handle.receiver.try_recv() { + if let chunk_worker::Reply::LoadedChunk(pos, result) = reply { + match result { + Ok((chunk, entities)) => { + state.lazy_insert_chunk(chunk); + + // Trigger event - lazily so it happens after the chunk is inserted into the chunk map + let event = ChunkLoadEvent { pos, entities }; + state.exec_with_scheduler(move |_, scheduler| { + scheduler.trigger(event); + }); + + trace!("Loaded chunk at {:?}", pos); + } + Err(err) => { + warn!("Failed to load chunk at {:?}: {}", pos, err); + let event = ChunkLoadFailEvent { pos }; + fail_events.trigger(event); } } } } - - fn setup(&mut self, world: &mut World) { - use specs::prelude::SystemData; - - let generator = world.fetch_mut::>().clone(); - let world_name = &world.fetch_mut::>().world.name.clone(); - let world_dir = Path::new(world_name); - - info!("Starting chunk worker thread"); - let (sender, receiver) = chunkworker::start(world_dir, generator); - world.insert(ChunkWorkerHandle { sender, receiver }); - - Self::SystemData::setup(world); - } -} - -/// Asynchronously loads the chunk at the given position. -/// At some point in time after this function is called, -/// the chunk will appear in the chunk map. -/// -/// In the event that the requested chunk does not exist -/// in the world save, it will be generated asynchronously. -pub fn load_chunk(handle: &ChunkWorkerHandle, pos: ChunkPosition) { - // Send request to chunk worker thread - handle - .sender - .send(chunkworker::Request::LoadChunk(pos)) - .unwrap(); -} - -/// Asynchronously saves the chunk at the given position. -pub fn save_chunk(handle: &ChunkWorkerHandle, chunk: Arc, entities: Vec) { - handle - .sender - .send(chunkworker::Request::SaveChunk(chunk, entities)) - .unwrap(); } /// The chunk holder map contains a mapping @@ -139,7 +85,7 @@ pub fn save_chunk(handle: &ChunkWorkerHandle, chunk: Arc, entities: Vec, } @@ -163,7 +109,7 @@ impl ChunkHolders { &mut self, chunk: ChunkPosition, holder: Entity, - events: &mut EventChannel, + trigger: &mut Trigger, ) { if let Some(vec) = self.inner.get_vec_mut(&chunk) { let index = vec.iter().position(|e| *e == holder); @@ -175,7 +121,7 @@ impl ChunkHolders { entity: holder, chunk, }; - events.single_write(event); + trigger.trigger(event); } } } @@ -191,15 +137,15 @@ pub struct ChunkHolderReleaseEvent { } /// The queue of chunks to be unloaded. -/// See `ChunkUnloadSystem` for details. -#[derive(Clone, Debug, Default)] +/// See `chunk_unload` for details. +#[derive(Clone, Debug, Default, Resource)] pub struct ChunkUnloadQueue { /// The internal queue. queue: VecDeque, } /// A chunk to be unloaded. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Resource)] struct ChunkUnload { /// The position of this chunk. chunk: ChunkPosition, @@ -212,13 +158,7 @@ struct ChunkUnload { const CHUNK_UNLOAD_TIME: u64 = TPS * 5; // 5 seconds - TODO make this configurable /// System for unloading chunks when they have no holders. -/// This system performs multiple actions: -/// -/// * It listens to `ChunkHolderReleaseEvent` and -/// checks if a chunk has no holders. If so, it queues -/// the chunk to be unloaded after some period of time -/// (defined by a constant). -/// * It goes through chunks which are currently +/// This system through chunks which are currently /// queued to be loaded and unloads them if the /// period of time has elapsed. /// @@ -228,91 +168,57 @@ const CHUNK_UNLOAD_TIME: u64 = TPS * 5; // 5 seconds - TODO make this configurab /// could quickly move between chunk boundaries, causing /// chunks at the edge of their view distance /// to be loaded and unloaded at an alarming rate. -#[derive(Default)] -pub struct ChunkUnloadSystem { - reader: Option>, -} - -impl ChunkUnloadSystem { - pub fn new() -> Self { - Self { reader: None } - } -} - -impl<'a> System<'a> for ChunkUnloadSystem { - type SystemData = ( - Write<'a, ChunkMap>, - Write<'a, EventChannel>, - Read<'a, EventChannel>, - Write<'a, ChunkUnloadQueue>, - Read<'a, ChunkHolders>, - Read<'a, TickCount>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut chunk_map, - mut unload_events, - release_events, - mut unload_queue, - holders, - tick_count, - ) = data; - - // Handle holder release events. - for event in release_events.read(&mut self.reader.as_mut().unwrap()) { - // If the chunk now has zero holders, queue it for unloading. - if !holders.chunk_has_holders(event.chunk) { - let unload = ChunkUnload { - chunk: event.chunk, - time: tick_count.0 + CHUNK_UNLOAD_TIME, - }; - unload_queue.queue.push_back(unload); - } - } - - // Unload chunks which are finished in the queue. - - // Since chunks are queued in the back and taken out - // from the front, the chunks in the front of the vector - // were queued the longest time ago. Because of this, - // we go through the unloads in the front of the queue - // to find which chunks to unload. - while let Some(unload) = unload_queue.queue.front() { - if tick_count.0 >= unload.time { - // Don't unload if new chunk holders have appeared. - if holders.chunk_has_holders(unload.chunk) { - unload_queue.queue.pop_front(); - continue; - } - - // Unload chunk and pop from queue. - if let Some(chunk) = chunk_map.unload_chunk_at(unload.chunk) { - let event = ChunkUnloadEvent { - chunk: Arc::new(chunk), - }; - unload_events.single_write(event); - } - +#[system] +fn chunk_unload( + state: &State, + unload_queue: &mut ChunkUnloadQueue, + holders: &ChunkHolders, + tick_count: &TickCount, +) { + // Unload chunks which are finished in the queue. + + // Since chunks are queued in the back and taken out + // from the front, the chunks in the front of the vector + // were queued the longest time ago. Because of this, + // we go through the unloads in the front of the queue + // to find which chunks to unload. + while let Some(unload) = unload_queue.queue.front() { + if tick_count.0 >= unload.time { + // Don't unload if new chunk holders have appeared. + if holders.chunk_has_holders(unload.chunk) { unload_queue.queue.pop_front(); - } else { - // We're done - all chunks farther up in - // the queue were queued before this one, - // so it isn't time to unload any of those. - break; + continue; } + + // Unload chunk and pop from queue. + state.lazy_remove_chunk(unload.chunk); + unload_queue.queue.pop_front(); + } else { + // We're done - all chunks farther up in + // the queue were queued before this one, + // so it isn't time to unload any of those. + break; } } +} - fn setup(&mut self, world: &mut World) { - use specs::SystemData; - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); +/// Event handler which handles holder release events. If +/// a chunk has no more holders, then a chunk unload is queued. +#[event_handler] +pub fn chunk_unload_no_holders( + event: &ChunkHolderReleaseEvent, + holders: &ChunkHolders, + unload_queue: &mut ChunkUnloadQueue, + tick_count: &TickCount, +) { + // Handle holder release events. + // If the chunk now has zero holders, queue it for unloading. + if !holders.chunk_has_holders(event.chunk) { + let unload = ChunkUnload { + chunk: event.chunk, + time: tick_count.0 + CHUNK_UNLOAD_TIME, + }; + unload_queue.queue.push_back(unload); } } @@ -324,69 +230,35 @@ impl<'a> System<'a> for ChunkUnloadSystem { /// stored in the `ChunkHolders` resource, /// using this component allows for efficiently /// finding which chunks a given entity has -/// a hold on. +/// a hold on, rather than having +/// to linear search all chunks (obviously ridiculous). #[derive(Default)] -pub struct ChunkHolderComponent { +pub struct ChunkHolder { pub holds: HashSet, } -impl ChunkHolderComponent { +impl ChunkHolder { pub fn new() -> Self { - Self { - holds: HashSet::new(), - } + Self::default() } } -impl Component for ChunkHolderComponent { - type Storage = BTreeStorage; -} - /// System for removing an entity's chunk holds /// once it is destroyed. -#[derive(Default)] -pub struct ChunkHoldRemoveSystem { - reader: Option>, -} - -impl ChunkHoldRemoveSystem { - pub fn new() -> Self { - Self { reader: None } - } -} - -impl<'a> System<'a> for ChunkHoldRemoveSystem { - type SystemData = ( - Read<'a, EventChannel>, - Write<'a, ChunkHolders>, - ReadStorage<'a, ChunkHolderComponent>, - Write<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (events, mut holders, holder_comps, mut release_events) = data; - - for event in events.read(&mut self.reader.as_mut().unwrap()) { - // If entity had chunk holds, remove them all - if let Some(holder_comp) = holder_comps.get(event.entity) { - debug!("Removing chunk holds for entity {:?}", event.entity); - holder_comp.holds.iter().for_each(|chunk| { - holders.remove_holder(*chunk, event.entity, &mut release_events); - }); - } - } - } - - fn setup(&mut self, world: &mut World) { - use specs::SystemData; - - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); +#[event_handler] +fn chunk_holder_remove( + event: &EntityDeleteEvent, + _query: &mut Query>, + world: &mut PreparedWorld, + holders: &mut ChunkHolders, + release_events: &mut Trigger, +) { + // If entity had chunk holds, remove them all + if let Some(holder_comp) = world.get_component::(event.entity) { + debug!("Removing chunk holds for entity {:?}", event.entity); + holder_comp.holds.iter().for_each(|chunk| { + holders.remove_holder(*chunk, event.entity, release_events); + }); } } @@ -402,121 +274,86 @@ const CHUNK_OPTIMIZE_INTERVAL: u64 = TPS * 60 * 5; // 5 minutes /// For optimal performance, this system is fully /// concurrent - each chunk optimization is split /// into a separate job and fed into `rayon`. -pub struct ChunkOptimizeSystem; - -impl<'a> System<'a> for ChunkOptimizeSystem { - type SystemData = (Write<'a, ChunkMap>, Read<'a, TickCount>); - - fn run(&mut self, data: Self::SystemData) { - let (mut chunk_map, tick_count) = data; - - // Only run every CHUNK_OPTIMIZE_INTERVAL ticks - if tick_count.0 % CHUNK_OPTIMIZE_INTERVAL != 0 { - return; - } - - let chunks = chunk_map.chunks_mut(); - - // Don't run if there aren't any chunks loaded - if chunks.is_empty() { - return; - } +#[system] +fn chunk_optimize(state: &State, tick_count: &TickCount) { + // Only run every CHUNK_OPTIMIZE_INTERVAL ticks + if tick_count.0 % CHUNK_OPTIMIZE_INTERVAL != 0 { + return; + } - debug!("Optimizing chunks"); + debug!("Optimizing chunks"); - let start_time = current_time_in_millis(); - let count = AtomicU32::new(0); + let start_time = current_time_in_millis(); + let count = AtomicU32::new(0); - chunks.par_iter_mut().for_each(|(_, chunk)| { - count.fetch_add(chunk.optimize(), Ordering::SeqCst); - }); + state.chunk_map.par_iter_chunks().for_each(|chunk| { + count.fetch_add(chunk.write().optimize(), Ordering::Relaxed); + }); - let end_time = current_time_in_millis(); - let elapsed = end_time - start_time; + let end_time = current_time_in_millis(); + let elapsed = end_time - start_time; - debug!( - "Optimized {} chunk sections (took {}ms - {:.2}ms/section)", - count.load(Ordering::SeqCst), - elapsed, - elapsed as f64 / f64::from(count.load(Ordering::SeqCst)) - ); - } -} - -pub fn init_logic(dispatcher: &mut DispatcherBuilder) { - dispatcher.add(ChunkLoadSystem, CHUNK_LOAD, &[]); - dispatcher.add(ChunkOptimizeSystem, CHUNK_OPTIMIZE, &[]); + debug!( + "Optimized {} chunk sections (took {}ms - {:.2}ms/section)", + count.load(Ordering::Relaxed), + elapsed, + elapsed as f64 / f64::from(count.load(Ordering::Relaxed)) + ); } -pub fn init_handlers(dispatcher: &mut DispatcherBuilder) { - dispatcher.add(ChunkUnloadSystem::default(), CHUNK_UNLOAD, &[]); - dispatcher.add(ChunkHoldRemoveSystem::default(), CHUNK_HOLD_REMOVE, &[]); +/// Adds a hold for a chunk for the given entity. +pub fn hold_chunk( + entity: Entity, + holder: &mut ChunkHolder, + holders: &mut ChunkHolders, + chunk: ChunkPosition, +) { + holder.holds.insert(chunk); + holders.inner.insert(chunk, entity); } -#[cfg(test)] -mod tests { - use specs::{RunNow, World, WorldExt}; - - use feather_core::world::chunk::Chunk; - use feather_core::world::ChunkPosition; - - use super::*; - - #[test] - fn test_chunk_system() { - let (send1, _recv1) = crossbeam::channel::unbounded(); - let (send2, recv2) = crossbeam::channel::unbounded(); - let handle = ChunkWorkerHandle { - sender: send1, - receiver: recv2, - }; +/// Releases a hold for a chunk for the given entity. +pub fn release_chunk( + entity: Entity, + holder: &mut ChunkHolder, + holders: &mut ChunkHolders, + chunk: ChunkPosition, + trigger: &mut Trigger, +) { + holder.holds.remove(&chunk); + if let Some(vec) = holders.inner.get_vec_mut(&chunk) { + let mut index = None; + for (i, e) in vec.iter().enumerate() { + if *e == entity { + index = Some(i); + } + } - let chunk_map = ChunkMap::new(); - let pos = ChunkPosition::new(0, 0); - send2 - .send(chunkworker::Reply::LoadedChunk( - pos, - Ok((Chunk::new(pos), vec![])), - )) - .unwrap(); - - let load_event_channel = EventChannel::::new(); - let fail_event_channel = EventChannel::::new(); - - let mut system = ChunkLoadSystem; - let mut world = World::new(); - world.insert(chunk_map); - world.insert(handle); - world.insert(load_event_channel); - world.insert(fail_event_channel); - - system.run_now(&world); - - // Confirm that chunk was loaded - let chunk_map = world.read_resource::(); - let chunk = chunk_map.chunk_at(pos); - - assert!(chunk.is_some()); - assert!(chunk.unwrap().position() == pos); + if let Some(index) = index { + vec.swap_remove(index); + } } + trigger.trigger(ChunkHolderReleaseEvent { entity, chunk }) +} - #[test] - fn test_load_chunk() { - let (send1, recv1) = crossbeam::channel::unbounded(); - let (_send2, recv2) = crossbeam::channel::unbounded(); - let handle = ChunkWorkerHandle { - sender: send1, - receiver: recv2, - }; - - let pos = ChunkPosition::new(0, 0); - - load_chunk(&handle, pos); +/// Asynchronously loads the chunk at the given position. +/// At some point in time after this function is called, +/// the chunk will appear in the chunk map. +/// +/// In the event that the requested chunk does not exist +/// in the world save, it will be generated asynchronously. +pub fn load_chunk(handle: &ChunkWorkerHandle, pos: ChunkPosition) { + // Send request to chunk worker thread + handle + .sender + .send(chunk_worker::Request::LoadChunk(pos)) + .unwrap(); +} - let recv = recv1.try_recv().unwrap(); - match recv { - chunkworker::Request::LoadChunk(recv_pos) => assert_eq!(recv_pos, pos), - _ => panic!(), - } - } +/// Asynchronously saves the chunk at the given position. +pub fn save_chunk(handle: &ChunkWorkerHandle, chunk: Arc, entities: Vec) { + handle + .sender + .send(chunk_worker::Request::SaveChunk(chunk, entities)) + .unwrap(); } diff --git a/server/src/chunkworker.rs b/server/src/chunk_worker.rs similarity index 100% rename from server/src/chunkworker.rs rename to server/src/chunk_worker.rs diff --git a/server/src/entity/broadcast.rs b/server/src/entity/broadcast.rs deleted file mode 100644 index b2444f92e..000000000 --- a/server/src/entity/broadcast.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Module for broadcasting when an entity comes within -//! range of a player. Also handles sending the correct -//! packet to spawn entities on the client. -//! -//! Sending entities to a client is handled lazily -//! through `LazyUpdate`, because arbitrary components -//! may need to be accessed. - -use crate::chunk_logic::ChunkHolders; -use crate::entity::{LastKnownPositionComponent, PacketCreatorComponent}; -use crate::entity::{Metadata, PositionComponent}; -use crate::lazy::LazyUpdateExt; -use crate::network::{send_packet_boxed_to_player, send_packet_to_player, NetworkComponent}; -use feather_core::network::packet::implementation::PacketEntityMetadata; -use shrev::EventChannel; -use specs::{Entity, LazyUpdate, Read, ReadStorage, ReaderId, System, WorldExt}; - -/// An entity send request, containing -/// the player to send to and the entity -/// to send. -#[derive(Debug)] -struct SendRequest { - player: Entity, - entity: Entity, -} - -/// Event which is triggered when an entity -/// is sent to a client. This can be used to send -/// associated information, such as entity equipment. -#[derive(Debug, Clone)] -pub struct EntitySendEvent { - /// The player for which this event was triggered. - pub player: Entity, - /// The entity which was sent to the player. - pub entity: Entity, -} - -/// Event triggered when an entity of any -/// type is spawned. -#[derive(Debug, Clone)] -pub struct EntitySpawnEvent { - /// The spawned entity. - pub entity: Entity, -} - -/// System for broadcasting when an entity is spawned. -/// -/// Broadcasts are lazily queued for sending -/// and are sent by `EntitySendSystem`. -/// -/// This system listens to `EntitySpawnEvent`s. -#[derive(Default)] -pub struct EntityBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for EntityBroadcastSystem { - type SystemData = ( - ReadStorage<'a, PositionComponent>, - ReadStorage<'a, NetworkComponent>, - Read<'a, ChunkHolders>, - Read<'a, EventChannel>, - Read<'a, LazyUpdate>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, networks, chunk_holders, spawn_events, lazy) = data; - - for event in spawn_events.read(self.reader.as_mut().unwrap()) { - // Broadcast entity to players who can see it. - let position = match positions.get(event.entity) { - Some(position) => position, - None => continue, - }; - let chunk = position.current.chunk_pos(); - - if let Some(holders) = chunk_holders.holders_for(chunk) { - for holder in holders { - if networks.get(*holder).is_none() { - // Not a player. - continue; - } - - // Don't send player to themself. - if *holder == event.entity { - continue; - } - - lazy.send_entity_to_player(*holder, event.entity); - } - } - } - } - - setup_impl!(reader); -} - -/// Lazily sends an entity to a player. -pub fn send_entity_to_player(lazy: &LazyUpdate, player: Entity, entity: Entity) { - lazy.exec(move |world| { - // Attempt to get the `PacketCreator` for the entity. - // If it doesn't exist, skip sending. - let packet_creators = world.read_component::(); - let packet_creator = match packet_creators.get(entity) { - Some(packet_creator) => packet_creator, - None => return, - }; - - let create_packet = packet_creator.0; - let packet = create_packet(world, entity); - - if let Some(network) = world.read_component::().get(player) { - send_packet_boxed_to_player(network, packet); - - // If the entity has metadata, send it. - let metas = world.read_component::(); - if let Some(meta) = metas.get(entity) { - let packet = PacketEntityMetadata { - entity_id: entity.id() as i32, - metadata: meta.to_full_raw_metadata(), - }; - send_packet_to_player(network, packet); - } - } - - // Insert last known position - let positions = world.read_component::(); - let mut last_positions = world.write_component::(); - if let Some(last_positions) = last_positions.get_mut(player) { - if let Some(pos) = positions.get(entity) { - last_positions.0.insert(entity, pos.current); - } - } - - // Trigger event - let event = EntitySendEvent { entity, player }; - world.fetch_mut::>().single_write(event); - }); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::entity::{item, VelocityComponent}; - use crate::player::ChunkCrossSystem; - use crate::testframework as t; - use feather_core::network::cast_packet; - use feather_core::network::packet::implementation::{SpawnObject, SpawnPlayer}; - use feather_core::network::packet::PacketType; - use feather_core::{Item, ItemStack}; - use specs::{Builder, WorldExt}; - - #[test] - fn test_spawn_player() { - let (mut w, mut d) = t::init_world(); - - let player1 = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - - let event = EntitySpawnEvent { - entity: player1.entity, - }; - - w.fetch_mut::>().single_write(event); - - d.dispatch(&w); - w.maintain(); - - t::assert_packet_not_received(&player1, PacketType::SpawnPlayer); // Player shouldn't have received packet for themselves - - let packet = t::assert_packet_received(&player2, PacketType::SpawnPlayer); - let packet = cast_packet::(&*packet); - - assert_eq!(packet.entity_id, player1.entity.id() as i32); - } - - #[test] - fn test_spawn_item() { - let (mut w, mut d) = t::builder() - .with(EntityBroadcastSystem::default(), "broadcast") - .with(ChunkCrossSystem::default(), "chunk_cross") - .build(); - - let player = t::add_player(&mut w); - - let item = item::create(&w.fetch(), &w.fetch(), ItemStack::new(Item::Stone, 1), 0) - .with(PositionComponent::default()) - .with(VelocityComponent::default()) - .build(); - - w.maintain(); - d.dispatch(&w); - w.maintain(); - - let spawn_entity = t::assert_packet_received(&player, PacketType::SpawnObject); - let spawn_entity = cast_packet::(&*spawn_entity); - - assert_eq!(spawn_entity.entity_id, item.id() as i32); - assert_eq!(spawn_entity.velocity_x, 0); - } -} diff --git a/server/src/entity/chunk.rs b/server/src/entity/chunk.rs deleted file mode 100644 index 22490f8c9..000000000 --- a/server/src/entity/chunk.rs +++ /dev/null @@ -1,446 +0,0 @@ -//! Maintains a list of entities which are in each -//! chunk, which allows for more efficient nearby -//! entity queries and packet broadcasting. - -use crate::chunk_logic::ChunkLoadEvent; -use crate::entity::{ - arrow, chicken, cow, donkey, horse, item, llama, mooshroom, pig, rabbit, sheep, squid, - EntityDestroyEvent, EntitySpawnEvent, PositionComponent, -}; -use crate::TickCount; -use feather_core::entity::EntityData; -use feather_core::world::ChunkPosition; -use hashbrown::{HashMap, HashSet}; -use shrev::EventChannel; -use specs::storage::ComponentEvent; -use specs::{ - BitSet, Entities, Entity, Join, LazyUpdate, Read, ReadStorage, ReaderId, System, World, - WorldExt, Write, -}; -use std::sync::atomic::{AtomicBool, Ordering}; - -/// Keeps track of which entities are in which chunk. -/// Also has a boolean for each chunk which indicates -/// whether its entities have been updated recently. -#[derive(Debug, Deref, DerefMut, Default)] -pub struct ChunkEntities(HashMap)>); - -lazy_static! { - static ref EMPTY_VEC: Vec = Vec::with_capacity(0); -} - -impl ChunkEntities { - /// Returns all entities in a given chunk. - pub fn entities_in_chunk(&self, chunk: ChunkPosition) -> &Vec { - if let Some((_, entities)) = self.0.get(&chunk) { - entities - } else { - &EMPTY_VEC - } - } - - /// Returns all entities in the chunk, in addition to - /// a boolean indicating whether the entities have been - /// updated since the last call to this function. - pub fn entities_in_chunk_and_modified(&self, chunk: ChunkPosition) -> (bool, &[Entity]) { - if let Some((dirty, entities)) = self.0.get(&chunk) { - let d = dirty.load(Ordering::SeqCst); - dirty.store(false, Ordering::SeqCst); - (d, entities) - } else { - (false, &[]) - } - } - - /// Adds an entity to a chunk. - pub fn add_to_chunk(&mut self, chunk: ChunkPosition, entity: Entity) { - self.0 - .entry(chunk) - .and_modify(|(dirty, vec)| { - dirty.store(true, Ordering::SeqCst); - vec.push(entity) - }) - .or_insert_with(|| (AtomicBool::new(true), vec![entity])); - } - - /// Removes an entity from a chunk. - /// - /// # Panics - /// May panic in some cases if the entity is not contained - /// within the given chunk. - pub fn remove_from_chunk(&mut self, chunk: ChunkPosition, entity: Entity) { - let (dirty, vec) = match self.0.get_mut(&chunk) { - Some(vec) => vec, - _ => return, - }; - - let (index, _) = match vec.iter().enumerate().find(|x| *x.1 == entity) { - Some(index) => index, - None => return, - }; - vec.swap_remove(index); - - dirty.store(true, Ordering::SeqCst); - - if vec.is_empty() { - self.0.remove(&chunk); - } - } - - /// Returns a vector of all entities in all chunks - /// within the given view distance of another chunk. - pub fn entites_within_view_distance( - &self, - chunk: ChunkPosition, - view_distance: u8, - ) -> HashSet { - let mut result = HashSet::new(); - - // 1 is subtracted from the view distance because of some odd - // client-side glitch (or maybe it's our fault?) where the last chunk within the view distance - // is not loaded correctly. - let view_distance = i32::from(view_distance) - 1; - - for x_offset in -view_distance..=view_distance { - for z_offset in -view_distance..=view_distance { - let chunk = ChunkPosition::new(chunk.x + x_offset, chunk.z + z_offset); - - result.extend(self.entities_in_chunk(chunk)); - } - } - - result - } -} - -/// System for updating the `ChunkEntities`. -/// -/// This system listens to `EntityMoveEvent`s, `EntitySpawnEvent`s, -/// and `EntityDestroyEvent`s. -#[derive(Default)] -pub struct ChunkEntityUpdateSystem { - dirty: BitSet, - move_reader: Option>, - spawn_reader: Option>, - destroy_reader: Option>, -} - -impl<'a> System<'a> for ChunkEntityUpdateSystem { - type SystemData = ( - ReadStorage<'a, PositionComponent>, - Write<'a, ChunkEntities>, - Read<'a, EventChannel>, - Read<'a, EventChannel>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, mut entity_chunks, spawn_events, destroy_events, entities) = data; - - self.dirty.clear(); - for event in positions.channel().read(self.move_reader.as_mut().unwrap()) { - if let ComponentEvent::Modified(id) = event { - self.dirty.add(*id); - } - } - - for (position, entity, _) in (&positions, &entities, &self.dirty).join() { - let new_pos = position.current.chunk_pos(); - let old_pos = position.previous.chunk_pos(); - - if new_pos != old_pos { - entity_chunks.remove_from_chunk(old_pos, entity); - entity_chunks.add_to_chunk(new_pos, entity); - } - } - - for event in spawn_events.read(self.spawn_reader.as_mut().unwrap()) { - if let Some(pos) = positions.get(event.entity) { - entity_chunks.add_to_chunk(pos.current.chunk_pos(), event.entity); - } - } - - for event in destroy_events.read(self.destroy_reader.as_mut().unwrap()) { - if let Some(pos) = positions.get(event.entity) { - entity_chunks.remove_from_chunk(pos.current.chunk_pos(), event.entity); - } - } - } - - fn setup(&mut self, world: &mut World) { - use specs::SystemData; - - Self::SystemData::setup(world); - - self.move_reader = Some( - world - .write_component::() - .register_reader(), - ); - self.destroy_reader = Some(world.fetch_mut::>().register_reader()); - self.spawn_reader = Some(world.fetch_mut::>().register_reader()); - } -} - -/// System for spawning entities inside newly-loaded chunks. -/// -/// This system listens to `ChunkLoadEvent`s. -#[derive(Default)] -pub struct EntityChunkLoadSystem { - reader: Option>, -} - -impl<'a> System<'a> for EntityChunkLoadSystem { - type SystemData = ( - Read<'a, EventChannel>, - Read<'a, LazyUpdate>, - Entities<'a>, - Read<'a, TickCount>, - ); - - #[allow(clippy::cognitive_complexity)] // Big match statement. Necessary - fn run(&mut self, data: Self::SystemData) { - let (load_events, lazy, entities, tick) = data; - - for event in load_events.read(self.reader.as_mut().unwrap()) { - for entity in &event.entities { - match entity { - EntityData::Item(item_data) => { - if item::create_from_data(&lazy, &entities, item_data, &tick).is_none() { - debug!("Error while loading item entity"); - } - } - EntityData::Arrow(arrow_data) => { - if arrow::create_from_data(&lazy, &entities, arrow_data).is_none() { - debug!("Error while loading arrow entity"); - } - } - EntityData::Cow(data) => { - if cow::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading cow entity") - } - } - EntityData::Pig(data) => { - if pig::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading pig entity") - } - } - EntityData::Chicken(data) => { - if chicken::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading chicken entity") - } - } - EntityData::Sheep(data) => { - if sheep::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading sheep entity") - } - } - EntityData::Horse(data) => { - if horse::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading horse entity") - } - } - EntityData::Llama(data) => { - if llama::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading llama entity") - } - } - EntityData::Mooshroom(data) => { - if mooshroom::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading mooshroom entity") - } - } - EntityData::Rabbit(data) => { - if rabbit::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading rabbit entity") - } - } - EntityData::Squid(data) => { - if squid::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading squid entity") - } - } - EntityData::Donkey(data) => { - if donkey::create_from_data(&lazy, &entities, data).is_none() { - debug!("Error while loading donkey entity") - } - } - // TODO: Spawn remaining entity types here. - EntityData::Unknown => { - trace!("Chunk {:?} contains an unknown entity type", event.pos); - } - } - } - } - } - - setup_impl!(reader); -} - -// Tests here cannot use the `testframework::add_entity` function -// because it automatically adds a ChunkEntities entry for the entity. -#[cfg(test)] -mod tests { - use super::*; - use crate::entity::{test, ArrowComponent, ItemComponent}; - use crate::testframework as t; - use feather_core::entity::{ArrowEntityData, ItemEntityData}; - use specs::{Builder, World, WorldExt}; - - #[test] - fn test_chunk_entities() { - let mut chunks = ChunkEntities::default(); - - let mut world = World::new(); - let entity = world.create_entity().build(); - - let pos = ChunkPosition::new(0, 0); - - chunks.add_to_chunk(pos, entity); - assert_eq!(chunks.entities_in_chunk(pos).as_slice(), &[entity]); - assert!(chunks.entities_in_chunk_and_modified(pos).0); - assert!(!chunks.entities_in_chunk_and_modified(pos).0); - } - - #[test] - fn test_new_entity() { - let (mut w, mut d) = t::builder() - .with(ChunkEntityUpdateSystem::default(), "") - .build(); - - let pos = position!(1.0, 64.0, 1003.5); - let entity = w - .create_entity() - .with(PositionComponent { - current: pos, - previous: pos, - }) - .build(); - - let event = EntitySpawnEvent { entity }; - t::trigger_event(&w, event); - - d.dispatch(&w); - w.maintain(); - - let chunk_entities = w.fetch::(); - assert_eq!( - chunk_entities.entities_in_chunk(pos.chunk_pos()).as_slice(), - &[entity] - ); - } - - #[test] - fn test_moved_entity() { - let (mut w, mut d) = t::builder() - .with(ChunkEntityUpdateSystem::default(), "") - .build(); - - let pos = position!(1.0, 64.0, -14.0); - let old_pos = position!(1.0, 64.0, -18.0); - - let entity = w - .create_entity() - .with(PositionComponent { - current: old_pos, - previous: old_pos, - }) - .build(); - - // Trigger flagged storage event. - w.write_component::() - .get_mut(entity) - .unwrap() - .current = pos; - - d.dispatch(&w); - w.maintain(); - - let chunk_entities = w.fetch::(); - assert!(chunk_entities - .entities_in_chunk(old_pos.chunk_pos()) - .is_empty()); - assert_eq!( - chunk_entities.entities_in_chunk(pos.chunk_pos()).as_slice(), - &[entity] - ); - } - - #[test] - fn test_destroyed_entity() { - let (mut w, mut d) = t::builder() - .with(ChunkEntityUpdateSystem::default(), "") - .build(); - - let pos = position!(100.0, -100.0, -100.0); - let entity = test::create(&mut w, pos).build(); - - let event = EntityDestroyEvent { entity }; - t::trigger_event(&w, event); - - d.dispatch(&w); - w.maintain(); - - let chunk_entities = w.fetch::(); - assert!(chunk_entities.entities_in_chunk(pos.chunk_pos()).is_empty()); - } - - #[test] - fn test_entities_within_view_distance() { - let mut chunk_entities = ChunkEntities::default(); - - let mut world = World::new(); - let entity1 = world.create_entity().build(); - let entity2 = world.create_entity().build(); - let entity3 = world.create_entity().build(); - let entity4 = world.create_entity().build(); - - let chunk1 = ChunkPosition::new(0, 0); - let chunk2 = ChunkPosition::new(0, 3); - let chunk3 = ChunkPosition::new(0, 4); - let chunk4 = ChunkPosition::new(-3, -3); - - chunk_entities.add_to_chunk(chunk1, entity1); - chunk_entities.add_to_chunk(chunk2, entity2); - chunk_entities.add_to_chunk(chunk3, entity3); - chunk_entities.add_to_chunk(chunk4, entity4); - - let view_distance = 4; - let entities = chunk_entities.entites_within_view_distance(chunk1, view_distance); - - assert!(entities.contains(&entity1)); - assert!(entities.contains(&entity2)); - assert!(!entities.contains(&entity3)); - assert!(entities.contains(&entity4)); - } - - #[test] - fn test_entities_loaded_in_chunk() { - let (mut w, mut d) = t::builder() - .with(EntityChunkLoadSystem::default(), "") - .build(); - - let entities = vec![ - EntityData::Item(ItemEntityData::default()), - EntityData::Arrow(ArrowEntityData::default()), - ]; - let pos = ChunkPosition::new(1, 2); - - let mut entity_spawn_reader = t::reader(&w); - let load_event = ChunkLoadEvent { pos, entities }; - t::trigger_event(&w, load_event); - - d.dispatch(&w); - w.maintain(); - d.dispatch(&w); - w.maintain(); - - // Confirm two entities were created: one arrow, one item - let mut events = t::triggered_events::(&w, &mut entity_spawn_reader); - - let first = events.remove(0).entity; - let second = events.remove(0).entity; - assert!(w.read_component::().contains(first)); - assert!(w.read_component::().contains(second)); - } -} diff --git a/server/src/entity/component.rs b/server/src/entity/component.rs deleted file mode 100644 index 35e2cf1b1..000000000 --- a/server/src/entity/component.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Various Specs components. - -use feather_core::entity::EntityData; -use feather_core::world::Position; -use feather_core::{Gamemode, Packet}; -use glm::DVec3; -use specs::storage::BTreeStorage; -use specs::{Component, Entity, FlaggedStorage, Join, System, VecStorage, World, WriteStorage}; -use uuid::Uuid; - -pub struct PlayerComponent { - pub profile_properties: Vec, - pub gamemode: Gamemode, -} - -impl Component for PlayerComponent { - type Storage = BTreeStorage; -} - -#[derive(Default, Debug, PartialEq, Clone, Copy)] -pub struct PositionComponent { - /// The current position of this entity. - pub current: Position, - /// The position of this entity on the previous - /// tick. At the end of each tick, `reset` should - /// be called. - pub previous: Position, -} - -impl PositionComponent { - /// Resets the current and previous position. - /// Should be called at the end of every tick. - pub fn reset(&mut self) { - self.previous = self.current; - } -} - -impl Component for PositionComponent { - type Storage = FlaggedStorage>; -} - -/// An entity's velocity, in blocks per tick. -/// -/// Entities without this component are assumed -/// to have a velocity of 0. -#[derive(Deref, DerefMut, Debug, PartialEq, Clone, Copy)] -pub struct VelocityComponent(pub DVec3); - -impl Component for VelocityComponent { - type Storage = FlaggedStorage>; -} - -impl Default for VelocityComponent { - fn default() -> Self { - Self(glm::vec3(0.0, 0.0, 0.0)) - } -} - -#[derive(Clone, Debug)] -pub struct NamedComponent { - pub display_name: String, - pub uuid: Uuid, -} - -impl Component for NamedComponent { - type Storage = BTreeStorage; -} - -pub trait PacketCreator: Fn(&World, Entity) -> Box + Send + Sync {} - -impl Box + Send + Sync> PacketCreator for F {} - -/// Component containing a closure which returns the packet -/// needed to spawn an entity. -/// -/// The closure requires world access because it may need to access -/// arbitrary components. -pub struct PacketCreatorComponent(pub &'static dyn PacketCreator); - -impl Component for PacketCreatorComponent { - type Storage = VecStorage; -} - -pub trait EntitySerializer: Fn(&World, Entity) -> EntityData + Send + Sync {} - -impl EntityData + Send + Sync> EntitySerializer for F {} - -/// Component containing a closure which returns the `EntityData` -/// for an entity. -/// -/// The closure requires world access because it may need to access -/// arbitrary components. -pub struct SerializerComponent(pub &'static dyn EntitySerializer); - -impl Component for SerializerComponent { - type Storage = VecStorage; -} - -/// System for resetting an entity's components -/// at the end of the tick. -pub struct ComponentResetSystem; - -impl<'a> System<'a> for ComponentResetSystem { - type SystemData = WriteStorage<'a, PositionComponent>; - - fn run(&mut self, mut positions: Self::SystemData) { - // Ensure that position update events are not triggered - // for this. See #81 - positions.set_event_emission(false); - - for position in (&mut positions).join() { - position.reset(); - } - - positions.set_event_emission(true); - } -} diff --git a/server/src/entity/destroy.rs b/server/src/entity/destroy.rs deleted file mode 100644 index 28aea7e9c..000000000 --- a/server/src/entity/destroy.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Module for broadcasting and handling entity destroy -//! events. - -use crate::util::Util; -use feather_core::network::packet::implementation::DestroyEntities; -use shrev::{EventChannel, ReaderId}; -use specs::SystemData; -use specs::{Entities, Entity, Read, System, World}; - -/// Event triggered when an entity -/// of any type is destroyed. -#[derive(Debug, Clone)] -pub struct EntityDestroyEvent { - /// Note that when this event is triggered, - /// the entity isn't actually removed from the world - /// yet. This allows systems to access the entity's - /// data before it is destroyed. - /// - /// `EntityDestroySystem` is responsible for removing - /// entities once the `EntityDestroyEvent` has been - /// handled by all readers. - pub entity: Entity, -} - -/// System for removing entities from the world when they -/// are destroyed. -#[derive(Default)] -pub struct EntityDestroySystem { - reader: Option>, -} - -impl<'a> System<'a> for EntityDestroySystem { - type SystemData = (Read<'a, EventChannel>, Entities<'a>); - - fn run(&mut self, data: Self::SystemData) { - let (events, entities) = data; - - for event in events.read(&mut self.reader.as_mut().unwrap()) { - let _ = entities.delete(event.entity); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} - -/// System for broadcasting when an entity is destroyed. -#[derive(Default)] -pub struct EntityDestroyBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for EntityDestroyBroadcastSystem { - type SystemData = (Read<'a, Util>, Read<'a, EventChannel>); - - fn run(&mut self, data: Self::SystemData) { - let (util, events) = data; - - for event in events.read(&mut self.reader.as_mut().unwrap()) { - let destroy_entities = DestroyEntities::new(vec![event.entity.id() as i32]); - - util.broadcast_entity_update(event.entity, destroy_entities, None); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} diff --git a/server/src/entity/impls/animal/chicken.rs b/server/src/entity/impls/animal/chicken.rs deleted file mode 100644 index a92e4b3d3..000000000 --- a/server/src/entity/impls/animal/chicken.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct ChickenComponent; - -impl Component for ChickenComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(ChickenComponent) - .with(PhysicsBuilder::for_living().bbox(0.4, 0.7, 0.4).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 7) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Chicken(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/cow.rs b/server/src/entity/impls/animal/cow.rs deleted file mode 100644 index e962c8daa..000000000 --- a/server/src/entity/impls/animal/cow.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct CowComponent; - -impl Component for CowComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(CowComponent) - .with(PhysicsBuilder::for_living().bbox(0.9, 1.4, 0.9).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 9) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Cow(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/donkey.rs b/server/src/entity/impls/animal/donkey.rs deleted file mode 100644 index c7918ffcb..000000000 --- a/server/src/entity/impls/animal/donkey.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct DonkeyComponent; - -impl Component for DonkeyComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(DonkeyComponent) - .with( - PhysicsBuilder::for_living() - .bbox(1.396_484_4, 1.6, 1.396_484_4) - .build(), - ) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 11) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Donkey(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/horse.rs b/server/src/entity/impls/animal/horse.rs deleted file mode 100644 index 7b5d6f82f..000000000 --- a/server/src/entity/impls/animal/horse.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct HorseComponent; - -impl Component for HorseComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(HorseComponent) - .with( - PhysicsBuilder::for_living() - .bbox(1.396_484_4, 1.6, 1.396_484_4) - .build(), - ) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 29) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Horse(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/llama.rs b/server/src/entity/impls/animal/llama.rs deleted file mode 100644 index 29f06e68a..000000000 --- a/server/src/entity/impls/animal/llama.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct LlamaComponent; - -impl Component for LlamaComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(LlamaComponent) - .with(PhysicsBuilder::for_living().bbox(0.9, 1.87, 0.9).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 36) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Llama(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/mod.rs b/server/src/entity/impls/animal/mod.rs deleted file mode 100644 index 89ede779b..000000000 --- a/server/src/entity/impls/animal/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Implementations for animals: cows, pigs, chickens, etc. - -pub mod chicken; -pub mod cow; -pub mod donkey; -pub mod horse; -pub mod llama; -pub mod mooshroom; -pub mod pig; -pub mod rabbit; -pub mod sheep; -pub mod squid; diff --git a/server/src/entity/impls/animal/mooshroom.rs b/server/src/entity/impls/animal/mooshroom.rs deleted file mode 100644 index 830a147bf..000000000 --- a/server/src/entity/impls/animal/mooshroom.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct MooshroomComponent; - -impl Component for MooshroomComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(MooshroomComponent) - .with(PhysicsBuilder::for_living().bbox(0.9, 1.4, 0.9).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 47) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Mooshroom(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/pig.rs b/server/src/entity/impls/animal/pig.rs deleted file mode 100644 index cfa88d29d..000000000 --- a/server/src/entity/impls/animal/pig.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct PigComponent; - -impl Component for PigComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(PigComponent) - .with(PhysicsBuilder::for_living().bbox(0.9, 0.9, 0.9).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 51) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Pig(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/rabbit.rs b/server/src/entity/impls/animal/rabbit.rs deleted file mode 100644 index b247c2f83..000000000 --- a/server/src/entity/impls/animal/rabbit.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct RabbitComponent; - -impl Component for RabbitComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(RabbitComponent) - .with(PhysicsBuilder::for_living().bbox(0.4, 0.5, 0.4).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 56) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Rabbit(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/sheep.rs b/server/src/entity/impls/animal/sheep.rs deleted file mode 100644 index 934456885..000000000 --- a/server/src/entity/impls/animal/sheep.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct SheepComponent; - -impl Component for SheepComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(SheepComponent) - .with(PhysicsBuilder::for_living().bbox(0.9, 1.3, 0.9).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 58) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Sheep(AnimalData { base }) -} diff --git a/server/src/entity/impls/animal/squid.rs b/server/src/entity/impls/animal/squid.rs deleted file mode 100644 index 76bd309d7..000000000 --- a/server/src/entity/impls/animal/squid.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::entity::{ - base_data, create_mob_packet, PacketCreatorComponent, PositionComponent, SerializerComponent, - VelocityComponent, -}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use feather_core::entity::{AnimalData, EntityData}; -use feather_core::Packet; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Builder, Component, Entity, LazyUpdate, NullStorage, World}; - -#[derive(Default)] -pub struct SquidComponent; - -impl Component for SquidComponent { - type Storage = NullStorage; -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &'a EntitiesRes) -> LazyBuilder<'a> { - lazy.spawn_entity(entities) - .with(SquidComponent) - .with(PhysicsBuilder::for_living().bbox(0.8, 0.8, 0.8).build()) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &AnimalData, -) -> Option { - let position = data.base.read_position()?; - let velocity = data.base.read_velocity()?; - - Some( - create(lazy, entities) - .with(PositionComponent { - current: position, - previous: position, - }) - .with(VelocityComponent(velocity)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - create_mob_packet(world, entity, 70) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let base = base_data(world, entity); - EntityData::Squid(AnimalData { base }) -} diff --git a/server/src/entity/impls/arrow.rs b/server/src/entity/impls/arrow.rs deleted file mode 100644 index a9754583f..000000000 --- a/server/src/entity/impls/arrow.rs +++ /dev/null @@ -1,168 +0,0 @@ -use shrev::EventChannel; -use specs::{ - Builder, Component, Entities, Entity, LazyUpdate, NullStorage, Read, ReaderId, System, World, - WorldExt, -}; - -use feather_core::packet::SpawnObject; -use feather_core::{Item, Packet, Position}; - -use crate::entity::component::{PacketCreatorComponent, SerializerComponent}; -use crate::entity::metadata::Metadata; -use crate::entity::movement::degrees_to_stops; -use crate::entity::{PositionComponent, VelocityComponent}; -use crate::lazy::LazyUpdateExt; -use crate::physics::PhysicsBuilder; -use crate::player::PLAYER_EYE_HEIGHT; -use crate::util::protocol_velocity; -use feather_core::entity::{ArrowEntityData, BaseEntityData, EntityData}; -use specs::world::{EntitiesRes, LazyBuilder}; -use uuid::Uuid; - -/// Component for arrow entities. -#[derive(Default)] -pub struct ArrowComponent; - -impl Component for ArrowComponent { - type Storage = NullStorage; -} - -/// Event triggered when arrow is shot. -#[derive(Debug, Clone)] -pub struct ShootArrowEvent { - pub arrow_type: Item, - pub shooter: Option, - pub position: Position, - pub critical: bool, -} - -#[derive(Default)] -pub struct ShootArrowSystem { - reader: Option>, -} - -impl<'a> System<'a> for ShootArrowSystem { - type SystemData = ( - Read<'a, LazyUpdate>, - Read<'a, EventChannel>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (lazy, shoot_arrow_events, entities) = data; - - for event in shoot_arrow_events.read(self.reader.as_mut().unwrap()) { - let mut pos = event.position - + glm::vec3(0.0, PLAYER_EYE_HEIGHT, 0.0) - + event.position.direction() * 1.5; - pos.on_ground = false; - - // TODO: Scale velocity based on power - let velocity = pos.direction(); - - // TODO: shooter - - create(&lazy, &entities, false) - .with(PositionComponent { - current: pos, - previous: pos, - }) - .with(VelocityComponent(velocity)) - .build(); - } - } - - setup_impl!(reader); -} - -pub fn create<'a>(lazy: &'a LazyUpdate, entities: &EntitiesRes, critical: bool) -> LazyBuilder<'a> { - let meta = { - let mut meta_arrow = crate::entity::metadata::Arrow::default(); - let mask = if critical { - crate::entity::metadata::ArrowBitMask::CRITICAL - } else { - crate::entity::metadata::ArrowBitMask::default() - }; - meta_arrow.set_arrow_bit_mask(mask.bits()); - // meta_arrow.set_shooter(shooter); TODO - Metadata::Arrow(meta_arrow) - }; - - lazy.spawn_entity(entities) - .with(ArrowComponent) - .with( - PhysicsBuilder::new() - .bbox(0.5, 0.5, 0.5) - .gravity(-0.05) - .drag(0.99) - .slip_multiplier(0.0) - .build(), - ) - .with(meta) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &ArrowEntityData, -) -> Option { - let pos = data.entity.read_position()?; - let vel = data.entity.read_velocity()?; - - let critical = match data.critical { - 0 => false, - _ => true, - }; - - // TODO: load other attributes - - Some( - create(lazy, entities, critical) - .with(PositionComponent { - current: pos, - previous: pos, - }) - .with(VelocityComponent(vel)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - let positions = world.read_component::(); - let velocities = world.read_component::(); - - let position = positions.get(entity).unwrap().current; - let (velocity_x, velocity_y, velocity_z) = protocol_velocity(velocities.get(entity).unwrap().0); - - let packet = SpawnObject { - entity_id: entity.id() as i32, - object_uuid: Uuid::new_v4(), // TODO - ty: 60, - x: position.x, - y: position.y, - z: position.z, - pitch: degrees_to_stops(position.pitch), - yaw: degrees_to_stops(position.yaw), - data: 1, // TODO: Shooter entity ID - velocity_x, - velocity_y, - velocity_z, - }; - - Box::new(packet) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let positions = world.read_component::(); - let velocities = world.read_component::(); - - EntityData::Arrow(ArrowEntityData { - entity: BaseEntityData::new( - positions.get(entity).unwrap().current, - velocities.get(entity).unwrap().0, - ), - critical: 0, // TODO - }) -} diff --git a/server/src/entity/impls/falling_block.rs b/server/src/entity/impls/falling_block.rs deleted file mode 100644 index 18c59a2cf..000000000 --- a/server/src/entity/impls/falling_block.rs +++ /dev/null @@ -1,142 +0,0 @@ -use shrev::ReaderId; -use specs::shrev::EventChannel; -use specs::{ - Builder, Component, DenseVecStorage, Entity, LazyUpdate, Read, ReadStorage, System, World, - WorldExt, Write, -}; - -use feather_blocks::{Block, BlockExt}; -use feather_core::packet::SpawnObject; -use feather_core::world::ChunkMap; - -use crate::blocks::{BlockUpdateCause, BlockUpdateEvent}; -use crate::entity::component::PacketCreatorComponent; -use crate::entity::metadata::Metadata; -use crate::entity::movement::degrees_to_stops; -use crate::entity::{EntityDestroyEvent, PositionComponent, VelocityComponent}; -use crate::lazy::LazyUpdateExt; -use crate::physics::{EntityPhysicsLandEvent, PhysicsBuilder}; -use crate::util::protocol_velocity; -use feather_core::{Packet, Position}; -use specs::world::{EntitiesRes, LazyBuilder}; -use uuid::Uuid; - -/// Component for falling block entities. -pub struct FallingBlockComponent { - pub block: Block, -} - -impl Default for FallingBlockComponent { - fn default() -> Self { - FallingBlockComponent { - block: Block::Stone, - } - } -} - -impl Component for FallingBlockComponent { - type Storage = DenseVecStorage; -} - -/// This system listens to `EntityPhysicsLandEvent`s. -#[derive(Default)] -pub struct FallingBlockLandSystem { - reader: Option>, -} - -/// System for handling when a falling block lands -/// on the ground, destroying the entity and setting the block. -impl<'a> System<'a> for FallingBlockLandSystem { - type SystemData = ( - Read<'a, EventChannel>, - ReadStorage<'a, FallingBlockComponent>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Write<'a, ChunkMap>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (events, falling_blocks, mut destroy_events, mut block_updates, mut chunk_map) = data; - - // Process events - for event in events.read(&mut self.reader.as_mut().unwrap()) { - let entity = event.entity; - - let falling_block = match falling_blocks.get(entity) { - Some(block) => block, - None => continue, // Not a falling block - }; - - let destroy_event = EntityDestroyEvent { entity }; - destroy_events.single_write(destroy_event); - - let pos = event.pos.block_pos(); - let old_block = chunk_map.block_at(pos).unwrap(); - chunk_map.set_block_at(pos, falling_block.block).unwrap(); - - let update_event = BlockUpdateEvent { - cause: BlockUpdateCause::FallingBlock, - pos, - old_block, - new_block: falling_block.block, - }; - - block_updates.single_write(update_event); - } - } - - setup_impl!(reader); -} - -pub fn create<'a>( - lazy: &'a LazyUpdate, - entities: &EntitiesRes, - block: Block, - position: Position, -) -> LazyBuilder<'a> { - let meta = { - let mut meta_falling_block = crate::entity::metadata::FallingBlock::default(); - meta_falling_block.set_spawn_position(position.block_pos()); - Metadata::FallingBlock(meta_falling_block) - }; - - lazy.spawn_entity(entities) - .with(FallingBlockComponent { block }) - .with( - PhysicsBuilder::new() - .gravity(-0.04) - .drag(0.98) - .bbox(0.98, 0.98, 0.98) - .build(), - ) - .with(meta) - .with(PacketCreatorComponent(&create_packet)) - //.with(SerializerComponent(&serialize)) TODO -} - -fn create_packet(world: &World, entity: Entity) -> Box { - let blocks = world.read_component::(); - let positions = world.read_component::(); - let velocities = world.read_component::(); - - let block = blocks.get(entity).unwrap().block.native_state_id(); - let position = positions.get(entity).unwrap().current; - let (velocity_x, velocity_y, velocity_z) = protocol_velocity(velocities.get(entity).unwrap().0); - - let packet = SpawnObject { - entity_id: entity.id() as i32, - object_uuid: Uuid::new_v4(), - ty: 70, - x: position.x, - y: position.y, - z: position.z, - pitch: degrees_to_stops(position.pitch), - yaw: degrees_to_stops(position.yaw), - data: i32::from(block), - velocity_x, - velocity_y, - velocity_z, - }; - - Box::new(packet) -} diff --git a/server/src/entity/impls/item.rs b/server/src/entity/impls/item.rs deleted file mode 100644 index edc54b881..000000000 --- a/server/src/entity/impls/item.rs +++ /dev/null @@ -1,582 +0,0 @@ -//! Logic for working with item entities. -use crate::entity::metadata::{self, Metadata}; -use crate::entity::{ - ChunkEntities, EntityDestroyEvent, PlayerComponent, PositionComponent, VelocityComponent, -}; -use crate::physics::{nearby_entities, PhysicsBuilder}; -use crate::player::{ - InventoryComponent, InventoryUpdateEvent, PlayerItemDropEvent, PLAYER_EYE_HEIGHT, -}; -use crate::util::{protocol_velocity, Util}; -use crate::{TickCount, TPS}; -use feather_core::network::packet::implementation::CollectItem; -use feather_core::{Item, ItemStack, Packet}; -use rand::Rng; -use shrev::EventChannel; -use smallvec::SmallVec; -use specs::storage::ComponentEvent; -use specs::{ - BitSet, Builder, Component, DenseVecStorage, Entities, Entity, Join, LazyUpdate, Read, - ReadStorage, ReaderId, System, SystemData, World, WorldExt, Write, WriteStorage, -}; - -use crate::entity::component::{PacketCreatorComponent, SerializerComponent}; -use crate::entity::movement::degrees_to_stops; -use crate::lazy::LazyUpdateExt; -use feather_core::entity::{BaseEntityData, EntityData, ItemData, ItemEntityData}; -use feather_core::packet::SpawnObject; -use specs::world::{EntitiesRes, LazyBuilder}; -use uuid::Uuid; - -/// Component for item entities. -pub struct ItemComponent { - /// The tick at which this item is collectable - /// by a player. - pub collectable_at: u64, - /// This item's stack. - pub stack: ItemStack, -} - -impl Component for ItemComponent { - type Storage = DenseVecStorage; -} - -/// System for spawning an item entity when -/// an item is dropped. -/// -/// This system listens to `PlayerItemDropEvent`s. -#[derive(Default)] -pub struct ItemSpawnSystem { - reader: Option>, -} - -impl<'a> System<'a> for ItemSpawnSystem { - type SystemData = ( - ReadStorage<'a, PositionComponent>, - Read<'a, LazyUpdate>, - Entities<'a>, - Read<'a, EventChannel>, - Read<'a, TickCount>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, lazy, entities, item_drop_events, tick) = data; - - let mut rng = rand::thread_rng(); - - for event in item_drop_events.read(self.reader.as_mut().unwrap()) { - // Spawn item entity. - - // Position is player's eye height minus 0.3 - let mut pos = { - let player_pos = positions.get(event.player).unwrap().current - + glm::vec3(0.0, PLAYER_EYE_HEIGHT, 0.0); - player_pos - glm::vec3(0.0f64, 0.3, 0.0) - }; - - pos.on_ground = false; - - // This velocity calculation was sourced from Glowstone's - // work. See https://github.com/GlowstoneMC/Glowstone/blob/dev/src/main/java/net/glowstone/entity/GlowHumanEntity.java - // (method drop(ItemStack stack)) for their code. - let velocity = { - let mut vel = pos.direction() * 0.3; - let rand_offset = 0.02; - - let x = rng.gen_range(0.0, rand_offset) - rand_offset / 2.0; - let y = rng.gen_range(0.0, 0.12); - let z = rng.gen_range(0.0, rand_offset) - rand_offset / 2.0; - - vel += glm::vec3(x, y, z); - - vel - }; - - create(&lazy, &entities, event.stack.clone(), tick.0 + TPS) - .with(PositionComponent { - current: pos, - previous: pos, - }) - .with(VelocityComponent(velocity)) - .build(); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some(world.fetch_mut::>().register_reader()); - } -} - -/// System for merging item entities of the same -/// type. -#[derive(Default)] -pub struct ItemMergeSystem { - dirty: BitSet, - reader: Option>, -} - -impl<'a> System<'a> for ItemMergeSystem { - type SystemData = ( - ReadStorage<'a, PositionComponent>, - ReadStorage<'a, ItemComponent>, - WriteStorage<'a, Metadata>, - Write<'a, EventChannel>, - Read<'a, ChunkEntities>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, item_markers, mut metadatas, mut destroy_events, chunk_entities, entities) = - data; - - self.dirty.clear(); - - for event in positions.channel().read(self.reader.as_mut().unwrap()) { - match event { - ComponentEvent::Modified(id) | ComponentEvent::Inserted(id) => { - self.dirty.add(*id); - } - _ => (), - } - } - - let mut metadatas_to_update: SmallVec<[(Entity, Metadata); 2]> = smallvec![]; - // Used to not destroy both entities - let mut destroyed: SmallVec<[Entity; 2]> = smallvec![]; - - for (position, entity, _, _) in (&positions, &entities, &item_markers, &self.dirty).join() { - if !entities.is_alive(entity) { - continue; - } - - if destroyed.iter().any(|x| *x == entity) { - continue; - } - - let mut stack = item_stack_from_meta(metadatas.get(entity).unwrap()); - - // Find nearby entities and check if they are of the same item - // type. If so, merge the two item stacks. - let nearby = nearby_entities( - &chunk_entities, - &positions, - position.current, - glm::vec3(1.0, 0.5, 1.0), - ); - - for other in nearby { - // Skip entity if it's dead. - if !entities.is_alive(other) { - continue; - } - - if other == entity { - continue; - } - - // Skip if it's not an item. - if item_markers.get(other).is_none() { - continue; - } - - let other_stack = item_stack_from_meta(metadatas.get(other).unwrap()); - - if other_stack.ty != stack.ty { - continue; - } - - // Merge two stacks. - // This works by deleting `other` and adding - // together the amounts of the two item stacks. - entities.delete(other).unwrap(); - - let event = EntityDestroyEvent { entity: other }; - destroy_events.single_write(event); - - // TODO this could overflow... - stack.amount += other_stack.amount; - - metadatas_to_update.push((entity, item_meta(stack.clone()))); - destroyed.push(other); - } - } - - metadatas_to_update.into_iter().for_each(|(entity, meta)| { - metadatas.insert(entity, meta).unwrap(); - }); - } - - flagged_setup_impl!(PositionComponent, reader); -} - -/// System for collecting items when a player comes -/// near them. -#[derive(Default)] -pub struct ItemCollectSystem { - dirty: BitSet, - reader: Option>, -} - -impl<'a> System<'a> for ItemCollectSystem { - type SystemData = ( - WriteStorage<'a, InventoryComponent>, - ReadStorage<'a, PositionComponent>, - ReadStorage<'a, PlayerComponent>, - ReadStorage<'a, ItemComponent>, - WriteStorage<'a, Metadata>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Read<'a, ChunkEntities>, - Read<'a, Util>, - Read<'a, TickCount>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut inventories, - positions, - players, - items, - mut metadatas, - mut inventory_events, - mut destroy_events, - chunk_entities, - util, - tick, - entities, - ) = data; - - self.dirty.clear(); - - read_flagged_events!(positions, self.reader, self.dirty); - - // For each player who has moved this tick, - // look for nearby items. - // We need to keep track of which items - // have already been collected to avoid - // having the same item being collected - // by two players at once; this would - // cause dupe exploits. - let mut collected_items: SmallVec<[Entity; 4]> = smallvec![]; - - for (position, inventory, player, _, _) in ( - &positions, - &mut inventories, - &entities, - &players, - &self.dirty, - ) - .join() - { - let nearby = nearby_entities( - &chunk_entities, - &positions, - position.current, - glm::vec3(1.0, 0.5, 1.0), - ); - - for other in nearby { - // If it's not an item, skip. - let item = continue_if_none!(items.get(other)); - - // Check if the item can be picked up yet. - if item.collectable_at > tick.0 { - continue; - } - - // If the item has already been collected, don't try it. - if collected_items.iter().any(|x| *x == other) { - continue; - } - - // Attempt to collect the item. - let mut stack = item_stack_from_meta(metadatas.get(other).unwrap()); - let (affected_slots, amount_left) = inventory.collect_item(stack.clone()); - - // Broadcast Collect Item packet, which gives an animation. - let packet = CollectItem { - collected: other.id() as i32, - collector: player.id() as i32, - count: i32::from(stack.amount - amount_left), - }; - util.broadcast_entity_update(player, packet, None); - - if amount_left == 0 { - entities.delete(other).unwrap(); - collected_items.push(other); - - let event = EntityDestroyEvent { entity: other }; - destroy_events.single_write(event); - } else { - stack.amount = amount_left; - let meta = item_meta(stack); - metadatas.insert(other, meta).unwrap(); - } - - // Trigger inventory update event. - let event = InventoryUpdateEvent { - slots: affected_slots, - player, - }; - inventory_events.single_write(event); - } - } - } - - flagged_setup_impl!(PositionComponent, reader); -} - -pub fn create<'a>( - lazy: &'a LazyUpdate, - entities: &EntitiesRes, - stack: ItemStack, - collectable_at: u64, -) -> LazyBuilder<'a> { - let meta = { - let mut meta_item = crate::entity::metadata::Item::default(); - meta_item.set_item(Some(stack.clone())); - Metadata::Item(meta_item) - }; - - lazy.spawn_entity(entities) - .with(ItemComponent { - stack, - collectable_at, - }) - .with( - PhysicsBuilder::new() - .bbox(0.25, 0.25, 0.25) - .gravity(-0.04) - .drag(0.98) - .build(), - ) - .with(VelocityComponent::default()) - .with(meta) - .with(PacketCreatorComponent(&create_packet)) - .with(SerializerComponent(&serialize)) -} - -pub fn create_from_data( - lazy: &LazyUpdate, - entities: &EntitiesRes, - data: &ItemEntityData, - tick: &TickCount, -) -> Option { - let pos = data.entity.read_position()?; - let vel = data.entity.read_velocity()?; - - let stack = ItemStack::new(Item::from_identifier(&data.item.item)?, data.item.count); - - let collectable_at = data.pickup_delay as u64 + tick.0; - - Some( - create(lazy, entities, stack, collectable_at) - .with(PositionComponent { - current: pos, - previous: pos, - }) - .with(VelocityComponent(vel)) - .build(), - ) -} - -fn create_packet(world: &World, entity: Entity) -> Box { - let positions = world.read_component::(); - let velocities = world.read_component::(); - - let position = positions.get(entity).unwrap().current; - let (velocity_x, velocity_y, velocity_z) = protocol_velocity(velocities.get(entity).unwrap().0); - - let packet = SpawnObject { - entity_id: entity.id() as i32, - object_uuid: Uuid::new_v4(), - ty: 2, // Type 2 for item stack - x: position.x, - y: position.y, - z: position.z, - pitch: degrees_to_stops(position.pitch), - yaw: degrees_to_stops(position.yaw), - data: 1, // Has velocity - velocity_x, - velocity_y, - velocity_z, - }; - - Box::new(packet) -} - -fn serialize(world: &World, entity: Entity) -> EntityData { - let positions = world.read_component::(); - let velocities = world.read_component::(); - let items = world.read_component::(); - - let item = items.get(entity).unwrap(); - let position = positions.get(entity).unwrap(); - let velocity = velocities.get(entity).unwrap(); - - EntityData::Item(ItemEntityData { - entity: BaseEntityData::new(position.current, velocity.0), - age: 0, // TODO - pickup_delay: 0, // TODO - item: ItemData { - item: item.stack.ty.identifier().to_string(), - count: item.stack.amount, - }, - }) -} - -pub fn item_stack_from_meta(meta: &Metadata) -> ItemStack { - match meta { - Metadata::Item(item) => item.item().unwrap(), - _ => panic!(), - } -} - -pub fn item_meta(stack: ItemStack) -> Metadata { - let mut item = metadata::Item::default(); - item.set_item(Some(stack)); - Metadata::Item(item) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::entity::{ChunkEntityUpdateSystem, EntitySpawnEvent}; - use crate::testframework as t; - use feather_core::inventory::SLOT_HOTBAR_OFFSET; - use feather_core::network::cast_packet; - use feather_core::{Item, ItemStack, PacketType}; - use specs::WorldExt; - - #[test] - fn test_item_spawn_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - t::set_entity_pos(&w, player.entity, position!(0.0, 1.0, 0.0)); - - let stack = ItemStack::new(Item::AcaciaBoat, 4); - - let mut entity_spawn_reader = t::reader(&w); - - let event = PlayerItemDropEvent { - slot: None, - stack, - player: player.entity, - }; - t::trigger_event(&w, event); - - d.dispatch(&w); - w.maintain(); - - // Confirm event was triggered - let events = t::triggered_events::(&w, &mut entity_spawn_reader); - assert_eq!(events.len(), 1); - let first = events.first().unwrap(); - let entity = first.entity; - - // Check position - let pos = t::entity_pos(&w, entity); - assert_float_eq!(pos.x, 0.0); - assert_float_eq!(pos.z, 0.0); - - // Confirm that velocity was created - let _vel = t::entity_vel(&w, entity).unwrap(); - } - - #[test] - fn test_item_merge_system() { - let (mut w, mut d) = t::builder() - .with_dep(ItemMergeSystem::default(), "item_merge", &[]) - .build(); - - let item1 = create( - &w.fetch(), - &w.fetch(), - ItemStack::new(Item::EnderPearl, 4), - 0, - ) - .with(PositionComponent::default()) - .build(); - let item2 = create( - &w.fetch(), - &w.fetch(), - ItemStack::new(Item::EnderPearl, 7), - 0, - ) - .with(PositionComponent::default()) - .build(); - - let mut updater = ChunkEntityUpdateSystem::default(); - updater.setup(&mut w); - - w.maintain(); - - // Update chunk entities so `nearby_entities` works - specs::RunNow::run_now(&mut updater, &w); - - d.dispatch(&w); - w.maintain(); - - assert!(!w.is_alive(item2)); - assert!(w.is_alive(item1)); - - let metadatas = w.read_component::(); - let metadata = metadatas.get(item1).unwrap(); - - let stack = item_stack_from_meta(&metadata); - assert_eq!(stack.ty, Item::EnderPearl); - assert_eq!(stack.amount, 11); - } - - #[test] - fn test_item_collect_system() { - let (mut w, mut d) = t::builder() - .with_dep(ItemCollectSystem::default(), "", &[]) - .build(); - - let player = t::add_player(&mut w); - let stack = ItemStack::new(Item::String, 4); - let item = create(&w.fetch(), &w.fetch(), stack.clone(), 0) - .with(PositionComponent::default()) - .build(); - - let mut destroy_reader = t::reader(&w); - - // Allow item to be collected - w.fetch_mut::().0 = 0; - - let mut updater = ChunkEntityUpdateSystem::default(); - updater.setup(&mut w); - - w.maintain(); - - // Update chunk entities so `nearby_entities` works - - specs::RunNow::run_now(&mut updater, &w); - - d.dispatch(&w); - w.maintain(); - - let destroy_events = t::triggered_events::(&w, &mut destroy_reader); - let first = destroy_events.first().unwrap(); - assert_eq!(first.entity, item); - - assert!(!w.is_alive(item)); - - let inventories = w.read_component::(); - let inventory = inventories.get(player.entity).unwrap(); - - assert_eq!(inventory.item_at(SLOT_HOTBAR_OFFSET), Some(&stack)); - - let packet = t::assert_packet_received(&player, PacketType::CollectItem); - let packet = cast_packet::(&*packet); - - assert_eq!(packet.collector, player.entity.id() as i32); - assert_eq!(packet.collected, item.id() as i32); - assert_eq!(packet.count, 4); - } -} diff --git a/server/src/entity/impls/mod.rs b/server/src/entity/impls/mod.rs deleted file mode 100644 index f46a2d94d..000000000 --- a/server/src/entity/impls/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Entity implementations. -//! -//! Every entity implementation is expected to define -//! the following functions: -//! -//! * `create(&LazyUpdate, &EntitiesRes) -> LazyBuilder`. When a system spawns an entity -//! of a known type, it should call this function on the `LazyBuilder` -//! returned by `LazyUpdate::spawn_entity` to apply components, such as markers, metadata, -//! `SerializerComponent`, and `SpawnPacketComponent`. This function may -//! take parameters. This function should not apply generic components, -//! such as position and velocity; the callee is responsible for this. -//! * `create_from_data(&LazyUpdate, &EntitiesRes, &{Entity}Data) -> Option`. Spawns an -//! entity loaded from the given entity data. If the entity creation failed, `None` is returned. -//! -//! These functions should be invoked in the form `name::function`, e.g. -//! `arrow::create` or `item::create_from_data`. -//! -//! Entity implementations should also define systems related to the entity: for -//! example, most entities will have an update system which updates an entity -//! on each tick. - -pub mod arrow; -pub mod falling_block; -pub mod item; - -mod animal; -pub use animal::*; - -use crate::entity::{ - degrees_to_stops, metadata::EMPTY_METADATA, Metadata, NamedComponent, PositionComponent, - VelocityComponent, -}; -use crate::util::protocol_velocity; -use feather_core::entity::BaseEntityData; -use feather_core::network::packet::implementation::SpawnMob; -use feather_core::Packet; -use specs::{Entity, World, WorldExt}; -use uuid::Uuid; - -#[cfg(test)] -pub mod test; - -/// Returns a `Spawn Mob` packet with the given entity type ID. -pub fn create_mob_packet(world: &World, entity: Entity, type_id: i32) -> Box { - let entity_id = entity.id() as i32; - let entity_uuid = world - .read_component::() - .get(entity) - .map(|named| named.uuid) - .unwrap_or_else(Uuid::new_v4); - - let positions = world.read_component::(); - let position = positions.get(entity).copied().unwrap_or_default(); - let velocities = world.read_component::(); - let velocity = velocities.get(entity).copied().unwrap_or_default(); - - let (velocity_x, velocity_y, velocity_z) = protocol_velocity(velocity.0); - - let metadatas = world.read_component::(); - let metadata = metadatas.get(entity).unwrap_or(&EMPTY_METADATA); - - let packet = SpawnMob { - entity_id, - entity_uuid, - ty: type_id, - x: position.current.x, - y: position.current.y, - z: position.current.z, - yaw: degrees_to_stops(position.current.yaw), - pitch: degrees_to_stops(position.current.pitch), - head_pitch: degrees_to_stops(position.current.pitch), // FIXME: is this correct? - velocity_x, - velocity_y, - velocity_z, - meta: metadata.to_full_raw_metadata(), - }; - - Box::new(packet) -} - -/// Creates a `BaseEntityData` for the given entity. -pub fn base_data(world: &World, entity: Entity) -> BaseEntityData { - let position = world - .read_component::() - .get(entity) - .copied() - .unwrap_or_default(); - let velocity = world - .read_component::() - .get(entity) - .copied() - .unwrap_or_default(); - - BaseEntityData::new(position.current, velocity.0) -} diff --git a/server/src/entity/impls/test.rs b/server/src/entity/impls/test.rs deleted file mode 100644 index 21ef64b9f..000000000 --- a/server/src/entity/impls/test.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! A fake entity implementation for unit tests. - -use crate::entity::{EntitySpawnEvent, PositionComponent, VelocityComponent}; -use feather_core::Position; -use shrev::EventChannel; -use specs::{Builder, EntityBuilder, World, WorldExt}; - -pub fn create(world: &mut World, pos: Position) -> EntityBuilder { - let builder = world - .create_entity() - .with(PositionComponent { - current: pos, - previous: pos, - }) - .with(VelocityComponent(glm::vec3(0.0, 0.0, 0.0))); - builder - .world - .fetch_mut::>() - .single_write(EntitySpawnEvent { - entity: builder.entity, - }); - builder -} diff --git a/server/src/entity/metadata.rs b/server/src/entity/metadata.rs deleted file mode 100644 index 82f3d1c3f..000000000 --- a/server/src/entity/metadata.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Definition for entity metadata enum. - -#![allow(clippy::too_many_arguments)] // TODO: builder patterm - -use crate::util::Util; -use feather_core::packet::PacketEntityMetadata; -use feather_core::{BlockPosition, EntityMetadata, Slot}; -use specs::storage::ComponentEvent; -use specs::{ - BitSet, Component, Entities, FlaggedStorage, Join, Read, ReaderId, System, VecStorage, - WriteStorage, -}; -use uuid::Uuid; - -type OptUuid = Option; - -bitflags! { - pub struct EntityBitMask: u8 { - const ON_FIRE = 0x01; - const CROUCHED = 0x02; - const SPRITING = 0x08; - const SWIMMING = 0x10; - const INVISIBLE = 0x20; - const GLOWING_EFFECT = 0x40; - const FLYING_WITH_ELYTRA = 0x80; - } -} - -bitflags! { - #[derive(Default)] - pub struct ArrowBitMask: u8 { - const CRITICAL = 0x01; - const NO_CLIP = 0x02; - } -} - -lazy_static! { - pub static ref EMPTY_METADATA: Metadata = { Metadata::Entity(Entity::default()) }; -} - -entity_metadata! { - Metadata, - Entity { - bit_mask: u8() = 0, - air: VarInt() = 1, - silent: bool() = 4, - no_gravity: bool() = 5, - }, - Item: Entity { - item: Slot() = 6, - }, - Living: Entity { - hand_states: u8() = 6, - health: f32(1.0) = 7, - potion_effect_color: VarInt() = 8, - potion_effect_ambient: bool() = 9, - arrows: VarInt() = 10, - }, - Player: Living { - additional_hearts: f32() = 11, - score: VarInt() = 12, - displayed_skin_parts: u8() = 13, - main_hand: u8(1) = 14, - }, - Arrow: Entity { - arrow_bit_mask: u8() = 6, - shooter: OptUuid() = 7, - }, - TippedArrow: Arrow { - color: VarInt() = 8, - }, - FallingBlock: Entity { - spawn_position: BlockPosition() = 6, - }, -} - -impl Component for Metadata { - type Storage = FlaggedStorage>; -} - -/// System for broadcasting entity metadata updates. -#[derive(Default)] -pub struct MetadataBroadcastSystem { - dirty: BitSet, - reader: Option>, -} - -impl<'a> System<'a> for MetadataBroadcastSystem { - type SystemData = (WriteStorage<'a, Metadata>, Read<'a, Util>, Entities<'a>); - - fn run(&mut self, data: Self::SystemData) { - let (mut metadatas, util, entities) = data; - - self.dirty.clear(); - - read_flagged_events!(metadatas, self.reader, self.dirty); - - // Ensure that metadata update events are not - // triggered for this mutation of the storage. - metadatas.set_event_emission(false); - - // Go through updated metadata and broadcast changes. - for (metadata, entity, _) in (&mut metadatas, &entities, &self.dirty).join() { - let packet = PacketEntityMetadata { - entity_id: entity.id() as i32, - metadata: metadata.to_raw_metadata(), - }; - - util.broadcast_entity_update(entity, packet, None); - } - - metadatas.set_event_emission(true); - } - - flagged_setup_impl!(Metadata, reader); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::entity::test; - use crate::testframework as t; - use feather_core::entitymeta::MetaEntry; - use feather_core::network::cast_packet; - use feather_core::PacketType; - use specs::{Builder, WorldExt}; - - #[test] - fn test_basic() { - let mut meta = Metadata::Entity(Entity::new( - (EntityBitMask::ON_FIRE | EntityBitMask::CROUCHED).bits(), - 0, - false, - false, - )); - - let raw = meta.to_raw_metadata(); - - assert_eq!(raw.get(0), Some(MetaEntry::Byte(0b0000_0011))); - assert_eq!(raw.get(5), Some(MetaEntry::Boolean(false))); - assert_eq!(raw.get(6), None); - } - - #[test] - fn test_inheritance() { - let _meta = Metadata::Item(Item::new( - (EntityBitMask::ON_FIRE).bits(), - 0, - false, - false, - None, - )); - } - - #[test] - fn test_metadata_update_system() { - let (mut w, mut d) = t::builder() - .with(MetadataBroadcastSystem::default(), "") - .build(); - - let entity = test::create(&mut w, position!(0.0, 0.0, 0.0)).build(); - - // Insert metadata - { - let mut metadatas = w.write_component::(); - metadatas - .insert(entity, Metadata::Entity(Entity::default())) - .unwrap(); - } - - let player = t::add_player(&mut w); - - d.dispatch(&w); - w.maintain(); - - // Ensure that packet was sent - let packet = t::assert_packet_received(&player, PacketType::EntityMetadata); - let packet = cast_packet::(&*packet); - - assert_eq!(packet.entity_id, entity.id() as i32); - } -} diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index 9aa2ee75c..727852dce 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -1,103 +1,155 @@ -//! Provides several useful components, including `EntityComponent` -//! and `PlayerComponent`. In the future, will also -//! provide entity-specific components and systems. - -mod broadcast; -mod chunk; -mod component; -mod destroy; -mod impls; -pub mod metadata; -mod movement; -mod save; - -pub use impls::*; - -use crate::systems::{ - BLOCK_FALLING_LANDING, CHUNK_CROSS, CHUNK_ENTITIES_LOAD, CHUNK_ENTITIES_UPDATE, CHUNK_SAVE, - ENTITY_DESTROY, ENTITY_DESTROY_BROADCAST, ENTITY_METADATA_BROADCAST, ENTITY_MOVE_BROADCAST, - ENTITY_PHYSICS, ENTITY_SPAWN_BROADCAST, ENTITY_VELOCITY_BROADCAST, ITEM_COLLECT, ITEM_MERGE, - ITEM_SPAWN, JOIN_BROADCAST, SHOOT_ARROW, -}; -pub use arrow::{ArrowComponent, ShootArrowEvent}; -pub use broadcast::send_entity_to_player; -pub use broadcast::{EntitySendEvent, EntitySpawnEvent}; -pub use chunk::ChunkEntities; -pub use chunk::ChunkEntityUpdateSystem; -pub use component::{ - NamedComponent, PacketCreatorComponent, PlayerComponent, PositionComponent, - SerializerComponent, VelocityComponent, -}; -pub use destroy::EntityDestroyEvent; -pub use falling_block::FallingBlockComponent; -pub use item::ItemComponent; -pub use metadata::{EntityBitMask, Metadata}; -pub use movement::{degrees_to_stops, LastKnownPositionComponent}; - -pub use save::save_chunks; - -use crate::entity::arrow::ShootArrowSystem; -use crate::entity::chunk::EntityChunkLoadSystem; -use crate::entity::destroy::EntityDestroyBroadcastSystem; -use crate::entity::falling_block::FallingBlockLandSystem; -use crate::entity::item::ItemCollectSystem; -use crate::entity::metadata::MetadataBroadcastSystem; -use crate::entity::save::ChunkSaveSystem; -use broadcast::EntityBroadcastSystem; -use component::ComponentResetSystem; -use destroy::EntityDestroySystem; -use item::{ItemMergeSystem, ItemSpawnSystem}; -use movement::{EntityMoveBroadcastSystem, EntityVelocityBroadcastSystem}; -use specs::DispatcherBuilder; - -pub fn init_logic(dispatcher: &mut DispatcherBuilder) { - dispatcher.add(ItemCollectSystem::default(), ITEM_COLLECT, &[]); +//! Dealing with entities, including associated components and events. + +use crate::lazy::EntityBuilder; +use crate::state::State; +use feather_core::{Packet, Position}; +use legion::prelude::Entity; +use legion::query::{Read, Write}; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicI32, Ordering}; +use tonks::{EntityAccessor, PreparedWorld, Query}; +use uuid::Uuid; + +/// ID of an entity. This value is generally unique. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct EntityId(pub i32); + +/// Entity ID counter, used to create new entity IDs. +pub static ENTITY_ID_COUNTER: AtomicI32 = AtomicI32::new(0); + +/// Event triggered when an entity is created. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EntityCreateEvent { + pub entity: Entity, +} + +/// Event triggered when an entity is removed. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EntityDeleteEvent { + pub entity: Entity, + pub position: Option, + pub id: EntityId, + pub uuid: Uuid, +} + +/// Event triggered when an entity moves. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EntityMoveEvent { + /// Entity which moved. + pub entity: Entity, +} + +/// The velocity of an entity. +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct Velocity(pub glm::DVec3); + +impl Default for Velocity { + fn default() -> Self { + Self(glm::vec3(0.0, 0.0, 0.0)) + } +} + +impl Deref for Velocity { + type Target = glm::DVec3; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Velocity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// The display name of the entity. +/// +/// Note that unnamed entities do not have this component. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Name(pub String); + +/// Position of an entity on the last tick. +/// +/// This is updated by `position_reset` system. +#[derive(Debug, Clone, Copy)] +pub struct PreviousPosition(pub Position); + +pub trait PacketCreatorFn: + Fn(&EntityAccessor, &PreparedWorld) -> Box + Send + Sync + 'static +{ +} +impl PacketCreatorFn for F where + F: Fn(&EntityAccessor, &PreparedWorld) -> Box + Send + Sync + 'static +{ } -pub fn init_handlers(dispatcher: &mut DispatcherBuilder) { - dispatcher.add( - ChunkEntityUpdateSystem::default(), - CHUNK_ENTITIES_UPDATE, - &[], - ); - dispatcher.add(EntityChunkLoadSystem::default(), CHUNK_ENTITIES_LOAD, &[]); - dispatcher.add(EntityDestroySystem::default(), ENTITY_DESTROY, &[]); - dispatcher.add(ItemSpawnSystem::default(), ITEM_SPAWN, &[]); - dispatcher.add(ItemMergeSystem::default(), ITEM_MERGE, &[]); - dispatcher.add( - MetadataBroadcastSystem::default(), - ENTITY_METADATA_BROADCAST, - &[], - ); - dispatcher.add(ShootArrowSystem::default(), SHOOT_ARROW, &[]); - dispatcher.add(ChunkSaveSystem::default(), CHUNK_SAVE, &[]); +/// Component which defines a function returning a packet to send +/// to clients when the entity comes within range. This packet +/// spawns the entity on the client. +pub struct SpawnPacketCreator(pub &'static dyn PacketCreatorFn); + +impl SpawnPacketCreator { + /// Returns the packet to send to clients when the entity is to be + /// sent to the client. + pub fn get(&self, accessor: &EntityAccessor, world: &PreparedWorld) -> Box { + let f = self.0; + + f(accessor, world) + } +} + +/// Component which defines a function returning a packet to send +/// to _all_ clients when the entity is created or the client joins. +/// This packet is sent before that returned by `SpawnPacketCreator`, +/// and it differs in that the packet is broadcasted globally +/// rather than to nearby clients. +/// +/// Another difference is that the packet from `SpawnPacketCreator` is not sent +/// to its own entity, while that from `CreationPacketCreator` is. +/// +/// An example of a use case for this packet is the `PlayerInfo` packet +/// sent when a player joins—it is sent to all players, not just those +/// that are able to see the player. +pub struct CreationPacketCreator(pub &'static dyn PacketCreatorFn); + +impl CreationPacketCreator { + /// Returns the packet to send to clients when the entity is created. + pub fn get(&self, accessor: &EntityAccessor, world: &PreparedWorld) -> Box { + let f = self.0; + + f(accessor, world) + } +} + +#[event_handler] +pub fn position_reset( + events: &[EntityMoveEvent], + _query: &mut Query<(Read, Write)>, + world: &mut PreparedWorld, +) { + events.iter().for_each(|event| { + let pos = *world.get_component::(event.entity).unwrap(); + let mut prev_pos = world + .get_component_mut::(event.entity) + .unwrap(); + + prev_pos.0 = pos; + }); } -pub fn init_broadcast(dispatcher: &mut DispatcherBuilder) { - dispatcher.add( - EntityMoveBroadcastSystem::default(), - ENTITY_MOVE_BROADCAST, - &[], - ); - dispatcher.add( - EntityBroadcastSystem::default(), - ENTITY_SPAWN_BROADCAST, - &[JOIN_BROADCAST, CHUNK_CROSS], - ); - dispatcher.add( - EntityVelocityBroadcastSystem::default(), - ENTITY_VELOCITY_BROADCAST, - &[], - ); - dispatcher.add( - EntityDestroyBroadcastSystem::default(), - ENTITY_DESTROY_BROADCAST, - &[], - ); - dispatcher.add( - FallingBlockLandSystem::default(), - BLOCK_FALLING_LANDING, - &[ENTITY_PHYSICS], - ); - dispatcher.add_thread_local(ComponentResetSystem); +/// Inserts the base components for an entity into an `EntityBuilder`. +/// +/// This currently includes: +/// * Position +/// * Velocity (0) +pub fn base(state: &State, position: Position) -> EntityBuilder { + let id = ENTITY_ID_COUNTER.fetch_add(1, Ordering::Relaxed); + state + .create_entity() + .with_component(EntityId(id)) + .with_component(position) + .with_component(PreviousPosition(position)) + .with_component(Velocity::default()) + .with_exec(|_, scheduler, entity| scheduler.trigger(EntityCreateEvent { entity })) } diff --git a/server/src/entity/movement.rs b/server/src/entity/movement.rs deleted file mode 100644 index 34a4d25b9..000000000 --- a/server/src/entity/movement.rs +++ /dev/null @@ -1,264 +0,0 @@ -use specs::storage::ComponentEvent; -use specs::{ - BitSet, Component, DenseVecStorage, Entities, Entity, Join, Read, ReadStorage, ReaderId, - System, WriteStorage, -}; - -use feather_core::network::packet::implementation::{ - EntityHeadLook, EntityLook, EntityLookAndRelativeMove, EntityRelativeMove, EntityVelocity, -}; -use feather_core::world::Position; - -use crate::chunk_logic::ChunkHolders; -use crate::entity::{PositionComponent, VelocityComponent}; -use crate::network::{send_packet_boxed_to_player, NetworkComponent}; -use crate::util::{protocol_velocity, Util}; -use feather_core::Packet; -use hashbrown::HashMap; -use smallvec::SmallVec; - -/// Component which stores the last known position for any given entity -/// for a player. -/// -/// This is used to ensure position remains synced across clients, since -/// relative movement packets are used. -#[derive(Default, Debug)] -pub struct LastKnownPositionComponent(pub HashMap); - -impl Component for LastKnownPositionComponent { - type Storage = DenseVecStorage; -} - -/// System for broadcasting when an entity moves. -#[derive(Default)] -pub struct EntityMoveBroadcastSystem { - dirty: BitSet, - reader: Option>, - held: BitSet, -} - -impl<'a> System<'a> for EntityMoveBroadcastSystem { - type SystemData = ( - ReadStorage<'a, PositionComponent>, - WriteStorage<'a, LastKnownPositionComponent>, - ReadStorage<'a, NetworkComponent>, - Read<'a, ChunkHolders>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, mut last_positions, networks, chunk_holders, entities) = data; - - self.dirty.clear(); - - for event in positions.channel().read(&mut self.reader.as_mut().unwrap()) { - match event { - ComponentEvent::Modified(index) | ComponentEvent::Inserted(index) => { - self.dirty.add(*index); - } - _ => (), - } - } - - for (position, entity, _) in (&positions, &entities, &self.dirty).join() { - // Populate `self.held` with chunk holders for this entity's chunk - for entity in chunk_holders - .holders_for(position.current.chunk_pos()) - .unwrap_or(&[]) - { - self.held.add(entity.id()); - } - - // For each player which can see this entity's chunk, send a movement update packet. - for (network, last_positions, _) in (&networks, &mut last_positions, &self.held).join() - { - let last_known_position = match last_positions.0.get(&entity) { - Some(pos) => pos, - None => continue, // Player hasn't yet known this entity - }; - - if let Some(packets) = - packet_for_movement_update(entity, *last_known_position, position.current) - { - packets - .into_iter() - .for_each(|packet| send_packet_boxed_to_player(network, packet)); - } - - last_positions.0.insert(entity, position.current); - } - - self.held.clear(); - } - } - - flagged_setup_impl!(PositionComponent, reader); -} - -/// Returns the packet needed to notify a client -/// of a position update, from the old position to the new one. -#[allow(clippy::float_cmp)] -pub fn packet_for_movement_update( - entity: Entity, - old_pos: Position, - new_pos: Position, -) -> Option; 2]>> { - if old_pos == new_pos { - return None; - } - - let mut packets = smallvec![]; - - let has_moved = old_pos.x != new_pos.x || old_pos.y != new_pos.y || old_pos.z != new_pos.z; - let has_looked = old_pos.pitch != new_pos.pitch || old_pos.yaw != new_pos.yaw; - - if has_moved { - let (rx, ry, rz) = calculate_relative_move(old_pos, new_pos); - - if (rx == 0 && ry == 0 && rz == 0) && !has_looked { - // Because of floating point errors, - // the physics system may trigger an - // event when the distance moved is minuscule, - // which causes jittering on the client. - // Don't send the packet if it has no effect. - return None; - } - - if has_looked { - let packet: Box = Box::new(EntityLookAndRelativeMove::new( - entity.id() as i32, - rx, - ry, - rz, - degrees_to_stops(new_pos.yaw), - degrees_to_stops(new_pos.pitch), - new_pos.on_ground, - )); - packets.push(packet); - } else { - let packet: Box = Box::new(EntityRelativeMove::new( - entity.id() as i32, - rx, - ry, - rz, - new_pos.on_ground, - )); - packets.push(packet); - } - } else { - let packet: Box = Box::new(EntityLook::new( - entity.id() as i32, - degrees_to_stops(new_pos.yaw), - degrees_to_stops(new_pos.pitch), - new_pos.on_ground, - )); - packets.push(packet); - } - - // Entity Head Look also needs to be sent if the entity turned its head - if has_looked { - let packet: Box = Box::new(EntityHeadLook::new( - entity.id() as i32, - degrees_to_stops(new_pos.yaw), - )); - packets.push(packet); - } - - Some(packets) -} - -/// System for broadcasting when an entity's velocity -/// is updated. -#[derive(Default)] -pub struct EntityVelocityBroadcastSystem { - dirty: BitSet, - reader: Option>, -} - -impl<'a> System<'a> for EntityVelocityBroadcastSystem { - type SystemData = ( - ReadStorage<'a, VelocityComponent>, - Read<'a, Util>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (velocities, util, entities) = data; - - self.dirty.clear(); - - for event in velocities.channel().read(self.reader.as_mut().unwrap()) { - match event { - ComponentEvent::Modified(index) | ComponentEvent::Inserted(index) => { - self.dirty.add(*index); - } - _ => (), - } - } - - for (velocity, entity, _) in (&velocities, &entities, &self.dirty).join() { - let (velocity_x, velocity_y, velocity_z) = protocol_velocity(velocity.0); - let packet = EntityVelocity { - entity_id: entity.id() as i32, - velocity_x, - velocity_y, - velocity_z, - }; - - util.broadcast_entity_update(entity, packet, Some(entity)); - } - } - - flagged_setup_impl!(VelocityComponent, reader); -} - -/// Calculates the relative move fields -/// as used in the Entity Relative Move packets. -pub fn calculate_relative_move(old: Position, current: Position) -> (i16, i16, i16) { - let x = ((current.x * 32.0 - old.x * 32.0) * 128.0) as i16; - let y = ((current.y * 32.0 - old.y * 32.0) * 128.0) as i16; - let z = ((current.z * 32.0 - old.z * 32.0) * 128.0) as i16; - (x, y, z) -} - -pub fn degrees_to_stops(degs: f32) -> u8 { - ((degs / 360.0) * 256.0) as u8 -} - -#[cfg(test)] -mod tests { - use specs::{Builder, WorldExt}; - - use feather_core::network::cast_packet; - use feather_core::network::packet::PacketType; - - use crate::entity::test; - use crate::testframework as t; - - use super::*; - - #[test] - fn test_velocity_broadcast_system() { - let (mut w, mut d) = t::builder() - .with(EntityVelocityBroadcastSystem::default(), "") - .build(); - - let player = t::add_player(&mut w); - - let entity = test::create(&mut w, position!(0.0, 0.0, 0.0)).build(); - - w.write_component::() - .insert(entity, VelocityComponent(glm::vec3(0.0, 0.0, 0.0))) - .unwrap(); - - d.dispatch(&w); - w.maintain(); - - let packet = t::assert_packet_received(&player, PacketType::EntityVelocity); - let packet = cast_packet::(&*packet); - assert_eq!(packet.entity_id, entity.id() as i32); - assert_eq!(packet.velocity_x, 0); - assert_eq!(packet.velocity_y, 0); - assert_eq!(packet.velocity_z, 0); - } -} diff --git a/server/src/entity/save.rs b/server/src/entity/save.rs deleted file mode 100644 index c1d5cde5a..000000000 --- a/server/src/entity/save.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! Saving of entity data (and chunk data along with it). - -use crate::chunk_logic; -use crate::chunk_logic::{ChunkUnloadEvent, ChunkWorkerHandle}; -use crate::config::Config; -use crate::entity::{ChunkEntities, SerializerComponent}; -use feather_core::world::ChunkMap; -use rayon::prelude::*; -use shrev::{EventChannel, ReaderId}; -use specs::{Entity, LazyUpdate, Read, ReadExpect, System, WorldExt, Write}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Instant; - -/// System to save chunk and entity data upon a chunk unload -/// and periodically. -/// -/// This system listens to `ChunkUnloadEvent`s. -#[derive(Default)] -pub struct ChunkSaveSystem { - reader: Option>, -} - -/// Previous time at which chunks were saved. -pub struct PreviousSaveTime(Instant); - -impl Default for PreviousSaveTime { - fn default() -> Self { - Self(Instant::now()) - } -} - -impl<'a> System<'a> for ChunkSaveSystem { - type SystemData = ( - Write<'a, PreviousSaveTime>, - Write<'a, ChunkMap>, - Read<'a, ChunkEntities>, - Read<'a, EventChannel>, - Read<'a, Arc>, - Read<'a, LazyUpdate>, - ReadExpect<'a, ChunkWorkerHandle>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut prev_save_time, - mut chunk_map, - chunk_entities, - unload_events, - config, - lazy, - worker_handle, - ) = data; - - for event in unload_events.read(self.reader.as_mut().unwrap()) { - let entities = vec![]; // TODO - chunk_logic::save_chunk(&worker_handle, Arc::clone(&event.chunk), entities); - } - - if prev_save_time.0.elapsed() >= config.world.save_interval { - // Save chunks - save_chunks(&mut chunk_map, &chunk_entities, &lazy); - prev_save_time.0 = Instant::now(); - } - } - - setup_impl!(reader); -} - -/// Saves all modified chunks. -/// -/// The saves themselves are performed lazily and asynchronously. -/// -/// Returns the number of chunks queued for saving. -pub fn save_chunks( - chunk_map: &mut ChunkMap, - chunk_entities: &ChunkEntities, - lazy: &LazyUpdate, -) -> u32 { - let count = AtomicUsize::new(0); - chunk_map - .chunks_mut() - .par_iter_mut() - .map(|(_, chunk)| { - let (dirty, entities) = chunk_entities.entities_in_chunk_and_modified(chunk.position()); - (chunk, entities, dirty) - }) - .for_each(|(chunk, entities, dirty)| { - // If all of the following are true, don't save the chunk: - // * The chunk has not been modified since the last save. - // * The entities in the chunk are empty (if they weren't, it is likely they were modified) - // * The entities in the chunk haven't changed. - if !chunk.check_modified() && (entities.is_empty() && !dirty) { - return; - } - - // World access is required for entity serialization, - // so we perform the saving itself asynchronously. - let chunk = Arc::new(chunk.clone()); - let entities: Vec = entities.to_vec(); - lazy.exec(move |world| { - // Compute entity data. - let entity_data = entities - .into_iter() - .filter_map(|entity| { - let serializers = world.read_component::(); - let serializer = match serializers.get(entity) { - Some(serializer) => serializer, - None => return None, // Entity not serialized - }; - - let serialize = serializer.0; - Some(serialize(world, entity)) - }) - .collect(); - - let handle = world.fetch::(); - chunk_logic::save_chunk(&handle, chunk, entity_data); - }); - - count.fetch_add(1, Ordering::Release); - }); - - let count = count.load(Ordering::Acquire); - debug!("Saving {} chunks", count); - count as u32 -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{chunkworker, testframework as t}; - use failure::_core::time::Duration; - use feather_core::{Chunk, ChunkPosition}; - - #[test] - fn test_chunk_unload() { - let (mut world, mut dispatcher) = t::builder().with(ChunkSaveSystem::default(), "").build(); - - let (tx, rx) = crossbeam::unbounded(); - let (_tx2, rx2) = crossbeam::unbounded(); - world.insert(ChunkWorkerHandle { - sender: tx, - receiver: rx2, - }); - - let event = ChunkUnloadEvent { - chunk: Arc::new(Chunk::default()), - }; - - t::trigger_event(&world, event); - - dispatcher.dispatch(&world); - - let msg = rx.try_recv().unwrap(); - - match msg { - chunkworker::Request::SaveChunk(chunk, entities) => { - assert_eq!(chunk.position(), ChunkPosition::new(0, 0)); - assert!(entities.is_empty()); // TODO - } - _ => panic!(), - } - } - - #[test] - fn test_periodic() { - let (mut world, mut dispatcher) = t::builder().with(ChunkSaveSystem::default(), "").build(); - - let (tx, rx) = crossbeam::unbounded(); - let (_tx2, rx2) = crossbeam::unbounded(); - world.insert(ChunkWorkerHandle { - sender: tx, - receiver: rx2, - }); - - let last_save_time = PreviousSaveTime(Instant::now() - Duration::from_secs(120)); - world.insert(last_save_time); - - let pos = ChunkPosition::new(0, 0); - world - .fetch_mut::() - .set_chunk_at(pos, Chunk::new(pos)); - - dispatcher.dispatch(&world); - world.maintain(); - - let msg = rx.try_recv().unwrap(); - - match msg { - chunkworker::Request::SaveChunk(chunk, entities) => { - assert_eq!(chunk.position(), pos); - assert!(entities.is_empty()); // TODO - } - _ => panic!(), - } - } -} diff --git a/server/src/io/initialhandler.rs b/server/src/io/initial_handler.rs similarity index 97% rename from server/src/io/initialhandler.rs rename to server/src/io/initial_handler.rs index 6c44f689d..897c33e8c 100644 --- a/server/src/io/initialhandler.rs +++ b/server/src/io/initial_handler.rs @@ -176,12 +176,12 @@ impl InitialHandler { async fn _handle_packet(ih: &mut InitialHandler, packet: Box) -> Result<(), Error> { // Find packet type and forward to correct function match packet.ty() { - PacketType::Handshake => handle_handshake(ih, cast_packet::(&*packet))?, - PacketType::Request => handle_request(ih, cast_packet::(&*packet))?, - PacketType::Ping => handle_ping(ih, cast_packet::(&*packet))?, - PacketType::LoginStart => handle_login_start(ih, cast_packet::(&*packet))?, + PacketType::Handshake => handle_handshake(ih, &cast_packet::(packet))?, + PacketType::Request => handle_request(ih, &cast_packet::(packet))?, + PacketType::Ping => handle_ping(ih, &cast_packet::(packet))?, + PacketType::LoginStart => handle_login_start(ih, &cast_packet::(packet))?, PacketType::EncryptionResponse => { - handle_encryption_response(ih, cast_packet::(&*packet)).await? + handle_encryption_response(ih, &cast_packet::(packet)).await? } ty => return Err(Error::InvalidPacket(ty, ih.stage)), } diff --git a/server/src/io/mod.rs b/server/src/io/mod.rs index 106f04480..fd506d6d8 100644 --- a/server/src/io/mod.rs +++ b/server/src/io/mod.rs @@ -1,11 +1,13 @@ use crate::config::Config; use crate::PlayerCount; use feather_core::network::packet::Packet; +use feather_core::player_data::PlayerData; +use feather_core::Position; use std::net::SocketAddr; use std::sync::Arc; use uuid::Uuid; -mod initialhandler; +mod initial_handler; mod listener; mod worker; @@ -28,11 +30,14 @@ pub struct NewClientInfo { pub username: String, pub profile: Vec, pub uuid: Uuid, + pub data: PlayerData, + pub position: Position, pub sender: futures::channel::mpsc::UnboundedSender, pub receiver: crossbeam::Receiver, } +#[derive(Resource)] pub struct NetworkIoManager { pub receiver: crossbeam::Receiver, /// Used for testing @@ -67,15 +72,9 @@ impl NetworkIoManager { } } -impl Default for NetworkIoManager { - fn default() -> Self { - panic!("Don't try this"); - } -} - /// Initializes certain static variables. pub fn init() { - lazy_static::initialize(&initialhandler::RSA_KEY); + lazy_static::initialize(&initial_handler::RSA_KEY); } async fn run_listener( diff --git a/server/src/io/worker.rs b/server/src/io/worker.rs index a312f6b39..546038598 100644 --- a/server/src/io/worker.rs +++ b/server/src/io/worker.rs @@ -7,19 +7,28 @@ //! to the worker for any given client. use crate::config::Config; -use crate::io::initialhandler::{Action, InitialHandler}; +use crate::io::initial_handler::{Action, InitialHandler}; use crate::io::{ListenerToServerMessage, NewClientInfo, ServerToWorkerMessage}; use crate::PlayerCount; use feather_core::network::codec::MinecraftCodec; use feather_core::network::packet::PacketDirection; +use feather_core::player_data::PlayerData; use futures::{select, StreamExt}; use futures::{FutureExt, SinkExt}; use std::net::SocketAddr; +use std::path::Path; use std::sync::Arc; use std::time::Duration; use tokio::codec::Framed; use tokio::net::TcpStream; use tokio::timer::Timeout; +use uuid::Uuid; + +#[derive(Debug, Fail)] +pub enum Error { + #[fail(display = "failed to read player data")] + PlayerData, +} /// Runs a worker task for the given client. pub async fn run_worker( @@ -66,7 +75,11 @@ async fn _run_worker( let mut framed = Framed::new(stream, codec); - let mut initial_handler = Some(InitialHandler::new(config, player_count, server_icon)); + let mut initial_handler = Some(InitialHandler::new( + Arc::clone(&config), + player_count, + server_icon, + )); let (tx_server_to_worker, mut rx_server_to_worker) = futures::channel::mpsc::unbounded(); let mut rx_worker_to_server = Some(rx_worker_to_server); @@ -119,6 +132,8 @@ async fn _run_worker( } Action::SetStage(stage) => framed.codec_mut().set_stage(stage), Action::JoinGame(res) => { + // let data = load_player_data(&config, res.uuid).await?; + let data = PlayerData::default(); let info = NewClientInfo { ip, username: res.username, @@ -126,6 +141,12 @@ async fn _run_worker( uuid: res.uuid, sender: tx_server_to_worker.clone(), receiver: rx_worker_to_server.take().unwrap(), + /*position: data + .entity + .read_position() + .ok_or_else(|| Error::PlayerData)?,*/ + position: position!(0.0, 80.0, 0.0), + data, }; global_sender .send(ListenerToServerMessage::NewClient(info))?; @@ -144,3 +165,7 @@ async fn _run_worker( } } } + +async fn load_player_data(config: &Config, uuid: Uuid) -> Result { + feather_core::player_data::load_player_data(Path::new(&config.world.name), uuid).await +} diff --git a/server/src/join.rs b/server/src/join.rs new file mode 100644 index 000000000..f6ed07d35 --- /dev/null +++ b/server/src/join.rs @@ -0,0 +1,100 @@ +//! After chunks are sent to a client, we complete the login sequence +//! by sending Spawn Position, Player Position and Look, and inventory, +//! among others. This is handled by the event handler `join`. + +use crate::entity::EntityId; +use crate::network::Network; +use crate::player::PlayerJoinEvent; +use crate::state::State; +use crate::view::ChunkSendEvent; +use feather_core::network::packet::implementation::{ + JoinGame, PlayerPositionAndLookClientbound, SpawnPosition, +}; +use feather_core::{BlockPosition, Gamemode, Position}; +use legion::query::{Read, Write}; +use parking_lot::RwLock; +use rayon::prelude::*; +use tonks::{PreparedWorld, Query}; + +/// Component indicating whether a player has completed the join sequence. +#[derive(Default, Debug)] +pub struct Joined(pub bool); + +/// System to run the join sequence. To determine when a player is ready to join, +/// we wait for the chunk that the player is in to be sent—this appears to work +/// well with the client. +#[event_handler] +fn join( + events: &[ChunkSendEvent], + _query: &mut Query<(Write, Read, Read)>, + world: &mut PreparedWorld, + state: &State, +) { + let world = RwLock::new(world); + events.par_iter().for_each(|event| { + let pos = { + let world = world.read(); + + let pos = world.get_component::(event.player).unwrap(); + let joined = world.get_component::(event.player).unwrap(); + + if pos.chunk_pos() != event.chunk || joined.0 { + return; + } + + *pos + }; + + // Run the join sequence. TODO: inventory. + world + .write() + .get_component_mut::(event.player) + .unwrap() + .0 = true; + + let world = world.read(); + let network = world.get_component::(event.player).unwrap(); + + let packet = SpawnPosition { + location: BlockPosition::new( + state.level.spawn_x, + state.level.spawn_y, + state.level.spawn_z, + ), + }; + network.send(packet); + + let packet = PlayerPositionAndLookClientbound { + x: pos.x, + y: pos.y, + z: pos.z, + yaw: pos.yaw, + pitch: pos.pitch, + flags: 0, + teleport_id: 0, + }; + network.send(packet); + }); +} + +#[event_handler] +fn send_join_game( + event: &PlayerJoinEvent, + _query: &mut Query<(Read, Read)>, + world: &mut PreparedWorld, +) { + let network = world.get_component::(event.player).unwrap(); + let id = world.get_component::(event.player).unwrap(); + + // TODO + let packet = JoinGame { + entity_id: id.0, + gamemode: Gamemode::Creative.get_id(), + dimension: 0, + difficulty: 0, + max_players: 0, + level_type: "default".to_string(), + reduced_debug_info: false, + }; + network.send(packet); +} diff --git a/server/src/joinhandler.rs b/server/src/joinhandler.rs deleted file mode 100644 index ad38803cb..000000000 --- a/server/src/joinhandler.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! The join handler, in contrast to the initial handler, -//! takes over after the login sequence has completed. -//! It's responsible for asyncrhonously loading the player's -//! data (inventory, chunks, etc.) and then sending the necessary -//! packets to join the player. After completion, the component is -//! removed. - -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use shrev::EventChannel; -use specs::{ - Component, Entities, Entity, HashMapStorage, Join, LazyUpdate, Read, ReadExpect, ReadStorage, - System, Write, WriteStorage, -}; - -use feather_core::level::LevelData; -use feather_core::network::packet::implementation::{ - JoinGame, PlayerPositionAndLookClientbound, SpawnPosition, -}; -use feather_core::world::{BlockPosition, ChunkMap, ChunkPosition}; -use feather_core::{Difficulty, Dimension}; - -use crate::chunk_logic::{ChunkHolderComponent, ChunkHolders, ChunkWorkerHandle}; -use crate::config::Config; -use crate::entity::{EntitySpawnEvent, PlayerComponent, PositionComponent}; -use crate::network::NetworkComponent; -use crate::player::{ChunkPendingComponent, InventoryUpdateEvent, LoadedChunksComponent}; -use crate::PlayerCount; - -#[derive(Default)] -pub struct JoinHandlerComponent { - stage: Stage, -} - -impl JoinHandlerComponent { - pub fn new() -> Self { - Self { - stage: Stage::Initial, - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum Stage { - Initial, - AwaitChunkSends, -} - -impl Default for Stage { - fn default() -> Self { - Stage::Initial - } -} - -impl Component for JoinHandlerComponent { - type Storage = HashMapStorage; -} - -/// Event which is triggered when a player -/// completes the join process (i.e. when -/// all chunks have been sent). -pub struct PlayerJoinEvent { - pub player: Entity, -} - -/// System for join handling. -pub struct JoinHandlerSystem; - -impl<'a> System<'a> for JoinHandlerSystem { - type SystemData = ( - WriteStorage<'a, JoinHandlerComponent>, - ReadStorage<'a, NetworkComponent>, - ReadStorage<'a, ChunkPendingComponent>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - ReadExpect<'a, ChunkWorkerHandle>, - Entities<'a>, - Read<'a, LazyUpdate>, - Read<'a, Arc>, - Read<'a, Arc>, - Read<'a, ChunkMap>, - Write<'a, ChunkHolders>, - Read<'a, LevelData>, - WriteStorage<'a, ChunkHolderComponent>, - WriteStorage<'a, LoadedChunksComponent>, - ReadStorage<'a, PlayerComponent>, - ReadStorage<'a, PositionComponent>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut joincomps, - netcomps, - pending_chunks, - mut join_events, - mut spawn_events, - mut inv_events, - worker_handle, - entities, - lazy, - config, - player_count, - chunk_map, - mut holders, - level, - mut holder_comps, - mut loaded_chunks_comps, - playercomps, - positions, - ) = data; - - let mut to_remove = vec![]; - - for (player, net, join_handler, pending_chunks) in - (&entities, &netcomps, &mut joincomps, &pending_chunks).join() - { - match join_handler.stage { - Stage::Initial => { - let playercomp = playercomps.get(player).unwrap(); - - let level_type = &level.generator_name; - - // Send Join Game, then queue chunks for loading + sending. - let join_game = JoinGame::new( - player.id() as i32, - playercomp.gamemode.get_id(), - Dimension::Overwold.get_id(), - Difficulty::Medium.get_id(), - 0, // Max players - not used - level_type.to_string(), - false, // Reduced debug info - ); - crate::network::send_packet_to_player(net, join_game); - - let mut holder_comp = ChunkHolderComponent::new(); - let mut loaded_chunks_comp = LoadedChunksComponent::default(); - - let player_pos = positions.get(player).unwrap().current.chunk_pos(); - - // Offsets from the origin to center view distance on - let chunk_offset_x = player_pos.x; - let chunk_offset_z = player_pos.z; - - // Queue chunks for sending. - let view_distance = i32::from(config.server.view_distance); - let mut chunks = Vec::with_capacity((view_distance * view_distance) as usize); - for x in -view_distance..=view_distance { - for z in -view_distance..=view_distance { - let chunk = ChunkPosition::new(x + chunk_offset_x, z + chunk_offset_z); - chunks.push(chunk); - } - } - - // Sort chunks so that closest chunks are sent first. - chunks.sort_unstable_by(|a, b| { - a.manhattan_distance(player_pos) - .cmp(&b.manhattan_distance(player_pos)) - }); - - // Queue chunks for loading + sending - chunks.into_iter().for_each(|chunk| { - crate::player::send_chunk_to_player( - chunk, - net, - player, - &chunk_map, - &worker_handle, - &mut holders, - &mut holder_comp, - &mut loaded_chunks_comp, - &lazy, - ); - }); - - holder_comps.insert(player, holder_comp).unwrap(); - loaded_chunks_comps - .insert(player, loaded_chunks_comp) - .unwrap(); - - // Increment player count - player_count.0.fetch_add(1, Ordering::SeqCst); - - join_handler.stage = Stage::AwaitChunkSends; - } - Stage::AwaitChunkSends => { - // If 0 chunks have yet to be sent, join the player by sending spawn position. - // See https://wiki.vg/Protocol_FAQ - if pending_chunks.len() != 0 { - continue; - } - - // SpawnPosition packet: world spawn (used for compass) - let level_spawn_block_pos = - BlockPosition::new(level.spawn_x, level.spawn_y, level.spawn_z); - let level_spawn_position = SpawnPosition::new(level_spawn_block_pos); - crate::network::send_packet_to_player(net, level_spawn_position); - - // Initial position/rotation for the player when they spawn - let player_pos = positions.get(player).unwrap().current; - let position_and_look = PlayerPositionAndLookClientbound::new( - player_pos.x, - player_pos.y, - player_pos.z, - player_pos.yaw, - player_pos.pitch, - 0, // Flags - unused by us - 0, // Teleport ID - unused by us - ); - crate::network::send_packet_to_player(net, position_and_look); - - // Trigger events - let event = PlayerJoinEvent { player }; - join_events.single_write(event); - - let event = EntitySpawnEvent { entity: player }; - spawn_events.single_write(event); - - // Trigger inventory update event on the entire inventory - let event = InventoryUpdateEvent { - slots: (0..46).collect(), - player, - }; - inv_events.single_write(event); - - // We're finished here. - to_remove.push(player); - } - } - } - - to_remove.into_iter().for_each(|player| { - joincomps.remove(player); - }); - } -} diff --git a/server/src/lazy.rs b/server/src/lazy.rs index c446a5f8b..385f09422 100644 --- a/server/src/lazy.rs +++ b/server/src/lazy.rs @@ -1,36 +1,105 @@ -//! Extension methods for `LazyUpdate`. - -use crate::entity::EntitySpawnEvent; -use shrev::EventChannel; -use specs::world::{EntitiesRes, LazyBuilder}; -use specs::{Entity, LazyUpdate}; - -pub trait LazyUpdateExt { - /// Creates an entity and lazily inserts components. - /// - /// This should be used instead of `LazyUpdate::create_entity` - /// because it automatically triggers an `EntitySpawnEvent`. - fn spawn_entity(&self, entities: &EntitiesRes) -> LazyBuilder; - - /// Lazily sends an entity to a player. This simply forwards - /// to `crate::entity::broadcast::send_entity_to_player`. - fn send_entity_to_player(&self, player: Entity, entity: Entity); +use crossbeam::queue::SegQueue; +use legion::entity::Entity; +use legion::storage::{Component, Tag}; +use legion::world::World; +use smallvec::SmallVec; +use tonks::Scheduler; + +pub trait LazyFnWithScheduler: FnOnce(&mut World, &mut Scheduler) + Send {} +impl LazyFnWithScheduler for F where F: FnOnce(&mut World, &mut Scheduler) + Send {} + +pub trait LazyFn: FnOnce(&mut World) + Send {} +impl LazyFn for F where F: FnOnce(&mut World) + Send {} + +pub trait LazyEntityFn: FnOnce(&mut World, &mut Scheduler, Entity) + Send {} +impl LazyEntityFn for F where F: FnOnce(&mut World, &mut Scheduler, Entity) + Send {} + +/// Resource which allows lazy creation of entities +/// or execution of functions with world access. +#[derive(Default, Resource)] +pub struct Lazy { + /// Internal queue of actions to perform. + queue: SegQueue, } -impl LazyUpdateExt for LazyUpdate { - fn spawn_entity(&self, entities: &EntitiesRes) -> LazyBuilder { - let entity = entities.create(); - // Trigger event - self.exec(move |world| { - world - .fetch_mut::>() - .single_write(EntitySpawnEvent { entity }); - }); +impl Lazy { + /// Lazily executes a closure with world access. + pub fn exec(&self, f: impl FnOnce(&mut World) + Send + 'static) { + self.exec_with_scheduler(move |world, _| f(world)); + } - LazyBuilder { lazy: self, entity } + /// Lazily executes a closure with world and scheduler (resource) + /// access. + pub fn exec_with_scheduler(&self, f: impl FnOnce(&mut World, &mut Scheduler) + Send + 'static) { + self.queue.push(Action::Exec(Box::new(f))); } - fn send_entity_to_player(&self, player: Entity, entity: Entity) { - crate::entity::send_entity_to_player(self, player, entity); + /// Creates an `EntityBuilder` which can be used to lazily + /// create an entity. + pub fn create_entity(&self) -> EntityBuilder { + EntityBuilder { + lazy: self, + fns: smallvec![], + } + } + + /// Performs all queued actions. + pub fn flush(&self, world: &mut World, scheduler: &mut Scheduler) { + while let Ok(action) = self.queue.pop() { + match action { + Action::Exec(f) => f(world, scheduler), + } + } + } +} + +/// An action which the lazy updater may perform. +enum Action { + Exec(Box), +} + +/// Builder for lazily creating entities. +pub struct EntityBuilder<'a> { + lazy: &'a Lazy, + fns: SmallVec<[Box; 8]>, +} + +impl<'a> EntityBuilder<'a> { + pub fn with_component(mut self, component: C) -> Self { + self.fns.push(Box::new( + move |world: &mut World, _: &mut Scheduler, entity: Entity| { + world.add_component(entity, component); + }, + )); + self + } + + pub fn with_tag(mut self, tag: T) -> Self { + self.fns.push(Box::new( + move |world: &mut World, _: &mut Scheduler, entity: Entity| { + world.add_tag(entity, tag); + }, + )); + self + } + + /// Executes a function with the entity after it is created. + pub fn with_exec( + mut self, + f: impl FnOnce(&mut World, &mut Scheduler, Entity) + Send + 'static, + ) -> Self { + self.fns.push(Box::new(f)); + self + } + + pub fn build(self) { + let fns = self.fns; + self.lazy.exec_with_scheduler(move |world, scheduler| { + let entity = world.insert((), [()].iter().copied())[0]; + + for f in fns { + f(world, scheduler, entity); + } + }) } } diff --git a/server/src/lib.rs b/server/src/lib.rs index f7942747f..480452e3e 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,7 +1,97 @@ -// Specs systems tend to have very long -// tuples as their SystemData, and Clippy -// doesn't seem to like this. -#![allow(clippy::type_complexity)] +//! Feather, a Minecraft server implementation in Rust. +//! +//! This is the developer documenation, and anyone wishing to contribute +//! should read this first. +//! +//! The core of Feather is based on [`legion`](https://github.com/TomGillen/legion), +//! a fast ECS for Rust, and [`tonks`](https://github.com/feather-rs/tonks), a system +//! scheduler built on Legion. As a result, we use the ECS architecture: the +//! entire server consists of _entities_, simple IDs with no data; _components_, +//! arbitrary data, such as positions, which can be attached to an entity; +//! and _systems_, functions which can run logic over entities and components. +//! +//! The benefit of this design is the splitting between data and logic. With a traditional +//! object-oriented design, there would be an Entity class from which other entities +//! inherit and can override logic. However, this model does not work well with Rust's +//! borrow checker (as we found out in the early days of Feather, when this design was +//! used), and more importantly, it reduces flexibility. Say, for example, that a plugin +//! wants to modify the physics behavior of a cow by increasing gravity. With the object-oriented +//! design, it would have to somehow modify the `run_physics` method on `Cow`, which is not +//! possible in a native language (although it can be done in some languages using class rewriting). +//! On the other hand, using Feather, there is a `Physics` component which stores gravity, +//! drag, etc. for an entity, and all the plugin has to do is modify that component. +//! +//! Another benefit of the ECS architecture is performance. With the OO design, entities +//! would likely be stored in a `Vec>`, which is horribly inefficient +//! with regards to cache locality and iteration performance. Legion, however, stores +//! entities in an efficient manner such that many of the same type of component +//! are stored contiguously, which is excellent for cache performance. +//! +//! Here is a more in-depth description of each concept in Feather. +//! +//! ## Entities +//! Entities, or `legion::entity::Entity`, are simple numerical IDs: they store no +//! data, but components can be attached to an entity. See the systems section +//! for information on how to access this data. +//! +//! ## Components +//! Components store data associated with an entity, such as `Position`. +//! Arbitrary amounts of components can be associated with any given entity. +//! +//! ## Resources +//! Resources are a branching off from the pure ECS concept. Like components, they +//! store arbitrary data in the form of structs; however, they are not associated +//! with any entity. An example of a resource might be the chunk map, which allows +//! access to blocks in the world. +//! +//! ## Systems +//! The systems concept is where things become more complex. `tonks`, the library +//! which runs systems, runs them _in parallel_, effectively multithreading the +//! entire server. This is still safe because the scheduler ensures that no +//! two systems which write to the same data run at the same time. +//! +//! A consequence of the above is that systems must explicitly state which +//! resources and components they might access. If a system needs access to positions, +//! it needs to state this upfront so the scheduler can ensure memory safety. +//! +//! Systems can be written by creating a function annotated with the `system` attribute. +//! `tonks` will automatically detect which resources are accessed based on the function +//! parameters, and it will register them to the system scheduler without you +//! having to do anything. (How does this work? Don't even bother.) +//! +//! ## Events +//! Events are another concept unique to Feather, at least in the way they are implemented +//! here. The name states what they are—BlockChangeEvent, for example, is triggered when +//! a block is updated. +//! +//! A system can trigger an event by specifying an `&mut Trigger` resource. +//! +//! ## Event handlers +//! Event handlers are similar to systems, but they only run when the event they +//! handle is triggered. Use the `event_handler` attribute on a function to register +//! it as an event handler. +//! +//! # Networking model +//! Feather consists of two key parts: the server threads, which run systems +//! over entities, and the networking tasks, which run on [`tokio`](https://github.com/tokio-rs/tokio). +//! The networking tasks will accept connections and parse any packets received +//! from the player. +//! +//! When a connection is first made to the server's TCP listener, the networking +//! task will spawn another task to handle the connection. It's important to note that +//! at this time, _the server thread is totally unaware of the connection_—networking +//! runs entirely isolated from the rest of the program. +//! +//! At this point, the initial handler takes over, which runs on the networking task. +//! It will handle the login sequence or status pings, perform authentication, etc. +//! If successful, the server is notified of the new player through a channel. +//! +//! When the server is notified of a new player, it's essential to realize +//! that the player still hasn't been sent important data, such as +//! chunk packets, inventory, time, nearby entities, etc. `PlayerJoinEvent` +//! is used to send this data. + +#![feature(vec_remove_item)] #[macro_use] extern crate log; @@ -12,87 +102,67 @@ extern crate serde_json; #[macro_use] extern crate failure; #[macro_use] -extern crate num_derive; -#[macro_use] extern crate smallvec; #[macro_use] extern crate lazy_static; #[macro_use] extern crate derive_deref; #[macro_use] -extern crate feather_codegen; +extern crate feather_core; #[macro_use] extern crate bitflags; #[macro_use] -extern crate feather_core; +extern crate tonks; extern crate nalgebra_glm as glm; use crossbeam::Receiver; use std::alloc::System; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::AtomicUsize; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use specs::{Builder, Dispatcher, DispatcherBuilder, Entity, LazyUpdate, World, WorldExt}; - -use feather_core::network::packet::implementation::DisconnectPlay; -use prelude::*; - -use crate::chunk_logic::{ChunkHolders, ChunkWorkerHandle}; -use crate::entity::chicken::ChickenComponent; -use crate::entity::cow::CowComponent; -use crate::entity::donkey::DonkeyComponent; -use crate::entity::horse::HorseComponent; -use crate::entity::llama::LlamaComponent; -use crate::entity::mooshroom::MooshroomComponent; -use crate::entity::pig::PigComponent; -use crate::entity::rabbit::RabbitComponent; -use crate::entity::sheep::SheepComponent; -use crate::entity::squid::SquidComponent; -use crate::entity::{ - EntityDestroyEvent, NamedComponent, PacketCreatorComponent, SerializerComponent, -}; -use crate::network::send_packet_to_player; -use crate::player::PlayerDisconnectEvent; -use crate::systems::{BROADCASTER, JOIN_HANDLER, NETWORK, PLAYER_INIT}; -use crate::util::Util; +use crate::chunk_logic::ChunkWorkerHandle; +use crate::config::Config; +use crate::io::NetworkIoManager; +use crate::state::State; use crate::worldgen::{ ComposableGenerator, EmptyWorldGenerator, SuperflatWorldGenerator, WorldGenerator, }; use feather_core::level; use feather_core::level::{deserialize_level_file, save_level_file, LevelData, LevelGeneratorType}; +use feather_core::world::ChunkMap; +use legion::world::World; use rand::Rng; -use shrev::EventChannel; use std::collections::hash_map::DefaultHasher; use std::fs::File; use std::hash::{Hash, Hasher}; use std::io::{Read, Write}; use std::path::Path; use std::process::exit; +use tonks::{Resources, Scheduler}; #[global_allocator] static ALLOC: System = System; -#[macro_use] -pub mod util; -pub mod blocks; +pub mod broadcasters; +pub mod chunk_entities; pub mod chunk_logic; -pub mod chunkworker; +pub mod chunk_worker; pub mod config; pub mod entity; pub mod io; -pub mod joinhandler; +pub mod join; pub mod lazy; pub mod network; +pub mod packet_handlers; pub mod physics; pub mod player; -pub mod prelude; pub mod shutdown; -pub mod systems; -#[cfg(test)] -pub mod testframework; +pub mod state; pub mod time; +pub mod util; +pub mod view; pub mod worldgen; pub const TPS: u64 = 20; @@ -100,12 +170,18 @@ pub const PROTOCOL_VERSION: u32 = 404; pub const SERVER_VERSION: &str = "Feather 1.13.2"; pub const TICK_TIME: u64 = 1000 / TPS; -#[derive(Default, Debug)] +#[derive(Default, Debug, Resource)] pub struct PlayerCount(AtomicUsize); -#[derive(Default, Debug)] +#[derive(Default, Debug, Resource)] pub struct TickCount(u64); +/// System to increment tick count each tick. +#[system] +fn tick_count_increment(tick: &mut TickCount) { + tick.0 += 1; +} + pub fn main() { let config = Arc::new(load_config()); init_log(&config); @@ -146,7 +222,10 @@ pub fn main() { exit(1) }); - let (mut world, mut dispatcher) = init_world(config, player_count, io_manager, level); + let chunk_worker_handle = init_chunk_worker(world_dir, &level); + + let mut scheduler = init_scheduler(Arc::clone(&config), chunk_worker_handle, level, io_manager); + let mut world = World::new(); // Channel used by the shutdown handler to notify the server thread. let (shutdown_tx, shutdown_rx) = crossbeam::unbounded(); @@ -158,11 +237,11 @@ pub fn main() { info!("Generating RSA keypair"); io::init(); - info!("Queuing spawn chunks for loading"); - load_spawn_chunks(&mut world); + info!("Queuing spawn chunks for loading UNIMPLEMENTED"); + // load_spawn_chunks(&mut world); TODO info!("Server started"); - run_loop(&mut world, &mut dispatcher, shutdown_rx); + run_loop(&mut world, &mut scheduler, shutdown_rx); info!("Shutting down"); @@ -177,6 +256,81 @@ pub fn main() { exit(0); } +/// Runs the main game loop. +fn run_loop(world: &mut World, scheduler: &mut Scheduler, shutdown_rx: Receiver<()>) { + loop { + if shutdown_rx.try_recv().is_ok() { + // Shut down + return; + } + + let start_time = current_time_in_millis(); + + scheduler.execute(world); + // https://github.com/TomGillen/legion/issues/60 + // world.defrag(None); // TODO: do this at interval rate? + + // Run lazily-executed closures. TODO: remove unsafe + unsafe { + let state = scheduler.resources().get::() as *const State; + (&*state).flush(world, scheduler); + } + + // Sleep correct amount + let end_time = current_time_in_millis(); + let elapsed = end_time - start_time; + if elapsed > TICK_TIME { + debug!("Running behind! Starting next tick immediately"); + continue; // Behind - start next tick immediately + } + + // Sleep in 1ms increments until we've slept enough + let mut sleep_time = (TICK_TIME - elapsed) as i64; + let mut last_sleep_time = current_time_in_millis(); + while sleep_time > 0 { + std::thread::sleep(Duration::from_millis(1)); + sleep_time -= (current_time_in_millis() - last_sleep_time) as i64; + last_sleep_time = current_time_in_millis(); + } + } +} + +/// Initializes the scheduler and resources. +fn init_scheduler( + config: Arc, + chunk_worker_handle: ChunkWorkerHandle, + level: LevelData, + io_manager: NetworkIoManager, +) -> Scheduler { + // Insert resources which don't have a `Default` impl. + let mut resources = Resources::new(); + let chunk_map = ChunkMap::new(); + resources.insert(State::new(config, chunk_map, level)); + resources.insert(chunk_worker_handle); + resources.insert(io_manager); + + tonks::build_scheduler().build(resources) +} + +/// Initializes the chunk worker. +fn init_chunk_worker(world_dir: &Path, level: &LevelData) -> ChunkWorkerHandle { + let generator: Arc = match level.generator_type() { + LevelGeneratorType::Flat => Arc::new(SuperflatWorldGenerator { + options: level.clone().generator_options.unwrap_or_default(), + }), + LevelGeneratorType::Default => { + Arc::new(ComposableGenerator::default_with_seed(level.seed as u64)) + } + _ => Arc::new(EmptyWorldGenerator {}), + }; + + let (tx, rx) = chunk_worker::start(world_dir, generator); + ChunkWorkerHandle { + sender: tx, + receiver: rx, + } +} + /// Loads the configuration file, creating a default /// one if it does not exist. fn load_config() -> Config { @@ -199,6 +353,35 @@ fn load_config() -> Config { } } +/// Starts the IO threads. +fn init_io_manager( + config: Arc, + player_count: Arc, + server_icon: Arc>, +) -> io::NetworkIoManager { + io::NetworkIoManager::start( + format!("{}:{}", config.server.address, config.server.port) + .parse() + .unwrap(), + config, + player_count, + server_icon, + ) +} + +fn init_log(config: &Config) { + let level = match config.log.level.as_str() { + "trace" => log::Level::Trace, + "debug" => log::Level::Debug, + "info" => log::Level::Info, + "warn" => log::Level::Warn, + "error" => log::Level::Error, + _ => panic!("Unknown log level {}", config.log.level), + }; + + simple_logger::init_with_level(level).unwrap(); +} + fn create_level(config: &Config) -> LevelData { let seed = get_seed(config); let world_name = &config.world.name; @@ -264,189 +447,6 @@ fn load_level(path: &Path) -> Result { Ok(data) } -/// Loads the chunks around the spawn area and creates -/// a chunk hold on those chunks to prevent them from -/// being unloaded. -/// -/// Note that these chunks are loaded asynchronously, -/// and this function will return before loading is complete. -fn load_spawn_chunks(world: &mut World) { - let view_distance = i32::from(world.fetch::>().server.view_distance); - - // Create an entity for the server and - // add chunk holders using it. - let server_entity = world.create_entity().build(); - - let mut chunk_holders = world.fetch_mut::(); - let chunk_worker_handle = world.fetch::(); - - let level = world.fetch::(); - let offset_x = level.spawn_x / 16; - let offset_z = level.spawn_z / 16; - for x in -view_distance..=view_distance { - for z in -view_distance..=view_distance { - let chunk = ChunkPosition::new(x + offset_x, z + offset_z); - - chunk_logic::load_chunk(&chunk_worker_handle, chunk); - chunk_holders.insert_holder(chunk, server_entity); - } - } -} - -/// Runs the server loop, blocking until the server -/// is shut down. -fn run_loop(world: &mut World, dispatcher: &mut Dispatcher, shutdown_rx: Receiver<()>) { - loop { - if shutdown_rx.try_recv().is_ok() { - // Shut down - return; - } - - let start_time = current_time_in_millis(); - - dispatcher.dispatch(&world); - world.maintain(); - - world.fetch_mut::().reset(); - - // Increment tick count - let mut tick_count = world.write_resource::(); - tick_count.0 += 1; - - // Sleep correct amount - let end_time = current_time_in_millis(); - let elapsed = end_time - start_time; - if elapsed > TICK_TIME { - debug!("Running behind! Starting next tick immediately"); - continue; // Behind - start next tick immediately - } - - // Sleep in 1ms increments until we've slept enough - let mut sleep_time = (TICK_TIME - elapsed) as i64; - let mut last_sleep_time = current_time_in_millis(); - while sleep_time > 0 { - std::thread::sleep(Duration::from_millis(1)); - sleep_time -= (current_time_in_millis() - last_sleep_time) as i64; - last_sleep_time = current_time_in_millis(); - } - } -} - -/// Starts the IO threads. -fn init_io_manager( - config: Arc, - player_count: Arc, - server_icon: Arc>, -) -> io::NetworkIoManager { - io::NetworkIoManager::start( - format!("{}:{}", config.server.address, config.server.port) - .parse() - .unwrap(), - config, - player_count, - server_icon, - ) -} - -/// Initializes the Specs world and dispatchers. -fn init_world<'a, 'b>( - config: Arc, - player_count: Arc, - ioman: io::NetworkIoManager, - level: LevelData, -) -> (World, Dispatcher<'a, 'b>) { - let mut world = World::new(); - time::init_time(&mut world, &level); - world.insert(config); - world.insert(player_count); - world.insert(ioman); - world.insert(TickCount::default()); - - world.register::(); - world.register::(); - - let generator: Arc = match level.generator_type() { - LevelGeneratorType::Flat => Arc::new(SuperflatWorldGenerator { - options: level.clone().generator_options.unwrap_or_default(), - }), - LevelGeneratorType::Default => { - Arc::new(ComposableGenerator::default_with_seed(level.seed as u64)) - } - _ => Arc::new(EmptyWorldGenerator {}), - }; - world.insert(level); - world.insert(generator); - - let mut dispatcher = DispatcherBuilder::new(); - - dispatcher.add(network::NetworkSystem, NETWORK, &[]); - - blocks::init_logic(&mut dispatcher); - physics::init_logic(&mut dispatcher); - entity::init_logic(&mut dispatcher); - player::init_logic(&mut dispatcher); - chunk_logic::init_logic(&mut dispatcher); - time::init_logic(&mut dispatcher); - - dispatcher.add_barrier(); - - blocks::init_handlers(&mut dispatcher); - physics::init_handlers(&mut dispatcher); - entity::init_handlers(&mut dispatcher); - player::init_handlers(&mut dispatcher); - chunk_logic::init_handlers(&mut dispatcher); - - // Player init dependency is so that player position is loaded - // before the join handle runs. - dispatcher.add( - joinhandler::JoinHandlerSystem, - JOIN_HANDLER, - &[NETWORK, PLAYER_INIT], - ); - - dispatcher.add_barrier(); - - player::init_broadcast(&mut dispatcher); - entity::init_broadcast(&mut dispatcher); - - // Broadcast system needs to run last. - dispatcher.add_barrier(); - dispatcher.add(util::BroadcasterSystem, BROADCASTER, &[]); - - let mut dispatcher = dispatcher.build(); - dispatcher.setup(&mut world); - - register_components(&mut world); - - (world, dispatcher) -} - -fn register_components(world: &mut World) { - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); -} - -fn init_log(config: &Config) { - let level = match config.log.level.as_str() { - "trace" => log::Level::Trace, - "debug" => log::Level::Debug, - "info" => log::Level::Info, - "warn" => log::Level::Warn, - "error" => log::Level::Error, - _ => panic!("Unknown log level {}", config.log.level), - }; - - simple_logger::init_with_level(level).unwrap(); -} - /// Tries to load a server icon from the current directory. fn load_server_icon() -> Option { let icon_file: Option = match File::open("server-icon.png") { @@ -475,7 +475,7 @@ pub fn current_time_in_secs() -> u64 { .as_secs() } -/// Retrieves the current time in milleseconds +/// Retrieves the current time in milliseconds /// since the UNIX epoch. pub fn current_time_in_millis() -> u64 { SystemTime::now() @@ -483,70 +483,3 @@ pub fn current_time_in_millis() -> u64 { .unwrap() .as_millis() as u64 } - -/// Disconnects the given player, removing them from the world. -/// This operation is performed lazily. -pub fn disconnect_player(player: Entity, reason: String, lazy: &LazyUpdate) { - lazy.exec_mut(move |world| { - let json = json!({ - "text": reason, - }); - - let packet = DisconnectPlay::new(json.to_string()); - send_packet_to_player(world.read_component().get(player).unwrap(), packet); - - disconnect_player_without_packet(player, world, reason); - }) -} - -/// Disconnects a player without sending Disconnect Play. -/// This should be used when the client disconnects. -pub fn disconnect_player_without_packet(player: Entity, world: &mut World, reason: String) { - let nameds = world.write_component::(); - let named = nameds.get(player).unwrap(); - - info!("Disconnecting player {}: {}", named.display_name, reason); - - // Decrement player count - let player_count = world.fetch_mut::>(); - player_count.0.fetch_sub(1, Ordering::SeqCst); - - // Trigger disconnect event - let event = PlayerDisconnectEvent { - player, - uuid: named.uuid, - reason, - }; - world - .fetch_mut::>() - .single_write(event); - - // Trigger entity destroy event - let event = EntityDestroyEvent { entity: player }; - world - .fetch_mut::>() - .single_write(event); - - // The entity is removed from the world by `entity::EntityDestroySystem`. -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_init_world() { - let config = Arc::new(Config::default()); - let player_count = Arc::new(PlayerCount(AtomicUsize::new(0))); - let server_icon = Arc::new(Some(String::from("server_icon"))); - let ioman = init_io_manager( - Arc::clone(&config), - Arc::clone(&player_count), - Arc::clone(&server_icon), - ); - let level = LevelData::default(); - - let (world, mut dispatcher) = init_world(config, player_count, ioman, level); - dispatcher.dispatch(&world); - } -} diff --git a/server/src/network.rs b/server/src/network.rs index fdf2b13fa..2921b7192 100644 --- a/server/src/network.rs +++ b/server/src/network.rs @@ -1,417 +1,243 @@ -use parking_lot::Mutex; - -use crossbeam::Receiver; -use futures::channel::mpsc::UnboundedSender as Sender; -use shrev::EventChannel; -use specs::{ - Component, DenseVecStorage, Entities, Entity, Join, LazyUpdate, Read, ReadStorage, System, - WorldExt, Write, WriteStorage, -}; - -use feather_core::network::packet::{implementation::*, Packet, PacketType}; - -use crate::entity::PlayerComponent; +//! Network logic. This module includes: +//! * `Network`, a component assigned to entities +//! which can send and receive packets. (This is only +//! added for players, obviously.) +//! * `PacketQueue`, which stores packets received +//! from players and allows systems to poll for packets +//! received of a given type. + +use crate::entity::{EntityDeleteEvent, EntityId}; use crate::io::{ListenerToServerMessage, NetworkIoManager, ServerToWorkerMessage}; -use crate::joinhandler::JoinHandlerComponent; -use crate::prelude::*; -use crate::{disconnect_player_without_packet, TickCount}; +use crate::player; +use crate::state::State; +use crossbeam::Receiver; +use feather_core::network::cast_packet; +use feather_core::{Packet, PacketType, Position}; +use futures::channel::mpsc::UnboundedSender; +use legion::entity::Entity; +use legion::query::Read; +use lock_api::RawMutex; +use parking_lot::{Mutex, MutexGuard}; +use std::iter; use strum::EnumCount; +use tonks::{PreparedWorld, Query}; +use uuid::Uuid; -//const MAX_KEEP_ALIVE_TIME: u64 = 30; -//const HEAD_OFFSET: f64 = 1.62; // Offset from feet pos to head pos +type QueuedPackets = Vec<(Entity, Box)>; -/// A packet received from a player. -pub type QueuedPacket = (Entity, Box); +struct UnsafeDrain { + ptr: *const T, + len: usize, + pos: usize, +} -/// Vector of `QueuedPacket`. -type QueuedPackets = Vec; +impl Iterator for UnsafeDrain { + type Item = T; -/// A component which contains the received packets -/// for this tick. -pub struct PacketQueue { - /// Vector of packet queues. For any given packet - /// type, the queued packets of that type can - /// be found by indexing into this vector with the ordinal - /// of the packet type. - /// - /// A locked `Vec` is used rather than a `SegQueue` because - /// there is typically no contention when accessing the queue - /// for a single packet type (there is at most one system handling - /// each packet type). As a result, there is no need for a lock-free - /// data structure. - queue: Vec>, + fn next(&mut self) -> Option { + if self.pos == self.len { + return None; + } + + let value = unsafe { std::ptr::read(self.ptr.add(self.pos)) }; + self.pos += 1; + Some(value) + } } -impl PacketQueue { - /// Returns the packets queued for handling - /// of the given type, draining the queue of this - /// type of packet. - pub fn for_packet(&self, ty: PacketType) -> Vec { - let ordinal = ty.ordinal(); +pub struct DrainedPackets<'a, I> { + mutex: &'a parking_lot::RawMutex, + value: I, +} - let mut queued_packets = self.queue[ordinal].lock(); +impl<'a, I> DrainedPackets<'a, I> { + unsafe fn new(mutex: &'a parking_lot::RawMutex, value: I) -> Self { + Self { mutex, value } + } +} - let mut new_queue = vec![]; - std::mem::swap(&mut new_queue, &mut queued_packets); +impl<'a, I> Iterator for DrainedPackets<'a, I> +where + I: Iterator, +{ + type Item = I::Item; - new_queue + fn next(&mut self) -> Option { + self.value.next() } +} - /// Adds a packet to the queue. - pub fn add_for_packet(&self, player: Entity, packet: Box) { - let ordinal = packet.ty().ordinal(); - - let mut queued_packets = self.queue[ordinal].lock(); - queued_packets.push((player, packet)); +impl<'a, I> Drop for DrainedPackets<'a, I> { + fn drop(&mut self) { + self.mutex.unlock(); } } +/// The packet queue. This type allows systems to poll for +/// received packets of a given type. +/// +/// A system should never require mutable access to this type. +#[derive(Resource)] +pub struct PacketQueue { + /// Vector of queued packets. This vector is indexed + /// by the ordinal of the packet type, and each + /// queue contains only packets of its type. + queue: Vec>, +} + impl Default for PacketQueue { fn default() -> Self { - // Initialize with an empty queue for each packet type. - // Packet type ordinals start at 1 (who decided this? FIXME), - // so we have to use an inclusive range. + Self::new() + } +} + +impl PacketQueue { + /// Creates a new, empty `PacketQueue`. + pub fn new() -> Self { Self { - queue: (0..=PacketType::count()) - .map(|_| Mutex::new(vec![])) + queue: iter::repeat_with(|| Mutex::new(vec![])) + .take(PacketType::count() + 1) .collect(), } } -} -pub struct NetworkComponent { - sender: Sender, - receiver: Receiver, - /// A vector of all chunks that are currently - /// being loaded and should be sent to the player - /// once they have been loaded. - pub chunks_to_send: Vec, - //last_keep_alive_time: u64, -} + /// Returns an iterator over packets of a given type. + pub fn received(&self) -> impl Iterator + '_ { + let mut queue = self.queue[P::ty_sized().ordinal()].lock(); -impl NetworkComponent { - pub fn new( - sender: Sender, - receiver: Receiver, - ) -> Self { - Self { - sender, - receiver, - chunks_to_send: vec![], + // Hack to map to draining iterator. + unsafe { + let raw = MutexGuard::mutex(&queue).raw(); + + let drain = UnsafeDrain { + ptr: queue.as_ptr(), + len: queue.len(), + pos: 0, + }; + + // Safety: the vector cannot be accessed as long as the returned `UnsafeDrain` + // has not been dropped, since the mutex is acquired. + queue.set_len(0); + // Ensure mutex is not released; we will do it manually in `UnsafeDrain` + std::mem::forget(queue); + + let iter = drain.map(|(entity, packet)| (entity, cast_packet::

(packet))); + + DrainedPackets::new(raw, iter) } } + + /// Adds a packet to the queue. + pub fn push(&self, packet: Box, entity: Entity) { + let ordinal = packet.ty().ordinal(); + + self.queue[ordinal].lock().push((entity, packet)); + } } -impl Component for NetworkComponent { - type Storage = DenseVecStorage; +/// Network component containing channels to send and receive packets. +/// +/// Systems should call `Self::send` to send a packet to this entity (player). +pub struct Network { + pub sender: UnboundedSender, + pub receiver: Receiver, } -/// The network system, responsible for -/// receiving and buffering packets received -/// from players. Received packets -/// are added to a queue (`PacketQueue`) so that -/// other systems can handle them. -pub struct NetworkSystem; - -/// Event which is triggered when a player joins -/// but before the join handler is completed. -pub struct PlayerPreJoinEvent { - pub player: Entity, - pub username: String, - pub uuid: Uuid, - pub profile_properties: Vec, +impl Network { + /// Sends a packet to this player. + pub fn send

(&self, packet: P) + where + P: Packet, + { + self.send_boxed(Box::new(packet)); + } + + /// Sends a boxed packet to this player. + pub fn send_boxed(&self, packet: Box) { + // Discard error in case the channel was disconnected + // (e.g. if the player disconnected and its worker task + // shut down, and the disconnect was not yet registered + // by the server) + let _ = self + .sender + .unbounded_send(ServerToWorkerMessage::SendPacket(packet)); + } } -impl<'a> System<'a> for NetworkSystem { - type SystemData = ( - WriteStorage<'a, NetworkComponent>, - ReadStorage<'a, PlayerComponent>, - Write<'a, EventChannel>, - Write<'a, PacketQueue>, - Read<'a, NetworkIoManager>, - Entities<'a>, - Read<'a, TickCount>, - Read<'a, LazyUpdate>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut netcomps, - pcomps, - mut join_events, - packet_queue, - ioman, - entities, - tick_count, - lazy, - ) = data; - // Poll for new connections - while let Ok(msg) = ioman.receiver.try_recv() { +/// The network system. This system is responsible for: +/// * Handling player disconnects. +/// * Pushing received packets to the packet queue. +/// * Accepting new clients and creating entities for them. +#[system] +pub fn network_( + state: &State, + io: &NetworkIoManager, + packet_queue: &PacketQueue, + query: &mut Query>, + world: &mut PreparedWorld, +) { + // For each `Network`, handle any disconnects and received packets. + query.par_entities_for_each(world, |(entity, network)| { + while let Ok(msg) = network.receiver.try_recv() { match msg { - ListenerToServerMessage::NewClient(info) => { - // New connection - handle it - info!("Accepting connection from {}", info.ip); - let netcomp = NetworkComponent::new(info.sender, info.receiver); - - // Create entity - let new_entity = entities.create(); - netcomps.insert(new_entity, netcomp).unwrap(); - - // Create join handler - let join_handler = JoinHandlerComponent::new(); - lazy.exec_mut(move |world| { - world - .write_component::() - .insert(new_entity, join_handler) - .unwrap(); + ServerToWorkerMessage::NotifyDisconnect(_) => { + state.exec_with_scheduler(move |world, scheduler| { + let position = *world.get_component::(entity).unwrap(); + let id = *world.get_component::(entity).unwrap(); + let uuid = *world.get_component::(entity).unwrap(); + scheduler.trigger(EntityDeleteEvent { + entity, + position: Some(position), + id, + uuid, + }); + assert!(world.delete(entity), "player already deleted"); }); - - // Queue event - let event = PlayerPreJoinEvent { - player: new_entity, - username: info.username.clone(), - uuid: info.uuid, - profile_properties: info.profile.clone(), - }; - join_events.single_write(event); } - } - } - - // Receive packets + disconnects from players - for (player, netcomp) in (&entities, &netcomps).join() { - while let Ok(msg) = netcomp.receiver.try_recv() { - match msg { - ServerToWorkerMessage::NotifyPacketReceived(packet) => { - packet_queue.add_for_packet(player, packet); - } - ServerToWorkerMessage::NotifyDisconnect(reason) => { - lazy.exec_mut(move |world| { - disconnect_player_without_packet(player, world, reason) - }); - break; - } - _ => panic!("Network system received invalid message from IO worker}"), + ServerToWorkerMessage::NotifyPacketReceived(packet) => { + packet_queue.push(packet, entity); } + _ => unreachable!(), } } - - // Send keepalives every second. The dependency on the player - // component is required because keepalives should - // only be sent to players who have joined (completed - // the login process). - // TODO check that player hasn't timed out - if tick_count.0 % TPS == 0 { - for (netcomp, _) in (&netcomps, &pcomps).join() { - send_packet_to_player(netcomp, KeepAliveClientbound::new(0)); - } - } - } -} - -/// Sends a packet to all players on the server, excluding -/// `neq`, if it exists. -pub fn send_packet_to_all_players( - net_comps: &ReadStorage, - entities: &Entities, - packet: P, - neq: Option, -) { - for (entity, net) in (entities, net_comps).join() { - if let Some(e) = neq.as_ref() { - if *e == entity { - continue; // Exclude this entity + }); + + // Handle new clients. + while let Ok(msg) = io.receiver.try_recv() { + match msg { + ListenerToServerMessage::NewClient(info) => { + debug!("Server received connection from {}", info.username); + player::create(state, info); } } - - send_packet_to_player(net, packet.clone()); } } -/// Sends a packet to the given player. -pub fn send_packet_to_player(comp: &NetworkComponent, packet: P) { - send_packet_boxed_to_player(comp, Box::new(packet)); -} - -/// Sends a packet to the given player. -pub fn send_packet_boxed_to_player(comp: &NetworkComponent, packet: Box) { - let _ = comp - .sender - .unbounded_send(ServerToWorkerMessage::SendPacket(packet)); -} - #[cfg(test)] mod tests { use super::*; - use crate::io::NewClientInfo; - use crate::player::PlayerDisconnectEvent; - use crate::testframework as t; - use std::net::SocketAddr; - - #[test] - fn test_packet_queue() { - let queue = PacketQueue::default(); - - let (mut w, _) = t::init_world(); - let player = t::add_player(&mut w); - - let packet = LoginStart::new("test".to_string()); - queue.add_for_packet(player.entity, Box::new(packet)); - - let packets = queue.for_packet(PacketType::LoginStart); - assert_eq!(packets.len(), 1); - - let (entity, packet) = packets.first().unwrap(); - assert_eq!(*entity, player.entity); - assert_eq!(packet.ty(), PacketType::LoginStart); - } + use feather_core::network::packet::implementation::Handshake; + use feather_core::network::packet::PacketType::SpawnObject; + use legion::world::World; #[test] - fn test_new_client() { - let (mut w, mut d) = t::init_world(); - - let ioman = w.fetch_mut::(); - - let (send1, _recv1) = futures::channel::mpsc::unbounded(); - let (_send2, recv2) = crossbeam::unbounded(); - - let new_client = NewClientInfo { - ip: SocketAddr::new("127.0.0.1".parse().unwrap(), 25565), - username: "".to_string(), - profile: vec![], - uuid: Uuid::new_v4(), - sender: send1, - receiver: recv2, - }; - - let msg = ListenerToServerMessage::NewClient(new_client); - ioman.listener_sender.send(msg).unwrap(); - - let mut event_reader = t::reader(&w); - - drop(ioman); - - // Call the network system - d.dispatch(&w); - - w.maintain(); - - // Confirm that an entity was created - let mut count = 0; - let mut entity = None; - for (e, _, _) in ( - &*w.entities(), - &w.read_component::(), - &w.read_component::(), - ) - .join() - { - entity = Some(e); - count += 1; - } - assert_eq!(count, 1); - - // Confirm that playerprejoinevent was queued - let channel = w.fetch_mut::>(); - let join_events: Vec<&PlayerPreJoinEvent> = channel.read(&mut event_reader).collect(); - assert_eq!(join_events.len(), 1); - let event = join_events.first().unwrap(); - - assert_eq!(event.player, entity.unwrap()); - } - - #[test] - fn test_packet_receive() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - // Send a packet - let packet = LoginStart::new("".to_string()); - t::send_packet(&player, packet); - - // Run system - d.dispatch(&w); - - w.maintain(); - - // Confirm that packet was received properly - let queue = w.fetch::(); - let packets = queue.for_packet(PacketType::LoginStart); - - assert_eq!(packets.len(), 1); - - let (entity, _packet) = packets.first().unwrap(); - assert_eq!(*entity, player.entity); - } - - #[test] - fn test_disconnect() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let mut event_reader = t::reader(&w); - - player - .network_sender - .send(ServerToWorkerMessage::NotifyDisconnect( - "reason".to_string(), - )) - .unwrap(); - - d.dispatch(&w); - - w.maintain(); - - let channel = w.fetch::>(); - let events = channel.read(&mut event_reader).collect::>(); - - assert_eq!(events.len(), 1); - let first = events.first().unwrap(); - assert_eq!(first.player, player.entity); - } - - #[test] - fn test_keep_alives() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - - d.dispatch(&w); - - t::assert_packet_received(&player, PacketType::KeepAliveClientbound); - t::assert_packet_received(&player2, PacketType::KeepAliveClientbound); - } - - #[test] - fn test_send_packet_to_all_players() { - let (mut w, _) = t::init_world(); - - let player1 = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - let player3 = t::add_player(&mut w); - - let packet = LoginStart::new("test".to_string()); + fn packet_queue() { + let queue = PacketQueue::new(); - send_packet_to_all_players( - &w.read_component(), - &w.entities(), - packet, - Some(player1.entity), - ); + let mut world = World::new(); + let entities = world.insert((), vec![(), ()]); - dbg!(); + queue.push(Box::new(Handshake::default()), entities[0]); + queue.push(Box::new(SpawnObject::default()), entities[1]); + queue.push(Box::new(Handshake::default()), entities[1]); - t::assert_packet_received(&player2, PacketType::LoginStart); - dbg!(); - t::assert_packet_received(&player3, PacketType::LoginStart); - dbg!(); + let mut handshakes = queue.received::(); + assert_eq!(handshakes.next().unwrap().0, entities[0]); + assert_eq!(handshakes.next().unwrap().0, entities[1]); + assert!(handshakes.next().is_none()); - // Check that exclusion was not sent - let sent = t::received_packets(&player1, None); - dbg!(); - assert!(sent.is_empty()); + let mut spawn_objects = queue.received::(); + assert_eq!(spawn_objects.next().unwrap().0, entities[1]); + assert!(spawn_objects.next().is_none()); } } diff --git a/server/src/packet_handlers/animation.rs b/server/src/packet_handlers/animation.rs new file mode 100644 index 000000000..e42a10d3f --- /dev/null +++ b/server/src/packet_handlers/animation.rs @@ -0,0 +1,20 @@ +use crate::network::PacketQueue; +use crate::player::PlayerAnimationEvent; +use feather_core::network::packet::implementation::AnimationServerbound; +use feather_core::{ClientboundAnimation, Hand}; +use tonks::Trigger; + +/// Handles animation packets. +#[system] +fn handle_animation(queue: &PacketQueue, trigger: &mut Trigger) { + queue + .received::() + .for_each(|(player, packet)| { + let animation = match packet.hand { + Hand::Main => ClientboundAnimation::SwingMainArm, + Hand::Off => ClientboundAnimation::SwingOffhand, + }; + + trigger.trigger(PlayerAnimationEvent { player, animation }); + }); +} diff --git a/server/src/packet_handlers/mod.rs b/server/src/packet_handlers/mod.rs new file mode 100644 index 000000000..3af94b557 --- /dev/null +++ b/server/src/packet_handlers/mod.rs @@ -0,0 +1,4 @@ +//! Systems which handle packets through `crate::network::PacketQueue`. + +mod animation; +mod movement; diff --git a/server/src/packet_handlers/movement.rs b/server/src/packet_handlers/movement.rs new file mode 100644 index 000000000..41240d4a1 --- /dev/null +++ b/server/src/packet_handlers/movement.rs @@ -0,0 +1,68 @@ +use crate::entity::EntityMoveEvent; +use crate::network::PacketQueue; +use feather_core::network::packet::implementation::{ + PlayerLook, PlayerPosition, PlayerPositionAndLookServerbound, +}; +use feather_core::Position; +use legion::entity::Entity; +use legion::query::Write; +use tonks::{PreparedWorld, Query, Trigger}; + +#[derive(Default, Resource)] +struct Buf(Vec<(Entity, Position)>); + +/// Handles player movement packets. +#[system] +fn handle_movement( + queue: &PacketQueue, + _query: &mut Query>, + world: &mut PreparedWorld, + buf: &mut Buf, + trigger: &mut Trigger, +) { + let positions = queue.received::().map(|(player, packet)| { + let old = *world.get_component::(player).unwrap(); + ( + player, + position!( + packet.x, + packet.feet_y, + packet.z, + old.pitch, + old.yaw, + packet.on_ground + ), + ) + }); + + let looks = queue.received::().map(|(player, packet)| { + let mut old = *world.get_component::(player).unwrap(); + old.pitch = packet.pitch; + old.yaw = packet.yaw; + old.on_ground = packet.on_ground; + (player, old) + }); + + let pos_looks = queue + .received::() + .map(|(player, packet)| { + ( + player, + position!( + packet.x, + packet.feet_y, + packet.z, + packet.pitch, + packet.yaw, + packet.on_ground + ), + ) + }); + + buf.0.extend(positions.chain(looks).chain(pos_looks)); + + buf.0.drain(..).for_each(|(player, new_pos)| { + *world.get_component_mut::(player).unwrap() = new_pos; + trigger.trigger(EntityMoveEvent { entity: player }); + }); +} diff --git a/server/src/physics/component.rs b/server/src/physics/component.rs index f10a707ec..979991bde 100644 --- a/server/src/physics/component.rs +++ b/server/src/physics/component.rs @@ -3,7 +3,6 @@ use glm::DVec3; use ncollide3d::bounding_volume::AABB; -use specs::{Component, VecStorage}; pub const DEFAULT_SLIP_MULTIPLIER: f64 = 0.6; @@ -13,7 +12,7 @@ pub const DEFAULT_SLIP_MULTIPLIER: f64 = 0.6; /// /// Typically, this component should be constructed using `PhysicsBuilder`. #[derive(Debug)] -pub struct PhysicsComponent { +pub struct Physics { /// This entity's bounding box. pub bbox: AABB, /// The drag coefficient for this entity. Each tick, @@ -33,18 +32,14 @@ pub struct PhysicsComponent { pub slip_multiplier: f64, } -impl Component for PhysicsComponent { - type Storage = VecStorage; -} - /// Builder for physics components. pub struct PhysicsBuilder { - comp: PhysicsComponent, + comp: Physics, } impl Default for PhysicsBuilder { fn default() -> Self { - let comp = PhysicsComponent { + let comp = Physics { bbox: bbox(0.5, 0.5, 0.5), drag: 0.98, gravity: -0.08, @@ -85,7 +80,7 @@ impl PhysicsBuilder { self } - pub fn build(self) -> PhysicsComponent { + pub fn build(self) -> Physics { self.comp } } diff --git a/server/src/physics/entity.rs b/server/src/physics/entity.rs index 4e1b931bd..4a372e4d6 100644 --- a/server/src/physics/entity.rs +++ b/server/src/physics/entity.rs @@ -1,17 +1,17 @@ //! Module for performing entity physics, including velocity, drag //! and position updates each tick. -use specs::{Entities, Entity, Join, Read, ReadStorage, System, Write, WriteStorage}; - -use crate::entity::{EntityDestroyEvent, PositionComponent, VelocityComponent}; -use crate::physics::{ - block_impacted_by_ray, blocks_intersecting_bbox, AABBExt, PhysicsComponent, Side, -}; -use feather_core::world::ChunkMap; +use crate::entity::Velocity; +use crate::physics::{block_impacted_by_ray, blocks_intersecting_bbox, AABBExt, Physics, Side}; +use crate::state::State; use feather_core::Position; use feather_core::{Block, BlockExt}; -use shrev::EventChannel; +use legion::entity::Entity; +use legion::query::{Read, Write}; +use parking_lot::Mutex; +use tonks::{PreparedWorld, Query, Trigger}; +/// Event triggered when an entity lands on the ground. #[derive(Debug, Clone)] pub struct EntityPhysicsLandEvent { pub entity: Entity, @@ -20,197 +20,128 @@ pub struct EntityPhysicsLandEvent { /// System for updating all entities' positions and velocities /// each tick. -pub struct EntityPhysicsSystem; - -impl<'a> System<'a> for EntityPhysicsSystem { - type SystemData = ( - WriteStorage<'a, PositionComponent>, - WriteStorage<'a, VelocityComponent>, - ReadStorage<'a, PhysicsComponent>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Read<'a, ChunkMap>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut positions, - mut velocities, - physics, - mut entity_destroy_events, - mut entity_land_events, - chunk_map, - entities, - ) = data; - // Go through entities and update their positions according - // to their velocities. - - // Unfortunately, we are currently not able to parallel - // join over the position storage due to slide-rs/specs#541. - // When this issue is resolved, the join below should be switched to a parallel - // join. - - // A restricted storage is used for `velocity` so as to avoid - // triggering a velocity update event when it is not actually - // modified. - for (position, mut restrict_velocity, physics, entity) in ( - &mut positions, - &mut velocities.restrict_mut(), - &physics, - &entities, - ) - .join() - { - let mut velocity = *restrict_velocity.get_unchecked(); - - let mut pending_position = position.current + velocity.0; - - // Check for blocks along path between old position and pending position. - // This prevents entities from flying through blocks when their - // velocity is sufficiently high. - let origin = position.current.into(); - let direction = (pending_position - position.current).into(); - let distance_squared = pending_position.distance_squared(position.current); - - if let Some(impacted) = - block_impacted_by_ray(&chunk_map, origin, direction, distance_squared) - { - // Set velocities along correct axis to 0 and then set position - // to just before the bbox would have impacted the block. - let face = impacted.face; - let impact = impacted.pos; - - if face.contains(Side::EAST) || face.contains(Side::WEST) { - velocity.x = 0.0; - pending_position.x = impact.x + physics.bbox.size().x * face.as_vector().x; - } - if face.contains(Side::NORTH) || face.contains(Side::SOUTH) { - velocity.z = 0.0; - pending_position.z = impact.z + physics.bbox.size().z * face.as_vector().z; - } - if face.contains(Side::TOP) || face.contains(Side::BOTTOM) { - velocity.y = 0.0; - pending_position.y = impact.y + physics.bbox.size().y * face.as_vector().y; - } - if face.contains(Side::TOP) { - pending_position.on_ground = true; - } - } - - // Check for blocks around the bbox and apply offset - // to position to stop the bbox from intersecting blocks. - let intersect = blocks_intersecting_bbox( - &chunk_map, - position.current, - pending_position, - &physics.bbox, - ); - intersect.apply_to(&mut pending_position); - - if intersect.x_affected() { +#[system] +fn entity_physics( + state: &State, + query: &mut Query<(Write, Write, Read)>, + world: &mut PreparedWorld, + land_events: &mut Trigger, +) { + // Using a mutex is fine, since land events are written very rarely + // and thus contention is low. + let land_events = Mutex::new(land_events); + + // Go through entities and update their positions according + // to their velocities. + query.par_entities_for_each(world, |(entity, (mut position, mut velocity, physics))| { + let mut pending_position = *position + velocity.0; + + // Check for blocks along path between old position and pending position. + // This prevents entities from flying through blocks when their + // velocity is sufficiently high. + let origin = (*position).into(); + let direction = (pending_position - *position).into(); + let distance_squared = pending_position.distance_squared(*position); + + if let Some(impacted) = block_impacted_by_ray(&state, origin, direction, distance_squared) { + // Set velocities along correct axis to 0 and then set position + // to just before the bbox would have impacted the block. + let face = impacted.face; + let impact = impacted.pos; + + if face.contains(Side::EAST) || face.contains(Side::WEST) { velocity.x = 0.0; + pending_position.x = impact.x + physics.bbox.size().x * face.as_vector().x; } - - if intersect.y_affected() { - velocity.y = 0.0; - } - - if intersect.z_affected() { + if face.contains(Side::NORTH) || face.contains(Side::SOUTH) { velocity.z = 0.0; + pending_position.z = impact.z + physics.bbox.size().z * face.as_vector().z; } - - // Delete entity if it has gone into unloaded chunks. - let block_at_pos = match chunk_map.block_at(pending_position.block_pos()) { - Some(block) => block, - None => { - // Delete entity. - let event = EntityDestroyEvent { entity }; - entity_destroy_events.single_write(event); - - entities.delete(entity).unwrap(); - continue; - } - }; - - // Set on ground status - pending_position.on_ground = match chunk_map.block_at( - position!( - pending_position.x, - pending_position.y - physics.bbox.size().y / 2.0 - 0.01, - pending_position.z - ) - .block_pos(), - ) { - Some(block) => block.is_solid(), - None => false, - }; - if pending_position.on_ground && !position.current.on_ground { - entity_land_events.single_write(EntityPhysicsLandEvent { - entity, - pos: pending_position, - }); + if face.contains(Side::TOP) || face.contains(Side::BOTTOM) { + velocity.y = 0.0; + pending_position.y = impact.y + physics.bbox.size().y * face.as_vector().y; } - - // Apply drag and gravity. - - // In water and lava, gravity is four times less, and velocity is multiplied by a special drag force. - let liquid_drag = 0.8; - match block_at_pos { - Block::Water(_) => { - velocity.0 *= liquid_drag; - velocity.0.y += physics.gravity / 4.0; - } - Block::Lava(_) => { - velocity.0 *= liquid_drag - 0.3; - velocity.0.y += physics.gravity / 4.0; - } - _ => { - let slip_multiplier = physics.slip_multiplier; - if pending_position.on_ground { - velocity.0.x *= slip_multiplier; - velocity.0.z *= slip_multiplier; - } else { - velocity.0.y = physics.drag * velocity.0.y + physics.gravity; - velocity.0.x *= physics.drag; - velocity.0.z *= physics.drag; - } - } + if face.contains(Side::TOP) { + pending_position.on_ground = true; } + } - // Set new position. - // A move event is triggered through FlaggedStorage. - position.current = pending_position; + // Check for blocks around the bbox and apply offset + // to position to stop the bbox from intersecting blocks. + let intersect = + blocks_intersecting_bbox(&state, *position, pending_position, &physics.bbox); + intersect.apply_to(&mut pending_position); - // Update velocity, if it changed. - if velocity != *restrict_velocity.get_unchecked() { - *restrict_velocity.get_mut_unchecked() = velocity; - } + if intersect.x_affected() { + velocity.x = 0.0; } - } -} -#[cfg(test)] -mod tests { - use super::*; - use crate::entity::test; - use crate::physics::PhysicsBuilder; - use crate::testframework as t; - use specs::{Builder, WorldExt}; + if intersect.y_affected() { + velocity.y = 0.0; + } - #[test] - fn test_unloaded_chunk() { - let (mut w, mut d) = t::builder().with(EntityPhysicsSystem, "").build(); + if intersect.z_affected() { + velocity.z = 0.0; + } - let entity = test::create(&mut w, position!(1000.0, 100.0, 1000.0)).build(); + // Delete entity if it has gone into unloaded chunks. + let block_at_pos = match state.block_at(pending_position.block_pos()) { + Some(block) => block, + None => { + // Delete entity. + state.exec(move |world| { + world.delete(entity); + }); + return; + } + }; + + // Set on ground status. + pending_position.on_ground = match state.block_at( + position!( + pending_position.x, + pending_position.y - physics.bbox.size().y / 2.0 - 0.01, + pending_position.z + ) + .block_pos(), + ) { + Some(block) => block.is_solid(), + None => false, + }; + if pending_position.on_ground && !position.on_ground { + land_events.lock().trigger(EntityPhysicsLandEvent { + entity, + pos: pending_position, + }); + } - w.write_component::() - .insert(entity, PhysicsBuilder::new().build()) - .unwrap(); + // Apply drag and gravity. - d.dispatch(&w); - w.maintain(); + // In water and lava, gravity is four times less, and velocity is multiplied by a special drag force. + let liquid_drag = 0.8; + match block_at_pos { + Block::Water(_) => { + velocity.0 *= liquid_drag; + velocity.0.y += physics.gravity / 4.0; + } + Block::Lava(_) => { + velocity.0 *= liquid_drag - 0.3; + velocity.0.y += physics.gravity / 4.0; + } + _ => { + let slip_multiplier = physics.slip_multiplier; + if pending_position.on_ground { + velocity.0.x *= slip_multiplier; + velocity.0.z *= slip_multiplier; + } else { + velocity.0.y = physics.drag * velocity.0.y + physics.gravity; + velocity.0.x *= physics.drag; + velocity.0.z *= physics.drag; + } + } + } - t::assert_removed(&w, entity); - } + // Set new position. + *position = pending_position; + }); } diff --git a/server/src/physics/math.rs b/server/src/physics/math.rs index 11f599df0..bae308495 100644 --- a/server/src/physics/math.rs +++ b/server/src/physics/math.rs @@ -1,24 +1,25 @@ //! A bunch of math-related functions for use with //! the physics system. -use crate::entity::{ChunkEntities, PositionComponent}; +use crate::chunk_entities::ChunkEntities; use crate::physics::block_bboxes::bbox_for_block; use crate::physics::AABBExt; +use crate::state::State; use feather_blocks::Block; -use feather_core::world::{BlockPosition, ChunkMap, Position}; +use feather_core::world::{BlockPosition, Position}; use feather_core::{BlockExt, ChunkPosition}; use glm::{vec3, DVec3, Vec3}; use heapless::consts::*; +use legion::entity::Entity; use nalgebra::{Isometry3, Point3}; use ncollide3d::bounding_volume::AABB; use ncollide3d::query; use ncollide3d::query::{Ray, RayCast}; use ncollide3d::shape::{Compound, Cuboid, ShapeHandle}; use smallvec::SmallVec; -use specs::storage::GenericReadStorage; -use specs::Entity; use std::cmp::Ordering; use std::f64::INFINITY; +use tonks::PreparedWorld; // TODO is a bitflag really the most // idiomatic way to do this? @@ -89,7 +90,7 @@ pub struct RayImpact { /// Traces up to `max_distance` before returning `None` /// if no block was found. pub fn block_impacted_by_ray( - chunk_map: &ChunkMap, + state: &State, origin: DVec3, ray: DVec3, max_distance_squared: f64, @@ -167,7 +168,7 @@ pub fn block_impacted_by_ray( let mut current_pos = Position::from(origin).block_pos(); while dist_traveled.magnitude_squared() < max_distance_squared { - if let Some(block) = chunk_map.block_at(current_pos) { + if let Some(block) = state.block_at(current_pos) { if block.is_solid() { // Calculate world-space position of // impact using `ncollide`. @@ -234,15 +235,12 @@ pub fn block_impacted_by_ray( /// /// # Panics /// Panics if either coordinate of the radius is negative. -pub fn nearby_entities( +pub fn nearby_entities( chunk_entities: &ChunkEntities, - positions: &S, + world: &PreparedWorld, pos: Position, radius: DVec3, -) -> SmallVec<[Entity; 4]> -where - S: GenericReadStorage, -{ +) -> SmallVec<[Entity; 4]> { assert!(radius.x >= 0.0); assert!(radius.y >= 0.0); assert!(radius.z >= 0.0); @@ -255,9 +253,8 @@ where .iter() .copied() .filter(|e| { - let epos = positions.get(*e); + let epos = world.get_component::(*e); if let Some(epos) = epos { - let epos = epos.current; (epos.x - pos.x).abs() <= radius.x && (epos.y - pos.y).abs() <= radius.y && (epos.z - pos.z).abs() <= radius.z @@ -318,7 +315,7 @@ impl BlockIntersect { /// than 1 are not supported. If the bounding box's size /// is more than 1, this function will panic. pub fn blocks_intersecting_bbox( - chunk_map: &ChunkMap, + state: &State, mut from: Position, mut dest: Position, bbox: &AABB, @@ -351,7 +348,7 @@ pub fn blocks_intersecting_bbox( let mut checked = heapless::FnvIndexSet::new(); for (axis, sign) in &axis { - let compound = adjacent_to_bbox(*axis, *sign, bbox, dest, &chunk_map, &mut checked); + let compound = adjacent_to_bbox(*axis, *sign, bbox, dest, &state, &mut checked); blocks.push(compound); } @@ -426,7 +423,7 @@ pub fn adjacent_to_bbox( sign: i32, bbox: &AABB, pos: Position, - chunk_map: &ChunkMap, + state: &State, checked: &mut heapless::FnvIndexSet, ) -> Compound { assert!(axis <= 2); @@ -486,7 +483,7 @@ pub fn adjacent_to_bbox( continue; } - match chunk_map.block_at(block_pos) { + match state.block_at(block_pos) { Some(block) => { if block.is_solid() { checked.insert(block_pos).unwrap(); diff --git a/server/src/physics/mod.rs b/server/src/physics/mod.rs index c3423d4c7..aeec599ef 100644 --- a/server/src/physics/mod.rs +++ b/server/src/physics/mod.rs @@ -5,16 +5,6 @@ mod component; mod entity; mod math; -use crate::systems::ENTITY_PHYSICS; -pub use component::{AABBExt, PhysicsBuilder, PhysicsComponent}; -pub use entity::{EntityPhysicsLandEvent, EntityPhysicsSystem}; +pub use component::{AABBExt, Physics, PhysicsBuilder}; +pub use entity::EntityPhysicsLandEvent; pub use math::*; -use specs::DispatcherBuilder; - -pub fn init_logic(dispatcher: &mut DispatcherBuilder) { - dispatcher.add(EntityPhysicsSystem, ENTITY_PHYSICS, &[]); -} - -pub fn init_handlers(_dispatcher: &mut DispatcherBuilder) { - // nothing -} diff --git a/server/src/player/animation.rs b/server/src/player/animation.rs deleted file mode 100644 index acf600b75..000000000 --- a/server/src/player/animation.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::network::PacketQueue; -use crate::util::Util; -use feather_core::network::cast_packet; -use feather_core::network::packet::implementation::{AnimationClientbound, AnimationServerbound}; -use feather_core::network::packet::PacketType; -use feather_core::{ClientboundAnimation, Hand}; -use shrev::EventChannel; -use specs::SystemData; -use specs::{Entity, Read, ReaderId, System, World, Write}; - -/// Event which is triggered when a player causes -/// an animation. -#[derive(Debug, Clone)] -pub struct PlayerAnimationEvent { - pub player: Entity, - pub animation: ClientboundAnimation, -} - -/// System for handling Animation Serverbound packets -/// and then triggering a `PlayerAnimationEvent`. -pub struct PlayerAnimationSystem; - -impl<'a> System<'a> for PlayerAnimationSystem { - type SystemData = ( - Write<'a, EventChannel>, - Read<'a, PacketQueue>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut events, packet_queue) = data; - - // Handle Animation Serverbound packets. - let packets = packet_queue.for_packet(PacketType::AnimationServerbound); - - for (player, packet) in packets { - let packet = cast_packet::(&*packet); - - let animation = match packet.hand { - Hand::Main => ClientboundAnimation::SwingMainArm, - Hand::Off => ClientboundAnimation::SwingOffhand, - }; - - let event = PlayerAnimationEvent { player, animation }; - events.single_write(event); - } - } -} - -/// System for broadcasting when a player causes an animation. -/// This system listens to `PlayerAnimationEvent`s. -#[derive(Default)] -pub struct AnimationBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for AnimationBroadcastSystem { - type SystemData = (Read<'a, EventChannel>, Read<'a, Util>); - - fn run(&mut self, data: Self::SystemData) { - let (events, util) = data; - - for event in events.read(&mut self.reader.as_mut().unwrap()) { - // Broadcast animation - let packet = AnimationClientbound::new(event.player.id() as i32, event.animation); - - util.broadcast_entity_update(event.player, packet, Some(event.player)) - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testframework as t; - use feather_core::network::packet::implementation::{ - AnimationClientbound, AnimationServerbound, - }; - use specs::WorldExt; - - #[test] - fn test_animation_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let packet = AnimationServerbound::new(Hand::Main); - t::receive_packet(&player, &w, packet); - - let mut event_reader = t::reader::(&w); - - d.dispatch(&w); - w.maintain(); - - let channel = w.fetch::>(); - - let events = channel.read(&mut event_reader).collect::>(); - assert_eq!(events.len(), 1); - let first = events.first().unwrap(); - - assert_eq!(first.player, player.entity); - assert_eq!(first.animation, ClientboundAnimation::SwingMainArm); - } - - #[test] - fn test_animation_broadcast_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - - let event = PlayerAnimationEvent { - player: player.entity, - animation: ClientboundAnimation::SwingMainArm, - }; - - t::trigger_event(&w, event.clone()); - - d.dispatch(&w); - w.maintain(); - - // Make sure animation wasn't sent to the player itself - t::assert_packet_not_received(&player, PacketType::AnimationClientbound); - - let packet = t::assert_packet_received(&player2, PacketType::AnimationClientbound); - let packet = cast_packet::(&*packet); - - assert_eq!(packet.entity_id, event.player.id() as i32); - assert_eq!(packet.animation, event.animation); - } -} diff --git a/server/src/player/broadcast.rs b/server/src/player/broadcast.rs deleted file mode 100644 index 40fe02c73..000000000 --- a/server/src/player/broadcast.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::config::Config; -use crate::entity::{ChunkEntities, NamedComponent, PlayerComponent, PositionComponent}; -use crate::joinhandler::PlayerJoinEvent; -use crate::lazy::LazyUpdateExt; -use crate::network::{send_packet_to_all_players, send_packet_to_player, NetworkComponent}; -use crate::player::chat::ChatBroadcastEvent; -use feather_core::network::packet::implementation::{PlayerInfo, PlayerInfoAction}; -use feather_core::Gamemode; -use shrev::EventChannel; -use specs::{Entities, Entity, Join, Read, ReadStorage, ReaderId, System, World, Write}; -use specs::{LazyUpdate, SystemData}; -use std::sync::Arc; -use uuid::Uuid; - -/// System for broadcasting when a player joins -/// the game. -/// -/// This system only broadcasts the -/// Player Info packet necessary to view to player -/// in the tablist - the `EntityBroadcastSystem` handles -/// the Spawn Player packet. -#[derive(Default)] -pub struct JoinBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for JoinBroadcastSystem { - type SystemData = ( - Read<'a, EventChannel>, - ReadStorage<'a, PositionComponent>, - ReadStorage<'a, NamedComponent>, - ReadStorage<'a, PlayerComponent>, - ReadStorage<'a, NetworkComponent>, - Write<'a, EventChannel>, - Read<'a, ChunkEntities>, - Read<'a, LazyUpdate>, - Read<'a, Arc>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - join_events, - positions, - nameds, - player_comps, - net_comps, - mut chat, - chunk_entities, - lazy, - config, - entities, - ) = data; - - for event in join_events.read(&mut self.reader.as_mut().unwrap()) { - // Broadcast join - let position = positions.get(event.player).unwrap(); - let named = nameds.get(event.player).unwrap(); - let player_comp = player_comps.get(event.player).unwrap(); - - let player_info = get_player_initialization_packet(position, named, player_comp); - - send_packet_to_all_players(&net_comps, &entities, player_info, None); - - let net_comp = net_comps.get(event.player).unwrap(); - - // Send existing players to new player - for (position, named, player_comp, entity) in - (&positions, &nameds, &player_comps, &entities).join() - { - if entity != event.player { - let player_info = - get_player_initialization_packet(position, named, player_comp); - send_packet_to_player(net_comp, player_info); - } - } - - // Send entities within view distance to new player - for entity in chunk_entities.entites_within_view_distance( - position.current.chunk_pos(), - config.server.view_distance, - ) { - if entity != event.player { - lazy.send_entity_to_player(event.player, entity); - } - } - - // Broadcast join message in chat - let message = json!({ - "translate": "multiplayer.player.joined", - "color": "yellow", - "with": [ - {"text": named.display_name}, - ], - }) - .to_string(); - chat.single_write(ChatBroadcastEvent { message }); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} - -/// Returns the player info packet -/// for the given player. -fn get_player_initialization_packet( - _position: &PositionComponent, - named: &NamedComponent, - pcomp: &PlayerComponent, -) -> PlayerInfo { - let display_name = json!({ - "text": named.display_name - }) - .to_string(); - - let mut props = vec![]; - for prop in pcomp.profile_properties.iter() { - props.push(( - prop.name.clone(), - prop.value.clone(), - prop.signature.clone(), - )); - } - - let action = PlayerInfoAction::AddPlayer( - named.display_name.clone(), - props, - Gamemode::Creative, - 50, - display_name, - ); - PlayerInfo::new(action, named.uuid) -} - -/// Event which is called when a player disconnected. -pub struct PlayerDisconnectEvent { - pub player: Entity, - pub reason: String, - pub uuid: Uuid, -} - -/// System for broadcasting when a player disconnects. -#[derive(Default)] -pub struct DisconnectBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for DisconnectBroadcastSystem { - type SystemData = ( - ReadStorage<'a, NamedComponent>, - ReadStorage<'a, NetworkComponent>, - Write<'a, EventChannel>, - Read<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (nameds, networks, mut chat, disconnect_events) = data; - - for event in disconnect_events.read(&mut self.reader.as_mut().unwrap()) { - // Broadcast disconnect. - // Note that the Destroy Entity packet is sent - // in a separate system (crate::entity::EntityDestroyBroadcastSystem). - // This system only updates the tablist for all clients. - let player_info = PlayerInfo::new(PlayerInfoAction::RemovePlayer, event.uuid); - - for net in (&networks).join() { - send_packet_to_player(net, player_info.clone()); - } - - let named = nameds.get(event.player).unwrap(); - - // Broadcast chat message. - let message = json!({ - "translate": "multiplayer.player.left", - "color": "yellow", - "with": [ - {"text": named.display_name}, - ], - }) - .to_string(); - let event = ChatBroadcastEvent { message }; - chat.single_write(event); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} diff --git a/server/src/player/chat.rs b/server/src/player/chat.rs deleted file mode 100644 index 60e7db460..000000000 --- a/server/src/player/chat.rs +++ /dev/null @@ -1,154 +0,0 @@ -use shrev::EventChannel; -use specs::SystemData; -use specs::{Entities, Read, ReadStorage, ReaderId, System, World, Write}; - -use feather_core::network::cast_packet; -use feather_core::network::packet::implementation::{ - ChatMessageClientbound, ChatMessageServerbound, -}; -use feather_core::network::packet::PacketType; - -use crate::entity::NamedComponent; -use crate::network::{send_packet_to_all_players, NetworkComponent, PacketQueue}; - -/// Event which is triggered when a new chat message is to be broadcasted to the whole server. -#[derive(Debug, Clone)] -pub struct ChatBroadcastEvent { - pub message: String, -} - -/// System for handling Chat Message Serverbound packets -/// and then triggering a `ChatBroadcastEvent`. -pub struct PlayerChatSystem; - -impl<'a> System<'a> for PlayerChatSystem { - type SystemData = ( - Write<'a, EventChannel>, - ReadStorage<'a, NamedComponent>, - Read<'a, PacketQueue>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut events, nameds, packet_queue) = data; - - // Handle Chat Message Serverbound packets. - let packets = packet_queue.for_packet(PacketType::ChatMessageServerbound); - - for (player, packet) in packets { - let packet = cast_packet::(&*packet); - let message = packet.message.clone(); - let player_name = &nameds.get(player).unwrap().display_name; - - // TODO: could use a more robust chat-component library. - let message_json = json!({ - "translate": "chat.type.text", - "with": [ - {"text": player_name}, - {"text": message}, - ], - }) - .to_string(); - - let event = ChatBroadcastEvent { - message: message_json, - }; - events.single_write(event); - - // Log in the console - info!("<{}> {}", player_name, message); - } - } -} - -/// System for broadcasting chat messages. -/// This system listens to `ChatBroadcastEvent`s. -#[derive(Default)] -pub struct ChatBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for ChatBroadcastSystem { - type SystemData = ( - Read<'a, EventChannel>, - ReadStorage<'a, NetworkComponent>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (events, networks, entities) = data; - - for event in events.read(&mut self.reader.as_mut().unwrap()) { - let message = event.message.clone(); - - // Broadcast chat message - let packet = ChatMessageClientbound { - json_data: message, - position: 0, - }; - - send_packet_to_all_players(&networks, &entities, packet, None); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testframework as t; - use feather_core::network::packet::implementation::ChatMessageServerbound; - use specs::WorldExt; - - #[test] - fn test_chat_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let packet = ChatMessageServerbound { - message: String::from("test"), - }; - t::receive_packet(&player, &w, packet); - - let mut event_reader = t::reader::(&w); - - d.dispatch(&w); - w.maintain(); - - let channel = w.fetch::>(); - - let events = channel.read(&mut event_reader).collect::>(); - assert_eq!(events.len(), 1); - } - - #[test] - fn test_chat_broadcast_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - - let event = ChatBroadcastEvent { - message: String::from("test"), - }; - - t::trigger_event(&w, event); - - d.dispatch(&w); - w.maintain(); - - t::assert_packet_received(&player, PacketType::ChatMessageClientbound); - let packet = t::assert_packet_received(&player2, PacketType::ChatMessageClientbound); - let packet = cast_packet::(&*packet); - assert_eq!(packet.json_data, String::from("test")); - } -} diff --git a/server/src/player/digging.rs b/server/src/player/digging.rs deleted file mode 100644 index 6b1d51466..000000000 --- a/server/src/player/digging.rs +++ /dev/null @@ -1,817 +0,0 @@ -//! This module handles the monolithic Player Digging packet. -//! -//! The packet's name is rather misleading, as it is also sent -//! for completely unrelated actions, including eating, shooting bows, -//! swapping items out the the offhand, and dropping items. - -use specs::{Entity, LazyUpdate, Read, ReadStorage, ReaderId, System, World, Write, WriteStorage}; - -use feather_core::network::cast_packet; -use feather_core::network::packet::implementation::{ - BlockChange, PlayerDigging, PlayerDiggingStatus, -}; -use feather_core::network::packet::PacketType; -use feather_core::world::block::{Block, BlockExt}; -use feather_core::world::ChunkMap; -use feather_core::{Gamemode, Item, Position}; - -use crate::blocks::{BlockUpdateCause, BlockUpdateEvent}; -use crate::disconnect_player; -use crate::entity::{PlayerComponent, PositionComponent, ShootArrowEvent}; -use crate::network::PacketQueue; -use crate::player::{InventoryComponent, InventoryUpdateEvent}; -use crate::util::Util; -use feather_core::inventory::{ItemStack, SlotIndex, SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}; -use shrev::EventChannel; -use specs::SystemData; - -/// Event triggered when a player drops an item. -/// -/// Before this event is triggered, the item -/// is removed from the player's inventory. -#[derive(Debug, Clone)] -pub struct PlayerItemDropEvent { - /// The slot from which the item was dropped, - /// if known. - pub slot: Option, - /// The item stack which was dropped. - pub stack: ItemStack, - /// The player who dropped the item. - pub player: Entity, -} - -/// System responsible for polling for PlayerDigging -/// packets and writing the corresponding events. -pub struct PlayerDiggingSystem; - -impl<'a> System<'a> for PlayerDiggingSystem { - type SystemData = ( - WriteStorage<'a, InventoryComponent>, - ReadStorage<'a, PlayerComponent>, // For gamemodes - ReadStorage<'a, PositionComponent>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Write<'a, ChunkMap>, - Read<'a, PacketQueue>, - Read<'a, LazyUpdate>, - ); - - fn run(&mut self, data: Self::SystemData) { - use PlayerDiggingStatus::*; - - let ( - mut inventories, - players, - positions, - mut block_breaks, - mut item_drops, - mut inventory_updates, - mut shoot_arrow_events, - mut chunk_map, - packet_queue, - lazy, - ) = data; - - let packets = packet_queue.for_packet(PacketType::PlayerDigging); - - for (player, packet) in packets { - let packet = cast_packet::(&*packet); - - match packet.status { - StartedDigging | FinishedDigging | CancelledDigging => handle_digging( - packet, - players.get(player).unwrap(), - inventories.get(player).unwrap().item_in_main_hand(), - player, - &mut block_breaks, - &mut chunk_map, - &lazy, - ), - DropItem | DropItemStack => handle_drop_item_stack( - packet, - player, - &mut inventory_updates, - &mut item_drops, - inventories.get_mut(player).unwrap(), - ), - ConsumeItem => handle_consume_item( - packet, - players.get(player).unwrap(), - player, - inventories.get_mut(player).unwrap(), - &mut inventory_updates, - positions.get(player).unwrap().current, - &mut shoot_arrow_events, - ), - status => warn!("Unhandled Player Digging status {:?}", status), - } - } - } -} - -fn handle_digging( - packet: &PlayerDigging, - player: &PlayerComponent, - item_in_main_hand: Option<&ItemStack>, - entity: Entity, - events: &mut EventChannel, - chunk_map: &mut ChunkMap, - lazy: &LazyUpdate, -) { - // Return early if needed - match packet.status { - PlayerDiggingStatus::StartedDigging => { - if player.gamemode != Gamemode::Creative { - return; - } - } - PlayerDiggingStatus::CancelledDigging => return, - _ => (), - } - - // Don't break block if player is holding a sword in creative mode. - if player.gamemode == Gamemode::Creative { - if let Some(item_in_main_hand) = item_in_main_hand { - match item_in_main_hand.ty { - Item::WoodenSword - | Item::StoneSword - | Item::GoldenSword - | Item::IronSword - | Item::DiamondSword => return, - _ => (), - } - } - } - - let old = chunk_map.block_at(packet.location); - - if chunk_map.set_block_at(packet.location, Block::Air).is_err() { - disconnect_player( - entity, - "Attempted to break block in unloaded chunk".to_string(), - lazy, - ); - return; - } - - let event = BlockUpdateEvent { - cause: BlockUpdateCause::Player(entity), - pos: packet.location, - old_block: old.unwrap(), // We checked that the location was valid above - new_block: Block::Air, - }; - - events.single_write(event); -} - -fn handle_drop_item_stack( - packet: &PlayerDigging, - entity: Entity, - inventory_updates: &mut EventChannel, - item_drops: &mut EventChannel, - inventory: &mut InventoryComponent, -) { - assert!( - packet.status == PlayerDiggingStatus::DropItem - || packet.status == PlayerDiggingStatus::DropItemStack - ); - - let slot = inventory.held_item + SLOT_HOTBAR_OFFSET; - - let stack = { - if let Some(item) = inventory.item_at(slot) { - item.clone() - } else { - // Silently fail - no item stack to drop - return; - } - }; - - let amnt = match packet.status { - PlayerDiggingStatus::DropItem => { - if stack.amount == 0 { - inventory.clear_item_at(slot); - 0 - } else if stack.amount == 1 { - inventory.clear_item_at(slot); - 1 - } else { - inventory.set_item_at(slot, ItemStack::new(stack.ty, stack.amount - 1)); - 1 - } - } - PlayerDiggingStatus::DropItemStack => { - inventory.clear_item_at(slot); - stack.amount - } - _ => unreachable!(), // Assertion above - }; - - let inv_update = InventoryUpdateEvent { - slots: smallvec![slot], - player: entity, - }; - inventory_updates.single_write(inv_update); - - if amnt != 0 { - let item_drop = PlayerItemDropEvent { - slot: Some(slot), - stack: ItemStack::new(stack.ty, amnt), - player: entity, - }; - item_drops.single_write(item_drop); - } -} - -/// Handles food consumption and shooting arrows. -fn handle_consume_item( - packet: &PlayerDigging, - player: &PlayerComponent, - entity: Entity, - inventory: &mut InventoryComponent, - inventory_updates: &mut EventChannel, - position: Position, - shoot_arrow_events: &mut EventChannel, -) { - assert_eq!(packet.status, PlayerDiggingStatus::ConsumeItem); - - // TODO: Fallback to off-hand if main-hand is not a consumable - let used_item = inventory.item_in_main_hand(); - - if let Some(item) = used_item { - if item.ty == Item::Bow { - handle_shoot_bow( - player, - entity, - inventory, - inventory_updates, - position, - shoot_arrow_events, - ); - } - // TODO: Food, potions - } -} - -fn handle_shoot_bow( - player: &PlayerComponent, - entity: Entity, - inventory: &mut InventoryComponent, - inventory_updates: &mut EventChannel, - position: Position, - shoot_arrow_events: &mut EventChannel, -) { - let arrow_to_consume: Option<(SlotIndex, ItemStack)> = find_arrow(&inventory); - if player.gamemode == Gamemode::Survival || player.gamemode == Gamemode::Adventure { - // If no arrow was found, don't shoot - let arrow_to_consume = arrow_to_consume.clone(); - if arrow_to_consume.is_none() { - debug!("Tried to shoot bow with no arrows."); - return; - } - - // Consume arrow - let (arrow_slot, arrow_stack) = arrow_to_consume.unwrap(); - let mut arrow_stack: ItemStack = arrow_stack; - arrow_stack.amount -= 1; - - inventory.set_item_at(arrow_slot, arrow_stack); - inventory_updates.single_write(InventoryUpdateEvent { - slots: smallvec![arrow_slot], - player: entity, - }); - } - - let arrow_type: Item = match arrow_to_consume { - None => Item::Arrow, // Default to generic arrow in creative mode with none in inventory - Some((_, arrow_stack)) => arrow_stack.ty, - }; - - shoot_arrow_events.single_write(ShootArrowEvent { - shooter: Some(entity), - position, - arrow_type, - critical: false, // TODO: Determine critical based on how long bow was pulled back - }); -} - -fn find_arrow(inventory: &InventoryComponent) -> Option<(SlotIndex, ItemStack)> { - // Order of priority is: off-hand, hotbar (0 to 8), rest of inventory - - if let Some(offhand) = inventory.item_at(SLOT_OFFHAND) { - if is_arrow_item(offhand.ty) { - return Some((SLOT_OFFHAND, offhand.clone())); - } - } - - for hotbar_slot in 0..9 { - if let Some(hotbar_stack) = inventory.item_at(SLOT_HOTBAR_OFFSET + hotbar_slot) { - if is_arrow_item(hotbar_stack.ty) { - return Some((SLOT_HOTBAR_OFFSET + hotbar_slot, hotbar_stack.clone())); - } - } - } - - for inv_slot in 9..=35 { - if let Some(inv_stack) = inventory.item_at(inv_slot) { - if is_arrow_item(inv_stack.ty) { - return Some((inv_slot, inv_stack.clone())); - } - } - } - None -} - -fn is_arrow_item(item: Item) -> bool { - match item { - Item::Arrow | Item::SpectralArrow | Item::TippedArrow => true, - _ => false, - } -} - -/// System for broadcasting block update -/// events to all clients. -/// -/// This system listens to `BlockUpdateEvent`s. -#[derive(Default)] -pub struct BlockUpdateBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for BlockUpdateBroadcastSystem { - type SystemData = (Read<'a, EventChannel>, Read<'a, Util>); - - fn run(&mut self, data: Self::SystemData) { - let (events, util) = data; - - // Process events - for event in events.read(&mut self.reader.as_mut().unwrap()) { - // Send Block Change packet to every player, - // except for the one that performed the update - // (if any) - let neq = if let BlockUpdateCause::Player(player) = event.cause { - Some(player) - } else { - None - }; - - let packet = BlockChange::new(event.pos, i32::from(event.new_block.native_state_id())); - util.broadcast_chunk_update(event.pos.chunk_pos(), packet, neq); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some(world.fetch_mut::>().register_reader()); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::blocks::BlockUpdateEvent; - use crate::testframework as t; - use feather_core::item::Item; - use feather_core::world::chunk::Chunk; - use feather_core::world::{BlockPosition, ChunkPosition}; - use specs::WorldExt; - - #[test] - fn test_started_digging() { - let (mut w, mut d) = t::init_world(); - - let cpos = ChunkPosition::new(0, 0); - let bpos = BlockPosition::new(0, 0, 0); - let mut chunk = Chunk::new(cpos); - chunk.set_block_at(0, 0, 0, Block::Stone); - w.fetch_mut::().set_chunk_at(cpos, chunk); - - let mut event_reader = t::reader(&w); - - // Creative mode - - let player = t::add_player(&mut w); - - let packet = PlayerDigging::new(PlayerDiggingStatus::StartedDigging, bpos, 0); - - t::receive_packet(&player, &w, packet.clone()); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - { - let mut chunk_map = w.fetch_mut::(); - - assert_eq!(chunk_map.block_at(bpos).unwrap(), Block::Air); - - chunk_map.set_block_at(bpos, Block::Stone).unwrap(); - - let channel = w.fetch_mut::>(); - let events = channel.read(&mut event_reader).collect::>(); - assert_eq!(events.len(), 1); - - let first = events.first().unwrap(); - assert_eq!(first.old_block, Block::Stone); - assert_eq!(first.new_block, Block::Air); - assert_eq!(first.cause, BlockUpdateCause::Player(player.entity)); - assert_eq!(first.pos, bpos); - } - - // Survival mode - let player = t::add_player(&mut w); - w.write_component::() - .get_mut(player.entity) - .unwrap() - .gamemode = Gamemode::Survival; - - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - let chunk_map = w.fetch::(); - assert_eq!(chunk_map.block_at(bpos).unwrap(), Block::Stone); - - let channel = w.fetch_mut::>(); - let events = channel.read(&mut event_reader).collect::>(); - assert_eq!(events.len(), 0); - } - - // This should be a no-op. - #[test] - fn test_cancelled_digging() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let mut event_reader = t::reader(&w); - - let packet = PlayerDigging::new( - PlayerDiggingStatus::CancelledDigging, - BlockPosition::default(), - 0, - ); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - let channel = w.fetch::>(); - let events = channel.read(&mut event_reader).collect::>(); - assert!(events.is_empty()); - } - - #[test] - fn test_finished_digging() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - w.write_component::() - .get_mut(player.entity) - .unwrap() - .gamemode = Gamemode::Survival; - - let mut event_reader = t::reader(&w); - - let bpos = BlockPosition::new(0, 0, 0); - let cpos = bpos.chunk_pos(); - - let mut chunk = Chunk::new(cpos); - chunk.set_block_at(0, 0, 0, Block::Stone); - - w.fetch_mut::().set_chunk_at(cpos, chunk); - - let packet = PlayerDigging::new(PlayerDiggingStatus::FinishedDigging, bpos, 0); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - let chunk_map = w.fetch::(); - - assert_eq!(chunk_map.block_at(bpos).unwrap(), Block::Air); - - let channel = w.fetch_mut::>(); - let events = channel.read(&mut event_reader).collect::>(); - assert_eq!(events.len(), 1); - - let first = events.first().unwrap(); - assert_eq!(first.old_block, Block::Stone); - assert_eq!(first.new_block, Block::Air); - assert_eq!(first.cause, BlockUpdateCause::Player(player.entity)); - assert_eq!(first.pos, bpos); - } - - #[test] - fn test_block_break_in_unloaded_chunk() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let mut event_reader = t::reader(&w); - - let bpos = BlockPosition::new(1000, 25, 1000); - - let packet = PlayerDigging::new(PlayerDiggingStatus::FinishedDigging, bpos, 0); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_disconnected(&player); - - let channel = w.fetch::>(); - let events = channel.read(&mut event_reader).collect::>(); - assert!(events.is_empty()); - } - - #[test] - fn test_drop_item() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let slot = SLOT_HOTBAR_OFFSET; - { - let mut invs = w.write_component::(); - let inv = invs.get_mut(player.entity).unwrap(); - inv.held_item = 0; - inv.set_item_at(slot, ItemStack::new(Item::CookedBeef, 4)); - } - - let mut drop_reader = t::reader(&w); - let mut update_reader = t::reader(&w); - - let packet = PlayerDigging::new(PlayerDiggingStatus::DropItem, BlockPosition::default(), 0); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - let drop_channel = w.fetch::>(); - let update_channel = w.fetch::>(); - - // Check that events are correct - let drop_events = drop_channel.read(&mut drop_reader).collect::>(); - assert_eq!(drop_events.len(), 1); - let first = drop_events.first().unwrap(); - assert_eq!(first.player, player.entity); - assert_eq!(first.slot, Some(slot)); - assert_eq!(first.stack, ItemStack::new(Item::CookedBeef, 1)); // 1 beef was dropped - - let update_events = update_channel.read(&mut update_reader).collect::>(); - assert_eq!(update_events.len(), 1); - let first = update_events.first().unwrap(); - assert_eq!(first.player, player.entity); - assert_eq!(first.slots.as_slice(), &[slot]); - - // Check that inventory was updated correctly - let invs = w.read_component::(); - let inv = invs.get(player.entity).unwrap(); - assert_eq!( - inv.item_at(slot).unwrap(), - &ItemStack::new(Item::CookedBeef, 3) - ); // 1 was removed - } - - #[test] - fn test_drop_item_no_stack() { - // This should be a no-op. - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let mut drop_reader = t::reader(&w); - let mut update_reader = t::reader(&w); - - let packet = PlayerDigging::new(PlayerDiggingStatus::DropItem, BlockPosition::default(), 0); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - let drop_channel = w.fetch::>(); - let update_channel = w.fetch::>(); - - let drop_events = drop_channel.read(&mut drop_reader).collect::>(); - assert!(drop_events.is_empty()); - let update_events = update_channel.read(&mut update_reader).collect::>(); - assert!(update_events.is_empty()); - } - - #[test] - fn test_drop_item_stack() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let mut drop_reader = t::reader(&w); - let mut update_reader = t::reader(&w); - - let slot = SLOT_HOTBAR_OFFSET; - let amnt = 32; - { - let mut invs = w.write_component::(); - let inv = invs.get_mut(player.entity).unwrap(); - inv.set_item_at(slot, ItemStack::new(Item::CookedBeef, amnt)); - } - - let packet = PlayerDigging::new( - PlayerDiggingStatus::DropItemStack, - BlockPosition::default(), - 0, - ); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - let drop_channel = w.fetch::>(); - let update_channel = w.fetch::>(); - - let update_events = update_channel.read(&mut update_reader).collect::>(); - assert_eq!(update_events.len(), 1); - let first = update_events.first().unwrap(); - assert_eq!(first.player, player.entity); - assert_eq!(first.slots.as_slice(), &[slot]); - - let drop_events = drop_channel.read(&mut drop_reader).collect::>(); - assert_eq!(drop_events.len(), 1); - let first = drop_events.first().unwrap(); - assert_eq!(first.player, player.entity); - assert_eq!(first.slot, Some(slot)); - assert_eq!(first.stack, ItemStack::new(Item::CookedBeef, amnt)); - - let invs = w.read_component::(); - let inv = invs.get(player.entity).unwrap(); - assert_eq!(inv.item_at(slot), None); - } - - #[test] - fn test_block_update_broadcast_system() { - let (mut w, mut d) = t::init_world(); - - let player1 = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - - let pos = BlockPosition::default(); - let block = Block::Sand; - - let event = BlockUpdateEvent { - cause: BlockUpdateCause::Player(player1.entity), - pos, - old_block: block, - new_block: Block::Air, - }; - w.fetch_mut::>().single_write(event); - - d.dispatch(&w); - w.maintain(); - - let block_change = t::assert_packet_received(&player2, PacketType::BlockChange); - let block_change = cast_packet::(&*block_change); - assert_eq!(block_change.location, pos); - assert_eq!( - block_change.block_id, - i32::from(Block::Air.native_state_id()) - ); - - t::assert_packet_not_received(&player1, PacketType::BlockChange); // Don't send update to own player - - // Now handle an event not caused by a player - let event = BlockUpdateEvent { - cause: BlockUpdateCause::Test, - pos, - old_block: block, - new_block: Block::Air, - }; - w.fetch_mut::>().single_write(event); - - d.dispatch(&w); - w.maintain(); - - // Packet should be sent to both players - t::assert_packet_received(&player1, PacketType::BlockChange); - t::assert_packet_received(&player2, PacketType::BlockChange); - } - - #[test] - pub fn test_find_arrow() { - let mut inv = InventoryComponent::new(); - inv.set_item_at( - SLOT_OFFHAND, - ItemStack { - ty: Item::Arrow, - amount: 1, - }, - ); - inv.set_item_at( - SLOT_HOTBAR_OFFSET, - ItemStack { - ty: Item::Arrow, - amount: 1, - }, - ); - inv.set_item_at( - 9, - ItemStack { - ty: Item::Arrow, - amount: 1, - }, - ); - - // 1. Off-hand - let (slot, stack) = find_arrow(&inv).unwrap(); - assert_eq!(slot, SLOT_OFFHAND); - assert_eq!(stack.ty, Item::Arrow); - inv.clear_item_at(SLOT_OFFHAND); - - // 2. Hot-bar - let (slot, stack) = find_arrow(&inv).unwrap(); - assert_eq!(slot, SLOT_HOTBAR_OFFSET); - assert_eq!(stack.ty, Item::Arrow); - inv.clear_item_at(SLOT_HOTBAR_OFFSET); - - // 3. Rest of inventory - let (slot, stack) = find_arrow(&inv).unwrap(); - assert_eq!(slot, 9); - assert_eq!(stack.ty, Item::Arrow); - inv.clear_item_at(9); - - // 4. No arrow found - assert!(find_arrow(&inv).is_none()); - } - - #[test] - pub fn test_shoot_arrow() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let mut shoot_reader = t::reader(&w); - let mut update_reader = t::reader(&w); - - let slot = SLOT_HOTBAR_OFFSET; - let amnt = 32; - { - let mut invs = w.write_component::(); - let inv = invs.get_mut(player.entity).unwrap(); - inv.set_item_at(slot, ItemStack::new(Item::Bow, 1)); - inv.set_item_at(slot + 1, ItemStack::new(Item::Arrow, amnt)); - } - - // Change to survival - w.write_component::() - .insert( - player.entity, - PlayerComponent { - gamemode: Gamemode::Survival, - profile_properties: vec![], - }, - ) - .unwrap(); - - let packet = PlayerDigging::new( - PlayerDiggingStatus::ConsumeItem, - BlockPosition::default(), - 0, - ); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - let shoot_channel = w.fetch::>(); - let update_channel = w.fetch::>(); - - let update_events = update_channel.read(&mut update_reader).collect::>(); - assert_eq!(update_events.len(), 1); - let first = update_events.first().unwrap(); - assert_eq!(first.player, player.entity); - assert_eq!(first.slots.as_slice(), &[slot + 1]); - - let shoot_events = shoot_channel.read(&mut shoot_reader).collect::>(); - assert_eq!(shoot_events.len(), 1); - let first = shoot_events.first().unwrap(); - assert_eq!(first.shooter.unwrap(), player.entity); - assert_eq!(first.arrow_type, Item::Arrow); - - // In survival, check if amount of arrow stack decreased. - let invs = w.read_component::(); - let inv = invs.get(player.entity).unwrap(); - assert_eq!(inv.item_at(slot + 1).unwrap().amount, amnt - 1); - } -} diff --git a/server/src/player/init.rs b/server/src/player/init.rs deleted file mode 100644 index a0c4d83aa..000000000 --- a/server/src/player/init.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::entity::{ - degrees_to_stops, LastKnownPositionComponent, PacketCreatorComponent, PlayerComponent, - VelocityComponent, -}; -use crate::entity::{Metadata, NamedComponent, PositionComponent}; -use crate::network::PlayerPreJoinEvent; -use crate::player::{ChunkPendingComponent, InventoryComponent, LoadedChunksComponent}; -use crate::prelude::*; -use feather_core::level::LevelData; -use feather_core::packet::SpawnPlayer; -use feather_core::{Gamemode, Packet}; -use hashbrown::HashSet; -use shrev::{EventChannel, ReaderId}; -use specs::{Entity, SystemData, WorldExt}; -use specs::{Read, System, World, WriteStorage}; -use std::path::Path; -use std::sync::Arc; - -/// System for initializing the necessary components -/// when a player joins. -#[derive(Default)] -pub struct PlayerInitSystem { - join_event_reader: Option>, -} - -impl<'a> System<'a> for PlayerInitSystem { - type SystemData = ( - Read<'a, EventChannel>, - WriteStorage<'a, PlayerComponent>, - WriteStorage<'a, PositionComponent>, - WriteStorage<'a, VelocityComponent>, - WriteStorage<'a, NamedComponent>, - WriteStorage<'a, ChunkPendingComponent>, - WriteStorage<'a, LoadedChunksComponent>, - WriteStorage<'a, InventoryComponent>, - WriteStorage<'a, Metadata>, - WriteStorage<'a, LastKnownPositionComponent>, - WriteStorage<'a, PacketCreatorComponent>, - Read<'a, LevelData>, - Read<'a, Arc>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - join_events, - mut player_comps, - mut positions, - mut velocities, - mut nameds, - mut chunk_pending_comps, - mut loaded_chunk_comps, - mut inventory_comps, - mut metadata, - mut last_positions, - mut packet_creators, - level, - config, - ) = data; - - // Run through events - for event in join_events.read(&mut self.join_event_reader.as_mut().unwrap()) { - // Load player data - let uuid = event.uuid; - // If this is a new player, set gamemode to server's default (config) - let default_gamemode = &config.server.default_gamemode.clone(); - let world_dir = Path::new(&config.world.name); - - debug!("Loading player data for UUID {}", uuid); - let (gamemode, pos, velocity, inventory_slots) = - match feather_core::player_data::load_player_data(world_dir, uuid) { - Ok(data) => ( - Gamemode::from_id(data.gamemode as u8), - data.entity.read_position(), - data.entity.read_velocity(), - data.inventory, - ), - Err(_) => ( - Gamemode::from_string(default_gamemode.as_str()), - None, // Invalid position will default to world spawn - None, - vec![], // Empty inventory - ), - }; - - let player_comp = PlayerComponent { - profile_properties: event.profile_properties.clone(), - gamemode, - }; - player_comps.insert(event.player, player_comp).unwrap(); - - let spawn_pos = pos.unwrap_or(position!( - f64::from(level.spawn_x), - f64::from(level.spawn_y), - f64::from(level.spawn_z) - )); - let position = PositionComponent { - current: spawn_pos, - previous: spawn_pos, - }; - positions.insert(event.player, position).unwrap(); - - let velocity = VelocityComponent(velocity.unwrap_or_else(|| glm::vec3(0.0, 0.0, 0.0))); - velocities.insert(event.player, velocity).unwrap(); - - let named = NamedComponent { - display_name: event.username.clone(), - uuid: event.uuid, - }; - nameds.insert(event.player, named).unwrap(); - - let chunk_pending_comp = ChunkPendingComponent { - pending: HashSet::new(), - }; - chunk_pending_comps - .insert(event.player, chunk_pending_comp) - .unwrap(); - - let loaded_chunk_comp = LoadedChunksComponent::default(); - loaded_chunk_comps - .insert(event.player, loaded_chunk_comp) - .unwrap(); - - let mut inventory_comp = InventoryComponent::new(); - for slot in inventory_slots { - let slot_index = slot.convert_index(); - if let Some(slot_index) = slot_index { - inventory_comp.set_item_at(slot_index, slot.to_stack()); - } - } - inventory_comps - .insert(event.player, inventory_comp) - .unwrap(); - - let last_position = LastKnownPositionComponent::default(); - last_positions.insert(event.player, last_position).unwrap(); - - let meta = Metadata::Player(crate::entity::metadata::Player::default()); - metadata.insert(event.player, meta).unwrap(); - - let packet_creator = PacketCreatorComponent(&create_packet); - packet_creators - .insert(event.player, packet_creator) - .unwrap(); - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.join_event_reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} - -pub fn create_packet(world: &World, entity: Entity) -> Box { - let positions = world.read_component::(); - let nameds = world.read_component::(); - let metas = world.read_component::(); - - let position = positions.get(entity).unwrap(); - - let packet = SpawnPlayer { - entity_id: entity.id() as i32, - player_uuid: nameds.get(entity).unwrap().uuid, - x: position.current.x, - y: position.current.y, - z: position.current.z, - yaw: degrees_to_stops(position.current.yaw), - pitch: degrees_to_stops(position.current.pitch), - metadata: metas.get(entity).unwrap().to_full_raw_metadata(), - }; - - Box::new(packet) -} diff --git a/server/src/player/inventory.rs b/server/src/player/inventory.rs deleted file mode 100644 index cfed37950..000000000 --- a/server/src/player/inventory.rs +++ /dev/null @@ -1,759 +0,0 @@ -use crate::disconnect_player; -use crate::entity::{EntitySendEvent, PlayerComponent}; -use crate::network::{send_packet_to_player, NetworkComponent, PacketQueue}; -use crate::player::digging::PlayerItemDropEvent; -use crate::util::Util; -use feather_core::inventory::{ - Inventory, InventoryType, SlotIndex, HOTBAR_SIZE, SLOT_ARMOR_CHEST, SLOT_ARMOR_FEET, - SLOT_ARMOR_HEAD, SLOT_ARMOR_LEGS, SLOT_HOTBAR_OFFSET, SLOT_OFFHAND, -}; -use feather_core::network::cast_packet; -use feather_core::network::packet::implementation::{ - CreativeInventoryAction, EntityEquipment, HeldItemChangeServerbound, SetSlot, -}; -use feather_core::network::packet::PacketType; -use feather_core::{Gamemode, ItemStack}; -use num_traits::ToPrimitive; -use shrev::EventChannel; -use smallvec::SmallVec; -use specs::{Component, LazyUpdate, Read, ReadStorage, ReaderId, World, WriteStorage}; -use specs::{DenseVecStorage, SystemData}; -use specs::{Entity, System, Write}; -use std::ops::{Deref, DerefMut}; - -/// Component for storing a player's inventory. -#[derive(Clone, Debug)] -pub struct InventoryComponent { - pub inventory: Inventory, - /// The player's held item. - /// This is stored as an index in the range 0..9. - pub held_item: SlotIndex, -} - -impl InventoryComponent { - pub fn new() -> Self { - Self { - inventory: Inventory::new(InventoryType::Player, 46), - held_item: 0, - } - } - - /// Returns the item in this inventory's - /// main hand. - pub fn item_in_main_hand(&self) -> Option<&ItemStack> { - self.inventory.item_at(SLOT_HOTBAR_OFFSET + self.held_item) - } - - /// Sets the item in this inventory's main hand. - pub fn set_item_in_main_hand(&mut self, item: ItemStack) { - self.inventory - .set_item_at(SLOT_HOTBAR_OFFSET + self.held_item, item); - } -} - -impl Default for InventoryComponent { - fn default() -> Self { - Self::new() - } -} - -impl Deref for InventoryComponent { - type Target = Inventory; - - fn deref(&self) -> &Self::Target { - &self.inventory - } -} - -impl DerefMut for InventoryComponent { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inventory - } -} - -impl Component for InventoryComponent { - type Storage = DenseVecStorage; -} - -/// An equipment slot, with variants -/// listed in the order of the Entity Equipment -/// IDs to allow for easy conversion using `ToPrimitive`/`FromPrimitive`. -#[derive(Debug, Clone, Copy, ToPrimitive, FromPrimitive, PartialEq, Eq, Hash)] -pub enum Equipment { - MainHand, - OffHand, - Boots, - Leggings, - Chestplate, - Helmet, -} - -impl Equipment { - pub fn from_slot_index(index: SlotIndex) -> Option { - match index { - SLOT_OFFHAND => Some(Equipment::OffHand), - SLOT_ARMOR_FEET => Some(Equipment::Boots), - SLOT_ARMOR_LEGS => Some(Equipment::Leggings), - SLOT_ARMOR_CHEST => Some(Equipment::Chestplate), - SLOT_ARMOR_HEAD => Some(Equipment::Helmet), - _ => None, - } - } - - pub fn slot_index(self, held_item: SlotIndex) -> SlotIndex { - match self { - Equipment::MainHand => held_item + SLOT_HOTBAR_OFFSET, - Equipment::OffHand => SLOT_OFFHAND, - Equipment::Boots => SLOT_ARMOR_FEET, - Equipment::Leggings => SLOT_ARMOR_LEGS, - Equipment::Chestplate => SLOT_ARMOR_CHEST, - Equipment::Helmet => SLOT_ARMOR_HEAD, - } - } -} - -/// Event which is triggered when a player -/// updates their inventory. -/// -/// This event could also be triggered when the player -/// changes their held item. -#[derive(Debug, Clone)] -pub struct InventoryUpdateEvent { - /// The slot(s) affected by the update. - /// - /// Multiple slots could be affected when, for - /// example, a player uses the "drag" inventory interaction. - pub slots: SmallVec<[SlotIndex; 2]>, - /// The player owning the updated inventory. - pub player: Entity, -} - -/// System for handling Creative Inventory Action packets. -pub struct CreativeInventorySystem; - -impl<'a> System<'a> for CreativeInventorySystem { - type SystemData = ( - WriteStorage<'a, InventoryComponent>, - ReadStorage<'a, PlayerComponent>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Read<'a, PacketQueue>, - Read<'a, LazyUpdate>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut inventories, players, mut update_events, mut drop_events, packet_queue, lazy) = - data; - - let packets = packet_queue.for_packet(PacketType::CreativeInventoryAction); - - for (player, packet) in packets { - // Creative Inventory Action can only be used in creative - // mode. - let player_comp = players.get(player).unwrap(); - if player_comp.gamemode != Gamemode::Creative { - disconnect_player( - player, - "Attempted to use Creative Inventory Action while not in creative mode" - .to_string(), - &lazy, - ); - continue; - } - - let packet = cast_packet::(&*packet); - - let inventory = inventories.get_mut(player).unwrap(); - - // Slot -1 means that the user clicked outside the window, - // dropping the item. - if packet.slot == -1 { - match &packet.clicked_item { - Some(stack) => { - let event = PlayerItemDropEvent { - slot: None, - stack: stack.clone(), - player, - }; - drop_events.single_write(event); - - // No need to update inventory - continue; - } - None => (), - } - } - - if packet.slot >= inventory.slot_count() as i16 || packet.slot < -1 { - disconnect_player(player, "Slot index out of bounds".to_string(), &lazy); - continue; - } - - match packet.clicked_item.as_ref() { - Some(item) => { - inventory.set_item_at(packet.slot as usize, item.clone()); - } - None => { - inventory.clear_item_at(packet.slot as usize); - } - } - - // Trigger inventory update event - let event = InventoryUpdateEvent { - slots: smallvec![packet.slot as usize], - player, - }; - update_events.single_write(event); - } - } -} - -/// System for handling Held Item Change packets. -pub struct HeldItemChangeSystem; - -impl<'a> System<'a> for HeldItemChangeSystem { - type SystemData = ( - WriteStorage<'a, InventoryComponent>, - Write<'a, EventChannel>, - Read<'a, PacketQueue>, - Read<'a, LazyUpdate>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut inventories, mut events, packet_queue, lazy) = data; - - let packets = packet_queue.for_packet(PacketType::HeldItemChangeServerbound); - - for (player, packet) in packets { - let packet = cast_packet::(&*packet); - - if packet.slot as usize >= HOTBAR_SIZE { - disconnect_player(player, "Hotbar index out of bounds".to_string(), &lazy); - continue; - } - - let inventory = inventories.get_mut(player).unwrap(); - inventory.held_item = packet.slot as usize; - - // Trigger event - let event = InventoryUpdateEvent { - slots: smallvec![inventory.held_item as usize + SLOT_HOTBAR_OFFSET], - player, - }; - events.single_write(event); - } - } -} - -/// System for broadcasting equipment updates. -#[derive(Default)] -pub struct HeldItemBroadcastSystem { - reader: Option>, -} - -impl<'a> System<'a> for HeldItemBroadcastSystem { - type SystemData = ( - ReadStorage<'a, InventoryComponent>, - Read<'a, EventChannel>, - Read<'a, Util>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (inventories, events, util) = data; - - for event in events.read(&mut self.reader.as_mut().unwrap()) { - let inv = inventories.get(event.player).unwrap(); - - for slot in &event.slots { - // Skip this slot if it is not an equipment update. - if let Ok(equipment) = is_equipment_update(&inv, *slot) { - let slot = equipment.slot_index(inv.held_item); - let item = inv.item_at(slot).cloned(); - - let packet = EntityEquipment::new( - event.player.id() as i32, - equipment.to_i32().unwrap(), - item, - ); - - util.broadcast_entity_update(event.player, packet, Some(event.player)); - } - } - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some( - world - .fetch_mut::>() - .register_reader(), - ); - } -} - -/// System which listens to `EntitySendEvent`s and -/// sends entity equipment alongside. -#[derive(Default)] -pub struct EquipmentSendSystem { - reader: Option>, -} - -impl<'a> System<'a> for EquipmentSendSystem { - type SystemData = ( - ReadStorage<'a, InventoryComponent>, - ReadStorage<'a, NetworkComponent>, - Read<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (inventories, networks, send_events) = data; - - for event in send_events.read(&mut self.reader.as_mut().unwrap()) { - let network = networks.get(event.player).unwrap(); - let inventory = match inventories.get(event.entity) { - Some(inv) => inv, - None => continue, - }; - - let equipments = [ - Equipment::MainHand, - Equipment::Boots, - Equipment::Leggings, - Equipment::Chestplate, - Equipment::Helmet, - Equipment::OffHand, - ]; - - for equipment in equipments.iter() { - let item = { - let slot = equipment.slot_index(inventory.held_item); - inventory.item_at(slot).cloned() - }; - - let equipment_slot = equipment.to_i32().unwrap(); - - let packet = EntityEquipment::new(event.entity.id() as i32, equipment_slot, item); - send_packet_to_player(network, packet); - } - } - } - - fn setup(&mut self, world: &mut World) { - Self::SystemData::setup(world); - - self.reader = Some(world.fetch_mut::>().register_reader()); - } -} - -/// System for sending the Set Slot packet -/// when a player's inventory is updated. -#[derive(Default)] -pub struct SetSlotSystem { - reader: Option>, -} - -impl<'a> System<'a> for SetSlotSystem { - type SystemData = ( - ReadStorage<'a, InventoryComponent>, - ReadStorage<'a, NetworkComponent>, - Read<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (inventories, networks, events) = data; - - for event in events.read(self.reader.as_mut().unwrap()) { - let inv = inventories.get(event.player).unwrap(); - let network = networks.get(event.player).unwrap(); - - for slot in &event.slots { - let packet = SetSlot { - window_id: 0, - slot: *slot as i16, - slot_data: inv.item_at(*slot as usize).cloned(), - }; - - send_packet_to_player(&network, packet); - } - } - } - - setup_impl!(reader); -} - -/// Returns whether the given update to an inventory -/// is an equipment update. -fn is_equipment_update(inv: &InventoryComponent, slot: SlotIndex) -> Result { - if slot >= SLOT_HOTBAR_OFFSET && slot - SLOT_HOTBAR_OFFSET == inv.held_item { - Ok(Equipment::MainHand) - } else if let Some(equipment) = Equipment::from_slot_index(slot) { - Ok(equipment) - } else { - Err(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testframework as t; - use feather_core::inventory::{ItemStack, SLOT_ENTITY_EQUIPMENT_MAIN_HAND}; - use feather_core::item::Item; - use specs::WorldExt; - - #[test] - fn test_creative_inventory_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let packet = CreativeInventoryAction::new( - SLOT_HOTBAR_OFFSET as i16, - Some(ItemStack::new(Item::IronSword, 1)), - ); - - t::receive_packet(&player, &w, packet); - - let mut update_reader = t::reader(&w); - let mut drop_reader = t::reader(&w); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - let inv_storage = w.read_component::(); - let inv = inv_storage.get(player.entity).unwrap(); - assert_eq!( - inv.item_at(SLOT_HOTBAR_OFFSET).unwrap(), - &ItemStack::new(Item::IronSword, 1) - ); - - // Confirm that event was triggered - { - let channel = w.fetch::>(); - let events = channel.read(&mut update_reader).collect::>(); - assert_eq!(events.len(), 1); - let first = events.first().unwrap(); - assert_eq!(first.player, player.entity); - assert_eq!(first.slots.as_slice(), &[SLOT_HOTBAR_OFFSET]); - - assert!(w - .fetch::>() - .read(&mut drop_reader) - .next() - .is_none()); - } - - drop(inv_storage); - - let packet = CreativeInventoryAction::new(0, None); - - t::receive_packet(&player, &w, packet.clone()); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - let inv_storage = w.read_component::(); - let inv = inv_storage.get(player.entity).unwrap(); - assert_eq!(inv.item_at(0), None); - - drop(inv_storage); - - // Now with a survival mode player... - w.write_component::() - .get_mut(player.entity) - .unwrap() - .gamemode = Gamemode::Survival; - - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_disconnected(&player); - } - - #[test] - fn test_creative_inventory_slot_out_of_bounds() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let packet = CreativeInventoryAction::new(46, Some(ItemStack::new(Item::IronSword, 1))); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_disconnected(&player); - } - - #[test] - fn test_creative_inventory_armor() { - let equipments = [ - Equipment::OffHand, - Equipment::Boots, - Equipment::Leggings, - Equipment::Chestplate, - Equipment::Helmet, - ]; - - for equipment in equipments.iter() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let mut event_reader = t::reader(&w); - - let packet = CreativeInventoryAction::new( - equipment.slot_index(0) as i16, - Some(ItemStack::new(Item::IronSword, 1)), - ); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - let ch = w.fetch::>(); - let events = ch.read(&mut event_reader).collect::>(); - - assert_eq!(events.len(), 1); - let first = events.first().unwrap(); - - assert_eq!(first.slots.as_slice(), &[equipment.slot_index(0)]); - assert_eq!(first.player, player.entity); - } - } - - #[test] - fn test_creative_inventory_system_drop_item() { - // Drop item - slot index -1 - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let stack = ItemStack::new(Item::CookedBeef, 1); - - let mut drop_reader = t::reader(&w); - let mut update_reader = t::reader(&w); - - let packet = CreativeInventoryAction { - slot: -1, - clicked_item: Some(stack.clone()), - }; - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_not_disconnected(&player); - - let channel = w.fetch::>(); - let events = channel.read(&mut update_reader).collect::>(); - assert!(events.is_empty()); - - let channel = w.fetch::>(); - let events = channel.read(&mut drop_reader).collect::>(); - - assert_eq!(events.len(), 1); - let first = events.first().unwrap(); - assert_eq!(first.stack, stack); - assert_eq!(first.player, player.entity); - assert_eq!(first.slot, None); - } - - #[test] - fn test_held_item_change_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let slot = 4; - - let mut event_reader = t::reader(&w); - - let packet = HeldItemChangeServerbound::new(slot); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - let channel = w.fetch::>(); - let events = channel.read(&mut event_reader).collect::>(); - - assert_eq!(events.len(), 1); - - let first = events.first().unwrap(); - assert_eq!(first.player, player.entity); - assert_eq!( - first.slots.as_slice(), - &[slot as usize + SLOT_HOTBAR_OFFSET] - ); - - let inventories = w.read_component::(); - let inv = inventories.get(player.entity).unwrap(); - - assert_eq!(inv.held_item, slot as usize); - } - - #[test] - fn test_held_item_change_system_out_of_bounds() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let slot = 9; - - let packet = HeldItemChangeServerbound::new(slot); - t::receive_packet(&player, &w, packet); - - d.dispatch(&w); - w.maintain(); - - t::assert_disconnected(&player); // Slot out of bounds - } - - #[test] - fn test_held_item_broadcast_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - - { - let mut invs = w.write_component::(); - let inv = invs.get_mut(player.entity).unwrap(); - inv.held_item = 0; - inv.set_item_at(SLOT_HOTBAR_OFFSET, ItemStack::new(Item::IronSword, 1)); - } - - let event = InventoryUpdateEvent { - player: player.entity, - slots: smallvec![SLOT_HOTBAR_OFFSET], - }; - - w.fetch_mut::>() - .single_write(event); - - d.dispatch(&w); - w.maintain(); - - let packet = t::assert_packet_received(&player2, PacketType::EntityEquipment); - - let packet = cast_packet::(&*packet); - assert_eq!(packet.slot, SLOT_ENTITY_EQUIPMENT_MAIN_HAND as i32); - assert_eq!(packet.entity_id, player.entity.id() as i32); - assert_eq!(packet.item, Some(ItemStack::new(Item::IronSword, 1))); - - t::assert_packet_not_received(&player, PacketType::EntityEquipment); - } - - #[test] - fn test_equipment_send_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - let player2 = t::add_player(&mut w); - - let event = EntitySendEvent { - player: player.entity, - entity: player2.entity, - }; - - w.fetch_mut::>().single_write(event); - - { - let mut invs = w.write_component::(); - let inv = invs.get_mut(player2.entity).unwrap(); - - inv.held_item = 1; - inv.set_item_at(SLOT_HOTBAR_OFFSET + 1, ItemStack::new(Item::IronSword, 1)); - inv.set_item_at(SLOT_ARMOR_HEAD, ItemStack::new(Item::DiamondHelmet, 1)); - } - - d.dispatch(&w); - w.maintain(); - - let packets = t::received_packets(&player, None); - - let packets = packets - .into_iter() - .filter(|packet| packet.ty() == PacketType::EntityEquipment) - .collect::>(); - - assert_eq!(packets.len(), 6); - - for packet in packets { - let packet = cast_packet::(&*packet); - assert_eq!(packet.entity_id, player2.entity.id() as i32); - } - } - - #[test] - fn test_set_slot_system() { - let (mut w, mut d) = t::builder().with(SetSlotSystem::default(), "").build(); - - let player = t::add_player(&mut w); - let stack = ItemStack::new(Item::EnderPearl, 8); - { - let mut inventories = w.write_component::(); - inventories - .get_mut(player.entity) - .unwrap() - .set_item_at(0, stack.clone()); - - let event = InventoryUpdateEvent { - slots: smallvec![0], - player: player.entity, - }; - t::trigger_event(&w, event); - } - - d.dispatch(&w); - w.maintain(); - - let packet = t::assert_packet_received(&player, PacketType::SetSlot); - let packet = cast_packet::(&*packet); - - assert_eq!(packet.window_id, 0); - assert_eq!(packet.slot_data, Some(stack)); - assert_eq!(packet.slot, 0); - } - - #[test] - fn test_is_equipment_update() { - let mut inv = InventoryComponent::default(); - inv.held_item = 0; - - assert!(is_equipment_update(&inv, 21).is_err()); - assert_eq!( - is_equipment_update(&inv, SLOT_HOTBAR_OFFSET), - Ok(Equipment::MainHand) - ); - assert_eq!( - is_equipment_update(&inv, SLOT_ARMOR_HEAD), - Ok(Equipment::Helmet) - ); - assert_eq!( - is_equipment_update(&inv, SLOT_ARMOR_CHEST), - Ok(Equipment::Chestplate) - ); - assert_eq!( - is_equipment_update(&inv, SLOT_ARMOR_LEGS), - Ok(Equipment::Leggings) - ); - assert_eq!( - is_equipment_update(&inv, SLOT_ARMOR_FEET), - Ok(Equipment::Boots) - ); - assert_eq!( - is_equipment_update(&inv, SLOT_OFFHAND), - Ok(Equipment::OffHand) - ); - } -} diff --git a/server/src/player/mod.rs b/server/src/player/mod.rs index 2cdeeaee6..67bdab92a 100644 --- a/server/src/player/mod.rs +++ b/server/src/player/mod.rs @@ -1,113 +1,119 @@ -//! This module provides systems and components -//! relating to players, including player movement -//! and inventory handling. +//! Systems and components specific to player entities. -/// Module for handling player animation broadcasting -/// (e.g. when a player swings their arm). -mod animation; -/// Module for broadcasting when a player joins and leaves. -mod broadcast; -/// Module for handling and broadcasting chat messages. -mod chat; -/// Module for handling the Player Digging packet. -mod digging; -/// Module for initializing the necessary components -/// when a player joins. -mod init; -/// Module for handling player inventory. -mod inventory; -/// Module for handling player movement packets. -/// Also handles loading/unloading chunks when necessary. -mod movement; -/// Module for handling player block placements. -mod placement; -mod resource_pack; -mod save; -mod view; +use crate::broadcasters::movement::LastKnownPositions; +use crate::chunk_logic::ChunkHolder; +use crate::entity; +use crate::entity::{CreationPacketCreator, EntityId, Name, SpawnPacketCreator}; +use crate::io::NewClientInfo; +use crate::join::Joined; +use crate::network::Network; +use crate::state::State; +use crate::util::degrees_to_stops; +use feather_core::network::packet::implementation::{PlayerInfo, PlayerInfoAction, SpawnPlayer}; +use feather_core::{ClientboundAnimation, Gamemode, Packet, Position}; +use legion::entity::Entity; +use mojang_api::ProfileProperty; +use tonks::{EntityAccessor, PreparedWorld}; +use uuid::Uuid; -pub use broadcast::PlayerDisconnectEvent; -pub use init::create_packet; +/// Profile properties of a player. +#[derive(Debug, Clone)] +pub struct ProfileProperties(pub Vec); -pub use movement::{ - send_chunk_to_player, ChunkCrossSystem, ChunkPendingComponent, LoadedChunksComponent, -}; +/// Event triggered when a player joins. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PlayerJoinEvent { + pub player: Entity, +} -pub use animation::PlayerAnimationEvent; +/// Event triggered when a player causes an animation. +#[derive(Debug, Clone)] +pub struct PlayerAnimationEvent { + pub player: Entity, + pub animation: ClientboundAnimation, +} -pub use digging::PlayerItemDropEvent; -pub use inventory::{InventoryComponent, InventoryUpdateEvent}; -pub use save::save_player_data; +/// Tag used to mark a player. +/// +/// Note that this is a _tag_, not a component. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Player; -use crate::player::inventory::SetSlotSystem; -use crate::player::placement::BlockPlacementSystem; -use crate::player::save::PlayerDataSaveSystem; -use crate::player::view::ViewUpdateSystem; -use crate::systems::{ - ANIMATION_BROADCAST, BLOCK_BREAK_BROADCAST, BLOCK_PLACEMENT, CHAT_BROADCAST, CHUNK_CROSS, - CHUNK_SEND, CLIENT_CHUNK_UNLOAD, CREATIVE_INVENTORY, DISCONNECT_BROADCAST, EQUIPMENT_SEND, - HELD_ITEM_BROADCAST, HELD_ITEM_CHANGE, JOIN_BROADCAST, NETWORK, PLAYER_ANIMATION, PLAYER_CHAT, - PLAYER_DATA_SAVE, PLAYER_DIGGING, PLAYER_INIT, PLAYER_MOVEMENT, RESOURCE_PACK_SEND, SET_SLOT, - VIEW_UPDATE, -}; -use animation::{AnimationBroadcastSystem, PlayerAnimationSystem}; -use broadcast::{DisconnectBroadcastSystem, JoinBroadcastSystem}; -use chat::{ChatBroadcastSystem, PlayerChatSystem}; -use digging::BlockUpdateBroadcastSystem; -use digging::PlayerDiggingSystem; -use init::PlayerInitSystem; -use inventory::{ - CreativeInventorySystem, EquipmentSendSystem, HeldItemBroadcastSystem, HeldItemChangeSystem, -}; -use movement::{ChunkSendSystem, ClientChunkUnloadSystem, PlayerMovementSystem}; -use resource_pack::ResourcePackSendSystem; -use specs::DispatcherBuilder; +/// Creates a new player from the given `NewClientInfo`. +/// +/// This function also triggers the `PlayerJoinEvent` for this player. +pub fn create(state: &State, info: NewClientInfo) { + entity::base(state, info.position) + .with_tag(Player) + .with_component(info.uuid) + .with_component(Network { + sender: info.sender, + receiver: info.receiver, + }) + .with_component(info.ip) + .with_component(ProfileProperties(info.profile)) + .with_component(Name(info.username)) + .with_component(ChunkHolder::default()) + .with_component(Joined(false)) + .with_component(LastKnownPositions::default()) + .with_component(SpawnPacketCreator(&create_spawn_packet)) + .with_component(CreationPacketCreator(&create_initialization_packet)) + .with_exec(|_, scheduler, player| { + scheduler.trigger(PlayerJoinEvent { player }); + }) + .build(); +} -pub const PLAYER_EYE_HEIGHT: f64 = 1.62; -pub const PLAYER_EYE_HEIGHT_WHILE_SNEAKING: f64 = 1.54; +/// Function to create a `SpawnPlayer` packet to spawn the player. +fn create_spawn_packet(accessor: &EntityAccessor, world: &PreparedWorld) -> Box { + let entity_id = accessor.get_component::(world).unwrap().0; + let player_uuid = *accessor.get_component::(world).unwrap(); + let pos = *accessor.get_component::(world).unwrap(); -pub fn init_logic(dispatcher: &mut DispatcherBuilder) { - dispatcher.add(PlayerDiggingSystem, PLAYER_DIGGING, &[NETWORK]); - dispatcher.add(PlayerAnimationSystem, PLAYER_ANIMATION, &[NETWORK]); - dispatcher.add(CreativeInventorySystem, CREATIVE_INVENTORY, &[NETWORK]); - dispatcher.add(HeldItemChangeSystem, HELD_ITEM_CHANGE, &[NETWORK]); - dispatcher.add(PlayerMovementSystem, PLAYER_MOVEMENT, &[NETWORK]); - dispatcher.add(PlayerChatSystem, PLAYER_CHAT, &[NETWORK]); - dispatcher.add(BlockPlacementSystem, BLOCK_PLACEMENT, &[NETWORK]); - dispatcher.add( - PlayerDataSaveSystem::default(), - PLAYER_DATA_SAVE, - &[NETWORK], - ); -} + // TODO: metadata -pub fn init_handlers(dispatcher: &mut DispatcherBuilder) { - dispatcher.add(ViewUpdateSystem::default(), VIEW_UPDATE, &[]); - dispatcher.add(ChunkCrossSystem::default(), CHUNK_CROSS, &[]); - dispatcher.add(ClientChunkUnloadSystem, CLIENT_CHUNK_UNLOAD, &[]); - dispatcher.add(PlayerInitSystem::default(), PLAYER_INIT, &[]); + let packet = SpawnPlayer { + entity_id, + player_uuid, + x: pos.x, + y: pos.y, + z: pos.z, + yaw: degrees_to_stops(pos.yaw), + pitch: degrees_to_stops(pos.pitch), + metadata: Default::default(), + }; + Box::new(packet) } -pub fn init_broadcast(dispatcher: &mut DispatcherBuilder) { - dispatcher.add(HeldItemBroadcastSystem::default(), HELD_ITEM_BROADCAST, &[]); - dispatcher.add(JoinBroadcastSystem::default(), JOIN_BROADCAST, &[]); - dispatcher.add( - DisconnectBroadcastSystem::default(), - DISCONNECT_BROADCAST, - &[], - ); - dispatcher.add( - AnimationBroadcastSystem::default(), - ANIMATION_BROADCAST, - &[], - ); - dispatcher.add(EquipmentSendSystem::default(), EQUIPMENT_SEND, &[]); - dispatcher.add(ResourcePackSendSystem::default(), RESOURCE_PACK_SEND, &[]); - dispatcher.add(ChunkSendSystem::default(), CHUNK_SEND, &[]); - dispatcher.add( - BlockUpdateBroadcastSystem::default(), - BLOCK_BREAK_BROADCAST, - &[], - ); - dispatcher.add(SetSlotSystem::default(), SET_SLOT, &[]); - dispatcher.add(ChatBroadcastSystem::default(), CHAT_BROADCAST, &[]); +/// Function to create a `PlayerInfo` packet to broadcast when the player joins. +fn create_initialization_packet( + accessor: &EntityAccessor, + world: &PreparedWorld, +) -> Box { + let name = accessor.get_component::(world).unwrap(); + let props = accessor.get_component::(world).unwrap(); + let uuid = *accessor.get_component::(world).unwrap(); + + let props = props + .0 + .iter() + .map(|prop| { + ( + prop.name.clone(), + prop.value.clone(), + prop.signature.clone(), + ) + }) + .collect::>(); + + let display_name = json!({ + "text": name.0 + }) + .to_string(); + + let action = + PlayerInfoAction::AddPlayer(name.0.clone(), props, Gamemode::Creative, 50, display_name); + + let packet = PlayerInfo { action, uuid }; + Box::new(packet) } diff --git a/server/src/player/movement.rs b/server/src/player/movement.rs deleted file mode 100644 index 919847c85..000000000 --- a/server/src/player/movement.rs +++ /dev/null @@ -1,469 +0,0 @@ -use std::collections::VecDeque; -use std::ops::{Deref, DerefMut}; -use std::sync::Arc; - -use hashbrown::HashSet; -use rayon::prelude::*; -use shrev::{EventChannel, ReaderId}; -use specs::storage::{BTreeStorage, ComponentEvent}; -use specs::{ - BitSet, Component, Entities, Entity, Join, LazyUpdate, ParJoin, Read, ReadExpect, ReadStorage, - System, WorldExt, Write, WriteStorage, -}; - -use feather_core::network::cast_packet; -use feather_core::network::packet::implementation::{ - ChunkData, PlayerLook, PlayerPosition, PlayerPositionAndLookServerbound, UnloadChunk, -}; -use feather_core::network::packet::{Packet, PacketType}; -use feather_core::world::chunk::Chunk; -use feather_core::world::{ChunkMap, ChunkPosition, Position}; - -use crate::chunk_logic::{ - load_chunk, ChunkHolderComponent, ChunkHolderReleaseEvent, ChunkHolders, ChunkLoadEvent, - ChunkLoadFailEvent, ChunkWorkerHandle, -}; -use crate::config::Config; -use crate::entity::PositionComponent; -use crate::network::{send_packet_to_player, NetworkComponent, PacketQueue}; -use crate::{TickCount, TPS}; - -// MOVEMENT HANDLING - -/// System for handling player movement -/// packets. -pub struct PlayerMovementSystem; - -impl<'a> System<'a> for PlayerMovementSystem { - type SystemData = (WriteStorage<'a, PositionComponent>, Read<'a, PacketQueue>); - - fn run(&mut self, data: Self::SystemData) { - let (mut positions, packet_queue) = data; - - // Take movement packets - let mut packets = vec![]; - packets.append(&mut packet_queue.for_packet(PacketType::PlayerPosition)); - packets.append(&mut packet_queue.for_packet(PacketType::PlayerPositionAndLookServerbound)); - packets.append(&mut packet_queue.for_packet(PacketType::PlayerLook)); - - // Handle movement packets - for (player, packet) in packets { - let position = positions.get(player).unwrap(); - - // Get position using packet and old position - let new_pos = new_pos_from_packet(position.previous, packet); - - // Set new position - positions.get_mut(player).unwrap().current = new_pos; - } - } -} - -fn new_pos_from_packet(old_pos: Position, packet: Box) -> Position { - match packet.ty() { - PacketType::PlayerPosition => { - let packet = cast_packet::(&*packet); - - position!( - packet.x, - packet.feet_y, - packet.z, - old_pos.pitch, - old_pos.yaw, - packet.on_ground - ) - } - PacketType::PlayerLook => { - let packet = cast_packet::(&*packet); - - position!( - old_pos.x, - old_pos.y, - old_pos.z, - packet.pitch, - packet.yaw, - packet.on_ground - ) - } - PacketType::PlayerPositionAndLookServerbound => { - let packet = cast_packet::(&*packet); - - position!( - packet.x, - packet.feet_y, - packet.z, - packet.pitch, - packet.yaw, - packet.on_ground - ) - } - _ => panic!(), - } -} - -// CHUNK LOAD/UNLOAD HANDLING - -/// Component for storing which chunks a client -/// has loaded and which are queued to be unloaded -/// on the client. -#[derive(Clone, Default, Debug)] -pub struct LoadedChunksComponent { - /// All chunks which are loaded on the client, i.e. - /// which have had a Chunk Data packet sent. - loaded_chunks: HashSet, - /// Chunks queued for unloading on the client. - /// - /// Note that that these chunks will not be unloaded - /// on the server - all that will happen is that an Unload - /// Chunk packet will be sent to the client. This avoids client-side - /// memory leaks. - unload_queue: VecDeque<(ChunkPosition, u64)>, -} - -impl Component for LoadedChunksComponent { - type Storage = BTreeStorage; -} - -/// Component storing what chunks are pending -/// to send to a player. -#[derive(Clone, Debug)] -pub struct ChunkPendingComponent { - pub pending: HashSet, -} - -impl Deref for ChunkPendingComponent { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.pending - } -} - -impl DerefMut for ChunkPendingComponent { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.pending - } -} - -impl Component for ChunkPendingComponent { - type Storage = BTreeStorage; -} - -/// Time after a player can no longer see a chunk -/// that it is unloaded. -const CHUNK_UNLOAD_TIME: u64 = TPS * 5; // 5 seconds - -/// Event which is triggered when a player crosses -/// chunk boundaries, causing their position's chunk -/// to change. -#[derive(Debug, Clone)] -pub struct ChunkCrossEvent { - /// The player affected by this event. - pub player: Entity, - /// The old chunk position. - pub old: ChunkPosition, - /// The new chunk position. - pub new: ChunkPosition, -} - -/// System that checks when a player crosses chunk boundaries. -/// When the player does so, the system sends Chunk Data packets -/// for chunks within the view distance and also unloads -/// chunks no longer within the player's view distance. -#[derive(Default)] -pub struct ChunkCrossSystem { - dirty: BitSet, - reader: Option>, -} - -impl<'a> System<'a> for ChunkCrossSystem { - type SystemData = ( - ReadStorage<'a, PositionComponent>, - Read<'a, ChunkMap>, - Read<'a, TickCount>, - Read<'a, Arc>, - WriteStorage<'a, LoadedChunksComponent>, - WriteStorage<'a, ChunkHolderComponent>, - ReadStorage<'a, NetworkComponent>, - Write<'a, ChunkHolders>, - Write<'a, EventChannel>, - ReadExpect<'a, ChunkWorkerHandle>, - Read<'a, LazyUpdate>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - positions, - chunk_map, - tick_count, - config, - mut loaded_chunks_comps, - mut chunk_holder_comps, - net_comps, - mut holders, - mut cross_events, - chunk_handle, - lazy, - entities, - ) = data; - - self.dirty.clear(); - - for event in positions.channel().read(self.reader.as_mut().unwrap()) { - match event { - ComponentEvent::Modified(id) | ComponentEvent::Inserted(id) => { - self.dirty.add(*id); - } - _ => (), - } - } - - // Go through events and handle them accordingly - for (position, net, chunk_holder, loaded_chunks, player, _) in ( - &positions, - &net_comps, - &mut chunk_holder_comps, - &mut loaded_chunks_comps, - &entities, - &self.dirty, - ) - .join() - { - let old_chunk_pos = position.previous.chunk_pos(); - let new_chunk_pos = position.current.chunk_pos(); - - if old_chunk_pos != new_chunk_pos { - // Player has moved across chunk boundaries. Handle accordingly. - let chunks = chunks_within_view_distance(&config, new_chunk_pos); - - for chunk in &chunks { - if loaded_chunks.loaded_chunks.contains(chunk) { - // Already sent - nothing to do. - continue; - } - - send_chunk_to_player( - *chunk, - net, - player, - &chunk_map, - &chunk_handle, - &mut holders, - chunk_holder, - loaded_chunks, - &lazy, - ); - } - - // Now, queue all chunks which need to be unloaded for unloading. - let old_chunks = chunks_within_view_distance(&config, old_chunk_pos); - - for chunk in old_chunks { - if chunks.contains(&chunk) { - // Chunk should remain loaded. Nothing to do - continue; - } - - // Queue chunk for unloading. - let time = tick_count.0 + CHUNK_UNLOAD_TIME; - loaded_chunks.unload_queue.push_back((chunk, time)); - } - - // Trigger chunk cross event. - let event = ChunkCrossEvent { - player, - old: old_chunk_pos, - new: new_chunk_pos, - }; - cross_events.single_write(event); - } - } - } - - flagged_setup_impl!(PositionComponent, reader); -} - -/// System for sending chunks to players once they're loaded. -/// -/// This system listens to `ChunkLoadEvent`s. -#[derive(Default)] -pub struct ChunkSendSystem { - load_event_reader: Option>, - fail_event_reader: Option>, -} - -impl<'a> System<'a> for ChunkSendSystem { - type SystemData = ( - WriteStorage<'a, ChunkPendingComponent>, - ReadStorage<'a, NetworkComponent>, - Read<'a, ChunkMap>, - Read<'a, EventChannel>, - Read<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut pendings, netcomps, chunk_map, load_events, fail_events) = data; - - for event in load_events.read(&mut self.load_event_reader.as_mut().unwrap()) { - // TODO perhaps this is slightly inefficient? - (&netcomps, &mut pendings) - .par_join() - .for_each(|(net, pending)| { - if pending.contains(&event.pos) { - // It's safe to unwrap the chunk value now, - // because we know it's been loaded. - let chunk = chunk_map.chunk_at(event.pos).unwrap(); - send_chunk_data(chunk, net); - - pending.remove(&event.pos); - } - }); - } - - for event in fail_events.read(self.fail_event_reader.as_mut().unwrap()) { - (&mut pendings).par_join().for_each(|pending| { - if pending.contains(&event.pos) { - // The chunk failed to load - skip sending it. - // See issue #71 - pending.remove(&event.pos); - } - }); - } - } - - setup_impl!(load_event_reader, fail_event_reader); -} - -/// System for sending the Unload Chunk packet when the time comes. -pub struct ClientChunkUnloadSystem; - -impl<'a> System<'a> for ClientChunkUnloadSystem { - type SystemData = ( - WriteStorage<'a, LoadedChunksComponent>, - ReadStorage<'a, NetworkComponent>, - ReadStorage<'a, PositionComponent>, - WriteStorage<'a, ChunkHolderComponent>, - Entities<'a>, - Write<'a, ChunkHolders>, - Read<'a, TickCount>, - Read<'a, Arc>, - Write<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut loaded_chunks_comps, - net_comps, - positions, - mut chunk_holder_comps, - entities, - mut chunk_holders, - tick_count, - config, - mut holder_release_events, - ) = data; - - ( - &mut loaded_chunks_comps, - &net_comps, - &positions, - &mut chunk_holder_comps, - &entities, - ) - .join() - .for_each( - |(loaded_chunks_comp, net_comp, position, chunk_holder_comp, player)| { - // Go through queue and see if it's time to unload any chunks. - while let Some((chunk, time)) = loaded_chunks_comp.unload_queue.front() { - let chunk = *chunk; - if tick_count.0 >= *time { - // Unload if needed. - - let chunks_within_view_distance = - chunks_within_view_distance(&config, position.current.chunk_pos()); - - if chunks_within_view_distance.contains(&chunk) { - // Chunk is within view distance again - don't unload it. - loaded_chunks_comp.unload_queue.pop_front(); - continue; - } - - let unload_chunk = UnloadChunk::new(chunk.x, chunk.z); - send_packet_to_player(net_comp, unload_chunk); - - // Remove chunk from queue. - loaded_chunks_comp.unload_queue.pop_front(); - // Remove from loaded chunk list. - loaded_chunks_comp.loaded_chunks.remove(&chunk); - // Remove hold on chunk so it can be unloaded. - chunk_holders.remove_holder(chunk, player, &mut holder_release_events); - // Remove hold from chunk holder component. - chunk_holder_comp.holds.remove(&chunk); - } else { - // No more chunks in queue that should - // be unloaded - finished. - break; - } - } - }, - ); - } -} - -/// Returns the set of all chunk positions -/// within the server view distance of a given -/// chunk. -fn chunks_within_view_distance(config: &Config, chunk: ChunkPosition) -> HashSet { - let view_distance = i32::from(config.server.view_distance); - let mut results = HashSet::with_capacity((view_distance * view_distance) as usize); - - for x in -view_distance..=view_distance { - for z in -view_distance..=view_distance { - results.insert(ChunkPosition::new(chunk.x + x, chunk.z + z)); - } - } - - results -} - -/// Attempts to send the chunk at the given position to -/// the given player. If the chunk is not loaded, it will -/// be loaded and sent at a later time as soon as it is -/// loaded. -#[allow(clippy::too_many_arguments)] // TODO: get rid of LoadedChunksComponent -pub fn send_chunk_to_player( - chunk_pos: ChunkPosition, - net: &NetworkComponent, - player: Entity, - chunk_map: &ChunkMap, - chunk_handle: &ChunkWorkerHandle, - holders: &mut ChunkHolders, - holder: &mut ChunkHolderComponent, - loaded_chunks: &mut LoadedChunksComponent, - lazy: &LazyUpdate, -) { - holders.insert_holder(chunk_pos, player); - holder.holds.insert(chunk_pos); - loaded_chunks.loaded_chunks.insert(chunk_pos); - - if let Some(chunk) = chunk_map.chunk_at(chunk_pos) { - send_chunk_data(chunk, net); - } else { - // Queue for loading - load_chunk(chunk_handle, chunk_pos); - lazy.exec_mut(move |world| { - world - .write_component::() - .get_mut(player) - .unwrap() - .pending - .insert(chunk_pos); - }); - } -} - -fn send_chunk_data(chunk: &Chunk, net: &NetworkComponent) { - let packet = ChunkData::new(chunk.clone()); - send_packet_to_player(net, packet); -} diff --git a/server/src/player/placement.rs b/server/src/player/placement.rs deleted file mode 100644 index 4974554f2..000000000 --- a/server/src/player/placement.rs +++ /dev/null @@ -1,215 +0,0 @@ -use crate::blocks::{BlockUpdateCause, BlockUpdateEvent}; -use crate::disconnect_player; -use crate::entity::PlayerComponent; -use crate::network::PacketQueue; -use crate::player::{InventoryComponent, InventoryUpdateEvent}; -use crate::prelude::Gamemode; -use feather_core::inventory::SLOT_HOTBAR_OFFSET; -use feather_core::network::cast_packet; -use feather_core::network::packet::implementation::PlayerBlockPlacement; -use feather_core::world::ChunkMap; -use feather_core::{Block, ItemStack, PacketType}; -use feather_item_block::ItemToBlock; -use shrev::EventChannel; -use specs::{LazyUpdate, Read, ReadStorage, System, Write, WriteStorage}; - -/// System for handling Player Block Placement packets -/// and updating the world accordingly. -pub struct BlockPlacementSystem; - -impl<'a> System<'a> for BlockPlacementSystem { - type SystemData = ( - WriteStorage<'a, InventoryComponent>, - ReadStorage<'a, PlayerComponent>, - Write<'a, ChunkMap>, - Write<'a, EventChannel>, - Write<'a, EventChannel>, - Read<'a, PacketQueue>, - Read<'a, LazyUpdate>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - mut inventories, - players, - mut chunk_map, - mut block_update_events, - mut inventory_update_events, - packet_queue, - lazy, - ) = data; - - let packets = packet_queue.for_packet(PacketType::PlayerBlockPlacement); - - for (player, packet) in packets { - let packet = cast_packet::(&*packet); - - // TODO: handle slabs, blocks with directions, etc. - let inventory = inventories.get_mut(player).unwrap(); - - let item = continue_if_none!(inventory.item_in_main_hand()); - - let block = continue_if_none!(item.ty.to_block()); - - let placed_on = match chunk_map.block_at(packet.location) { - Some(block) => block, - None => { - disconnect_player( - player, - String::from("Attempted to place block in unloaded chunk"), - &lazy, - ); - continue; - } - }; - - // TODO: waterlogged blocks, more - let pos = match placed_on { - Block::Grass | Block::TallGrass(_) | Block::Water(_) | Block::Lava(_) => { - packet.location - } - _ => packet.location + packet.face.placement_offset(), - }; - - let old = match chunk_map.block_at(pos) { - Some(block) => block, - None => { - disconnect_player( - player, - String::from("Attempted to place block in unloaded chunk"), - &lazy, - ); - continue; - } - }; - - chunk_map.set_block_at(pos, block).unwrap(); - - let event = BlockUpdateEvent { - cause: BlockUpdateCause::Player(player), - pos, - old_block: old, - new_block: block, - }; - - block_update_events.single_write(event); - - let gamemode = players.get(player).unwrap().gamemode; - - // Update player's inventory if in survival - if gamemode == Gamemode::Survival { - let item = ItemStack::new(item.ty, item.amount - 1); - inventory.set_item_in_main_hand(item); - - let event = InventoryUpdateEvent { - slots: smallvec![SLOT_HOTBAR_OFFSET + inventory.held_item], - player, - }; - inventory_update_events.single_write(event); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testframework as t; - use feather_core::inventory::SLOT_HOTBAR_OFFSET; - use feather_core::network::packet::implementation::Face; - use feather_core::{Block, BlockPosition, Item, ItemStack}; - use specs::WorldExt; - - #[test] - fn test_block_placement_system() { - let (mut w, mut d) = t::builder().with(BlockPlacementSystem, "").build(); - - t::populate_with_air(&mut w); - - let player = t::add_player(&mut w); - - { - let mut inventories = w.write_component::(); - inventories - .get_mut(player.entity) - .unwrap() - .set_item_at(SLOT_HOTBAR_OFFSET, ItemStack::new(Item::Cobblestone, 1)); - - let mut players = w.write_component::(); - players.get_mut(player.entity).unwrap().gamemode = Gamemode::Survival; - } - - let pos = BlockPosition::new(10, 20, 30); - - let packet = PlayerBlockPlacement { - location: pos, - face: Face::Top, - hand: 0, - cursor_position_x: 0.0, - cursor_position_y: 0.0, - cursor_position_z: 0.0, - }; - t::receive_packet(&player, &w, packet); - - let mut reader = t::reader(&w); - - d.dispatch(&w); - w.maintain(); - - let events = t::triggered_events::(&w, &mut reader); - let first = events.first().unwrap(); - - assert_eq!(first.cause, BlockUpdateCause::Player(player.entity)); - assert_eq!(first.old_block, Block::Air); - assert_eq!(first.new_block, Block::Cobblestone); - assert_eq!(first.pos, pos + BlockPosition::new(0, 1, 0)); - - let inventory = w - .read_component::() - .get(player.entity) - .unwrap() - .clone(); - assert_eq!(inventory.item_in_main_hand(), None); - } - - #[test] - fn test_block_placement_system_unloaded_chunk() { - let (mut w, mut d) = t::builder().with(BlockPlacementSystem, "").build(); - - t::populate_with_air(&mut w); - - let player = t::add_player(&mut w); - - { - let mut inventories = w.write_component::(); - inventories - .get_mut(player.entity) - .unwrap() - .set_item_at(SLOT_HOTBAR_OFFSET, ItemStack::new(Item::Cobblestone, 1)); - - let mut players = w.write_component::(); - players.get_mut(player.entity).unwrap().gamemode = Gamemode::Survival; - } - - let pos = BlockPosition::new(1000, 100, 2000); - - let packet = PlayerBlockPlacement { - location: pos, - face: Face::Top, - hand: 0, - cursor_position_x: 0.0, - cursor_position_y: 0.0, - cursor_position_z: 0.0, - }; - t::receive_packet(&player, &w, packet); - - let mut reader = t::reader(&w); - - d.dispatch(&w); - w.maintain(); - - t::assert_disconnected(&player); - - assert!(t::triggered_events::(&w, &mut reader).is_empty()); - } -} diff --git a/server/src/player/resource_pack.rs b/server/src/player/resource_pack.rs deleted file mode 100644 index 917536250..000000000 --- a/server/src/player/resource_pack.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! System for sending resource pack to new players. - -use crate::config::Config; -use crate::joinhandler::PlayerJoinEvent; -use crate::network::{send_packet_to_player, NetworkComponent}; -use feather_core::network::packet::implementation::ResourcePackSend; -use shrev::{EventChannel, ReaderId}; -use specs::{Read, ReadStorage, System}; -use std::sync::Arc; - -/// System for sending resource pack to new players, -/// if enabled. -/// -/// This system listens to `PlayerJoinEvent`s. -#[derive(Default)] -pub struct ResourcePackSendSystem { - reader: Option>, -} - -impl<'a> System<'a> for ResourcePackSendSystem { - type SystemData = ( - ReadStorage<'a, NetworkComponent>, - Read<'a, Arc>, - Read<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (networks, config, join_events) = data; - if config.resource_pack.url.is_empty() { - return; // Resource pack not enabled - } - - for event in join_events.read(self.reader.as_mut().unwrap()) { - let network = networks.get(event.player).unwrap(); - - let packet = ResourcePackSend { - url: config.resource_pack.url.clone(), - hash: config.resource_pack.hash.to_lowercase(), - }; - - send_packet_to_player(&network, packet); - } - } - - setup_impl!(reader); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testframework as t; - use feather_core::network::cast_packet; - use feather_core::network::packet::PacketType; - use specs::WorldExt; - - #[test] - fn test_resource_pack_send_system() { - let (mut w, mut d) = t::init_world(); - - let player = t::add_player(&mut w); - - let event = PlayerJoinEvent { - player: player.entity, - }; - t::trigger_event(&w, event); - - let url = "https://rust-lang.org/".to_string(); - let hash = "bLa".to_string(); - - { - let mut config = Config::clone(&w.fetch::>()); - config.resource_pack.url = url.clone(); - config.resource_pack.hash = hash.clone(); - w.insert(Arc::new(config)); - } - - d.dispatch(&w); - w.maintain(); - - let packet = t::assert_packet_received(&player, PacketType::ResourcePackSend); - let packet = cast_packet::(&*packet); - - assert_eq!(packet.url, url); - assert_eq!(packet.hash, hash.to_lowercase()); - } -} diff --git a/server/src/player/save.rs b/server/src/player/save.rs deleted file mode 100644 index df1ac1140..000000000 --- a/server/src/player/save.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Saving of player data files and a system to save -//! player data on disconnect. - -use crate::entity::{NamedComponent, PlayerComponent, PositionComponent}; -use crate::player::{InventoryComponent, PlayerDisconnectEvent}; -use crate::prelude::Config; -use crossbeam::Receiver; -use feather_core::entity::BaseEntityData; -use feather_core::inventory::Inventory; -use feather_core::player_data::{InventorySlot, PlayerData}; -use feather_core::{player_data, Gamemode, Position}; -use shrev::{EventChannel, ReaderId}; -use specs::{Read, ReadStorage, System}; -use std::path::Path; -use std::sync::Arc; -use uuid::Uuid; - -/// System to save player data upon disconnect. -/// -/// This system listens to `PlayerDisconnectEvent`s. -#[derive(Default)] -pub struct PlayerDataSaveSystem { - reader: Option>, -} - -impl<'a> System<'a> for PlayerDataSaveSystem { - type SystemData = ( - Read<'a, Arc>, - ReadStorage<'a, PositionComponent>, - ReadStorage<'a, PlayerComponent>, - ReadStorage<'a, NamedComponent>, - ReadStorage<'a, InventoryComponent>, - Read<'a, EventChannel>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (config, positions, players, nameds, inventories, disconnect_events) = data; - - for event in disconnect_events.read(self.reader.as_mut().unwrap()) { - let player = players.get(event.player).unwrap(); - save_player_data( - &config, - positions.get(event.player).unwrap().current, - player.gamemode, - &inventories.get(event.player).unwrap().inventory, - nameds.get(event.player).unwrap().uuid, - ); - } - } - - setup_impl!(reader); -} - -/// Saves a player's data. -/// -/// This operation is performed asynchronously, -/// and a channel is returned which will receive -/// a message upon completion. -pub fn save_player_data( - config: &Config, - position: Position, - gamemode: Gamemode, - inventory: &Inventory, - uuid: Uuid, -) -> Receiver<()> { - let data = PlayerData { - entity: BaseEntityData { - velocity: vec![0.0; 3], // Player velocity has no effect - position: vec![position.x, position.y, position.z], - rotation: vec![position.yaw, position.pitch], - }, - gamemode: gamemode.get_id() as i32, - inventory: inventory - .items() - .iter() - .enumerate() - .filter_map(|(index, item)| match item.clone() { - Some(item) => Some((index, item)), - None => None, - }) - .map(|(index, item)| InventorySlot::from_network_index(index, item)) - .collect(), - }; - - // Channel used to communicate with Tokio task - let (tx, rx) = crossbeam::bounded(1); - - let world_dir = Path::new(&config.world.name).to_owned(); - - tokio_executor::blocking::run(move || { - if let Err(e) = player_data::save_player_data(world_dir.as_path(), uuid, data) { - error!("Failed to save player data for UUID {}: {:?}", uuid, e); - } else { - debug!("Saved player data for UUID {}", uuid); - } - - let _ = tx.send(()); // Channel could have been dropped, so ignore result - }); - - rx -} diff --git a/server/src/player/view.rs b/server/src/player/view.rs deleted file mode 100644 index d0cf7504c..000000000 --- a/server/src/player/view.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! This module implements creating and destroying -//! entities on the client when a player moves. -//! -//! When a player crosses chunk boundaries, the following -//! takes place: -//! * We send a `Destroy Entities` packet containing all -//! entities which are no longer within the view distance. -//! * We spawn an entity on the client for every entity -//! which is now within the view distance. -//! -//! This is handled by `ViewUpdateSystem`, which listens -//! to `ChunkCrossEvent`s. - -use crate::config::Config; -use crate::entity::ChunkEntities; -use crate::lazy::LazyUpdateExt; -use crate::network::{send_packet_to_player, NetworkComponent}; -use crate::player::movement::ChunkCrossEvent; -use feather_core::network::packet::implementation::DestroyEntities; -use shrev::EventChannel; -use specs::{LazyUpdate, Read, ReadStorage, ReaderId, System}; -use std::sync::Arc; - -/// System for updating entities visible -/// by the client. -#[derive(Default)] -pub struct ViewUpdateSystem { - reader: Option>, -} - -impl<'a> System<'a> for ViewUpdateSystem { - type SystemData = ( - ReadStorage<'a, NetworkComponent>, - Read<'a, EventChannel>, - Read<'a, ChunkEntities>, - Read<'a, Arc>, - Read<'a, LazyUpdate>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (networks, cross_events, chunk_entities, config, lazy) = data; - - for event in cross_events.read(self.reader.as_mut().unwrap()) { - // Find new and old entities. - let old_entities = - chunk_entities.entites_within_view_distance(event.old, config.server.view_distance); - let new_entities = - chunk_entities.entites_within_view_distance(event.new, config.server.view_distance); - - let mut to_destroy = vec![]; - - // Compute entities which are only present in one of the sets. - // If an entity is only present in `old_entities` and not `new_entities`, - // it should be destroyed on the client. - // If an entity is only present in `new_entities`, it should be spawned. - for entity in old_entities.symmetric_difference(&new_entities) { - if *entity == event.player { - continue; - } - - if old_entities.contains(entity) { - // Entity is in `old_entities` but not in `new_entities`. - // Destroy it. If the entity is a player, also destroy this player - // on the client. - to_destroy.push(entity.id() as i32); - - if let Some(network) = networks.get(*entity) { - let packet = DestroyEntities { - entity_ids: vec![event.player.id() as i32], - }; - send_packet_to_player(network, packet); - } - } else { - // Entity is in `new_entities` but not in `old_entities`. - // Spawn it. If the entity is a player, also send this player - // to that entity. - lazy.send_entity_to_player(event.player, *entity); - - if networks.get(*entity).is_some() { - lazy.send_entity_to_player(*entity, event.player); - } - } - } - - if !to_destroy.is_empty() { - let packet = DestroyEntities { - entity_ids: to_destroy, - }; - send_packet_to_player(&networks.get(event.player).unwrap(), packet); - } - } - } - - setup_impl!(reader); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::entity::{item, PositionComponent}; - use crate::testframework as t; - use feather_core::network::cast_packet; - use feather_core::network::packet::implementation::{SpawnObject, SpawnPlayer}; - use feather_core::{ChunkPosition, Item, ItemStack, PacketType}; - use hashbrown::HashSet; - use specs::{Builder, WorldExt}; - - #[test] - fn test_view_update_system() { - let (mut world, mut dispatcher) = t::builder() - .with(ViewUpdateSystem::default(), "view") - .build(); - - let player_chunk = ChunkPosition::new(0, 0); - - let player1 = t::add_player_without_holder(&mut world); - let player2 = t::add_player_without_holder(&mut world); - - let entity1 = item::create( - &world.fetch(), - &world.fetch(), - ItemStack::new(Item::Stone, 0), - 0, - ) - .with(PositionComponent::default()) - .build(); - let entity2 = item::create( - &world.fetch(), - &world.fetch(), - ItemStack::new(Item::Stone, 0), - 0, - ) - .with(PositionComponent::default()) - .build(); - let entity3 = item::create( - &world.fetch(), - &world.fetch(), - ItemStack::new(Item::Stone, 0), - 0, - ) - .with(PositionComponent::default()) - .build(); - let entity4 = item::create( - &world.fetch(), - &world.fetch(), - ItemStack::new(Item::Stone, 0), - 0, - ) - .with(PositionComponent::default()) - .build(); - - let mut config = Config::default(); - config.server.view_distance = 4; - world.insert(Arc::new(config)); - - { - let mut chunk_entities = world.fetch_mut::(); - chunk_entities.add_to_chunk(player_chunk, player1.entity); - chunk_entities.add_to_chunk(player_chunk, player2.entity); - chunk_entities.add_to_chunk(ChunkPosition::new(3, -3), entity1); - chunk_entities.add_to_chunk(player_chunk, entity2); - chunk_entities.add_to_chunk(ChunkPosition::new(4, -3), entity3); - chunk_entities.add_to_chunk(ChunkPosition::new(100, 103), entity4); - } - - let event = ChunkCrossEvent { - player: player1.entity, - old: ChunkPosition::new(100, 103), - new: player_chunk, - }; - t::trigger_event(&world, event); - - world.maintain(); - dispatcher.dispatch(&world); - world.maintain(); - dispatcher.dispatch(&world); - world.maintain(); - - let packets = t::received_packets(&player1, None); - - let mut received_spawns = HashSet::new(); - - for packet in packets { - dbg!(packet.ty()); - match packet.ty() { - PacketType::DestroyEntities => { - let packet = cast_packet::(&*packet); - let destroyed = packet.entity_ids.iter().cloned().collect::>(); - assert_eq!(destroyed.len(), 1); - assert!(destroyed.contains(&(entity4.id() as i32))); - } - PacketType::SpawnObject => { - let packet = cast_packet::(&*packet); - received_spawns.insert(packet.entity_id); - } - PacketType::SpawnPlayer => { - let packet = cast_packet::(&*packet); - received_spawns.insert(packet.entity_id); - } - _ => (), - } - } - - println!("{:?}", received_spawns); - assert_eq!(received_spawns.len(), 3); - assert!(received_spawns.contains(&(entity1.id() as i32))); - assert!(received_spawns.contains(&(entity2.id() as i32))); - assert!(received_spawns.contains(&(player2.entity.id() as i32))); - - // Confirm that `player1` was sent to `player2`. - let packets = t::received_packets(&player2, None); - assert_eq!(packets.len(), 2); // One for Spawn Player, one for metadata - - let packet = packets - .into_iter() - .find(|packet| packet.ty() == PacketType::SpawnPlayer) - .unwrap(); - let packet = cast_packet::(&*packet); - assert_eq!(packet.entity_id, player1.entity.id() as i32); - } -} diff --git a/server/src/prelude.rs b/server/src/prelude.rs deleted file mode 100644 index b656211e1..000000000 --- a/server/src/prelude.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub use super::TPS; -pub use crate::config::Config; -pub use feather_core::prelude::*; -pub use std::cell::RefCell; -pub use std::rc::Rc; diff --git a/server/src/shutdown.rs b/server/src/shutdown.rs index 5bcb42a43..53ba1f5e2 100644 --- a/server/src/shutdown.rs +++ b/server/src/shutdown.rs @@ -1,18 +1,6 @@ //! Shutdown behavior. - -use crate::chunk_logic::ChunkWorkerHandle; -use crate::config::Config; -use crate::entity::{NamedComponent, PlayerComponent, PositionComponent}; -use crate::player; -use crate::player::InventoryComponent; -use crate::time::Time; -use crate::{chunkworker, entity}; use crossbeam::Sender; -use feather_core::level::{save_level_file, LevelData, Root}; -use feather_core::prelude::ChunkMap; -use specs::{Join, World, WorldExt}; -use std::fs::File; -use std::sync::Arc; +use legion::world::World; pub fn init(tx: Sender<()>) { ctrlc::set_handler(move || { @@ -21,79 +9,14 @@ pub fn init(tx: Sender<()>) { .unwrap(); } -pub fn save_chunks(world: &mut World) { - let mut chunk_map = world.fetch_mut::(); - let handle = world.fetch::(); - let count = entity::save_chunks(&mut chunk_map, &world.fetch(), &world.fetch()); - - drop(chunk_map); - drop(handle); - - // Need to call `world.maintain()` for lazy chunk saving - // to take effect - world.maintain(); - - let handle = world.fetch::(); - handle.sender.send(chunkworker::Request::ShutDown).unwrap(); - - let mut saved = 0; - // Wait for chunks to finish saving - while let Ok(msg) = handle.receiver.recv() { - if let chunkworker::Reply::SavedChunk(_) = msg { - saved += 1; - } - - if saved == count { - break; - } - } - - assert!( - count == saved, - "didn't save all chunks: {} != {}", - count, - saved - ); +pub fn save_chunks(_world: &mut World) { + unimplemented!() } -pub fn save_level(world: &World) { - let mut level = (*world.fetch::()).clone(); - - // Sync world time + level time - let time = world.fetch::

(&self, entity: Entity, packet: P, neq: Option) - where - P: Packet + 'static, - { - self.queue.push(BroadcastRequest { - condition: BroadcastCondition::Entity(entity), - packet: Box::new(packet), - neq, - }); - } - - /// Lazily broadcasts a packet to all players - /// able to see a given chunk. - pub fn broadcast_chunk_update

(&self, chunk: ChunkPosition, packet: P, neq: Option) - where - P: Packet + 'static, - { - self.queue.push(BroadcastRequest { - condition: BroadcastCondition::Chunk(chunk), - packet: Box::new(packet), - neq, - }); - } - - /// Lazily sends a packet to a player. - pub fn lazy_send_packet_to_player

(&self, player: Entity, packet: P) - where - P: Packet + 'static, - { - self.queue.push(BroadcastRequest { - condition: BroadcastCondition::ToPlayer(player), - packet: Box::new(packet), - neq: None, // No effect - }); - } -} - -/// A broadcast request. -struct BroadcastRequest { - /// Packet will only be sent to players able to see - /// this entity or chunk. - condition: BroadcastCondition, - /// The packet to broadcast. - packet: Box, - /// Optional entity not to send to. - neq: Option, -} - -#[derive(Debug, Clone, Copy)] -enum BroadcastCondition { - Entity(Entity), - Chunk(ChunkPosition), - ToPlayer(Entity), -} - -/// System for flushing the `Broadcaster` queue and broadcasting -/// the necessary packets. -pub struct BroadcasterSystem; - -impl<'a> System<'a> for BroadcasterSystem { - type SystemData = ( - ReadStorage<'a, PositionComponent>, - ReadStorage<'a, NetworkComponent>, - Read<'a, Util>, - Read<'a, ChunkHolders>, - Entities<'a>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, networks, util, chunk_holders, entities) = data; - - let broadcaster = &util.broadcaster; - - while let Ok(request) = broadcaster.queue.pop() { - // Broadcast packet. - // Iterate over entities in the chunk_holders - // entry for the chunk. If they are a player, - // send the packet. - // This works because any player able to see - // a chunk will always have a chunk holder on the chunk. - - let chunk = match request.condition { - BroadcastCondition::Entity(entity) => { - // Prevents a panic if the entity was destroyed. - if !entities.is_alive(entity) { - continue; - } - - positions.get(entity).unwrap().current.chunk_pos() - } - BroadcastCondition::Chunk(chunk) => chunk, - BroadcastCondition::ToPlayer(player) => { - if entities.is_alive(player) { - send_packet_boxed_to_player(networks.get(player).unwrap(), request.packet); - } - - continue; // Special case - no chunk - } - }; - if let Some(holders) = chunk_holders.holders_for(chunk) { - for holder in holders { - if let Some(neq) = request.neq.as_ref() { - if *holder == *neq { - continue; - } - } - - if let Some(network) = networks.get(*holder) { - send_packet_boxed_to_player(network, request.packet.box_clone()); - } - } - } - } - } -} diff --git a/server/src/util/macros.rs b/server/src/util/macros.rs deleted file mode 100644 index 66e6b841a..000000000 --- a/server/src/util/macros.rs +++ /dev/null @@ -1,70 +0,0 @@ -/// Asserts that a floating-point value is within -/// a certain range of the expected value. -#[cfg(test)] -macro_rules! assert_float_eq { - ($left:expr, $right:expr) => { - assert_float_eq!($left, $right, 0.001); - }; - ($left:expr, $right:expr, $range:expr) => { - let range = ($left - $range)..($left + $range); - assert!(range.contains(&$right)); - }; -} - -/// Checks that two positions are approximately equivalent. -#[cfg(test)] -macro_rules! assert_pos_eq { - ($left:expr, $right:expr) => { - assert_float_eq!($left.x, $right.x, 0.01); - assert_float_eq!($left.y, $right.y, 0.01); - assert_float_eq!($left.z, $right.z, 0.011); - }; -} - -/// Generates a setup() implementation for a system -/// which initializes an internal event reader. -macro_rules! setup_impl { - ($($reader:ident),+) => { - fn setup(&mut self, world: &mut specs::World) { - use specs::SystemData; - use shrev::EventChannel; - Self::SystemData::setup(world); - - $(self.$reader = Some(world.fetch_mut::>().register_reader());)+ - } - }; -} - -macro_rules! flagged_setup_impl { - ($component:ident, $reader:ident) => { - fn setup(&mut self, world: &mut specs::World) { - use specs::{SystemData, WorldExt}; - Self::SystemData::setup(world); - - self.$reader = Some(world.write_component::<$component>().register_reader()); - } - } -} - -macro_rules! read_flagged_events { - ($storage:ident, $reader:expr, $dirty:expr) => { - for event in $storage.channel().read($reader.as_mut().unwrap()) { - match event { - ComponentEvent::Inserted(id) | ComponentEvent::Modified(id) => { - $dirty.add(*id); - } - _ => (), - } - } - }; -} - -macro_rules! continue_if_none { - ($option:expr) => { - if let Some(value) = $option { - value - } else { - continue; - } - }; -} diff --git a/server/src/util/mod.rs b/server/src/util/mod.rs deleted file mode 100644 index e3798445a..000000000 --- a/server/src/util/mod.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! Assorted utilities for use in Feather's codebase. -use bumpalo::Bump; -use feather_core::{ChunkPosition, Packet}; -use glm::DVec3; -use thread_local::ThreadLocal; - -#[macro_use] -mod macros; -mod broadcaster; - -use broadcaster::Broadcaster; -pub use broadcaster::BroadcasterSystem; -pub use macros::*; -use specs::Entity; - -/// Converts float-based velocity in blocks per tick -/// to the format used by the protocol. -pub fn protocol_velocity(vel: DVec3) -> (i16, i16, i16) { - // Apparently, these are in units of 1/8000 block per tick. - ( - (vel.x * 8000.0) as i16, - (vel.y * 8000.0) as i16, - (vel.z * 8000.0) as i16, - ) -} - -/// General-purpose utility resource, used to -/// ergonomically perform various actions without -/// specifying ridiculous amounts of system dependencies. -/// -/// This struct is thread-safe and should never be used -/// as a `Write` dependency. -/// -/// # Available functions -/// * `alloc` - used to allocate temporary values -/// using a bump allocator. When allocating values -/// which will only be used inside a function, for example, -/// this function should be used rather than allocating -/// directly on the heap. -/// * `broadcast` - lazily broadcasts a packet to all players -/// who are able to see a given chunk. This can be used -/// to broadcast movement updates, for example. -#[derive(Default)] -pub struct Util { - /// Thread-local bump allocator, reset - /// every tick. - /// - /// This is used to reduce allocation frequency. - bump: ThreadLocal, - /// The broadcaster, used to lazily broadcast packets. - broadcaster: Broadcaster, -} - -impl Util { - /// Returns the thread-local bump allocator. - pub fn bump(&self) -> &Bump { - self.bump.get_or_default() - } - - /// Allocates a value using the bump allocator - /// for this thread. - /// - /// This is equivalent to `Util::bump().alloc(value)`. - #[allow(clippy::mut_from_ref)] // This is sound—it just redirects to `bumpalo`. - pub fn alloc(&self, value: T) -> &mut T { - self.bump().alloc(value) - } - - /// This should be called at the end of every tick. - pub fn reset(&mut self) { - // Reset bump allocators - for bump in self.bump.iter_mut() { - bump.reset(); - } - } - - /// Broadcasts a packet to all players who - /// are able to see a given entity. - /// - /// The packet is sent lazily in a separate system. - /// - /// If `neq` is set to an entity, the packet - /// will not be sent to that player. - /// - /// This function runs in linear time with - /// regard to the number of players able to see - /// the entity. - pub fn broadcast_entity_update

(&self, entity: Entity, packet: P, neq: Option) - where - P: Packet + 'static, - { - self.broadcaster - .broadcast_entity_update(entity, packet, neq); - } - - /// Broadcasts a packet to all players who - /// are able to see a given chunk. - /// - /// The packet is sent lazily in a separate system. - /// - /// If `neq` is set to an entity, the packet - /// will not be sent to that player. - /// - /// This function runs in linear time with - /// regard to the number of players able to see - /// the chunk. - pub fn broadcast_chunk_update

(&self, chunk: ChunkPosition, packet: P, neq: Option) - where - P: Packet + 'static, - { - self.broadcaster.broadcast_chunk_update(chunk, packet, neq); - } - - /// Similar to `broadcast_*_update`, but only sends to the specified - /// player. - /// - /// This can be used when a packet needs to be sent to a specified - /// player before another packet is broadcasted. - pub fn lazy_send_packet_to_player

(&self, player: Entity, packet: P) - where - P: Packet + 'static, - { - self.broadcaster.lazy_send_packet_to_player(player, packet); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::chunk_logic::ChunkHolders; - use crate::testframework as t; - use crate::util::broadcaster::BroadcasterSystem; - use feather_core::network::packet::implementation::EntityHeadLook; - use feather_core::PacketType; - use specs::WorldExt; - - #[test] - fn test_broadcast() { - let mut chunk_holders = ChunkHolders::default(); - - let chunk = ChunkPosition::new(0, 0); - let other_chunk = ChunkPosition::new(10, 1); - - let (mut world, mut dispatcher) = t::builder().with(BroadcasterSystem, "").build(); - let player1 = t::add_player(&mut world); - let player2 = t::add_player(&mut world); - let player3 = t::add_player(&mut world); - - chunk_holders.insert_holder(chunk, player1.entity); - chunk_holders.insert_holder(chunk, player2.entity); - chunk_holders.insert_holder(other_chunk, player3.entity); - - let packet = EntityHeadLook::default(); - - let util = Util::default(); - - util.broadcast_entity_update(player1.entity, packet, Some(player2.entity)); - - world.insert(util); - world.insert(chunk_holders); - - dispatcher.dispatch(&world); - world.maintain(); - - t::assert_packet_received(&player1, PacketType::EntityHeadLook); - t::assert_packet_not_received(&player2, PacketType::EntityHeadLook); - t::assert_packet_not_received(&player3, PacketType::EntityHeadLook); - } -} diff --git a/server/src/view.rs b/server/src/view.rs new file mode 100644 index 000000000..eec0f42f7 --- /dev/null +++ b/server/src/view.rs @@ -0,0 +1,368 @@ +//! Handling of a player's "view." +//! +//! This module includes systems and components +//! which handle sending new data as +//! a player moves through the world. +//! +//! When a player crosses a chunk boundary, its +//! view has changed: some chunks are no longer visible, +//! while others now are. To account for this, we +//! must send the new chunks, unload the old +//! chunks on the client, send new entities, and +//! delete old ones. +//! +//! This is handled as follows: +//! * A system listens for player move events and checks if the player +//! crossed a chunk boundary. If so, a `ViewUpdateEvent` is triggered. +//! * Various systems listen to `ViewUpdateEvent` and send necessary packets. +//! This includes systems to load/unload chunks and send entities. + +use crate::chunk_logic; +use crate::chunk_logic::{ + ChunkHolder, ChunkHolderReleaseEvent, ChunkHolders, ChunkLoadEvent, ChunkWorkerHandle, +}; +use crate::config::Config; +use crate::entity::{EntityId, EntityMoveEvent, PreviousPosition, SpawnPacketCreator}; +use crate::network::Network; +use crate::player::PlayerJoinEvent; +use crate::state::State; +use chashmap::CHashMap; +use feather_core::network::packet::implementation::{ChunkData, DestroyEntities, UnloadChunk}; +use feather_core::{Chunk, ChunkPosition, Position}; +use hashbrown::HashSet; +use legion::entity::Entity; +use legion::query::{Read, Write}; +use parking_lot::Mutex; +use rayon::prelude::*; +use smallvec::SmallVec; +use tonks::{PreparedWorld, Query, QueryAccessor, Trigger}; + +/// Event triggered when a player's view is updated, i.e. when they +/// cross into a new chunk or when they join. +pub struct ViewUpdateEvent { + /// The player whose view was updated. + pub player: Entity, + /// The new chunk. + pub new_chunk: ChunkPosition, + /// The old chunk, or `None` if there was no old chunk + /// (i.e. this player just joined). + pub old_chunk: Option, + /// Old visible chunks. + pub visible_old: HashSet, + /// New visible chunks. + pub visible_new: HashSet, +} + +/// Event triggered when a chunk is sent to a player. +#[derive(Debug)] +pub struct ChunkSendEvent { + pub chunk: ChunkPosition, + pub player: Entity, +} + +/// System which checks for players crossing chunk boundaries +/// and triggers `ViewUpdateEvent`s. +#[event_handler] +fn view_update( + events: &[EntityMoveEvent], + _query: &mut Query<(Read, Read)>, + world: &mut PreparedWorld, + state: &State, + trigger: &mut Trigger, +) { + let trigger = Mutex::new(trigger); + events.par_iter().for_each(|event| { + let pos = *world.get_component::(event.entity).unwrap(); + let prev_pos = world + .get_component::(event.entity) + .unwrap() + .0; + + // Find the old chunks and new chunks. + let visible_new = chunks_within_view_distance(&state.config, pos.chunk_pos()); + let visible_old = chunks_within_view_distance(&state.config, prev_pos.chunk_pos()); + + if pos.chunk_pos() != prev_pos.chunk_pos() { + // New chunk: trigger view update. + let event = ViewUpdateEvent { + player: event.entity, + new_chunk: pos.chunk_pos(), + old_chunk: Some(prev_pos.chunk_pos()), + visible_old, + visible_new, + }; + trigger.lock().trigger(event); + } + }); +} + +/// System which triggers `ViewUpdateEvent`s on player join. +#[event_handler] +fn view_update_on_join( + event: &PlayerJoinEvent, + _query: &mut Query>, + world: &mut PreparedWorld, + trigger: &mut Trigger, + state: &State, +) { + let position = *world.get_component::(event.player).unwrap(); + + // Find the visible chunks. + let visible_new = chunks_within_view_distance(&state.config, position.chunk_pos()); + + trigger.trigger(ViewUpdateEvent { + player: event.player, + new_chunk: position.chunk_pos(), + old_chunk: None, + visible_new, + visible_old: HashSet::new(), // No chunks were previously visible, since the player just joined + }); +} + +/// System which sends new chunks and unloads old chunks on the client +/// when the view is updated. +#[event_handler] +fn view_handle_chunks( + events: &[ViewUpdateEvent], + _query: &mut Query<(Read, Write)>, + world: &mut PreparedWorld, + holders: &mut ChunkHolders, + state: &State, + chunks_to_send: &ChunksToSend, + handle: &ChunkWorkerHandle, + holder_release_trigger: &mut Trigger, + chunk_send_trigger: &mut Trigger, +) { + events.iter().for_each(|event| { + let to_send = event.visible_new.difference(&event.visible_old); + let to_unload = event.visible_old.difference(&event.visible_new); + + let network = world.get_component::(event.player).unwrap(); + let mut holder = + unsafe { world.get_component_mut_unchecked::(event.player) }.unwrap(); + + // Sort sent chunks so that closer chunks are sent first. + let mut to_send = to_send.copied().collect::>(); + to_send.sort_unstable_by_key(|chunk| { + chunk.manhattan_distance(event.new_chunk); + }); + + // Send new chunks. + to_send.into_iter().for_each(|chunk| { + send_chunk_to_player( + state, + event.player, + &network, + &mut holder, + holders, + chunk, + chunks_to_send, + handle, + chunk_send_trigger, + ); + }); + + // Unload old chunks on client. + to_unload.for_each(|chunk| { + unload_chunk_for_player( + event.player, + &network, + holder_release_trigger, + &mut holder, + holders, + *chunk, + ); + }); + }); +} + +/// System which sends new entities and removes +/// old entities on the client when the player's +/// view is updated. +/// +/// Before this event handler is run, `crate::broadcast::entity_creation::broadcast_entity_creation` +/// will run, sending entity initialization packets before spawn packets as dictated +/// by the protocol. +#[event_handler] +fn view_handle_entities( + events: &[ViewUpdateEvent], + state: &State, + _query: &mut Query<(Read, Read)>, + accessor: &QueryAccessor>, + world: &mut PreparedWorld, +) { + events.par_iter().for_each(|event: &ViewUpdateEvent| { + let to_send = event.visible_new.difference(&event.visible_old); + let to_unload = event.visible_old.difference(&event.visible_new); + + let network = world.get_component::(event.player).unwrap(); + + // Send new entities. + to_send.copied().for_each(|chunk| { + let entities = state.chunk_entities.entities_in_chunk(chunk); + + if !entities.is_empty() { + dbg!(event.player, &entities); + } + + entities.iter().copied().for_each(|entity| { + // Don't send client to themself. + if entity == event.player { + return; + } + + // Attempt to create spawn packet for this entity. + if let Some(accessor) = accessor.find(entity) { + if let Some(packet_creator) = + accessor.get_component::(world) + { + // Send packet. + let packet = packet_creator.get(&accessor, world); + network.send_boxed(packet); + state.register_entity_send(entity, event.player); + } + } + }); + }); + + // Remove old entities. + let mut to_delete = vec![]; + for chunk in to_unload.copied() { + for entity in state + .chunk_entities + .entities_in_chunk(chunk) + .iter() + .copied() + { + let id = world.get_component::(entity).unwrap().0; + to_delete.push(id); + state.register_entity_unload(entity, event.player); + } + } + + if !to_delete.is_empty() { + let packet = DestroyEntities { + entity_ids: to_delete, + }; + network.send(packet); + } + }); +} + +/// Resource containing a mapping from chunks -> sets of players indicating +/// which chunks are pending to send to a given player. +#[derive(Default, Resource)] +pub struct ChunksToSend(CHashMap>); + +/// Asynchronously sends a chunk to a player. +#[allow(clippy::too_many_arguments)] +fn send_chunk_to_player( + state: &State, + player: Entity, + network: &Network, + holder: &mut ChunkHolder, + holders: &mut ChunkHolders, + chunk: ChunkPosition, + chunks_to_send: &ChunksToSend, + handle: &ChunkWorkerHandle, + trigger: &mut Trigger, +) { + // Ensure that the chunk isn't unloaded while the player has it loaded. + chunk_logic::hold_chunk(player, holder, holders, chunk); + + // If the chunk is already loaded, send it. Otherwise, we need to + // queue it for loading. + if let Some(chunk) = state.chunk_at(chunk) { + network.send(create_chunk_data(&chunk)); + trigger.trigger(ChunkSendEvent { + chunk: chunk.position(), + player, + }); + } else { + let contains = chunks_to_send.0.contains_key(&chunk); + + let mut vec = match chunks_to_send.0.get_mut(&chunk) { + Some(vec) => vec, + None => { + chunks_to_send.0.insert(chunk, smallvec![]); + chunks_to_send.0.get_mut(&chunk).unwrap() + } + }; + vec.push(player); + + if !contains { + // Queue chunk for loading if it isn't already. + chunk_logic::load_chunk(handle, chunk); + } + } +} + +/// Unloads a chunk on a client. +fn unload_chunk_for_player( + player: Entity, + network: &Network, + trigger: &mut Trigger, + holder: &mut ChunkHolder, + holders: &mut ChunkHolders, + chunk: ChunkPosition, +) { + // Release hold on chunk so it can be unloaded on the server + chunk_logic::release_chunk(player, holder, holders, chunk, trigger); + + // Send Unload Chunk packet. + network.send(UnloadChunk { + chunk_x: chunk.x, + chunk_z: chunk.z, + }); +} + +/// System which sends chunks to pending players when a chunk is loaded. +#[event_handler] +fn chunk_send( + event: &ChunkLoadEvent, + state: &State, + to_send: &ChunksToSend, + _query: &mut Query>, + world: &mut PreparedWorld, + trigger: &mut Trigger, +) { + if let Some(players) = to_send.0.get(&event.pos) { + let chunk = state + .chunk_at(event.pos) + .expect("chunk not loaded, but load event was triggered"); + players.iter().for_each(|player| { + let network = world.get_component::(*player).unwrap(); + network.send(create_chunk_data(&chunk)); + trigger.trigger(ChunkSendEvent { + chunk: chunk.position(), + player: *player, + }); + }); + } + + to_send.0.remove(&event.pos); +} + +/// Creates a chunk data packet for the given chunk. +fn create_chunk_data(chunk: &Chunk) -> ChunkData { + ChunkData { + chunk: chunk.clone(), // TODO: optimize + } +} + +/// Finds all chunks within the view distance of a given chunk. +fn chunks_within_view_distance(config: &Config, position: ChunkPosition) -> HashSet { + let view_distance = config.server.view_distance as i32; + + let dimensions = view_distance * 2 + 1; + + let mut set = HashSet::with_capacity((dimensions * dimensions) as usize); + + for x in -view_distance..=view_distance { + for z in -view_distance..=view_distance { + set.insert(position + ChunkPosition::new(x, z)); + } + } + + set +}