diff --git a/.gitignore b/.gitignore index 79c1a3956..5e28f45ec 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,14 @@ .cargo world/ -/config.toml +feather/downloaded +feather/datapacks/minecraft/ +config.toml +plugins +banned-players.json +banned-ips.json +usercache.json +.feather_command_history # Python cache files (libcraft) **/__pycache__/ diff --git a/Cargo.lock b/Cargo.lock index 7f6eaa755..3240149a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,7 +156,7 @@ dependencies = [ "slab", "socket2", "waker-fn", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -173,7 +173,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -220,9 +220,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.3.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitvec" @@ -265,6 +265,15 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +[[package]] +name = "bungeecord-servers-plugin" +version = "0.1.0" +dependencies = [ + "anyhow", + "commands", + "quill", +] + [[package]] name = "bytecount" version = "0.6.2" @@ -412,7 +421,7 @@ dependencies = [ "num-integer", "num-traits", "time", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -430,6 +439,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "clipboard-win" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8" +dependencies = [ + "error-code", + "str-buf", + "winapi 0.3.9", +] + [[package]] name = "cmake" version = "0.1.45" @@ -447,7 +467,7 @@ checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" dependencies = [ "atty", "lazy_static", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -458,7 +478,20 @@ checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" dependencies = [ "atty", "lazy_static", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "commands" +version = "0.1.0" +source = "git+https://github.com/Iaiao/commands?rev=42bebd15e18cf511355aaf5f241fb4218031c4ee#42bebd15e18cf511355aaf5f241fb4218031c4ee" +dependencies = [ + "anyhow", + "log", + "quartz_nbt 0.2.4 (git+https://github.com/Rusty-Quartz/quartz_nbt)", + "serde", + "slab", + "uuid", ] [[package]] @@ -693,12 +726,39 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enumset" version = "1.0.7" @@ -729,6 +789,16 @@ dependencies = [ "serde", ] +[[package]] +name = "error-code" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -744,6 +814,17 @@ dependencies = [ "instant", ] +[[package]] +name = "fd-lock" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8806dd91a06a7a403a8e596f9bfbfb34e469efbc363fc9c9713e79e26472e36" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + [[package]] name = "feather-base" version = "0.1.0" @@ -778,7 +859,7 @@ dependencies = [ "smallvec", "thiserror", "uuid", - "vek", + "vek 0.14.1", ] [[package]] @@ -792,7 +873,7 @@ dependencies = [ "once_cell", "serde", "thiserror", - "vek", + "vek 0.14.1", ] [[package]] @@ -812,14 +893,41 @@ dependencies = [ "syn", ] +[[package]] +name = "feather-commands" +version = "0.6.0" +dependencies = [ + "anyhow", + "commands", + "feather-base", + "feather-blocks", + "feather-common", + "feather-datapacks", + "feather-ecs", + "libcraft-core", + "libcraft-items", + "libcraft-text", + "log", + "quill-common", + "rand 0.7.3", + "serde", + "smallvec", + "tokio 0.2.25", + "uuid", + "vek 0.10.4", +] + [[package]] name = "feather-common" version = "0.1.0" dependencies = [ "ahash 0.7.4", "anyhow", + "chrono", + "crossbeam-utils", "feather-base", "feather-blocks", + "feather-datapacks", "feather-ecs", "feather-utils", "feather-worldgen", @@ -833,6 +941,8 @@ dependencies = [ "quill-common", "rand 0.8.4", "rayon", + "serde", + "serde_json", "smartstring", "uuid", ] @@ -873,13 +983,16 @@ dependencies = [ "bincode", "bumpalo", "bytemuck", + "commands", "feather-base", + "feather-commands", "feather-common", "feather-ecs", "feather-plugin-host-macros", "libloading", "log", "paste 1.0.5", + "quill", "quill-common", "quill-plugin-format", "serde", @@ -932,9 +1045,12 @@ dependencies = [ "base64", "chrono", "colored 2.0.0", + "commands", "crossbeam-utils", "feather-base", + "feather-commands", "feather-common", + "feather-datapacks", "feather-ecs", "feather-plugin-host", "feather-protocol", @@ -947,6 +1063,7 @@ dependencies = [ "hematite-nbt", "libcraft-core", "libcraft-items", + "libcraft-text", "log", "md-5", "num-bigint 0.4.2", @@ -958,11 +1075,12 @@ dependencies = [ "ring", "rsa", "rsa-der", + "rustyline", "serde", "serde_json", "sha-1", "slab", - "tokio", + "tokio 1.12.0", "toml", "ureq", "uuid", @@ -1010,7 +1128,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1055,6 +1173,22 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + [[package]] name = "funty" version = "1.2.0" @@ -1084,7 +1218,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite", + "pin-project-lite 0.2.7", "waker-fn", ] @@ -1259,9 +1393,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" dependencies = [ "cfg-if 1.0.0", ] @@ -1288,6 +1422,15 @@ dependencies = [ "syn", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "itertools" version = "0.10.1" @@ -1312,6 +1455,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1342,9 +1495,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.102" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" [[package]] name = "libcraft-blocks" @@ -1374,7 +1527,7 @@ dependencies = [ "serde", "strum", "strum_macros", - "vek", + "vek 0.14.1", ] [[package]] @@ -1430,9 +1583,9 @@ dependencies = [ name = "libcraft-text" version = "0.1.0" dependencies = [ - "hematite-nbt", "nom", "nom_locate", + "quartz_nbt 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "serde_with", @@ -1447,7 +1600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" dependencies = [ "cfg-if 1.0.0", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1553,6 +1706,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.4.1" @@ -1602,6 +1761,25 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + [[package]] name = "mio" version = "0.7.13" @@ -1610,9 +1788,44 @@ checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" dependencies = [ "libc", "log", - "miow", + "miow 0.3.7", "ntapi", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio 0.6.23", + "miow 0.3.7", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", ] [[package]] @@ -1621,7 +1834,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1639,6 +1852,39 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3bb9a13fa32bc5aeb64150cd3f32d6cf4c748f8f8a417cce5d2eb976a8370ba" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "5.1.2" @@ -1667,7 +1913,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1841,7 +2087,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1922,6 +2168,12 @@ dependencies = [ "syn", ] +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + [[package]] name = "pin-project-lite" version = "0.2.7" @@ -1930,9 +2182,9 @@ checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" [[package]] name = "plugin-message" @@ -1951,7 +2203,7 @@ dependencies = [ "libc", "log", "wepoll-ffi", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2019,6 +2271,54 @@ dependencies = [ "syn", ] +[[package]] +name = "quartz_nbt" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24532990479062a9c515987986225879bd115ccb97672b1fb56d788a5adb7d39" +dependencies = [ + "anyhow", + "byteorder", + "cesu8", + "flate2", + "quartz_nbt_macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", +] + +[[package]] +name = "quartz_nbt" +version = "0.2.4" +source = "git+https://github.com/Rusty-Quartz/quartz_nbt#f2b92c3945ab6d5e72d1149eada11ed9c58871d6" +dependencies = [ + "anyhow", + "byteorder", + "cesu8", + "flate2", + "quartz_nbt_macros 0.1.1 (git+https://github.com/Rusty-Quartz/quartz_nbt)", + "serde", +] + +[[package]] +name = "quartz_nbt_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "289baa0c8a4d1f840d2de528a7f8c29e0e9af48b3018172b3edad4f716e8daed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quartz_nbt_macros" +version = "0.1.1" +source = "git+https://github.com/Rusty-Quartz/quartz_nbt#f2b92c3945ab6d5e72d1149eada11ed9c58871d6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "query-entities" version = "0.1.0" @@ -2033,6 +2333,7 @@ version = "0.1.0" dependencies = [ "bincode", "bytemuck", + "commands", "itertools", "libcraft-blocks", "libcraft-core", @@ -2077,8 +2378,10 @@ dependencies = [ name = "quill-sys" version = "0.1.0" dependencies = [ + "commands", "quill-common", "quill-sys-macros", + "slab", ] [[package]] @@ -2105,6 +2408,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.7.3" @@ -2238,6 +2551,16 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.3", + "redox_syscall", +] + [[package]] name = "regalloc" version = "0.0.31" @@ -2275,7 +2598,7 @@ dependencies = [ "bitflags", "libc", "mach", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2284,7 +2607,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2299,7 +2622,7 @@ dependencies = [ "spin 0.5.2", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2405,6 +2728,30 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" +[[package]] +name = "rustyline" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790487c3881a63489ae77126f57048b42d62d3b2bafbf37453ea19eedb6340d6" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi 0.3.9", +] + [[package]] name = "ryu" version = "1.0.5" @@ -2627,7 +2974,7 @@ dependencies = [ "chrono", "colored 1.9.3", "log", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2638,9 +2985,9 @@ checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "smartstring" @@ -2659,7 +3006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2689,6 +3036,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" + [[package]] name = "strsim" version = "0.10.0" @@ -2721,9 +3074,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" dependencies = [ "proc-macro2", "quote", @@ -2782,7 +3135,7 @@ dependencies = [ "rand 0.8.4", "redox_syscall", "remove_dir_all", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2812,7 +3165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2826,9 +3179,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5241dd6f21443a3606b432718b166d3cedc962fd4b8bea54a8bc7f514ebda986" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -2848,29 +3201,64 @@ dependencies = [ [[package]] name = "tokio" -version = "1.11.0" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio 0.6.23", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite 0.1.12", + "signal-hook-registry", + "slab", + "tokio-macros 0.2.6", + "winapi 0.3.9", +] + +[[package]] +name = "tokio" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" dependencies = [ "autocfg 1.0.1", "bytes 1.1.0", "libc", "memchr", - "mio", + "mio 0.7.13", "num_cpus", "once_cell", "parking_lot", - "pin-project-lite", + "pin-project-lite 0.2.7", "signal-hook-registry", - "tokio-macros", - "winapi", + "tokio-macros 1.4.1", + "winapi 0.3.9", ] [[package]] name = "tokio-macros" -version = "1.3.0" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-macros" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb" dependencies = [ "proc-macro2", "quote", @@ -2888,12 +3276,12 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba9ab62b7d6497a8638dfda5e5c4fb3b2d5a7fca4118f2b96151c8ef1a437e" +checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" dependencies = [ "cfg-if 1.0.0", - "pin-project-lite", + "pin-project-lite 0.2.7", "tracing-attributes", "tracing-core", ] @@ -2975,6 +3363,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -3017,6 +3411,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "uuid" version = "0.8.2" @@ -3024,6 +3424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom 0.2.3", + "md5", "serde", ] @@ -3039,6 +3440,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae23c56872cdb2d1b1ddb90112da26615654fa4d4e3ee84e2d3b3e9c9853145" +[[package]] +name = "vek" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e44defd4e0c629bdc842e5d180dda428b3abd2c6b0c7e1fced8c718f65d5f77" +dependencies = [ + "approx 0.3.2", + "num-integer", + "num-traits", + "rustc_version 0.2.3", + "serde", + "static_assertions", +] + [[package]] name = "vek" version = "0.14.1" @@ -3152,7 +3567,7 @@ dependencies = [ "wasmer-engine-universal", "wasmer-types", "wasmer-vm", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3289,7 +3704,7 @@ dependencies = [ "wasmer-engine", "wasmer-types", "wasmer-vm", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3336,7 +3751,7 @@ dependencies = [ "serde", "thiserror", "wasmer-types", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3355,7 +3770,7 @@ dependencies = [ "typetag", "wasmer", "wasmer-wasi-types", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3440,6 +3855,12 @@ dependencies = [ "libc", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -3450,6 +3871,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -3462,6 +3889,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "wyz" version = "0.2.0" @@ -3479,18 +3916,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd" +checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1" +checksum = "bdff2024a851a322b08f179173ae2ba620445aef1e838f0c196820eade4ae0c7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c4b46578e..628d7b4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "quill/example-plugins/query-entities", "quill/example-plugins/simple", "quill/example-plugins/observe-creativemode-flight-event", + "quill/example-plugins/bungeecord-servers", # Feather (common and server) "feather/utils", diff --git a/feather/base/src/lib.rs b/feather/base/src/lib.rs index 730d4c51a..9e610298c 100644 --- a/feather/base/src/lib.rs +++ b/feather/base/src/lib.rs @@ -28,7 +28,7 @@ pub use libcraft_core::{ pub use libcraft_inventory::{Area, Inventory}; pub use libcraft_items::{Item, ItemStack, ItemStackBuilder, ItemStackError}; pub use libcraft_particles::{Particle, ParticleKind}; -pub use libcraft_text::{deserialize_text, Text, Title}; +pub use libcraft_text::{deserialize_text, Text, TextComponentBuilder, TextValue, Title}; #[doc(inline)] pub use metadata::EntityMetadata; diff --git a/feather/blocks/src/lib.rs b/feather/blocks/src/lib.rs index f8cf91187..baea881dd 100644 --- a/feather/blocks/src/lib.rs +++ b/feather/blocks/src/lib.rs @@ -93,7 +93,6 @@ impl BlockId { VANILLA_ID_TABLE[self.kind as u16 as usize][self.state as usize] } - /* /// Returns the vanilla fluid ID for this block in case it is a fluid. /// The fluid ID is used in the Tags packet. pub fn vanilla_fluid_id(self) -> Option { @@ -111,7 +110,6 @@ impl BlockId { None } } - */ /// Returns the block corresponding to the given vanilla ID. /// diff --git a/feather/commands/Cargo.toml b/feather/commands/Cargo.toml new file mode 100644 index 000000000..c492ef136 --- /dev/null +++ b/feather/commands/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "feather-commands" +version = "0.6.0" +authors = ["caelunshun "] +edition = "2018" + +[dependencies] + +commands = { git = "https://github.com/Iaiao/commands", rev = "42bebd15e18cf511355aaf5f241fb4218031c4ee" } +smallvec = "1.4" +anyhow = "1.0" +rand = "0.7" +vek = "0.10" +uuid = { version = "0.8", features = [ "v3" ] } +tokio = { version = "0.2", features = [ "full" ] } +serde = { version = "1", features = [ "derive" ] } +log = "0.4.14" +common = { path = "../common", package = "feather-common" } +ecs = { path = "../ecs", package = "feather-ecs" } +quill-common = { path = "../../quill/common" } +libcraft-text = { path = "../../libcraft/text" } +libcraft-items = { path = "../../libcraft/items" } +libcraft-core = { path = "../../libcraft/core" } +feather-datapacks = { path = "../datapacks" } +feather-blocks = { path = "../blocks" } +feather-base = { path = "../base" } diff --git a/feather/commands/src/impls.rs b/feather/commands/src/impls.rs new file mode 100644 index 000000000..f6bda2b11 --- /dev/null +++ b/feather/commands/src/impls.rs @@ -0,0 +1,1388 @@ +//! The implementations of various commands. + +use std::collections::BTreeMap; + +use anyhow::bail; +use commands::arguments::*; +use commands::create_command::CreateCommand; +use commands::dispatcher::{CommandDispatcher, CommandOutput}; +use smallvec::SmallVec; +use uuid::Uuid; + +use common::banlist::{BanList, BanReason}; +use common::{Game, Window}; +use ecs::{Ecs, Entity}; +use feather_blocks::BlockId; +use libcraft_core::Position; +use libcraft_items::{InventorySlot, ItemStack}; +use libcraft_text::{Text, TextComponentBuilder}; +use quill_common::components::{ + ChatBox, DefaultGamemode, Gamemode, Name, PreviousGamemode, RealIp, +}; +use quill_common::entities::Player; +use quill_common::events::{DisconnectEvent, GamemodeUpdateEvent, InventoryUpdateEvent}; + +use crate::utils::*; +use crate::{BlockPosError, CommandCtx}; + +pub fn register_all(dispatcher: &mut CommandDispatcher) { + // /me + dispatcher + .create_command("me") + .argument("text", StringArgument::GREEDY_PHRASE, None) + .executes(|context: CommandCtx, action: &mut String| { + let command_output = Text::translate_with( + "chat.type.emote", + vec![ + get_entity_name(context.sender, &context.ecs), + action.to_owned(), + ], + ); + context + .ecs + .query::<&mut ChatBox>() + .iter() + .for_each(|(_, chat_box)| chat_box.send_chat(command_output.clone())); + Ok(1) + }); + + // /gamemode + dispatcher + .create_command("gamemode") + .with(|command| gamemode_command(command, "survival", Gamemode::Survival)) + .with(|command| gamemode_command(command, "creative", Gamemode::Creative)) + .with(|command| gamemode_command(command, "adventure", Gamemode::Adventure)) + .with(|command| gamemode_command(command, "spectator", Gamemode::Spectator)); + fn gamemode_command(command: CreateCommand, s: &str, gamemode: Gamemode) { + command + .subcommand(s) + .executes(move |mut context: CommandCtx| { + if context.ecs.get::(context.sender).is_ok() { + update_gamemode(context.sender, &mut context.ecs, gamemode)?; + context.send_message(Text::translate_with( + "commands.gamemode.success.self", + vec![Text::translate(format!( + "gameMode.{}", + gamemode.to_string() + ))], + )); + Ok(1) + } else { + context.send_message(Text::translate("permissions.requires.player").red()); + bail!("Requires a player") + } + }) + .argument("target", EntityArgument::PLAYERS, "minecraft:entity") + .executes( + move |mut context: CommandCtx, selector: &mut EntitySelector| { + if let Some(targets) = + context.find_non_empty_entities_by_selector(selector, true) + { + let mut len = 0; + for target in targets { + let name = get_entity_name(target, &context.ecs); + if update_gamemode(target, &mut context.ecs, gamemode).is_ok() { + len += 1; + context.send_message(if target == context.sender { + Text::translate_with( + "commands.gamemode.success.self", + vec![Text::translate(format!( + "gameMode.{}", + gamemode.to_string() + ))], + ) + } else { + Text::translate_with( + "commands.gamemode.success.other", + vec![ + name.into(), + Text::translate(format!( + "gameMode.{}", + gamemode.to_string() + )), + ], + ) + }); + } + } + Ok(len) + } else { + bail!("No entities were found") + } + }, + ); + } + + fn update_gamemode(entity: Entity, ecs: &mut Ecs, gamemode: Gamemode) -> anyhow::Result<()> { + let mut new_mut = ecs.get_mut::(entity)?; + let mut old_mut = ecs.get_mut::(entity)?; + if *new_mut == gamemode { + bail!("Already this gamemode") + } + + *old_mut = PreviousGamemode(Some(*new_mut)); + *new_mut = gamemode; + + let (old, new) = (*old_mut, *new_mut); + drop(new_mut); + drop(old_mut); + + ecs.insert_entity_event(entity, GamemodeUpdateEvent { old, new })?; + + Ok(()) + } + + // /clear + dispatcher + .create_command("clear") + .executes(|mut context: CommandCtx| { + if context.ecs.get::(context.sender).is_ok() { + // Go through the player's inventory and set all the slots to no items. + // Also, keep track of how many items we delete. + let mut count = 0; + let sender = context.sender; + clear_items(&mut context, sender, None, None, &mut count)?; + // If count is zero, the player's inventory was empty and the command fails + // "No items were found on player {0}." + if count == 0 { + context.send_message( + Text::translate_with( + "clear.failed.single", + vec![get_entity_name(context.sender, &context.ecs)], + ) + .red(), + ); + bail!("No items were found"); + } + // If the count is not zero, we return the count of items we deleted. Command succeeds. + // "Removed {1} items from player {0}" + context.send_message(Text::translate_with( + "commands.clear.success.single", + vec![ + count.to_string(), + get_entity_name(context.sender, &context.ecs), + ], + )); + Ok(count) + } else { + context.send_message(Text::translate("permissions.requires.player").red()); + bail!("Requires a player") + } + }) + .argument("target", EntityArgument::PLAYERS, "minecraft:entity") + .executes(|mut context: CommandCtx, selector: &mut EntitySelector| { + if let Some(entities) = context.find_non_empty_entities_by_selector(selector, true) { + let mut count = 0; + for entity in &entities { + clear_items(&mut context, *entity, None, None, &mut count)?; + } + + send_clear_message(&context, count, entities) + } else { + bail!("No entities were found") + } + }) + .argument("item", ItemPredicateArgument, "minecraft:item_predicate") + .executes( + |mut context: CommandCtx, selector: &mut EntitySelector, item: &mut ItemPredicate| { + if let Some(entities) = context.find_non_empty_entities_by_selector(selector, true) + { + let mut count = 0; + for entity in &entities { + clear_items(&mut context, *entity, Some(item), None, &mut count)?; + } + + send_clear_message(&context, count, entities) + } else { + bail!("No entities were found") + } + }, + ) + .argument("maxCount", IntegerArgument::new(0..=i32::MAX), None) + .executes( + |mut context: CommandCtx, + selector: &mut EntitySelector, + item: &mut ItemPredicate, + max_count: &mut i32| { + if let Some(entities) = context.find_non_empty_entities_by_selector(selector, true) + { + let mut count = 0; + for entity in &entities { + clear_items( + &mut context, + *entity, + Some(item), + Some(*max_count), + &mut count, + )?; + } + + if *max_count == 0 { + if entities.len() == 1 { + context.send_message(Text::translate_with( + "commands.clear.test.single", + vec![ + count.to_string(), + get_entity_name(*entities.first().unwrap(), &context.ecs), + ], + )); + } else { + context.send_message(Text::translate_with( + "commands.clear.test.multiple", + vec![count.to_string(), entities.len().to_string()], + )); + } + Ok(count) + } else { + send_clear_message(&context, count, entities) + } + } else { + bail!("No entities were found") + } + }, + ); + + fn send_clear_message( + context: &CommandCtx, + count: i32, + entities: Vec, + ) -> CommandOutput { + match (count, entities.len()) { + (0, 1) => { + context.send_message( + Text::translate_with( + "clear.failed.single", + vec![get_entity_name(*entities.first().unwrap(), &context.ecs)], + ) + .red(), + ); + bail!("No items were found") + } + (0, entities) => { + context.send_message( + Text::translate_with("clear.failed.multiple", vec![entities.to_string()]).red(), + ); + bail!("No items were found") + } + (count, 1) => { + context.send_message(Text::translate_with( + "commands.clear.success.single", + vec![ + count.to_string(), + get_entity_name(*entities.first().unwrap(), &context.ecs), + ], + )); + Ok(count) + } + (count, entities) => { + context.send_message(Text::translate_with( + "commands.clear.success.multiple", + vec![count.to_string(), entities.to_string()], + )); + Ok(count) + } + } + } + + /// Go through a player's inventory and set all the slots that match "item" to empty, up to maxcount items removed. + /// Also, keep track of how many items we delete total in the variable count. + /// Will panic if entity does not have an inventory + fn clear_items( + context: &mut CommandCtx, + player: Entity, + item: Option<&ItemPredicate>, + max_count: Option, + count: &mut i32, + ) -> anyhow::Result<()> { + let inventory = context.ecs.get_mut::(player).unwrap(); + let mut changed_items: SmallVec<[usize; 2]> = SmallVec::new(); + // TODO don't clone items, they may have big NBT tags + for (index, slot) in inventory.inner().to_vec().into_iter().enumerate() { + if let InventorySlot::Filled(mut stack) = slot { + if let Some(predicate) = item.as_ref() { + if !item_matches(context, &stack, predicate) { + continue; + } + } + let max_count = max_count.unwrap_or(i32::MAX); + if max_count == 0 { + *count += stack.count() as i32; + } else if (stack.count() as i32) <= max_count - *count { + *count += stack.count() as i32; + inventory.set_item(index, InventorySlot::Empty)?; + changed_items.push(index); + } else { + stack.set_count(stack.count() - (max_count - *count) as u32)?; + inventory.set_item(index, InventorySlot::Filled(stack))?; + *count = max_count; + changed_items.push(index); + break; + } + } + } + drop(inventory); + if !changed_items.is_empty() { + context + .ecs + .insert_entity_event(player, InventoryUpdateEvent(changed_items.into_vec()))?; + } + Ok(()) + } + + fn item_matches(_game: &Game, item: &ItemStack, predicate: &ItemPredicate) -> bool { + #[allow(clippy::needless_bool)] + if !match &predicate.predicate_type { + ItemPredicateType::Tag(_s) => + /*game.tag_registry.check_item_tag( + item.item, + &NamespacedId::from_str(s.to_string().as_str()).unwrap(), + )*/ + { + unimplemented!() + } + ItemPredicateType::Item(s) => item.item().name() == s.value(), + } { + false + } else { + // TODO compare nbt tags + true + } + } + + // /ban + dispatcher + .create_command("ban") + .argument("targets", EntityArgument::PLAYERS, "minecraft:entity") + .executes(|mut context: CommandCtx, selector: &mut EntitySelector| { + if let Some(targets) = context.find_non_empty_entities_by_selector(selector, true) { + Ok(ban_players(&mut context, targets, BanReason::default()) as i32) + } else { + bail!("No entities were found") + } + }) + .argument("reason", MessageArgument, None) + .executes( + |mut context: CommandCtx, selector: &mut EntitySelector, reason: &mut Message| { + if let Some(targets) = context.find_non_empty_entities_by_selector(selector, true) { + let reason = BanReason::new(reason.to_string(|s| { + get_entity_names(context.sender, &context, &EntitySelector::Selector(s)) + })); + + Ok(ban_players(&mut context, targets, reason) as i32) + } else { + bail!("No entities were found") + } + }, + ); + + fn ban_players(context: &mut CommandCtx, players: Vec, reason: BanReason) -> usize { + let source = get_entity_name(context.sender, &context.ecs); + if players.is_empty() { + 0 + } else { + let mut count = 0; + for target in players { + // TODO ban offline players + let uuid = *context.ecs.get::(target).unwrap(); + let name = get_entity_name(target, &context.ecs); + if context.resources.get_mut::().unwrap().ban( + uuid, + name.clone(), + Some(source.clone()), + reason.clone(), + None, + ) { + count += 1; + context.send_message(Text::translate_with( + "commands.ban.success", + vec![name, reason.to_string()], + )); + context + .ecs + .insert_entity_event( + target, + DisconnectEvent::new(Text::translate_with( + "multiplayer.disconnect.banned.reason", + vec![reason.to_string()], + )), + ) + .unwrap(); + } else { + context.send_message(Text::translate("commands.ban.failed").red()); + } + } + count + } + } + + // /ban-ip + dispatcher + .create_command("ban-ip") + .argument("target", StringArgument::SINGLE_WORD, "player_names") + .executes(|mut context, target: &mut String| { + ban_ip(&mut context, target.to_owned(), BanReason::default()).map(|n| n as i32) + }) + .argument("reason", MessageArgument, None) + .executes( + |mut context: CommandCtx, target: &mut String, reason: &mut Message| { + let reason = BanReason::new(reason.to_string(|s| { + get_entity_names(context.sender, &context, &EntitySelector::Selector(s)) + })); + ban_ip(&mut context, target.to_owned(), reason).map(|n| n as i32) + }, + ); + + fn ban_ip(context: &mut CommandCtx, ip: String, reason: BanReason) -> anyhow::Result { + let sender = context.sender; + let source = get_entity_name(sender, &context.ecs); + let ip = if let Ok(ip) = ip.parse() { + ip + } else if let Some(target) = context + .ecs + .query::<(&Player, &Name)>() + .iter() + .find(|(_, (_, name))| ****name == ip) + .map(|(e, _)| e) + { + context.ecs.get::(target).unwrap().0 + } else { + context.send_message(Text::translate("commands.banip.invalid").red()); + bail!("Invalid IP") + }; + + if context.resources.get_mut::().unwrap().ban_ip( + ip, + Some(source), + reason.clone(), + None, + ) { + context.send_message(Text::translate_with( + "commands.banip.success", + vec![ip.to_string(), reason.to_string()], + )); + let targets = context + .ecs + .query::<(&Player, &RealIp)>() + .iter() + .filter(|(_, (_, &addr))| *addr == ip) + .map(|(entity, _)| entity) + .collect::>(); + for target in targets { + context + .ecs + .insert_entity_event( + target, + DisconnectEvent::new(Text::translate_with( + "multiplayer.disconnect.banned_ip.reason", + vec![reason.to_string()], + )), + ) + .unwrap(); + } + Ok(1) + } else { + context.send_message(Text::translate("commands.banip.failed").red()); + bail!("This IP is not banned") + } + } + + // /pardon + dispatcher + .create_command("pardon") + .argument("targets", EntityArgument::PLAYERS, "minecraft:banned_players") + .executes(|context: CommandCtx, selector: &mut EntitySelector| { + match selector { + EntitySelector::Selector(_) => { + if context.find_non_empty_entities_by_selector(selector, true).is_some() { + // If targets are online, then they're not banned + context.send_message(Text::translate("commands.pardon.failed")); + } + bail!("Tried to /pardon a selector of online players, but since they're online, they're not banned") + } + EntitySelector::Name(name) => { + let mut banlist = context.resources.get_mut::().unwrap(); + if banlist.pardon_name(name) { + context.send_message(Text::translate_with( + "commands.pardon.success", + vec![name.to_owned()], + )); + Ok(1) + } else { + context.send_message(Text::translate("commands.pardon.failed").red()); + bail!("This player is not banned") + } + } + EntitySelector::Uuid(uuid) => { + let mut banlist = context.resources.get_mut::().unwrap(); + if banlist.pardon_id(uuid) { + context.send_message(Text::translate_with( + "commands.pardon.success", + vec![uuid.to_string()], + )); + Ok(1) + } else { + context.send_message(Text::translate("commands.pardon.failed").red()); + bail!("This player is not banned") + } + } + } + }); + + // /pardon-ip + dispatcher + .create_command("pardon-ip") + .argument( + "targets", + StringArgument::SINGLE_WORD, + "minecraft:banned_ips", + ) + .executes(|context: CommandCtx, ip: &mut String| { + if let Ok(ip) = ip.parse() { + let mut banlist = context.resources.get_mut::().unwrap(); + if banlist.pardon_ip(&ip) { + context.send_message(Text::translate_with( + "commands.pardonip.success", + vec![ip.to_string()], + )); + Ok(1) + } else { + context.send_message(Text::translate("commands.pardonip.failed").red()); + bail!("This IP is not banned") + } + } else { + context.send_message(Text::translate("commands.pardonip.invalid").red()); + bail!("Invalid IP") + } + }); + + // /say + dispatcher + .create_command("say") + .argument("message", MessageArgument, None) + .executes(|context: CommandCtx, message: &mut Message| { + let command_output = Text::translate_with( + "chat.type.announcement", + vec![ + get_entity_name(context.sender, &context.ecs), + message.to_string(|s| { + get_entity_names(context.sender, &context, &EntitySelector::Selector(s)) + }), + ], + ); + context + .ecs + .query::<&mut ChatBox>() + .iter() + .for_each(|(_, chat_box)| chat_box.send_chat(command_output.clone())); + Ok(1) + }); + + // /execute + let root = 0; + let command = dispatcher.create_command("execute"); + let execute = command.current_node_id(); + command + .with(|command| { + command.subcommand("run").redirect(root); + }) + .with(|command| { + command + .subcommand("as") + .argument("executors", EntityArgument::ENTITIES, "minecraft:entity") + .redirect(execute) + .fork(|args, mut context, mut f| { + let arg = args.remove(0); + let selector = arg.downcast_ref::().unwrap(); + if let Some(entities) = + context.find_non_empty_entities_by_selector(selector, false) + { + let position = context.position; + let anchor = context.anchor.clone(); + if entities.len() == 1 { + f( + args, + CommandCtx::new(&mut *context, entities[0], position, anchor), + ) + } else { + for entity in entities { + let _ = f( + args, + CommandCtx::new( + &mut *context, + entity, + position, + anchor.clone(), + ), + ); + } + Ok(0) + } + } else { + bail!("No entities were found") + } + }); + }) + .with(|command| { + command + .subcommand("at") + .argument("positions", EntityArgument::ENTITIES, "minecraft:entity") + .redirect(execute) + .fork(|args, mut context, mut f| { + let arg = args.remove(0); + let selector = arg.downcast_ref::().unwrap(); + if let Some(entities) = + context.find_non_empty_entities_by_selector(selector, false) + { + let sender = context.sender; + let anchor = context.anchor.clone(); + if entities.len() == 1 { + let pos = context + .ecs + .get::(entities[0]) + .ok() + .map(|pos| *pos); + f(args, CommandCtx::new(&mut *context, sender, pos, anchor)) + } else { + for entity in entities { + let pos = context.ecs.get::(entity).ok().map(|pos| *pos); + let _ = f( + args, + CommandCtx::new(&mut *context, sender, pos, anchor.clone()), + ); + } + Ok(0) + } + } else { + bail!("No entities were found") + } + }); + }) + .with(|command| { + command + .subcommand("align") + .argument("positions", SwizzleArgument, None) + .redirect(execute) + .fork(|args, mut context, mut f| { + let arg = args.remove(0); + let swizzle = arg.downcast_ref::().unwrap(); + if let Some(pos) = context.position.as_mut() { + if swizzle.x { + pos.x = pos.x.floor(); + } + if swizzle.y { + pos.y = pos.y.floor(); + } + if swizzle.z { + pos.z = pos.z.floor(); + } + } + f(args, context) + }); + }) + .with(|command| { + command + .subcommand("anchored") + .argument("anchor", EntityAnchorArgument, "minecraft:entity_anchor") + .redirect(execute) + .fork(|args, mut context, mut f| { + let arg = args.remove(0); + context.anchor = arg.downcast_ref::().unwrap().clone(); + f(args, context) + }); + }); + + // /banlist + dispatcher + .create_command("banlist") + .executes(|ctx: CommandCtx| { + let banlist = ctx.resources.get::().unwrap(); + let bans = banlist.players().len() + banlist.ips().len(); + + if bans == 0 { + ctx.send_message(Text::translate("commands.banlist.none")); + Ok(0) + } else { + ctx.send_message(Text::translate_with( + "commands.banlist.list", + vec![bans.to_string()], + )); + for entry in banlist.players() { + ctx.send_message(Text::translate_with( + "commands.banlist.entry", + vec![ + entry.value.1.clone(), + entry + .source + .clone() + .unwrap_or_else(|| "Console".to_string()), + entry.reason.to_string(), + ], + )); + } + for entry in banlist.ips() { + ctx.send_message(Text::translate_with( + "commands.banlist.entry", + vec![ + entry.value.to_string(), + entry + .source + .clone() + .unwrap_or_else(|| "Console".to_string()), + entry.reason.to_string(), + ], + )); + } + Ok(bans as i32) + } + }); + + // /time + dispatcher + .create_command("time") + .with(|command| { + command + .subcommand("query") + .with(|command| { + command.subcommand("daytime").executes(|ctx: CommandCtx| { + Ok(time_query(&ctx, ctx.world.time.time_of_day())) + }); + }) + .with(|command| { + command + .subcommand("day") + .executes(|ctx: CommandCtx| Ok(time_query(&ctx, ctx.world.time.days()))); + }) + .with(|command| { + command.subcommand("gametime").executes(|ctx: CommandCtx| { + Ok(time_query(&ctx, ctx.world.time.world_age())) + }); + }); + }) + .with(|command| { + command + .subcommand("add") + .argument("time", TimeArgument, "minecraft:time") + .executes(|mut ctx: CommandCtx, time: &mut (TimeUnit, f32)| { + let time = ctx.world.time.time() + time_to_ticks(time); + Ok(set_time(&mut ctx, time)) + }); + }) + .with(|command| { + command + .subcommand("set") + .with(|command| { + command + .argument("time", TimeArgument, "minecraft:time") + .executes(|mut ctx: CommandCtx, time: &mut (TimeUnit, f32)| { + let time = time_to_ticks(time); + Ok(set_time(&mut ctx, time)) + }); + }) + .with(|command| { + command + .subcommand("day") + .executes(|mut ctx| Ok(set_time(&mut ctx, 1000))); + }) + .with(|command| { + command + .subcommand("noon") + .executes(|mut ctx| Ok(set_time(&mut ctx, 6000))); + }) + .with(|command| { + command + .subcommand("night") + .executes(|mut ctx| Ok(set_time(&mut ctx, 13000))); + }) + .with(|command| { + command + .subcommand("midnight") + .executes(|mut ctx| Ok(set_time(&mut ctx, 18000))); + }); + }); + + fn time_to_ticks(time: &(TimeUnit, f32)) -> u64 { + (time.1 + * match time.0 { + TimeUnit::Days => 24000.0, + TimeUnit::Seconds => 20.0, + TimeUnit::Ticks => 1.0, + }) as u64 + } + + fn time_query(context: &CommandCtx, time: u64) -> i32 { + context.send_message(Text::translate_with( + "commands.time.query", + vec![time.to_string()], + )); + time as i32 + } + + fn set_time(context: &mut CommandCtx, time: u64) -> i32 { + context.set_time(time); + let time_of_day = context.world.time.time_of_day(); + context.send_message(Text::translate_with( + "commands.time.set", + vec![time_of_day.to_string()], + )); + time_of_day as i32 + } + + // /defaultgamemode + dispatcher + .create_command("defaultgamemode") + .with(|command| default_gamemode(command, "survival", Gamemode::Survival)) + .with(|command| default_gamemode(command, "creative", Gamemode::Creative)) + .with(|command| default_gamemode(command, "adventure", Gamemode::Adventure)) + .with(|command| default_gamemode(command, "spectator", Gamemode::Spectator)); + fn default_gamemode(command: CreateCommand, s: &str, gamemode: Gamemode) { + command.subcommand(s).executes(move |context: CommandCtx| { + **context.resources.get_mut::().unwrap() = gamemode; + context.send_message(Text::translate_with( + "commands.defaultgamemode.success", + vec![Text::translate(format!( + "gameMode.{}", + gamemode.to_string() + ))], + )); + Ok(0) + }); + } + + // /setblock + dispatcher + .create_command("setblock") + .argument("pos", BlockPosArgument, "minecraft:block_pos") + .argument("block", BlockStateArgument, "minecraft:block_state") + .executes(|context, pos: &mut BlockPos, block: &mut BlockState| { + set_block(context, pos, block, SetBlockMode::default()) + }) + .with(|command| { + command.subcommand("destroy").executes( + |context, pos: &mut BlockPos, block: &mut BlockState| { + set_block(context, pos, block, SetBlockMode::Destroy) + }, + ); + }) + .with(|command| { + command.subcommand("keep").executes( + |context, pos: &mut BlockPos, block: &mut BlockState| { + set_block(context, pos, block, SetBlockMode::Keep) + }, + ); + }) + .with(|command| { + command.subcommand("replace").executes( + |context, pos: &mut BlockPos, block: &mut BlockState| { + set_block(context, pos, block, SetBlockMode::Replace) + }, + ); + }); + + enum SetBlockMode { + Destroy, + Keep, + Replace, + } + + impl Default for SetBlockMode { + fn default() -> Self { + SetBlockMode::Replace + } + } + + fn set_block( + mut context: CommandCtx, + pos: &mut BlockPos, + block: &mut BlockState, + mode: SetBlockMode, + ) -> anyhow::Result { + match context.block_pos(pos) { + Ok(pos) => { + if context.world.is_chunk_loaded(pos.chunk()) { + if matches!(mode, SetBlockMode::Keep) + && !context.world.block_at(pos).unwrap().is_air() + { + context.send_message(Text::translate("commands.setblock.failed").red()); + bail!("The old block is not empty and the setblock mode is `keep`") + } + if let Some(block_id) = BlockId::from_identifier(&block.block.to_string()) { + let mut properties = block_id + .to_properties_map() + .into_iter() + .map(|(key, value)| (key.into(), value.into())) + .collect::>(); + properties.extend( + block + .properties + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())), + ); + if let Some(new) = BlockId::from_identifier_and_properties( + &block.block.to_string(), + &properties, + ) { + if context.set_block_at(pos, new, matches!(mode, SetBlockMode::Destroy)) + { + // TODO Tick this block + + context.send_message(Text::translate_with( + "commands.setblock.success", + vec![ + pos.x().to_string(), + pos.y().to_string(), + pos.z().to_string(), + ], + )); + Ok(1) + } else { + context.send_message( + Text::translate("commands.setblock.failed").red(), + ); + bail!("This block is already set") + } + } else { + // TODO report errors from BlockId::from_identifier_and_properties + context.send_message(Text::translate("command.failed").red()); + bail!( + "Too many properties, some of them doesn't exist in `{}`", + block.block + ) + } + } else { + // TODO report errors from BlockId::from_identifier_and_properties + context.send_message(Text::translate("command.failed").red()); + bail!("Invalid block identifier `{}`", block.block) + } + } else { + context.send_message(Text::translate("argument.pos.unloaded").red()); + bail!("Unloaded position") + } + } + Err(BlockPosError::NotAnEntity) => { + context.send_message(Text::translate("commands.setblock.failed").red()); + bail!("Local and relative coordinates with non-entity sender") + } + Err(BlockPosError::InvalidPosition) => { + context.send_message(Text::translate("argument.pos.unloaded").red()); + bail!("Invalid position") + } + } + } + + // /seed + dispatcher + .create_command("seed") + .executes(|context: CommandCtx| { + context.send_message( + Text::from("Seed: [") + + Text::from(context.world.seed().to_string()) + .green() + .on_click_copy_to_clipboard(context.world.seed().to_string()) + .on_hover_show_text(Text::translate("chat.copy.click")) + + Text::from("]"), + ); + Ok(context.world.seed() as i32) + }); + + dispatcher.register_tab_completion("minecraft:banned_players", |text, ctx| { + ( + 0, + text.len(), + ctx.resources + .get::() + .unwrap() + .players() + .into_iter() + .map(|entry| entry.value.1.clone()) + .filter(|name| name.starts_with(text) && name != text) + .map(|name| (name, None)) + .collect(), + ) + }); + + dispatcher.register_tab_completion("minecraft:banned_ips", |text, ctx| { + ( + 0, + text.len(), + ctx.resources + .get::() + .unwrap() + .ips() + .into_iter() + .map(|entry| entry.value.to_string()) + .filter(|ip| ip.starts_with(text) && ip != text) + .map(|ip| (ip, None)) + .collect(), + ) + }); + + dispatcher.register_tab_completion( + "minecraft:entity_anchor", + fixed_completion(vec!["feet", "eyes"]), + ); + + dispatcher.register_tab_completion("minecraft:entity", |text, ctx| { + let players = ctx + .ecs + .query::<(&Player, &Name)>() + .iter() + .map(|(_, (_, name))| name.to_string()) + .collect::>(); + if text.is_empty() { + let mut results = players + .into_iter() + .map(|name| (name, None)) + .collect::>(); + results.extend([ + ( + "@a".to_string(), + Some(Text::translate("argument.entity.selector.allPlayers")), + ), + ( + "@e".to_string(), + Some(Text::translate("argument.entity.selector.allEntities")), + ), + ( + "@p".to_string(), + Some(Text::translate("argument.entity.selector.nearestPlayer")), + ), + ( + "@r".to_string(), + Some(Text::translate("argument.entity.selector.randomPlayer")), + ), + ( + "@s".to_string(), + Some(Text::translate("argument.entity.selector.self")), + ), + ]); + (0, 0, results) + } else if text == "@" { + ( + 0, + 1, + vec![ + ( + "@a".to_string(), + Some(Text::translate("argument.entity.selector.allPlayers")), + ), + ( + "@e".to_string(), + Some(Text::translate("argument.entity.selector.allEntities")), + ), + ( + "@p".to_string(), + Some(Text::translate("argument.entity.selector.nearestPlayer")), + ), + ( + "@r".to_string(), + Some(Text::translate("argument.entity.selector.randomPlayer")), + ), + ( + "@s".to_string(), + Some(Text::translate("argument.entity.selector.self")), + ), + ], + ) + } else if ["@a", "@e", "@p", "@r", "@s"].contains(&text) { + (2, 0, vec![("[".to_string(), None)]) + } else if text.starts_with("@a[") + || text.starts_with("@e[") + || text.starts_with("@p[") + || text.starts_with("@r[") + || text.starts_with("@s[") + { + // TODO rewrite selector parser + (0, 0, Vec::new()) + } else { + ( + 0, + text.len(), + players + .into_iter() + .filter(|name| name.starts_with(text)) + .map(|name| (name, None)) + .collect::>(), + ) + } + }); + + dispatcher.register_tab_completion("minecraft:item_predicate", |text, _ctx| { + #[allow(clippy::if_same_then_else)] + let items: Vec = if text.starts_with('#') { + // TODO tags + vec![] + } else { + // TODO Item::values() + vec![] + }; + if let Some(item) = items.into_iter().find(|item| item.starts_with(text)) { + if item == text { + (item.len(), 0, vec![("{".to_string(), None)]) + } else { + (0, text.len(), vec![(item, None)]) + } + } else { + (0, 0, Vec::new()) + } + }); + + dispatcher.register_tab_completion("minecraft:time", |text, _ctx| { + if text.parse::().is_ok() { + ( + text.len(), + 0, + vec![ + ("d".to_string(), None), + ("s".to_string(), None), + ("t".to_string(), None), + ], + ) + } else { + (0, 0, Vec::new()) + } + }) +} + +// #[command(usage = "tp|teleport ")] +// pub fn tp_1(context: &mut CommandCtx, destination: EntitySelector) -> anyhow::Result> { +// let entities = find_selected_entities(ctx, &destination.requirements)?; +// if let Some(first) = entities.first() { +// if let Ok(pos) = context.ecs.get::(*first).map(|r| *r) { +// teleport_entity_to_pos(&context.ecs, context.sender, pos); +// } +// +// Ok(Some(format!( +// "Teleported {0} to {1}", +// *context.ecs.get::(context.sender).unwrap(), +// *context.ecs.get::(*first).unwrap() +// ))) +// } else { +// Err(TpError::NoMatchingEntities.into()) +// } +// } +// +// #[command(usage = "tp|teleport ")] +// pub fn tp_2(context: &mut CommandCtx, location: Coordinates) -> anyhow::Result> { +// teleport_entity(&context.ecs, context.sender, location); +// +// let position = context.ecs.get::(context.sender).unwrap(); +// Ok(Some(format!( +// "Teleported {0} to {1}, {2}, {3}", +// *context.ecs.get::(context.sender).unwrap(), +// position.x, +// position.y, +// position.z +// ))) +// } +// +// #[command(usage = "tp|teleport ")] +// pub fn tp_3( +// context: &mut CommandCtx, +// targets: EntitySelector, +// location: Coordinates, +// ) -> anyhow::Result> { +// let entities = find_selected_entities(ctx, &targets.requirements)?; +// if entities.is_empty() { +// Err(TpError::NoMatchingEntities.into()) +// } else { +// for entity in &entities { +// teleport_entity(&context.ecs, *entity, location); +// } +// +// let position = context.ecs.get::(*entities.first().unwrap()).unwrap(); +// Ok(Some(format!( +// "Teleported {0} to {1}, {2}, {3}", +// targets.entities_to_string(ctx, &entities.into_vec(), false), +// position.x, +// position.y, +// position.z +// ))) +// } +// } +// +// #[command(usage = "tp|teleport ")] +// pub fn tp_4( +// context: &mut CommandCtx, +// targets: EntitySelector, +// destination: EntitySelector, +// ) -> anyhow::Result> { +// let entities = find_selected_entities(ctx, &targets.requirements)?; +// if entities.len() > 1 { +// Err(TpError::TooManyEntities.into()) +// } else if let Some(Ok(location)) = entities +// .first() +// .map(|e| context.ecs.get::(*e).map(|r| *r)) +// { +// if entities.is_empty() { +// Err(TpError::NoMatchingEntities.into()) +// } else { +// for entity in &entities { +// teleport_entity_to_pos(&context.ecs, *entity, location); +// } +// let entities = entities.into_vec(); +// Ok(Some(format!( +// "Teleported {0} to {1}", +// targets.entities_to_string(ctx, &entities, false), +// destination.entities_to_string(ctx, &entities, false) +// ))) +// } +// } else { +// Err(TpError::NoMatchingEntities.into()) +// } +// } +// +// fn teleport_entity(ecs: &mut Ecs, entity: Entity, location: Coordinates) { +// let new_pos = ecs +// .get::(entity) +// .map(|r| *r) +// .map(|relative_to| location.into_position(relative_to)); +// +// if let Ok(new_pos) = new_pos { +// teleport_entity_to_pos(ecs, entity, new_pos); +// } +// } +// +// fn teleport_entity_to_pos(ecs: &mut Ecs, entity: Entity, pos: Position) { +// if let Ok(mut old_pos) = ecs.get_mut::(entity) { +// *old_pos = pos; +// } +// //let _ = game.ecs.entity(entity).add(Teleported); +// } +// +// +// if let Some(event) = event { +// context.ecs.insert_entity_event(entity, event).unwrap(); +// } +// } +// +// #[command(usage = "tell|msg|w ")] +// pub fn whisper( +// context: &mut CommandCtx, +// target: EntitySelector, +// message: TextArgument, +// ) -> anyhow::Result> { +// let entities = find_selected_entities(ctx, &target.requirements)?; +// let sender_name = if let Ok(sender_name) = context.ecs.get::(context.sender) { +// (**sender_name).to_owned() +// } else { +// // Use a default value if the executor has no Name component +// String::from("Server") +// }; +// +// // The message that is returned to the whisperer +// // You whisper to [player] (and [player]): [message] +// let mut response_message = String::from("You whisper to"); +// +// // Tracks if there needs to be "and" before the next player added to the response message +// let mut needs_and = false; +// +// for entity in entities { +// if let Ok(mut chat) = context.ecs.get_mut::(context.sender) { +// chat.send_system( +// Text::from(format!( +// "{} whispers to you: {}", +// sender_name, +// message.0.clone() +// )) +// .gray() +// .italic(), +// ); +// } else { +// // If the entity doesn't have a message receiver it is not a player and there is no need to continue +// continue; +// }; +// +// if let Ok(player_name) = context.ecs.get::(entity) { +// if needs_and { +// response_message += format!(" and {}", *player_name).as_str(); +// } else { +// needs_and = true; +// +// response_message += format!(" {}", *player_name).as_str(); +// } +// } +// } +// +// // Send the whisperer a confirmation message +// if let Ok(mut chat) = context.ecs.get_mut::(context.sender) { +// response_message += format!(": {}", message.0).as_str(); +// let return_text = Text::from(response_message).gray().italic(); +// +// chat.send_system(return_text); +// } +// +// Ok(None) +// } +// +// #[derive(Debug, Error)] +// pub enum KickError { +// #[error( +// "Only players may be affected by this command, but the provided selector includes entities" +// )] +// NoEntities, +// } +// +// #[command(usage = "kick ")] +// pub fn kick_1(context: &mut CommandCtx, targets: EntitySelector) -> anyhow::Result> { +// kick_players( +// ctx, +// &targets, +// TextValue::translate("multiplayer.disconnect.kicked").into(), +// ) +// } +// +// #[command(usage = "kick ")] +// pub fn kick_2( +// context: &mut CommandCtx, +// targets: EntitySelector, +// reason: TextArgument, +// ) -> anyhow::Result> { +// kick_players(ctx, &targets, reason.0.into()) +// } +// +// fn kick_players( +// context: &mut CommandCtx, +// targets: &mut EntitySelector, +// reason: Text, +// ) -> anyhow::Result> { +// let entities = find_selected_entities(ctx, &targets.requirements)?; +// for entity in &entities { +// if context.ecs.get::(*entity).is_err() { +// return Err(KickError::NoEntities.into()); +// } +// } +// +// for entity in &entities { +// let name = (**context.ecs.get::(*entity).unwrap()).to_owned(); +// let _client_id = context.ecs.get::(*entity).unwrap(); +// +// // Send confirmation message +// // TODO Server ops should also see the message +// if let Ok(mut chat) = context.ecs.get_mut::(context.sender) { +// let kick_confirm = Text::translate_with( +// "commands.kick.success", +// vec![Text::from(name), reason.clone()], +// ); +// chat.send_system(kick_confirm); +// } +// } +// Ok(None) +// } +// +// #[command(usage = "stop")] +// pub fn stop(context: &mut CommandCtx) -> anyhow::Result> { +// // Confirmation message +// // TODO Server ops should also see the message +// if let Ok(mut chat) = context.ecs.get_mut::(context.sender) { +// let text = Text::from(TextValue::translate("commands.stop.stopping")); +// chat.send_system(text); +// } +// +// //context.game +// // .resources +// // .get::() +// // .tx +// // .try_send(())?; +// +// Ok(None) +// } diff --git a/feather/commands/src/lib.rs b/feather/commands/src/lib.rs new file mode 100644 index 000000000..85228c302 --- /dev/null +++ b/feather/commands/src/lib.rs @@ -0,0 +1,247 @@ +//! Implements the Feather command dispatching framework +//! and vanilla commands not defined by plugins. + +use std::convert::TryFrom; +use std::ops::{Deref, DerefMut}; + +use commands::arguments::{BlockPos, EntityAnchor, EntitySelector}; +pub use commands::dispatcher::CommandDispatcher; +use commands::dispatcher::{CommandOutput, TabCompletion}; +use log::{debug, info}; + +use common::events::BlockChangeEvent; +use common::Game; +use ecs::Entity; +use feather_base::ValidBlockPosition; +use feather_blocks::BlockId; +use libcraft_core::{BlockPosition, Position, Vec3d}; +use libcraft_text::{Text, TextComponentBuilder}; +use quill_common::components::{ChatBox, Name, Sneaking}; + +mod impls; +pub mod utils; + +/// Context passed into a command. This value can be used +/// for access to game and entity data, such as components. +pub struct CommandCtx { + /// The entity which triggered the command. + /// + /// _Not necessarily a player_. If the command was executed + /// from the server console, then this will be the "server entity" + /// associated with the console. You may check if an entity is a player + /// by checking if it has the `Player` component. Similarly, you + /// may check if an entity is the server console through the `Console` component. + /// + /// Note that players and the console are not the only possible command senders, + /// and command implementations should account for this. + pub sender: Entity, + /// The position at which the command is executed. + /// This is not necessarily sender's location: it can be modified by /execute at + pub position: Option, + pub anchor: EntityAnchor, + /// The game state. We don't want CommandDispatcher to have a lifetime, so + /// raw pointers are here to elide the lifetime. + /// Since CommandCtx is not Send, it should be sound + game: *mut Game, +} + +impl Deref for CommandCtx { + type Target = Game; + + fn deref(&self) -> &Self::Target { + unsafe { &*self.game } + } +} + +impl DerefMut for CommandCtx { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.game } + } +} + +impl CommandCtx { + pub fn new( + game: &mut Game, + sender: Entity, + position: Option, + anchor: EntityAnchor, + ) -> CommandCtx { + CommandCtx { + game: game as *mut Game, + sender, + position, + anchor, + } + } + pub fn send_message(&self, message: impl Into) { + self.ecs + .get_mut::(self.sender) + .unwrap() + .send_system(message) + } + /// Find entities by selector and report an error if no entities/players were found + pub fn find_non_empty_entities_by_selector( + &self, + selector: &EntitySelector, + players_only: bool, + ) -> Option> { + let entities = self.find_entities_by_selector(selector); + if entities.is_empty() { + self.send_message(match (selector, players_only) { + (EntitySelector::Name(_), true) => Text::translate("argument.player.unknown").red(), + (_, true) => Text::translate("argument.entity.notfound.player").red(), + (_, false) => Text::translate("argument.entity.notfound.entity").red(), + }); + None + } else { + Some(entities) + } + } + pub fn find_entities_by_selector(&self, selector: &EntitySelector) -> Vec { + utils::find_entities_by_selector(self.sender, self, selector) + } + pub fn block_pos(&self, pos: &BlockPos) -> Result { + const EYE_LEVEL: f64 = 1.27; + const EYE_LEVEL_SNEAKING: f64 = 1.62; + + if self.position.is_none() + && matches!( + pos, + BlockPos::Local(..) + | BlockPos::Relative { x: (true, _), .. } + | BlockPos::Relative { y: (true, _), .. } + | BlockPos::Relative { z: (true, _), .. } + ) + { + Err(BlockPosError::NotAnEntity) + } else { + let pos = match pos { + BlockPos::Relative { x, y, z } => BlockPosition { + x: if x.0 { + self.position.unwrap().x.floor() as i32 + x.1 + } else { + x.1 + }, + y: if y.0 { + self.position.unwrap().y.floor() as i32 + y.1 + } else { + y.1 + }, + z: if z.0 { + self.position.unwrap().z.floor() as i32 + z.1 + } else { + z.1 + }, + }, + BlockPos::Local(_, _, _) => { + let _origin = match self.anchor { + EntityAnchor::Feet => self.position.unwrap(), + EntityAnchor::Eyes => { + if ***self.ecs.get::<&Sneaking>(self.sender).unwrap() { + self.position.unwrap() + Vec3d::new(0.0, EYE_LEVEL_SNEAKING, 0.0) + } else { + self.position.unwrap() + Vec3d::new(0.0, EYE_LEVEL, 0.0) + } + } + }; + unimplemented!() + } + }; + + ValidBlockPosition::try_from(pos).map_err(|_| BlockPosError::InvalidPosition) + } + } + pub fn set_block_at(&mut self, pos: ValidBlockPosition, block: BlockId, destroy: bool) -> bool { + if !destroy && self.world.block_at(pos) == Some(block) { + false + } else { + if destroy { + // TODO drop old block, play its destroy sound + } + self.world.set_block_at(pos, block); + self.ecs.insert_event(BlockChangeEvent::single(pos)); + true + } + } +} + +#[derive(Debug)] +pub enum BlockPosError { + NotAnEntity, + InvalidPosition, +} + +pub fn register_vanilla_commands(dispatcher: &mut CommandDispatcher) { + impls::register_all(dispatcher) +} + +pub fn dispatch_command( + dispatcher: &mut CommandDispatcher, + game: &mut Game, + sender: Entity, + command: &str, + log: bool, +) -> Option { + let ctx = CommandCtx::new( + game, + sender, + game.ecs.get::(sender).ok().map(|pos| *pos), + EntityAnchor::default(), + ); + + if dispatcher.find_command(command).is_none() { + if let Ok(mut chat) = ctx.ecs.get_mut::(sender) { + chat.send_system( + Text::translate("command.unknown.command") + .push_extra( + Text::Array(vec![ + Text::of("\n/").gray(), + Text::of(command.to_string()).underlined(), + Text::translate("command.context.here").italic(), + ]) + .on_click_suggest_command(format!("/{}", command)), + ) + .red(), + ); + } + debug!("Unknown command: /{}", command); + None + } else { + if log { + info!( + "{} issued server command: /{}", + ctx.ecs + .get::(sender) + .map(|name| (***name).to_string()) + .unwrap_or_else(|_| "Console".to_owned()), + command + ); + } + let result = dispatcher.execute_command(command, ctx); + match &result { + Some(Ok(n)) => debug!("Command result: {:?}", n), + Some(Err(e)) => debug!("Command error: {}", e), + None => debug!("Command not found"), + } + result + } +} + +pub fn tab_complete( + dispatcher: &CommandDispatcher, + game: &mut Game, + sender: Entity, + prompt: &str, +) -> TabCompletion { + dispatcher + .tab_complete( + prompt, + CommandCtx::new( + game, + sender, + game.ecs.get::(sender).ok().map(|pos| *pos), + EntityAnchor::default(), + ), + ) + .unwrap_or_default() +} diff --git a/feather/commands/src/utils.rs b/feather/commands/src/utils.rs new file mode 100644 index 000000000..7c260b500 --- /dev/null +++ b/feather/commands/src/utils.rs @@ -0,0 +1,245 @@ +use std::collections::HashMap; + +use commands::arguments::{EntitySelector, EntitySelectorPredicate, EntitySelectorSorting}; +use commands::dispatcher::TabCompletion; +use uuid::Uuid; + +use common::Game; +use ecs::{Ecs, Entity, EntityRef}; +use libcraft_core::{EntityKind, Position}; +use libcraft_text::Text; +use quill_common::components::{CustomName, Gamemode, Name}; + +pub fn get_entity_names(sender: Entity, game: &Game, selector: &EntitySelector) -> Vec { + find_entities_by_selector(sender, game, selector) + .iter() + .map(|&e| get_entity_name(e, &game.ecs)) + .collect() +} + +pub fn get_entity_name(e: Entity, ecs: &Ecs) -> String { + ecs.get::(e) + .map(|name| (***name).to_string()) + .or_else(|_| ecs.get::(e).map(|name| (***name).to_string())) + .or_else(|_| ecs.get::(e).map(|k| k.display_name().into())) + .unwrap() +} + +pub fn find_entities_by_selector( + sender: Entity, + game: &Game, + selector: &EntitySelector, +) -> Vec { + match selector { + EntitySelector::Selector(selector) => { + let sender_position = game + .ecs + .get::(sender) + .map(|pos| *pos) + .unwrap_or_default(); + let mut sort = EntitySelectorSorting::Arbitrary; + let mut entities = if selector.contains(&EntitySelectorPredicate::Sender) { + vec![sender] + } else { + game.ecs + .query::<&EntityKind>() + .iter() + .map(|(e, _)| e) + .collect() + } + .into_iter() + .map(|entity_id| (entity_id, game.ecs.entity(entity_id).unwrap())) + .filter(|(_, entity)| { + let pos = entity.get::().unwrap(); + + let mut origin = sender_position; + let mut dpos = [None; 3]; + let mut distance = None; + + for filter in selector { + if !match filter { + EntitySelectorPredicate::Type(t) => entity + .get::() + .map(|k| k.name() == t.name()) + .unwrap_or(false), + EntitySelectorPredicate::Advancements(_) => todo!(), + EntitySelectorPredicate::Distance(d) => { + distance = Some(d.clone()); + true + } + EntitySelectorPredicate::Dx(x) => { + dpos[0] = Some(*x); + true + } + EntitySelectorPredicate::Dy(y) => { + dpos[1] = Some(*y); + true + } + EntitySelectorPredicate::Dz(z) => { + dpos[2] = Some(*z); + true + } + EntitySelectorPredicate::Gamemode(mode) => entity + .get::() + .map(|m| mode.0 == (m.to_string() == mode.1.to_string())) + .unwrap_or(false), + EntitySelectorPredicate::Level(_) => todo!(), + EntitySelectorPredicate::Limit(_) => true, + EntitySelectorPredicate::Name(name) => { + macro_rules! name { + () => { + |name| (***name).to_string() + }; + } + entity + .get::() + .map(name!()) + .or_else(|_| entity.get::().map(name!())) + .map(|n| name.0 == (n == name.1)) + .unwrap_or(false) + } + EntitySelectorPredicate::Predicate(_) => todo!(), + EntitySelectorPredicate::Scores(_) => todo!(), + EntitySelectorPredicate::Sort(s) => { + sort = s.clone(); + true + } + EntitySelectorPredicate::Tag(_) => todo!(), + EntitySelectorPredicate::Team(_) => todo!(), + EntitySelectorPredicate::X(x) => { + origin.x = *x; + true + } + EntitySelectorPredicate::Y(y) => { + origin.y = *y; + true + } + EntitySelectorPredicate::Z(z) => { + origin.z = *z; + true + } + EntitySelectorPredicate::XRotation(x_rot) => x_rot.contains(&pos.pitch), + EntitySelectorPredicate::YRotation(y_rot) => y_rot.contains(&pos.yaw), + EntitySelectorPredicate::Sender => true, // already checked for this + } { + return false; + } + } + // TODO use Aabb when it's a component + if let Some(dx) = dpos[0] { + let range = if dx > 0.0 { + origin.x..origin.x + dx + } else { + origin.x + dx..origin.x + }; + if !range.contains(&pos.x) { + return false; + } + } + if let Some(dy) = dpos[1] { + let range = if dy > 0.0 { + origin.y..origin.y + dy + } else { + origin.y + dy..origin.y + }; + if !range.contains(&pos.y) { + return false; + } + } + if let Some(dz) = dpos[2] { + let range = if dz > 0.0 { + origin.z..origin.z + dz + } else { + origin.z + dz..origin.z + }; + if !range.contains(&pos.z) { + return false; + } + } + if dpos.iter().all(Option::is_none) + && distance.is_some() + && !distance.unwrap().contains(&pos.distance_to(origin)) + { + return false; + } + true + }) + .collect::>(); + entities.sort_by(|(entity_id, entity), (entity_id2, entity2)| { + let by = |entity_id: &Entity, entity: &EntityRef| match sort { + EntitySelectorSorting::Nearest => entity + .get::() + .unwrap() + .distance_to(sender_position), + EntitySelectorSorting::Furthest => -entity + .get::() + .unwrap() + .distance_to(sender_position), + EntitySelectorSorting::Random => rand::random(), + EntitySelectorSorting::Arbitrary => entity_id.id() as f64, + }; + by(entity_id, entity) + .partial_cmp(&by(entity_id2, entity2)) + .unwrap() + }); + entities + .into_iter() + .map(|(entity_id, _)| entity_id) + .collect() + } + EntitySelector::Name(name) => game + .ecs + .query::<&Name>() + .iter() + .filter(|(_, n)| &***n == name) + .map(|(e, _)| e) + .collect(), + EntitySelector::Uuid(uuid) => game + .ecs + .query::<&Uuid>() + .iter() + .filter(|(_, id)| *id == uuid) + .map(|(e, _)| e) + .collect(), + } +} + +pub fn fixed_completion>( + completions: Vec, +) -> impl Fn(&str, &mut T) -> TabCompletion + 'static { + let completions = completions + .into_iter() + .map(|c| c.as_ref().to_string()) + .collect::>(); + move |text, _ctx| { + ( + 0, + text.len(), + completions + .iter() + .filter(|c| c.starts_with(text)) + .map(|c| (c.to_owned(), None)) + .collect(), + ) + } +} + +pub fn fixed_completion_with_tooltip>( + completions: HashMap>, +) -> impl Fn(&str, &mut T) -> TabCompletion + 'static { + let completions = completions + .into_iter() + .map(|(c, t)| (c.as_ref().to_string(), t)) + .collect::>(); + move |text, _ctx| { + ( + 0, + text.len(), + completions + .iter() + .filter(|(c, _)| c.starts_with(text)) + .map(|(c, t)| (c.to_owned(), t.as_ref().map(|t| t.clone().into()))) + .collect(), + ) + } +} diff --git a/feather/common/Cargo.toml b/feather/common/Cargo.toml index a371549d5..5969d2a55 100644 --- a/feather/common/Cargo.toml +++ b/feather/common/Cargo.toml @@ -23,4 +23,11 @@ libcraft-inventory = { path = "../../libcraft/inventory" } libcraft-items = { path = "../../libcraft/items" } rayon = "1.5" worldgen = { path = "../worldgen", package = "feather-worldgen" } -rand = "0.8" \ No newline at end of file +datapacks = { path = "../datapacks", package = "feather-datapacks" } +rand = "0.8" +chrono = "0.4" +serde = { version = "1", features = [ "derive" ] } +serde_json = "1.0.68" + +[dev-dependencies] +crossbeam-utils = "0.8.5" \ No newline at end of file diff --git a/feather/common/src/banlist.rs b/feather/common/src/banlist.rs new file mode 100644 index 000000000..4afa0cb20 --- /dev/null +++ b/feather/common/src/banlist.rs @@ -0,0 +1,246 @@ +use std::fmt::{Debug, Formatter}; +use std::io::Cursor; +use std::net::IpAddr; +use std::ops::{Add, Deref}; +use std::path::Path; + +use crate::Game; +use chrono::{DateTime, Duration, Local}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug)] +pub struct BanList { + banned_players: Vec>, + banned_ips: Vec>, +} + +impl BanList { + /// Ban the player. Returns false if the player is already banned + pub fn ban( + &mut self, + uuid: Uuid, + name: String, + by: Option, + reason: BanReason, + duration: impl Into>, + ) -> bool { + if self.banned_players.iter().any(|e| e.value.0 == uuid) { + false + } else { + let time = Local::now(); + self.banned_players.push(BanEntry { + value: (uuid, name), + banned: time, + source: by, + expires: duration.into().map(|duration| time.add(duration)), + reason, + }); + true + } + } + /// Ban the IP address. Returns false if the ip is already banned + pub fn ban_ip( + &mut self, + ip: IpAddr, + by: Option, + reason: BanReason, + duration: impl Into>, + ) -> bool { + if self.banned_ips.iter().any(|e| e.value == ip) { + false + } else { + let time = Local::now(); + self.banned_ips.push(BanEntry { + value: ip, + banned: time, + source: by, + expires: duration.into().map(|duration| time.add(duration)), + reason, + }); + true + } + } + + /// Returns none if not banned + pub fn get_ban_entry(&self, uuid: &Uuid) -> Option<&BanEntry<(Uuid, String)>> { + self.banned_players + .iter() + .find(|entry| entry.value.0 == *uuid) + } + + /// Returns none if not banned + pub fn get_ip_ban_entry(&self, ip: IpAddr) -> Option<&BanEntry> { + self.banned_ips.iter().find(|entry| entry.value == ip) + } + + /// Returns true if unbanned, false if the player is not banned + pub fn pardon_name(&mut self, name: &str) -> bool { + let old_len = self.banned_players.len(); + self.banned_players.retain(|entry| entry.value.1 != *name); + old_len != self.banned_players.len() + } + + /// Returns true if unbanned, false if the player is not banned + pub fn pardon_id(&mut self, id: &Uuid) -> bool { + let old_len = self.banned_players.len(); + self.banned_players.retain(|entry| entry.value.0 != *id); + old_len != self.banned_players.len() + } + + /// Returns true if unbanned, false if the ip is not banned + pub fn pardon_ip(&mut self, ip: &IpAddr) -> bool { + let old_len = self.banned_ips.len(); + self.banned_ips.retain(|entry| entry.value != *ip); + old_len != self.banned_ips.len() + } + + pub fn players(&self) -> Vec<&BanEntry<(Uuid, String)>> { + self.banned_players.iter().collect() + } + + pub fn ips(&self) -> Vec<&BanEntry> { + self.banned_ips.iter().collect() + } +} + +#[derive(Debug)] +pub struct BanEntry { + pub value: T, + /// Timestamp when the player/ip was banned + pub banned: DateTime, + /// Some if banned by a player, None if banned by console + pub source: Option, + /// Timestamp when the player/ip should be unbanned + pub expires: Option>, + pub reason: BanReason, +} + +#[derive(Clone, PartialEq)] +pub struct BanReason(String); + +impl BanReason { + pub fn new(s: impl ToString) -> BanReason { + BanReason(s.to_string()) + } +} + +impl Deref for BanReason { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Debug for BanReason { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&*self) + } +} + +impl Default for BanReason { + fn default() -> Self { + Self("Banned by an operator.".to_string()) + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct PlayerEntry { + uuid: Uuid, + name: String, + created: String, + source: String, + expires: String, + reason: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct IpEntry { + ip: IpAddr, + created: String, + source: String, + expires: String, + reason: String, +} + +pub const DATETIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S %z"; + +pub fn read_banlist(server_dir: impl AsRef) -> BanList { + let s = std::fs::read_to_string(format!( + "{}/banned-players.json", + server_dir.as_ref().to_str().unwrap() + )); + let players: Vec = if let Ok(s) = s { + serde_json::from_reader(Cursor::new(&s)).unwrap_or_else(|_| { + log::warn!("Invalid banned-players.json: \n{}", s); + Vec::new() + }) + } else { + Vec::new() + }; + let players = players + .into_iter() + .map(|entry| BanEntry { + value: (entry.uuid, entry.name), + banned: DateTime::parse_from_str(&entry.created, DATETIME_FORMAT) + .expect("Invalid datetime format in banned-players.json") + .into(), + source: if entry.source == "Server" { + None + } else { + Some(entry.source) + }, + expires: DateTime::parse_from_str(&entry.expires, DATETIME_FORMAT) + .map(Into::into) + .ok(), + reason: BanReason(entry.reason), + }) + .collect(); + + let s = std::fs::read_to_string(format!( + "{}/banned-ips.json", + server_dir.as_ref().to_str().unwrap() + )); + let ips: Vec = if let Ok(s) = s { + serde_json::from_reader(Cursor::new(&s)).unwrap_or_else(|_| { + log::warn!("Invalid banned-ips.json: \n{}", s); + Vec::new() + }) + } else { + Vec::new() + }; + let ips = ips + .into_iter() + .map(|entry| BanEntry { + value: entry.ip, + banned: DateTime::parse_from_str(&entry.created, DATETIME_FORMAT) + .expect("Invalid datetime format in banned-ips.json") + .into(), + source: if entry.source == "Server" { + None + } else { + Some(entry.source) + }, + expires: DateTime::parse_from_str(&entry.expires, DATETIME_FORMAT) + .map(Into::into) + .ok(), + reason: BanReason(entry.reason), + }) + .collect(); + + BanList { + banned_players: players, + banned_ips: ips, + } +} + +#[allow(unused)] +pub fn write_banlist(_server_dir: impl AsRef) { + // There's no shutdown yet + unimplemented!() +} + +pub fn register(game: &mut Game) { + game.insert_resource(read_banlist(".")); +} diff --git a/feather/common/src/chat.rs b/feather/common/src/chat.rs deleted file mode 100644 index 287860495..000000000 --- a/feather/common/src/chat.rs +++ /dev/null @@ -1,118 +0,0 @@ -use base::{Text, Title}; - -/// An entity's "mailbox" for receiving chat messages. -/// -/// Internally stores a list of [`ChatMessage`]s. -/// It is up to the user to flush the mailbox. -/// (`feather-server` flushes mailboxes by sending chat packets.) -#[derive(Debug)] -pub struct ChatBox { - messages: Vec, - titles: Vec, - preference: ChatPreference, -} - -impl ChatBox { - pub fn new(preference: ChatPreference) -> Self { - Self { - messages: Vec::new(), - titles: Vec::new(), - preference, - } - } - - pub fn set_preference(&mut self, preference: ChatPreference) { - self.preference = preference; - } - - pub fn send(&mut self, message: ChatMessage) { - self.messages.push(message); - } - - pub fn send_chat(&mut self, message: impl Into<Text>) { - self.send(ChatMessage::new(ChatKind::PlayerChat, message.into())); - } - - pub fn send_system(&mut self, message: impl Into<Text>) { - self.send(ChatMessage::new(ChatKind::System, message.into())); - } - - pub fn send_above_hotbar(&mut self, message: impl Into<Text>) { - self.send(ChatMessage::new(ChatKind::AboveHotbar, message.into())); - } - - /// Adds the [`Title`] to the title queue. - pub fn send_title(&mut self, title: Title) { - self.titles.push(title); - } - - /// Drains titles in the mailbox - pub fn drain_titles(&mut self) -> impl Iterator<Item = Title> + '_ { - self.titles.drain(..) - } - - /// Drains messages in the mailbox. - pub fn drain(&mut self) -> impl Iterator<Item = ChatMessage> + '_ { - let preference = self.preference; - self.messages - .drain(..) - .filter(move |msg| msg.kind.should_send(preference)) - } -} - -/// Represents a chat message. -#[derive(Debug, Clone)] -pub struct ChatMessage { - kind: ChatKind, - message: Text, -} - -impl ChatMessage { - pub fn new(kind: ChatKind, message: Text) -> Self { - Self { kind, message } - } - - pub fn kind(&self) -> ChatKind { - self.kind - } - - pub fn text(&self) -> &Text { - &self.message - } -} - -/// Kind of chat message. The client determines whether -/// to display a message based on this kind. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub enum ChatKind { - /// A player chat message or similar. - PlayerChat, - /// The output of a command or other messages - /// not originating from players. - System, - /// A message displayed above the hotbar. - AboveHotbar, -} - -impl ChatKind { - pub fn should_send(self, preference: ChatPreference) -> bool { - match self { - ChatKind::PlayerChat => preference == ChatPreference::All, - ChatKind::System => preference >= ChatPreference::System, - ChatKind::AboveHotbar => true, - } - } -} - -/// A player's chat preference. -/// Determines which [`ChatKind`]s will -/// be sent to this player. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum ChatPreference { - /// Receive only game info messages. - GameInfoOnly, - /// Receive only messages from commands and game info messages. - System, - /// Receive all messages. - All, -} diff --git a/feather/common/src/events.rs b/feather/common/src/events.rs index 2426c2896..7d15f4067 100644 --- a/feather/common/src/events.rs +++ b/feather/common/src/events.rs @@ -3,10 +3,8 @@ use base::{ChunkHandle, ChunkPosition}; use crate::view::View; mod block_change; -mod plugin_message; pub use block_change::BlockChangeEvent; -pub use plugin_message::PluginMessageEvent; /// Triggered when a player joins the `Game`. #[derive(Debug)] diff --git a/feather/common/src/events/plugin_message.rs b/feather/common/src/events/plugin_message.rs deleted file mode 100644 index b648ae375..000000000 --- a/feather/common/src/events/plugin_message.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Debug)] -pub struct PluginMessageEvent { - pub channel: String, - pub data: Vec<u8>, -} diff --git a/feather/common/src/game.rs b/feather/common/src/game.rs index aa61f52cc..cdc0747e8 100644 --- a/feather/common/src/game.rs +++ b/feather/common/src/game.rs @@ -1,20 +1,18 @@ -use std::{cell::RefCell, mem, rc::Rc, sync::Arc}; +use std::{mem, sync::Arc}; use base::{BlockId, ChunkPosition, Position, Text, Title, ValidBlockPosition}; -use ecs::{ - Ecs, Entity, EntityBuilder, HasEcs, HasResources, NoSuchEntity, Resources, SysResult, - SystemExecutor, -}; +use ecs::{Ecs, Entity, EntityBuilder, HasEcs, HasResources, NoSuchEntity, Resources, SysResult}; +use quill_common::components::{ChatBox, ChatKind, ChatMessage}; +use quill_common::events::TimeUpdateEvent; use quill_common::{entities::Player, entity_init::EntityInit}; use crate::{ - chat::{ChatKind, ChatMessage}, chunk::entities::ChunkEntities, events::{BlockChangeEvent, EntityCreateEvent, EntityRemoveEvent, PlayerJoinEvent}, - ChatBox, World, + World, }; -type EntitySpawnCallback = Box<dyn FnMut(&mut EntityBuilder, &EntityInit)>; +pub type EntitySpawnCallback = Box<dyn FnMut(&mut EntityBuilder, &EntityInit)>; /// Stores the entire state of a Minecraft game. /// @@ -37,8 +35,6 @@ pub struct Game { pub world: World, /// Contains entities, including players. pub ecs: Ecs, - /// Contains systems. - pub system_executor: Rc<RefCell<SystemExecutor<Game>>>, /// User-defined resources. /// @@ -51,8 +47,6 @@ pub struct Game { /// Total ticks elapsed since the server started. pub tick_count: u64, - entity_spawn_callbacks: Vec<EntitySpawnCallback>, - entity_builder: EntityBuilder, } @@ -66,13 +60,11 @@ impl Game { /// Creates a new, empty `Game`. pub fn new() -> Self { Self { - world: World::new(), + world: World::default(), ecs: Ecs::new(), - system_executor: Rc::new(RefCell::new(SystemExecutor::new())), resources: Arc::new(Resources::new()), chunk_entities: ChunkEntities::default(), tick_count: 0, - entity_spawn_callbacks: Vec::new(), entity_builder: EntityBuilder::new(), } } @@ -101,7 +93,10 @@ impl Game { &mut self, callback: impl FnMut(&mut EntityBuilder, &EntityInit) + 'static, ) { - self.entity_spawn_callbacks.push(Box::new(callback)); + self.resources + .get_mut::<Vec<EntitySpawnCallback>>() + .unwrap() + .push(Box::new(callback)); } /// Creates an empty entity builder to create entities in @@ -132,11 +127,14 @@ impl Game { } fn invoke_entity_spawn_callbacks(&mut self, builder: &mut EntityBuilder, init: EntityInit) { - let mut callbacks = mem::take(&mut self.entity_spawn_callbacks); - for callback in &mut callbacks { + let callbacks = &mut *self + .resources + .get_mut::<Vec<EntitySpawnCallback>>() + .unwrap(); + for callback in callbacks { callback(builder, &init); } - self.entity_spawn_callbacks = callbacks; + //self.entity_spawn_callbacks = callbacks; } fn trigger_entity_spawn_events(&mut self, entity: Entity) { @@ -229,6 +227,15 @@ impl Game { pub fn break_block(&mut self, pos: ValidBlockPosition) -> bool { self.set_block(pos, BlockId::air()) } + + /// Sets the world time and notifies players + pub fn set_time(&mut self, time: u64) { + self.ecs.insert_event(TimeUpdateEvent { + old: self.world.time.time(), + new: time, + }); + self.world.time.set_time(time); + } } impl HasResources for Game { diff --git a/feather/common/src/lib.rs b/feather/common/src/lib.rs index 9f9e7348a..367cae640 100644 --- a/feather/common/src/lib.rs +++ b/feather/common/src/lib.rs @@ -5,32 +5,28 @@ #![allow(clippy::unnecessary_wraps)] // systems are required to return Results -mod game; use ecs::SystemExecutor; -pub use game::Game; - -mod tick_loop; +pub use game::{EntitySpawnCallback, Game}; pub use tick_loop::TickLoop; +pub use window::Window; +pub use world::World; +mod game; +mod tick_loop; pub mod view; -pub mod window; -pub use window::Window; - pub mod events; +pub mod window; pub mod chunk; mod region_worker; -pub mod world; -pub use world::World; - -pub mod chat; -pub use chat::ChatBox; - pub mod entities; +pub mod world; +pub mod banlist; pub mod interactable; +pub mod player_count; /// Registers gameplay systems with the given `Game` and `SystemExecutor`. pub fn register(game: &mut Game, systems: &mut SystemExecutor<Game>) { @@ -38,6 +34,8 @@ pub fn register(game: &mut Game, systems: &mut SystemExecutor<Game>) { chunk::loading::register(game, systems); chunk::entities::register(systems); interactable::register(game); + banlist::register(game); + game.insert_resource(Vec::<EntitySpawnCallback>::new()); game.add_entity_spawn_callback(entities::add_entity_components); } diff --git a/feather/server/src/player_count.rs b/feather/common/src/player_count.rs similarity index 100% rename from feather/server/src/player_count.rs rename to feather/common/src/player_count.rs diff --git a/feather/common/src/world.rs b/feather/common/src/world.rs index 201eeb13e..58a57beae 100644 --- a/feather/common/src/world.rs +++ b/feather/common/src/world.rs @@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::Arc}; use ahash::{AHashMap, AHashSet}; use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; + use uuid::Uuid; use base::anvil::player::PlayerData; @@ -31,36 +32,42 @@ pub struct World { loading_chunks: AHashSet<ChunkPosition>, canceled_chunk_loads: AHashSet<ChunkPosition>, world_dir: PathBuf, + pub time: WorldTime, + seed: i64, } impl Default for World { fn default() -> Self { + let seed = 0; Self { chunk_map: ChunkMap::new(), chunk_worker: ChunkWorker::new( "world", - Arc::new(ComposableGenerator::default_with_seed(0)), + Arc::new(ComposableGenerator::default_with_seed(seed)), ), cache: ChunkCache::new(), loading_chunks: AHashSet::new(), canceled_chunk_loads: AHashSet::new(), world_dir: "world".into(), + time: WorldTime { + world_age: 0, + time: 0, + }, + seed, } } } impl World { - pub fn new() -> Self { - Self::default() - } - - pub fn with_gen_and_path( + pub fn new( generator: Arc<dyn WorldGenerator>, world_dir: impl Into<PathBuf> + Clone, + seed: i64, ) -> Self { Self { world_dir: world_dir.clone().into(), chunk_worker: ChunkWorker::new(world_dir, generator), + seed, ..Default::default() } } @@ -169,6 +176,10 @@ impl World { pub fn save_player_data(&self, uuid: Uuid, data: &PlayerData) -> anyhow::Result<()> { base::anvil::player::save_player_data(&self.world_dir, uuid, data) } + + pub fn seed(&self) -> i64 { + self.seed + } } pub type ChunkMapInner = AHashMap<ChunkPosition, ChunkHandle>; @@ -261,6 +272,48 @@ fn chunk_relative_pos(block_pos: BlockPosition) -> (usize, usize, usize) { ) } +/// This struct stores world time +pub struct WorldTime { + world_age: u64, + time: u64, +} + +impl WorldTime { + pub const DAY_TICKS: u64 = 24000; + + /// Real world time which is not affected by commands and plugins + pub fn world_age(&self) -> u64 { + self.world_age + } + + /// World time + pub fn time(&self) -> u64 { + self.time + } + + /// Time of day (0-23999) + pub fn time_of_day(&self) -> u64 { + self.time % WorldTime::DAY_TICKS + } + /// Gets world days. It's not the actual world age, + /// to get real world days use [`world_time.world_age()`](WorldTime::world_age) / [`WorldTime::DAY_TICKS`](WorldTime::DAY_TICKS) + pub fn days(&self) -> u64 { + self.time / WorldTime::DAY_TICKS + } + + /// Sets server-side world time. If you want to set world time both server-side and client-side, + /// you probably want to use [`game.set_time`](crate::Game::set_time) + pub fn set_time(&mut self, time: u64) { + self.time = time + } + + /// Increases server-side time by 1 tick + pub fn increment(&mut self) { + self.set_time(self.time() + 1); + self.world_age += 1; + } +} + #[cfg(test)] mod tests { use std::convert::TryInto; @@ -269,7 +322,7 @@ mod tests { #[test] fn world_out_of_bounds() { - let mut world = World::new(); + let mut world = World::default(); world .chunk_map_mut() .insert_chunk(Chunk::new(ChunkPosition::new(0, 0))); @@ -281,4 +334,19 @@ mod tests { .block_at(BlockPosition::new(0, 0, 0).try_into().unwrap()) .is_some()); } + + #[test] + fn time() { + let mut time = WorldTime { + world_age: 0, + time: 0, + }; + time.set_time(WorldTime::DAY_TICKS * 2 - 1); + assert_eq!(time.time_of_day(), WorldTime::DAY_TICKS - 1); + time.increment(); + assert_eq!(time.days(), 2); + assert_eq!(time.time_of_day(), 0); + assert_eq!(time.time(), WorldTime::DAY_TICKS * 2); + assert_eq!(time.world_age(), 1); + } } diff --git a/feather/generated/src/lib.rs b/feather/generated/src/lib.rs new file mode 100644 index 000000000..01a61fa93 --- /dev/null +++ b/feather/generated/src/lib.rs @@ -0,0 +1,242 @@ +use parking_lot::{Mutex, MutexGuard}; +use std::sync::Arc; + +#[allow(clippy::all)] +mod biome; +#[allow(clippy::all)] +mod block; +#[allow(clippy::all)] +mod entity; +#[allow(clippy::all)] +mod inventory; +#[allow(clippy::all)] +mod item; +#[allow(clippy::all)] +mod particle; +#[allow(clippy::all)] +mod simplified_block; +#[allow(clippy::all)] +pub mod vanilla_tags; + +pub use biome::Biome; +pub use block::BlockKind; +pub use entity::EntityKind; +pub use inventory::{Area, InventoryBacking, Window}; +pub use item::Item; +pub use particle::Particle; +pub use simplified_block::SimplifiedBlockKind; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ItemStack { + pub item: Item, + pub count: u32, + + /// Damage to the item, if it's damageable. + pub damage: Option<u32>, +} + +impl ItemStack { + /// Creates a new `ItemStack`. + pub fn new(item: Item, count: u32) -> Self { + Self { + item, + count, + damage: item.durability().map(|_| 0), + } + } + + /// Returns whether the given item stack has + /// the same type as (but not necessarily the same + /// amount as) `self`. + pub fn has_same_type(&self, other: &ItemStack) -> bool { + other.item == self.item && other.damage == self.damage + } + + /// Returns the item type for this `ItemStack`. + pub fn item(&self) -> Item { + self.item + } + + /// Returns the number of items in this `ItemStack`. + pub fn count(&self) -> u32 { + self.count + } + + /// Adds more items to this ItemStack. Returns the new count. + pub fn add(&mut self, count: u32) -> u32 { + self.count += count; + self.count + } + + /// Removes some items from this ItemStack. Returns whether there + /// were enough items to be removed. + pub fn remove(&mut self, count: u32) -> bool { + self.count = match self.count.checked_sub(count) { + Some(count) => count, + None => return false, + }; + true + } + + /// Sets the item for this `ItemStack`. + pub fn set_item(&mut self, item: Item) { + self.item = item; + } + + /// Sets the count for this `ItemStack`. + pub fn set_count(&mut self, count: u32) { + self.count = count; + } + + /// Splits this `ItemStack` in half, returning the + /// second item stack. If the amount is odd, `self` + /// will be left with the least items. + pub fn take_half(&mut self) -> ItemStack { + let count_left = self.count / 2; + let other_half = ItemStack { + count: self.count - count_left, + ..self.clone() + }; + self.count = count_left; + other_half + } + + /// Splits this `ItemStack`. + pub fn take(&mut self, amount: u32) -> ItemStack { + let count_left = self.count.saturating_sub(amount); + let count_lost = self.count - count_left; + self.count = count_left; + ItemStack { + count: count_lost, + ..self.clone() + } + } + + /// Merges another `ItemStack` with this one. + pub fn merge_with(&mut self, other: &mut ItemStack) { + if !self.has_same_type(other) { + return; + } + let new_count = (self.count + other.count).min(self.item.stack_size()); + let amount_added = new_count - self.count; + self.count = new_count; + other.count -= amount_added; + } + + /// Transfers up to `n` items to `other`. + pub fn transfer_to(&mut self, n: u32, other: &mut ItemStack) { + let max_transfer = other.item.stack_size().saturating_sub(other.count); + let transfer = max_transfer.min(self.count).min(n); + self.count -= transfer; + other.count += transfer; + } + + /// Damages the item by the specified amount. + /// If this returns `true`, then the item is broken. + pub fn damage(&mut self, amount: u32) -> bool { + match &mut self.damage { + Some(damage) => { + *damage += amount; + if let Some(durability) = self.item.durability() { + *damage >= durability + } else { + false + } + } + None => false, + } + } +} + +type Slot = Mutex<Option<ItemStack>>; + +/// A handle to an inventory. +/// +/// An inventory is composed of one or more _areas_, each +/// if which contains one or more item stacks stored in an array. Areas are defined +/// by the `Area` enum; examples include `Storage`, `Hotbar`, `Helmet`, `Offhand`, +/// and `CraftingInput`. +/// +/// Note that an `Inventory` is a _handle_; it's backed by an `Arc`. As such, cloning +/// it is cheap and creates a new handle to the same inventory. Interior mutability +/// is used to make this safe. +#[derive(Debug, Clone)] +pub struct Inventory { + backing: Arc<InventoryBacking<Slot>>, +} + +impl Inventory { + /// Returns whether two `Inventory` handles point to the same + /// backing inventory. + pub fn ptr_eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.backing, &other.backing) + } + + /// Gets the item at the given index within an area in this inventory. + /// + /// The returned value is a `MutexGuard` and can be mutated. + /// + /// # Note + /// _Never_ keep two returned `MutexGuard`s for the same inventory alive + /// at once. Deadlocks are not fun. + pub fn item(&self, area: Area, slot: usize) -> Option<MutexGuard<Option<ItemStack>>> { + let slice = self.backing.area_slice(area)?; + slice.get(slot).map(Mutex::lock) + } + + /// Gets all the items in this inventory. + pub fn to_vec(&self) -> Vec<Option<ItemStack>> { + let mut vec = Vec::new(); + for area in self.backing.areas() { + for item in self.backing.area_slice(*area).unwrap() { + vec.push(item.lock().clone()); + } + } + vec + } + + /// Creates a new handle to the same inventory. + /// + /// This operation is the same as calling `clone()`, but it's more explicit + /// in its intent. + pub fn new_handle(&self) -> Inventory { + self.clone() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum WindowError { + #[error("slot index {0} is out of bounds")] + OutOfBounds(usize), +} + +impl Window { + /// Gets the item at the provided protocol index. + /// Returns an error if index is invalid. + pub fn item(&self, index: usize) -> Result<MutexGuard<Option<ItemStack>>, WindowError> { + let (inventory, area, slot) = self + .index_to_slot(index) + .ok_or(WindowError::OutOfBounds(index))?; + inventory + .item(area, slot) + .ok_or(WindowError::OutOfBounds(index)) + } + + /// Sets the item at the provided protocol index. + /// Returns an error if the index is invalid. + pub fn set_item(&self, index: usize, item: Option<ItemStack>) -> Result<(), WindowError> { + *self.item(index)? = item; + Ok(()) + } + + /// Gets a vector of all items in this window. + pub fn to_vec(&self) -> Vec<Option<ItemStack>> { + let mut i = 0; + let mut vec = Vec::new(); + while let Ok(item) = self.item(i) { + vec.push(item.clone()); + i += 1; + } + vec + } +} diff --git a/feather/plugin-host/Cargo.toml b/feather/plugin-host/Cargo.toml index a42db8360..7982907df 100644 --- a/feather/plugin-host/Cargo.toml +++ b/feather/plugin-host/Cargo.toml @@ -10,16 +10,19 @@ anyhow = "1" bincode = "1" bumpalo = "3" bytemuck = "1" +commands = { git = "https://github.com/Iaiao/commands", rev = "42bebd15e18cf511355aaf5f241fb4218031c4ee" } feather-base = { path = "../base" } feather-common = { path = "../common" } feather-ecs = { path = "../ecs" } feather-plugin-host-macros = { path = "macros" } +feather-commands = { path = "../commands" } libloading = "0.7" log = "0.4" paste = "1" quill-common = { path = "../../quill/common" } quill-plugin-format = { path = "../../quill/plugin-format" } +quill = { path = "../../quill/api" } serde = "1" tempfile = "3" vec-arena = "1" diff --git a/feather/plugin-host/src/host_calls.rs b/feather/plugin-host/src/host_calls.rs index bf90ccb81..ece57f731 100644 --- a/feather/plugin-host/src/host_calls.rs +++ b/feather/plugin-host/src/host_calls.rs @@ -9,6 +9,7 @@ use crate::env::PluginEnv; use crate::host_function::{NativeHostFunction, WasmHostFunction}; mod block; +mod command; mod component; mod entity; mod entity_builder; @@ -46,6 +47,7 @@ macro_rules! host_calls { } use block::*; +use command::*; use component::*; use entity::*; use entity_builder::*; @@ -69,4 +71,5 @@ host_calls! { "block_set" => block_set, "block_fill_chunk_section" => block_fill_chunk_section, "plugin_message_send" => plugin_message_send, + "modify_command_executor" => modify_command_executor, } diff --git a/feather/plugin-host/src/host_calls/command.rs b/feather/plugin-host/src/host_calls/command.rs new file mode 100644 index 000000000..b3d1a4fff --- /dev/null +++ b/feather/plugin-host/src/host_calls/command.rs @@ -0,0 +1,135 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::mem::ManuallyDrop; +use std::rc::Rc; +use std::sync::Arc; +use std::time::Duration; + +use commands::args::Func; +use commands::dispatcher::{Args, CommandDispatcher, CommandOutput, Completer, Fork}; +use commands::node::CommandNode; +use commands::parser::ArgumentParser; +use wasmer::FromToNativeWasmType; + +use feather_commands::CommandCtx; +use feather_plugin_host_macros::host_function; +use quill::command::{Caller, CommandContext}; +use quill::Game; +use quill_common::EntityId; + +use crate::context::{PluginContext, PluginPtr, PluginPtrMut}; +use crate::PluginManager; +use feather_base::Text; + +#[host_function] +pub fn modify_command_executor( + cx: &PluginContext, + nodes: PluginPtrMut<u8>, + nodes_len: u32, + nodes_cap: u32, + executors: PluginPtrMut<u8>, + executors_len: u32, + executors_cap: u32, + tab_completers: PluginPtrMut<u8>, + tab_completers_len: u32, + tab_completers_cap: u32, + forks: PluginPtrMut<u8>, + forks_len: u32, + forks_cap: u32, +) -> anyhow::Result<()> { + // SAFETY: Plugins should pass valid raw vec data. + let (nodes, executors, tab_completers, forks) = unsafe { + let nodes = Vec::from_raw_parts( + nodes.as_native() as *mut CommandNode, + nodes_len as usize, + nodes_cap as usize, + ); + let executors = Vec::from_raw_parts( + executors.as_native() as *mut Box<dyn Func<CommandContext<()>, ()>>, + executors_len as usize, + executors_cap as usize, + ); + let tab_completers = Vec::from_raw_parts( + tab_completers.as_native() as *mut (String, Completer<CommandContext<()>, Text>), + tab_completers_len as usize, + tab_completers_cap as usize, + ); + let forks = Vec::from_raw_parts( + forks.as_native() as *mut Box<Fork<CommandContext<()>>>, + forks_len as usize, + forks_cap as usize, + ); + (nodes, executors, tab_completers, forks) + }; + let game = cx.game_mut(); + let mut dispatcher = game + .resources + .get_mut::<CommandDispatcher<CommandCtx, Text>>() + .unwrap(); + let id = cx.plugin_id(); + + dispatcher.add_nodes(nodes); + + for executor in executors.into_iter() { + dispatcher.add_executor(move |args: &mut Args, mut context: CommandCtx| { + let plugin_manager = context + .resources + .get::<Rc<RefCell<PluginManager>>>() + .unwrap(); + let rc = plugin_manager.clone(); + drop(plugin_manager); + let plugin_manager = rc.borrow(); + let plugin = plugin_manager.plugin(id).unwrap(); + let b = plugin.run_command( + PluginPtrMut::from_native(&executor as *const _ as i64), + args, + context, + ); + b + }); + } + + let mut fork_now = dispatcher.forks().count(); + for fork in forks { + let fork = &fork as *const _ as i64; + fork_now = dispatcher.add_fork(Box::new( + move |args: &mut Args, + context: CommandCtx, + mut f: Box<&mut dyn FnMut(&mut Args, CommandCtx) -> CommandOutput>| { + let plugin_manager = context + .resources + .get::<Rc<RefCell<PluginManager>>>() + .unwrap(); + let rc = plugin_manager.clone(); + drop(plugin_manager); + let plugin_manager = rc.borrow(); + let plugin = plugin_manager.plugin(id).unwrap(); + plugin.run_command_fork( + PluginPtrMut::from_native(fork), + args, + context, + fork_now as u32, + ) + }, + ) as Box<_>); + } + + for (key, complete) in tab_completers { + dispatcher.register_tab_completion(&key, move |text, context| { + let plugin_manager = context + .resources + .get::<Rc<RefCell<PluginManager>>>() + .unwrap(); + let rc = plugin_manager.clone(); + drop(plugin_manager); + let plugin_manager = rc.borrow(); + let plugin = plugin_manager.plugin(id).unwrap(); + plugin.run_command_completer( + PluginPtrMut::from_native(&complete as *const _ as usize as i64), + text, + context, + ) + }); + } + Ok(()) +} diff --git a/feather/plugin-host/src/host_calls/entity.rs b/feather/plugin-host/src/host_calls/entity.rs index f6d443476..f75cc6f31 100644 --- a/feather/plugin-host/src/host_calls/entity.rs +++ b/feather/plugin-host/src/host_calls/entity.rs @@ -1,9 +1,9 @@ use feather_base::Text; -use feather_common::chat::{ChatKind, ChatMessage}; use feather_ecs::Entity; use feather_plugin_host_macros::host_function; use crate::context::{PluginContext, PluginPtr}; +use quill_common::components::{ChatKind, ChatMessage}; #[host_function] pub fn entity_exists(cx: &PluginContext, entity: u64) -> anyhow::Result<u32> { @@ -15,13 +15,12 @@ pub fn entity_send_message( cx: &PluginContext, entity: u64, message_ptr: PluginPtr<u8>, - message_len: u32, ) -> anyhow::Result<()> { - let message = cx.read_string(message_ptr, message_len)?; + let message = message_ptr.as_native() as *const Text; let entity = Entity::from_bits(entity); let _ = cx.game_mut().send_message( entity, - ChatMessage::new(ChatKind::System, Text::from(message)), + ChatMessage::new(ChatKind::System, unsafe { &*message }.clone()), ); Ok(()) } diff --git a/feather/plugin-host/src/host_calls/plugin_message.rs b/feather/plugin-host/src/host_calls/plugin_message.rs index 55108733c..ad8952483 100644 --- a/feather/plugin-host/src/host_calls/plugin_message.rs +++ b/feather/plugin-host/src/host_calls/plugin_message.rs @@ -1,8 +1,8 @@ -use feather_common::events::PluginMessageEvent; use feather_ecs::Entity; use feather_plugin_host_macros::host_function; use crate::context::{PluginContext, PluginPtr}; +use quill::events::{PluginMessageReceiveEvent, PluginMessageSendEvent}; #[host_function] pub fn plugin_message_send( @@ -17,7 +17,7 @@ pub fn plugin_message_send( let data = cx.read_bytes(data_ptr, data_len)?; let entity = Entity::from_bits(entity); - let event = PluginMessageEvent { channel, data }; + let event = PluginMessageSendEvent { channel, data }; cx.game_mut().ecs.insert_entity_event(entity, event)?; Ok(()) diff --git a/feather/plugin-host/src/host_calls/system.rs b/feather/plugin-host/src/host_calls/system.rs index a1c4c0635..866a5562f 100644 --- a/feather/plugin-host/src/host_calls/system.rs +++ b/feather/plugin-host/src/host_calls/system.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, rc::Rc}; use feather_common::Game; -use feather_ecs::{HasResources, SysResult}; +use feather_ecs::{HasResources, SysResult, SystemExecutor}; use feather_plugin_host_macros::host_function; use crate::{ @@ -20,9 +20,10 @@ pub fn register_system( ) -> anyhow::Result<()> { let name = cx.read_string(name_ptr, name_len)?; - let game = cx.game_mut(); - game.system_executor - .borrow_mut() + cx.game_mut() + .resources + .get_mut::<SystemExecutor<Game>>() + .unwrap() .add_system_with_name(plugin_system(cx.plugin_id(), data_ptr), &name); Ok(()) diff --git a/feather/plugin-host/src/plugin.rs b/feather/plugin-host/src/plugin.rs index 9390e3a65..e77ef63a1 100644 --- a/feather/plugin-host/src/plugin.rs +++ b/feather/plugin-host/src/plugin.rs @@ -1,13 +1,19 @@ use std::sync::Arc; use anyhow::bail; +use commands::dispatcher::{Args, CommandOutput, TabCompletion}; + +use feather_commands::CommandCtx; use feather_common::Game; +use quill::{Caller, CommandContext}; +use quill_common::EntityId; use quill_plugin_format::{PluginFile, PluginMetadata, PluginTarget, Triple}; use crate::{ context::{PluginContext, PluginPtrMut}, PluginId, PluginManager, }; +use feather_base::Text; mod native; mod wasm; @@ -94,6 +100,75 @@ impl Plugin { } }) } + + pub fn run_command( + &self, + data: PluginPtrMut<u8>, + args: &mut Args, + mut context: CommandCtx, + ) -> CommandOutput { + let caller = Caller::from(Some(EntityId(context.sender.to_bits()))); + self.context.enter(&mut *context, || { + let ctx = CommandContext { + game: quill::Game::new(), + caller, + // Command context with the plugin will be created on the plugin side + plugin: &mut (), + }; + match &self.inner { + Inner::Wasm(w) => w.run_command(data, args, ctx), + Inner::Native(n) => n.run_command(data, args, ctx), + } + }) + } + + pub fn run_command_fork( + &self, + data: PluginPtrMut<u8>, + args: &mut Args, + mut context: CommandCtx, + f: u32, + ) -> CommandOutput { + let caller = Caller::from(Some(EntityId(context.sender.to_bits()))); + self.context.enter(&mut *context, || { + let ctx = CommandContext { + game: quill::Game::new(), + caller, + // Command context with the plugin will be created on the plugin side + plugin: &mut (), + }; + match &self.inner { + Inner::Wasm(w) => w.run_command_fork(data, args, ctx, f), + Inner::Native(n) => n.run_command_fork(data, args, ctx, f), + } + }) + } + + pub fn run_command_completer( + &self, + data: PluginPtrMut<u8>, + text: &str, + context: &mut CommandCtx, + ) -> TabCompletion<Text> { + let caller = Caller::from(Some(EntityId(context.sender.to_bits()))); + self.context.enter(context, || { + let ctx = CommandContext { + game: quill::Game::new(), + caller, + // Command context with the plugin will be created on the plugin side + plugin: &mut (), + }; + match &self.inner { + Inner::Wasm(w) => w.run_command_completer(data, text, ctx), + Inner::Native(n) => n.run_command_completer(data, text, ctx), + } + .unwrap_or_default() + }) + } + + pub fn enter<R>(&self, game: &mut Game, callback: impl FnOnce() -> R) -> R { + self.context.enter(game, callback) + } } enum Inner { diff --git a/feather/plugin-host/src/plugin/native.rs b/feather/plugin-host/src/plugin/native.rs index 48601ffe1..854d05451 100644 --- a/feather/plugin-host/src/plugin/native.rs +++ b/feather/plugin-host/src/plugin/native.rs @@ -1,9 +1,14 @@ +use std::mem::ManuallyDrop; use std::{io::Write, sync::Arc}; -use anyhow::Context; +use anyhow::{bail, Context}; +use commands::dispatcher::{Args, CommandOutput, TabCompletion}; use libloading::Library; use tempfile::{NamedTempFile, TempPath}; +use feather_base::Text; +use quill::CommandContext; + use crate::context::{PluginContext, PluginPtrMut}; /// A native plugin loaded from a shared library @@ -27,6 +32,41 @@ pub struct NativePlugin { /// Parameters: /// 1. Plugin data pointer for this system run_system: unsafe extern "C" fn(*mut u8), + + /// The plugin's exported quill_run_command function. + /// + /// Parameters: + /// 1. Plugin data pointer for the command executor + /// 2. Arguments buffer pointer + /// 3. Arguments length + /// 4. Pointer to the command context + /// + /// Returns: command response (first: 0 = Ok(second), 1 = Err(_)) + run_command: unsafe extern "C" fn(*mut u8, *mut u8, u32, *mut u8) -> (u32, u64), + + /// The plugin's exported quill_run_command function. + /// + /// Parameters: + /// 1. Plugin data pointer for the command executor + /// 2. Arguments buffer pointer + /// 3. Arguments length + /// 4. Pointer to the command context + /// 5. Fork index + /// + /// Returns: command response (first: 0 = Ok(second), 1 = Err(_)) + run_command_fork: unsafe extern "C" fn(*mut u8, *mut u8, u32, *mut u8, u32) -> (u32, u64), + + /// The plugin's exported quill_run_command function. + /// + /// Parameters: + /// 1. Plugin data pointer for the command executor + /// 2. Text buffer pointer + /// 3. Text length + /// 4. Pointer to the command context + /// + /// Returns: command completions Vec<(String, is_some, optional String)> + run_command_completer: + unsafe extern "C" fn(*mut u8, *mut u8, u32, *mut u8) -> (u32, u32, u32, u32, u32), } impl NativePlugin { @@ -55,12 +95,30 @@ impl NativePlugin { .get("quill_run_system".as_bytes()) .context("plugin is missing quill_run_system export")? }; + let run_command = unsafe { + *library + .get("quill_run_command".as_bytes()) + .context("plugin is missing quill_run_command export")? + }; + let run_command_fork = unsafe { + *library + .get("quill_run_command_fork".as_bytes()) + .context("plugin is missing quill_run_command_fork export")? + }; + let run_command_completer = unsafe { + *library + .get("quill_run_command_completer".as_bytes()) + .context("plugin is missing quill_run_command export")? + }; Ok(Self { tempfile: path, library, enable, run_system, + run_command, + run_command_fork, + run_command_completer, }) } @@ -86,7 +144,127 @@ impl NativePlugin { } pub fn run_system(&self, data: PluginPtrMut<u8>) { - // SAFETY: we assume the plugin is sound. unsafe { (self.run_system)(data.as_native()) } } + + pub fn run_command( + &self, + data_ptr: PluginPtrMut<u8>, + args: &mut Args, + ctx: CommandContext<()>, + ) -> CommandOutput { + // SAFETY: we assume the plugin is sound. + unsafe { + args.shrink_to_fit(); + match { + (self.run_command)( + data_ptr.as_native(), + args.as_mut_ptr() as *mut _, + args.len() as u32, + &ctx as *const _ as *mut _, + ) + } { + (0, result) => Ok(result as i32), + (1, error_ptr) => { + // Reading string from a pointer + const USIZE_SIZE: usize = std::mem::size_of::<usize>(); // 4 on wasm32, same as usize on native + type USIZE = usize; // u32 on wasm32, usize on native + + let ptr = error_ptr as *const USIZE; + let len = *ptr.add(1); + let cap = *ptr.add(2); + let s = String::from_raw_parts(USIZE::from(*ptr) as *mut u8, len, cap); + + bail!("Plugin command returned an error: {}", s) + } + _ => bail!("Invalid bool returned from function"), + } + } + } + + pub fn run_command_fork( + &self, + data_ptr: PluginPtrMut<u8>, + args: &mut Args, + ctx: CommandContext<()>, + f: u32, + ) -> CommandOutput { + // SAFETY: we assume the plugin is sound. + unsafe { + match { + (self.run_command_fork)( + data_ptr.as_native(), + args.as_ptr() as *const _ as *mut _, + args.len() as u32, + &ctx as *const _ as *mut _, + f, + ) + } { + (0, result) => Ok(result as i32), + (1, error_ptr) => { + // Reading string from a pointer + const USIZE_SIZE: usize = std::mem::size_of::<usize>(); // 4 on wasm32, same as usize on native + type USIZE = usize; // u32 on wasm32, usize on native + + let ptr = error_ptr as *const USIZE; + let len = *ptr.add(1); + let cap = *ptr.add(2); + let s = String::from_raw_parts(USIZE::from(*ptr) as *mut u8, len, cap); + + bail!("Plugin command returned an error: {}", s) + } + _ => bail!("Invalid bool returned from function"), + } + } + } + + pub fn run_command_completer( + &self, + data_ptr: PluginPtrMut<u8>, + text: &str, + ctx: CommandContext<()>, + ) -> anyhow::Result<TabCompletion<Text>> { + // SAFETY: Text should be dropped on plugin side + let mut text = ManuallyDrop::new(text.to_string()); + + // SAFETY: we assume the plugin is sound. + let (start, end, ptr, len, cap) = unsafe { + (self.run_command_completer)( + data_ptr.as_native(), + text.as_mut_ptr(), + text.len() as u32, + &ctx as *const _ as *mut _, + ) + }; + + // SAFETY: assuming plugin sent valid data + Ok((start as usize, end as usize, unsafe { + Vec::from_raw_parts( + ptr as *mut ((*mut u8, u32, u32), bool, (*mut u8, u32, u32)), + len as usize, + cap as usize, + ) + .into_iter() + .map( + |((comp, comp_len, comp_cap), is_some, (tooltip, tooltip_len, tooltip_cap))| { + ( + String::from_raw_parts(comp, comp_len as usize, comp_cap as usize), + if is_some { + Some( + Text::from_json(String::from_raw_parts( + tooltip, + tooltip_len as usize, + tooltip_cap as usize, + )) + .unwrap(), + ) + } else { + None + }, + ) + }, + ) + .collect() + })) + } } diff --git a/feather/plugin-host/src/plugin/wasm.rs b/feather/plugin-host/src/plugin/wasm.rs index 73d839e9d..80cd28814 100644 --- a/feather/plugin-host/src/plugin/wasm.rs +++ b/feather/plugin-host/src/plugin/wasm.rs @@ -1,16 +1,23 @@ +use std::mem::ManuallyDrop; use std::sync::Arc; -use quill_plugin_format::PluginMetadata; +use anyhow::bail; +use commands::dispatcher::{Args, CommandOutput, TabCompletion}; use wasmer::{ ChainableNamedResolver, Features, Function, ImportObject, Instance, Module, NativeFunc, Store, }; use wasmer_wasi::{WasiEnv, WasiState, WasiVersion}; +use quill::CommandContext; +use quill_common::{Pointer, PointerMut}; +use quill_plugin_format::PluginMetadata; + use crate::{ context::{PluginContext, PluginPtr, PluginPtrMut}, env::PluginEnv, PluginManager, }; +use feather_base::Text; pub struct WasmPlugin { /// The WebAssembly instancing containing @@ -21,7 +28,19 @@ pub struct WasmPlugin { enable: Function, /// Exported function to run a system given its data pointer. - run_system: NativeFunc<u32>, + run_system: NativeFunc<(u32)>, + + /// Exported function to run a command executor given its data pointer, args, args length, + /// command context pointer and return command execution result (should be casted to bool). + run_command: NativeFunc<(u32, u32, u32, u32), (u32, u64)>, + + /// Exported function to run a command fork given its data pointer, args, args length, + /// command context pointer, fork index and return command execution result + run_command_fork: NativeFunc<(u32, u32, u32, u32, u32), (u32, u64)>, + + /// Exported function to run a command completer given its data pointer, text, text length, + /// command context pointer and return command completions: Vec<(String, is_some, optional String)>. + run_command_completer: NativeFunc<(u32, u32, u32, u32), (u32, u32, u32, u32, u32)>, } impl WasmPlugin { @@ -46,12 +65,30 @@ impl WasmPlugin { .get_function("quill_run_system")? .native()? .clone(); + let run_command = instance + .exports + .get_function("quill_run_command")? + .native()? + .clone(); + let run_command_fork = instance + .exports + .get_function("quill_run_command_fork")? + .native()? + .clone(); + let run_command_completer = instance + .exports + .get_function("quill_run_command_completer")? + .native()? + .clone(); let enable = instance.exports.get_function("quill_setup")?.clone(); Ok(Self { instance, - run_system, enable, + run_system, + run_command, + run_command_fork, + run_command_completer, }) } @@ -64,6 +101,117 @@ impl WasmPlugin { self.run_system.call(data_ptr.ptr as u32)?; Ok(()) } + + pub fn run_command( + &self, + data_ptr: PluginPtrMut<u8>, + args: &mut Args, + ctx: CommandContext<()>, + ) -> CommandOutput { + match self.run_command.call( + data_ptr.ptr as u32, + args.as_mut_ptr() as usize as u32, + args.len() as u32, + &ctx as *const _ as usize as u32, + )? { + (0, result) => Ok(result as i32), + (1, error_ptr) => { + // Reading string from a pointer + const USIZE_SIZE: usize = 4; // 4 on wasm32, same as usize on native + type USIZE = u32; // u32 on wasm32, usize on native + + unsafe { + let ptr = error_ptr as *const USIZE; + let len = *ptr.add(1); + let cap = *ptr.add(2); + let s = String::from_raw_parts(todo!(), len as usize, cap as usize); + + bail!("Plugin command returned an error: {}", s) + } + } + _ => bail!("Invalid bool returned from function"), + } + } + + pub fn run_command_fork( + &self, + data_ptr: PluginPtrMut<u8>, + args: &mut Args, + ctx: CommandContext<()>, + f: u32, + ) -> CommandOutput { + match self.run_command_fork.call( + data_ptr.ptr as u32, + args.as_ptr() as usize as u32, + args.len() as u32, + &ctx as *const _ as usize as u32, + f as u32, + )? { + (0, result) => Ok(result as i32), + (1, error_ptr) => { + // Reading string from a pointer + const USIZE_SIZE: usize = 4; // 4 on wasm32, same as usize on native + type USIZE = u32; // u32 on wasm32, usize on native + + unsafe { + let ptr = error_ptr as *const USIZE; + let len = *ptr.add(1); + let cap = *ptr.add(2); + let s = String::from_raw_parts(todo!(), len as usize, cap as usize); + + bail!("Plugin command returned an error: {}", s) + } + } + _ => bail!("Invalid bool returned from function"), + } + } + + pub fn run_command_completer( + &self, + data_ptr: PluginPtrMut<u8>, + text: &str, + ctx: CommandContext<()>, + ) -> anyhow::Result<TabCompletion<Text>> { + // SAFETY: Text should be dropped on plugin side + let text = ManuallyDrop::new(text.to_string()); + + let (start, end, ptr, len, cap) = self.run_command_completer.call( + data_ptr.ptr as u32, + text.as_ptr() as usize as u32, + text.len() as u32, + &ctx as *const _ as usize as u32, + )?; + + // SAFETY: assuming plugin sent valid data + Ok((start as usize, end as usize, unsafe { + Vec::from_raw_parts( + ptr as *mut ((*mut u8, u32, u32), bool, (*mut u8, u32, u32)), + len as usize, + cap as usize, + ) + .into_iter() + .map( + |((comp, comp_len, comp_cap), is_some, (tooltip, tooltip_len, tooltip_cap))| { + ( + String::from_raw_parts(comp, comp_len as usize, comp_cap as usize), + if is_some { + Some( + Text::from_json(String::from_raw_parts( + tooltip, + tooltip_len as usize, + tooltip_cap as usize, + )) + .unwrap(), + ) + } else { + None + }, + ) + }, + ) + .collect() + })) + } } fn generate_wasi_import_object(store: &Store, plugin_name: &str) -> anyhow::Result<ImportObject> { diff --git a/feather/protocol/src/packets/server/play.rs b/feather/protocol/src/packets/server/play.rs index 01c83b03f..6e202cd66 100644 --- a/feather/protocol/src/packets/server/play.rs +++ b/feather/protocol/src/packets/server/play.rs @@ -13,6 +13,7 @@ use super::*; mod chunk_data; mod update_light; + packets! { SpawnEntity { entity_id VarInt; @@ -232,25 +233,10 @@ packets! { TabCompleteMatch { value String; - has_tooltip bool; tooltip Option<String>; } DeclareCommands { - // (not implemented) - __todo__ LengthInferredVecU8; - /* nodes LengthPrefixedVec<CommandNode>; - root_index VarInt; */ - } - - CommandNode { - flags u8; - children VarIntPrefixedVec<VarInt>; - redirect_node Option<VarInt>; - name Option<String>; - parser Option<String>; - // TODO: handle properties, which vary depending on the value of `parser`. - // This can be handled with an enum. __todo__ LengthInferredVecU8; } diff --git a/feather/server/Cargo.toml b/feather/server/Cargo.toml index 4e9758fa4..a0385de4b 100644 --- a/feather/server/Cargo.toml +++ b/feather/server/Cargo.toml @@ -40,6 +40,7 @@ rand = "0.7" ring = "0.16" rsa = "0.3" rsa-der = "0.2" +rustyline = "9" serde = { version = "1", features = [ "derive" ] } serde_json = "1" sha-1 = "0.9" @@ -51,7 +52,11 @@ uuid = "0.8" slab = "0.4" libcraft-core = { path = "../../libcraft/core" } libcraft-items = { path = "../../libcraft/items" } +libcraft-text = { path = "../../libcraft/text" } worldgen = { path = "../worldgen", package = "feather-worldgen" } +datapacks = { path = "../datapacks", package = "feather-datapacks" } +feather-commands = { path = "../commands" } +commands = { git = "https://github.com/Iaiao/commands", rev = "42bebd15e18cf511355aaf5f241fb4218031c4ee" } [features] default = [ "plugin-cranelift" ] diff --git a/feather/server/config.toml b/feather/server/config.toml index b0206bc8a..381706f7e 100644 --- a/feather/server/config.toml +++ b/feather/server/config.toml @@ -50,3 +50,7 @@ proxy_mode = "none" # For Velocity, you must specify the forwarding-secret from Velocity's # velocity.toml file. velocity_secret = "" + +[cli] +completion_type = "list" +edit_mode = "emacs" diff --git a/feather/server/src/chunk_subscriptions.rs b/feather/server/src/chunk_subscriptions.rs index ccec7a780..2a87129aa 100644 --- a/feather/server/src/chunk_subscriptions.rs +++ b/feather/server/src/chunk_subscriptions.rs @@ -1,4 +1,5 @@ use ahash::AHashMap; + use base::ChunkPosition; use common::{ events::{EntityRemoveEvent, ViewUpdateEvent}, @@ -6,9 +7,10 @@ use common::{ Game, }; use ecs::{SysResult, SystemExecutor}; +use quill_common::components::ClientId; use utils::vec_remove_item; -use crate::{ClientId, Server}; +use crate::Server; /// Data structure to query which clients should /// receive updates from a given chunk, fast. diff --git a/feather/server/src/client.rs b/feather/server/src/client.rs index 20211f39b..f1db2adf1 100644 --- a/feather/server/src/client.rs +++ b/feather/server/src/client.rs @@ -13,14 +13,13 @@ use base::{ BlockId, ChunkHandle, ChunkPosition, EntityKind, EntityMetadata, Gamemode, Position, ProfileProperty, Text, ValidBlockPosition, }; -use common::{ - chat::{ChatKind, ChatMessage}, - Window, -}; +use common::world::WorldTime; +use feather_commands::{CommandCtx, CommandDispatcher}; use libcraft_items::InventorySlot; use packets::server::{Particle, SetSlot, SpawnLivingEntity, UpdateLight, WindowConfirmation}; use protocol::packets::server::{ - EntityPosition, EntityPositionAndRotation, EntityTeleport, HeldItemChange, PlayerAbilities, + ChangeGameState, DeclareCommands, EntityPosition, EntityPositionAndRotation, EntityTeleport, + HeldItemChange, PlayerAbilities, StateReason, TabComplete, TabCompleteMatch, TimeUpdate, }; use protocol::{ packets::{ @@ -34,23 +33,22 @@ use protocol::{ }, ClientPlayPacket, Nbt, ProtocolVersion, ServerPlayPacket, Writeable, }; -use quill_common::components::{OnGround, PreviousGamemode}; +use quill_common::components::{ + ChatKind, ChatMessage, ClientId, NetworkId, OnGround, PreviousGamemode, +}; use crate::{ entities::{PreviousOnGround, PreviousPosition}, initial_handler::NewPlayer, - network_id_registry::NetworkId, Options, }; +use common::Window; use slab::Slab; +use std::net::IpAddr; /// Max number of chunks to send to a client per tick. const MAX_CHUNKS_PER_TICK: usize = 10; -/// ID of a client. Can be reused. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct ClientId(usize); - /// Stores all `Client`s. #[derive(Default)] pub struct Clients { @@ -110,6 +108,9 @@ pub struct Client { client_known_position: Cell<Option<Position>>, disconnected: Cell<bool>, + + /// Player's real IP address + real_ip: IpAddr, } impl Client { @@ -129,6 +130,7 @@ impl Client { chunk_send_queue: RefCell::new(VecDeque::new()), client_known_position: Cell::new(None), disconnected: Cell::new(false), + real_ip: player.ip, } } @@ -156,6 +158,10 @@ impl Client { &self.username } + pub fn real_ip(&self) -> IpAddr { + self.real_ip + } + pub fn received_packets(&self) -> impl Iterator<Item = ClientPlayPacket> + '_ { self.received_packets.try_iter() } @@ -331,6 +337,10 @@ impl Client { self.send_packet(PlayerInfo::RemovePlayers(vec![uuid])); } + pub fn change_player_tablist_gamemode(&self, uuid: Uuid, gamemode: Gamemode) { + self.send_packet(PlayerInfo::UpdateGamemodes(vec![(uuid, gamemode)])); + } + pub fn unload_entity(&self, id: NetworkId) { log::trace!("Unloading {:?} on {}", id, self.username); self.sent_entities.borrow_mut().remove(&id); @@ -529,6 +539,21 @@ impl Client { self.send_packet(packet); } + pub fn send_slot_item(&self, window_id: u8, slot: i16, item: InventorySlot) { + log::trace!( + "Updating slot {} in window {} for {}", + slot, + window_id, + self.username + ); + let packet = SetSlot { + window_id, + slot, + slot_data: item, + }; + self.send_packet(packet); + } + pub fn set_slot(&self, slot: i16, item: &InventorySlot) { log::trace!("Setting slot {} of {} to {:?}", slot, self.username, item); self.send_packet(SetSlot { @@ -602,6 +627,47 @@ impl Client { self.send_packet(HeldItemChange { slot }); } + pub fn change_gamemode(&self, gamemode: Gamemode) { + self.send_packet(ChangeGameState { + reason: StateReason::ChangeGameMode, + value: gamemode as u8 as f32, + }) + } + + pub fn send_time(&self, time: &WorldTime) { + self.send_packet(TimeUpdate { + world_age: time.world_age(), + time_of_day: time.time(), + }) + } + + pub fn send_commands(&self, commands: &CommandDispatcher<CommandCtx, Text>) { + self.send_packet(DeclareCommands { + __todo__: commands.packet().unwrap(), + }) + } + + pub fn send_tab_completions( + &self, + transaction_id: i32, + completions: Vec<(String, Option<Text>)>, + start: usize, + length: usize, + ) { + self.send_packet(TabComplete { + id: transaction_id, + start: start as i32, + length: length as i32, + matches: completions + .into_iter() + .map(|(value, tooltip)| TabCompleteMatch { + value, + tooltip: tooltip.map(|t| t.to_string()), + }) + .collect(), + }) + } + fn register_entity(&self, network_id: NetworkId) { self.sent_entities.borrow_mut().insert(network_id); } @@ -610,10 +676,10 @@ impl Client { let _ = self.packets_to_send.try_send(packet.into()); } - pub fn disconnect(&self, reason: &str) { + pub fn disconnect(&self, reason: impl Into<Text>) { self.disconnected.set(true); self.send_packet(Disconnect { - reason: Text::from(reason.to_owned()).to_string(), + reason: Into::<Text>::into(reason).to_string(), }); } } diff --git a/feather/server/src/config.rs b/feather/server/src/config.rs index 9111a037f..7fca38e43 100644 --- a/feather/server/src/config.rs +++ b/feather/server/src/config.rs @@ -3,10 +3,13 @@ use std::{fs, net::IpAddr, path::Path, str::FromStr}; use anyhow::Context; -use base::Gamemode; +use rustyline::{CompletionType, EditMode}; use serde::{Deserialize, Deserializer}; -use crate::{favicon::Favicon, Options}; +use base::Gamemode; + +use crate::favicon::Favicon; +use crate::options::Options; const DEFAULT_CONFIG: &str = include_str!("../config.toml"); @@ -44,6 +47,7 @@ pub struct Config { pub log: Log, pub world: World, pub proxy: Proxy, + pub cli: Cli, } impl Config { @@ -72,6 +76,8 @@ impl Config { ProxyMode::Velocity => Some(crate::options::ProxyMode::Velocity), }, velocity_secret: self.proxy.velocity_secret.clone(), + cli_tab_completion_type: self.cli.completion_type, + cli_mode: self.cli.edit_mode, } } } @@ -119,6 +125,32 @@ pub enum ProxyMode { Velocity, } +#[derive(Debug, Deserialize)] +pub struct Cli { + #[serde(with = "CompletionTypeRef")] + pub completion_type: CompletionType, + #[serde(with = "EditModeRef")] + pub edit_mode: EditMode, +} + +#[non_exhaustive] +#[derive(Deserialize)] +#[serde(remote = "CompletionType", rename_all = "snake_case")] +pub enum CompletionTypeRef { + Circular, + List, + #[cfg(all(unix, feature = "rustyline/with-fuzzy"))] + Fuzzy, +} + +#[non_exhaustive] +#[derive(Deserialize)] +#[serde(remote = "EditMode", rename_all = "snake_case")] +pub enum EditModeRef { + Emacs, + Vi, +} + fn deserialize_log_level<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result<log::LevelFilter, D::Error> { diff --git a/feather/server/src/connection_worker.rs b/feather/server/src/connection_worker.rs index 2fe57e784..1d5fc5123 100644 --- a/feather/server/src/connection_worker.rs +++ b/feather/server/src/connection_worker.rs @@ -1,13 +1,8 @@ +use io::ErrorKind; use std::{fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration}; -use base::Text; use flume::{Receiver, Sender}; use futures_lite::FutureExt; -use io::ErrorKind; -use protocol::{ - codec::CryptKey, packets::server::Disconnect, ClientPlayPacket, MinecraftCodec, Readable, - ServerPlayPacket, Writeable, -}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{ @@ -17,12 +12,17 @@ use tokio::{ time::timeout, }; -use crate::{ - initial_handler::{InitialHandling, NewPlayer}, - options::Options, - player_count::PlayerCount, +use base::Text; +use protocol::{ + codec::CryptKey, packets::server::Disconnect, ClientPlayPacket, MinecraftCodec, Readable, + ServerPlayPacket, Writeable, }; +use crate::initial_handler::{InitialHandling, NewPlayer}; +use crate::options::Options; +use common::player_count::PlayerCount; +use std::net::IpAddr; + /// Tokio task which handles a connection and processes /// packets. /// @@ -39,6 +39,7 @@ pub struct Worker { packets_to_send_tx: Sender<ServerPlayPacket>, received_packets_rx: Receiver<ClientPlayPacket>, new_players: Sender<NewPlayer>, + ip: IpAddr, } impl Worker { @@ -49,6 +50,7 @@ impl Worker { player_count: PlayerCount, new_players: Sender<NewPlayer>, ) -> Self { + let ip = stream.peer_addr().unwrap().ip(); let (reader, writer) = stream.into_split(); let (received_packets_tx, received_packets_rx) = flume::bounded(32); @@ -64,6 +66,7 @@ impl Worker { packets_to_send_tx, received_packets_rx, new_players, + ip, } } @@ -159,6 +162,10 @@ impl Worker { pub fn received_packets(&self) -> Receiver<ClientPlayPacket> { self.received_packets_rx.clone() } + + pub fn ip(&self) -> &IpAddr { + &self.ip + } } struct Reader { diff --git a/feather/server/src/console_input.rs b/feather/server/src/console_input.rs new file mode 100644 index 000000000..dd7b6b170 --- /dev/null +++ b/feather/server/src/console_input.rs @@ -0,0 +1,343 @@ +use std::borrow::Cow; +use std::cell::RefCell; +use std::io::{ErrorKind, Write}; +use std::iter::FromIterator; +use std::sync::Arc; +use std::time::Duration; + +use commands::dispatcher::CommandDispatcher; +use commands::node::CommandNode; +use flume::{Receiver, Sender, TryIter}; +use parking_lot::Mutex; +use rustyline::completion::Completer; +use rustyline::config::Configurer; +use rustyline::error::ReadlineError; +use rustyline::highlight::Highlighter; +use rustyline::hint::Hinter; +use rustyline::validate::Validator; +use slab::Slab; + +use common::Game; +use ecs::{Entity, HasResources}; +use feather_commands::CommandCtx; +use libcraft_text::Text; +use rustyline::{CompletionType, Context, EditMode, Editor, Helper}; + +const PROMPT: &str = "\x1B[1m/\x1B[0m"; +const HISTORY_FILE: &str = ".feather_command_history"; + +pub struct ConsoleInput { + commands_receiver: Receiver<String>, + stdout: Receiver<u8>, + line: Arc<Mutex<String>>, + tab_completion_sender: Sender<(usize, Vec<String>)>, + tab_completion_receiver: Receiver<String>, + command_graph: Sender<Slab<CommandNode>>, +} + +impl ConsoleInput { + pub fn new<T>( + stdout: Receiver<u8>, + completion_type: CompletionType, + edit_mode: EditMode, + ) -> ConsoleInput { + let (command_sender, command_receiver) = flume::unbounded(); + + let (tab_sender, tab_receiver) = flume::bounded(1); + let (tab_sender_2, tab_receiver_2) = flume::bounded(1); + + let (command_graph_sender, command_graph_receiver) = flume::unbounded(); + + let line = Arc::new(Mutex::new(String::new())); + let line1 = line.clone(); + + tokio::spawn(async move { + let mut rl = Editor::<CommandHelper<T>>::new(); + if rl.load_history(HISTORY_FILE).is_err() { + log::info!("No previous console command history.") + } + rl.set_auto_add_history(true); + rl.set_completion_type(completion_type); + rl.set_edit_mode(edit_mode); + rl.set_helper(Some(CommandHelper { + tab_sender, + tab_receiver: tab_receiver_2, + command_graph: Default::default(), + command_graph_receiver, + line: line1.clone(), + })); + loop { + let s = rl.readline(PROMPT); + match s { + Ok(s) => { + *line1.lock() = String::new(); + command_sender.send(s).unwrap(); + rl.append_history(HISTORY_FILE).unwrap(); + } + Err(ReadlineError::Interrupted) => { + std::process::exit(0); + // TODO shutdown + } + _ => (), + }; + } + }); + + ConsoleInput { + commands_receiver: command_receiver, + stdout, + line, + tab_completion_sender: tab_sender_2, + tab_completion_receiver: tab_receiver, + command_graph: command_graph_sender, + } + } + pub fn try_iter(&self) -> TryIter<String> { + self.commands_receiver.try_iter() + } + pub fn flush_stdout(&self) { + flush_stdout(&self.stdout, &self.line.lock()) + } + pub fn tab_complete_if_needed(&self, game: &mut Game, console: Entity) { + while let Ok(line) = self.tab_completion_receiver.try_recv() { + if let Ok(dispatcher) = game + .resources() + .get::<CommandDispatcher<CommandCtx, Text>>() + { + let dispatcher = &*dispatcher; + let completions = feather_commands::tab_complete(dispatcher, game, console, &line); + if !completions.2.is_empty() { + self.tab_completion_sender + .send(( + completions.0, + completions + .2 + .into_iter() + .map(|(completion, _tooltip)| completion) + .collect(), + )) + .unwrap(); + return; + } + } + let _ = self.tab_completion_sender.try_send((0, vec![])); + } + } + + pub fn update_command_graph(&self, graph: &CommandDispatcher<CommandCtx, Text>) { + self.command_graph + .send(Slab::from_iter(graph.nodes().map(|(i, node)| { + ( + i, + match node { + CommandNode::Root { children } => CommandNode::Root { + children: children.clone(), + }, + CommandNode::Literal { + execute, + name, + children, + parent, + redirect, + fork, + } => CommandNode::Literal { + execute: *execute, + name: name.clone(), + children: children.clone(), + parent: *parent, + redirect: *redirect, + fork: *fork, + }, + CommandNode::Argument { + execute, + name, + suggestions_type, + parser, + children, + parent, + redirect, + fork, + } => CommandNode::Argument { + execute: *execute, + name: name.clone(), + suggestions_type: suggestions_type.clone(), + parser: parser.clone_boxed(), + children: children.clone(), + parent: *parent, + redirect: *redirect, + fork: *fork, + }, + }, + ) + }))) + .unwrap(); + } +} + +pub fn flush_stdout(queue: &Receiver<u8>, line: &str) { + let mut stdout = std::io::stdout(); + let mut wrote = false; + for message in queue.try_iter() { + if !wrote { + wrote = true; + stdout.write_all(b"\x1b[2K").unwrap(); // erase line + stdout.write_all(b"\x1b[1G").unwrap(); // move cursor to the beginning of the line + } + stdout.write_all(&[message]).unwrap(); + } + if wrote { + stdout.write_all(b"\x1b[1G").unwrap(); // move cursor to the beginning of the line + stdout.write_all(PROMPT.as_bytes()).unwrap(); + stdout.write_all(line.as_bytes()).unwrap(); + stdout.flush().unwrap(); + } +} + +struct CommandHelper<T> { + tab_sender: Sender<String>, + tab_receiver: Receiver<(usize, Vec<String>)>, + + /// a temporary copy of the server's command dispatcher (used for faster command highlighting) + command_graph: RefCell<CommandDispatcher<T, Text>>, + command_graph_receiver: Receiver<Slab<CommandNode>>, + + line: Arc<Mutex<String>>, +} + +impl<T> Validator for CommandHelper<T> {} + +impl<T> Highlighter for CommandHelper<T> { + fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { + const RESET: &str = "\x1B[0m"; + const BOLD: &str = "\x1B[1m"; + const RED: &str = "\x1B[31;1m"; + const ARGUMENT_COLORS: [&str; 5] = [ + "\x1B[34;1m", + "\x1B[33;1m", + "\x1B[32;1m", + "\x1B[35;1m", + "\x1B[33m", + ]; + + fn matches<'a>(node: &CommandNode, s: &'a str) -> (bool, &'a str) { + match node { + CommandNode::Root { .. } => unreachable!(), + CommandNode::Literal { name, .. } => { + if s == name { + (true, &s[s.len()..]) + } else if s.starts_with(&format!("{} ", name)) { + (true, &s[name.len() + 1..]) + } else { + (false, "") + } + } + CommandNode::Argument { parser, .. } => { + if let Some((i, _)) = parser.parse(s) { + if i == s.len() { + (true, &s[i..]) + } else { + (true, &s[i + 1..]) + } + } else { + (false, "") + } + } + } + } + + while let Ok(nodes) = self.command_graph_receiver.try_recv() { + *self.command_graph.borrow_mut() = CommandDispatcher::default(); + self.command_graph + .borrow_mut() + .add_nodes(nodes.into_iter().map(|(_, node)| node).collect()); + } + + let mut i = 0; + let mut result = String::new(); + + let commands = self.command_graph.borrow(); + let mut command = line; + let mut node = commands.nodes().next().unwrap().1; // root node is always first + 'next: loop { + let mut children = node.children().clone(); + match node { + CommandNode::Literal { + redirect: Some(redirect), + .. + } + | CommandNode::Argument { + redirect: Some(redirect), + .. + } => children.extend(commands.nodes().nth(*redirect).unwrap().1.children()), + _ => (), + } + for child in children { + let n = commands.nodes().nth(child).unwrap().1; + if let (true, s) = matches(n, command) { + if matches!(n, CommandNode::Argument { .. }) { + result += ARGUMENT_COLORS[i]; + result += &command[0..command.len() - s.len()]; + i += 1; + } else { + result += RESET; + result += BOLD; + result += &command[0..command.len() - s.len()]; + } + if !node.children().contains(&child) { + // if redirected, reset argument colors + for color in ARGUMENT_COLORS { + result = result.replace(color, ""); + } + } + node = commands.nodes().nth(child).unwrap().1; + command = s; + continue 'next; + } + } + if !command.is_empty() { + result += RED; + result += command; + } + break; + } + result += RESET; + + *self.line.lock() = result.clone(); + + Cow::Owned(result) + } + fn highlight_char(&self, _line: &str, _pos: usize) -> bool { + true + } +} + +impl<T> Hinter for CommandHelper<T> { + type Hint = String; +} + +impl<T> Completer for CommandHelper<T> { + type Candidate = String; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> { + let _ = self.tab_sender.try_send(line[..pos].to_string()); + let completions = self + .tab_receiver + .recv_timeout(Duration::from_secs(10)) + .map_err(|_| { + log::warn!("The server didn't respond for tab-completion request in 10 seconds"); + ReadlineError::Io(std::io::Error::new( + ErrorKind::TimedOut, + "The tab-completion request has timed out", + )) + }); + self.tab_receiver.drain(); + completions + } +} + +impl<T> Helper for CommandHelper<T> {} diff --git a/feather/server/src/entities.rs b/feather/server/src/entities.rs index 67015afda..1f5acde10 100644 --- a/feather/server/src/entities.rs +++ b/feather/server/src/entities.rs @@ -3,7 +3,8 @@ use ecs::{EntityBuilder, EntityRef, SysResult}; use quill_common::{components::OnGround, entity_init::EntityInit}; use uuid::Uuid; -use crate::{Client, NetworkId}; +use crate::client::Client; +use quill_common::components::NetworkId; /// Component that sends the spawn packet for an entity /// using its components. diff --git a/feather/server/src/initial_handler.rs b/feather/server/src/initial_handler.rs index 4266d5884..b02c4b8b8 100644 --- a/feather/server/src/initial_handler.rs +++ b/feather/server/src/initial_handler.rs @@ -1,12 +1,19 @@ //! Initial handling of a connection. -use crate::{connection_worker::Worker, favicon::Favicon}; +use std::convert::TryInto; + use anyhow::bail; -use base::{ProfileProperty, Text}; use flume::{Receiver, Sender}; use md5::Digest; use num_bigint::BigInt; use once_cell::sync::Lazy; +use rand::rngs::OsRng; +use rsa::{PaddingScheme, PublicKeyParts, RSAPrivateKey}; +use serde::{Deserialize, Serialize}; +use sha1::Sha1; +use uuid::Uuid; + +use base::{ProfileProperty, Text}; use protocol::{ codec::CryptKey, packets::{ @@ -18,14 +25,12 @@ use protocol::{ ClientHandshakePacket, ClientLoginPacket, ClientPlayPacket, ClientStatusPacket, ServerLoginPacket, ServerPlayPacket, ServerStatusPacket, }; -use rand::rngs::OsRng; -use rsa::{PaddingScheme, PublicKeyParts, RSAPrivateKey}; -use serde::{Deserialize, Serialize}; -use sha1::Sha1; -use std::convert::TryInto; -use uuid::Uuid; + +use crate::connection_worker::Worker; use self::proxy::ProxyData; +use crate::favicon::Favicon; +use std::net::IpAddr; const SERVER_NAME: &str = "Feather 1.16.5"; const PROTOCOL_VERSION: i32 = 754; @@ -38,6 +43,7 @@ pub struct NewPlayer { pub uuid: Uuid, pub username: String, pub profile: Vec<ProfileProperty>, + pub ip: IpAddr, pub received_packets: Receiver<ClientPlayPacket>, pub packets_to_send: Sender<ServerPlayPacket>, @@ -166,15 +172,19 @@ async fn handle_login( if worker.options().online_mode { enable_encryption(worker, login_start.name).await } else { + let mut ip = *worker.ip(); let profile = match proxy_data { - Some(proxy_data) => AuthResponse { - id: proxy_data.uuid, - name: login_start.name.clone(), - properties: proxy_data.profile, - }, + Some(proxy_data) => { + ip = proxy_data.client.parse()?; + AuthResponse { + id: proxy_data.uuid, + name: login_start.name.clone(), + properties: proxy_data.profile, + } + } None => offline_mode_profile(login_start.name), }; - finish_login(worker, profile).await + finish_login(worker, profile, ip).await } } @@ -221,7 +231,7 @@ async fn enable_encryption( let response = authenticate(shared_secret, username).await?; - finish_login(worker, response).await + finish_login(worker, response, *worker.ip()).await } async fn do_encryption_handshake(worker: &mut Worker) -> anyhow::Result<CryptKey> { @@ -293,6 +303,7 @@ fn hexdigest(bytes: &[u8]) -> String { async fn finish_login( worker: &mut Worker, response: AuthResponse, + real_ip: IpAddr, ) -> anyhow::Result<InitialHandling> { enable_compression(worker).await?; @@ -308,6 +319,7 @@ async fn finish_login( username: response.name, uuid: response.id, profile: response.properties, + ip: real_ip, received_packets: worker.received_packets(), packets_to_send: worker.packets_to_send(), }; diff --git a/feather/server/src/lib.rs b/feather/server/src/lib.rs index c1aec55dc..86e680d5c 100644 --- a/feather/server/src/lib.rs +++ b/feather/server/src/lib.rs @@ -2,34 +2,34 @@ use std::{sync::Arc, time::Instant}; +use flume::Receiver; + use base::Position; use chunk_subscriptions::ChunkSubscriptions; +pub use client::{Client, Clients}; +use common::player_count::PlayerCount; use common::Game; use ecs::SystemExecutor; -use flume::Receiver; use initial_handler::NewPlayer; -use listener::Listener; +pub use options::Options; +use quill_common::components::ClientId; +use systems::view::WaitingChunks; + +use crate::listener::Listener; mod chunk_subscriptions; pub mod client; pub mod config; mod connection_worker; +pub mod console_input; mod entities; pub mod favicon; mod initial_handler; -mod listener; -mod network_id_registry; -mod options; +pub mod listener; +pub mod options; mod packet_handlers; -mod player_count; mod systems; -pub use client::{Client, ClientId, Clients}; -pub use network_id_registry::NetworkId; -pub use options::Options; -use player_count::PlayerCount; -use systems::view::WaitingChunks; - /// A Minecraft server. /// /// Call [`link_with_game`](Server::link_with_game) to register the server @@ -54,7 +54,7 @@ impl Server { /// Starts a server with the given `Options`. /// /// Must be called within the context of a Tokio runtime. - pub async fn bind(options: Options) -> anyhow::Result<Self> { + pub async fn bind(options: Options) -> anyhow::Result<Server> { let options = Arc::new(options); let player_count = PlayerCount::new(options.max_players); diff --git a/feather/server/src/listener.rs b/feather/server/src/listener.rs index 6e8cddbd2..846c04383 100644 --- a/feather/server/src/listener.rs +++ b/feather/server/src/listener.rs @@ -1,14 +1,13 @@ use std::{net::SocketAddr, sync::Arc}; +use crate::connection_worker::Worker; +use crate::initial_handler::NewPlayer; +use crate::options::Options; use anyhow::Context; +use common::player_count::PlayerCount; use flume::Sender; use tokio::net::{TcpListener, TcpStream}; -use crate::{ - connection_worker::Worker, initial_handler::NewPlayer, options::Options, - player_count::PlayerCount, -}; - /// Listens for and accepts incoming connections. pub struct Listener { listener: TcpListener, diff --git a/feather/server/src/logging.rs b/feather/server/src/logging.rs index cef6c415e..ff3b24a35 100644 --- a/feather/server/src/logging.rs +++ b/feather/server/src/logging.rs @@ -1,7 +1,11 @@ +use std::io::{ErrorKind, Write}; + use colored::Colorize; +use fern::Output; +use flume::Sender; use log::{Level, LevelFilter}; -pub fn init(level: LevelFilter) { +pub fn init(level: LevelFilter, stdout: Sender<u8>) { fern::Dispatch::new() .format(|out, message, record| { let level_string = match record.level() { @@ -28,7 +32,31 @@ pub fn init(level: LevelFilter) { // cranelift_codegen spams debug-level logs .level_for("cranelift_codegen", LevelFilter::Info) .level_for("regalloc", LevelFilter::Off) - .chain(std::io::stdout()) + .level_for("rustyline", LevelFilter::Off) + .chain(FlumeSenderLogger(stdout)) .apply() .unwrap(); } + +struct FlumeSenderLogger(Sender<u8>); + +impl From<FlumeSenderLogger> for Output { + fn from(f: FlumeSenderLogger) -> Self { + Output::writer(Box::new(f), "\n") + } +} + +impl Write for FlumeSenderLogger { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + for b in buf { + self.0 + .send(*b) + .map_err(|e| std::io::Error::new(ErrorKind::BrokenPipe, e.to_string()))?; + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/feather/server/src/main.rs b/feather/server/src/main.rs index 615857028..176177d48 100644 --- a/feather/server/src/main.rs +++ b/feather/server/src/main.rs @@ -1,11 +1,13 @@ use std::{cell::RefCell, rc::Rc, sync::Arc}; -use anyhow::Context; use base::anvil::level::SuperflatGeneratorOptions; use common::{Game, TickLoop, World}; -use ecs::SystemExecutor; +use ecs::{HasResources, SystemExecutor}; +use feather_commands::{CommandCtx, CommandDispatcher}; +use feather_server::console_input::{flush_stdout, ConsoleInput}; use feather_server::{config::Config, Server}; use plugin_host::PluginManager; +use quill_common::components::DefaultGamemode; use worldgen::{ComposableGenerator, SuperflatWorldGenerator, WorldGenerator}; mod logging; @@ -18,20 +20,41 @@ async fn main() -> anyhow::Result<()> { let feather_server::config::ConfigContainer { config, was_config_created, - } = feather_server::config::load(CONFIG_PATH).context("failed to load configuration file")?; - logging::init(config.log.level); + } = feather_server::config::load(CONFIG_PATH).expect("failed to load configuration file"); + + let (stdout_tx, stdout_rx) = flume::unbounded(); + logging::init(config.log.level, stdout_tx); if was_config_created { log::info!("Created default config"); } log::info!("Loaded config"); log::info!("Creating server"); + flush_stdout(&stdout_rx, ""); let options = config.to_options(); - let server = Server::bind(options).await?; - - let game = init_game(server, &config)?; - - run(game); + match Server::bind(options).await { + Ok(server) => match init_game(server, &config) { + Ok(mut game) => { + let console_input = ConsoleInput::new::<CommandCtx>( + stdout_rx, + config.cli.completion_type, + config.cli.edit_mode, + ); + game.insert_resource(console_input); + run(game); + } + Err(err) => { + log::error!("{}", err); + flush_stdout(&stdout_rx, ""); + std::process::exit(1); + } + }, + Err(err) => { + log::error!("{}", err); + flush_stdout(&stdout_rx, ""); + std::process::exit(1); + } + } Ok(()) } @@ -40,7 +63,9 @@ fn init_game(server: Server, config: &Config) -> anyhow::Result<Game> { let mut game = Game::new(); init_systems(&mut game, server); init_world_source(&mut game, config); + init_commands(&mut game); init_plugin_manager(&mut game)?; + init_config(&mut game, config); Ok(game) } @@ -55,16 +80,27 @@ fn init_systems(game: &mut Game, server: Server) { print_systems(&systems); - game.system_executor = Rc::new(RefCell::new(systems)); + game.insert_resource(systems); } fn init_world_source(game: &mut Game, config: &Config) { // Load chunks from the world save first, - // and fall back to generating a superflat - // world otherwise. This is a placeholder: - // we don't have proper world generation yet. - - let seed = 42; // FIXME: load from the level file + // and fall back to generating a world otherwise. + + // FIXME: load from the level file if exists + let seed = config.world.seed.parse().unwrap_or_else(|_| { + if config.world.seed.is_empty() { + rand::random() + } else { + // Java String#hashCode + config + .world + .seed + .chars() + .fold(0i32, |val, ch| val.wrapping_mul(31).wrapping_add(ch as i32)) + as i64 + } + }); let generator: Arc<dyn WorldGenerator> = match &config.world.generator[..] { "flat" => Arc::new(SuperflatWorldGenerator::new( @@ -72,7 +108,13 @@ fn init_world_source(game: &mut Game, config: &Config) { )), _ => Arc::new(ComposableGenerator::default_with_seed(seed)), }; - game.world = World::with_gen_and_path(generator, config.world.name.clone()); + game.world = World::new(generator, config.world.name.clone(), seed); +} + +fn init_commands(game: &mut Game) { + let mut dispatcher = CommandDispatcher::new(); + feather_commands::register_vanilla_commands(&mut dispatcher); + game.insert_resource(dispatcher); } fn init_plugin_manager(game: &mut Game) -> anyhow::Result<()> { @@ -84,6 +126,10 @@ fn init_plugin_manager(game: &mut Game) -> anyhow::Result<()> { Ok(()) } +fn init_config(game: &mut Game, config: &Config) { + game.insert_resource(DefaultGamemode::new(config.server.default_gamemode)); +} + fn print_systems(systems: &SystemExecutor<Game>) { let systems: Vec<&str> = systems.system_names().collect(); log::debug!("---SYSTEMS---\n{:#?}\n", systems); @@ -97,8 +143,10 @@ fn run(game: Game) { fn create_tick_loop(mut game: Game) -> TickLoop { TickLoop::new(move || { - let systems = Rc::clone(&game.system_executor); - systems.borrow_mut().run(&mut game); + game.resources() + .get_mut::<SystemExecutor<Game>>() + .unwrap() + .run(&mut game); game.tick_count += 1; false diff --git a/feather/server/src/network_id_registry.rs b/feather/server/src/network_id_registry.rs deleted file mode 100644 index 117c293a0..000000000 --- a/feather/server/src/network_id_registry.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::sync::atomic::{AtomicI32, Ordering}; - -/// An entity's ID used by the protocol -/// in `entity_id` fields. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct NetworkId(pub i32); - -impl NetworkId { - /// Creates a new, unique network ID. - pub(crate) fn new() -> Self { - static NEXT: AtomicI32 = AtomicI32::new(0); - // In theory, this can overflow if the server - // creates 4 billion entities. The hope is that - // old entities will have died out at that point. - Self(NEXT.fetch_add(1, Ordering::SeqCst)) - } -} diff --git a/feather/server/src/options.rs b/feather/server/src/options.rs index f75fc203f..c6f4234f0 100644 --- a/feather/server/src/options.rs +++ b/feather/server/src/options.rs @@ -1,6 +1,6 @@ -use base::Gamemode; - use crate::favicon::Favicon; +use base::Gamemode; +use rustyline::{CompletionType, EditMode}; /// Options for building a [`Server`](crate::Server). #[derive(Debug, Clone)] @@ -35,6 +35,9 @@ pub struct Options { /// Packet size threshold at which to compress data pub compression_threshold: Option<usize>, + + pub cli_tab_completion_type: CompletionType, + pub cli_mode: EditMode, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/feather/server/src/packet_handlers.rs b/feather/server/src/packet_handlers.rs index b0452c96a..d9c6798e9 100644 --- a/feather/server/src/packet_handlers.rs +++ b/feather/server/src/packet_handlers.rs @@ -1,6 +1,7 @@ use base::{Position, Text}; -use common::{chat::ChatKind, Game}; -use ecs::{Entity, EntityRef, SysResult}; +use common::Game; +use ecs::{Entity, EntityRef, HasResources, SysResult}; +use feather_commands::{CommandCtx, CommandDispatcher}; use interaction::{ handle_held_item_change, handle_interact_entity, handle_player_block_placement, handle_player_digging, @@ -12,9 +13,10 @@ use protocol::{ }, ClientPlayPacket, }; -use quill_common::components::Name; +use quill_common::components::{ChatKind, ClientId, Name, NetworkId}; +use quill_common::events::PluginMessageReceiveEvent; -use crate::{NetworkId, Server}; +use crate::Server; mod entity_action; mod interaction; @@ -45,7 +47,10 @@ pub fn handle_packet( ClientPlayPacket::Animation(packet) => handle_animation(server, player, packet), - ClientPlayPacket::ChatMessage(packet) => handle_chat_message(game, player, packet), + ClientPlayPacket::ChatMessage(packet) => { + drop(player); + handle_chat_message(game, player_id, packet) + } ClientPlayPacket::PlayerDigging(packet) => { handle_player_digging(game, server, packet, player_id) @@ -77,15 +82,19 @@ pub fn handle_packet( entity_action::handle_entity_action(game, player_id, packet) } + ClientPlayPacket::TabComplete(packet) => { + handle_tab_complete(game, server, player_id, packet) + } + + ClientPlayPacket::PluginMessage(packet) => handle_plugin_message(game, player_id, packet), + ClientPlayPacket::TeleportConfirm(_) | ClientPlayPacket::QueryBlockNbt(_) | ClientPlayPacket::SetDifficulty(_) | ClientPlayPacket::ClientStatus(_) - | ClientPlayPacket::TabComplete(_) | ClientPlayPacket::WindowConfirmation(_) | ClientPlayPacket::ClickWindowButton(_) | ClientPlayPacket::CloseWindow(_) - | ClientPlayPacket::PluginMessage(_) | ClientPlayPacket::EditBook(_) | ClientPlayPacket::QueryEntityNbt(_) | ClientPlayPacket::GenerateStructure(_) @@ -132,10 +141,29 @@ fn handle_animation( Ok(()) } -fn handle_chat_message(game: &Game, player: EntityRef, packet: client::ChatMessage) -> SysResult { - let name = player.get::<Name>()?; - let message = Text::translate_with("chat.type.text", vec![name.to_string(), packet.message]); - game.broadcast_chat(ChatKind::PlayerChat, message); +fn handle_chat_message( + game: &mut Game, + player_id: Entity, + packet: client::ChatMessage, +) -> SysResult { + if packet.message.starts_with('/') { + let _result = feather_commands::dispatch_command( + &mut *game + .resources() + .get_mut::<CommandDispatcher<CommandCtx, Text>>() + .unwrap(), + game, + player_id, + &packet.message[1..], + true, + ); + } else { + let player = game.ecs.entity(player_id)?; + let name = player.get::<Name>()?; + let message = + Text::translate_with("chat.type.text", vec![name.to_string(), packet.message]); + game.broadcast_chat(ChatKind::PlayerChat, message); + } Ok(()) } @@ -150,3 +178,47 @@ fn handle_client_settings( }); Ok(()) } + +fn handle_tab_complete( + game: &mut Game, + server: &Server, + player_id: Entity, + packet: client::TabComplete, +) -> SysResult { + let completions = feather_commands::tab_complete( + &*game + .resources() + .get::<CommandDispatcher<CommandCtx, Text>>() + .unwrap(), + game, + player_id, + &packet.text[1..], + ); + let player = game.ecs.entity(player_id).unwrap(); + server + .clients + .get(*player.get::<ClientId>().unwrap()) + .unwrap() + .send_tab_completions( + packet.transaction_id, + completions.2, + completions.0 + 1, // with "/" symbol + completions.1, + ); + Ok(()) +} + +fn handle_plugin_message( + game: &mut Game, + player_id: Entity, + packet: client::PluginMessage, +) -> SysResult { + game.ecs.insert_entity_event( + player_id, + PluginMessageReceiveEvent { + channel: packet.channel, + data: packet.data, + }, + )?; + Ok(()) +} diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index 1c6f69d9c..7ad894148 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -1,4 +1,4 @@ -use crate::{ClientId, NetworkId, Server}; +use crate::Server; use base::inventory::{SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}; use common::entities::player::HotbarSlot; use common::interactable::InteractableRegistry; @@ -10,10 +10,12 @@ use protocol::packets::client::{ BlockFace, HeldItemChange, InteractEntity, InteractEntityKind, PlayerBlockPlacement, PlayerDigging, PlayerDiggingStatus, }; +use quill_common::components::{ClientId, NetworkId}; use quill_common::{ events::{BlockInteractEvent, BlockPlacementEvent, InteractEntityEvent}, EntityId, }; + /// Handles the player block placement packet. Currently just removes the block client side for the player. pub fn handle_player_block_placement( game: &mut Game, @@ -119,7 +121,7 @@ pub fn handle_player_digging( ) -> SysResult { log::trace!("Got player digging with status {:?}", packet.status); match packet.status { - PlayerDiggingStatus::StartDigging | PlayerDiggingStatus::CancelDigging => { + PlayerDiggingStatus::FinishDigging => { game.break_block(packet.position); Ok(()) } diff --git a/feather/server/src/packet_handlers/inventory.rs b/feather/server/src/packet_handlers/inventory.rs index 48a397197..733e473ba 100644 --- a/feather/server/src/packet_handlers/inventory.rs +++ b/feather/server/src/packet_handlers/inventory.rs @@ -4,7 +4,8 @@ use common::{window::BackingWindow, Window}; use ecs::{EntityRef, SysResult}; use protocol::packets::client::{ClickWindow, CreativeInventoryAction}; -use crate::{ClientId, Server}; +use crate::Server; +use quill_common::components::ClientId; pub fn handle_creative_inventory_action( player: EntityRef, diff --git a/feather/server/src/packet_handlers/movement.rs b/feather/server/src/packet_handlers/movement.rs index 27b03ee23..8883f90d3 100644 --- a/feather/server/src/packet_handlers/movement.rs +++ b/feather/server/src/packet_handlers/movement.rs @@ -9,7 +9,8 @@ use quill_common::{ events::CreativeFlyingEvent, }; -use crate::{ClientId, Server}; +use crate::Server; +use quill_common::components::ClientId; /// If a player has been teleported by the server, /// we don't want to override their position if diff --git a/feather/server/src/systems.rs b/feather/server/src/systems.rs index ee7e85b37..574fce675 100644 --- a/feather/server/src/systems.rs +++ b/feather/server/src/systems.rs @@ -1,23 +1,25 @@ //! Systems linking a `Server` and a `Game`. +use std::time::{Duration, Instant}; + +use crate::Server; +use common::Game; +use ecs::{SysResult, SystemExecutor}; +use quill_common::components::{ClientId, Name}; + mod block; mod chat; mod entity; +mod gamemode; +mod inventory; mod particle; mod player_join; mod player_leave; mod plugin_message; mod tablist; +mod time; pub mod view; -use std::time::{Duration, Instant}; - -use common::Game; -use ecs::{SysResult, SystemExecutor}; -use quill_common::components::Name; - -use crate::{client::ClientId, Server}; - /// Registers systems for a `Server` with a `Game`. pub fn register(server: Server, game: &mut Game, systems: &mut SystemExecutor<Game>) { game.insert_resource(server); @@ -36,6 +38,9 @@ pub fn register(server: Server, game: &mut Game, systems: &mut SystemExecutor<Ga chat::register(game, systems); particle::register(systems); plugin_message::register(systems); + gamemode::register(systems); + time::register(systems); + inventory::register(systems); systems.group::<Server>().add_system(tick_clients); } diff --git a/feather/server/src/systems/block.rs b/feather/server/src/systems/block.rs index 5544da2bd..651f307d9 100644 --- a/feather/server/src/systems/block.rs +++ b/feather/server/src/systems/block.rs @@ -13,13 +13,12 @@ //! like WorldEdit. This module chooses the optimal packet from //! the above three options to achieve ideal performance. +use crate::Server; use ahash::AHashMap; use base::{chunk::SECTION_VOLUME, position, ChunkPosition, CHUNK_WIDTH}; use common::{events::BlockChangeEvent, Game}; use ecs::{SysResult, SystemExecutor}; -use crate::Server; - pub fn register(systems: &mut SystemExecutor<Game>) { systems .group::<Server>() diff --git a/feather/server/src/systems/chat.rs b/feather/server/src/systems/chat.rs index d19aa64ed..ae1f97c22 100644 --- a/feather/server/src/systems/chat.rs +++ b/feather/server/src/systems/chat.rs @@ -1,23 +1,36 @@ -use common::{chat::ChatPreference, ChatBox, Game}; -use ecs::{EntityBuilder, SysResult, SystemExecutor}; +use commands::arguments::EntityArgument; -use crate::{ClientId, Server}; +use common::Game; +use ecs::{EntityBuilder, HasResources, SysResult, SystemExecutor}; +use feather_commands::{CommandCtx, CommandDispatcher}; +use libcraft_text::text::{Color, IntoTextComponent, Style}; +use libcraft_text::Text; +use quill_common::components::{ChatBox, ChatPreference, ClientId, Console, Name}; -/// Marker component for the console entity. -struct Console; +use crate::console_input::ConsoleInput; +use crate::Server; pub fn register(game: &mut Game, systems: &mut SystemExecutor<Game>) { // Create the console entity so the console can receive messages let mut console = EntityBuilder::new(); - console.add(Console).add(ChatBox::new(ChatPreference::All)); + console + .add(Console) + .add(Name::new("Console")) + .add(ChatBox::new(ChatPreference::All)); // We can use the raw spawn method because // the console isn't a "normal" entity. game.ecs.spawn(console.build()); - systems.add_system(flush_console_chat_box); - systems.group::<Server>().add_system(flush_chat_boxes); - systems.group::<Server>().add_system(flush_title_chat_boxes); + systems + .add_system(flush_console_chat_box) + .add_system(flush_stdout) + .add_system(flush_console_commands) + .add_system(tab_complete_console) + .add_system(send_command_graph_to_console) + .group::<Server>() + .add_system(flush_chat_boxes) + .add_system(flush_title_chat_boxes); } /// Flushes players' chat mailboxes and sends the needed packets. @@ -35,16 +48,82 @@ fn flush_chat_boxes(game: &mut Game, server: &mut Server) -> SysResult { /// Prints chat messages to the console. fn flush_console_chat_box(game: &mut Game) -> SysResult { - for (_, (_console, mailbox)) in game.ecs.query::<(&Console, &mut ChatBox)>().iter() { + for (console, (_, mailbox)) in game.ecs.query::<(&Console, &mut ChatBox)>().iter() { for message in mailbox.drain() { - // TODO: properly display chat message - log::info!("{:?}", message.text()); + // TODO: translate components + log::info!( + "{}", + message.text().clone().into_component().to_string( + &|translate, with| format!("{}{:?}", String::from(translate), with), + &|color| match color { + Color::DarkRed => "\x1B[31m".to_string(), + Color::Red => "\x1B[31;1m".to_string(), + Color::Gold => "\x1B[33m".to_string(), + Color::Yellow => "\x1B[33;1m".to_string(), + Color::DarkGreen => "\x1B[32m".to_string(), + Color::Green => "\x1B[32;1m".to_string(), + Color::Aqua => "\x1B[36;1m".to_string(), + Color::DarkAqua => "\x1B[36m".to_string(), + Color::DarkBlue => "\x1B[34m".to_string(), + Color::Blue => "\x1B[34;1m".to_string(), + Color::LightPurple => "\x1B[35;1m".to_string(), + Color::DarkPurple => "\x1B[35m".to_string(), + Color::White => "\x1B[37;1m".to_string(), + Color::Gray => "\x1B[37;1m".to_string(), + Color::DarkGray => "\x1B[30m".to_string(), + Color::Black => "\x1B[30m".to_string(), + Color::Custom(rgb) => format!( + "\033[38;2;{};{};{}m", + u8::from_str_radix(&rgb[1..3], 16).unwrap(), + u8::from_str_radix(&rgb[3..5], 16).unwrap(), + u8::from_str_radix(&rgb[5..7], 16).unwrap() + ), + }, + &|style| match style { + Style::Bold => "\x1B[1m".to_string(), + Style::Italic => "\x1B[3m".to_string(), + Style::Underlined => "\x1B[4m".to_string(), + Style::Strikethrough => "\x1B[9m".to_string(), + Style::Obfuscated => "".to_string(), + }, + &|selector| { + let s = EntityArgument::ENTITIES.parse(selector, false); + if let Some((_, s)) = s { + feather_commands::utils::get_entity_names(console, game, &s).join(", ") + // TODO gray commas + } else { + String::new() + } + }, + "\x1B[0m" + ) + ); } } Ok(()) } +/// Executes commands from console +fn flush_console_commands(game: &mut Game) -> SysResult { + for command in game.resources().get::<ConsoleInput>().unwrap().try_iter() { + log::debug!("Console command: {}", command); + let console = game.ecs.query::<&Console>().iter().next().unwrap().0; + let _result = feather_commands::dispatch_command( + &mut *game + .resources() + .get_mut::<CommandDispatcher<CommandCtx, Text>>() + .unwrap(), + game, + console, + &command, + true, + ); + } + + Ok(()) +} + fn flush_title_chat_boxes(game: &mut Game, server: &mut Server) -> SysResult { for (_, (&client_id, mailbox)) in game.ecs.query::<(&ClientId, &mut ChatBox)>().iter() { if let Some(client) = server.clients.get(client_id) { @@ -56,3 +135,34 @@ fn flush_title_chat_boxes(game: &mut Game, server: &mut Server) -> SysResult { Ok(()) } + +fn flush_stdout(game: &mut Game) -> SysResult { + game.resources.get::<ConsoleInput>().unwrap().flush_stdout(); + Ok(()) +} + +/// Prints chat messages to the console. +fn tab_complete_console(game: &mut Game) -> SysResult { + let console = game.ecs.query::<&Console>().iter().next().unwrap().0; + game.resources() + .get::<ConsoleInput>() + .unwrap() + .tab_complete_if_needed(game, console); + + Ok(()) +} + +fn send_command_graph_to_console(game: &mut Game) -> SysResult { + if game.tick_count % (20 * 60) == 1 { + game.resources() + .get::<ConsoleInput>() + .unwrap() + .update_command_graph( + &*game + .resources() + .get::<CommandDispatcher<CommandCtx, Text>>() + .unwrap(), + ) + } + Ok(()) +} diff --git a/feather/server/src/systems/entity.rs b/feather/server/src/systems/entity.rs index 49aed542f..4a96b4347 100644 --- a/feather/server/src/systems/entity.rs +++ b/feather/server/src/systems/entity.rs @@ -7,15 +7,17 @@ use base::{ }; use common::Game; use ecs::{SysResult, SystemExecutor}; + use quill_common::{ - components::{OnGround, Sprinting}, + components::Sprinting, events::{SneakEvent, SprintEvent}, }; use crate::{ entities::{PreviousOnGround, PreviousPosition}, - NetworkId, Server, + Server, }; +use quill_common::components::{NetworkId, OnGround}; mod spawn_packet; diff --git a/feather/server/src/systems/entity/spawn_packet.rs b/feather/server/src/systems/entity/spawn_packet.rs index fcb4ea03a..9c57e44a9 100644 --- a/feather/server/src/systems/entity/spawn_packet.rs +++ b/feather/server/src/systems/entity/spawn_packet.rs @@ -7,7 +7,9 @@ use common::{ }; use ecs::{SysResult, SystemExecutor}; -use crate::{entities::SpawnPacketSender, ClientId, NetworkId, Server}; +use crate::entities::SpawnPacketSender; +use crate::Server; +use quill_common::components::{ClientId, NetworkId}; pub fn register(_game: &mut Game, systems: &mut SystemExecutor<Game>) { systems diff --git a/feather/server/src/systems/gamemode.rs b/feather/server/src/systems/gamemode.rs new file mode 100644 index 000000000..5a58481d5 --- /dev/null +++ b/feather/server/src/systems/gamemode.rs @@ -0,0 +1,217 @@ +use std::collections::HashMap; + +use base::Gamemode; +use common::Game; +use ecs::{SysResult, SystemExecutor}; +use quill_common::components::{ + CanBuild, CanCreativeFly, ClientId, CreativeFlying, CreativeFlyingSpeed, Instabreak, + Invulnerable, WalkSpeed, +}; +use quill_common::events::{ + BuildingAbilityChangeEvent, CreativeFlyingEvent, FlyingAbilityChangeEvent, GamemodeUpdateEvent, + InstabreakChangeEvent, InvulnerabilityChangeEvent, +}; + +use crate::Server; +use base::anvil::player::PlayerAbilities; + +pub fn register(systems: &mut SystemExecutor<Game>) { + systems.group::<Server>().add_system(gamemode_change); +} + +fn gamemode_change(game: &mut Game, server: &mut Server) -> SysResult { + let mut may_fly_changes = HashMap::new(); + let mut fly_changes = HashMap::new(); + let mut instabreak_changes = HashMap::new(); + let mut build_changes = HashMap::new(); + let mut invulnerability_changes = HashMap::new(); + for ( + entity, + ( + event, + &client_id, + &walk_speed, + &fly_speed, + mut may_fly, + mut is_flying, + mut instabreak, + mut may_build, + mut invulnerable, + ), + ) in game + .ecs + .query::<( + &GamemodeUpdateEvent, + &ClientId, + &WalkSpeed, + &CreativeFlyingSpeed, + &mut CanCreativeFly, + &mut CreativeFlying, + &mut Instabreak, + &mut CanBuild, + &mut Invulnerable, + )>() + .iter() + { + match event.new { + Gamemode::Creative => { + if !**instabreak { + instabreak_changes.insert(entity, true); + instabreak.0 = true; + } + if !**may_fly { + may_fly_changes.insert(entity, true); + may_fly.0 = true; + } + if !**may_build { + build_changes.insert(entity, true); + may_build.0 = true; + } + if !**invulnerable { + invulnerability_changes.insert(entity, true); + invulnerable.0 = true; + } + } + Gamemode::Spectator => { + if !**is_flying { + fly_changes.insert(entity, true); + is_flying.0 = true; + } + if **instabreak { + instabreak_changes.insert(entity, false); + instabreak.0 = false; + } + if !**may_fly { + may_fly_changes.insert(entity, true); + may_fly.0 = true; + } + if **may_build { + build_changes.insert(entity, false); + may_build.0 = false; + } + if !**invulnerable { + invulnerability_changes.insert(entity, true); + invulnerable.0 = true; + } + } + Gamemode::Survival => { + if **is_flying { + fly_changes.insert(entity, false); + is_flying.0 = false; + } + if **instabreak { + instabreak_changes.insert(entity, false); + instabreak.0 = false; + } + if **may_fly { + may_fly_changes.insert(entity, false); + may_fly.0 = false; + } + if !**may_build { + build_changes.insert(entity, true); + may_build.0 = true; + } + if **invulnerable { + invulnerability_changes.insert(entity, false); + invulnerable.0 = false; + } + } + Gamemode::Adventure => { + if **is_flying { + fly_changes.insert(entity, false); + is_flying.0 = false; + } + if **instabreak { + instabreak_changes.insert(entity, false); + instabreak.0 = false; + } + if **may_fly { + may_fly_changes.insert(entity, false); + may_fly.0 = false; + } + if **may_build { + build_changes.insert(entity, false); + may_build.0 = false; + } + if **invulnerable { + invulnerability_changes.insert(entity, false); + invulnerable.0 = false; + } + } + } + server + .clients + .get(client_id) + .unwrap() + .change_gamemode(event.new); + server + .clients + .get(client_id) + .unwrap() + .send_abilities(&PlayerAbilities { + walk_speed, + fly_speed, + may_fly: *may_fly, + is_flying: *is_flying, + may_build: *may_build, + instabreak: *instabreak, + invulnerable: *invulnerable, + }); + } + for (entity, flying) in fly_changes { + if flying { + game.ecs + .insert_entity_event(entity, CreativeFlyingEvent::new(true)) + .unwrap(); + } else { + game.ecs + .insert_entity_event(entity, CreativeFlyingEvent::new(false)) + .unwrap(); + } + } + for (entity, instabreak) in instabreak_changes { + if instabreak { + game.ecs + .insert_entity_event(entity, InstabreakChangeEvent(true)) + .unwrap(); + } else { + game.ecs + .insert_entity_event(entity, InstabreakChangeEvent(false)) + .unwrap(); + } + } + for (entity, may_fly) in may_fly_changes { + if may_fly { + game.ecs + .insert_entity_event(entity, FlyingAbilityChangeEvent(true)) + .unwrap(); + } else { + game.ecs + .insert_entity_event(entity, FlyingAbilityChangeEvent(false)) + .unwrap(); + } + } + for (entity, build) in build_changes { + if build { + game.ecs + .insert_entity_event(entity, BuildingAbilityChangeEvent(true)) + .unwrap(); + } else { + game.ecs + .insert_entity_event(entity, BuildingAbilityChangeEvent(false)) + .unwrap(); + } + } + for (entity, invulnerable) in invulnerability_changes { + if invulnerable { + game.ecs + .insert_entity_event(entity, InvulnerabilityChangeEvent(true)) + .unwrap(); + } else { + game.ecs + .insert_entity_event(entity, InvulnerabilityChangeEvent(false)) + .unwrap(); + } + } + Ok(()) +} diff --git a/feather/server/src/systems/inventory.rs b/feather/server/src/systems/inventory.rs new file mode 100644 index 000000000..8bdfb01b6 --- /dev/null +++ b/feather/server/src/systems/inventory.rs @@ -0,0 +1,31 @@ +use common::{Game, Window}; +use ecs::{SysResult, SystemExecutor}; +use quill_common::events::InventoryUpdateEvent; + +use crate::Server; +use quill_common::components::ClientId; + +pub fn register(systems: &mut SystemExecutor<Game>) { + systems.group::<Server>().add_system(inventory_change); +} + +fn inventory_change(game: &mut Game, server: &mut Server) -> SysResult { + for (entity, (event, window)) in game.ecs.query::<(&InventoryUpdateEvent, &Window)>().iter() { + let client = server + .clients + .get(*game.ecs.get::<ClientId>(entity).unwrap()) + .unwrap(); + for slot in &event.0 { + client.send_slot_item( + 0, + *slot as i16, + window + .item(*slot) + .ok() + .map(|item| item.clone()) + .unwrap_or_default(), + ) + } + } + Ok(()) +} diff --git a/feather/server/src/systems/player_join.rs b/feather/server/src/systems/player_join.rs index 24ddf090d..92839fab6 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -1,23 +1,23 @@ -use libcraft_items::InventorySlot; use log::debug; use base::anvil::player::PlayerAbilities; use base::{Gamemode, Inventory, ItemStack, Position, Text}; -use common::{ - chat::{ChatKind, ChatPreference}, - entities::player::HotbarSlot, - view::View, - window::BackingWindow, - ChatBox, Game, Window, -}; +use common::{entities::player::HotbarSlot, view::View, window::BackingWindow, Game, Window}; use ecs::{SysResult, SystemExecutor}; +use feather_commands::{CommandCtx, CommandDispatcher}; +use libcraft_items::InventorySlot; use quill_common::components::{ - CanBuild, CanCreativeFly, CreativeFlying, CreativeFlyingSpeed, Health, Instabreak, - Invulnerable, PreviousGamemode, WalkSpeed, + CanBuild, CanCreativeFly, ChatBox, ChatKind, ChatPreference, CreativeFlying, + CreativeFlyingSpeed, DefaultGamemode, Health, Instabreak, Invulnerable, Name, PreviousGamemode, + RealIp, WalkSpeed, +}; +use quill_common::{ + components::{ClientId, NetworkId}, + entity_init::EntityInit, }; -use quill_common::{components::Name, entity_init::EntityInit}; -use crate::{ClientId, NetworkId, Server}; +use crate::Server; +use common::banlist::{BanEntry, BanList}; pub fn register(systems: &mut SystemExecutor<Game>) { systems.group::<Server>().add_system(poll_new_players); @@ -34,6 +34,22 @@ fn poll_new_players(game: &mut Game, server: &mut Server) -> SysResult { fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) -> SysResult { let client = server.clients.get_mut(client_id).unwrap(); + let banlist = game.resources.get::<BanList>().unwrap(); + if let Some(BanEntry { reason, .. }) = banlist.get_ban_entry(&client.uuid()) { + client.disconnect(Text::translate_with( + "multiplayer.disconnect.banned.reason", + vec![reason.to_string()], + )); + return Ok(()); + } + if let Some(BanEntry { reason, .. }) = banlist.get_ip_ban_entry(client.real_ip()) { + client.disconnect(Text::translate_with( + "multiplayer.disconnect.banned_ip.reason", + vec![reason.to_string()], + )); + return Ok(()); + } + drop(banlist); let player_data = game.world.load_player_data(client.uuid()); let mut builder = game.create_entity_builder( player_data @@ -56,7 +72,7 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) let gamemode = player_data .as_ref() .map(|data| Gamemode::from_id(data.gamemode as u8).expect("Unsupported gamemode")) - .unwrap_or(server.options.default_gamemode); + .unwrap_or(**game.resources.get::<DefaultGamemode>().unwrap()); let previous_gamemode = player_data .as_ref() .map(|data| PreviousGamemode::from_id(data.previous_gamemode as i8)) @@ -64,6 +80,12 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) client.send_join_game(gamemode, previous_gamemode); client.send_brand(); + client.send_commands( + &*game + .resources + .get::<CommandDispatcher<CommandCtx, Text>>() + .unwrap(), + ); // Abilities let abilities = player_abilities_or_default( @@ -129,7 +151,8 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) .add(abilities.may_fly) .add(abilities.may_build) .add(abilities.instabreak) - .add(abilities.invulnerable); + .add(abilities.invulnerable) + .add(RealIp(client.real_ip())); game.spawn_entity(builder); diff --git a/feather/server/src/systems/player_leave.rs b/feather/server/src/systems/player_leave.rs index 8034d0b0a..10ef9fcd5 100644 --- a/feather/server/src/systems/player_leave.rs +++ b/feather/server/src/systems/player_leave.rs @@ -1,17 +1,19 @@ -use num_traits::cast::ToPrimitive; - -use base::anvil::entity::{AnimalData, BaseEntityData}; use base::anvil::player::{InventorySlot, PlayerAbilities, PlayerData}; -use base::{Gamemode, Inventory, Position, Text}; +use base::Text; +use base::{Gamemode, Inventory, Position}; use common::entities::player::HotbarSlot; -use common::{chat::ChatKind, Game}; +use common::Game; use ecs::{SysResult, SystemExecutor}; +use num_traits::cast::ToPrimitive; use quill_common::components::{ - CanBuild, CanCreativeFly, CreativeFlying, CreativeFlyingSpeed, Health, Instabreak, - Invulnerable, Name, PreviousGamemode, WalkSpeed, + CanBuild, CanCreativeFly, ChatKind, ClientId, CreativeFlying, CreativeFlyingSpeed, Health, + Instabreak, Invulnerable, Name, PreviousGamemode, WalkSpeed, }; -use crate::{ClientId, Server}; +use crate::Server; +use base::anvil::entity::{AnimalData, BaseEntityData}; +use quill_common::entities::Player; +use quill_common::events::DisconnectEvent; pub fn register(systems: &mut SystemExecutor<Game>) { systems @@ -20,6 +22,18 @@ pub fn register(systems: &mut SystemExecutor<Game>) { } fn remove_disconnected_clients(game: &mut Game, server: &mut Server) -> SysResult { + for (_, (_, event, &client)) in game + .ecs + .query::<(&Player, &DisconnectEvent, &ClientId)>() + .iter() + { + server + .clients + .get(client) + .unwrap() + .disconnect((**event).clone()); + } + let mut entities_to_remove = Vec::new(); for ( player, diff --git a/feather/server/src/systems/plugin_message.rs b/feather/server/src/systems/plugin_message.rs index 3b426cf3b..4368a1f9d 100644 --- a/feather/server/src/systems/plugin_message.rs +++ b/feather/server/src/systems/plugin_message.rs @@ -1,6 +1,8 @@ -use crate::{ClientId, Server}; -use common::{events::PluginMessageEvent, Game}; +use crate::Server; +use common::Game; use ecs::{SysResult, SystemExecutor}; +use quill_common::components::ClientId; +use quill_common::events::PluginMessageSendEvent; pub fn register(systems: &mut SystemExecutor<Game>) { systems @@ -9,7 +11,11 @@ pub fn register(systems: &mut SystemExecutor<Game>) { } fn send_plugin_message_packets(game: &mut Game, server: &mut Server) -> SysResult { - for (_, (&client_id, event)) in game.ecs.query::<(&ClientId, &PluginMessageEvent)>().iter() { + for (_, (&client_id, event)) in game + .ecs + .query::<(&ClientId, &PluginMessageSendEvent)>() + .iter() + { if let Some(client) = server.clients.get(client_id) { client.send_plugin_message(event.channel.clone(), event.data.clone()); } diff --git a/feather/server/src/systems/tablist.rs b/feather/server/src/systems/tablist.rs index 2f38b6aa9..ab7325b9d 100644 --- a/feather/server/src/systems/tablist.rs +++ b/feather/server/src/systems/tablist.rs @@ -9,13 +9,16 @@ use ecs::{SysResult, SystemExecutor}; use quill_common::{components::Name, entities::Player}; use uuid::Uuid; -use crate::{ClientId, Server}; +use crate::Server; +use quill_common::components::ClientId; +use quill_common::events::GamemodeUpdateEvent; pub fn register(systems: &mut SystemExecutor<Game>) { systems .group::<Server>() .add_system(remove_tablist_players) - .add_system(add_tablist_players); + .add_system(add_tablist_players) + .add_system(change_tablist_player_gamemode); } fn remove_tablist_players(game: &mut Game, server: &mut Server) -> SysResult { @@ -62,3 +65,15 @@ fn add_tablist_players(game: &mut Game, server: &mut Server) -> SysResult { } Ok(()) } + +fn change_tablist_player_gamemode(game: &mut Game, server: &mut Server) -> SysResult { + for (_, (_, &uuid, &gamemode)) in game + .ecs + .query::<(&GamemodeUpdateEvent, &Uuid, &Gamemode)>() + .iter() + { + // Change this player's gamemode in players' tablists + server.broadcast_with(|client| client.change_player_tablist_gamemode(uuid, gamemode)); + } + Ok(()) +} diff --git a/feather/server/src/systems/time.rs b/feather/server/src/systems/time.rs new file mode 100644 index 000000000..ca49d90c0 --- /dev/null +++ b/feather/server/src/systems/time.rs @@ -0,0 +1,30 @@ +use common::Game; +use ecs::{SysResult, SystemExecutor}; +use quill_common::events::TimeUpdateEvent; + +use crate::Server; + +pub fn register(systems: &mut SystemExecutor<Game>) { + systems + .group::<Server>() + .add_system(time_change) + .add_system(time_tick); +} + +fn time_change(game: &mut Game, server: &mut Server) -> SysResult { + for _ in game.ecs.query::<(&TimeUpdateEvent,)>().iter() { + server.broadcast_with(|client| { + client.send_time(&game.world.time); + }); + } + Ok(()) +} + +fn time_tick(game: &mut Game, _server: &mut Server) -> SysResult { + game.ecs.insert_event(TimeUpdateEvent { + old: game.world.time.time(), + new: game.world.time.time() + 1, + }); + game.world.time.increment(); + Ok(()) +} diff --git a/feather/server/src/systems/view.rs b/feather/server/src/systems/view.rs index 5d43a3eee..784dcddf3 100644 --- a/feather/server/src/systems/view.rs +++ b/feather/server/src/systems/view.rs @@ -3,6 +3,8 @@ //! The entities and chunks visible to each client are //! determined based on the player's [`common::view::View`]. +use crate::client::Client; +use crate::Server; use ahash::AHashMap; use base::{ChunkPosition, Position}; use common::{ @@ -10,8 +12,7 @@ use common::{ Game, }; use ecs::{Entity, SysResult, SystemExecutor}; - -use crate::{Client, ClientId, Server}; +use quill_common::components::ClientId; pub fn register(_game: &mut Game, systems: &mut SystemExecutor<Game>) { systems diff --git a/feather/worldgen/src/lib.rs b/feather/worldgen/src/lib.rs index 7737747e2..c940f43a6 100644 --- a/feather/worldgen/src/lib.rs +++ b/feather/worldgen/src/lib.rs @@ -73,7 +73,7 @@ pub struct ComposableGenerator { /// by this composable generator. finishers: SmallVec<[Box<dyn FinishingGenerator>; 8]>, /// The world seed. - seed: u64, + seed: i64, } impl ComposableGenerator { @@ -83,7 +83,7 @@ impl ComposableGenerator { density_map: D, composition: C, finishers: F, - seed: u64, + seed: i64, ) -> Self where B: BiomeGenerator + 'static, @@ -102,7 +102,7 @@ impl ComposableGenerator { /// A default composable generator, used /// for worlds with "default" world type. - pub fn default_with_seed(seed: u64) -> Self { + pub fn default_with_seed(seed: i64) -> Self { let finishers: Vec<Box<dyn FinishingGenerator>> = vec![ Box::new(SnowFinisher::default()), Box::new(SingleFoliageFinisher::default()), @@ -120,7 +120,7 @@ impl ComposableGenerator { impl WorldGenerator for ComposableGenerator { fn generate_chunk(&self, position: ChunkPosition) -> Chunk { - let mut seed_shuffler = XorShiftRng::seed_from_u64(self.seed); + let mut seed_shuffler = XorShiftRng::seed_from_u64(self.seed as u64); // Generate biomes for 3x3 grid of chunks around current chunk. let biome_seed = seed_shuffler.gen(); diff --git a/libcraft/core/src/gamemode.rs b/libcraft/core/src/gamemode.rs index 9996efea8..b0c8fd4c1 100644 --- a/libcraft/core/src/gamemode.rs +++ b/libcraft/core/src/gamemode.rs @@ -1,5 +1,6 @@ use num_derive::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; +use std::str::FromStr; /// A gamemode. #[derive( @@ -26,3 +27,29 @@ impl Gamemode { }) } } + +impl FromStr for Gamemode { + type Err = (); + + fn from_str(input: &str) -> Result<Self, <Self as FromStr>::Err> { + match input.to_ascii_lowercase().as_str() { + "survival" => Ok(Gamemode::Survival), + "creative" => Ok(Gamemode::Creative), + "adventure" => Ok(Gamemode::Adventure), + "spectator" => Ok(Gamemode::Spectator), + _ => Err(()), + } + } +} + +impl ToString for Gamemode { + fn to_string(&self) -> String { + match self { + Gamemode::Survival => "survival", + Gamemode::Creative => "creative", + Gamemode::Adventure => "adventure", + Gamemode::Spectator => "spectator", + } + .to_owned() + } +} diff --git a/libcraft/generators/python/tags.py b/libcraft/generators/python/tags.py new file mode 100644 index 000000000..8ebc8e2ce --- /dev/null +++ b/libcraft/generators/python/tags.py @@ -0,0 +1,33 @@ +from os import listdir +import common +prefix = "../datapacks/minecraft/data/minecraft/tags/" +block_tags = listdir(prefix + "blocks") +entity_types = listdir(prefix + "entity_types") +fluid_tags = listdir(prefix + "fluids") +item_tags = listdir(prefix + "items") +tag_list_list = (block_tags, entity_types, fluid_tags, item_tags) +enum_names = ("VanillaBlockTags", "VanillaEntityTypes", "VanillaFluidTags", "VanillaItemTags") +new_line = "\n" +quotes = "\"" +output = "" +for (tags, enum_name) in zip(tag_list_list, enum_names): + output += "#[derive(Copy, Clone, Debug, PartialEq, Eq)]\n" + output += f"pub enum {enum_name} {{{new_line}" + for s in tags: + camelcase = ''.join(map(str.title, s[:-5].split('_'))) + output += f" {camelcase},{new_line}" + output += f"}}{new_line}{new_line}" +for (tags, enum_name) in zip(tag_list_list, enum_names): + output += f"impl {enum_name} {{{new_line}" + output += f" pub fn name(&self) -> &'static str {{{new_line}" + output += f" match self {{{new_line}" + for s in tags: + snakecase = s[:-5] + camelcase = ''.join(map(str.title, snakecase.split('_'))) + output += f" {enum_name}::{camelcase} => {quotes}{snakecase}{quotes},{new_line}" + output += " }\n }\n}\n" + output += f"impl From<{enum_name}> for &'static str {{{new_line}" + output += f" fn from(tag: {enum_name}) -> Self {{{new_line}" + output += " tag.name()\n" + output += " }\n}\n" +common.output("src/vanilla_tags.rs", output) \ No newline at end of file diff --git a/libcraft/inventory/src/vanilla_tags.rs b/libcraft/inventory/src/vanilla_tags.rs new file mode 100644 index 000000000..9c7706e9d --- /dev/null +++ b/libcraft/inventory/src/vanilla_tags.rs @@ -0,0 +1,355 @@ +// This file is @generated. Please do not edit. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum VanillaBlockTags { + AcaciaLogs, + Anvil, + BambooPlantableOn, + Banners, + BaseStoneNether, + BaseStoneOverworld, + BeaconBaseBlocks, + Beds, + Beehives, + BeeGrowables, + BirchLogs, + Buttons, + Campfires, + Carpets, + Climbable, + Corals, + CoralBlocks, + CoralPlants, + CrimsonStems, + Crops, + DarkOakLogs, + Doors, + DragonImmune, + EndermanHoldable, + Fences, + FenceGates, + Fire, + Flowers, + FlowerPots, + GoldOres, + GuardedByPiglins, + HoglinRepellents, + Ice, + Impermeable, + InfiniburnEnd, + InfiniburnNether, + InfiniburnOverworld, + JungleLogs, + Leaves, + Logs, + LogsThatBurn, + MushroomGrowBlock, + NonFlammableWood, + Nylium, + OakLogs, + PiglinRepellents, + Planks, + Portals, + PressurePlates, + PreventMobSpawningInside, + Rails, + Sand, + Saplings, + ShulkerBoxes, + Signs, + Slabs, + SmallFlowers, + SoulFireBaseBlocks, + SoulSpeedBlocks, + SpruceLogs, + Stairs, + StandingSigns, + StoneBricks, + StonePressurePlates, + StriderWarmBlocks, + TallFlowers, + Trapdoors, + UnderwaterBonemeals, + UnstableBottomCenter, + ValidSpawn, + Walls, + WallCorals, + WallPostOverride, + WallSigns, + WarpedStems, + WartBlocks, + WitherImmune, + WitherSummonBaseBlocks, + WoodenButtons, + WoodenDoors, + WoodenFences, + WoodenPressurePlates, + WoodenSlabs, + WoodenStairs, + WoodenTrapdoors, + Wool, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum VanillaEntityTypes { + Arrows, + BeehiveInhabitors, + ImpactProjectiles, + Raiders, + Skeletons, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum VanillaFluidTags { + Lava, + Water, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum VanillaItemTags { + AcaciaLogs, + Anvil, + Arrows, + Banners, + BeaconPaymentItems, + Beds, + BirchLogs, + Boats, + Buttons, + Carpets, + Coals, + CreeperDropMusicDiscs, + CrimsonStems, + DarkOakLogs, + Doors, + Fences, + Fishes, + Flowers, + GoldOres, + JungleLogs, + Leaves, + LecternBooks, + Logs, + LogsThatBurn, + MusicDiscs, + NonFlammableWood, + OakLogs, + PiglinLoved, + PiglinRepellents, + Planks, + Rails, + Sand, + Saplings, + Signs, + Slabs, + SmallFlowers, + SoulFireBaseBlocks, + SpruceLogs, + Stairs, + StoneBricks, + StoneCraftingMaterials, + StoneToolMaterials, + TallFlowers, + Trapdoors, + Walls, + WarpedStems, + WoodenButtons, + WoodenDoors, + WoodenFences, + WoodenPressurePlates, + WoodenSlabs, + WoodenStairs, + WoodenTrapdoors, + Wool, +} + +impl VanillaBlockTags { + pub fn name(&self) -> &'static str { + match self { + VanillaBlockTags::AcaciaLogs => "acacia_logs", + VanillaBlockTags::Anvil => "anvil", + VanillaBlockTags::BambooPlantableOn => "bamboo_plantable_on", + VanillaBlockTags::Banners => "banners", + VanillaBlockTags::BaseStoneNether => "base_stone_nether", + VanillaBlockTags::BaseStoneOverworld => "base_stone_overworld", + VanillaBlockTags::BeaconBaseBlocks => "beacon_base_blocks", + VanillaBlockTags::Beds => "beds", + VanillaBlockTags::Beehives => "beehives", + VanillaBlockTags::BeeGrowables => "bee_growables", + VanillaBlockTags::BirchLogs => "birch_logs", + VanillaBlockTags::Buttons => "buttons", + VanillaBlockTags::Campfires => "campfires", + VanillaBlockTags::Carpets => "carpets", + VanillaBlockTags::Climbable => "climbable", + VanillaBlockTags::Corals => "corals", + VanillaBlockTags::CoralBlocks => "coral_blocks", + VanillaBlockTags::CoralPlants => "coral_plants", + VanillaBlockTags::CrimsonStems => "crimson_stems", + VanillaBlockTags::Crops => "crops", + VanillaBlockTags::DarkOakLogs => "dark_oak_logs", + VanillaBlockTags::Doors => "doors", + VanillaBlockTags::DragonImmune => "dragon_immune", + VanillaBlockTags::EndermanHoldable => "enderman_holdable", + VanillaBlockTags::Fences => "fences", + VanillaBlockTags::FenceGates => "fence_gates", + VanillaBlockTags::Fire => "fire", + VanillaBlockTags::Flowers => "flowers", + VanillaBlockTags::FlowerPots => "flower_pots", + VanillaBlockTags::GoldOres => "gold_ores", + VanillaBlockTags::GuardedByPiglins => "guarded_by_piglins", + VanillaBlockTags::HoglinRepellents => "hoglin_repellents", + VanillaBlockTags::Ice => "ice", + VanillaBlockTags::Impermeable => "impermeable", + VanillaBlockTags::InfiniburnEnd => "infiniburn_end", + VanillaBlockTags::InfiniburnNether => "infiniburn_nether", + VanillaBlockTags::InfiniburnOverworld => "infiniburn_overworld", + VanillaBlockTags::JungleLogs => "jungle_logs", + VanillaBlockTags::Leaves => "leaves", + VanillaBlockTags::Logs => "logs", + VanillaBlockTags::LogsThatBurn => "logs_that_burn", + VanillaBlockTags::MushroomGrowBlock => "mushroom_grow_block", + VanillaBlockTags::NonFlammableWood => "non_flammable_wood", + VanillaBlockTags::Nylium => "nylium", + VanillaBlockTags::OakLogs => "oak_logs", + VanillaBlockTags::PiglinRepellents => "piglin_repellents", + VanillaBlockTags::Planks => "planks", + VanillaBlockTags::Portals => "portals", + VanillaBlockTags::PressurePlates => "pressure_plates", + VanillaBlockTags::PreventMobSpawningInside => "prevent_mob_spawning_inside", + VanillaBlockTags::Rails => "rails", + VanillaBlockTags::Sand => "sand", + VanillaBlockTags::Saplings => "saplings", + VanillaBlockTags::ShulkerBoxes => "shulker_boxes", + VanillaBlockTags::Signs => "signs", + VanillaBlockTags::Slabs => "slabs", + VanillaBlockTags::SmallFlowers => "small_flowers", + VanillaBlockTags::SoulFireBaseBlocks => "soul_fire_base_blocks", + VanillaBlockTags::SoulSpeedBlocks => "soul_speed_blocks", + VanillaBlockTags::SpruceLogs => "spruce_logs", + VanillaBlockTags::Stairs => "stairs", + VanillaBlockTags::StandingSigns => "standing_signs", + VanillaBlockTags::StoneBricks => "stone_bricks", + VanillaBlockTags::StonePressurePlates => "stone_pressure_plates", + VanillaBlockTags::StriderWarmBlocks => "strider_warm_blocks", + VanillaBlockTags::TallFlowers => "tall_flowers", + VanillaBlockTags::Trapdoors => "trapdoors", + VanillaBlockTags::UnderwaterBonemeals => "underwater_bonemeals", + VanillaBlockTags::UnstableBottomCenter => "unstable_bottom_center", + VanillaBlockTags::ValidSpawn => "valid_spawn", + VanillaBlockTags::Walls => "walls", + VanillaBlockTags::WallCorals => "wall_corals", + VanillaBlockTags::WallPostOverride => "wall_post_override", + VanillaBlockTags::WallSigns => "wall_signs", + VanillaBlockTags::WarpedStems => "warped_stems", + VanillaBlockTags::WartBlocks => "wart_blocks", + VanillaBlockTags::WitherImmune => "wither_immune", + VanillaBlockTags::WitherSummonBaseBlocks => "wither_summon_base_blocks", + VanillaBlockTags::WoodenButtons => "wooden_buttons", + VanillaBlockTags::WoodenDoors => "wooden_doors", + VanillaBlockTags::WoodenFences => "wooden_fences", + VanillaBlockTags::WoodenPressurePlates => "wooden_pressure_plates", + VanillaBlockTags::WoodenSlabs => "wooden_slabs", + VanillaBlockTags::WoodenStairs => "wooden_stairs", + VanillaBlockTags::WoodenTrapdoors => "wooden_trapdoors", + VanillaBlockTags::Wool => "wool", + } + } +} +impl From<VanillaBlockTags> for &'static str { + fn from(tag: VanillaBlockTags) -> Self { + tag.name() + } +} +impl VanillaEntityTypes { + pub fn name(&self) -> &'static str { + match self { + VanillaEntityTypes::Arrows => "arrows", + VanillaEntityTypes::BeehiveInhabitors => "beehive_inhabitors", + VanillaEntityTypes::ImpactProjectiles => "impact_projectiles", + VanillaEntityTypes::Raiders => "raiders", + VanillaEntityTypes::Skeletons => "skeletons", + } + } +} +impl From<VanillaEntityTypes> for &'static str { + fn from(tag: VanillaEntityTypes) -> Self { + tag.name() + } +} +impl VanillaFluidTags { + pub fn name(&self) -> &'static str { + match self { + VanillaFluidTags::Lava => "lava", + VanillaFluidTags::Water => "water", + } + } +} +impl From<VanillaFluidTags> for &'static str { + fn from(tag: VanillaFluidTags) -> Self { + tag.name() + } +} +impl VanillaItemTags { + pub fn name(&self) -> &'static str { + match self { + VanillaItemTags::AcaciaLogs => "acacia_logs", + VanillaItemTags::Anvil => "anvil", + VanillaItemTags::Arrows => "arrows", + VanillaItemTags::Banners => "banners", + VanillaItemTags::BeaconPaymentItems => "beacon_payment_items", + VanillaItemTags::Beds => "beds", + VanillaItemTags::BirchLogs => "birch_logs", + VanillaItemTags::Boats => "boats", + VanillaItemTags::Buttons => "buttons", + VanillaItemTags::Carpets => "carpets", + VanillaItemTags::Coals => "coals", + VanillaItemTags::CreeperDropMusicDiscs => "creeper_drop_music_discs", + VanillaItemTags::CrimsonStems => "crimson_stems", + VanillaItemTags::DarkOakLogs => "dark_oak_logs", + VanillaItemTags::Doors => "doors", + VanillaItemTags::Fences => "fences", + VanillaItemTags::Fishes => "fishes", + VanillaItemTags::Flowers => "flowers", + VanillaItemTags::GoldOres => "gold_ores", + VanillaItemTags::JungleLogs => "jungle_logs", + VanillaItemTags::Leaves => "leaves", + VanillaItemTags::LecternBooks => "lectern_books", + VanillaItemTags::Logs => "logs", + VanillaItemTags::LogsThatBurn => "logs_that_burn", + VanillaItemTags::MusicDiscs => "music_discs", + VanillaItemTags::NonFlammableWood => "non_flammable_wood", + VanillaItemTags::OakLogs => "oak_logs", + VanillaItemTags::PiglinLoved => "piglin_loved", + VanillaItemTags::PiglinRepellents => "piglin_repellents", + VanillaItemTags::Planks => "planks", + VanillaItemTags::Rails => "rails", + VanillaItemTags::Sand => "sand", + VanillaItemTags::Saplings => "saplings", + VanillaItemTags::Signs => "signs", + VanillaItemTags::Slabs => "slabs", + VanillaItemTags::SmallFlowers => "small_flowers", + VanillaItemTags::SoulFireBaseBlocks => "soul_fire_base_blocks", + VanillaItemTags::SpruceLogs => "spruce_logs", + VanillaItemTags::Stairs => "stairs", + VanillaItemTags::StoneBricks => "stone_bricks", + VanillaItemTags::StoneCraftingMaterials => "stone_crafting_materials", + VanillaItemTags::StoneToolMaterials => "stone_tool_materials", + VanillaItemTags::TallFlowers => "tall_flowers", + VanillaItemTags::Trapdoors => "trapdoors", + VanillaItemTags::Walls => "walls", + VanillaItemTags::WarpedStems => "warped_stems", + VanillaItemTags::WoodenButtons => "wooden_buttons", + VanillaItemTags::WoodenDoors => "wooden_doors", + VanillaItemTags::WoodenFences => "wooden_fences", + VanillaItemTags::WoodenPressurePlates => "wooden_pressure_plates", + VanillaItemTags::WoodenSlabs => "wooden_slabs", + VanillaItemTags::WoodenStairs => "wooden_stairs", + VanillaItemTags::WoodenTrapdoors => "wooden_trapdoors", + VanillaItemTags::Wool => "wool", + } + } +} +impl From<VanillaItemTags> for &'static str { + fn from(tag: VanillaItemTags) -> Self { + tag.name() + } +} diff --git a/libcraft/text/Cargo.toml b/libcraft/text/Cargo.toml index b99bd785c..92cd82dcd 100644 --- a/libcraft/text/Cargo.toml +++ b/libcraft/text/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Gijs de Jong <berichtaangijs@gmail.com>", "caelunshun <caelunshun@gm edition = "2018" [dependencies] -hematite-nbt = { git = "https://github.com/PistonDevelopers/hematite_nbt" } +quartz_nbt = { version = "0.2.4", features = [ "serde" ] } nom = "5" nom_locate = "2" serde = { version = "1", features = [ "derive" ] } diff --git a/libcraft/text/src/text.rs b/libcraft/text/src/text.rs index 90af776de..37f3f9120 100644 --- a/libcraft/text/src/text.rs +++ b/libcraft/text/src/text.rs @@ -1,9 +1,11 @@ //! Implementation of the Minecraft chat component format. -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use std::str::FromStr; + +use quartz_nbt::NbtTag; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; pub mod markdown; @@ -371,10 +373,33 @@ pub enum TextValue { keybind: Keybind, }, Nbt { - nbt: nbt::Blob, + #[serde(with = "snbt")] + nbt: NbtTag, }, } +mod snbt { + use quartz_nbt::NbtTag; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize<S>(tag: &NbtTag, serialize: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serialize.serialize_str(&tag.to_snbt()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<NbtTag, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + quartz_nbt::snbt::parse(&s) + .map(NbtTag::Compound) + .or(Ok(NbtTag::String(s))) + } +} + impl<T> From<T> for TextValue where T: Into<Cow<'static, str>>, @@ -434,7 +459,7 @@ impl TextValue { } } - pub fn nbt<A: Into<nbt::Blob>>(nbt: A) -> Self { + pub fn nbt<A: Into<NbtTag>>(nbt: A) -> Self { TextValue::Nbt { nbt: nbt.into() } } } @@ -473,6 +498,86 @@ impl TextComponent { pub fn empty() -> TextComponent { TextComponent::from("") } + + pub fn to_string< + T: Fn(&Translate, &Vec<String>) -> String, + C: Fn(&Color) -> String, + S: Fn(&Style) -> String, + S2: Fn(&str) -> String, + >( + &self, + display_translate: &T, + display_colors: &C, + display_styles: &S, + display_selector: &S2, + reset: &str, + ) -> String { + let mut style = String::new(); + if let Some(color) = self.color.as_ref() { + style += reset; + style += &display_colors(color); + } + if self.bold == Some(true) { + style += &display_styles(&Style::Bold) + } + if self.italic == Some(true) { + style += &display_styles(&Style::Italic) + } + if self.underlined == Some(true) { + style += &display_styles(&Style::Underlined) + } + if self.strikethrough == Some(true) { + style += &display_styles(&Style::Strikethrough) + } + if self.obfuscated == Some(true) { + style += &display_styles(&Style::Obfuscated) + } + + let mut s = String::new(); + s += &style; + s += &match &self.value { + TextValue::Text { text } => text.to_string(), + TextValue::Translate { translate, with } => display_translate( + translate, + &with + .iter() + .map(|text| { + text.clone().into_component().to_string( + display_translate, + display_colors, + display_styles, + display_selector, + reset, + ) + }) + .collect(), + ), + TextValue::Score { .. } => unimplemented!(), + TextValue::Selector { selector } => display_selector(&**selector), + TextValue::Keybind { keybind } => keybind.into(), + TextValue::Nbt { nbt } => match nbt { + NbtTag::String(s) => s.to_owned(), + other => other.to_snbt(), + }, + }; + s += reset; + + if let Some(extra) = self.extra.as_ref() { + for text in extra { + s += &style; + s += &text.clone().into_component().to_string( + display_translate, + display_colors, + display_styles, + display_selector, + reset, + ); + s += reset; + } + } + + s + } } pub enum Reset { @@ -938,6 +1043,13 @@ impl Text { Text::from(text) } + pub fn translate<A>(translate: A) -> Self + where + A: Into<Translate>, + { + Text::from(TextValue::translate(translate)) + } + pub fn translate_with<A, B>(translate: A, with: B) -> Self where A: Into<Translate>, @@ -963,9 +1075,13 @@ impl Text { Text::from(TextValue::keybind(keybind)) } - pub fn nbt<A: Into<nbt::Blob>>(nbt: A) -> Text { + pub fn nbt<A: Into<NbtTag>>(nbt: A) -> Text { Text::from(TextValue::nbt(nbt)) } + + pub fn from_json<A: AsRef<str>>(json: A) -> Result<Text, serde_json::Error> { + serde_json::from_str(json.as_ref()) + } } impl From<Text> for String { @@ -1105,9 +1221,10 @@ impl_operators!(TextRoot, Text, TextComponent); #[cfg(test)] mod tests { - use super::*; use std::error::Error; + use super::*; + #[test] pub fn text_text_single() -> Result<(), Box<dyn Error>> { let text_orignal: Text = Text::from("hello"); diff --git a/libcraft/text/src/title.rs b/libcraft/text/src/title.rs index 38c995352..3363a2743 100644 --- a/libcraft/text/src/title.rs +++ b/libcraft/text/src/title.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; // Based on https://wiki.vg/index.php?title=Protocol&oldid=16459#Title -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] pub struct Title { pub title: Option<String>, pub sub_title: Option<String>, diff --git a/quill/api/Cargo.toml b/quill/api/Cargo.toml index 54f2abf6e..ef453e6c5 100644 --- a/quill/api/Cargo.toml +++ b/quill/api/Cargo.toml @@ -16,4 +16,5 @@ quill-common = { path = "../common" } thiserror = "1" uuid = "0.8" itertools = "0.10.0" +commands = { git = "https://github.com/Iaiao/commands", rev = "42bebd15e18cf511355aaf5f241fb4218031c4ee" } diff --git a/quill/api/src/command.rs b/quill/api/src/command.rs new file mode 100644 index 000000000..938b717d3 --- /dev/null +++ b/quill/api/src/command.rs @@ -0,0 +1,58 @@ +/* +This file contains defenitions used for quills/feathers command dispatching/calling. +A command is something like "/msg KillerBunny You stole my name!" +*/ + +use libcraft_text::Text; +use quill_common::{EntityId, Pointer}; + +use crate::Entity; +use crate::Game; + +#[derive(Debug, Clone)] +#[repr(C)] +pub enum Caller { + Player(Entity), + Terminal, +} + +impl From<Option<EntityId>> for Caller { + fn from(it: Option<EntityId>) -> Self { + match it { + Some(id) => Caller::Player(Entity::new(crate::EntityId(id))), + None => Caller::Terminal, + } + } +} + +impl Caller { + pub fn send_message(&self, message: impl Into<Text>) { + unsafe { + match self { + Caller::Player(entity) => quill_sys::entity_send_message( + entity.id().0, + Pointer::new(&message.into() as *const _ as *const _), + ), + Caller::Terminal => todo!(), + } + } + } +} + +/// This is what is passed into the command when its called. +#[repr(C)] +pub struct CommandContext<'a, Plugin> { + pub game: Game, + pub caller: Caller, + pub plugin: &'a mut Plugin, +} + +impl<'a, Plugin> CommandContext<'a, Plugin> { + pub fn new(game: Game, caller: Caller, plugin: &'a mut Plugin) -> CommandContext<'a, Plugin> { + CommandContext { + game, + caller, + plugin, + } + } +} diff --git a/quill/api/src/entity.rs b/quill/api/src/entity.rs index e5bc9bf92..a011efedf 100644 --- a/quill/api/src/entity.rs +++ b/quill/api/src/entity.rs @@ -1,5 +1,6 @@ use std::{marker::PhantomData, ptr}; +use crate::Text; use quill_common::{Component, Pointer, PointerMut}; /// Unique internal ID of an entity. @@ -23,9 +24,9 @@ pub struct MissingComponent(&'static str); /// /// Use [`crate::Game::entity`] to get an `Entity` instance. /// -/// An `Entity` be sent or shared between threads. However, +/// An `Entity` can't be sent or shared between threads. However, /// an [`EntityId`] can. -#[derive(Debug)] +#[derive(Debug, Clone)] #[repr(C)] pub struct Entity { id: EntityId, @@ -92,10 +93,13 @@ impl Entity { /// /// The message sends as a "system" message. /// See [the wiki](https://wiki.vg/Chat) for more details. - pub fn send_message(&self, message: impl AsRef<str>) { - let message = message.as_ref(); + pub fn send_message(&self, message: impl Into<Text>) { + let message = message.into(); unsafe { - quill_sys::entity_send_message(self.id.0, message.as_ptr().into(), message.len() as u32) + quill_sys::entity_send_message( + self.id.0, + Pointer::new(&message as *const _ as *const _), + ) } } diff --git a/quill/api/src/game.rs b/quill/api/src/game.rs index 9a72ab982..c4ca6ecff 100644 --- a/quill/api/src/game.rs +++ b/quill/api/src/game.rs @@ -33,6 +33,7 @@ pub struct EntityRemoved; /// /// A `Game` is passed to systems when they run. #[derive(Debug)] +#[repr(C)] pub struct Game { _not_send_sync: PhantomData<*mut ()>, } @@ -205,7 +206,7 @@ impl Game { } /// Sends a custom packet to an entity. - pub fn send_plugin_message(entity: EntityId, channel: &str, data: &[u8]) { + pub fn send_plugin_message(&self, entity: EntityId, channel: &str, data: &[u8]) { let channel_ptr = channel.as_ptr().into(); let data_ptr = data.as_ptr().into(); unsafe { diff --git a/quill/api/src/lib.rs b/quill/api/src/lib.rs index 23b3a9418..186c1f4ea 100644 --- a/quill/api/src/lib.rs +++ b/quill/api/src/lib.rs @@ -1,17 +1,20 @@ //! A WebAssembly-based plugin API for Minecraft servers. -pub mod entities; -mod entity; -mod entity_builder; -mod game; -pub mod query; -mod setup; +// Needed for macros +#[doc(hidden)] +pub extern crate bincode; +#[doc(hidden)] +pub extern crate quill_sys as sys; +#[doc(hidden)] +pub use commands; +#[doc(inline)] +pub use uuid::Uuid; + +pub use command::*; pub use entity::{Entity, EntityId}; pub use entity_builder::EntityBuilder; pub use game::Game; -pub use setup::Setup; - #[doc(inline)] pub use libcraft_blocks::{BlockKind, BlockState}; #[doc(inline)] @@ -20,17 +23,17 @@ pub use libcraft_core::{BlockPosition, ChunkPosition, Gamemode, Position}; pub use libcraft_particles::{Particle, ParticleKind}; #[doc(inline)] pub use libcraft_text::*; - #[doc(inline)] pub use quill_common::{components, entity_init::EntityInit, events, Component}; -#[doc(inline)] -pub use uuid::Uuid; +pub use setup::Setup; -// Needed for macros -#[doc(hidden)] -pub extern crate bincode; -#[doc(hidden)] -pub extern crate quill_sys as sys; +pub mod command; +pub mod entities; +mod entity; +mod entity_builder; +mod game; +pub mod query; +mod setup; /// Implement this trait for your plugin's struct. pub trait Plugin: Sized { @@ -89,6 +92,8 @@ macro_rules! plugin { // guarantees it will not invoke plugin systems outside of the main thread. static mut PLUGIN: Option<$plugin> = None; + type CommandContext<'a> = $crate::CommandContext<'a, $plugin>; + // Exports to the host required for all plugins #[no_mangle] #[doc(hidden)] @@ -144,6 +149,130 @@ macro_rules! plugin { system(plugin, &mut $crate::Game::new()); } + #[no_mangle] + #[doc(hidden)] + pub unsafe extern "C" fn quill_run_command( + data: *mut u8, + args: *mut u8, + args_len: u32, + ctx: *mut u8, + ) -> (u32, u64) { + let executor = &*data.cast::<Box< + dyn Fn( + &mut $crate::commands::dispatcher::Args, + $crate::CommandContext<$plugin>, + ) -> $crate::commands::dispatcher::CommandOutput, + >>(); + let ctx = &*ctx.cast::<$crate::CommandContext<()>>(); + let ctx = $crate::CommandContext { + game: $crate::Game::new(), + caller: ctx.caller.clone(), + plugin: PLUGIN.as_mut().expect("quill_setup never called"), + }; + let mut args = std::mem::ManuallyDrop::new(Vec::from_raw_parts( + args as *mut Box<dyn std::any::Any>, + args_len as usize, + args_len as usize, + )); + match executor(&mut args, ctx) { + Ok(result) => (0, result as u64), + Err(err) => { + let s = err.to_string(); + let ptr = &s as *const _ as u64; + std::mem::forget(s); // dropped on host side + (1, ptr) + } + } + } + + #[no_mangle] + #[doc(hidden)] + pub unsafe extern "C" fn quill_run_command_fork( + data: *mut u8, + args: *mut u8, + args_len: u32, + ctx: *mut u8, + f: u32, + ) -> (u32, u64) { + let fork = &mut *data + .cast::<Box<$crate::commands::dispatcher::Fork<$crate::CommandContext<$plugin>>>>(); + let ctx = &*ctx.cast::<$crate::CommandContext<()>>(); + let ctx = $crate::CommandContext { + game: $crate::Game::new(), + caller: ctx.caller.clone(), + plugin: PLUGIN.as_mut().expect("quill_setup never called"), + }; + let mut args = std::mem::ManuallyDrop::new(Vec::from_raw_parts( + args as *mut Box<dyn std::any::Any>, + args_len as usize, + args_len as usize, + )); + match fork( + &mut args, + ctx, + Box::new(&mut |args, context| todo!("create fork-callback host call")), + ) { + Ok(result) => (0, result as u64), + Err(err) => { + let s = err.to_string(); + let ptr = &s as *const _ as u64; + std::mem::forget(s); // dropped on host side + (1, ptr) + } + } + } + + #[no_mangle] + #[doc(hidden)] + pub unsafe extern "C" fn quill_run_command_completer( + data: *mut u8, + text: *mut u8, + text_len: u32, + ctx: *mut u8, + ) -> (u32, u32, u32, u32, u32) { + let complete = &*data.cast::<Box< + dyn Fn( + &str, + $crate::CommandContext<$plugin>, + ) -> $crate::commands::dispatcher::TabCompletion<$crate::Text>, + >>(); + let ctx = &*ctx.cast::<$crate::CommandContext<()>>(); + let ctx = $crate::CommandContext { + game: Game::new(), + caller: ctx.caller.clone(), + plugin: PLUGIN.as_mut().expect("quill_setup never called"), + }; + let text = String::from_raw_parts(text, text_len as usize, text_len as usize); + let completion = complete(&text, ctx); + let completions = completion + .2 + .into_iter() + .map(|(c, t)| { + ( + (c.as_ptr(), c.len(), c.capacity()), + t.is_some(), + if t.is_none() { + (0, 0, 0) + } else { + let t = t.as_ref().unwrap().to_string(); + ( + t.as_ptr() as usize as u32, + t.len() as u32, + t.capacity() as u32, + ) + }, + ) + }) + .collect::<Vec<_>>(); + ( + completion.0 as u32, + completion.1 as u32, + completions.as_ptr() as usize as u32, + completions.len() as u32, + completions.capacity() as u32, + ) + } + /// Never called by Quill, but this is needed /// to avoid linker errors with WASI. #[doc(hidden)] diff --git a/quill/api/src/setup.rs b/quill/api/src/setup.rs index 0b555b25f..f0096c661 100644 --- a/quill/api/src/setup.rs +++ b/quill/api/src/setup.rs @@ -1,15 +1,22 @@ use std::marker::PhantomData; +use std::mem::ManuallyDrop; +use commands::dispatcher::CommandDispatcher; + +use quill_common::PointerMut; + +use crate::command::CommandContext; use crate::Game; +use libcraft_text::Text; /// Struct passed to your plugin's `enable()` function. /// /// Allows you to register systems, etc. -pub struct Setup<Plugin> { +pub struct Setup<Plugin: crate::Plugin> { _marker: PhantomData<Plugin>, } -impl<Plugin> Setup<Plugin> { +impl<Plugin: crate::Plugin> Setup<Plugin> { /// For Quill internal use only. Do not call. #[doc(hidden)] #[allow(clippy::new_without_default)] @@ -36,4 +43,45 @@ impl<Plugin> Setup<Plugin> { self } + + /// Perform various actions on command dispatcher: Register commands, tab-completions, etc. + pub fn with_dispatcher( + &mut self, + f: impl FnOnce(&mut CommandDispatcher<CommandContext<Plugin>, Text>), + ) -> &mut Self { + let mut dispatcher = CommandDispatcher::new(); + + f(&mut dispatcher); + + let (nodes, executors, tab_completers, forks) = dispatcher.into_parts(); + let nodes: Vec<_> = nodes.into_iter().map(|(_, a)| a).collect(); + let executors: Vec<_> = executors.into_iter().map(|(_, a)| a).collect(); + let tab_completers: Vec<_> = tab_completers.into_iter().collect(); + let forks: Vec<_> = forks.into_iter().map(|(_, a)| a).collect(); + + let (nodes, executors, tab_completers, forks) = ( + ManuallyDrop::new(nodes), + ManuallyDrop::new(executors), + ManuallyDrop::new(tab_completers), + ManuallyDrop::new(forks), + ); + // SAFETY: passing raw vec data to recreate the vec on host + unsafe { + quill_sys::modify_command_executor( + PointerMut::new(nodes.as_ptr() as *const _ as *mut _), + nodes.len() as u32, + nodes.capacity() as u32, + PointerMut::new(executors.as_ptr() as *const _ as *mut _), + executors.len() as u32, + executors.capacity() as u32, + PointerMut::new(tab_completers.as_ptr() as *const _ as *mut _), + tab_completers.len() as u32, + tab_completers.capacity() as u32, + PointerMut::new(forks.as_ptr() as *const _ as *mut _), + forks.len() as u32, + forks.capacity() as u32, + ); + } + self + } } diff --git a/quill/common/src/component.rs b/quill/common/src/component.rs index f113fa29a..776cc5a92 100644 --- a/quill/common/src/component.rs +++ b/quill/common/src/component.rs @@ -198,6 +198,19 @@ host_component_enum! { CanBuild = 1020, Instabreak = 1021, Invulnerable = 1022, + GamemodeUpdateEvent = 1023, + TimeUpdateEvent = 1024, + InventoryUpdateEvent = 1025, + ChatBox = 1026, + PluginMessageReceiveEvent = 1027, + InstabreakChangeEvent = 1028, + FlyingAbilityChangeEvent = 1029, + BuildingAbilityChangeEvent = 1030, + InvulnerabilityChangeEvent = 1031, + RealIp = 1032, + DisconnectEvent = 1033, + Console = 1034, + DefaultGamemode = 1035, } @@ -363,3 +376,12 @@ bincode_component_impl!(BlockInteractEvent); bincode_component_impl!(CreativeFlyingEvent); bincode_component_impl!(SneakEvent); bincode_component_impl!(SprintEvent); +bincode_component_impl!(GamemodeUpdateEvent); +bincode_component_impl!(TimeUpdateEvent); +bincode_component_impl!(InventoryUpdateEvent); +bincode_component_impl!(PluginMessageReceiveEvent); +bincode_component_impl!(InstabreakChangeEvent); +bincode_component_impl!(FlyingAbilityChangeEvent); +bincode_component_impl!(BuildingAbilityChangeEvent); +bincode_component_impl!(InvulnerabilityChangeEvent); +bincode_component_impl!(DisconnectEvent); diff --git a/quill/common/src/components.rs b/quill/common/src/components.rs index 352c45b69..5dcbbb107 100644 --- a/quill/common/src/components.rs +++ b/quill/common/src/components.rs @@ -4,11 +4,14 @@ //! components. use std::fmt::Display; +use std::net::IpAddr; +use std::sync::atomic::{AtomicI32, Ordering}; use serde::{Deserialize, Serialize}; use smartstring::{LazyCompact, SmartString}; -use libcraft_core::Gamemode; +pub use libcraft_core::Gamemode; +use libcraft_text::{Text, Title}; /// Whether an entity is touching the ground. #[derive( @@ -284,3 +287,178 @@ impl Sprinting { } } bincode_component_impl!(Sprinting); + +/// An entity's ID used by the protocol +/// in `entity_id` fields. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct NetworkId(pub i32); + +impl NetworkId { + /// Creates a new, unique network ID. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + static NEXT: AtomicI32 = AtomicI32::new(0); + // In theory, this can overflow if the server + // creates 4 billion entities. The hope is that + // old entities will have died out at that point. + Self(NEXT.fetch_add(1, Ordering::SeqCst)) + } +} + +/// ID of a client. Can be reused. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ClientId(pub usize); + +/// An entity's "mailbox" for receiving chat messages. +/// +/// Internally stores a list of [`ChatMessage`]s. +/// It is up to the user to flush the mailbox. +/// (`feather-server` flushes mailboxes by sending chat packets.) +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct ChatBox { + messages: Vec<ChatMessage>, + titles: Vec<Title>, + preference: ChatPreference, +} + +bincode_component_impl!(ChatBox); + +impl ChatBox { + pub fn new(preference: ChatPreference) -> Self { + Self { + messages: Vec::new(), + titles: Vec::new(), + preference, + } + } + + pub fn set_preference(&mut self, preference: ChatPreference) { + self.preference = preference; + } + + pub fn send(&mut self, message: ChatMessage) { + self.messages.push(message); + } + + pub fn send_chat(&mut self, message: impl Into<Text>) { + self.send(ChatMessage::new(ChatKind::PlayerChat, message.into())); + } + + pub fn send_system(&mut self, message: impl Into<Text>) { + self.send(ChatMessage::new(ChatKind::System, message.into())); + } + + pub fn send_above_hotbar(&mut self, message: impl Into<Text>) { + self.send(ChatMessage::new(ChatKind::AboveHotbar, message.into())); + } + + /// Adds the [`Title`] to the title queue. + pub fn send_title(&mut self, title: Title) { + self.titles.push(title); + } + + /// Drains titles in the mailbox + pub fn drain_titles(&mut self) -> impl Iterator<Item = Title> + '_ { + self.titles.drain(..) + } + + /// Drains messages in the mailbox. + pub fn drain(&mut self) -> impl Iterator<Item = ChatMessage> + '_ { + let preference = self.preference; + self.messages + .drain(..) + .filter(move |msg| msg.kind.should_send(preference)) + } +} + +/// Represents a chat message. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct ChatMessage { + kind: ChatKind, + message: Text, +} + +impl ChatMessage { + pub fn new(kind: ChatKind, message: Text) -> Self { + Self { kind, message } + } + + pub fn kind(&self) -> ChatKind { + self.kind + } + + pub fn text(&self) -> &Text { + &self.message + } +} + +/// Kind of chat message. The client determines whether +/// to display a message based on this kind. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ChatKind { + /// A player chat message or similar. + PlayerChat, + /// The output of a command or other messages + /// not originating from players. + System, + /// A message displayed above the hotbar. + AboveHotbar, +} + +impl ChatKind { + pub fn should_send(self, preference: ChatPreference) -> bool { + match self { + ChatKind::PlayerChat => preference == ChatPreference::All, + ChatKind::System => preference >= ChatPreference::System, + ChatKind::AboveHotbar => true, + } + } +} + +/// A player's chat preference. +/// Determines which [`ChatKind`]s will +/// be sent to this player. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub enum ChatPreference { + /// Receive only game info messages. + GameInfoOnly, + /// Receive only messages from commands and game info messages. + System, + /// Receive all messages. + All, +} + +/// A player's real ip address +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, derive_more::Deref)] +pub struct RealIp(pub IpAddr); + +bincode_component_impl!(RealIp); + +/// Marker component for the console entity. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Console; + +bincode_component_impl!(Console); + +/// A player's previous gamemode +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + derive_more::Deref, + derive_more::DerefMut, +)] +pub struct DefaultGamemode(Gamemode); + +impl DefaultGamemode { + pub fn new(gamemode: Gamemode) -> DefaultGamemode { + DefaultGamemode(gamemode) + } +} + +bincode_component_impl!(DefaultGamemode); diff --git a/quill/common/src/events.rs b/quill/common/src/events.rs index 734d572cb..9965ff111 100644 --- a/quill/common/src/events.rs +++ b/quill/common/src/events.rs @@ -1,7 +1,11 @@ +pub use block_interact::*; +pub use change::*; +pub use disconnect::*; +pub use interact_entity::*; +pub use plugin_message::*; + mod block_interact; mod change; +mod disconnect; mod interact_entity; - -pub use block_interact::{BlockInteractEvent, BlockPlacementEvent}; -pub use change::{CreativeFlyingEvent, SneakEvent, SprintEvent}; -pub use interact_entity::InteractEntityEvent; +mod plugin_message; diff --git a/quill/common/src/events/change.rs b/quill/common/src/events/change.rs index f6bca77ac..25b11ce58 100644 --- a/quill/common/src/events/change.rs +++ b/quill/common/src/events/change.rs @@ -1,7 +1,10 @@ /* -All events in this file are triggerd when there is a change in a certain value. +All events in this file are triggered when there is a change in a certain value. */ +use crate::components::PreviousGamemode; +use derive_more::Deref; +use libcraft_core::Gamemode; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -42,3 +45,30 @@ impl SprintEvent { } } } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GamemodeUpdateEvent { + pub old: PreviousGamemode, + pub new: Gamemode, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TimeUpdateEvent { + pub old: u64, + pub new: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct InventoryUpdateEvent(pub Vec<usize>); + +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct InstabreakChangeEvent(pub bool); + +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct FlyingAbilityChangeEvent(pub bool); + +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct BuildingAbilityChangeEvent(pub bool); + +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct InvulnerabilityChangeEvent(pub bool); diff --git a/quill/common/src/events/disconnect.rs b/quill/common/src/events/disconnect.rs new file mode 100644 index 000000000..b1be0099b --- /dev/null +++ b/quill/common/src/events/disconnect.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use libcraft_text::Text; + +/// Disconnects the player with a specified kick message +#[derive( + Clone, Debug, PartialEq, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut, +)] +pub struct DisconnectEvent(Text); + +impl DisconnectEvent { + pub fn new(message: impl Into<Text>) -> DisconnectEvent { + DisconnectEvent(message.into()) + } +} diff --git a/quill/common/src/events/plugin_message.rs b/quill/common/src/events/plugin_message.rs new file mode 100644 index 000000000..ba39f13c5 --- /dev/null +++ b/quill/common/src/events/plugin_message.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct PluginMessageReceiveEvent { + pub channel: String, + pub data: Vec<u8>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct PluginMessageSendEvent { + pub channel: String, + pub data: Vec<u8>, +} diff --git a/quill/example-plugins/bungeecord-servers/Cargo.toml b/quill/example-plugins/bungeecord-servers/Cargo.toml new file mode 100644 index 000000000..a7693ffe0 --- /dev/null +++ b/quill/example-plugins/bungeecord-servers/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bungeecord-servers-plugin" +version = "0.1.0" +authors = ["Iaiao <iaiao.abc@gmail.com>"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +quill = { path = "../../api" } +anyhow = "1.0.44" +commands = { git = "https://github.com/Iaiao/commands", rev = "42bebd15e18cf511355aaf5f241fb4218031c4ee" } \ No newline at end of file diff --git a/quill/example-plugins/bungeecord-servers/src/lib.rs b/quill/example-plugins/bungeecord-servers/src/lib.rs new file mode 100644 index 000000000..2e3548b8e --- /dev/null +++ b/quill/example-plugins/bungeecord-servers/src/lib.rs @@ -0,0 +1,154 @@ +use std::io::{Cursor, ErrorKind, Read, Write}; + +use anyhow::bail; +use commands::arguments::*; + +use quill::entities::Player; +use quill::events::PluginMessageReceiveEvent; +use quill::{Caller, Game, Plugin, Setup, Text, TextComponentBuilder}; + +const BUNGEECORD: &str = "bungeecord:main"; + +quill::plugin!(BungeecordServersPlugin); + +struct BungeecordServersPlugin { + servers: Option<Vec<String>>, + server: Option<String>, + tick_counter: Option<usize>, +} + +impl Plugin for BungeecordServersPlugin { + fn enable(_game: &mut Game, setup: &mut Setup<Self>) -> Self { + let plugin = BungeecordServersPlugin { + servers: None, + server: None, + tick_counter: Some(0), + }; + + setup.add_system(get_servers); + + setup.with_dispatcher(move |dispatcher| { + dispatcher.register_tab_completion("bungeecord_servers:servers_without_this", move |text, context| { + ( + 0, + text.len(), + context.plugin.servers + .iter() + .flatten() + .filter(|&s| Some(s) != context.plugin.server.as_ref() && s.starts_with(text)) + .map(|s| (s.clone(), None)) + .collect() + ) + }); + dispatcher.create_command("test") + .argument("server", StringArgument::GREEDY_PHRASE, "bungeecord_servers:servers_without_this") + .executes(|context: CommandContext, target_server: &mut String| { + let (server, servers) = (context.plugin.server.as_ref(), context.plugin.servers.as_ref()); + if server.is_some() && *server.unwrap() == *target_server { + context.caller.send_message(format!("You are already on {}", target_server)); + bail!("Already on this server") + } else if servers.is_some() && servers.unwrap().contains(&target_server.to_string()) { + context.caller.send_message(format!("Sending you to {}", target_server)); + if let Caller::Player(entity) = context.caller { + context.game.send_plugin_message(entity.id(), BUNGEECORD, &{ + let mut vec = Vec::new(); + write_str("Connect", &mut vec)?; + write_str(target_server, &mut vec)?; + vec + }[..]); + Ok(1) + } else { + context.caller.send_message(Text::translate("permissions.requires.player").red()); + bail!("Requires a player") + } + } else { + context.caller.send_message( + Text::of(format!("Server \"{}\" doesn't exist! Choose one of these:", target_server)) + .extra(servers + .map(|servers| servers + .iter() + .filter(|s| server.is_none() || server.unwrap() != *s) + .map(|server| Text::of(format!(" [{}]", server)) + .on_hover_show_text("Click here!") + .on_click_run_command(format!("/test {}", server))) + .collect()) + .unwrap_or_else(|| vec![Text::of(" (not found any servers). Are you running a bungeecord server?")]))); + bail!("Server doesn't exist") + } + }); + }); + + plugin + } + + fn disable(self, _game: &mut Game) {} +} + +fn get_servers(plugin: &mut BungeecordServersPlugin, game: &mut Game) { + if plugin.servers.is_none() || plugin.server.is_none() { + if let Some(a) = plugin.tick_counter.as_mut() { + *a += 1; + } + if plugin.tick_counter.unwrap() % 10 == 0 { + if let Some((player, _)) = game.query::<&Player>().next() { + if plugin.servers.is_none() { + game.send_plugin_message( + player.id(), + BUNGEECORD, + &{ + let mut vec = Vec::new(); + write_str("GetServers", &mut vec).unwrap(); + vec + }[..], + ); + } + if plugin.server.is_none() { + game.send_plugin_message( + player.id(), + BUNGEECORD, + &{ + let mut vec = Vec::new(); + write_str("GetServer", &mut vec).unwrap(); + vec + }[..], + ); + } + } + } + for (_, plugin_message) in game.query::<&PluginMessageReceiveEvent>() { + if plugin_message.channel.as_str() == BUNGEECORD { + let mut data = Cursor::new(plugin_message.data); + match read_string(&mut data).unwrap().as_str() { + "GetServers" => { + plugin.servers = Some( + read_string(&mut data) + .unwrap() + .split(", ") + .map(ToOwned::to_owned) + .collect(), + ); + } + "GetServer" => { + plugin.server = Some(read_string(&mut data).unwrap()); + } + _ => (), + } + } + } + } +} + +pub fn write_str(str: &str, mut out: impl Write) -> std::io::Result<()> { + out.write_all(&(str.len() as u16).to_be_bytes())?; + out.write_all(str.as_bytes())?; + Ok(()) +} + +pub fn read_string(mut input: impl Read) -> std::io::Result<String> { + let mut buf = [0; 2]; + input.read_exact(&mut buf)?; + let len = u16::from_be_bytes(buf) as usize; + let mut buf = vec![0; len]; + input.read_exact(&mut buf)?; + String::from_utf8(buf).map_err(|err| std::io::Error::new(ErrorKind::InvalidInput, err)) +} diff --git a/quill/example-plugins/plugin-message/src/lib.rs b/quill/example-plugins/plugin-message/src/lib.rs index 5efb7ea77..aa19c093e 100644 --- a/quill/example-plugins/plugin-message/src/lib.rs +++ b/quill/example-plugins/plugin-message/src/lib.rs @@ -28,7 +28,7 @@ fn plugin_message_system(_plugin: &mut PluginMessage, game: &mut Game) { data.extend_from_slice(b"Connect"); data.extend_from_slice(&u16::to_be_bytes(5)); data.extend_from_slice(b"lobby"); - Game::send_plugin_message(entity.id(), "bungeecord:main", &data); + game.send_plugin_message(entity.id(), "bungeecord:main", &data); } } } diff --git a/quill/sys/Cargo.toml b/quill/sys/Cargo.toml index 1ff2e6c0a..29f543f62 100644 --- a/quill/sys/Cargo.toml +++ b/quill/sys/Cargo.toml @@ -5,5 +5,7 @@ authors = ["caelunshun <caelunshun@gmail.com>"] edition = "2018" [dependencies] +slab = "0.4.4" quill-common = { path = "../common" } quill-sys-macros = { path = "../sys-macros" } +commands = { git = "https://github.com/Iaiao/commands", rev = "42bebd15e18cf511355aaf5f241fb4218031c4ee" } \ No newline at end of file diff --git a/quill/sys/src/lib.rs b/quill/sys/src/lib.rs index 14e6e6176..853954c80 100644 --- a/quill/sys/src/lib.rs +++ b/quill/sys/src/lib.rs @@ -90,7 +90,7 @@ extern "C" { /// The given message should be in the JSON format. /// /// Does nothing if the entity does not exist or it does not have the `Chat` component. - pub fn entity_send_message(entity: EntityId, message_ptr: Pointer<u8>, message_len: u32); + pub fn entity_send_message(entity: EntityId, message_ptr: Pointer<u8>); /// Sends a title to an entity. /// @@ -174,4 +174,29 @@ extern "C" { data_ptr: Pointer<u8>, data_len: u32, ); + + /// Modifies command executor. `executors` have quill's CommandContexts + /// Arguments are emptied after this call + /// + /// Arguments: + /// Nodes: Vec<CommandNode> + /// Executors: Vec<Box<dyn Fn(Args, CommandContext) -> bool>>> + /// Tab completers: Vec<(String, Box<dyn Fn(&str, CommandContext) -> Vec<(String, Option<String>)>>)> + /// + /// Arguments are dropped on the host, so you shouldn't drop them after calling this method + #[allow(clippy::too_many_arguments)] + pub fn modify_command_executor( + nodes_ptr: PointerMut<u8>, + nodes_len: u32, + nodes_cap: u32, + executors_ptr: PointerMut<u8>, + executors_len: u32, + executors_cap: u32, + tab_completers_ptr: PointerMut<u8>, + tab_completers_len: u32, + tab_completers_cap: u32, + forks_ptr: PointerMut<u8>, + forks_len: u32, + forks_cap: u32, + ); }