diff --git a/.dockerignore b/.dockerignore index 5c5187748..0269ba472 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ # Allow the source code folders !/pumpkin*/ +!/assets # Dependencies !Cargo.lock diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1ef83c66f..df45c9d43 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: node-version: 20 cache: npm - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v5 - name: Install dependencies run: npm ci - name: Build with VitePress diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa557628e..05f54afe4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,8 @@ And in release: cargo run --no-default-features --release ``` -### Project Structure - -Before contributing, it would be helpful to get to know the project structure, for further information, visit [STRUCTURE.md](STRUCTURE.md) +### Docs +The Documentation of Pumpkin can be found at https://snowiiii.github.io/Pumpkin/ ### Additional Information diff --git a/Cargo.lock b/Cargo.lock index 1c1cbac2c..73b534e16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,19 +13,13 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli 0.29.0", + "gimli 0.31.1", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -81,9 +75,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "arbitrary" @@ -93,9 +87,9 @@ checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -110,23 +104,23 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ - "addr2line 0.22.0", - "cc", + "addr2line 0.24.2", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -170,11 +164,11 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.11.0-rc.1" +version = "0.11.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8969801e57d15e15bc4d7cdc5600dc15ca06a9a62b622bd4871c2d21d8aeb42d" +checksum = "939c0e62efa052fb0b2db2c0f7c479ad32e364c192c3aab605a7641de265a1a7" dependencies = [ - "crypto-common 0.2.0-rc.1", + "hybrid-array", ] [[package]] @@ -195,23 +189,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cap-fs-ext" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb23061fc1c4ead4e45ca713080fe768e6234e959f5a5c399c39eb41aa34e56e" +checksum = "712695628f77a28acd7c9135b9f05f9c1563f8eb91b317f63876bac550032403" dependencies = [ "cap-primitives", "cap-std", @@ -221,9 +209,9 @@ dependencies = [ [[package]] name = "cap-primitives" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d00bd8d26c4270d950eaaa837387964a2089a1c3c349a690a1fa03221d29531" +checksum = "ff5bcbaf57897c8f14098cc9ad48a78052930a9948119eea01b80ca224070fa6" dependencies = [ "ambient-authority", "fs-set-times", @@ -238,9 +226,9 @@ dependencies = [ [[package]] name = "cap-rand" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb16a619d8b8211ed61f42bd290d2a1ac71277a69cf8417ec0996fa92f5211" +checksum = "e7c780812948b31f362c3bab82d23b902529c26705d0e094888bc7fdb9656908" dependencies = [ "ambient-authority", "rand", @@ -248,9 +236,9 @@ dependencies = [ [[package]] name = "cap-std" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19eb8e3d71996828751c1ed3908a439639752ac6bdc874e41469ef7fc15fbd7f" +checksum = "e6cf1a22e6eab501e025a9953532b1e95efb8a18d6364bf8a4a7547b30c49186" dependencies = [ "cap-primitives", "io-extras", @@ -260,9 +248,9 @@ dependencies = [ [[package]] name = "cap-time-ext" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61142dc51e25b7acc970ca578ce2c3695eac22bbba46c1073f5f583e78957725" +checksum = "1e1547a95cd071db92382c649260bcc6721879ef5d1f0f442af33bff75003dd7" dependencies = [ "ambient-authority", "cap-primitives", @@ -292,9 +280,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.16" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" dependencies = [ "jobserver", "libc", @@ -396,9 +384,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -534,6 +522,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -553,6 +563,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -657,7 +676,7 @@ version = "0.11.0-pre.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" dependencies = [ - "block-buffer 0.11.0-rc.1", + "block-buffer 0.11.0-rc.2", "crypto-common 0.2.0-rc.1", ] @@ -747,9 +766,9 @@ dependencies = [ [[package]] name = "extism" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74614574b03f716c9d1439d46dbd993dc9ed778ebd149bec40a2d32142297b78" +checksum = "c352d53d63c58d66868b65246ec491dab9c78b7425d2d221f0219d97765a3548" dependencies = [ "anyhow", "cbindgen", @@ -773,9 +792,9 @@ dependencies = [ [[package]] name = "extism-convert" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573b553cad4f82bd5625825803744815f40de4524fac5a6a6d1f137ab60c878b" +checksum = "6b2042dab1fdb408d7504446cfb10079815da8b45e192a8954bf568c8a43e65d" dependencies = [ "anyhow", "base64 0.22.1", @@ -789,9 +808,9 @@ dependencies = [ [[package]] name = "extism-convert-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd2e4b0608d189ded6694a6c2c2da7522b54564ca143996b69cdda0ae56b7ac" +checksum = "8bb2f0038f2c3b14daa95b132f4fc1bad786a92946c7c9c06e120985a1fc4028" dependencies = [ "manyhow", "proc-macro-crate", @@ -802,9 +821,9 @@ dependencies = [ [[package]] name = "extism-manifest" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be4814f095a74d0547175bd9d8747106a03b21cfbe675f73457c0677a97042b" +checksum = "675c0d7e15bb5e6e2a520ea26c4309c047c30b16de852f26373de1906677a58d" dependencies = [ "base64 0.22.1", "serde", @@ -847,21 +866,21 @@ dependencies = [ [[package]] name = "fdeflate" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -870,6 +889,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -892,9 +917,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -907,9 +932,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -917,15 +942,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -934,15 +959,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -951,21 +976,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1035,9 +1060,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -1083,6 +1108,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "foldhash", +] + [[package]] name = "heck" version = "0.4.1" @@ -1140,15 +1174,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hybrid-array" -version = "0.2.0-rc.9" +version = "0.2.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d306b679262030ad8813a82d4915fc04efff97776e4db7f8eb5137039d56400" +checksum = "a5a41e5b0754cae5aaf7915f1df1147ba8d316fc6e019cfcc00fbaba96d5e030" dependencies = [ "typenum", ] @@ -1193,9 +1227,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -1206,16 +1240,15 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1250,26 +1283,14 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "image" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" -dependencies = [ - "bytemuck", - "byteorder-lite", - "num-traits", - "png", -] - [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.0", "serde", ] @@ -1300,9 +1321,9 @@ checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "itertools" @@ -1383,9 +1404,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libm" @@ -1409,6 +1430,16 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -1499,16 +1530,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", - "simd-adler32", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1516,6 +1537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1649,21 +1671,21 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "crc32fast", - "hashbrown 0.14.5", + "hashbrown 0.15.0", "indexmap", "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "overload" @@ -1671,6 +1693,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "paste" version = "1.0.15" @@ -1692,26 +1737,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1747,21 +1772,21 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.7.4", + "miniz_oxide", ] [[package]] @@ -1813,18 +1838,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", "prost-derive", @@ -1832,9 +1857,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", "itertools 0.13.0", @@ -1858,16 +1883,18 @@ version = "0.1.0-dev" dependencies = [ "base64 0.22.1", "bytes", + "crossbeam", "ctrlc", "digest 0.11.0-pre.9", "hmac", - "image", "itertools 0.13.0", "log", "mio", "num-bigint", "num-derive", "num-traits", + "parking_lot", + "png", "pumpkin-config", "pumpkin-core", "pumpkin-entity", @@ -1898,6 +1925,7 @@ dependencies = [ "log", "pumpkin-core", "serde", + "serde-inline-default", "toml", ] @@ -1922,9 +1950,11 @@ version = "0.1.0" name = "pumpkin-inventory" version = "0.1.0" dependencies = [ + "crossbeam", "itertools 0.13.0", "num-derive", "num-traits", + "parking_lot", "pumpkin-world", "thiserror", ] @@ -1933,8 +1963,11 @@ dependencies = [ name = "pumpkin-macros" version = "0.1.0" dependencies = [ + "itertools 0.13.0", "proc-macro2", "quote", + "serde", + "serde_json", "syn", ] @@ -1960,6 +1993,7 @@ dependencies = [ "log", "num-derive", "num-traits", + "pumpkin-config", "pumpkin-core", "pumpkin-macros", "pumpkin-world", @@ -1991,7 +2025,9 @@ dependencies = [ "noise", "num-derive", "num-traits", + "parking_lot", "pumpkin-core", + "pumpkin-macros", "rand", "rayon", "serde", @@ -2117,6 +2153,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -2143,14 +2188,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -2164,13 +2209,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -2181,15 +2226,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "base64 0.22.1", "bytes", @@ -2315,9 +2360,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.36" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -2330,9 +2375,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "log", "once_cell", @@ -2345,25 +2390,24 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" -version = "0.102.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -2376,6 +2420,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.23" @@ -2387,13 +2437,24 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-inline-default" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484b43bb1114a28d1a574f5682d6079fa4c20e76faaff0cfd048216650e57101" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_bytes" version = "0.11.15" @@ -2405,9 +2466,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -2428,9 +2489,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -2609,9 +2670,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -2672,9 +2733,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -2694,18 +2755,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -2844,9 +2905,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -2855,27 +2916,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" @@ -2958,42 +2998,42 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -3170,9 +3210,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.216.0" +version = "0.218.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c23aebea22c8a75833ae08ed31ccc020835b12a41999e58c31464271b94a88" +checksum = "22b896fa8ceb71091ace9bcb81e853f54043183a1c9667cf93422c40252ffa0a" dependencies = [ "leb128", ] @@ -3469,24 +3509,24 @@ dependencies = [ [[package]] name = "wast" -version = "216.0.0" +version = "218.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7eb1f2eecd913fdde0dc6c3439d0f24530a98ac6db6cb3d14d92a5328554a08" +checksum = "8a53cd1f0fa505df97557e36a58bddb8296e2fcdcd089529545ebfdb18a1b9d7" dependencies = [ "bumpalo", "leb128", "memchr", "unicode-width", - "wasm-encoder 0.216.0", + "wasm-encoder 0.218.0", ] [[package]] name = "wat" -version = "1.216.0" +version = "1.218.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0409090fb5154f95fb5ba3235675fd9e579e731524d63b6a2f653e1280c82a" +checksum = "4f87f8e14e776762e07927c27c2054d2cf678aab9aae2d431a79b3e31e4dd391" dependencies = [ - "wast 216.0.0", + "wast 218.0.0", ] [[package]] @@ -3501,9 +3541,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] @@ -3788,9 +3828,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 38d79ccf3..8577de730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,14 +25,21 @@ codegen-units = 1 [workspace.dependencies] log = "0.4" tokio = { version = "1.40", features = [ - "net", - "macros", - "rt-multi-thread", "fs", "io-util", + "macros", + "net", + "rt-multi-thread", "sync", ] } + +# Concurrency/Parallelism and Synchronization rayon = "1.10.0" +parking_lot = "0.12.3" +crossbeam = "0.8.4" + uuid = { version = "1.10.0", features = ["serde", "v3", "v4"] } derive_more = { version = "1.0.0", features = ["full"] } serde = { version = "1.0", features = ["derive"] } + +itertools = "0.13.0" diff --git a/Dockerfile b/Dockerfile index 69631ab81..f1fb3ddd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1-alpine3.19 AS builder +FROM rust:1-alpine3.20 AS builder ENV RUSTFLAGS="-C target-feature=-crt-static -C target-cpu=native" RUN apk add --no-cache musl-dev WORKDIR /pumpkin @@ -6,8 +6,9 @@ COPY . /pumpkin RUN cargo build --release RUN strip target/release/pumpkin -FROM alpine:3.19 +FROM alpine:3.20 WORKDIR /pumpkin RUN apk add --no-cache libgcc COPY --from=builder /pumpkin/target/release/pumpkin /pumpkin/pumpkin +EXPOSE 25565 ENTRYPOINT ["/pumpkin/pumpkin"] diff --git a/README.md b/README.md index e45ccd31e..2c23edd84 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ -Pumpkin is a Minecraft server built entirely in Rust, offering a fast, efficient, +[Pumpkin](https://snowiiii.github.io/Pumpkin/) is a Minecraft server built entirely in Rust, offering a fast, efficient, and customizable experience. It prioritizes performance and player enjoyment while adhering to the core mechanics of the game. ![image](https://github.com/user-attachments/assets/7e2e865e-b150-4675-a2d5-b52f9900378e) @@ -24,7 +24,8 @@ and customizable experience. It prioritizes performance and player enjoyment whi ## What Pumpkin will not -- Provide compatibility with Vanilla or Bukkit servers (including configs and plugins). +- Be a drop-in replacement for vanilla or other servers +- Be compatible with plugins or mods for other servers - Function as a framework for building a server from scratch. > [!IMPORTANT] @@ -73,55 +74,22 @@ and customizable experience. It prioritizes performance and player enjoyment whi Check out our [Github Project](https://github.com/users/Snowiiii/projects/12/views/3) to see current progress ## How to run - -There are currently no release builds, because there was no release :D. - -To get Pumpkin running you first have to clone it: - -```shell -git clone https://github.com/Snowiiii/Pumpkin.git -cd Pumpkin -``` - -You also may have to [install rust](https://www.rust-lang.org/tools/install) when you don't already have. - -You can place a vanilla world into the Pumpkin/ directory when you want. Just name the World to `world` - -Then run: - -> [!NOTE] -> This can take a while. Because we enabled heavy optimizations for release builds -> -> To apply further optimizations specfic to your CPU and use your CPU features. You should set the target-cpu=native -> Rust flag. - -```shell -cargo run --release -``` - -### Docker - -Experimental Docker support is available. -The image is currently not published anywhere, but you can use the following command to build it: - -```shell -docker build . -t pumpkin -``` - -To run it use the following command: - -```shell -docker run --rm -v "./world:/pumpkin/world" pumpkin -``` +See https://snowiiii.github.io/Pumpkin/about/quick-start.html ## Contributions Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) +## Docs +The Documentation of Pumpkin can be found at https://snowiiii.github.io/Pumpkin/ + ## Communication Consider joining our [discord](https://discord.gg/wT8XjrjKkf) to stay up-to-date on events, updates, and connect with other members. +## Funding +If you want to fund me and help the project, Check out my [GitHub sponsors](https://github.com/sponsors/Snowiiii) + ## Thanks A big thanks to [wiki.vg](https://wiki.vg/) for providing valuable information used in the development of this project. diff --git a/STRUCTURE.md b/STRUCTURE.md deleted file mode 100644 index 52f2208f5..000000000 --- a/STRUCTURE.md +++ /dev/null @@ -1,17 +0,0 @@ -# Project Structure - -## Overview - -Pumpkin is split into multiple crates, thus having a set project structure between contributors is essential. - -## Pumpkin-Core - -The core crate has some special rules that only apply to it: - -- It may not depend on any other pumpkin crate -- There may not be any files directly under src/, except for the mod.rs file (this is to help with organisation) - -## Other crate rules - -- [`pumpkin-protocol`](/pumpkin-protocol/) - contains definitions for packet types **and** their serialization (be it through serde, or manually implementing `ClientPacket`/`ServerPacket`), only the `pumpkin` crate may depend on this -- `pumpkin-macros` - similarly to `pumpkin-core`, it may not depend on any other pumpkin crate diff --git a/pumpkin-world/assets/blocks.json b/assets/blocks.json similarity index 100% rename from pumpkin-world/assets/blocks.json rename to assets/blocks.json diff --git a/pumpkin-world/assets/items.json b/assets/items.json similarity index 100% rename from pumpkin-world/assets/items.json rename to assets/items.json diff --git a/pumpkin-world/assets/registries.json b/assets/registries.json similarity index 100% rename from pumpkin-world/assets/registries.json rename to assets/registries.json diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index fe7139230..be407364b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -9,26 +9,42 @@ export default defineConfig({ base: "/Pumpkin/", themeConfig: { // https://vitepress.dev/reference/default-theme-config + search: { + provider: "local", + }, sidebar: [ { text: "About", items: [ { text: "Introduction", link: "/about/introduction" }, { text: "Quick Start", link: "/about/quick-start" }, + ], + }, + { + text: "Developers", + items: [ { text: "Contributing", link: "https://github.com/Snowiiii/Pumpkin/blob/master/CONTRIBUTING.md", }, + { text: "Introduction", link: "/developer/introduction" }, + { text: "Networking", link: "/developer/networking" }, + { text: "Authentication", link: "/developer/authentication" }, + ], + }, + + { + text: "Configuration", + items: [ + { text: "Introduction", link: "/config/introduction" }, + { text: "Basic", link: "/config/basic" }, + { text: "Advanced", link: "/config/advanced" }, ], }, { - text: "Plugins", + text: "Troubleshooting", items: [ - { text: "About Plugins", link: "/plugins/about" }, - { - text: "Getting Started in Rust", - link: "/plugins/getting-started-rs", - }, + { text: "Common Issues", link: "/troubleshooting/common_issues.md" }, ], }, ], @@ -39,6 +55,22 @@ export default defineConfig({ ], logo: "/assets/icon.png", + footer: { + message: "Released under the MIT License.", + copyright: "Copyright © 2024-present Aleksandr Medvedev", + }, + editLink: { + pattern: "https://github.com/Snowiiii/Pumpkin/blob/master/docs/:path", + text: "Edit this page on GitHub", + }, + lastUpdated: { + text: "Updated at", + formatOptions: { + dateStyle: "medium", + timeStyle: "medium", + }, + }, + outline: "deep" }, head: [["link", { rel: "icon", href: "/assets/favicon.ico" }]], }); diff --git a/docs/about/introduction.md b/docs/about/introduction.md index ffb6f8b28..42ed30c2a 100644 --- a/docs/about/introduction.md +++ b/docs/about/introduction.md @@ -15,7 +15,8 @@ and customizable experience. It prioritizes performance and player enjoyment whi ## What Pumpkin will not -- Provide compatibility with Vanilla or Bukkit servers (including configs and plugins). +- Be a drop-in replacement for vanilla or other servers +- Be compatible with plugins or mods for other servers - Function as a framework for building a server from scratch. > [!IMPORTANT] diff --git a/docs/config/advanced.md b/docs/config/advanced.md new file mode 100644 index 000000000..60fd99047 --- /dev/null +++ b/docs/config/advanced.md @@ -0,0 +1,427 @@ +### Advanced Configuration + +### Proxy + +`proxy` + +Wether Proxy Configuration is enabled + +```toml +enabled=false +``` + +#### Velocity + +`proxy.velocity` + +Wether [Velocity](https://papermc.io/software/velocity) Proxy is enabled + +> [!IMPORTANT] +> Velocity support is currently WIP + +```toml +enabled=false +``` + +##### Velocity Secret + +This secret is used to ensure that player info forwarded by Velocity comes from your proxy and not from someone pretending to run Velocity + +```toml +secret= +``` + +### Authentication + +`authentication` + +Wether Authentication is enabled + +```toml +enabled=false +``` + +#### Authentication URL + +The Authentication URL being used + +> [!IMPORTANT] +> {username} | The Username from the requested player +> +> {server_hash} | The SHA1 Encrypted hash + +```toml +auth_url="https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}" +``` + +#### Prevent Proxy Connections + +Prevent proxy connections + +```toml +prevent_proxy_connections=false +``` + +#### Prevent Proxy Connections URL + +The Authentication URL being used + +> [!IMPORTANT] +> {username} | The Username from the requested player +> +> {server_hash} | The SHA1 Encrypted hash +> +> {ip} | The IP of the requested Player + +```toml +prevent_proxy_connection_auth_url = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}" +``` + +#### Player Profile + +`authentication.player_profile` + +##### Allow Banned Players + +Allow players flagged by Mojang (banned, forced name change) + +```toml +allow_banned_players=false +``` + +##### Allowed Actions + +Depends on the value above + +```toml +allowed_actions=["FORCED_NAME_CHANGE", "USING_BANNED_SKIN"] +``` + +```toml +FORCED_NAME_CHANGE +USING_BANNED_SKIN +``` + +#### Textures + +`authentication.textures` + +Whether to filter/validate player textures (e.g. Skins/Capes) + +```toml +enabled=true +``` + +##### Allowed URL Schemes + +Allowed URL Schemes for Textures + +```toml +allowed_url_schemes=["http", "https"] +``` + +##### Allowed URL Domains + +Allowed URL domains for Textures + +```toml +allowed_url_domains=[".minecraft.net", ".mojang.com"] +``` + +#### Texture Types + +`authentication.textures.types` + +##### Skin + +Use player skins + +```toml +skin=true +``` + +##### Cape + +Use player capes + +```toml +cape=true +``` + +##### Elytra + +Use player elytras +(i didn't know myself that there are custom elytras) + +```toml +elytra=true +``` + +### Compression + +`packet_compression` + +Wether Packet Compression is enabled + +```toml +enable=true +``` + +#### Compression Info + +##### Threshold + +The compression threshold used when compression is enabled + +```toml +threshold=256 +``` + +##### Level + +The Compression Level + +> [!IMPORTANT] +> A value between 0..9 +> +> 1 = Optimize for the best speed of encoding. +> +> 9 = Optimize for the size of data being encoded. + +```toml +level=4 +``` + +### Resource Pack + +`resource_pack` + +Wether a Resource Pack is enabled + +```toml +enable=false +``` + +#### Resource Pack URL + +The download URL of the resource pack + +```toml +resource_pack_url= +``` + +#### Resource Pack SHA1 + +The SHA1 hash (40) of the resource pack + +```toml +resource_pack_sha1= +``` + +#### Prompt Message + +Custom prompt Text component, Leave blank for none + +```toml +prompt_message= +``` + +#### Force + +Will force the Player to accept the resource pack + +```toml +force=false +``` + +### Commands + +`commands` + +#### Use Console + +Are commands from the Console accepted + +```toml +use_console=true +``` + +#### Log Console + +Should be commands from players be logged in console + +```toml +log_console=true +``` + +### RCON Config + +`rcon` + +Wether RCON is enabled + +```toml +enable=false +``` + +#### Address + +The network address and port where the RCON server will listen for connections + +```toml +address=false +``` + +#### Password + +The password required for RCON authentication + +```toml +password= +``` + +#### Maximum Connections + +The maximum number of concurrent RCON connections allowed + +If 0 there is no limit + +```toml +max_connections=0 +``` + +#### RCON Logging + +`rcon.logging` + +##### Logged Successfully + +Whether successful RCON logins should be logged + +```toml +log_logged_successfully=true +``` + +##### Wrong Password + +Whether failed RCON login attempts with incorrect passwords should be logged + +```toml +log_wrong_password=true +``` + +##### Commands + +Whether all RCON commands, regardless of success or failure, should be logged + +```toml +log_commands=true +``` + +##### Disconnect + +Whether RCON client quit should be logged + +```toml +log_quit=true +``` + +### PVP + +`pvp` + +Whether PVP is enabled + +```toml +enable=true +``` + +#### Hurt Animation + +Do we want to have the Red hurt animation & fov bobbing + +```toml +hurt_animation=true +``` + +#### Protect Creative + +Should players in creative be protected against PVP + +```toml +protect_creative=true +``` + +#### Knockback + +Has PVP Knockback (Velocity) + +```toml +knockback=true +``` + +#### Swing + +Should player swing when attacking + +```toml +swing=true +``` + +### Logging + +`logging` +Whether Logging is enabled + +```toml +enable=true +``` + +#### Level + +At which level should be logged + +```toml +level=Info +``` + +```toml +Off +Error +Warn +Info +Debug +Trace +``` + +#### Env + +Enables the user to choose log level by setting `RUST_LOG=` environment variable + +```toml +env=false +``` + +#### Threads + +Should threads be printed in the message + +```toml +threads=true +``` + +#### Color + +Should color be enabled for logging messages + +```toml +color=true +``` + +#### Timestamp + +Should the timestamp be printed in the message + +```toml +timestamp=true +``` diff --git a/docs/config/basic.md b/docs/config/basic.md new file mode 100644 index 000000000..ac8b118fc --- /dev/null +++ b/docs/config/basic.md @@ -0,0 +1,117 @@ +### Basic Configuration + +Representing `configuration.toml` + +### Server Address + +The address to bind the server to + +```toml +server_address=0.0.0.0 +``` + +### Seed + +The seed for world generation + +```toml +seed= +``` + +### Max players + +The maximum number of players allowed on the server + +```toml +max_players=10000 +``` + +### View distance + +The maximum view distance for players + +```toml +view_distance=10 +``` + +### Simulation distance + +The maximum simulation distance for players + +```toml +simulation_distance=10 +``` + +### Default difficulty + +The default game difficulty + +```toml +default_difficulty=Normal +``` + +```toml +Peaceful +Easy +Normal +Hard +``` + +### Allow nether + +Whether the Nether dimension is enabled + +```toml +allow_nether=true +``` + +### Hardcore + +Whether the server is in hardcore mode. + +```toml +hardcore=true +``` + +### Online Mode + +Whether online mode is enabled. Requires valid Minecraft accounts + +```toml +online_mode=true +``` + +### Encryption + +Whether packet encryption is enabled + +> [!IMPORTANT] +> Required when online mode is enabled + +```toml +encryption=true +``` + +### Motd + +The server's description displayed on the status screen. + +```toml +motd=true +``` + +### Default gamemode + +The default game mode for players + +```toml +default_gamemode=Survival +``` + +```toml +Undefined +Survival +Creative +Adventure +Spectator +``` diff --git a/docs/config/introduction.md b/docs/config/introduction.md new file mode 100644 index 000000000..b7cd31e68 --- /dev/null +++ b/docs/config/introduction.md @@ -0,0 +1,16 @@ +### Configuration + +Pumpkin offers a robust configuration system that allows users to customize various aspects of the server's behavior without relying on external plugins. This provides flexibility and control over the server's operation. + +### Basic / Advanced + +Pumpkin's Configuration is split into a basic Configuration made for quick changes and important changes and a more Advanced Configuration + +- `configuration.toml`: simple and can be compared to the vanilla `server.properties`. +- `features.toml`: designed to have all features of pumpkin at one place, making it a large configuration + +#### Key Features: + +- Extensive Customization: Configure server settings, player behavior, world generation, and more. +- Performance Optimization: Optimize server performance through configuration tweaks. +- Plugin-Free Customization: Achieve desired changes without the need for additional plugins. diff --git a/docs/developer/authentication.md b/docs/developer/authentication.md new file mode 100644 index 000000000..acbc8b1aa --- /dev/null +++ b/docs/developer/authentication.md @@ -0,0 +1,73 @@ +### Authentication + +### Why Authentication + +Minecraft is the most Popular game out there, And is is very easy to play it without paying for it. In Fact you don't pay for the Game, You pay for an Minecraft Account. +People who don't bough the Game but play online are using [Cracked Accounts](#cracked-accounts) + +#### Cracked Accounts + +- Don't cost any Money +- Everyone can set their own Nickname +- Have no UUID +- Have no Skin/Cape +- Not Secure + +The Problem is that everyone can name themself how they want, Allowing to Join the Server as a Staff Member for example and having extended permissions, +Cracked accounts are also often used for Botting and [Denial of Service](https://de.wikipedia.org/wiki/Denial_of_Service) Attacks. + +### Cracked Server + +By default the `online_mode` is enabled in the configuration, This enables Authentication disallowing [Cracked Accounts](#cracked-accounts). When you are willing to allow Cracked Accounts, you can dissable `online_mode` +in the `configuration.toml` + +### How Mojang Authentication works + +To ensure a player has a premium accounts: + +1. A client with a premium account sends a login request to the Mojang session server. +2. **Mojang's servers** verify the client's credentials and add the player to the their Servers +3. Now our server will send a Request to the Session servers and check if the Player has joined the Session Server. +4. If the request was successfull, It will give use more information about the Player (e.g. UUID, Name, Skin/Cape...) + +### Custom Authentication Server + +Pumpkin does support custom Authentication servers, You can replace the Authentication URL in `features.toml`. + +#### How Pumpkin Authentication Works + +1. **GET Request:** Pumpkin sends a GET request to the specified authentication URL. + +2. **Status Code 200:** If the authentication is successful, the server responds with a status code of 200. + +3. **Parse JSON Game Profile:** Pumpkin parses the JSON game profile returned in the response. + +#### Game Profile + +```rust +struct GameProfile { + id: UUID, + name: String, + properties: Vec, + profile_actions: Option>, // Optional, Only present when actions are applied +} +``` + +##### Property + +```rust +struct Property { + name: String, + value: String, // Base64 encoded + signature: Option, // Optional, Base64 encoded +} +``` + +##### Profile Action + +```rust +enum ProfileAction { + FORCED_NAME_CHANGE, + USING_BANNED_SKIN, +} +``` diff --git a/docs/developer/introduction.md b/docs/developer/introduction.md new file mode 100644 index 000000000..c1670a622 --- /dev/null +++ b/docs/developer/introduction.md @@ -0,0 +1,8 @@ +### Introduction + +Welcome to the Pumpkin Documentation! + +Whether you're an internal Pumpkin developer or working on a Pumpkin plugin, this documentation is your resource for everything Pumpkin. + +> [!IMPORTANT] +> While Pumpkin currently doesn't have plugin support yet, this documentation provides valuable insights into the platform's architecture and functionality, which can be helpful for understanding how to create potential future plugins. diff --git a/docs/developer/networking.md b/docs/developer/networking.md new file mode 100644 index 000000000..824c5045d --- /dev/null +++ b/docs/developer/networking.md @@ -0,0 +1,272 @@ +### Networking + +Most of the Networking code in Pumpkin, can be found at [Pumpkin-Protocol](https://github.com/Snowiiii/Pumpkin/tree/master/pumpkin-protocol) + +Serverbound: Client->Server + +Clientbound: Server->Client + +### Structure + +Packets in the Pumpkin protocol are organized by functionality and state. + +`server`: Contains definitions for serverbound packets. + +`client`: Contains definitions for clientbound packets. + +### States + +**Handshake**: Always the first packet being send from the Client. This begins also determins the next state, usally to indicate if the player thans perform a Status Request, Join the Server or wants to be transfered. + +**Status**: Indicates the Client wants to see a Status response (MOTD). + +**Login**: The Login sequence. Indicates the Client wants to join to the Server + +**Config**: A sequence of Configuration packets beining mostly send from the Server to the Client. (Features, Resource Pack, Server Links...) + +**Play**: The final state which indicate the Player is now ready to Join in also used to handle all other Gameplay packets. + +### Minecraft Protocol + +You can find all Minecraft Java packets at https://wiki.vg/Protocol. There you also can see in which [State](#States) they are. +You also can see all the information the Packets has which we can either Write or Read depending if its Serverbound or Clientbound + +### Adding a Clientbound Packet + +1. Adding a Packet is easy. First you have to dereive serde Serialize for packets. + +```rust +#[derive(Serialize)] +``` + +2. Next you have set the packet id using the packet macro + +```rust +#[packet(0x1D)] +``` + +3. Now you can create the Struct. + +> [!IMPORTANT] +> Please start the Packet name with "C" for Clientbound. +> Also please add the State to the packet if its a Packet sended in multiple States, For example there are 3 Disconnect Packets. +> +> - CLoginDisconnect +> - CConfigDisconnect +> - CPlayDisconnect + +Create fields within your packet structure to represent the data that will be sent to the client. + +> [!IMPORTANT] +> Use descriptive field names and appropriate data types. + +Example: + +```rust +pub struct CPlayDisconnect { + reason: TextComponent, + more fields... +} +``` + +4. Also don't forgot to impl a new function for Clientbound Packets so we can actaully send then by putting in the values + +Example: + +```rust +impl CPlayDisconnect { + pub fn new(reason: TextComponent) -> Self { + Self { reason } + } +} +``` + +5. At the End everything should come together, + +```rust +#[derive(Serialize)] +#[packet(0x1D)] +pub struct CPlayDisconnect { + reason: TextComponent, +} + +impl CPlayDisconnect { + pub fn new(reason: TextComponent) -> Self { + Self { reason } + } +} +``` + +6. You can also Serialize the Packet manually, Which can be usefull if the Packet is more complex + +```diff +-#[derive(Serialize)] + ++ impl ClientPacket for CPlayDisconnect { ++ fn write(&self, bytebuf: &mut crate::bytebuf::ByteBuffer) { ++ bytebuf.put_slice(&self.reason.encode()); ++ } +``` + +7. You can now send the Packet. See [Sending Packets](#sending-packets) + +### Adding a Serverbound Packet + +1. Adding a Packet is easy. First you have to dereive serde Deserialize for packets. + +```rust +#[derive(Deserialize)] +``` + +2. Next you have set the packet id using the packet macro + +```rust +#[packet(0x1A)] +``` + +3. Now you can create the Struct. + +> [!IMPORTANT] +> Please start the Packet name with "S" for Serverbound. +> Also please add the State to the packet if its a Packet sended in multiple States. + +Create fields within your packet structure to represent the data that will be sent to the client. + +> [!IMPORTANT] +> Use descriptive field names and appropriate data types. + +Example: + +```rust +pub struct SPlayerPosition { + pub x: f64, + pub feet_y: f64, + pub z: f64, + pub ground: bool, +} +``` + +4. At the End everything should come together, + +```rust +#[derive(Deserialize)] +#[packet(0x1A)] +pub struct SPlayerPosition { + pub x: f64, + pub feet_y: f64, + pub z: f64, + pub ground: bool, +} +``` + +5. You can also Deserialize the Packet manually, Which can be usefull if the Packet is more complex + +```diff +-#[derive(Deserialize)] + ++ impl ServerPacket for SPlayerPosition { ++ fn read(bytebuf: &mut ByteBuffer) -> Result { ++ Ok(Self { ++ x: bytebuf.get_f64()?, ++ feet_y: bytebuf.get_f64()?, ++ z: bytebuf.get_f64()?, ++ ground: bytebuf.get_bool()?, ++ }) ++ } +``` + +6. You can listen for the Packet. See [Receive Packets](#receiving-packets) + +### Client + +Pumpkin has stores Client and Players seperatly, Everything what is not reached the Play State is a Simple Client. Here are the Differences + +**Client** + +- Can only be in Status/Login/Transfer/Config State +- Is not a living entity +- Has small resource consumption + +**Player** + +- Can only be in Play State +- Is a living entity in a world +- Has more data, Consumes more resources + +#### Sending Packets + +Example: + +```rust +// Works only in Status State +client.send_packet(&CStatusResponse::new("{ description: "A Description"}")); +``` + +#### Receiving Packets + +For Clients: +`src/client/mod.rs` + +```diff +// Put the Packet into the right State + fn handle_mystate_packet( + &self, + server: &Arc, + packet: &mut RawPacket, +) -> Result<(), DeserializerError> { + let bytebuf = &mut packet.bytebuf; + match packet.id.0 { + SHandShake::PACKET_ID => { + self.handle_handshake(server, SHandShake::read(bytebuf)?); + Ok(()) + } ++ MyPacket::PACKET_ID => { ++ self.handle_mypacket(server, MyPacket::read(bytebuf)?); ++ Ok(()) ++ } + _ => { + log::error!( + "Failed to handle packet id {} while in ... state", + packet.id.0 + ); + Ok(()) + } + } +} +``` + +For Players: +`src/entity/player.rs` + +```diff +// Players only have Play State + fn handle_play_packet( + &self, + server: &Arc, + packet: &mut RawPacket, +) -> Result<(), DeserializerError> { + let bytebuf = &mut packet.bytebuf; + match packet.id.0 { + SHandShake::PACKET_ID => { + self.handle_handshake(server, SHandShake::read(bytebuf)?); + Ok(()) + } ++ MyPacket::PACKET_ID => { ++ self.handle_mypacket(server, MyPacket::read(bytebuf)?); ++ Ok(()) ++ } + _ => { + log::error!( + "Failed to handle packet id {} while in ... state", + packet.id.0 + ); + Ok(()) + } + } +} +``` + +### Porting + +To port to a new Minecraft version, You can compare difference in Protocol on wiki.vg https://wiki.vg/index.php?title=Protocol&action=history +Also change the `CURRENT_MC_PROTOCOL` in `src/lib.rs` diff --git a/docs/index.md b/docs/index.md index 8a34490e0..2359e0c8d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,11 +11,11 @@ hero: text: Quick Start link: /about/quick-start - theme: alt - text: Documentation - link: /about/introduction + text: Configuration + link: /config/introduction - theme: alt text: For developers - link: /plugins/about + link: /developer/introduction features: - title: Written in Rust diff --git a/docs/plugins/about.md b/docs/plugins/about.md deleted file mode 100644 index 110fe955a..000000000 --- a/docs/plugins/about.md +++ /dev/null @@ -1,15 +0,0 @@ -# Plugins - -Pumpkin uses [Extism](https://extism.org/) for loading plugins. -This means that you can write your plugins in any language that can compile to Extism WASM. -These languages include: - -- Rust -- JavaScript / TypeScript -- Golang -- C# -- F# -- C -- Haskell -- Zig -- AssemblyScript diff --git a/docs/plugins/getting-started-rs.md b/docs/plugins/getting-started-rs.md deleted file mode 100644 index 5bed83b39..000000000 --- a/docs/plugins/getting-started-rs.md +++ /dev/null @@ -1,4 +0,0 @@ -# Getting Started in Rust - -Rust in one of the supported plugin languages. -This page has not been written yet. diff --git a/docs/troubleshooting/common_issues.md b/docs/troubleshooting/common_issues.md new file mode 100644 index 000000000..881cae1da --- /dev/null +++ b/docs/troubleshooting/common_issues.md @@ -0,0 +1,33 @@ +### Common Issues + +1. ### Broken Chunk Lighting + + See [#93](https://github.com/Snowiiii/Pumpkin/issues/93) + + **Issue:** Broken chunk lighting in your Minecraft server. + + **Cause:** The server is currently not calculating lighting for chunks, we working on that. + + **Temporary Fix:** Use a full-bright resource pack. This will temporarily resolve the issue by making all chunks appear brightly lit. You can find many full-bright resource packs online. + +2. ### I can place blocks inside me + + See [#49](https://github.com/Snowiiii/Pumpkin/issues/49) + + **Issue:** Players are able to place block in them. + + **Cause:** The server is currently not calculating hitboxes for blocks, we working on that. + +3. ### Server is unresponsive + + **Issue:** You have to wait before reconnect or can't do basic things while chunks are loading. + + **Cause:** The server has currently blocking issues, we working on that. + +4. ### Failed to verify username + + **Issue:** Some players reported having issues loggin into the Server, Having "Failed to verify username" error. + + **Cause:** This has to do with Authentication, Usally with the prevent proxy connections setting. + + **Fix:** Disable `prevent_proxy_connections` in `features.toml` diff --git a/package-lock.json b/package-lock.json index 2325a32a8..df845bdb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,18 @@ { - "name": "Pumpkin", + "name": "pumpkin", "lockfileVersion": 3, "requires": true, "packages": { "": { "devDependencies": { - "vitepress": "^1.3.4", - "vue": "^3.4.38" + "vitepress": "^1.4.1", + "vue": "^3.5.12" } }, "node_modules/@algolia/autocomplete-core": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", + "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", "dev": true, "license": "MIT", "dependencies": { @@ -20,6 +22,8 @@ }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", + "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", "dev": true, "license": "MIT", "dependencies": { @@ -31,6 +35,8 @@ }, "node_modules/@algolia/autocomplete-preset-algolia": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz", + "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==", "dev": true, "license": "MIT", "dependencies": { @@ -43,6 +49,8 @@ }, "node_modules/@algolia/autocomplete-shared": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", + "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -52,6 +60,8 @@ }, "node_modules/@algolia/cache-browser-local-storage": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.24.0.tgz", + "integrity": "sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==", "dev": true, "license": "MIT", "dependencies": { @@ -60,11 +70,15 @@ }, "node_modules/@algolia/cache-common": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.24.0.tgz", + "integrity": "sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==", "dev": true, "license": "MIT" }, "node_modules/@algolia/cache-in-memory": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.24.0.tgz", + "integrity": "sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==", "dev": true, "license": "MIT", "dependencies": { @@ -73,6 +87,8 @@ }, "node_modules/@algolia/client-account": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.24.0.tgz", + "integrity": "sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==", "dev": true, "license": "MIT", "dependencies": { @@ -81,8 +97,33 @@ "@algolia/transporter": "4.24.0" } }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-search": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, "node_modules/@algolia/client-analytics": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.24.0.tgz", + "integrity": "sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==", "dev": true, "license": "MIT", "dependencies": { @@ -92,8 +133,10 @@ "@algolia/transporter": "4.24.0" } }, - "node_modules/@algolia/client-common": { + "node_modules/@algolia/client-analytics/node_modules/@algolia/client-common": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", "dev": true, "license": "MIT", "dependencies": { @@ -101,8 +144,10 @@ "@algolia/transporter": "4.24.0" } }, - "node_modules/@algolia/client-personalization": { + "node_modules/@algolia/client-analytics/node_modules/@algolia/client-search": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", "dev": true, "license": "MIT", "dependencies": { @@ -111,23 +156,68 @@ "@algolia/transporter": "4.24.0" } }, + "node_modules/@algolia/client-common": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.8.1.tgz", + "integrity": "sha512-MLX/gipPFEhJPCExsxXf9tnt+kLfWCe9JWRp1adcoVySkhzPxpIeSiWaQaOqyy0TYIgIpdeVx/emlBT9Ni8GFw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.24.0.tgz", + "integrity": "sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-personalization/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, "node_modules/@algolia/client-search": { - "version": "4.24.0", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.8.1.tgz", + "integrity": "sha512-zy3P4fI28GfzKihUw5+L76pEedQxyLDiMsdDYEWghIz8yAnELDatPNEThyWuUk8fD0PeVoCi1M4tr1iz00fOtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@algolia/client-common": "4.24.0", - "@algolia/requester-common": "4.24.0", - "@algolia/transporter": "4.24.0" + "@algolia/client-common": "5.8.1", + "@algolia/requester-browser-xhr": "5.8.1", + "@algolia/requester-fetch": "5.8.1", + "@algolia/requester-node-http": "5.8.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/logger-common": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.24.0.tgz", + "integrity": "sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==", "dev": true, "license": "MIT" }, "node_modules/@algolia/logger-console": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.24.0.tgz", + "integrity": "sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==", "dev": true, "license": "MIT", "dependencies": { @@ -136,6 +226,8 @@ }, "node_modules/@algolia/recommend": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.24.0.tgz", + "integrity": "sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==", "dev": true, "license": "MIT", "dependencies": { @@ -152,29 +244,102 @@ "@algolia/transporter": "4.24.0" } }, - "node_modules/@algolia/requester-browser-xhr": { + "node_modules/@algolia/recommend/node_modules/@algolia/client-common": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/recommend/node_modules/@algolia/client-search": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/recommend/node_modules/@algolia/requester-browser-xhr": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz", + "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==", "dev": true, "license": "MIT", "dependencies": { "@algolia/requester-common": "4.24.0" } }, + "node_modules/@algolia/recommend/node_modules/@algolia/requester-node-http": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz", + "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.8.1.tgz", + "integrity": "sha512-x0iULVrx5PocaYBqH+G6jyEsEHf7m5FDiZW7CP8AaJdzdCzoUyx7YH6e6TSCNlkFEjwmn8uj05coN8uljCHXTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.8.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@algolia/requester-common": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.24.0.tgz", + "integrity": "sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==", "dev": true, "license": "MIT" }, - "node_modules/@algolia/requester-node-http": { - "version": "4.24.0", + "node_modules/@algolia/requester-fetch": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.8.1.tgz", + "integrity": "sha512-SRWGrNsKSLNYIDNlVKVkf4wxsm6h57xI+0b8JPm0wUe0ly0jymAgQU2yW2GDzNuXyiPiS7U1oWwaVGs71IT5Pw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@algolia/requester-common": "4.24.0" + "@algolia/client-common": "5.8.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.8.1.tgz", + "integrity": "sha512-pYylr2gBsV68E88bltaVoJHIc3YNIllVmA12d+jefAcutR9ytQM7iP6dXbCYuRqF4CHF32YvZuwvqNI3J4kowA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.8.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/transporter": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.24.0.tgz", + "integrity": "sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==", "dev": true, "license": "MIT", "dependencies": { @@ -184,7 +349,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", "dev": true, "license": "MIT", "engines": { @@ -192,7 +359,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "dev": true, "license": "MIT", "engines": { @@ -200,11 +369,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.6", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.25.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -214,12 +385,14 @@ } }, "node_modules/@babel/types": { - "version": "7.25.6", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -227,27 +400,33 @@ } }, "node_modules/@docsearch/css": { - "version": "3.6.1", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.6.2.tgz", + "integrity": "sha512-vKNZepO2j7MrYBTZIGXvlUOIR+v9KRf70FApRgovWrj3GTs1EITz/Xb0AOlm1xsQBp16clVZj1SY/qaOJbQtZw==", "dev": true, "license": "MIT" }, "node_modules/@docsearch/js": { - "version": "3.6.1", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.6.2.tgz", + "integrity": "sha512-pS4YZF+VzUogYrkblCucQ0Oy2m8Wggk8Kk7lECmZM60hTbaydSIhJTTiCrmoxtBqV8wxORnOqcqqOfbmkkQEcA==", "dev": true, "license": "MIT", "dependencies": { - "@docsearch/react": "3.6.1", + "@docsearch/react": "3.6.2", "preact": "^10.0.0" } }, "node_modules/@docsearch/react": { - "version": "3.6.1", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.6.2.tgz", + "integrity": "sha512-rtZce46OOkVflCQH71IdbXSFK+S8iJZlUF56XBW5rIgx/eG5qoomC7Ag3anZson1bBac/JFQn7XOBfved/IMRA==", "dev": true, "license": "MIT", "dependencies": { "@algolia/autocomplete-core": "1.9.3", "@algolia/autocomplete-preset-algolia": "1.9.3", - "@docsearch/css": "3.6.1", + "@docsearch/css": "3.6.2", "algoliasearch": "^4.19.1" }, "peerDependencies": { @@ -271,8 +450,282 @@ } } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -286,13 +739,273 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.2", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -304,7 +1017,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.2", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -315,35 +1030,125 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@shikijs/core": { - "version": "1.16.1", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", + "integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^9.2.0", - "@types/hast": "^3.0.4" + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", + "integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-js": "0.4.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", + "integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0" } }, "node_modules/@shikijs/transformers": { - "version": "1.16.1", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.22.0.tgz", + "integrity": "sha512-k7iMOYuGQA62KwAuJOQBgH2IQb5vP8uiB3lMvAMGUgAMMurePOx3Z7oNqJdcpxqZP6I9cc7nc4DNqSKduCxmdg==", "dev": true, "license": "MIT", "dependencies": { - "shiki": "1.16.1" + "shiki": "1.22.0" + } + }, + "node_modules/@shikijs/types": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", + "integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/vscode-textmate": { - "version": "9.2.0", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.5", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, "node_modules/@types/hast": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dev": true, "license": "MIT", "dependencies": { @@ -352,11 +1157,15 @@ }, "node_modules/@types/linkify-it": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "dev": true, "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", "dependencies": { @@ -364,23 +1173,48 @@ "@types/mdurl": "^2" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mdurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true, "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true, "license": "MIT" }, "node_modules/@types/web-bluetooth": { "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", "dev": true, "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@vitejs/plugin-vue": { - "version": "5.1.3", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", "dev": true, "license": "MIT", "engines": { @@ -392,65 +1226,77 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/shared": "3.4.38", + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.4.38", - "@vue/shared": "3.4.38" + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/compiler-core": "3.4.38", - "@vue/compiler-dom": "3.4.38", - "@vue/compiler-ssr": "3.4.38", - "@vue/shared": "3.4.38", + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", "estree-walker": "^2.0.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.40", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.4.38", - "@vue/shared": "3.4.38" + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/devtools-api": { - "version": "7.3.9", + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.4.6.tgz", + "integrity": "sha512-XipBV5k0/IfTr0sNBDTg7OBUCp51cYMMXyPxLXJZ4K/wmUeMqt8cVdr2ZZGOFq+si/jTyCYnNxeKoyev5DOUUA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.3.9" + "@vue/devtools-kit": "^7.4.6" } }, "node_modules/@vue/devtools-kit": { - "version": "7.3.9", + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.4.6.tgz", + "integrity": "sha512-NbYBwPWgEic1AOd9bWExz9weBzFdjiIfov0yRn4DrRfR+EQJCI9dn4I0XS7IxYGdkmUJi8mFW42LLk18WsGqew==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.3.9", + "@vue/devtools-shared": "^7.4.6", "birpc": "^0.2.17", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -460,7 +1306,9 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "7.3.9", + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.4.6.tgz", + "integrity": "sha512-rPeSBzElnHYMB05Cc056BQiJpgocQjY8XVulgni+O9a9Gr9tNXgPteSzFFD+fT/iWMxNuUgGKs9CuW5DZewfIg==", "dev": true, "license": "MIT", "dependencies": { @@ -468,71 +1316,112 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.4.38" + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.4.38", - "@vue/shared": "3.4.38" + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.4.38", - "@vue/runtime-core": "3.4.38", - "@vue/shared": "3.4.38", + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.4.38", - "@vue/shared": "3.4.38" + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { - "vue": "3.4.38" + "vue": "3.5.12" } }, "node_modules/@vue/shared": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", "dev": true, "license": "MIT" }, "node_modules/@vueuse/core": { - "version": "11.0.3", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.1.0.tgz", + "integrity": "sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==", "dev": true, "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "11.0.3", - "@vueuse/shared": "11.0.3", + "@vueuse/metadata": "11.1.0", + "@vueuse/shared": "11.1.0", "vue-demi": ">=0.14.10" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@vueuse/integrations": { - "version": "11.0.3", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.1.0.tgz", + "integrity": "sha512-O2ZgrAGPy0qAjpoI2YR3egNgyEqwG85fxfwmA9BshRIGjV4G6yu6CfOPpMHAOoCD+UfsIl7Vb1bXJ6ifrHYDDA==", "dev": true, "license": "MIT", "dependencies": { - "@vueuse/core": "11.0.3", - "@vueuse/shared": "11.0.3", + "@vueuse/core": "11.1.0", + "@vueuse/shared": "11.1.0", "vue-demi": ">=0.14.10" }, "funding": { @@ -591,8 +1480,37 @@ } } }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@vueuse/metadata": { - "version": "11.0.3", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.1.0.tgz", + "integrity": "sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==", "dev": true, "license": "MIT", "funding": { @@ -600,7 +1518,9 @@ } }, "node_modules/@vueuse/shared": { - "version": "11.0.3", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", + "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", "dev": true, "license": "MIT", "dependencies": { @@ -610,8 +1530,37 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/algoliasearch": { "version": "4.24.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.24.0.tgz", + "integrity": "sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==", "dev": true, "license": "MIT", "dependencies": { @@ -632,16 +1581,107 @@ "@algolia/transporter": "4.24.0" } }, + "node_modules/algoliasearch/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/client-search": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/requester-browser-xhr": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz", + "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/requester-node-http": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz", + "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, "node_modules/birpc": { - "version": "0.2.17", + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/copy-anything": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", "dev": true, "license": "MIT", "dependencies": { @@ -656,11 +1696,39 @@ }, "node_modules/csstype": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/entities": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -672,6 +1740,8 @@ }, "node_modules/esbuild": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -709,24 +1779,96 @@ }, "node_modules/estree-walker": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, "license": "MIT" }, "node_modules/focus-trap": { - "version": "7.5.4", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", "dev": true, "license": "MIT", "dependencies": { "tabbable": "^6.2.0" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hookable": { "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "dev": true, "license": "MIT" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-what": { "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", "dev": true, "license": "MIT", "engines": { @@ -737,7 +1879,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.11", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "license": "MIT", "dependencies": { @@ -746,21 +1890,145 @@ }, "node_modules/mark.js": { "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", "dev": true, "license": "MIT" }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/minisearch": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.0.tgz", + "integrity": "sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA==", "dev": true, "license": "MIT" }, "node_modules/mitt": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -776,18 +2044,37 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex": "^4.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, "node_modules/postcss": { - "version": "8.4.44", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -806,15 +2093,17 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/preact": { - "version": "10.23.2", + "version": "10.24.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", "dev": true, "license": "MIT", "funding": { @@ -822,17 +2111,39 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "dev": true, + "license": "MIT" + }, "node_modules/rfdc": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true, "license": "MIT" }, "node_modules/rollup": { - "version": "4.21.2", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -842,59 +2153,98 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.2", - "@rollup/rollup-android-arm64": "4.21.2", - "@rollup/rollup-darwin-arm64": "4.21.2", - "@rollup/rollup-darwin-x64": "4.21.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", - "@rollup/rollup-linux-arm-musleabihf": "4.21.2", - "@rollup/rollup-linux-arm64-gnu": "4.21.2", - "@rollup/rollup-linux-arm64-musl": "4.21.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", - "@rollup/rollup-linux-riscv64-gnu": "4.21.2", - "@rollup/rollup-linux-s390x-gnu": "4.21.2", - "@rollup/rollup-linux-x64-gnu": "4.21.2", - "@rollup/rollup-linux-x64-musl": "4.21.2", - "@rollup/rollup-win32-arm64-msvc": "4.21.2", - "@rollup/rollup-win32-ia32-msvc": "4.21.2", - "@rollup/rollup-win32-x64-msvc": "4.21.2", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, "node_modules/search-insights": { - "version": "2.17.0", + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.2.tgz", + "integrity": "sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==", "dev": true, "license": "MIT", "peer": true }, "node_modules/shiki": { - "version": "1.16.1", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", + "integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "1.16.1", - "@shikijs/vscode-textmate": "^9.2.0", + "@shikijs/core": "1.22.0", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } }, "node_modules/source-map-js": { - "version": "1.2.0", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/speakingurl": { "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/superjson": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", "dev": true, "license": "MIT", "dependencies": { @@ -906,24 +2256,144 @@ }, "node_modules/tabbable": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "dev": true, "license": "MIT" }, "node_modules/to-fast-properties": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { - "version": "5.4.2", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.41", + "postcss": "^8.4.43", "rollup": "^4.20.0" }, "bin": { @@ -976,26 +2446,29 @@ } }, "node_modules/vitepress": { - "version": "1.3.4", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.4.1.tgz", + "integrity": "sha512-C2rQ7PMlDVqgsaHOa0uJtgGGWaGv74QMaGL62lxKbtFkYtosJB5HAfZ8+pEbfzzvLemYaYwaiQdFLBlexK2sFw==", "dev": true, "license": "MIT", "dependencies": { - "@docsearch/css": "^3.6.1", - "@docsearch/js": "^3.6.1", - "@shikijs/core": "^1.13.0", - "@shikijs/transformers": "^1.13.0", + "@docsearch/css": "^3.6.2", + "@docsearch/js": "^3.6.2", + "@shikijs/core": "^1.22.0", + "@shikijs/transformers": "^1.22.0", + "@shikijs/types": "^1.22.0", "@types/markdown-it": "^14.1.2", - "@vitejs/plugin-vue": "^5.1.2", - "@vue/devtools-api": "^7.3.8", - "@vue/shared": "^3.4.38", - "@vueuse/core": "^11.0.0", - "@vueuse/integrations": "^11.0.0", - "focus-trap": "^7.5.4", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/devtools-api": "^7.4.6", + "@vue/shared": "^3.5.12", + "@vueuse/core": "^11.1.0", + "@vueuse/integrations": "^11.1.0", + "focus-trap": "^7.6.0", "mark.js": "8.11.1", "minisearch": "^7.1.0", - "shiki": "^1.13.0", - "vite": "^5.4.1", - "vue": "^3.4.38" + "shiki": "^1.22.0", + "vite": "^5.4.8", + "vue": "^3.5.12" }, "bin": { "vitepress": "bin/vitepress.js" @@ -1014,15 +2487,17 @@ } }, "node_modules/vue": { - "version": "3.4.38", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.4.38", - "@vue/compiler-sfc": "3.4.38", - "@vue/runtime-dom": "3.4.38", - "@vue/server-renderer": "3.4.38", - "@vue/shared": "3.4.38" + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { "typescript": "*" @@ -1033,29 +2508,15 @@ } } }, - "node_modules/vue-demi": { - "version": "0.14.10", + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "type": "github", + "url": "https://github.com/sponsors/wooorm" } } } diff --git a/package.json b/package.json index 29967d6b8..677311e75 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "docs:preview": "vitepress preview docs" }, "devDependencies": { - "vitepress": "^1.3.4", - "vue": "^3.4.38" + "vitepress": "^1.4.1", + "vue": "^3.5.12" } } \ No newline at end of file diff --git a/pumpkin-config/Cargo.toml b/pumpkin-config/Cargo.toml index eacb43380..5c2a8df09 100644 --- a/pumpkin-config/Cargo.toml +++ b/pumpkin-config/Cargo.toml @@ -9,4 +9,4 @@ serde.workspace = true log.workspace = true toml = "0.8" - +serde-inline-default = "0.2.1" diff --git a/pumpkin-config/src/auth.rs b/pumpkin-config/src/auth.rs index c6606cd86..09b77b8b2 100644 --- a/pumpkin-config/src/auth.rs +++ b/pumpkin-config/src/auth.rs @@ -1,50 +1,84 @@ use pumpkin_core::ProfileAction; use serde::{Deserialize, Serialize}; +use serde_inline_default::serde_inline_default; +#[serde_inline_default] #[derive(Deserialize, Serialize)] pub struct AuthenticationConfig { /// Whether to use Mojang authentication. + #[serde_inline_default(true)] pub enabled: bool, + pub auth_url: String, + /// Prevent proxy connections. + #[serde_inline_default(false)] pub prevent_proxy_connections: bool, + pub prevent_proxy_connection_auth_url: String, + /// Player profile handling. + #[serde(default)] pub player_profile: PlayerProfileConfig, /// Texture handling. + #[serde(default)] pub textures: TextureConfig, } +impl Default for AuthenticationConfig { + fn default() -> Self { + Self { + enabled: true, + prevent_proxy_connections: false, + player_profile: Default::default(), + textures: Default::default(), + auth_url: "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}".to_string(), + prevent_proxy_connection_auth_url: "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}".to_string(), + } + } +} + #[derive(Deserialize, Serialize)] +#[serde(default)] pub struct PlayerProfileConfig { /// Allow players flagged by Mojang (banned, forced name change). pub allow_banned_players: bool, /// Depends on the value above + #[serde(default = "default_allowed_actions")] pub allowed_actions: Vec, } +fn default_allowed_actions() -> Vec { + vec![ + ProfileAction::ForcedNameChange, + ProfileAction::UsingBannedSkin, + ] +} + impl Default for PlayerProfileConfig { fn default() -> Self { Self { allow_banned_players: false, - allowed_actions: vec![ - ProfileAction::ForcedNameChange, - ProfileAction::UsingBannedSkin, - ], + allowed_actions: default_allowed_actions(), } } } +#[serde_inline_default] #[derive(Deserialize, Serialize)] pub struct TextureConfig { /// Whether to use player textures. + #[serde_inline_default(true)] pub enabled: bool, + #[serde_inline_default(vec!["http".into(), "https".into()])] pub allowed_url_schemes: Vec, + #[serde_inline_default(vec![".minecraft.net".into(), ".mojang.com".into()])] pub allowed_url_domains: Vec, /// Specific texture types. + #[serde(default)] pub types: TextureTypes, } @@ -60,13 +94,17 @@ impl Default for TextureConfig { } #[derive(Deserialize, Serialize)] +#[serde_inline_default] pub struct TextureTypes { /// Use player skins. + #[serde_inline_default(true)] pub skin: bool, /// Use player capes. + #[serde_inline_default(true)] pub cape: bool, /// Use player elytras. /// (i didn't know myself that there are custom elytras) + #[serde_inline_default(true)] pub elytra: bool, } @@ -79,14 +117,3 @@ impl Default for TextureTypes { } } } - -impl Default for AuthenticationConfig { - fn default() -> Self { - Self { - enabled: true, - prevent_proxy_connections: false, - player_profile: Default::default(), - textures: Default::default(), - } - } -} diff --git a/pumpkin-config/src/commands.rs b/pumpkin-config/src/commands.rs index 94ee4d8c2..936360818 100644 --- a/pumpkin-config/src/commands.rs +++ b/pumpkin-config/src/commands.rs @@ -1,14 +1,22 @@ use serde::{Deserialize, Serialize}; +use serde_inline_default::serde_inline_default; #[derive(Deserialize, Serialize)] +#[serde_inline_default] pub struct CommandsConfig { /// Are commands from the Console accepted ? + #[serde_inline_default(true)] pub use_console: bool, - // TODO: commands... + /// Should be commands from players be logged in console? + #[serde_inline_default(true)] + pub log_console: bool, // TODO: commands... } impl Default for CommandsConfig { fn default() -> Self { - Self { use_console: true } + Self { + use_console: true, + log_console: true, + } } } diff --git a/pumpkin-config/src/compression.rs b/pumpkin-config/src/compression.rs index 544c8d31d..74eb1c1bc 100644 --- a/pumpkin-config/src/compression.rs +++ b/pumpkin-config/src/compression.rs @@ -1,24 +1,46 @@ use serde::{Deserialize, Serialize}; +use serde_inline_default::serde_inline_default; +#[serde_inline_default] #[derive(Deserialize, Serialize)] -// Packet compression +/// Packet compression pub struct CompressionConfig { - /// Is compression enabled ? + /// Wether compression is enabled + #[serde_inline_default(true)] pub enabled: bool, + #[serde(flatten)] + #[serde(default)] + pub compression_info: CompressionInfo, +} + +#[serde_inline_default] +#[derive(Deserialize, Serialize, Clone)] +/// We have this in a Seperate struct so we can use it outside of the Config +pub struct CompressionInfo { /// The compression threshold used when compression is enabled - pub compression_threshold: u32, + #[serde_inline_default(256)] + pub threshold: u32, /// A value between 0..9 /// 1 = Optimize for the best speed of encoding. /// 9 = Optimize for the size of data being encoded. - pub compression_level: u32, + #[serde_inline_default(4)] + pub level: u32, +} + +impl Default for CompressionInfo { + fn default() -> Self { + Self { + threshold: 256, + level: 4, + } + } } impl Default for CompressionConfig { fn default() -> Self { Self { enabled: true, - compression_threshold: 256, - compression_level: 4, + compression_info: Default::default(), } } } diff --git a/pumpkin-config/src/lib.rs b/pumpkin-config/src/lib.rs index f24ca2810..6062bb259 100644 --- a/pumpkin-config/src/lib.rs +++ b/pumpkin-config/src/lib.rs @@ -1,7 +1,11 @@ use log::warn; +use logging::LoggingConfig; use pumpkin_core::{Difficulty, GameMode}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +// TODO: when https://github.com/rust-lang/rfcs/pull/3681 gets merged, replace serde-inline-default with native syntax +use serde_inline_default::serde_inline_default; + use std::{ fs, net::{Ipv4Addr, SocketAddr}, @@ -10,6 +14,7 @@ use std::{ }; pub mod auth; +pub mod logging; pub mod proxy; pub mod resource_pack; @@ -20,16 +25,13 @@ pub use pvp::PVPConfig; pub use rcon::RCONConfig; mod commands; -mod compression; +pub mod compression; mod pvp; mod rcon; use proxy::ProxyConfig; use resource_pack::ResourcePackConfig; -/// Current Config version of the Base Config -const CURRENT_BASE_VERSION: &str = "1.0.0"; - pub static ADVANCED_CONFIG: LazyLock = LazyLock::new(AdvancedConfiguration::load); @@ -41,6 +43,7 @@ pub static BASIC_CONFIG: LazyLock = LazyLock::new(BasicConfi /// This also allows you get some Performance or Resource boosts. /// Important: The Configuration should match Vanilla by default #[derive(Deserialize, Serialize, Default)] +#[serde(default)] pub struct AdvancedConfiguration { pub proxy: ProxyConfig, pub authentication: AuthenticationConfig, @@ -49,43 +52,58 @@ pub struct AdvancedConfiguration { pub commands: CommandsConfig, pub rcon: RCONConfig, pub pvp: PVPConfig, + pub logging: LoggingConfig, } +#[serde_inline_default] #[derive(Serialize, Deserialize)] pub struct BasicConfiguration { - /// A version identifier for the configuration format. - pub config_version: String, /// The address to bind the server to. + #[serde(default = "default_server_address")] pub server_address: SocketAddr, /// The seed for world generation. + #[serde(default = "String::new")] pub seed: String, /// The maximum number of players allowed on the server. + #[serde_inline_default(10000)] pub max_players: u32, /// The maximum view distance for players. + #[serde_inline_default(10)] pub view_distance: u8, /// The maximum simulated view distance. + #[serde_inline_default(10)] pub simulation_distance: u8, /// The default game difficulty. + #[serde_inline_default(Difficulty::Normal)] pub default_difficulty: Difficulty, /// Whether the Nether dimension is enabled. + #[serde_inline_default(true)] pub allow_nether: bool, /// Whether the server is in hardcore mode. + #[serde_inline_default(false)] pub hardcore: bool, /// Whether online mode is enabled. Requires valid Minecraft accounts. + #[serde_inline_default(true)] pub online_mode: bool, /// Whether packet encryption is enabled. Required when online mode is enabled. + #[serde_inline_default(true)] pub encryption: bool, /// The server's description displayed on the status screen. + #[serde_inline_default("A Blazing fast Pumpkin Server!".to_string())] pub motd: String, /// The default game mode for players. + #[serde_inline_default(GameMode::Survival)] pub default_gamemode: GameMode, } +fn default_server_address() -> SocketAddr { + SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25565) +} + impl Default for BasicConfiguration { fn default() -> Self { Self { - config_version: CURRENT_BASE_VERSION.to_string(), - server_address: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25565), + server_address: default_server_address(), seed: "".to_string(), max_players: 100000, view_distance: 10, @@ -114,7 +132,7 @@ trait LoadConfiguration { toml::from_str(&file_content).unwrap_or_else(|err| { panic!( - "Couldn't parse config at {:?}. Reason: {}", + "Couldn't parse config at {:?}. Reason: {}. This is is proberbly caused by an Config update, Just delete the old Config and start Pumpkin again", path, err.message() ) @@ -124,7 +142,7 @@ trait LoadConfiguration { if let Err(err) = fs::write(path, toml::to_string(&content).unwrap()) { warn!( - "Couldn't write default config to {:?}. Reason: {}", + "Couldn't write default config to {:?}. Reason: {}. This is is proberbly caused by an Config update, Just delete the old Config and start Pumpkin again", path, err ); } @@ -157,10 +175,6 @@ impl LoadConfiguration for BasicConfiguration { } fn validate(&self) { - assert_eq!( - self.config_version, CURRENT_BASE_VERSION, - "Config version does not match used Config version. Please update your config" - ); assert!(self.view_distance >= 2, "View distance must be at least 2"); assert!( self.view_distance <= 32, diff --git a/pumpkin-config/src/logging.rs b/pumpkin-config/src/logging.rs new file mode 100644 index 000000000..62991aa8c --- /dev/null +++ b/pumpkin-config/src/logging.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; +use serde_inline_default::serde_inline_default; + +#[serde_inline_default] +#[derive(Deserialize, Serialize)] +pub struct LoggingConfig { + #[serde_inline_default(true)] + pub enabled: bool, + #[serde_inline_default(LevelFilter::Info)] + pub level: LevelFilter, + #[serde_inline_default(false)] + pub env: bool, + #[serde_inline_default(true)] + pub threads: bool, + #[serde_inline_default(true)] + pub color: bool, + #[serde_inline_default(true)] + pub timestamp: bool, +} + +impl Default for LoggingConfig { + fn default() -> Self { + Self { + enabled: true, + level: LevelFilter::Info, + env: false, + threads: true, + color: true, + timestamp: true, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub enum LevelFilter { + /// A level lower than all log levels. + Off, + /// Corresponds to the `Error` log level. + Error, + /// Corresponds to the `Warn` log level. + Warn, + /// Corresponds to the `Info` log level. + Info, + /// Corresponds to the `Debug` log level. + Debug, + /// Corresponds to the `Trace` log level. + Trace, +} diff --git a/pumpkin-config/src/proxy.rs b/pumpkin-config/src/proxy.rs index dba52f66e..1d1e44336 100644 --- a/pumpkin-config/src/proxy.rs +++ b/pumpkin-config/src/proxy.rs @@ -1,12 +1,14 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Default)] +#[serde(default)] pub struct ProxyConfig { pub enabled: bool, pub velocity: VelocityConfig, } #[derive(Deserialize, Serialize)] +#[serde(default)] pub struct VelocityConfig { pub enabled: bool, pub secret: String, diff --git a/pumpkin-config/src/pvp.rs b/pumpkin-config/src/pvp.rs index cfbddc6c4..de5d9c70f 100644 --- a/pumpkin-config/src/pvp.rs +++ b/pumpkin-config/src/pvp.rs @@ -1,16 +1,23 @@ use serde::{Deserialize, Serialize}; +use serde_inline_default::serde_inline_default; +#[serde_inline_default] #[derive(Deserialize, Serialize)] pub struct PVPConfig { /// Is PVP enabled ? + #[serde_inline_default(true)] pub enabled: bool, /// Do we want to have the Red hurt animation & fov bobbing + #[serde_inline_default(true)] pub hurt_animation: bool, /// Should players in creative be protected against PVP + #[serde_inline_default(true)] pub protect_creative: bool, /// Has PVP Knockback? + #[serde_inline_default(true)] pub knockback: bool, /// Should player swing when attacking? + #[serde_inline_default(true)] pub swing: bool, } diff --git a/pumpkin-config/src/rcon.rs b/pumpkin-config/src/rcon.rs index af5aa6ac0..bbffa53c9 100644 --- a/pumpkin-config/src/rcon.rs +++ b/pumpkin-config/src/rcon.rs @@ -1,20 +1,68 @@ use std::net::{Ipv4Addr, SocketAddr}; use serde::{Deserialize, Serialize}; +use serde_inline_default::serde_inline_default; +#[serde_inline_default] #[derive(Deserialize, Serialize, Clone)] pub struct RCONConfig { + /// Is RCON Enabled? + #[serde_inline_default(false)] pub enabled: bool, + /// The network address and port where the RCON server will listen for connections. + #[serde(default = "default_rcon_address")] pub address: SocketAddr, + /// The password required for RCON authentication. + #[serde(default)] pub password: String, + /// The maximum number of concurrent RCON connections allowed. + /// If 0 there is no limit + #[serde(default)] + pub max_connections: u32, + /// RCON Logging + pub logging: RCONLogging, +} + +#[serde_inline_default] +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct RCONLogging { + /// Whether successful RCON logins should be logged. + #[serde_inline_default(true)] + pub log_logged_successfully: bool, + /// Whether failed RCON login attempts with incorrect passwords should be logged. + #[serde_inline_default(true)] + pub log_wrong_password: bool, + /// Whether all RCON commands, regardless of success or failure, should be logged. + #[serde_inline_default(true)] + pub log_commands: bool, + /// Whether RCON quit commands should be logged. + #[serde_inline_default(true)] + pub log_quit: bool, +} + +impl Default for RCONLogging { + fn default() -> Self { + Self { + log_logged_successfully: true, + log_wrong_password: true, + log_commands: true, + log_quit: true, + } + } +} + +fn default_rcon_address() -> SocketAddr { + SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25575) } impl Default for RCONConfig { fn default() -> Self { Self { enabled: false, - address: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25575), + address: default_rcon_address(), password: "".to_string(), + max_connections: 0, + logging: Default::default(), } } } diff --git a/pumpkin-config/src/resource_pack.rs b/pumpkin-config/src/resource_pack.rs index 795595ada..f09b89455 100644 --- a/pumpkin-config/src/resource_pack.rs +++ b/pumpkin-config/src/resource_pack.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] +#[serde(default)] pub struct ResourcePackConfig { pub enabled: bool, /// The path to the resource pack. diff --git a/pumpkin-core/src/math/position.rs b/pumpkin-core/src/math/position.rs index c324e89df..7d44f029f 100644 --- a/pumpkin-core/src/math/position.rs +++ b/pumpkin-core/src/math/position.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use super::vector3::Vector3; +#[derive(Clone, Copy)] /// Aka Block Position pub struct WorldPosition(pub Vector3); diff --git a/pumpkin-core/src/math/vector3.rs b/pumpkin-core/src/math/vector3.rs index 65da454e6..d8f0b3916 100644 --- a/pumpkin-core/src/math/vector3.rs +++ b/pumpkin-core/src/math/vector3.rs @@ -93,12 +93,14 @@ impl Neg for Vector3 { } impl From<(T, T, T)> for Vector3 { + #[inline(always)] fn from((x, y, z): (T, T, T)) -> Self { Vector3 { x, y, z } } } impl From> for (T, T, T) { + #[inline(always)] fn from(vector: Vector3) -> Self { (vector.x, vector.y, vector.z) } diff --git a/pumpkin-core/src/random/gaussian.rs b/pumpkin-core/src/random/gaussian.rs index 8364cb910..35d4095cd 100644 --- a/pumpkin-core/src/random/gaussian.rs +++ b/pumpkin-core/src/random/gaussian.rs @@ -1,18 +1,14 @@ -use super::Random; +use super::RandomImpl; -pub trait GaussianGenerator: Random { - fn has_next_gaussian(&self) -> bool; +pub trait GaussianGenerator: RandomImpl { + fn stored_next_gaussian(&self) -> Option; - fn set_has_next_gaussian(&mut self, value: bool); - - fn stored_next_gaussian(&self) -> f64; - - fn set_stored_next_gaussian(&mut self, value: f64); + fn set_stored_next_gaussian(&mut self, value: Option); fn calculate_gaussian(&mut self) -> f64 { - if self.has_next_gaussian() { - self.set_has_next_gaussian(false); - self.stored_next_gaussian() + if let Some(gaussian) = self.stored_next_gaussian() { + self.set_stored_next_gaussian(None); + gaussian } else { loop { let d = 2f64 * self.next_f64() - 1f64; @@ -21,8 +17,7 @@ pub trait GaussianGenerator: Random { if f < 1f64 && f != 0f64 { let g = (-2f64 * f.ln() / f).sqrt(); - self.set_stored_next_gaussian(e * g); - self.set_has_next_gaussian(true); + self.set_stored_next_gaussian(Some(e * g)); return d * g; } } diff --git a/pumpkin-core/src/random/legacy_rand.rs b/pumpkin-core/src/random/legacy_rand.rs index f7f60ff41..4a2cb1b98 100644 --- a/pumpkin-core/src/random/legacy_rand.rs +++ b/pumpkin-core/src/random/legacy_rand.rs @@ -1,11 +1,10 @@ use super::{ - gaussian::GaussianGenerator, hash_block_pos, java_string_hash, Random, RandomSplitter, + gaussian::GaussianGenerator, hash_block_pos, java_string_hash, RandomDeriverImpl, RandomImpl, }; -struct LegacyRand { +pub struct LegacyRand { seed: u64, - internal_next_gaussian: f64, - internal_has_next_gaussian: bool, + internal_next_gaussian: Option, } impl LegacyRand { @@ -18,29 +17,20 @@ impl LegacyRand { } impl GaussianGenerator for LegacyRand { - fn has_next_gaussian(&self) -> bool { - self.internal_has_next_gaussian - } - - fn stored_next_gaussian(&self) -> f64 { + fn stored_next_gaussian(&self) -> Option { self.internal_next_gaussian } - fn set_has_next_gaussian(&mut self, value: bool) { - self.internal_has_next_gaussian = value; - } - - fn set_stored_next_gaussian(&mut self, value: f64) { + fn set_stored_next_gaussian(&mut self, value: Option) { self.internal_next_gaussian = value; } } -impl Random for LegacyRand { +impl RandomImpl for LegacyRand { fn from_seed(seed: u64) -> Self { LegacyRand { seed: (seed ^ 0x5DEECE66D) & 0xFFFFFFFFFFFF, - internal_has_next_gaussian: false, - internal_next_gaussian: 0f64, + internal_next_gaussian: None, } } @@ -77,7 +67,8 @@ impl Random for LegacyRand { self.next(1) != 0 } - fn next_splitter(&mut self) -> impl RandomSplitter { + #[allow(refining_impl_trait)] + fn next_splitter(&mut self) -> LegacySplitter { LegacySplitter::new(self.next_i64() as u64) } @@ -86,13 +77,13 @@ impl Random for LegacyRand { } fn next_bounded_i32(&mut self, bound: i32) -> i32 { - if bound & (bound - 1) == 0 { - (bound as u64).wrapping_mul(self.next(31) >> 31) as i32 + if (bound & bound.wrapping_sub(1)) == 0 { + ((bound as u64).wrapping_mul(self.next(31)) >> 31) as i32 } else { loop { let i = self.next(31) as i32; let j = i % bound; - if (i - j + (bound - 1)) > 0 { + if (i.wrapping_sub(j).wrapping_add(bound.wrapping_sub(1))) >= 0 { return j; } } @@ -100,7 +91,7 @@ impl Random for LegacyRand { } } -struct LegacySplitter { +pub struct LegacySplitter { seed: u64, } @@ -110,17 +101,18 @@ impl LegacySplitter { } } -impl RandomSplitter for LegacySplitter { - fn split_u64(&self, seed: u64) -> impl Random { +#[allow(refining_impl_trait)] +impl RandomDeriverImpl for LegacySplitter { + fn split_u64(&self, seed: u64) -> LegacyRand { LegacyRand::from_seed(seed) } - fn split_string(&self, seed: &str) -> impl Random { + fn split_string(&self, seed: &str) -> LegacyRand { let string_hash = java_string_hash(seed); LegacyRand::from_seed((string_hash as u64) ^ self.seed) } - fn split_pos(&self, x: i32, y: i32, z: i32) -> impl Random { + fn split_pos(&self, x: i32, y: i32, z: i32) -> LegacyRand { let pos_hash = hash_block_pos(x, y, z); LegacyRand::from_seed((pos_hash as u64) ^ self.seed) } @@ -128,7 +120,7 @@ impl RandomSplitter for LegacySplitter { #[cfg(test)] mod test { - use crate::random::{Random, RandomSplitter}; + use crate::random::{RandomDeriverImpl, RandomImpl}; use super::LegacyRand; @@ -163,6 +155,17 @@ mod test { for value in values { assert_eq!(rand.next_bounded_i32(0xf), value); } + + let mut rand = LegacyRand::from_seed(0); + for _ in 0..10 { + assert_eq!(rand.next_bounded_i32(1), 0); + } + + let mut rand = LegacyRand::from_seed(0); + let values = [1, 1, 0, 1, 1, 0, 1, 0, 1, 1]; + for value in values { + assert_eq!(rand.next_bounded_i32(2), value); + } } #[test] diff --git a/pumpkin-core/src/random/mod.rs b/pumpkin-core/src/random/mod.rs index b5389aa49..d0200ebc4 100644 --- a/pumpkin-core/src/random/mod.rs +++ b/pumpkin-core/src/random/mod.rs @@ -1,13 +1,156 @@ +use legacy_rand::{LegacyRand, LegacySplitter}; +use xoroshiro128::{Xoroshiro, XoroshiroSplitter}; + mod gaussian; pub mod legacy_rand; pub mod xoroshiro128; -pub trait Random { +pub enum RandomGenerator { + Xoroshiro(Xoroshiro), + Legacy(LegacyRand), +} + +impl RandomGenerator { + #[inline] + pub fn split(&mut self) -> Self { + match self { + Self::Xoroshiro(rand) => Self::Xoroshiro(rand.split()), + Self::Legacy(rand) => Self::Legacy(rand.split()), + } + } + + #[inline] + pub fn next_splitter(&mut self) -> RandomDeriver { + match self { + Self::Xoroshiro(rand) => RandomDeriver::Xoroshiro(rand.next_splitter()), + Self::Legacy(rand) => RandomDeriver::Legacy(rand.next_splitter()), + } + } + + #[inline] + pub fn next(&mut self, bits: u64) -> u64 { + match self { + Self::Xoroshiro(rand) => rand.next(bits), + Self::Legacy(rand) => rand.next(bits), + } + } + + #[inline] + pub fn next_i32(&mut self) -> i32 { + match self { + Self::Xoroshiro(rand) => rand.next_i32(), + Self::Legacy(rand) => rand.next_i32(), + } + } + + #[inline] + pub fn next_bounded_i32(&mut self, bound: i32) -> i32 { + match self { + Self::Xoroshiro(rand) => rand.next_bounded_i32(bound), + Self::Legacy(rand) => rand.next_bounded_i32(bound), + } + } + + #[inline] + pub fn next_inbetween_i32(&mut self, min: i32, max: i32) -> i32 { + self.next_bounded_i32(max - min + 1) + min + } + + #[inline] + pub fn next_i64(&mut self) -> i64 { + match self { + Self::Xoroshiro(rand) => rand.next_i64(), + Self::Legacy(rand) => rand.next_i64(), + } + } + + #[inline] + pub fn next_bool(&mut self) -> bool { + match self { + Self::Xoroshiro(rand) => rand.next_bool(), + Self::Legacy(rand) => rand.next_bool(), + } + } + + #[inline] + pub fn next_f32(&mut self) -> f32 { + match self { + Self::Xoroshiro(rand) => rand.next_f32(), + Self::Legacy(rand) => rand.next_f32(), + } + } + + #[inline] + pub fn next_f64(&mut self) -> f64 { + match self { + Self::Xoroshiro(rand) => rand.next_f64(), + Self::Legacy(rand) => rand.next_f64(), + } + } + + #[inline] + pub fn next_gaussian(&mut self) -> f64 { + match self { + Self::Xoroshiro(rand) => rand.next_gaussian(), + Self::Legacy(rand) => rand.next_gaussian(), + } + } + + #[inline] + pub fn next_triangular(&mut self, mode: f64, deviation: f64) -> f64 { + mode + deviation * (self.next_f64() - self.next_f64()) + } + + #[inline] + pub fn skip(&mut self, count: i32) { + for _ in 0..count { + self.next_i64(); + } + } + + #[inline] + pub fn next_inbetween_i32_exclusive(&mut self, min: i32, max: i32) -> i32 { + min + self.next_bounded_i32(max - min) + } +} + +pub enum RandomDeriver { + Xoroshiro(XoroshiroSplitter), + Legacy(LegacySplitter), +} + +impl RandomDeriver { + #[inline] + pub fn split_string(&self, seed: &str) -> RandomGenerator { + match self { + Self::Xoroshiro(deriver) => RandomGenerator::Xoroshiro(deriver.split_string(seed)), + Self::Legacy(deriver) => RandomGenerator::Legacy(deriver.split_string(seed)), + } + } + + #[inline] + pub fn split_u64(&self, seed: u64) -> RandomGenerator { + match self { + Self::Xoroshiro(deriver) => RandomGenerator::Xoroshiro(deriver.split_u64(seed)), + Self::Legacy(deriver) => RandomGenerator::Legacy(deriver.split_u64(seed)), + } + } + + #[inline] + pub fn split_pos(&self, x: i32, y: i32, z: i32) -> RandomGenerator { + match self { + Self::Xoroshiro(deriver) => RandomGenerator::Xoroshiro(deriver.split_pos(x, y, z)), + Self::Legacy(deriver) => RandomGenerator::Legacy(deriver.split_pos(x, y, z)), + } + } +} + +pub trait RandomImpl { fn from_seed(seed: u64) -> Self; fn split(&mut self) -> Self; - fn next_splitter(&mut self) -> impl RandomSplitter; + fn next_splitter(&mut self) -> impl RandomDeriverImpl; fn next(&mut self, bits: u64) -> u64; @@ -44,12 +187,12 @@ pub trait Random { } } -pub trait RandomSplitter { - fn split_string(&self, seed: &str) -> impl Random; +pub trait RandomDeriverImpl { + fn split_string(&self, seed: &str) -> impl RandomImpl; - fn split_u64(&self, seed: u64) -> impl Random; + fn split_u64(&self, seed: u64) -> impl RandomImpl; - fn split_pos(&self, x: i32, y: i32, z: i32) -> impl Random; + fn split_pos(&self, x: i32, y: i32, z: i32) -> impl RandomImpl; } fn hash_block_pos(x: i32, y: i32, z: i32) -> i64 { diff --git a/pumpkin-core/src/random/xoroshiro128.rs b/pumpkin-core/src/random/xoroshiro128.rs index e8d7e4f12..a82fcf75c 100644 --- a/pumpkin-core/src/random/xoroshiro128.rs +++ b/pumpkin-core/src/random/xoroshiro128.rs @@ -1,10 +1,9 @@ -use super::{gaussian::GaussianGenerator, hash_block_pos, Random, RandomSplitter}; +use super::{gaussian::GaussianGenerator, hash_block_pos, RandomDeriverImpl, RandomImpl}; pub struct Xoroshiro { lo: u64, hi: u64, - internal_next_gaussian: f64, - internal_has_next_gaussian: bool, + internal_next_gaussian: Option, } impl Xoroshiro { @@ -17,8 +16,7 @@ impl Xoroshiro { Self { lo, hi, - internal_next_gaussian: 0f64, - internal_has_next_gaussian: false, + internal_next_gaussian: None, } } @@ -45,21 +43,13 @@ impl Xoroshiro { } impl GaussianGenerator for Xoroshiro { - fn stored_next_gaussian(&self) -> f64 { + fn stored_next_gaussian(&self) -> Option { self.internal_next_gaussian } - fn has_next_gaussian(&self) -> bool { - self.internal_has_next_gaussian - } - - fn set_stored_next_gaussian(&mut self, value: f64) { + fn set_stored_next_gaussian(&mut self, value: Option) { self.internal_next_gaussian = value; } - - fn set_has_next_gaussian(&mut self, value: bool) { - self.internal_has_next_gaussian = value; - } } fn mix_stafford_13(z: u64) -> u64 { @@ -68,7 +58,7 @@ fn mix_stafford_13(z: u64) -> u64 { z ^ (z >> 31) } -impl Random for Xoroshiro { +impl RandomImpl for Xoroshiro { fn from_seed(seed: u64) -> Self { let (lo, hi) = Self::mix_u64(seed); let lo = mix_stafford_13(lo); @@ -84,7 +74,8 @@ impl Random for Xoroshiro { self.next_random() >> (64 - bits) } - fn next_splitter(&mut self) -> impl RandomSplitter { + #[allow(refining_impl_trait)] + fn next_splitter(&mut self) -> XoroshiroSplitter { XoroshiroSplitter { lo: self.next_random(), hi: self.next_random(), @@ -137,18 +128,19 @@ pub struct XoroshiroSplitter { hi: u64, } -impl RandomSplitter for XoroshiroSplitter { - fn split_pos(&self, x: i32, y: i32, z: i32) -> impl Random { +#[allow(refining_impl_trait)] +impl RandomDeriverImpl for XoroshiroSplitter { + fn split_pos(&self, x: i32, y: i32, z: i32) -> Xoroshiro { let l = hash_block_pos(x, y, z) as u64; let m = l ^ self.lo; Xoroshiro::new(m, self.hi) } - fn split_u64(&self, seed: u64) -> impl Random { + fn split_u64(&self, seed: u64) -> Xoroshiro { Xoroshiro::new(seed ^ self.lo, seed ^ self.hi) } - fn split_string(&self, seed: &str) -> impl Random { + fn split_string(&self, seed: &str) -> Xoroshiro { let bytes = md5::compute(seed.as_bytes()); let l = u64::from_be_bytes(bytes[0..8].try_into().expect("incorrect length")); let m = u64::from_be_bytes(bytes[8..16].try_into().expect("incorrect length")); @@ -159,7 +151,7 @@ impl RandomSplitter for XoroshiroSplitter { #[cfg(test)] mod tests { - use crate::random::{Random, RandomSplitter}; + use crate::random::{RandomDeriverImpl, RandomImpl}; use super::{mix_stafford_13, Xoroshiro}; diff --git a/pumpkin-inventory/Cargo.toml b/pumpkin-inventory/Cargo.toml index 3b6c8c972..abeb7cfd1 100644 --- a/pumpkin-inventory/Cargo.toml +++ b/pumpkin-inventory/Cargo.toml @@ -5,9 +5,11 @@ edition.workspace = true [dependencies] # For items -pumpkin-world = { path = "../pumpkin-world"} +pumpkin-world = { path = "../pumpkin-world" } num-traits = "0.2" num-derive = "0.4" thiserror = "1.0.63" -itertools = "0.13.0" \ No newline at end of file +itertools.workspace = true +parking_lot.workspace = true +crossbeam.workspace = true diff --git a/pumpkin-inventory/src/drag_handler.rs b/pumpkin-inventory/src/drag_handler.rs index 11d8b2f82..8479c3635 100644 --- a/pumpkin-inventory/src/drag_handler.rs +++ b/pumpkin-inventory/src/drag_handler.rs @@ -2,9 +2,10 @@ use crate::container_click::MouseDragType; use crate::{Container, InventoryError}; use itertools::Itertools; use num_traits::Euclid; +use parking_lot::{Mutex, RwLock}; use pumpkin_world::item::ItemStack; use std::collections::HashMap; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::Arc; #[derive(Debug, Default)] pub struct DragHandler(RwLock>>>); @@ -23,10 +24,7 @@ impl DragHandler { drag_type, slots: vec![], }; - let mut drags = match self.0.write() { - Ok(drags) => drags, - Err(_) => Err(InventoryError::LockError)?, - }; + let mut drags = self.0.write(); drags.insert(container_id, Arc::new(Mutex::new(drag))); Ok(()) } @@ -37,13 +35,10 @@ impl DragHandler { player: i32, slot: usize, ) -> Result<(), InventoryError> { - let drags = match self.0.read() { - Ok(drags) => drags, - Err(_) => Err(InventoryError::LockError)?, - }; + let drags = self.0.read(); match drags.get(&container_id) { Some(drag) => { - let mut drag = drag.lock().unwrap(); + let mut drag = drag.lock(); if drag.player != player { Err(InventoryError::MultiplePlayersDragging)? } @@ -68,13 +63,11 @@ impl DragHandler { return Ok(()); } - let Ok(mut drags) = self.0.write() else { - Err(InventoryError::LockError)? - }; + let mut drags = self.0.write(); let Some((_, drag)) = drags.remove_entry(container_id) else { Err(InventoryError::OutOfOrderDragging)? }; - let drag = drag.lock().unwrap(); + let drag = drag.lock(); if player != drag.player { Err(InventoryError::MultiplePlayersDragging)? diff --git a/pumpkin-inventory/src/open_container.rs b/pumpkin-inventory/src/open_container.rs index 25207d018..261ba3b74 100644 --- a/pumpkin-inventory/src/open_container.rs +++ b/pumpkin-inventory/src/open_container.rs @@ -1,6 +1,7 @@ use crate::{Container, WindowType}; +use parking_lot::Mutex; use pumpkin_world::item::ItemStack; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; pub struct OpenContainer { players: Vec, diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index d8e127b78..999c9a9e1 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -1,3 +1,5 @@ +use std::sync::atomic::AtomicU32; + use crate::container_click::MouseClick; use crate::{handle_item_change, Container, InventoryError, WindowType}; use pumpkin_world::item::ItemStack; @@ -11,7 +13,7 @@ pub struct PlayerInventory { offhand: Option, // current selected slot in hotbar selected: usize, - pub state_id: u32, + pub state_id: AtomicU32, // Notchian server wraps this value at 100, we can just keep it as a u8 that automatically wraps pub total_opened_containers: u8, } @@ -32,7 +34,7 @@ impl PlayerInventory { offhand: None, // TODO: What when player spawns in with an different index ? selected: 0, - state_id: 0, + state_id: AtomicU32::new(0), total_opened_containers: 2, } } diff --git a/pumpkin-macros/Cargo.toml b/pumpkin-macros/Cargo.toml index c677c7c01..5dca73b94 100644 --- a/pumpkin-macros/Cargo.toml +++ b/pumpkin-macros/Cargo.toml @@ -10,3 +10,6 @@ proc-macro = true proc-macro2 = "1.0" quote = "1.0" syn = "2.0" +serde.workspace = true +itertools.workspace = true +serde_json = "1.0.128" diff --git a/pumpkin-macros/src/block_state.rs b/pumpkin-macros/src/block_state.rs new file mode 100644 index 000000000..3f10f3943 --- /dev/null +++ b/pumpkin-macros/src/block_state.rs @@ -0,0 +1,261 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::LazyLock, +}; + +use itertools::Itertools; +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::Parser; + +#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +struct RegistryBlockDefinition { + /// e.g. minecraft:door or minecraft:button + #[serde(rename = "type")] + pub category: String, + + /// Specifies the variant of the blocks category. + /// e.g. minecraft:iron_door has the variant iron + #[serde(rename = "block_set_type")] + pub variant: Option, +} + +/// One possible state of a Block. +/// This could e.g. be an extended piston facing left. +#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +struct RegistryBlockState { + pub id: i32, + + /// Whether this is the default state of the Block + #[serde(default, rename = "default")] + pub is_default: bool, + + /// The propertise active for this `BlockState`. + #[serde(default)] + pub properties: HashMap, +} + +/// A fully-fledged block definition. +/// Stores the category, variant, all of the possible states and all of the possible properties. +#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +struct RegistryBlockType { + pub definition: RegistryBlockDefinition, + pub states: Vec, + + // TODO is this safe to remove? It's currently not used in the Project. @lukas0008 @Snowiiii + /// A list of valid property keys/values for a block. + #[serde(default, rename = "properties")] + valid_properties: HashMap>, +} + +static BLOCKS: LazyLock> = LazyLock::new(|| { + serde_json::from_str(include_str!("../../assets/blocks.json")) + .expect("Could not parse block.json registry.") +}); + +fn pascal_case(original: &str) -> String { + let mut pascal = String::new(); + let mut capitalize = true; + for ch in original.chars() { + if ch == '_' { + capitalize = true; + } else if capitalize { + pascal.push(ch.to_ascii_uppercase()); + capitalize = false; + } else { + pascal.push(ch); + } + } + pascal +} + +pub fn block_type_enum_impl() -> TokenStream { + let categories: &HashSet<&str> = &BLOCKS + .values() + .map(|val| val.definition.category.as_str()) + .collect(); + + let original_and_converted_stream = categories.iter().map(|key| { + ( + key, + pascal_case(key.split_once(':').expect("Bad minecraft id").1), + ) + }); + let new_names: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(_, x)| x) + .join(",\n") + .parse() + .unwrap(); + + let from_string: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(original, converted)| format!("\"{}\" => BlockCategory::{},", original, converted)) + .join("\n") + .parse() + .unwrap(); + + // I;ve never used macros before so call me out on this lol + quote! { + #[derive(PartialEq, Clone)] + pub enum BlockCategory { + #new_names + } + + impl BlockCategory { + pub fn from_registry_id(id: &str) -> BlockCategory { + match id { + #from_string + _ => panic!("Not a valid block type id"), + } + } + } + } + .into() +} + +pub fn block_enum_impl() -> TokenStream { + let original_and_converted_stream = &BLOCKS.keys().map(|key| { + ( + key, + pascal_case(key.split_once(':').expect("Bad minecraft id").1), + ) + }); + let new_names: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(_, x)| x) + .join(",\n") + .parse() + .unwrap(); + + let from_string: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(original, converted)| format!("\"{}\" => Block::{},", original, converted)) + .join("\n") + .parse() + .unwrap(); + + // I;ve never used macros before so call me out on this lol + quote! { + #[derive(PartialEq, Clone)] + pub enum Block { + #new_names + } + + impl Block { + pub fn from_registry_id(id: &str) -> Block { + match id { + #from_string + _ => panic!("Not a valid block id"), + } + } + } + } + .into() +} + +pub fn block_state_impl(item: TokenStream) -> TokenStream { + let data = syn::punctuated::Punctuated::::parse_terminated + .parse(item) + .unwrap(); + let block_name = data + .first() + .expect("The first argument should be a block name"); + + let block_name = match block_name { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Str(name) => name.value(), + _ => panic!("The first argument should be a string, have: {:?}", lit), + }, + _ => panic!( + "The first argument should be a string, have: {:?}", + block_name + ), + }; + + let mut properties = HashMap::new(); + for expr_thingy in data.into_iter().skip(1) { + match expr_thingy { + syn::Expr::Assign(assign) => { + let left = match assign.left.as_ref() { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Str(name) => name.value(), + _ => panic!( + "All not-first arguments should be assignments (\"foo\" = \"bar\")" + ), + }, + _ => { + panic!("All not-first arguments should be assignments (\"foo\" = \"bar\")") + } + }; + let right = match assign.right.as_ref() { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Str(name) => name.value(), + _ => panic!( + "All not-first arguments should be assignments (\"foo\" = \"bar\")" + ), + }, + _ => { + panic!("All not-first arguments should be assignments (\"foo\" = \"bar\")") + } + }; + properties.insert(left, right); + } + _ => panic!("All not-first arguments should be assignments (\"foo\" = \"bar\")"), + } + } + + // panic!("{:?}", properties); + + let block_info = &BLOCKS + .get(&block_name) + .expect("Block with that name does not exist"); + + let state = if properties.is_empty() { + block_info + .states + .iter() + .find(|state| state.is_default) + .expect( + "Error inside blocks.json file: Every Block should have at least 1 default state", + ) + } else { + match block_info + .states + .iter() + .find(|state| state.properties == properties) + { + Some(state) => state, + None => panic!( + "Could not find block with these properties, the following are valid properties: \n{}", + block_info + .valid_properties + .iter() + .map(|(name, values)| format!("{name} = {}", values.join(" | "))) + .join("\n") + ), + } + }; + + let id = state.id; + let category_name = block_info.definition.category.clone(); + + if std::env::var("CARGO_PKG_NAME").unwrap() == "pumpkin-world" { + quote! { + crate::block::block_state::BlockState::new_unchecked( + #id as u16, + crate::block::Block::from_registry_id(#block_name), + crate::block::BlockCategory::from_registry_id(#category_name), + ) + } + } else { + quote! { + pumpkin_world::block::block_id::BlockStateId::new_unchecked( + #id as u16, + pumpkin_world::block::Block::from_registry_id(#block_name), + pumpkin_world::block::BlockCategory::from_registry_id(#category_name), + ) + } + } + .into() +} diff --git a/pumpkin-macros/src/lib.rs b/pumpkin-macros/src/lib.rs index 346159229..3c27f91a2 100644 --- a/pumpkin-macros/src/lib.rs +++ b/pumpkin-macros/src/lib.rs @@ -22,3 +22,21 @@ pub fn packet(input: TokenStream, item: TokenStream) -> TokenStream { gen.into() } + +mod block_state; +#[proc_macro] +pub fn block(item: TokenStream) -> TokenStream { + block_state::block_state_impl(item) +} + +#[proc_macro] +/// Creates an enum for all block types. Should only be used once +pub fn blocks_enum(_item: TokenStream) -> TokenStream { + block_state::block_enum_impl() +} + +#[proc_macro] +/// Creates an enum for all block categories. Should only be used once +pub fn block_categories_enum(_item: TokenStream) -> TokenStream { + block_state::block_type_enum_impl() +} diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index 0c0ab1296..61d69ffe0 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -4,8 +4,9 @@ version.workspace = true edition.workspace = true [dependencies] +pumpkin-config = { path = "../pumpkin-config" } pumpkin-macros = { path = "../pumpkin-macros" } -pumpkin-world = { path = "../pumpkin-world" } +pumpkin-world = { path = "../pumpkin-world" } pumpkin-core = { path = "../pumpkin-core" } bytes = "1.7" @@ -14,7 +15,7 @@ uuid.workspace = true serde.workspace = true -flate2 = "1.0.33" +flate2 = "1.0" thiserror = "1.0" log.workspace = true @@ -25,5 +26,5 @@ num-derive = "0.4" aes = "0.8.4" cfb8 = "0.8.1" -itertools = "0.13.0" +itertools.workspace = true fastnbt = { git = "https://github.com/owengage/fastnbt.git" } diff --git a/pumpkin-protocol/README.md b/pumpkin-protocol/README.md index 4ea5c5c71..e7b469872 100644 --- a/pumpkin-protocol/README.md +++ b/pumpkin-protocol/README.md @@ -1,6 +1,10 @@ ### Pumpkin Protocol Contains all Serverbound(Client->Server) and Clientbound(Server->Client) Packets. +### Features +- [x] ZLib Compression +- [x] AES/CFB8 Encryiption + Packets in the Pumpkin protocol are organized by functionality and state. `server`: Contains definitions for serverbound packets. @@ -69,4 +73,8 @@ Thats a Serverbound packet pub struct CPlayDisconnect { reason: TextComponent, } -`` \ No newline at end of file +``` + +### Porting +You can compare difference in Protocol on wiki.vg https://wiki.vg/index.php?title=Protocol&action=history +Also change the `CURRENT_MC_PROTOCOL` in `src/lib.rs` \ No newline at end of file diff --git a/pumpkin-protocol/src/bytebuf/mod.rs b/pumpkin-protocol/src/bytebuf/mod.rs index 4ec1dc5a6..3b46f2f90 100644 --- a/pumpkin-protocol/src/bytebuf/mod.rs +++ b/pumpkin-protocol/src/bytebuf/mod.rs @@ -72,19 +72,19 @@ impl ByteBuffer { } pub fn get_string(&mut self) -> Result { - self.get_string_len(32767) + self.get_string_len(i16::MAX as i32) } - pub fn get_string_len(&mut self, max_size: usize) -> Result { + pub fn get_string_len(&mut self, max_size: i32) -> Result { let size = self.get_var_int()?.0; - if size as usize > max_size { + if size > max_size { return Err(DeserializerError::Message( "String length is bigger than max size".to_string(), )); } let data = self.copy_to_bytes(size as usize)?; - if data.len() > max_size { + if data.len() as i32 > max_size { return Err(DeserializerError::Message( "String is bigger than max size".to_string(), )); @@ -125,6 +125,14 @@ impl ByteBuffer { } pub fn put_string(&mut self, val: &str) { + self.put_string_len(val, i16::MAX as i32); + } + + pub fn put_string_len(&mut self, val: &str, max_size: i32) { + if val.len() as i32 > max_size { + // Should be panic?, I mean its our fault + panic!("String is too big"); + } self.put_var_int(&val.len().into()); self.buffer.put(val.as_bytes()); } diff --git a/pumpkin-protocol/src/client/config/c_add_resource_pack.rs b/pumpkin-protocol/src/client/config/c_add_resource_pack.rs index 824141e25..48b011faa 100644 --- a/pumpkin-protocol/src/client/config/c_add_resource_pack.rs +++ b/pumpkin-protocol/src/client/config/c_add_resource_pack.rs @@ -2,12 +2,10 @@ use pumpkin_core::text::TextComponent; use pumpkin_macros::packet; use serde::Serialize; -use crate::uuid::UUID; - #[derive(Serialize)] #[packet(0x09)] pub struct CConfigAddResourcePack<'a> { - uuid: UUID, + uuid: uuid::Uuid, url: &'a str, hash: &'a str, // max 40 forced: bool, @@ -16,7 +14,7 @@ pub struct CConfigAddResourcePack<'a> { impl<'a> CConfigAddResourcePack<'a> { pub fn new( - uuid: UUID, + uuid: uuid::Uuid, url: &'a str, hash: &'a str, forced: bool, diff --git a/pumpkin-protocol/src/client/play/c_entity_animation.rs b/pumpkin-protocol/src/client/play/c_entity_animation.rs index 28542fac4..5618374ae 100644 --- a/pumpkin-protocol/src/client/play/c_entity_animation.rs +++ b/pumpkin-protocol/src/client/play/c_entity_animation.rs @@ -1,4 +1,3 @@ -use num_derive::ToPrimitive; use pumpkin_macros::packet; use serde::Serialize; @@ -21,7 +20,7 @@ impl CEntityAnimation { } } -#[derive(ToPrimitive)] +#[repr(u8)] pub enum Animation { SwingMainArm, LeaveBed, diff --git a/pumpkin-protocol/src/client/play/c_entity_status.rs b/pumpkin-protocol/src/client/play/c_entity_status.rs new file mode 100644 index 000000000..301fa0ee9 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_entity_status.rs @@ -0,0 +1,18 @@ +use pumpkin_macros::packet; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(0x1F)] +pub struct CEntityStatus { + entity_id: i32, + entity_status: i8, +} + +impl CEntityStatus { + pub fn new(entity_id: i32, entity_status: i8) -> Self { + Self { + entity_id, + entity_status, + } + } +} diff --git a/pumpkin-protocol/src/client/play/c_game_event.rs b/pumpkin-protocol/src/client/play/c_game_event.rs index 8873ab210..f8a01b5cf 100644 --- a/pumpkin-protocol/src/client/play/c_game_event.rs +++ b/pumpkin-protocol/src/client/play/c_game_event.rs @@ -8,8 +8,30 @@ pub struct CGameEvent { value: f32, } +/// Somewhere you need to implement all the random stuff right? impl CGameEvent { - pub fn new(event: u8, value: f32) -> Self { - Self { event, value } + pub fn new(event: GameEvent, value: f32) -> Self { + Self { + event: event as u8, + value, + } } } + +#[repr(u8)] +pub enum GameEvent { + NoRespawnBlockAvailable, + BeginRaining, + EndRaining, + ChangeGameMode, + WinGame, + DemoEvent, + ArrowHitPlayer, + RainLevelChange, + ThunderLevelChange, + PlayPufferfishStringSound, + PlayElderGuardianMobAppearance, + EnabledRespawnScreen, + LimitedCrafting, + StartWaitingChunks, +} diff --git a/pumpkin-protocol/src/client/play/c_keep_alive.rs b/pumpkin-protocol/src/client/play/c_keep_alive.rs new file mode 100644 index 000000000..c1d95f094 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_keep_alive.rs @@ -0,0 +1,8 @@ +use pumpkin_macros::packet; +use serde::Serialize; + +#[packet(0x26)] +#[derive(Serialize)] +pub struct CKeepAlive { + pub keep_alive_id: i64, +} diff --git a/pumpkin-protocol/src/client/play/c_player_chat_message.rs b/pumpkin-protocol/src/client/play/c_player_chat_message.rs index 8ec95ddca..7d5e7c7ce 100644 --- a/pumpkin-protocol/src/client/play/c_player_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_player_chat_message.rs @@ -2,11 +2,12 @@ use pumpkin_core::text::TextComponent; use pumpkin_macros::packet; use serde::Serialize; -use crate::{uuid::UUID, BitSet, VarInt}; +use crate::{BitSet, VarInt}; #[derive(Serialize)] #[packet(0x39)] pub struct CPlayerChatMessage<'a> { - sender: UUID, + #[serde(with = "uuid::serde::compact")] + sender: uuid::Uuid, index: VarInt, message_signature: Option<&'a [u8]>, message: &'a str, @@ -24,7 +25,7 @@ pub struct CPlayerChatMessage<'a> { impl<'a> CPlayerChatMessage<'a> { #[expect(clippy::too_many_arguments)] pub fn new( - sender: UUID, + sender: uuid::Uuid, index: VarInt, message_signature: Option<&'a [u8]>, message: &'a str, diff --git a/pumpkin-protocol/src/client/play/c_player_info_update.rs b/pumpkin-protocol/src/client/play/c_player_info_update.rs index b62108680..054ebb028 100644 --- a/pumpkin-protocol/src/client/play/c_player_info_update.rs +++ b/pumpkin-protocol/src/client/play/c_player_info_update.rs @@ -7,12 +7,12 @@ use super::PlayerAction; #[packet(0x3E)] pub struct CPlayerInfoUpdate<'a> { pub actions: i8, - pub players: &'a [Player], + pub players: &'a [Player<'a>], } -pub struct Player { +pub struct Player<'a> { pub uuid: uuid::Uuid, - pub actions: Vec, + pub actions: Vec>, } impl<'a> CPlayerInfoUpdate<'a> { diff --git a/pumpkin-protocol/src/client/play/c_player_remove.rs b/pumpkin-protocol/src/client/play/c_player_remove.rs index 4d128a816..20b7ba218 100644 --- a/pumpkin-protocol/src/client/play/c_player_remove.rs +++ b/pumpkin-protocol/src/client/play/c_player_remove.rs @@ -1,20 +1,32 @@ use pumpkin_macros::packet; -use serde::Serialize; +use serde::{ser::SerializeSeq, Serialize}; -use crate::{uuid::UUID, VarInt}; +use crate::VarInt; #[derive(Serialize)] #[packet(0x3D)] pub struct CRemovePlayerInfo<'a> { players_count: VarInt, - players: &'a [UUID], + #[serde(serialize_with = "serialize_slice_uuids")] + players: &'a [uuid::Uuid], } impl<'a> CRemovePlayerInfo<'a> { - pub fn new(players_count: VarInt, players: &'a [UUID]) -> Self { + pub fn new(players_count: VarInt, players: &'a [uuid::Uuid]) -> Self { Self { players_count, players, } } } + +fn serialize_slice_uuids( + uuids: &[uuid::Uuid], + serializer: S, +) -> Result { + let mut seq = serializer.serialize_seq(Some(uuids.len()))?; + for uuid in uuids { + seq.serialize_element(uuid.as_bytes())?; + } + seq.end() +} diff --git a/pumpkin-protocol/src/client/play/c_set_health.rs b/pumpkin-protocol/src/client/play/c_set_health.rs new file mode 100644 index 000000000..a598444f7 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_set_health.rs @@ -0,0 +1,22 @@ +use pumpkin_macros::packet; +use serde::Serialize; + +use crate::VarInt; + +#[derive(Serialize)] +#[packet(0x5D)] +pub struct CSetHealth { + health: f32, + food: VarInt, + food_saturation: f32, +} + +impl CSetHealth { + pub fn new(health: f32, food: VarInt, food_saturation: f32) -> Self { + Self { + health, + food, + food_saturation, + } + } +} diff --git a/pumpkin-protocol/src/client/play/c_spawn_player.rs b/pumpkin-protocol/src/client/play/c_spawn_player.rs index 73d0f8541..c9c65b92d 100644 --- a/pumpkin-protocol/src/client/play/c_spawn_player.rs +++ b/pumpkin-protocol/src/client/play/c_spawn_player.rs @@ -1,13 +1,14 @@ use pumpkin_macros::packet; use serde::Serialize; -use crate::{uuid::UUID, VarInt}; +use crate::VarInt; #[derive(Serialize)] #[packet(0x01)] pub struct CSpawnEntity { entity_id: VarInt, - entity_uuid: UUID, + #[serde(with = "uuid::serde::compact")] + entity_uuid: uuid::Uuid, typ: VarInt, x: f64, y: f64, @@ -25,7 +26,7 @@ impl CSpawnEntity { #[expect(clippy::too_many_arguments)] pub fn new( entity_id: VarInt, - entity_uuid: UUID, + entity_uuid: uuid::Uuid, typ: VarInt, x: f64, y: f64, diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index 90a405280..47838aed6 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -9,10 +9,12 @@ mod c_close_container; mod c_disguised_chat_message; mod c_entity_animation; mod c_entity_metadata; +mod c_entity_status; mod c_entity_velocity; mod c_game_event; mod c_head_rot; mod c_hurt_animation; +mod c_keep_alive; mod c_login; mod c_open_screen; mod c_particle; @@ -26,6 +28,7 @@ mod c_remove_entities; mod c_set_container_content; mod c_set_container_property; mod c_set_container_slot; +mod c_set_health; mod c_set_held_item; mod c_set_title; mod c_spawn_player; @@ -51,10 +54,12 @@ pub use c_close_container::*; pub use c_disguised_chat_message::*; pub use c_entity_animation::*; pub use c_entity_metadata::*; +pub use c_entity_status::*; pub use c_entity_velocity::*; pub use c_game_event::*; pub use c_head_rot::*; pub use c_hurt_animation::*; +pub use c_keep_alive::*; pub use c_login::*; pub use c_open_screen::*; pub use c_particle::*; @@ -68,6 +73,7 @@ pub use c_remove_entities::*; pub use c_set_container_content::*; pub use c_set_container_property::*; pub use c_set_container_slot::*; +pub use c_set_health::*; pub use c_set_held_item::*; pub use c_set_title::*; pub use c_spawn_player::*; diff --git a/pumpkin-protocol/src/client/play/player_action.rs b/pumpkin-protocol/src/client/play/player_action.rs index 3a5f2a80a..48baf4eca 100644 --- a/pumpkin-protocol/src/client/play/player_action.rs +++ b/pumpkin-protocol/src/client/play/player_action.rs @@ -1,9 +1,9 @@ use crate::{Property, VarInt}; -pub enum PlayerAction { +pub enum PlayerAction<'a> { AddPlayer { - name: String, - properties: Vec, + name: &'a str, + properties: &'a [Property], }, InitializeChat(u8), /// Gamemode ? diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index 66caee7e9..aea2e4891 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -10,8 +10,9 @@ pub mod packet_decoder; pub mod packet_encoder; pub mod server; pub mod slot; -pub mod uuid; +/// To current Minecraft protocol +/// Don't forget to change this when porting pub const CURRENT_MC_PROTOCOL: u32 = 767; pub const MAX_PACKET_SIZE: i32 = 2097152; @@ -151,7 +152,7 @@ pub enum PacketError { MalformedLength, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum ConnectionState { HandShake, Status, @@ -175,7 +176,6 @@ impl From for ConnectionState { } } } - pub struct RawPacket { pub id: VarInt, pub bytebuf: ByteBuffer, @@ -191,32 +191,45 @@ pub trait ServerPacket: Packet + Sized { #[derive(Serialize)] pub struct StatusResponse { - pub version: Version, - pub players: Players, + /// The version on which the Server is running. Optional + pub version: Option, + /// Information about currently connected Players. Optional + pub players: Option, + /// The description displayed also called MOTD (Message of the day). Optional pub description: String, - pub favicon: Option, // data:image/png;base64, - // Players, favicon ... + /// The icon displayed, Optional + pub favicon: Option, + /// Players are forced to use Secure chat + pub enforce_secure_chat: bool, } #[derive(Serialize)] pub struct Version { + /// The current name of the Version (e.g. 1.21.1) pub name: String, + /// The current Protocol Version (e.g. 767) pub protocol: u32, } #[derive(Serialize)] pub struct Players { + /// The maximum Player count the server allows pub max: u32, + /// The current online player count pub online: u32, + /// Information about currently connected players. + /// Note player can disable listing here. pub sample: Vec, } #[derive(Serialize)] pub struct Sample { + /// Players Name pub name: String, - pub id: String, // uuid + /// Players UUID + pub id: String, } -// basicly game profile +// basically game profile #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Property { pub name: String, diff --git a/pumpkin-protocol/src/packet_decoder.rs b/pumpkin-protocol/src/packet_decoder.rs index 308716ccb..f3aa9e41f 100644 --- a/pumpkin-protocol/src/packet_decoder.rs +++ b/pumpkin-protocol/src/packet_decoder.rs @@ -13,11 +13,13 @@ use crate::{ type Cipher = cfb8::Decryptor; // Decoder: Client -> Server +// Supports ZLib decoding/decompression +// Supports Aes128 Encyption #[derive(Default)] pub struct PacketDecoder { buf: BytesMut, decompress_buf: BytesMut, - compression: Option, + compression: bool, cipher: Option, } @@ -43,7 +45,7 @@ impl PacketDecoder { let packet_len_len = VarInt(packet_len).written_size(); let mut data; - if self.compression.is_some() { + if self.compression { r = &r[..packet_len as usize]; let data_len = VarInt::decode(&mut r).map_err(|_| PacketError::TooLong)?.0; @@ -94,18 +96,26 @@ impl PacketDecoder { })) } - pub fn enable_encryption(&mut self, key: &[u8; 16]) { - assert!(self.cipher.is_none(), "encryption is already enabled"); + pub fn set_encryption(&mut self, key: Option<&[u8; 16]>) { + if let Some(key) = key { + assert!(self.cipher.is_none(), "encryption is already enabled"); - let mut cipher = Cipher::new_from_slices(key, key).expect("invalid key"); + let mut cipher = Cipher::new_from_slices(key, key).expect("invalid key"); - // Don't forget to decrypt the data we already have. - Self::decrypt_bytes(&mut cipher, &mut self.buf); + // Don't forget to decrypt the data we already have. - self.cipher = Some(cipher); + Self::decrypt_bytes(&mut cipher, &mut self.buf); + + self.cipher = Some(cipher); + } else { + assert!(self.cipher.is_some(), "encryption is already disabled"); + + self.cipher = None; + } } - pub fn set_compression(&mut self, compression: Option) { + /// Sets ZLib Deompression + pub fn set_compression(&mut self, compression: bool) { self.compression = compression; } diff --git a/pumpkin-protocol/src/packet_encoder.rs b/pumpkin-protocol/src/packet_encoder.rs index 36b9c3697..947fdf81e 100644 --- a/pumpkin-protocol/src/packet_encoder.rs +++ b/pumpkin-protocol/src/packet_encoder.rs @@ -2,6 +2,7 @@ use std::io::Write; use aes::cipher::{generic_array::GenericArray, BlockEncryptMut, BlockSizeUser, KeyIvInit}; use bytes::{BufMut, BytesMut}; +use pumpkin_config::compression::CompressionInfo; use std::io::Read; @@ -13,18 +14,19 @@ use crate::{bytebuf::ByteBuffer, ClientPacket, PacketError, VarInt, MAX_PACKET_S type Cipher = cfb8::Encryptor; // Encoder: Server -> Client +// Supports ZLib endecoding/compression +// Supports Aes128 Encyption #[derive(Default)] pub struct PacketEncoder { buf: BytesMut, compress_buf: Vec, - compression: Option<(u32, u32)>, + compression: Option, cipher: Option, } impl PacketEncoder { pub fn append_packet(&mut self, packet: &P) -> Result<(), PacketError> { let start_len = self.buf.len(); - let mut writer = (&mut self.buf).writer(); let mut packet_buf = ByteBuffer::empty(); @@ -39,10 +41,10 @@ impl PacketEncoder { let data_len = self.buf.len() - start_len; - if let Some((threshold, compression_level)) = self.compression { - if data_len > threshold as usize { + if let Some(compression) = &self.compression { + if data_len > compression.threshold as usize { let mut z = - ZlibEncoder::new(&self.buf[start_len..], Compression::new(compression_level)); + ZlibEncoder::new(&self.buf[start_len..], Compression::new(compression.level)); self.compress_buf.clear(); @@ -85,7 +87,6 @@ impl PacketEncoder { let mut front = &mut self.buf[start_len..]; - #[allow(clippy::needless_borrows_for_generic_args)] VarInt(packet_len as i32) .encode(&mut front) .map_err(|_| PacketError::EncodeLength)?; @@ -117,12 +118,20 @@ impl PacketEncoder { Ok(()) } - pub fn enable_encryption(&mut self, key: &[u8; 16]) { - assert!(self.cipher.is_none(), "encryption is already enabled"); - self.cipher = Some(Cipher::new_from_slices(key, key).expect("invalid key")); + pub fn set_encryption(&mut self, key: Option<&[u8; 16]>) { + if let Some(key) = key { + assert!(self.cipher.is_none(), "encryption is already enabled"); + + self.cipher = Some(Cipher::new_from_slices(key, key).expect("invalid key")); + } else { + assert!(self.cipher.is_some(), "encryption is disabled"); + + self.cipher = None; + } } - pub fn set_compression(&mut self, compression: Option<(u32, u32)>) { + /// Enables ZLib Compression + pub fn set_compression(&mut self, compression: Option) { self.compression = compression; } diff --git a/pumpkin-protocol/src/server/config/s_plugin_message.rs b/pumpkin-protocol/src/server/config/s_plugin_message.rs index 19e06c853..9150bf296 100644 --- a/pumpkin-protocol/src/server/config/s_plugin_message.rs +++ b/pumpkin-protocol/src/server/config/s_plugin_message.rs @@ -14,7 +14,7 @@ pub struct SPluginMessage { impl ServerPacket for SPluginMessage { fn read(bytebuf: &mut ByteBuffer) -> Result { Ok(Self { - channel: bytebuf.get_string().unwrap(), + channel: bytebuf.get_string()?, data: bytebuf.get_slice().to_vec(), }) } diff --git a/pumpkin-protocol/src/server/login/s_plugin_response.rs b/pumpkin-protocol/src/server/login/s_plugin_response.rs index 44360bde2..8b2dd0052 100644 --- a/pumpkin-protocol/src/server/login/s_plugin_response.rs +++ b/pumpkin-protocol/src/server/login/s_plugin_response.rs @@ -9,7 +9,6 @@ use crate::{ #[packet(0x02)] pub struct SLoginPluginResponse { pub message_id: VarInt, - pub successful: bool, pub data: Option, } @@ -17,7 +16,6 @@ impl ServerPacket for SLoginPluginResponse { fn read(bytebuf: &mut ByteBuffer) -> Result { Ok(Self { message_id: bytebuf.get_var_int()?, - successful: bytebuf.get_bool()?, data: bytebuf.get_option(|v| Ok(v.get_slice()))?, }) } diff --git a/pumpkin-protocol/src/server/play/mod.rs b/pumpkin-protocol/src/server/play/mod.rs index 59a8fc3f7..4723a6b57 100644 --- a/pumpkin-protocol/src/server/play/mod.rs +++ b/pumpkin-protocol/src/server/play/mod.rs @@ -5,6 +5,7 @@ mod s_client_information; mod s_close_container; mod s_confirm_teleport; mod s_interact; +mod s_keep_alive; mod s_ping_request; mod s_player_action; mod s_player_command; @@ -25,6 +26,7 @@ pub use s_client_information::*; pub use s_close_container::*; pub use s_confirm_teleport::*; pub use s_interact::*; +pub use s_keep_alive::*; pub use s_ping_request::*; pub use s_player_action::*; pub use s_player_command::*; diff --git a/pumpkin-protocol/src/server/play/s_keep_alive.rs b/pumpkin-protocol/src/server/play/s_keep_alive.rs new file mode 100644 index 000000000..449f00b06 --- /dev/null +++ b/pumpkin-protocol/src/server/play/s_keep_alive.rs @@ -0,0 +1,8 @@ +use pumpkin_macros::packet; +use serde::Deserialize; + +#[packet(0x18)] +#[derive(Deserialize)] +pub struct SKeepAlive { + pub keep_alive_id: i64, +} diff --git a/pumpkin-protocol/src/uuid.rs b/pumpkin-protocol/src/uuid.rs deleted file mode 100644 index ab70ef83a..000000000 --- a/pumpkin-protocol/src/uuid.rs +++ /dev/null @@ -1,13 +0,0 @@ -use serde::Serialize; - -#[derive(Clone)] -pub struct UUID(pub uuid::Uuid); - -impl Serialize for UUID { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_bytes(self.0.as_bytes()) - } -} diff --git a/pumpkin-registry/Cargo.toml b/pumpkin-registry/Cargo.toml index f4fe985d9..922cd5b06 100644 --- a/pumpkin-registry/Cargo.toml +++ b/pumpkin-registry/Cargo.toml @@ -4,8 +4,8 @@ version.workspace = true edition.workspace = true [dependencies] -pumpkin-protocol = { path = "../pumpkin-protocol"} -pumpkin-core = { path = "../pumpkin-core"} +pumpkin-protocol = { path = "../pumpkin-protocol" } +pumpkin-core = { path = "../pumpkin-core" } # nbt fastnbt = { git = "https://github.com/owengage/fastnbt.git" } diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index 90be69d5c..453842903 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -4,13 +4,14 @@ version.workspace = true edition.workspace = true [dependencies] -pumpkin-core = { path = "../pumpkin-core"} +pumpkin-core = { path = "../pumpkin-core" } +pumpkin-macros = { path = "../pumpkin-macros" } fastnbt = { git = "https://github.com/owengage/fastnbt.git" } tokio.workspace = true rayon.workspace = true derive_more.workspace = true -itertools = "0.13.0" +itertools.workspace = true thiserror = "1.0" futures = "0.3" flate2 = "1.0" @@ -19,6 +20,8 @@ serde_json = "1.0" static_assertions = "1.1.0" log.workspace = true +parking_lot.workspace = true + noise = "0.9.0" rand = "0.8.5" diff --git a/pumpkin-world/README.md b/pumpkin-world/README.md new file mode 100644 index 000000000..40637233c --- /dev/null +++ b/pumpkin-world/README.md @@ -0,0 +1,17 @@ +### Pumpkin World +Contains everything World related for example + +- Loading Chunks (Anvil Format) +- Generating Chunks +- Loading Blocks/Items + +### Porting +When updating your Minecraft server to a newer version, you typically need to replace the files in the assets directory to ensure compatibility with the new version's resources. +Thankfully, vanilla Minecraft provides a way to extract these updated assets directly from the server JAR file itself. + +1. Download the latest Minecraft server JAR file for the version you want to upgrade to. +2. Run `java -DbundlerMainClass=net.minecraft.data.Main -jar .jar --reports`. +3. This command will create a new folder named `reports` in the same directory as the server JAR. This folder contains the updated "assets" directory for the new version. +4. Copy the assets folder from the reports folder and replace the existing assets directory within your server directory. + +For details see https://wiki.vg/Data_Generators diff --git a/pumpkin-world/src/block/block_id.rs b/pumpkin-world/src/block/block_id.rs deleted file mode 100644 index 80380842c..000000000 --- a/pumpkin-world/src/block/block_id.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::collections::HashMap; - -use serde::Deserialize; - -use super::block_registry::BLOCKS; -use crate::level::WorldError; - -// 0 is air -> reasonable default -#[derive(Default, Deserialize, Debug, Hash, Clone, Copy, PartialEq, Eq)] -#[serde(transparent)] -pub struct BlockId { - data: u16, -} - -impl BlockId { - pub const AIR: Self = Self::from_id(0); - - pub fn new( - text_id: &str, - properties: Option<&HashMap>, - ) -> Result { - let mut block_states = BLOCKS - .get(text_id) - .ok_or(WorldError::BlockIdentifierNotFound)? - .states - .iter(); - - let block_state = match properties { - Some(properties) => match block_states.find(|state| &state.properties == properties) { - Some(state) => state, - None => return Err(WorldError::BlockStateIdNotFound), - }, - None => block_states - .find(|state| state.is_default) - .expect("Every Block should have at least 1 default state"), - }; - - Ok(block_state.id) - } - - pub const fn from_id(id: u16) -> Self { - // TODO: add check if the id is actually valid - Self { data: id } - } - - pub fn is_air(&self) -> bool { - self.data == 0 || self.data == 12959 || self.data == 12958 - } - - pub fn get_id(&self) -> u16 { - self.data - } - - /// An i32 is the way mojang internally represents their Blocks - pub fn get_id_mojang_repr(&self) -> i32 { - self.data as i32 - } -} diff --git a/pumpkin-world/src/block/block_registry.rs b/pumpkin-world/src/block/block_registry.rs index 7d7b3f69f..896efd8b2 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -2,13 +2,16 @@ use std::{collections::HashMap, sync::LazyLock}; use serde::Deserialize; -use super::block_id::BlockId; +use super::BlockState; pub static BLOCKS: LazyLock> = LazyLock::new(|| { - serde_json::from_str(include_str!("../../assets/blocks.json")) + serde_json::from_str(include_str!("../../../assets/blocks.json")) .expect("Could not parse block.json registry.") }); +pumpkin_macros::blocks_enum!(); +pumpkin_macros::block_categories_enum!(); + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct RegistryBlockDefinition { /// e.g. minecraft:door or minecraft:button @@ -48,3 +51,31 @@ pub struct RegistryBlockType { #[serde(default, rename = "properties")] valid_properties: HashMap>, } + +#[derive(Default, Copy, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(transparent)] +pub struct BlockId { + pub data: u16, +} + +impl BlockId { + pub fn is_air(&self) -> bool { + self.data == 0 || self.data == 12959 || self.data == 12958 + } + + pub fn get_id_mojang_repr(&self) -> i32 { + self.data as i32 + } + + pub fn get_id(&self) -> u16 { + self.data + } +} + +impl From for BlockId { + fn from(value: BlockState) -> Self { + Self { + data: value.get_id(), + } + } +} diff --git a/pumpkin-world/src/block/block_state.rs b/pumpkin-world/src/block/block_state.rs new file mode 100644 index 000000000..437349df2 --- /dev/null +++ b/pumpkin-world/src/block/block_state.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +use crate::level::WorldError; + +use super::block_registry::{Block, BlockCategory, BLOCKS}; + +#[derive(Clone)] +pub struct BlockState { + state_id: u16, + block: Block, + category: BlockCategory, +} + +impl BlockState { + pub const AIR: BlockState = BlockState { + state_id: 0, + block: Block::Air, + category: BlockCategory::Air, + }; + + pub fn new( + registry_id: &str, + properties: Option<&HashMap>, + ) -> Result { + let block_registry = BLOCKS + .get(registry_id) + .ok_or(WorldError::BlockIdentifierNotFound)?; + let mut block_states = block_registry.states.iter(); + + let block_state = match properties { + Some(properties) => block_states + .find(|state| &state.properties == properties) + .ok_or_else(|| WorldError::BlockStateIdNotFound)?, + None => block_states + .find(|state| state.is_default) + .expect("Every Block should have at least 1 default state"), + }; + + Ok(Self { + state_id: block_state.id.data, + block: Block::from_registry_id(registry_id), + category: BlockCategory::from_registry_id(&block_registry.definition.category), + }) + } + + pub const fn new_unchecked(state_id: u16, block: Block, category: BlockCategory) -> Self { + Self { + state_id, + block, + category, + } + } + + pub fn is_air(&self) -> bool { + self.category == BlockCategory::Air + } + + pub fn get_id(&self) -> u16 { + self.state_id + } + + pub fn get_id_mojang_repr(&self) -> i32 { + self.state_id as i32 + } + + pub fn of_block(&self, block: Block) -> bool { + self.block == block + } + + pub fn of_category(&self, category: BlockCategory) -> bool { + self.category == category + } +} diff --git a/pumpkin-world/src/block/mod.rs b/pumpkin-world/src/block/mod.rs index f5fbab664..8a838dedc 100644 --- a/pumpkin-world/src/block/mod.rs +++ b/pumpkin-world/src/block/mod.rs @@ -1,11 +1,13 @@ use num_derive::FromPrimitive; -pub mod block_id; mod block_registry; +pub mod block_state; -pub use block_id::BlockId; use pumpkin_core::math::vector3::Vector3; +pub use block_registry::{Block, BlockCategory, BlockId}; +pub use block_state::BlockState; + #[derive(FromPrimitive)] pub enum BlockFace { Bottom = 0, diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index dd5aa643b..2037bf5fa 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -7,7 +7,7 @@ use pumpkin_core::math::vector2::Vector2; use serde::{Deserialize, Serialize}; use crate::{ - block::BlockId, + block::{BlockId, BlockState}, coordinates::{ChunkRelativeBlockCoordinates, Height}, level::{ChunkNotGeneratedError, WorldError}, WORLD_HEIGHT, @@ -215,7 +215,12 @@ impl ChunkData { let palette = block_states .palette .iter() - .map(|entry| BlockId::new(&entry.name, entry.properties.as_ref())) + .map( + |entry| match BlockState::new(&entry.name, entry.properties.as_ref()) { + Err(e) => Err(e), + Ok(state) => Ok(state.into()), + }, + ) .collect::, _>>()?; let block_data = match block_states.data { diff --git a/pumpkin-world/src/global_registry.rs b/pumpkin-world/src/global_registry.rs index 19f8a6e6f..f822b57ab 100644 --- a/pumpkin-world/src/global_registry.rs +++ b/pumpkin-world/src/global_registry.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::LazyLock}; pub const ITEM_REGISTRY: &str = "minecraft:item"; -const REGISTRY_JSON: &str = include_str!("../assets/registries.json"); +const REGISTRY_JSON: &str = include_str!("../../assets/registries.json"); #[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct RegistryElement { diff --git a/pumpkin-world/src/item/item_registry.rs b/pumpkin-world/src/item/item_registry.rs index f91dd300c..756c30b4e 100644 --- a/pumpkin-world/src/item/item_registry.rs +++ b/pumpkin-world/src/item/item_registry.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::LazyLock}; use super::Rarity; use crate::global_registry::{self, ITEM_REGISTRY}; -const ITEMS_JSON: &str = include_str!("../../assets/items.json"); +const ITEMS_JSON: &str = include_str!("../../../assets/items.json"); pub static ITEMS: LazyLock> = LazyLock::new(|| { serde_json::from_str(ITEMS_JSON).expect("Could not parse items.json registry.") diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 8707912be..28c991124 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -3,11 +3,12 @@ use std::{ fs::OpenOptions, io::{Read, Seek}, path::PathBuf, - sync::{Arc, Mutex}, + sync::Arc, }; use flate2::{bufread::ZlibDecoder, read::GzDecoder}; use itertools::Itertools; +use parking_lot::Mutex; use pumpkin_core::math::vector2::Vector2; use rayon::prelude::*; use thiserror::Error; @@ -18,7 +19,15 @@ use crate::{ world_gen::{get_world_gen, Seed, WorldGenerator}, }; -/// The Level represents a single Dimension. +/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world. +/// +/// Key features include: +/// +/// - **Chunk Loading:** Efficiently loads chunks from disk (Anvil format). +/// - **Chunk Caching:** Stores accessed chunks in memory for faster access. +/// - **Chunk Generation:** Generates new chunks on-demand using a specified `WorldGenerator`. +/// +/// For more details on world generation, refer to the `WorldGenerator` module. pub struct Level { save_file: Option, loaded_chunks: Arc, Arc>>>, @@ -126,16 +135,7 @@ impl Level { } } - // /// Read one chunk in the world - // /// - // /// Do not use this function if reading many chunks is required, since in case those two chunks which are read separately using `.read_chunk` are in the same region file, it will need to be opened and closed separately for both of them, leading to a performance loss. - // pub async fn read_chunk(&self, chunk: (i32, i32)) -> Result { - // self.read_chunks(vec![chunk]) - // .await - // .pop() - // .expect("Read chunks must return a chunk") - // .1 - // } + pub fn get_block() {} /// Reads/Generates many chunks in a world /// MUST be called from a tokio runtime thread @@ -149,43 +149,41 @@ impl Level { ) { chunks.into_par_iter().for_each(|at| { if is_alive { - dbg!("a"); return; } - if let Ok(mut loaded_chunks) = self.loaded_chunks.lock() { - let channel = channel.clone(); + let mut loaded_chunks = self.loaded_chunks.lock(); + let channel = channel.clone(); - // Check if chunks is already loaded - if loaded_chunks.contains_key(at) { - channel - .blocking_send(Ok(loaded_chunks.get(at).unwrap().clone())) - .expect("Failed sending ChunkData."); - return; - } - let at = *at; - let data = match &self.save_file { - Some(save_file) => { - match Self::read_chunk(save_file, at) { - Err(WorldError::ChunkNotGenerated(_)) => { - // This chunk was not generated yet. - Ok(self.world_gen.generate_chunk(at)) - } - // TODO this doesn't warn the user about the error. fix. - result => result, - } - } - None => { - // There is no savefile yet -> generate the chunks - Ok(self.world_gen.generate_chunk(at)) - } - } - .unwrap(); - let data = Arc::new(data); + // Check if chunks is already loaded + if loaded_chunks.contains_key(at) { channel - .blocking_send(Ok(data.clone())) + .blocking_send(Ok(loaded_chunks.get(at).unwrap().clone())) .expect("Failed sending ChunkData."); - loaded_chunks.insert(at, data); + return; } + let at = *at; + let data = match &self.save_file { + Some(save_file) => { + match Self::read_chunk(save_file, at) { + Err(WorldError::ChunkNotGenerated(_)) => { + // This chunk was not generated yet. + Ok(self.world_gen.generate_chunk(at)) + } + // TODO this doesn't warn the user about the error. fix. + result => result, + } + } + None => { + // There is no savefile yet -> generate the chunks + Ok(self.world_gen.generate_chunk(at)) + } + } + .unwrap(); + let data = Arc::new(data); + channel + .blocking_send(Ok(data.clone())) + .expect("Failed sending ChunkData."); + loaded_chunks.insert(at, data); }) } @@ -238,29 +236,21 @@ impl Level { // Read the file using the offset and size let mut file_buf = { - let seek_result = region_file.seek(std::io::SeekFrom::Start(offset)); - if seek_result.is_err() { - return Err(WorldError::RegionIsInvalid); - } + region_file + .seek(std::io::SeekFrom::Start(offset)) + .map_err(|_| WorldError::RegionIsInvalid)?; let mut out = vec![0; size]; - let read_result = region_file.read_exact(&mut out); - if read_result.is_err() { - return Err(WorldError::RegionIsInvalid); - } + region_file + .read_exact(&mut out) + .map_err(|_| WorldError::RegionIsInvalid)?; out }; // TODO: check checksum to make sure chunk is not corrupted let header = file_buf.drain(0..5).collect_vec(); - let compression = match Compression::from_byte(header[4]) { - Some(c) => c, - None => { - return Err(WorldError::Compression( - CompressionError::UnknownCompression, - )) - } - }; + let compression = Compression::from_byte(header[4]) + .ok_or_else(|| WorldError::Compression(CompressionError::UnknownCompression))?; let size = u32::from_be_bytes(header[..4].try_into().unwrap()); @@ -280,23 +270,15 @@ impl Level { Compression::Gzip => { let mut z = GzDecoder::new(&compressed_data[..]); let mut chunk_data = Vec::with_capacity(compressed_data.len()); - match z.read_to_end(&mut chunk_data) { - Ok(_) => {} - Err(e) => { - return Err(CompressionError::GZipError(e)); - } - } + z.read_to_end(&mut chunk_data) + .map_err(CompressionError::GZipError)?; Ok(chunk_data) } Compression::Zlib => { let mut z = ZlibDecoder::new(&compressed_data[..]); let mut chunk_data = Vec::with_capacity(compressed_data.len()); - match z.read_to_end(&mut chunk_data) { - Ok(_) => {} - Err(e) => { - return Err(CompressionError::ZlibError(e)); - } - } + z.read_to_end(&mut chunk_data) + .map_err(CompressionError::ZlibError)?; Ok(chunk_data) } Compression::None => Ok(compressed_data), diff --git a/pumpkin-world/src/world_gen/generator.rs b/pumpkin-world/src/world_gen/generator.rs index 0a7d043d6..36f3f9553 100644 --- a/pumpkin-world/src/world_gen/generator.rs +++ b/pumpkin-world/src/world_gen/generator.rs @@ -3,7 +3,7 @@ use pumpkin_core::math::vector2::Vector2; use static_assertions::assert_obj_safe; use crate::biome::Biome; -use crate::block::BlockId; +use crate::block::block_state::BlockState; use crate::chunk::ChunkData; use crate::coordinates::{BlockCoordinates, XZBlockCoordinates}; use crate::world_gen::Seed; @@ -26,12 +26,12 @@ pub(crate) trait TerrainGenerator: Sync + Send { fn prepare_chunk(&self, at: &Vector2); /// Is static - fn generate_block(&self, at: BlockCoordinates, biome: Biome) -> BlockId; + fn generate_block(&self, at: BlockCoordinates, biome: Biome) -> BlockState; } pub(crate) trait PerlinTerrainGenerator: Sync + Send { fn prepare_chunk(&self, at: &Vector2, perlin: &Perlin); /// Dependens on the perlin noise height - fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, biome: Biome) -> BlockId; + fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, biome: Biome) -> BlockState; } diff --git a/pumpkin-world/src/world_gen/generic_generator.rs b/pumpkin-world/src/world_gen/generic_generator.rs index 8bdc17135..1f0a17657 100644 --- a/pumpkin-world/src/world_gen/generic_generator.rs +++ b/pumpkin-world/src/world_gen/generic_generator.rs @@ -62,11 +62,13 @@ impl WorldGenerator for GenericGen blocks.set_block( coordinates, - self.terrain_generator.generate_block( - coordinates.with_chunk_coordinates(at), - chunk_height as i16, - biome, - ), + self.terrain_generator + .generate_block( + coordinates.with_chunk_coordinates(at), + chunk_height as i16, + biome, + ) + .into(), ); } } diff --git a/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs b/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs index cb3f3e981..001c71be7 100644 --- a/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs +++ b/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs @@ -3,7 +3,7 @@ use pumpkin_core::math::vector2::Vector2; use crate::{ biome::Biome, - block::BlockId, + block::block_state::BlockState, coordinates::{BlockCoordinates, XZBlockCoordinates}, world_gen::{ generator::{BiomeGenerator, GeneratorInit, PerlinTerrainGenerator}, @@ -40,21 +40,21 @@ impl GeneratorInit for PlainsTerrainGenerator { impl PerlinTerrainGenerator for PlainsTerrainGenerator { fn prepare_chunk(&self, _at: &Vector2, _perlin: &Perlin) {} // TODO allow specifying which blocks should be at which height in the config. - fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, _: Biome) -> BlockId { + fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, _: Biome) -> BlockState { let begin_stone_height = chunk_height - 5; let begin_dirt_height = chunk_height - 1; let y = *at.y; if y == -64 { - BlockId::from_id(79) // BEDROCK + pumpkin_macros::block!("minecraft:bedrock") } else if y >= -63 && y <= begin_stone_height { - return BlockId::from_id(1); // STONE + pumpkin_macros::block!("minecraft:stone") } else if y >= begin_stone_height && y < begin_dirt_height { - return BlockId::from_id(10); // DIRT; + pumpkin_macros::block!("minecraft:dirt") } else if y == chunk_height - 1 { - return BlockId::from_id(9); // GRASS BLOCK + pumpkin_macros::block!("minecraft:grass_block") } else { - BlockId::AIR + BlockState::AIR } } } diff --git a/pumpkin-world/src/world_gen/implementation/superflat.rs b/pumpkin-world/src/world_gen/implementation/superflat.rs index 3acf32495..872c31515 100644 --- a/pumpkin-world/src/world_gen/implementation/superflat.rs +++ b/pumpkin-world/src/world_gen/implementation/superflat.rs @@ -1,8 +1,9 @@ use pumpkin_core::math::vector2::Vector2; +use pumpkin_macros::block; use crate::{ biome::Biome, - block::BlockId, + block::block_state::BlockState, coordinates::{BlockCoordinates, XZBlockCoordinates}, world_gen::{ generator::{BiomeGenerator, GeneratorInit, TerrainGenerator}, @@ -40,12 +41,12 @@ impl GeneratorInit for SuperflatTerrainGenerator { impl TerrainGenerator for SuperflatTerrainGenerator { fn prepare_chunk(&self, _at: &Vector2) {} // TODO allow specifying which blocks should be at which height in the config. - fn generate_block(&self, at: BlockCoordinates, _: Biome) -> BlockId { + fn generate_block(&self, at: BlockCoordinates, _: Biome) -> BlockState { match *at.y { - -64 => BlockId::from_id(79), // Bedrock - -63..=-62 => BlockId::from_id(10), // Dirt - -61 => BlockId::from_id(9), // Grass - _ => BlockId::AIR, + -64 => block!("minecraft:bedrock"), + -63..=-62 => block!("minecraft:dirt"), + -61 => block!("minecraft:grass_block"), + _ => BlockState::AIR, } } } diff --git a/pumpkin-world/src/world_gen/mod.rs b/pumpkin-world/src/world_gen/mod.rs index ddb1bbb9e..4712a36f3 100644 --- a/pumpkin-world/src/world_gen/mod.rs +++ b/pumpkin-world/src/world_gen/mod.rs @@ -1,6 +1,7 @@ mod generator; mod generic_generator; mod implementation; +mod noise; mod seed; pub use generator::WorldGenerator; diff --git a/pumpkin-world/src/world_gen/noise/mod.rs b/pumpkin-world/src/world_gen/noise/mod.rs new file mode 100644 index 000000000..ea5d51196 --- /dev/null +++ b/pumpkin-world/src/world_gen/noise/mod.rs @@ -0,0 +1,65 @@ +#![allow(dead_code)] +mod perlin; +mod simplex; + +pub fn lerp(delta: f64, start: f64, end: f64) -> f64 { + start + delta * (end - start) +} + +pub fn lerp2(delta_x: f64, delta_y: f64, x0y0: f64, x1y0: f64, x0y1: f64, x1y1: f64) -> f64 { + lerp( + delta_y, + lerp(delta_x, x0y0, x1y0), + lerp(delta_x, x0y1, x1y1), + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn lerp3( + delta_x: f64, + delta_y: f64, + delta_z: f64, + x0y0z0: f64, + x1y0z0: f64, + x0y1z0: f64, + x1y1z0: f64, + x0y0z1: f64, + x1y0z1: f64, + x0y1z1: f64, + x1y1z1: f64, +) -> f64 { + lerp( + delta_z, + lerp2(delta_x, delta_y, x0y0z0, x1y0z0, x0y1z0, x1y1z0), + lerp2(delta_x, delta_y, x0y0z1, x1y0z1, x0y1z1, x1y1z1), + ) +} + +struct Gradient { + x: i32, + y: i32, + z: i32, +} + +const GRADIENTS: [Gradient; 16] = [ + Gradient { x: 1, y: 1, z: 0 }, + Gradient { x: -1, y: 1, z: 0 }, + Gradient { x: 1, y: -1, z: 0 }, + Gradient { x: -1, y: -1, z: 0 }, + Gradient { x: 1, y: 0, z: 1 }, + Gradient { x: -1, y: 0, z: 1 }, + Gradient { x: 1, y: 0, z: -1 }, + Gradient { x: -1, y: 0, z: -1 }, + Gradient { x: 0, y: 1, z: 1 }, + Gradient { x: 0, y: -1, z: 1 }, + Gradient { x: 0, y: 1, z: -1 }, + Gradient { x: 0, y: -1, z: -1 }, + Gradient { x: 1, y: 1, z: 0 }, + Gradient { x: 0, y: -1, z: 1 }, + Gradient { x: -1, y: 1, z: 0 }, + Gradient { x: 0, y: -1, z: -1 }, +]; + +fn dot(gradient: &Gradient, x: f64, y: f64, z: f64) -> f64 { + gradient.x as f64 * x + gradient.y as f64 * y + gradient.z as f64 * z +} diff --git a/pumpkin-world/src/world_gen/noise/perlin.rs b/pumpkin-world/src/world_gen/noise/perlin.rs new file mode 100644 index 000000000..a96b1fc6c --- /dev/null +++ b/pumpkin-world/src/world_gen/noise/perlin.rs @@ -0,0 +1,1125 @@ +use itertools::Itertools; +use num_traits::{Pow, Zero}; +use pumpkin_core::random::{RandomDeriverImpl, RandomGenerator, RandomImpl}; + +use super::{dot, lerp3, GRADIENTS}; + +pub struct PerlinNoiseSampler { + permutation: Box<[u8]>, + x_origin: f64, + y_origin: f64, + z_origin: f64, +} + +impl PerlinNoiseSampler { + pub fn new(random: &mut impl RandomImpl) -> Self { + let x_origin = random.next_f64() * 256f64; + let y_origin = random.next_f64() * 256f64; + let z_origin = random.next_f64() * 256f64; + + let mut permutation = [0u8; 256]; + + permutation + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = i as u8); + + for i in 0..256 { + let j = random.next_bounded_i32((256 - i) as i32) as usize; + permutation.swap(i, i + j); + } + + Self { + permutation: Box::new(permutation), + x_origin, + y_origin, + z_origin, + } + } + + pub fn sample_flat_y(&self, x: f64, y: f64, z: f64) -> f64 { + self.sample_no_fade(x, y, z, 0f64, 0f64) + } + + pub fn sample_no_fade(&self, x: f64, y: f64, z: f64, y_scale: f64, y_max: f64) -> f64 { + let trans_x = x + self.x_origin; + let trans_y = y + self.y_origin; + let trans_z = z + self.z_origin; + + let x_int = trans_x.floor() as i32; + let y_int = trans_y.floor() as i32; + let z_int = trans_z.floor() as i32; + + let x_dec = trans_x - x_int as f64; + let y_dec = trans_y - y_int as f64; + let z_dec = trans_z - z_int as f64; + + let y_noise = if y_scale != 0f64 { + let raw_y_dec = if y_max >= 0f64 && y_max < y_dec { + y_max + } else { + y_dec + }; + (raw_y_dec / y_scale + 1.0E-7f32 as f64).floor() * y_scale + } else { + 0f64 + }; + + self.sample(x_int, y_int, z_int, x_dec, y_dec - y_noise, z_dec, y_dec) + } + + fn grad(hash: i32, x: f64, y: f64, z: f64) -> f64 { + dot(&GRADIENTS[(hash & 15) as usize], x, y, z) + } + + fn perlin_fade(value: f64) -> f64 { + value * value * value * (value * (value * 6f64 - 15f64) + 10f64) + } + + fn map(&self, input: i32) -> i32 { + self.permutation[(input & 0xFF) as usize] as i32 + } + + #[allow(clippy::too_many_arguments)] + fn sample( + &self, + x: i32, + y: i32, + z: i32, + local_x: f64, + local_y: f64, + local_z: f64, + fade_local_y: f64, + ) -> f64 { + let i = self.map(x); + let j = self.map(x.wrapping_add(1)); + let k = self.map(i.wrapping_add(y)); + + let l = self.map(i.wrapping_add(y).wrapping_add(1)); + let m = self.map(j.wrapping_add(y)); + let n = self.map(j.wrapping_add(y).wrapping_add(1)); + + let d = Self::grad(self.map(k.wrapping_add(z)), local_x, local_y, local_z); + let e = Self::grad( + self.map(m.wrapping_add(z)), + local_x - 1f64, + local_y, + local_z, + ); + let f = Self::grad( + self.map(l.wrapping_add(z)), + local_x, + local_y - 1f64, + local_z, + ); + let g = Self::grad( + self.map(n.wrapping_add(z)), + local_x - 1f64, + local_y - 1f64, + local_z, + ); + let h = Self::grad( + self.map(k.wrapping_add(z).wrapping_add(1)), + local_x, + local_y, + local_z - 1f64, + ); + let o = Self::grad( + self.map(m.wrapping_add(z).wrapping_add(1)), + local_x - 1f64, + local_y, + local_z - 1f64, + ); + let p = Self::grad( + self.map(l.wrapping_add(z).wrapping_add(1)), + local_x, + local_y - 1f64, + local_z - 1f64, + ); + let q = Self::grad( + self.map(n.wrapping_add(z).wrapping_add(1)), + local_x - 1f64, + local_y - 1f64, + local_z - 1f64, + ); + let r = Self::perlin_fade(local_x); + let s = Self::perlin_fade(fade_local_y); + let t = Self::perlin_fade(local_z); + + lerp3(r, s, t, d, e, f, g, h, o, p, q) + } +} + +pub struct OctavePerlinNoiseSampler { + octave_samplers: Vec>, + amplitudes: Vec, + first_octave: i32, + persistence: f64, + lacunarity: f64, + max_value: f64, +} + +impl OctavePerlinNoiseSampler { + fn get_total_amplitude(scale: f64, persistence: f64, amplitudes: &[f64]) -> f64 { + let mut d = 0f64; + let mut e = persistence; + + for amplitude in amplitudes.iter() { + d += amplitude * scale * e; + e /= 2f64; + } + + d + } + + fn maintain_precision(value: f64) -> f64 { + value - (value / 3.3554432E7f64 + 0.5f64).floor() * 3.3554432E7f64 + } + + pub fn calculate_amplitudes(octaves: &[i32]) -> (i32, Vec) { + let mut octaves = Vec::from_iter(octaves); + octaves.sort(); + + let i = -**octaves.first().expect("we should have some octaves"); + let j = **octaves.last().expect("we should have some octaves"); + let k = i + j + 1; + + let mut double_list: Vec = Vec::with_capacity(k as usize); + for _ in 0..k { + double_list.push(0f64) + } + + for l in octaves { + double_list[(l + i) as usize] = 1f64; + } + + (-i, double_list) + } + + pub fn new(random: &mut RandomGenerator, first_octave: i32, amplitudes: &[f64]) -> Self { + let i = amplitudes.len(); + let j = -first_octave; + + let mut samplers: Vec> = Vec::with_capacity(i); + for _ in 0..i { + samplers.push(None); + } + + match random { + RandomGenerator::Xoroshiro(random) => { + let splitter = random.next_splitter(); + for k in 0..i { + if amplitudes[k] != 0f64 { + let l = first_octave + k as i32; + samplers[k] = Some(PerlinNoiseSampler::new( + &mut splitter.split_string(&format!("octave_{}", l)), + )); + } + } + } + RandomGenerator::Legacy(random) => { + let sampler = PerlinNoiseSampler::new(random); + if j >= 0 && j < i as i32 { + let d = amplitudes[j as usize]; + if d != 0f64 { + samplers[j as usize] = Some(sampler); + } + } + + for kx in (0..j as usize).rev() { + if kx < i { + let e = amplitudes[kx]; + if e != 0f64 { + samplers[kx] = Some(PerlinNoiseSampler::new(random)); + } else { + random.skip(262); + } + } else { + random.skip(262); + } + } + + if let Ok(length1) = samplers.iter().filter(|x| x.is_some()).try_len() { + if let Ok(length2) = amplitudes.iter().filter(|x| !x.is_zero()).try_len() { + assert_eq!(length1, length2); + } + } + assert!(j >= i as i32 - 1); + } + } + + let persistence = 2f64.pow((i as i32).wrapping_sub(1) as f64) / (2f64.pow(i as f64) - 1f64); + let max_value = Self::get_total_amplitude(2f64, persistence, amplitudes); + Self { + octave_samplers: samplers, + amplitudes: amplitudes.to_vec(), + first_octave, + persistence, + lacunarity: 2f64.pow((-j) as f64), + max_value, + } + } + + pub fn sample(&self, x: f64, y: f64, z: f64) -> f64 { + let mut d = 0f64; + let mut e = self.lacunarity; + let mut f = self.persistence; + + for (sampler, amplitude) in self.octave_samplers.iter().zip(self.amplitudes.iter()) { + if let Some(sampler) = sampler { + let g = sampler.sample_no_fade( + Self::maintain_precision(x * e), + Self::maintain_precision(y * e), + Self::maintain_precision(z * e), + 0f64, + 0f64, + ); + + d += amplitude * g * f; + } + + e *= 2f64; + f /= 2f64; + } + + d + } +} + +pub struct DoublePerlinNoiseSampler { + first_sampler: OctavePerlinNoiseSampler, + second_sampler: OctavePerlinNoiseSampler, + amplitude: f64, + max_value: f64, +} + +impl DoublePerlinNoiseSampler { + fn create_amplitude(octaves: i32) -> f64 { + 0.1f64 * (1f64 + 1f64 / (octaves + 1) as f64) + } + + pub fn new(rand: &mut RandomGenerator, first_octave: i32, amplitudes: &[f64]) -> Self { + let first_sampler = OctavePerlinNoiseSampler::new(rand, first_octave, amplitudes); + let second_sampler = OctavePerlinNoiseSampler::new(rand, first_octave, amplitudes); + + let mut j = i32::MAX; + let mut k = i32::MIN; + + for (index, amplitude) in amplitudes.iter().enumerate() { + if *amplitude != 0f64 { + j = i32::min(j, index as i32); + k = i32::max(k, index as i32); + } + } + + let amplitude = 0.16666666666666666f64 / Self::create_amplitude(k - j); + let max_value = (first_sampler.max_value + second_sampler.max_value) * amplitude; + + Self { + first_sampler, + second_sampler, + amplitude, + max_value, + } + } + + pub fn sample(&self, x: f64, y: f64, z: f64) -> f64 { + let d = x * 1.0181268882175227f64; + let e = y * 1.0181268882175227f64; + let f = z * 1.0181268882175227f64; + + (self.first_sampler.sample(x, y, z) + self.second_sampler.sample(d, e, f)) * self.amplitude + } +} + +#[cfg(test)] +mod double_perlin_noise_sampler_test { + use pumpkin_core::random::{legacy_rand::LegacyRand, xoroshiro128::Xoroshiro, RandomImpl}; + + use crate::world_gen::noise::perlin::{DoublePerlinNoiseSampler, RandomGenerator}; + + #[test] + fn sample_legacy() { + let mut rand = LegacyRand::from_seed(513513513); + assert_eq!(rand.next_i32(), -1302745855); + + let mut rand_gen = RandomGenerator::Legacy(rand); + let sampler = DoublePerlinNoiseSampler::new(&mut rand_gen, 0, &[4f64]); + + let values = [ + ( + ( + 3.7329617139221236E7, + 2.847228022372606E8, + -1.8244299064688918E8, + ), + -0.5044027150385925, + ), + ( + ( + 8.936597679535551E7, + 1.491954533221004E8, + 3.457494216166344E8, + ), + -1.0004671438756043, + ), + ( + ( + -2.2479845046034336E8, + -4.085449163378981E7, + 1.343082907470065E8, + ), + 2.1781128778536973, + ), + ( + ( + -1.9094944979652843E8, + 3.695081561625232E8, + 2.1566424798360935E8, + ), + -1.2571847948126453, + ), + ( + ( + 1.8486356004931596E8, + -4.148713734284534E8, + 4.8687219454012525E8, + ), + -0.550285244015363, + ), + ( + ( + 1.7115351141710258E8, + -1.8835885697652313E8, + 1.7031060329927653E8, + ), + -0.6953327750604766, + ), + ( + ( + 8.952317194270046E7, + -5.420942524023042E7, + -2.5987559023045145E7, + ), + 2.7361630914824393, + ), + ( + ( + -8.36195975247282E8, + -1.2167090318484206E8, + 2.1237199673286602E8, + ), + -1.5518675789351004, + ), + ( + ( + 3.333103540906928E8, + 5.088236187007203E8, + -3.521137809477999E8, + ), + 0.6928720433082317, + ), + ( + ( + 7.82760234776598E7, + -2.5204361464037597E7, + -1.6615974590937865E8, + ), + -0.5102124930620466, + ), + ]; + + for ((x, y, z), sample) in values { + assert_eq!(sampler.sample(x, y, z), sample) + } + } + + #[test] + fn sample_xoroshiro() { + let mut rand = Xoroshiro::from_seed(5); + assert_eq!(rand.next_i32(), -1678727252); + + let mut rand_gen = RandomGenerator::Xoroshiro(rand); + let sampler = DoublePerlinNoiseSampler::new(&mut rand_gen, 1, &[2f64, 4f64]); + + let values = [ + ( + ( + -2.4823401687190732E8, + 1.6909869132832196E8, + 1.0510057123823991E8, + ), + -0.09627881756376819, + ), + ( + ( + 1.2971355215791291E8, + -3.614855223614046E8, + 1.9997149869463342E8, + ), + 0.4412466810560897, + ), + ( + ( + -1.9858224577678584E7, + 2.5103843334053648E8, + 2.253841390457064E8, + ), + -1.3086196098510068, + ), + ( + ( + 1.4243878295159304E8, + -1.9185612600051942E8, + 4.7736284830701286E8, + ), + 1.727683424808049, + ), + ( + ( + -9.411241394159131E7, + 4.4052130232611096E8, + 5.1042225596740514E8, + ), + -0.4651812519989636, + ), + ( + ( + 3.007670445405074E8, + 1.4630490674448165E8, + -1.681994537227527E8, + ), + -0.8607587886441551, + ), + ( + ( + -2.290369962944646E8, + -4.9627750061129004E8, + 9.751744069476394E7, + ), + -0.3592693708849225, + ), + ( + ( + -5.380825223911383E7, + 6.317706682942032E7, + -3.0105795661690116E8, + ), + 0.7372424991843702, + ), + ( + ( + -1.4261684559190175E8, + 9.987839104129419E7, + 3.3290027416415906E8, + ), + 0.27706980571082485, + ), + ( + ( + -8.881637146904664E7, + 1.1033687270820947E8, + -1.0014482192140123E8, + ), + -0.4602443245357103, + ), + ]; + + for ((x, y, z), sample) in values { + assert_eq!(sampler.sample(x, y, z), sample) + } + } +} + +#[cfg(test)] +mod octave_perline_noise_sampler_test { + use pumpkin_core::random::{legacy_rand::LegacyRand, xoroshiro128::Xoroshiro, RandomImpl}; + + use crate::world_gen::noise::perlin::RandomGenerator; + + use super::OctavePerlinNoiseSampler; + + #[test] + fn test_create_xoroshiro() { + let mut rand = Xoroshiro::from_seed(513513513); + assert_eq!(rand.next_i32(), 404174895); + + let (start, amplitudes) = OctavePerlinNoiseSampler::calculate_amplitudes(&[1, 2, 3]); + assert_eq!(start, 1); + assert_eq!(amplitudes, [1f64, 1f64, 1f64]); + + let mut rand_gen = RandomGenerator::Xoroshiro(rand); + let sampler = OctavePerlinNoiseSampler::new(&mut rand_gen, start, &litudes); + + assert_eq!(sampler.first_octave, 1); + assert_eq!(sampler.persistence, 0.5714285714285714f64); + assert_eq!(sampler.lacunarity, 2f64); + assert_eq!(sampler.max_value, 2f64); + + let coords = [ + (210.19539348148294, 203.08258445596215, 45.29925114984684), + (24.841250686920773, 181.62678157390076, 69.49871248131629), + (21.65886467061867, 97.80131502331685, 225.9273676334467), + ]; + + for (sampler, (x, y, z)) in sampler.octave_samplers.iter().zip(coords) { + match sampler { + Some(sampler) => { + assert_eq!(sampler.x_origin, x); + assert_eq!(sampler.y_origin, y); + assert_eq!(sampler.z_origin, z); + } + None => panic!(), + } + } + } + + #[test] + fn test_create_legacy() { + let mut rand = LegacyRand::from_seed(513513513); + assert_eq!(rand.next_i32(), -1302745855); + + let (start, amplitudes) = OctavePerlinNoiseSampler::calculate_amplitudes(&[0]); + assert_eq!(start, 0); + assert_eq!(amplitudes, [1f64]); + + let mut rand_gen = RandomGenerator::Legacy(rand); + let sampler = OctavePerlinNoiseSampler::new(&mut rand_gen, start, &litudes); + assert_eq!(sampler.first_octave, 0); + assert_eq!(sampler.persistence, 1f64); + assert_eq!(sampler.lacunarity, 1f64); + assert_eq!(sampler.max_value, 2f64); + + let coords = [(226.220117499588, 32.67924779023767, 202.84067325597647)]; + + for (sampler, (x, y, z)) in sampler.octave_samplers.iter().zip(coords) { + match sampler { + Some(sampler) => { + assert_eq!(sampler.x_origin, x); + assert_eq!(sampler.y_origin, y); + assert_eq!(sampler.z_origin, z); + } + None => panic!(), + } + } + } + + #[test] + fn test_sample() { + let mut rand = Xoroshiro::from_seed(513513513); + assert_eq!(rand.next_i32(), 404174895); + + let (start, amplitudes) = OctavePerlinNoiseSampler::calculate_amplitudes(&[1, 2, 3]); + let mut rand_gen = RandomGenerator::Xoroshiro(rand); + let sampler = OctavePerlinNoiseSampler::new(&mut rand_gen, start, &litudes); + + let values = [ + ( + ( + 1.4633897801218182E8, + 3.360929121402108E8, + -1.7376184515043163E8, + ), + -0.16510137639683028, + ), + ( + ( + -3.952093942501234E8, + -8.149682915016855E7, + 2.0761709535397574E8, + ), + -0.19865227457826365, + ), + ( + ( + 1.0603518812861493E8, + -1.6028050039630303E8, + 9.621510690305333E7, + ), + -0.16157548492944798, + ), + ( + ( + -2.2789281609860754E8, + 1.2416505757723756E8, + -3.047619296454517E8, + ), + -0.05762575118540847, + ), + ( + ( + -1.6361322604690066E8, + -1.862652364900794E8, + 9.03458926538596E7, + ), + 0.21589404036742288, + ), + ( + ( + -1.6074718857061076E8, + -4.816551924254624E8, + -9.930236785759543E7, + ), + 0.1888188057014473, + ), + ( + ( + -1.6848478115907547E8, + 1.9495247771890038E8, + 1.3780564333313772E8, + ), + 0.23114508298896774, + ), + ( + ( + 2.5355640846261957E8, + -2.5973376726076955E8, + 3.7834594620459855E7, + ), + -0.23703473310230702, + ), + ( + ( + -8.636649828254433E7, + 1.7017680431584623E8, + 2.941033134334743E8, + ), + -0.14050102207739693, + ), + ( + ( + -4.573784466442647E8, + 1.789046617664721E8, + -5.515223967099891E8, + ), + -0.1422470544720957, + ), + ]; + + for ((x, y, z), sample) in values { + assert_eq!(sampler.sample(x, y, z), sample); + } + } +} + +#[cfg(test)] +mod perlin_noise_sampler_test { + use std::ops::Deref; + + use pumpkin_core::random::{xoroshiro128::Xoroshiro, RandomImpl}; + + use crate::world_gen::noise::perlin::PerlinNoiseSampler; + + #[test] + fn test_create() { + let mut rand = Xoroshiro::from_seed(111); + assert_eq!(rand.next_i32(), -1467508761); + + let sampler = PerlinNoiseSampler::new(&mut rand); + assert_eq!(sampler.x_origin, 48.58072036717974); + assert_eq!(sampler.y_origin, 110.73235882678037); + assert_eq!(sampler.z_origin, 65.26438852860176); + + let permutation: [u8; 256] = [ + 159, 113, 41, 143, 203, 123, 95, 177, 25, 79, 229, 219, 194, 60, 130, 14, 83, 99, 24, + 202, 207, 232, 167, 152, 220, 201, 29, 235, 87, 147, 74, 160, 155, 97, 111, 31, 85, + 205, 115, 50, 13, 171, 77, 237, 149, 116, 209, 174, 169, 109, 221, 9, 166, 84, 54, 216, + 121, 106, 211, 16, 69, 244, 65, 192, 183, 146, 124, 37, 56, 45, 193, 158, 126, 217, 36, + 255, 162, 163, 230, 103, 63, 90, 191, 214, 20, 138, 32, 39, 238, 67, 64, 105, 250, 140, + 148, 114, 68, 75, 200, 161, 239, 125, 227, 199, 101, 61, 175, 107, 129, 240, 170, 51, + 139, 86, 186, 145, 212, 178, 30, 251, 89, 226, 120, 153, 47, 141, 233, 2, 179, 236, 1, + 19, 98, 21, 164, 108, 11, 23, 91, 204, 119, 88, 165, 195, 168, 26, 48, 206, 128, 6, 52, + 118, 110, 180, 197, 231, 117, 7, 3, 135, 224, 58, 82, 78, 4, 59, 222, 18, 72, 57, 150, + 43, 246, 100, 122, 112, 53, 133, 93, 17, 27, 210, 142, 234, 245, 80, 22, 46, 185, 172, + 71, 248, 33, 173, 76, 35, 40, 92, 228, 127, 254, 70, 42, 208, 73, 104, 187, 62, 154, + 243, 189, 241, 34, 66, 249, 94, 8, 12, 134, 132, 102, 242, 196, 218, 181, 28, 38, 15, + 151, 157, 247, 223, 198, 55, 188, 96, 0, 182, 49, 190, 156, 10, 215, 252, 131, 137, + 184, 176, 136, 81, 44, 213, 253, 144, 225, 5, + ]; + assert_eq!(sampler.permutation.deref(), permutation); + } + + #[test] + fn test_no_y() { + let mut rand = Xoroshiro::from_seed(111); + assert_eq!(rand.next_i32(), -1467508761); + let sampler = PerlinNoiseSampler::new(&mut rand); + + let values = [ + ( + ( + -3.134738528791615E8, + 5.676610095659718E7, + 2.011711832498507E8, + ), + 0.38582139614602945, + ), + ( + (-1369026.560586418, 3.957311252810864E8, 6.797037355570006E8), + 0.15777501333157193, + ), + ( + ( + 6.439373693833767E8, + -3.36218773041759E8, + -3.265494249695775E8, + ), + -0.2806135912409497, + ), + ( + ( + 1.353820060118252E8, + -3.204701624793043E8, + -4.612474746056331E8, + ), + -0.15052865500837787, + ), + ( + ( + -6906850.625560562, + 1.0153663948838013E8, + 2.4923185478305575E8, + ), + -0.3079300694558318, + ), + ( + ( + -7.108376621385525E7, + -2.029413580824217E8, + 2.5164602748045415E8, + ), + 0.03051312670440398, + ), + ( + ( + 1.0591429119126628E8, + -4.7911044364543396E8, + -2918719.2277242197, + ), + -0.11775123159138573, + ), + ( + ( + 4.04615501401398E7, + -3.074409286586152E8, + 5.089118769334092E7, + ), + 0.08763639340713025, + ), + ( + ( + -4.8645283544246924E8, + -3.922570151180015E8, + 2.3741632952563038E8, + ), + 0.08857245482456311, + ), + ( + ( + 2.861710031285905E8, + -1.8973201372718483E8, + -3.2653143323982143E8, + ), + -0.2378339698793312, + ), + ( + ( + 2.885407603819252E8, + -3.358708100884505E7, + -1.4480399660676318E8, + ), + -0.46661747461279457, + ), + ( + ( + 3.6548491156354237E8, + 7.995429702025633E7, + 2.509991661702412E8, + ), + 0.1671543972176835, + ), + ( + ( + 1.3298684552869435E8, + 3.6743804723880893E8, + 5.791092458225288E7, + ), + -0.2704070746642889, + ), + ( + ( + -1.3123184148036437E8, + -2.722300890805201E8, + 2.1601883778132245E7, + ), + 0.05049887915906969, + ), + ( + ( + -5.56047682304707E8, + 3.554803693060646E8, + 3.1647392358159083E8, + ), + -0.21178547899422662, + ), + ( + ( + 5.638216625134594E8, + -2.236907346192737E8, + -5.0562852022285646E8, + ), + 0.03351245780858128, + ), + ( + ( + -5.436956979127073E7, + -1.129261611506945E8, + -1.7909512156895646E8, + ), + 0.31670010349494726, + ), + ( + ( + 1.0915760091641709E8, + 1.932642099859593E7, + -3.405060533753616E8, + ), + -0.13987439655026918, + ), + ( + ( + -6.73911758014991E8, + -2.2147483413687566E8, + -4.531457195005102E7, + ), + 0.07824440437151846, + ), + ( + ( + -2.4827386778136212E8, + -2.6640208832089204E8, + -3.354675096522197E8, + ), + -0.2989735599541437, + ), + ]; + + for ((x, y, z), sample) in values { + assert_eq!(sampler.sample_flat_y(x, y, z), sample); + } + } + + #[test] + fn test_no_fade() { + let mut rand = Xoroshiro::from_seed(111); + assert_eq!(rand.next_i32(), -1467508761); + let sampler = PerlinNoiseSampler::new(&mut rand); + + let values = [ + ( + ( + -3.134738528791615E8, + 5.676610095659718E7, + 2.011711832498507E8, + -1369026.560586418, + 3.957311252810864E8, + ), + 23234.47859421248, + ), + ( + ( + 6.797037355570006E8, + 6.439373693833767E8, + -3.36218773041759E8, + -3.265494249695775E8, + 1.353820060118252E8, + ), + -0.016403984198221984, + ), + ( + ( + -3.204701624793043E8, + -4.612474746056331E8, + -6906850.625560562, + 1.0153663948838013E8, + 2.4923185478305575E8, + ), + 0.3444286491766397, + ), + ( + ( + -7.108376621385525E7, + -2.029413580824217E8, + 2.5164602748045415E8, + 1.0591429119126628E8, + -4.7911044364543396E8, + ), + 0.03051312670440398, + ), + ( + ( + -2918719.2277242197, + 4.04615501401398E7, + -3.074409286586152E8, + 5.089118769334092E7, + -4.8645283544246924E8, + ), + 0.3434020232968479, + ), + ( + ( + -3.922570151180015E8, + 2.3741632952563038E8, + 2.861710031285905E8, + -1.8973201372718483E8, + -3.2653143323982143E8, + ), + -0.07935517045771859, + ), + ( + ( + 2.885407603819252E8, + -3.358708100884505E7, + -1.4480399660676318E8, + 3.6548491156354237E8, + 7.995429702025633E7, + ), + -0.46661747461279457, + ), + ( + ( + 2.509991661702412E8, + 1.3298684552869435E8, + 3.6743804723880893E8, + 5.791092458225288E7, + -1.3123184148036437E8, + ), + 0.0723439870279631, + ), + ( + ( + -2.722300890805201E8, + 2.1601883778132245E7, + -5.56047682304707E8, + 3.554803693060646E8, + 3.1647392358159083E8, + ), + -0.656560662515624, + ), + ( + ( + 5.638216625134594E8, + -2.236907346192737E8, + -5.0562852022285646E8, + -5.436956979127073E7, + -1.129261611506945E8, + ), + 0.03351245780858128, + ), + ( + ( + -1.7909512156895646E8, + 1.0915760091641709E8, + 1.932642099859593E7, + -3.405060533753616E8, + -6.73911758014991E8, + ), + -0.2089142558681482, + ), + ( + ( + -2.2147483413687566E8, + -4.531457195005102E7, + -2.4827386778136212E8, + -2.6640208832089204E8, + -3.354675096522197E8, + ), + 0.38250837565598395, + ), + ( + ( + 3.618095500266467E8, + -1.785261966631494E8, + 8.855575989580283E7, + -1.3702508894700047E8, + -3.564818414428105E8, + ), + 0.00883370523171791, + ), + ( + ( + 3.585592594479808E7, + 1.8822208340571395E8, + -386327.524558296, + -2.613548000006699E8, + 1995562.4304017993, + ), + -0.27653878487738676, + ), + ( + ( + 3.0800276873619422E7, + 1.166750302259058E7, + 8.502636255675305E7, + 4.347409652503064E8, + 1.0678086363325526E8, + ), + -0.13800758751097497, + ), + ( + ( + -2.797805968820768E8, + 9.446376468140173E7, + 2.2821543438325477E8, + -4.8176550369786626E8, + 7.316871126959312E7, + ), + 0.05505478945301634, + ), + ( + ( + -2.236596113898912E7, + 1.5296478602495643E8, + 3.903966235164034E8, + 9.40479475527148E7, + 1.0948229366673347E8, + ), + 0.1158678618158655, + ), + ( + ( + 3.5342596632385695E8, + 3.1584773170834744E8, + -2.1860087172846535E8, + -1.8126626716239208E8, + -2.5263456116162892E7, + ), + -0.354953975313882, + ), + ( + ( + -1.2711958434031656E8, + -4.541988855460623E7, + -1.375878074907788E8, + 6.72693784001799E7, + 6815739.665531283, + ), + -0.23849179316215247, + ), + ( + ( + 1.2660906027019228E8, + -3.3769609799741164E7, + -3.4331505330046E8, + -6.663866659430536E7, + -1.6603843763414428E8, + ), + 0.07974650858448407, + ), + ]; + + for ((x, y, z, y_scale, y_max), sample) in values { + assert_eq!(sampler.sample_no_fade(x, y, z, y_scale, y_max), sample); + } + } +} diff --git a/pumpkin-world/src/world_gen/noise/simplex.rs b/pumpkin-world/src/world_gen/noise/simplex.rs new file mode 100644 index 000000000..8d6fba7fa --- /dev/null +++ b/pumpkin-world/src/world_gen/noise/simplex.rs @@ -0,0 +1,702 @@ +use num_traits::Pow; +use pumpkin_core::random::{legacy_rand::LegacyRand, RandomImpl}; + +use super::{dot, GRADIENTS}; + +pub struct SimplexNoiseSampler { + permutation: Box<[u8]>, + x_origin: f64, + y_origin: f64, + z_origin: f64, +} + +impl SimplexNoiseSampler { + const SQRT_3: f64 = 1.7320508075688772f64; + const SKEW_FACTOR_2D: f64 = 0.5f64 * (Self::SQRT_3 - 1f64); + const UNSKEW_FACTOR_2D: f64 = (3f64 - Self::SQRT_3) / 6f64; + + pub fn new(random: &mut impl RandomImpl) -> Self { + let x_origin = random.next_f64() * 256f64; + let y_origin = random.next_f64() * 256f64; + let z_origin = random.next_f64() * 256f64; + + let mut permutation = [0u8; 256]; + + permutation + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = i as u8); + + for i in 0..256 { + let j = random.next_bounded_i32(256 - i) as usize; + permutation.swap(i as usize, i as usize + j); + } + + Self { + permutation: Box::new(permutation), + x_origin, + y_origin, + z_origin, + } + } + + fn map(&self, input: i32) -> i32 { + self.permutation[(input & 0xFF) as usize] as i32 + } + + fn grad(gradient_index: usize, x: f64, y: f64, z: f64, distance: f64) -> f64 { + let d = distance - x * x - y * y - z * z; + if d < 0f64 { + 0f64 + } else { + let d = d * d; + d * d * dot(&GRADIENTS[gradient_index], x, y, z) + } + } + + pub fn sample_2d(&self, x: f64, y: f64) -> f64 { + let d = (x + y) * Self::SKEW_FACTOR_2D; + let i = (x + d).floor() as i32; + let j = (y + d).floor() as i32; + + let e = (i.wrapping_add(j)) as f64 * Self::UNSKEW_FACTOR_2D; + let f = i as f64 - e; + let g = j as f64 - e; + + let h = x - f; + let k = y - g; + + let (l, m) = if h > k { (1, 0) } else { (0, 1) }; + + let n = h - l as f64 + Self::UNSKEW_FACTOR_2D; + let o = k - m as f64 + Self::UNSKEW_FACTOR_2D; + let p = h - 1f64 + 2f64 * Self::UNSKEW_FACTOR_2D; + let q = k - 1f64 + 2f64 * Self::UNSKEW_FACTOR_2D; + + let r = i & 0xFF; + let s = j & 0xFF; + + let t = self.map(r.wrapping_add(self.map(s))) % 12; + let u = self.map(r.wrapping_add(l).wrapping_add(self.map(s.wrapping_add(m)))) % 12; + let v = self.map(r.wrapping_add(1).wrapping_add(self.map(s.wrapping_add(1)))) % 12; + + let w = Self::grad(t as usize, h, k, 0f64, 0.5f64); + let z = Self::grad(u as usize, n, o, 0f64, 0.5f64); + let aa = Self::grad(v as usize, p, q, 0f64, 0.5f64); + + 70f64 * (w + z + aa) + } + + pub fn sample_3d(&self, x: f64, y: f64, z: f64) -> f64 { + let e = (x + y + z) * 0.3333333333333333f64; + + let i = (x + e).floor() as i32; + let j = (y + e).floor() as i32; + let k = (z + e).floor() as i32; + + let g = (i.wrapping_add(j).wrapping_add(k)) as f64 * 0.16666666666666666f64; + let h = i as f64 - g; + let l = j as f64 - g; + let m = k as f64 - g; + + let n = x - h; + let o = y - l; + let p = z - m; + + let (q, r, s, t, u, v) = if n >= o { + if o >= p { + (1, 0, 0, 1, 1, 0) + } else if n >= p { + (1, 0, 0, 1, 0, 1) + } else { + (0, 0, 1, 1, 0, 1) + } + } else if o < p { + (0, 0, 1, 0, 1, 1) + } else if n < p { + (0, 1, 0, 0, 1, 1) + } else { + (0, 1, 0, 1, 1, 0) + }; + + let w = n - q as f64 + 0.16666666666666666f64; + let aa = o - r as f64 + 0.16666666666666666f64; + let ab = p - s as f64 + 0.16666666666666666f64; + + let ac = n - t as f64 + 0.3333333333333333f64; + let ad = o - u as f64 + 0.3333333333333333f64; + let ae = p - v as f64 + 0.3333333333333333f64; + + let af = n - 1f64 + 0.5f64; + let ag = o - 1f64 + 0.5f64; + let ah = p - 1f64 + 0.5f64; + + let ai = i & 0xFF; + let aj = j & 0xFF; + let ak = k & 0xFF; + + let al = self.map(ai.wrapping_add(self.map(aj.wrapping_add(self.map(ak))))) % 12; + let am = self.map( + ai.wrapping_add(q).wrapping_add( + self.map( + aj.wrapping_add(r) + .wrapping_add(self.map(ak.wrapping_add(s))), + ), + ), + ) % 12; + let an = self.map( + ai.wrapping_add(t).wrapping_add( + self.map( + aj.wrapping_add(u) + .wrapping_add(self.map(ak.wrapping_add(v))), + ), + ), + ) % 12; + let ao = self.map( + ai.wrapping_add(1).wrapping_add( + self.map( + aj.wrapping_add(1) + .wrapping_add(self.map(ak.wrapping_add(1))), + ), + ), + ) % 12; + + let ap = Self::grad(al as usize, n, o, p, 0.6f64); + let aq = Self::grad(am as usize, w, aa, ab, 0.6f64); + let ar = Self::grad(an as usize, ac, ad, ae, 0.6f64); + let az = Self::grad(ao as usize, af, ag, ah, 0.6f64); + + 32f64 * (ap + aq + ar + az) + } +} + +pub struct OctaveSimplexNoiseSampler { + octave_samplers: Vec>, + persistence: f64, + lacunarity: f64, +} + +impl OctaveSimplexNoiseSampler { + pub fn new(random: &mut impl RandomImpl, octaves: &[i32]) -> Self { + let mut octaves = Vec::from_iter(octaves); + octaves.sort(); + + let i = -**octaves.first().expect("Should have some octaves"); + let j = **octaves.last().expect("Should have some octaves"); + let k = i.wrapping_add(j).wrapping_add(1); + + let sampler = SimplexNoiseSampler::new(random); + let l = j; + let mut samplers: Vec> = Vec::with_capacity(k as usize); + for _ in 0..k { + samplers.push(None); + } + + for m in (j + 1)..k { + if m >= 0 && octaves.contains(&&(l - m)) { + let sampler = SimplexNoiseSampler::new(random); + samplers[m as usize] = Some(sampler); + } else { + random.skip(262); + } + } + + if j > 0 { + let sample = sampler.sample_3d(sampler.x_origin, sampler.y_origin, sampler.z_origin); + let n = (sample * 9.223372E18f32 as f64) as i64; + let mut random = LegacyRand::from_seed(n as u64); + + for o in (0..=(l - 1)).rev() { + if o < k && octaves.contains(&&(l - o)) { + let sampler = SimplexNoiseSampler::new(&mut random); + samplers[o as usize] = Some(sampler); + } else { + random.skip(262); + } + } + } + + if j >= 0 && j < k && octaves.contains(&&0) { + samplers[j as usize] = Some(sampler); + } + + Self { + octave_samplers: samplers, + persistence: 1f64 / (2f64.pow(k) - 1f64), + lacunarity: 2f64.pow(j), + } + } + + pub fn sample(&self, x: f64, y: f64, use_origin: bool) -> f64 { + let mut d = 0f64; + let mut e = self.lacunarity; + let mut f = self.persistence; + + for sampler in self.octave_samplers.iter() { + if let Some(sampler) = sampler { + d += sampler.sample_2d( + x * e + if use_origin { sampler.x_origin } else { 0f64 }, + y * e + if use_origin { sampler.y_origin } else { 0f64 }, + ) * f; + } + + e /= 2f64; + f *= 2f64; + } + + d + } +} + +#[cfg(test)] +mod octave_simplex_noise_sampler_test { + use pumpkin_core::random::{xoroshiro128::Xoroshiro, RandomImpl}; + + use crate::world_gen::noise::simplex::OctaveSimplexNoiseSampler; + + #[test] + fn test_new() { + let mut rand = Xoroshiro::from_seed(450); + assert_eq!(rand.next_i32(), 1394613419); + let sampler = OctaveSimplexNoiseSampler::new(&mut rand, &[-1, 1, 0]); + + assert_eq!(sampler.lacunarity, 2f64); + assert_eq!(sampler.persistence, 0.14285714285714285); + + let values = [ + (33.48154133535127, 200.15584029786743, 239.82697852863149), + (115.65071632913913, 5.88805286077266, 184.4887403898897), + (64.69791492580848, 19.256055216755044, 97.01795462351956), + ]; + + assert_eq!(values.len(), sampler.octave_samplers.len()); + for (sampler, (x, y, z)) in sampler.octave_samplers.iter().zip(values) { + match sampler { + Some(sampler) => { + assert_eq!(sampler.x_origin, x); + assert_eq!(sampler.y_origin, y); + assert_eq!(sampler.z_origin, z); + } + None => panic!(), + } + } + } + + #[test] + fn test_sample() { + let mut rand = Xoroshiro::from_seed(450); + assert_eq!(rand.next_i32(), 1394613419); + let sampler = OctaveSimplexNoiseSampler::new(&mut rand, &[-1, 1, 0]); + + let values_1 = [ + ( + (-1.3127900550351206E7, 792897.4979227383), + -0.4321152413690901, + ), + ( + (-1.6920637874404985E7, -2.7155569346339065E8), + -0.5262902093081003, + ), + ( + (4.3144247722741723E8, 5.681942883881191E8), + 0.11591369897395602, + ), + ( + (1.4302738270336467E8, -1.4548998886244193E8), + -0.3879951077548365, + ), + ( + (-3.9028350711219925E8, -5.213995559811158E7), + -0.7540785159288218, + ), + ( + (-1.3442750163759476E8, -6.725465365393716E8), + 0.31442035977402105, + ), + ( + (-1.1937282161424601E8, 3.2134650034986335E8), + 0.28218849676360336, + ), + ( + (-3.128475507865152E8, -3.014112871163455E8), + 0.593770404657594, + ), + ( + (1.2027011883589141E8, -5.045175636913682E8), + -0.2893240282016911, + ), + ( + (-9.065155753781198E7, 6106991.342893547), + -0.3402301205344082, + ), + ]; + + for ((x, y), sample) in values_1 { + assert_eq!(sampler.sample(x, y, false), sample); + } + + let values_2 = [ + ( + (-1.3127900550351206E7, 792897.4979227383), + 0.21834818545873672, + ), + ( + (-1.6920637874404985E7, -2.7155569346339065E8), + 0.025042742676442978, + ), + ( + (4.3144247722741723E8, 5.681942883881191E8), + 0.3738693783591451, + ), + ( + (1.4302738270336467E8, -1.4548998886244193E8), + -0.023113657524218345, + ), + ( + (-3.9028350711219925E8, -5.213995559811158E7), + 0.5195582376240916, + ), + ( + (-1.3442750163759476E8, -6.725465365393716E8), + 0.020366186088347903, + ), + ( + (-1.1937282161424601E8, 3.2134650034986335E8), + -0.10921072611129382, + ), + ( + (-3.128475507865152E8, -3.014112871163455E8), + 0.18066933648141983, + ), + ( + (1.2027011883589141E8, -5.045175636913682E8), + -0.36788084946294336, + ), + ( + (-9.065155753781198E7, 6106991.342893547), + -0.5677921377363926, + ), + ]; + + for ((x, y), sample) in values_2 { + assert_eq!(sampler.sample(x, y, true), sample); + } + } +} +#[cfg(test)] +mod simplex_noise_sampler_test { + use std::ops::Deref; + + use pumpkin_core::random::{xoroshiro128::Xoroshiro, RandomImpl}; + + use crate::world_gen::noise::simplex::SimplexNoiseSampler; + + #[test] + fn test_create() { + let mut rand = Xoroshiro::from_seed(111); + assert_eq!(rand.next_i32(), -1467508761); + let sampler = SimplexNoiseSampler::new(&mut rand); + assert_eq!(sampler.x_origin, 48.58072036717974f64); + assert_eq!(sampler.y_origin, 110.73235882678037f64); + assert_eq!(sampler.z_origin, 65.26438852860176f64); + + let permutation: [u8; 256] = [ + 159, 113, 41, 143, 203, 123, 95, 177, 25, 79, 229, 219, 194, 60, 130, 14, 83, 99, 24, + 202, 207, 232, 167, 152, 220, 201, 29, 235, 87, 147, 74, 160, 155, 97, 111, 31, 85, + 205, 115, 50, 13, 171, 77, 237, 149, 116, 209, 174, 169, 109, 221, 9, 166, 84, 54, 216, + 121, 106, 211, 16, 69, 244, 65, 192, 183, 146, 124, 37, 56, 45, 193, 158, 126, 217, 36, + 255, 162, 163, 230, 103, 63, 90, 191, 214, 20, 138, 32, 39, 238, 67, 64, 105, 250, 140, + 148, 114, 68, 75, 200, 161, 239, 125, 227, 199, 101, 61, 175, 107, 129, 240, 170, 51, + 139, 86, 186, 145, 212, 178, 30, 251, 89, 226, 120, 153, 47, 141, 233, 2, 179, 236, 1, + 19, 98, 21, 164, 108, 11, 23, 91, 204, 119, 88, 165, 195, 168, 26, 48, 206, 128, 6, 52, + 118, 110, 180, 197, 231, 117, 7, 3, 135, 224, 58, 82, 78, 4, 59, 222, 18, 72, 57, 150, + 43, 246, 100, 122, 112, 53, 133, 93, 17, 27, 210, 142, 234, 245, 80, 22, 46, 185, 172, + 71, 248, 33, 173, 76, 35, 40, 92, 228, 127, 254, 70, 42, 208, 73, 104, 187, 62, 154, + 243, 189, 241, 34, 66, 249, 94, 8, 12, 134, 132, 102, 242, 196, 218, 181, 28, 38, 15, + 151, 157, 247, 223, 198, 55, 188, 96, 0, 182, 49, 190, 156, 10, 215, 252, 131, 137, + 184, 176, 136, 81, 44, 213, 253, 144, 225, 5, + ]; + assert_eq!(sampler.permutation.deref(), permutation); + } + + #[test] + fn test_sample_2d() { + let data1 = [ + ((-50000, 0), -0.013008608535752102), + ((-49999, 1000), 0.0), + ((-49998, 2000), -0.03787856584046271), + ((-49997, 3000), 0.0), + ((-49996, 4000), 0.5015373706471664), + ((-49995, 5000), -0.032797908620906514), + ((-49994, 6000), -0.19158655563621785), + ((-49993, 7000), 0.49893473629544977), + ((-49992, 8000), 0.31585737840402556), + ((-49991, 9000), 0.43909577227435836), + ]; + + let data2 = [ + ( + (-3.134738528791615E8, 5.676610095659718E7), + 0.018940199193618792, + ), + ( + (-1369026.560586418, 3.957311252810864E8), + -0.1417598930091471, + ), + ( + (6.439373693833767E8, -3.36218773041759E8), + 0.07129176668335062, + ), + ( + (1.353820060118252E8, -3.204701624793043E8), + 0.330648835988156, + ), + ( + (-6906850.625560562, 1.0153663948838013E8), + 0.46826928755778685, + ), + ( + (-7.108376621385525E7, -2.029413580824217E8), + -0.515950097501492, + ), + ( + (1.0591429119126628E8, -4.7911044364543396E8), + -0.5467822192664874, + ), + ( + (4.04615501401398E7, -3.074409286586152E8), + 0.7470460844090322, + ), + ( + (-4.8645283544246924E8, -3.922570151180015E8), + 0.8521699147242563, + ), + ( + (2.861710031285905E8, -1.8973201372718483E8), + 0.1889297962671115, + ), + ( + (2.885407603819252E8, -3.358708100884505E7), + 0.24006029504945695, + ), + ( + (3.6548491156354237E8, 7.995429702025633E7), + -0.8114171447379924, + ), + ( + (1.3298684552869435E8, 3.6743804723880893E8), + 0.07042306408164949, + ), + ( + (-1.3123184148036437E8, -2.722300890805201E8), + 0.5093850689193259, + ), + ( + (-5.56047682304707E8, 3.554803693060646E8), + -0.6343788467687929, + ), + ( + (5.638216625134594E8, -2.236907346192737E8), + 0.5848746152449286, + ), + ( + (-5.436956979127073E7, -1.129261611506945E8), + -0.05456282199582522, + ), + ( + (1.0915760091641709E8, 1.932642099859593E7), + -0.273739377096594, + ), + ( + (-6.73911758014991E8, -2.2147483413687566E8), + 0.05464681163741797, + ), + ( + (-2.4827386778136212E8, -2.6640208832089204E8), + -0.0902449424742273, + ), + ]; + + let mut rand = Xoroshiro::from_seed(111); + assert_eq!(rand.next_i32(), -1467508761); + + let sampler = SimplexNoiseSampler::new(&mut rand); + for ((x, y), sample) in data1 { + assert_eq!(sampler.sample_2d(x as f64, y as f64), sample); + } + + for ((x, y), sample) in data2 { + assert_eq!(sampler.sample_2d(x, y), sample); + } + } + + #[test] + fn test_sample_3d() { + let data = [ + ( + ( + -3.134738528791615E8, + 5.676610095659718E7, + 2.011711832498507E8, + ), + -0.07626353895981935, + ), + ( + (-1369026.560586418, 3.957311252810864E8, 6.797037355570006E8), + 0.0, + ), + ( + ( + 6.439373693833767E8, + -3.36218773041759E8, + -3.265494249695775E8, + ), + -0.5919400355725402, + ), + ( + ( + 1.353820060118252E8, + -3.204701624793043E8, + -4.612474746056331E8, + ), + -0.5220477236433517, + ), + ( + ( + -6906850.625560562, + 1.0153663948838013E8, + 2.4923185478305575E8, + ), + -0.39146687767898636, + ), + ( + ( + -7.108376621385525E7, + -2.029413580824217E8, + 2.5164602748045415E8, + ), + -0.629386846329711, + ), + ( + ( + 1.0591429119126628E8, + -4.7911044364543396E8, + -2918719.2277242197, + ), + 0.5427502531663232, + ), + ( + ( + 4.04615501401398E7, + -3.074409286586152E8, + 5.089118769334092E7, + ), + -0.4273080639878097, + ), + ( + ( + -4.8645283544246924E8, + -3.922570151180015E8, + 2.3741632952563038E8, + ), + 0.32129944093252394, + ), + ( + ( + 2.861710031285905E8, + -1.8973201372718483E8, + -3.2653143323982143E8, + ), + 0.35839032946039706, + ), + ( + ( + 2.885407603819252E8, + -3.358708100884505E7, + -1.4480399660676318E8, + ), + -0.02451312935907038, + ), + ( + ( + 3.6548491156354237E8, + 7.995429702025633E7, + 2.509991661702412E8, + ), + -0.36830526266318003, + ), + ( + ( + 1.3298684552869435E8, + 3.6743804723880893E8, + 5.791092458225288E7, + ), + -0.023683302916542803, + ), + ( + ( + -1.3123184148036437E8, + -2.722300890805201E8, + 2.1601883778132245E7, + ), + -0.261629562325043, + ), + ( + ( + -5.56047682304707E8, + 3.554803693060646E8, + 3.1647392358159083E8, + ), + -0.4959372930161496, + ), + ( + ( + 5.638216625134594E8, + -2.236907346192737E8, + -5.0562852022285646E8, + ), + -0.06079315675880484, + ), + ( + ( + -5.436956979127073E7, + -1.129261611506945E8, + -1.7909512156895646E8, + ), + -0.37726907424345196, + ), + ( + ( + 1.0915760091641709E8, + 1.932642099859593E7, + -3.405060533753616E8, + ), + 0.37747828159811136, + ), + ( + ( + -6.73911758014991E8, + -2.2147483413687566E8, + -4.531457195005102E7, + ), + -0.32929020207000603, + ), + ( + ( + -2.4827386778136212E8, + -2.6640208832089204E8, + -3.354675096522197E8, + ), + -0.3046390200444667, + ), + ]; + + let mut rand = Xoroshiro::from_seed(111); + assert_eq!(rand.next_i32(), -1467508761); + + let sampler = SimplexNoiseSampler::new(&mut rand); + for ((x, y, z), sample) in data { + assert_eq!(sampler.sample_3d(x, y, z), sample); + } + } +} diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 29fa203dc..3320c4d6c 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -10,16 +10,16 @@ plugins = ["pumpkin-plugin/plugins"] [dependencies] # pumpkin -pumpkin-core = { path = "../pumpkin-core"} +pumpkin-core = { path = "../pumpkin-core" } pumpkin-config = { path = "../pumpkin-config" } -pumpkin-plugin = { path = "../pumpkin-plugin"} -pumpkin-inventory = { path = "../pumpkin-inventory"} -pumpkin-world = { path = "../pumpkin-world"} -pumpkin-entity = { path = "../pumpkin-entity"} -pumpkin-protocol = { path = "../pumpkin-protocol"} -pumpkin-registry = { path = "../pumpkin-registry"} +pumpkin-plugin = { path = "../pumpkin-plugin" } +pumpkin-inventory = { path = "../pumpkin-inventory" } +pumpkin-world = { path = "../pumpkin-world" } +pumpkin-entity = { path = "../pumpkin-entity" } +pumpkin-protocol = { path = "../pumpkin-protocol" } +pumpkin-registry = { path = "../pumpkin-registry" } -itertools = "0.13.0" +itertools.workspace = true # config serde.workspace = true @@ -40,7 +40,12 @@ rsa = "0.9.6" rsa-der = "0.3.0" # authentication -reqwest = { version = "0.12.7", default-features= false, features = ["json", "rustls-tls", "http2", "macos-system-configuration"]} +reqwest = { version = "0.12.7", default-features = false, features = [ + "http2", + "json", + "macos-system-configuration", + "rustls-tls", +] } sha1 = "0.10.6" digest = "=0.11.0-pre.9" @@ -53,15 +58,17 @@ thiserror = "1.0" # icon loading base64 = "0.22.1" -image = { version = "0.25", default-features = false, features = ["png"]} +png = "0.17.14" # logging -simple_logger = "5.0.0" +simple_logger = { version = "5.0.0", features = ["threads"] } log.workspace = true # networking -mio = { version = "1.0.2", features = ["os-poll", "net"]} +mio = { version = "1.0.2", features = ["net", "os-poll"] } +parking_lot.workspace = true +crossbeam.workspace = true uuid.workspace = true tokio.workspace = true rayon.workspace = true diff --git a/pumpkin/src/client/authentication.rs b/pumpkin/src/client/authentication.rs index 60d6c5523..195dfa2ae 100644 --- a/pumpkin/src/client/authentication.rs +++ b/pumpkin/src/client/authentication.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, net::IpAddr, sync::Arc}; use base64::{engine::general_purpose, Engine}; -use num_bigint::BigInt; use pumpkin_config::{auth::TextureConfig, ADVANCED_CONFIG}; use pumpkin_core::ProfileAction; use pumpkin_protocol::Property; @@ -39,6 +38,19 @@ pub struct GameProfile { pub profile_actions: Option>, } +/// Sends a GET request to Mojang's authentication servers to verify a client's Minecraft account. +/// +/// **Purpose:** +/// +/// This function is used to ensure that a client connecting to the server has a valid, premium Minecraft account. It's a crucial step in preventing unauthorized access and maintaining server security. +/// +/// **How it Works:** +/// +/// 1. A client with a premium account sends a login request to the Mojang session server. +/// 2. Mojang's servers verify the client's credentials and add the player to the their Servers +/// 3. Now our server will send a Request to the Session servers and check if the Player has joined the Session Server . +/// +/// **Note:** This process helps prevent unauthorized access to the server and ensures that only legitimate Minecraft accounts can connect. pub async fn authenticate( username: &str, server_hash: &str, @@ -48,9 +60,18 @@ pub async fn authenticate( assert!(ADVANCED_CONFIG.authentication.enabled); assert!(server.auth_client.is_some()); let address = if ADVANCED_CONFIG.authentication.prevent_proxy_connections { - format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}") + ADVANCED_CONFIG + .authentication + .auth_url + .replace("{username}", username) + .replace("{server_hash}", server_hash) + .replace("{}", &ip.to_string()) } else { - format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}") + ADVANCED_CONFIG + .authentication + .auth_url + .replace("{username}", username) + .replace("{server_hash}", server_hash) }; let auth_client = server .auth_client @@ -65,35 +86,44 @@ pub async fn authenticate( match response.status() { StatusCode::OK => {} StatusCode::NO_CONTENT => Err(AuthError::UnverifiedUsername)?, - other => Err(AuthError::UnknownStatusCode(other.as_str().to_string()))?, + other => Err(AuthError::UnknownStatusCode(other))?, } let profile: GameProfile = response.json().await.map_err(|_| AuthError::FailedParse)?; Ok(profile) } -pub fn unpack_textures(property: Property, config: &TextureConfig) { - // TODO: no unwrap - let from64 = general_purpose::STANDARD.decode(property.value).unwrap(); - let textures: ProfileTextures = serde_json::from_slice(&from64).unwrap(); +pub fn unpack_textures(property: &Property, config: &TextureConfig) -> Result<(), TextureError> { + let from64 = general_purpose::STANDARD + .decode(&property.value) + .map_err(|e| TextureError::DecodeError(e.to_string()))?; + let textures: ProfileTextures = + serde_json::from_slice(&from64).map_err(|e| TextureError::JSONError(e.to_string()))?; for texture in textures.textures { - is_texture_url_valid(Url::parse(&texture.1.url).unwrap(), config); + let url = + Url::parse(&texture.1.url).map_err(|e| TextureError::InvalidURL(e.to_string()))?; + is_texture_url_valid(url, config)? } + Ok(()) } -pub fn auth_digest(bytes: &[u8]) -> String { - BigInt::from_signed_bytes_be(bytes).to_str_radix(16) -} - -pub fn is_texture_url_valid(url: Url, config: &TextureConfig) -> bool { +pub fn is_texture_url_valid(url: Url, config: &TextureConfig) -> Result<(), TextureError> { let scheme = url.scheme(); - if !config.allowed_url_schemes.contains(&scheme.to_string()) { - return false; + if !config + .allowed_url_schemes + .iter() + .any(|allowed_scheme| scheme.ends_with(allowed_scheme)) + { + return Err(TextureError::DisallowedUrlScheme(scheme.to_string())); } let domain = url.domain().unwrap_or(""); - if !config.allowed_url_domains.contains(&domain.to_string()) { - return false; + if !config + .allowed_url_domains + .iter() + .any(|allowed_domain| domain.ends_with(allowed_domain)) + { + return Err(TextureError::DisallowedUrlDomain(domain.to_string())); } - true + Ok(()) } #[derive(Error, Debug)] @@ -107,5 +137,19 @@ pub enum AuthError { #[error("Failed to parse JSON into Game Profile")] FailedParse, #[error("Unknown Status Code")] - UnknownStatusCode(String), + UnknownStatusCode(StatusCode), +} + +#[derive(Error, Debug)] +pub enum TextureError { + #[error("Invalid URL")] + InvalidURL(String), + #[error("Invalid URL scheme for player texture: {0}")] + DisallowedUrlScheme(String), + #[error("Invalid URL domain for player texture: {0}")] + DisallowedUrlDomain(String), + #[error("Failed to decode base64 player texture: {0}")] + DecodeError(String), + #[error("Failed to parse JSON from player texture: {0}")] + JSONError(String), } diff --git a/pumpkin/src/client/client_packet.rs b/pumpkin/src/client/client_packet.rs index 38c734b40..bcdb541dd 100644 --- a/pumpkin/src/client/client_packet.rs +++ b/pumpkin/src/client/client_packet.rs @@ -6,8 +6,8 @@ use pumpkin_core::text::TextComponent; use pumpkin_protocol::{ client::{ config::{CConfigAddResourcePack, CFinishConfig, CKnownPacks, CRegistryData}, - login::{CEncryptionRequest, CLoginSuccess, CSetCompression}, - status::{CPingResponse, CStatusResponse}, + login::{CLoginSuccess, CSetCompression}, + status::CPingResponse, }, server::{ config::{SAcknowledgeFinishConfig, SClientInformationConfig, SKnownPacks, SPluginMessage}, @@ -17,8 +17,7 @@ use pumpkin_protocol::{ }, ConnectionState, KnownPack, CURRENT_MC_PROTOCOL, }; -use rsa::Pkcs1v15Encrypt; -use sha1::{Digest, Sha1}; +use uuid::Uuid; use crate::{ client::authentication::{self, GameProfile}, @@ -27,22 +26,22 @@ use crate::{ server::{Server, CURRENT_MC_VERSION}, }; -use super::{ - authentication::{auth_digest, unpack_textures}, - Client, EncryptionError, PlayerConfig, -}; +use super::{authentication::unpack_textures, Client, PlayerConfig}; /// Processes incoming Packets from the Client to the Server /// Implements the `Client` Packets /// NEVER TRUST THE CLIENT. HANDLE EVERY ERROR, UNWRAP/EXPECT /// TODO: REMOVE ALL UNWRAPS impl Client { - pub fn handle_handshake(&mut self, _server: &Arc, handshake: SHandShake) { + pub fn handle_handshake(&self, _server: &Arc, handshake: SHandShake) { dbg!("handshake"); - self.protocol_version = handshake.protocol_version.0; - self.connection_state = handshake.next_state; - if self.connection_state != ConnectionState::Status { - let protocol = self.protocol_version; + let version = handshake.protocol_version.0; + self.protocol_version + .store(version, std::sync::atomic::Ordering::Relaxed); + + self.connection_state.store(handshake.next_state); + if self.connection_state.load() != ConnectionState::Status { + let protocol = version; match protocol.cmp(&(CURRENT_MC_PROTOCOL as i32)) { std::cmp::Ordering::Less => { self.kick(&format!("Client outdated ({protocol}), Server uses Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL}")); @@ -55,11 +54,11 @@ impl Client { } } - pub fn handle_status_request(&mut self, server: &Arc, _status_request: SStatusRequest) { - self.send_packet(&CStatusResponse::new(&server.status_response_json)); + pub fn handle_status_request(&self, server: &Arc, _status_request: SStatusRequest) { + self.send_packet(&server.get_status()); } - pub fn handle_ping_request(&mut self, _server: &Arc, ping_request: SStatusPingRequest) { + pub fn handle_ping_request(&self, _server: &Arc, ping_request: SStatusPingRequest) { dbg!("ping"); self.send_packet(&CPingResponse::new(ping_request.payload)); self.close(); @@ -72,7 +71,7 @@ impl Client { .all(|c| c > 32_u8 as char && c < 127_u8 as char) } - pub fn handle_login_start(&mut self, server: &Arc, login_start: SLoginStart) { + pub fn handle_login_start(&self, server: &Arc, login_start: SLoginStart) { log::debug!("login start, State {:?}", self.connection_state); if !Self::is_valid_player_name(&login_start.name) { @@ -81,7 +80,8 @@ impl Client { } // default game profile, when no online mode // TODO: make offline uuid - self.gameprofile = Some(GameProfile { + let mut gameprofile = self.gameprofile.lock(); + *gameprofile = Some(GameProfile { id: login_start.uuid, name: login_start.name, properties: vec![], @@ -97,87 +97,74 @@ impl Client { // TODO: check config for encryption let verify_token: [u8; 4] = rand::random(); - let public_key_der = &server.public_key_der; - let packet = CEncryptionRequest::new( - "", - public_key_der, - &verify_token, - BASIC_CONFIG.online_mode, // TODO - ); - self.send_packet(&packet); + self.send_packet(&server.encryption_request(&verify_token, BASIC_CONFIG.online_mode)); } pub async fn handle_encryption_response( - &mut self, + &self, server: &Arc, encryption_response: SEncryptionResponse, ) { - let shared_secret = server - .private_key - .decrypt(Pkcs1v15Encrypt, &encryption_response.shared_secret) - .map_err(|_| EncryptionError::FailedDecrypt) - .unwrap(); - self.enable_encryption(&shared_secret) + let shared_secret = server.decrypt(&encryption_response.shared_secret).unwrap(); + + self.set_encryption(Some(&shared_secret)) .unwrap_or_else(|e| self.kick(&e.to_string())); + let mut gameprofile = self.gameprofile.lock(); + if BASIC_CONFIG.online_mode { - let hash = Sha1::new() - .chain_update(&shared_secret) - .chain_update(&server.public_key_der) - .finalize(); - let hash = auth_digest(&hash); - let ip = self.address.ip(); + let hash = server.digest_secret(&shared_secret); + let ip = self.address.lock().ip(); + match authentication::authenticate( - &self.gameprofile.as_ref().unwrap().name, + &gameprofile.as_ref().unwrap().name, &hash, &ip, server, ) .await { - Ok(p) => { + Ok(profile) => { // Check if player should join - if let Some(p) = &p.profile_actions { + if let Some(actions) = &profile.profile_actions { if !ADVANCED_CONFIG .authentication .player_profile .allow_banned_players { - if !p.is_empty() { + if !actions.is_empty() { self.kick("Your account can't join"); } } else { - for allowed in ADVANCED_CONFIG + for allowed in &ADVANCED_CONFIG .authentication .player_profile .allowed_actions - .clone() { - if !p.contains(&allowed) { + if !actions.contains(allowed) { self.kick("Your account can't join"); } } } } - self.gameprofile = Some(p); + *gameprofile = Some(profile); } Err(e) => self.kick(&e.to_string()), } } - for ele in self.gameprofile.as_ref().unwrap().properties.clone() { - // todo, use this - unpack_textures(ele, &ADVANCED_CONFIG.authentication.textures); + for property in &gameprofile.as_ref().unwrap().properties { + unpack_textures(property, &ADVANCED_CONFIG.authentication.textures) + .unwrap_or_else(|e| self.kick(&e.to_string())); } // enable compression if ADVANCED_CONFIG.packet_compression.enabled { - let threshold = ADVANCED_CONFIG.packet_compression.compression_threshold; - let level = ADVANCED_CONFIG.packet_compression.compression_level; - self.send_packet(&CSetCompression::new(threshold.into())); - self.set_compression(Some((threshold, level))); + let compression = ADVANCED_CONFIG.packet_compression.compression_info.clone(); + self.send_packet(&CSetCompression::new(compression.threshold.into())); + self.set_compression(Some(compression)); } - if let Some(profile) = self.gameprofile.as_ref().cloned() { + if let Some(profile) = gameprofile.as_ref() { let packet = CLoginSuccess::new(&profile.id, &profile.name, &profile.properties, false); self.send_packet(&packet); } else { @@ -186,37 +173,38 @@ impl Client { } pub fn handle_plugin_response( - &mut self, + &self, _server: &Arc, _plugin_response: SLoginPluginResponse, ) { } pub fn handle_login_acknowledged( - &mut self, + &self, server: &Arc, _login_acknowledged: SLoginAcknowledged, ) { - self.connection_state = ConnectionState::Config; - server.send_brand(self); + self.connection_state.store(ConnectionState::Config); + self.send_packet(&server.get_branding()); let resource_config = &ADVANCED_CONFIG.resource_pack; if resource_config.enabled { - let prompt_message = if resource_config.prompt_message.is_empty() { - None - } else { - Some(TextComponent::text(&resource_config.prompt_message)) - }; - self.send_packet(&CConfigAddResourcePack::new( - pumpkin_protocol::uuid::UUID(uuid::Uuid::new_v3( + let resource_pack = CConfigAddResourcePack::new( + Uuid::new_v3( &uuid::Uuid::NAMESPACE_DNS, resource_config.resource_pack_url.as_bytes(), - )), + ), &resource_config.resource_pack_url, &resource_config.resource_pack_sha1, resource_config.force, - prompt_message, - )); + if !resource_config.prompt_message.is_empty() { + Some(TextComponent::text(&resource_config.prompt_message)) + } else { + None + }, + ); + + self.send_packet(&resource_pack); } // known data packs @@ -225,39 +213,46 @@ impl Client { id: "core", version: "1.21", }])); - dbg!("login achnowlaged"); + dbg!("login acknowledged"); } pub fn handle_client_information_config( - &mut self, + &self, _server: &Arc, client_information: SClientInformationConfig, ) { dbg!("got client settings"); - self.config = Some(PlayerConfig { - locale: client_information.locale, - view_distance: client_information.view_distance, - chat_mode: ChatMode::from_i32(client_information.chat_mode.into()).unwrap(), - chat_colors: client_information.chat_colors, - skin_parts: client_information.skin_parts, - main_hand: Hand::from_i32(client_information.main_hand.into()).unwrap(), - text_filtering: client_information.text_filtering, - server_listing: client_information.server_listing, - }); + if let (Some(main_hand), Some(chat_mode)) = ( + Hand::from_i32(client_information.main_hand.into()), + ChatMode::from_i32(client_information.chat_mode.into()), + ) { + *self.config.lock() = Some(PlayerConfig { + locale: client_information.locale, + view_distance: client_information.view_distance, + chat_mode, + chat_colors: client_information.chat_colors, + skin_parts: client_information.skin_parts, + main_hand, + text_filtering: client_information.text_filtering, + server_listing: client_information.server_listing, + }); + } else { + self.kick("Invalid hand or chat type") + } } - pub fn handle_plugin_message(&mut self, _server: &Arc, plugin_message: SPluginMessage) { + pub fn handle_plugin_message(&self, _server: &Arc, plugin_message: SPluginMessage) { if plugin_message.channel.starts_with("minecraft:brand") || plugin_message.channel.starts_with("MC|Brand") { dbg!("got a client brand"); match String::from_utf8(plugin_message.data) { - Ok(brand) => self.brand = Some(brand), + Ok(brand) => *self.brand.lock() = Some(brand), Err(e) => self.kick(&e.to_string()), } } } - pub fn handle_known_packs(&mut self, server: &Arc, _config_acknowledged: SKnownPacks) { + pub fn handle_known_packs(&self, server: &Arc, _config_acknowledged: SKnownPacks) { for registry in &server.cached_registry { self.send_packet(&CRegistryData::new( ®istry.registry_id, @@ -271,12 +266,13 @@ impl Client { } pub async fn handle_config_acknowledged( - &mut self, + &self, _server: &Arc, _config_acknowledged: SAcknowledgeFinishConfig, ) { dbg!("config acknowledged"); - self.connection_state = ConnectionState::Play; - self.make_player = true; + self.connection_state.store(ConnectionState::Play); + self.make_player + .store(true, std::sync::atomic::Ordering::Relaxed); } } diff --git a/pumpkin/src/client/container.rs b/pumpkin/src/client/container.rs index 68b4391b9..bd1cf5db0 100644 --- a/pumpkin/src/client/container.rs +++ b/pumpkin/src/client/container.rs @@ -1,6 +1,7 @@ use crate::entity::player::Player; use crate::server::Server; use itertools::Itertools; +use parking_lot::Mutex; use pumpkin_core::text::TextComponent; use pumpkin_core::GameMode; use pumpkin_inventory::container_click::{ @@ -16,16 +17,17 @@ use pumpkin_protocol::client::play::{ use pumpkin_protocol::server::play::SClickContainer; use pumpkin_protocol::slot::Slot; use pumpkin_world::item::ItemStack; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; impl Player { - pub fn open_container(&mut self, server: &Arc, minecraft_menu_id: &str) { - self.inventory.state_id = 0; - let total_opened_containers = self.inventory.total_opened_containers; + pub fn open_container(&self, server: &Arc, minecraft_menu_id: &str) { + let inventory = self.inventory.lock(); + inventory + .state_id + .store(0, std::sync::atomic::Ordering::Relaxed); + let total_opened_containers = inventory.total_opened_containers; let container = self.get_open_container(server); - let mut container = container - .as_ref() - .map(|container| container.lock().unwrap()); + let mut container = container.as_ref().map(|container| container.lock()); let menu_protocol_id = (*pumpkin_world::global_registry::REGISTRY .get("minecraft:menu") .unwrap() @@ -38,7 +40,7 @@ impl Player { let window_title = container .as_ref() .map(|container| container.window_name()) - .unwrap_or(self.inventory.window_name()); + .unwrap_or_else(|| inventory.window_name()); let title = TextComponent::text(window_title); self.client.send_packet(&COpenScreen::new( @@ -46,12 +48,15 @@ impl Player { menu_protocol_id, title, )); + drop(inventory); self.set_container_content(container.as_deref_mut()); } - pub fn set_container_content(&mut self, container: Option<&mut Box>) { - let total_opened_containers = self.inventory.total_opened_containers; - let container = OptionallyCombinedContainer::new(&mut self.inventory, container); + pub fn set_container_content(&self, container: Option<&mut Box>) { + let mut inventory = self.inventory.lock(); + + let total_opened_containers = inventory.total_opened_containers; + let container = OptionallyCombinedContainer::new(&mut inventory, container); let slots = container .all_slots_ref() @@ -59,17 +64,19 @@ impl Player { .map(Slot::from) .collect_vec(); - let carried_item = { - if let Some(item) = self.carried_item.as_ref() { - item.into() - } else { - Slot::empty() - } - }; - self.inventory.state_id += 1; + let carried_item = self + .carried_item + .load() + .as_ref() + .map_or_else(Slot::empty, |item| item.into()); + + // Gets the previous value + let i = inventory + .state_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); let packet = CSetContainerContent::new( total_opened_containers, - (self.inventory.state_id as i32).into(), + ((i + 1) as i32).into(), &slots, &carried_item, ); @@ -77,11 +84,11 @@ impl Player { } /// The official Minecraft client is weird, and will always just close *any* window that is opened when this gets sent - pub fn close_container(&mut self) { - self.inventory.total_opened_containers += 1; - self.client.send_packet(&CCloseContainer::new( - self.inventory.total_opened_containers, - )) + pub fn close_container(&self) { + let mut inventory = self.inventory.lock(); + inventory.total_opened_containers += 1; + self.client + .send_packet(&CCloseContainer::new(inventory.total_opened_containers)) } pub fn set_container_property( @@ -90,24 +97,26 @@ impl Player { ) { let (id, value) = window_property.into_tuple(); self.client.send_packet(&CSetContainerProperty::new( - self.inventory.total_opened_containers, + self.inventory.lock().total_opened_containers, id, value, )); } pub async fn handle_click_container( - &mut self, + &self, server: &Arc, packet: SClickContainer, ) -> Result<(), InventoryError> { let opened_container = self.get_open_container(server); - let mut opened_container = opened_container - .as_ref() - .map(|container| container.lock().unwrap()); + let mut opened_container = opened_container.as_ref().map(|container| container.lock()); let drag_handler = &server.drag_handler; - let state_id = self.inventory.state_id; + let state_id = self + .inventory + .lock() + .state_id + .load(std::sync::atomic::Ordering::Relaxed); // This is just checking for regular desync, client hasn't done anything malicious if state_id != packet.state_id.0 as u32 { self.set_container_content(opened_container.as_deref_mut()); @@ -115,7 +124,7 @@ impl Player { } if opened_container.is_some() { - if packet.window_id != self.inventory.total_opened_containers { + if packet.window_id != self.inventory.lock().total_opened_containers { return Err(InventoryError::ClosedContainerInteract(self.entity_id())); } } else if packet.window_id != 0 { @@ -177,10 +186,9 @@ impl Player { drop(opened_container); self.send_whole_container_change(server).await?; } else if let container_click::Slot::Normal(slot_index) = click.slot { - let combined_container = OptionallyCombinedContainer::new( - &mut self.inventory, - Some(&mut opened_container), - ); + let mut inventory = self.inventory.lock(); + let combined_container = + OptionallyCombinedContainer::new(&mut inventory, Some(&mut opened_container)); if let Some(slot) = combined_container.get_slot_excluding_inventory(slot_index) { let slot = Slot::from(slot); drop(opened_container); @@ -193,27 +201,32 @@ impl Player { } fn mouse_click( - &mut self, + &self, opened_container: Option<&mut Box>, mouse_click: MouseClick, slot: container_click::Slot, ) -> Result<(), InventoryError> { - let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + let mut inventory = self.inventory.lock(); + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); match slot { container_click::Slot::Normal(slot) => { - container.handle_item_change(&mut self.carried_item, slot, mouse_click) + let mut carried_item = self.carried_item.load(); + let res = container.handle_item_change(&mut carried_item, slot, mouse_click); + self.carried_item.store(carried_item); + res } container_click::Slot::OutsideInventory => Ok(()), } } fn shift_mouse_click( - &mut self, + &self, opened_container: Option<&mut Box>, slot: container_click::Slot, ) -> Result<(), InventoryError> { - let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + let mut inventory = self.inventory.lock(); + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); match slot { container_click::Slot::Normal(slot) => { @@ -255,7 +268,7 @@ impl Player { } fn number_button_pressed( - &mut self, + &self, opened_container: Option<&mut Box>, key_click: KeyClick, slot: usize, @@ -264,35 +277,38 @@ impl Player { KeyClick::Slot(slot) => slot, KeyClick::Offhand => 45, }; - let mut changing_item_slot = self.inventory.get_slot(changing_slot as usize)?.to_owned(); - let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + let mut inventory = self.inventory.lock(); + let mut changing_item_slot = inventory.get_slot(changing_slot as usize)?.to_owned(); + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); container.handle_item_change(&mut changing_item_slot, slot, MouseClick::Left)?; - *self.inventory.get_slot(changing_slot as usize)? = changing_item_slot; + *inventory.get_slot(changing_slot as usize)? = changing_item_slot; Ok(()) } fn creative_pick_item( - &mut self, + &self, opened_container: Option<&mut Box>, slot: usize, ) -> Result<(), InventoryError> { - if self.gamemode != GameMode::Creative { + if self.gamemode.load() != GameMode::Creative { return Err(InventoryError::PermissionError); } - let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + let mut inventory = self.inventory.lock(); + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); if let Some(Some(item)) = container.all_slots().get_mut(slot) { - self.carried_item = Some(item.to_owned()) + self.carried_item.store(Some(item.to_owned())); } Ok(()) } fn double_click( - &mut self, + &self, opened_container: Option<&mut Box>, slot: usize, ) -> Result<(), InventoryError> { - let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container); + let mut inventory = self.inventory.lock(); + let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container); let mut slots = container.all_slots(); let Some(item) = slots.get_mut(slot) else { @@ -320,12 +336,12 @@ impl Player { } } } - self.carried_item = Some(carried_item); + self.carried_item.store(Some(carried_item)); Ok(()) } fn mouse_drag( - &mut self, + &self, drag_handler: &DragHandler, opened_container: Option<&mut Box>, mouse_drag_state: MouseDragState, @@ -337,36 +353,35 @@ impl Player { .unwrap_or(player_id as u64); match mouse_drag_state { MouseDragState::Start(drag_type) => { - if drag_type == MouseDragType::Middle && self.gamemode != GameMode::Creative { + if drag_type == MouseDragType::Middle && self.gamemode.load() != GameMode::Creative + { Err(InventoryError::PermissionError)? } drag_handler.new_drag(container_id, player_id, drag_type) } MouseDragState::AddSlot(slot) => drag_handler.add_slot(container_id, player_id, slot), MouseDragState::End => { + let mut inventory = self.inventory.lock(); let mut container = - OptionallyCombinedContainer::new(&mut self.inventory, opened_container); - drag_handler.apply_drag( - &mut self.carried_item, + OptionallyCombinedContainer::new(&mut inventory, opened_container); + let mut carried_item = self.carried_item.load(); + let res = drag_handler.apply_drag( + &mut carried_item, &mut container, &container_id, player_id, - ) + ); + self.carried_item.store(carried_item); + res } } } - async fn get_current_players_in_container( - &mut self, - server: &Server, - ) -> Vec>> { + async fn get_current_players_in_container(&self, server: &Server) -> Vec> { let player_ids = { - let open_containers = server - .open_containers - .read() - .expect("open_containers is poisoned"); + let open_containers = server.open_containers.read(); open_containers - .get(&self.open_container.unwrap()) + .get(&self.open_container.load().unwrap()) .unwrap() .all_player_ids() .into_iter() @@ -378,13 +393,16 @@ impl Player { // TODO: Figure out better way to get only the players from player_ids // Also refactor out a better method to get individual advanced state ids - let world = self.entity.world.lock().await; - let players = world + let players = self + .living_entity + .entity + .world .current_players + .lock() .iter() .filter_map(|(token, player)| { if *token != player_token { - let entity_id = player.lock().unwrap().entity_id(); + let entity_id = player.entity_id(); if player_ids.contains(&entity_id) { Some(player.clone()) } else { @@ -399,19 +417,22 @@ impl Player { } async fn send_container_changes( - &mut self, + &self, server: &Server, slot_index: usize, slot: Slot, ) -> Result<(), InventoryError> { for player in self.get_current_players_in_container(server).await { - let mut player = player.lock().unwrap(); - let total_opened_containers = player.inventory.total_opened_containers; + let inventory = player.inventory.lock(); + let total_opened_containers = inventory.total_opened_containers; - player.inventory.state_id += 1; + // Returns previous value + let i = inventory + .state_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); let packet = CSetContainerSlot::new( total_opened_containers as i8, - player.inventory.state_id as i32, + (i + 1) as i32, slot_index, &slot, ); @@ -420,23 +441,20 @@ impl Player { Ok(()) } - async fn send_whole_container_change(&mut self, server: &Server) -> Result<(), InventoryError> { + async fn send_whole_container_change(&self, server: &Server) -> Result<(), InventoryError> { let players = self.get_current_players_in_container(server).await; for player in players { - let mut player = player.lock().unwrap(); let container = player.get_open_container(server); - let mut container = container.as_ref().map(|v| v.lock().unwrap()); + let mut container = container.as_ref().map(|v| v.lock()); player.set_container_content(container.as_deref_mut()); } Ok(()) } pub fn get_open_container(&self, server: &Server) -> Option>>> { - if let Some(id) = self.open_container { - server.try_get_container(self.entity_id(), id) - } else { - None - } + self.open_container + .load() + .map_or_else(|| None, |id| server.try_get_container(self.entity_id(), id)) } } diff --git a/pumpkin/src/client/mod.rs b/pumpkin/src/client/mod.rs index 7a7d9ba05..2fcd008a9 100644 --- a/pumpkin/src/client/mod.rs +++ b/pumpkin/src/client/mod.rs @@ -1,7 +1,10 @@ use std::{ io::{self, Write}, net::SocketAddr, - sync::Arc, + sync::{ + atomic::{AtomicBool, AtomicI32}, + Arc, + }, }; use crate::{ @@ -10,7 +13,10 @@ use crate::{ }; use authentication::GameProfile; +use crossbeam::atomic::AtomicCell; use mio::{event::Event, net::TcpStream, Token}; +use parking_lot::Mutex; +use pumpkin_config::compression::CompressionInfo; use pumpkin_core::text::TextComponent; use pumpkin_protocol::{ bytebuf::{packet_id::Packet, DeserializerError}, @@ -34,15 +40,30 @@ mod client_packet; mod container; pub mod player_packet; +/// Represents a player's configuration settings. +/// +/// This struct contains various options that can be customized by the player, affecting their gameplay experience. +/// +/// **Usage:** +/// +/// This struct is typically used to store and manage a player's preferences. It can be sent to the server when a player joins or when they change their settings. #[derive(Clone)] pub struct PlayerConfig { + /// The player's preferred language. pub locale: String, // 16 + /// The maximum distance at which chunks are rendered. pub view_distance: i8, + /// The player's chat mode settings pub chat_mode: ChatMode, + /// Whether chat colors are enabled. pub chat_colors: bool, + /// The player's skin configuration options. pub skin_parts: u8, + /// The player's dominant hand (left or right). pub main_hand: Hand, + /// Whether text filtering is enabled. pub text_filtering: bool, + /// Whether the player wants to appear in the server list. pub server_listing: bool, } @@ -61,206 +82,158 @@ impl Default for PlayerConfig { } } +/// Everything which makes a Connection with our Server is a `Client`. +/// Client will become Players when they reach the `Play` state pub struct Client { - pub gameprofile: Option, - - pub config: Option, - pub brand: Option, - - pub protocol_version: i32, - pub connection_state: ConnectionState, - pub encryption: bool, - pub closed: bool, + /// The client's game profile information. + pub gameprofile: Mutex>, + /// The client's configuration settings, Optional + pub config: Mutex>, + /// The client's brand or modpack information, Optional. + pub brand: Mutex>, + /// The minecraft protocol version used by the client. + pub protocol_version: AtomicI32, + /// The current connection state of the client (e.g., Handshaking, Status, Play). + pub connection_state: AtomicCell, + /// Whether encryption is enabled for the connection. + pub encryption: AtomicBool, + /// Indicates if the client connection is closed. + pub closed: AtomicBool, + /// A unique token identifying the client. pub token: Token, - pub connection: TcpStream, - pub address: SocketAddr, - enc: PacketEncoder, - dec: PacketDecoder, - pub client_packets_queue: Vec, + /// The underlying TCP connection to the client. + pub connection: Arc>, + /// The client's IP address. + pub address: Mutex, + /// The packet encoder for outgoing packets. + enc: Arc>, + /// The packet decoder for incoming packets. + dec: Arc>, + /// A queue of raw packets received from the client, waiting to be processed. + pub client_packets_queue: Arc>>, - pub make_player: bool, + /// Indicates whether the client should be converted into a player. + pub make_player: AtomicBool, + /// Sends each keep alive packet that the server receives for a player to here, which gets picked up in a tokio task + pub keep_alive_sender: Arc>, + /// Stores the last time it was confirmed that the client is alive + pub last_alive_received: AtomicCell, } impl Client { - pub fn new(token: Token, connection: TcpStream, address: SocketAddr) -> Self { + pub fn new( + token: Token, + connection: TcpStream, + address: SocketAddr, + keep_alive_sender: Arc>, + ) -> Self { Self { - protocol_version: 0, - gameprofile: None, - config: None, - brand: None, + protocol_version: AtomicI32::new(0), + gameprofile: Mutex::new(None), + config: Mutex::new(None), + brand: Mutex::new(None), token, - address, - connection_state: ConnectionState::HandShake, - connection, - enc: PacketEncoder::default(), - dec: PacketDecoder::default(), - encryption: true, - closed: false, - client_packets_queue: Vec::new(), - make_player: false, + address: Mutex::new(address), + connection_state: AtomicCell::new(ConnectionState::HandShake), + connection: Arc::new(Mutex::new(connection)), + enc: Arc::new(Mutex::new(PacketEncoder::default())), + dec: Arc::new(Mutex::new(PacketDecoder::default())), + encryption: AtomicBool::new(false), + closed: AtomicBool::new(false), + client_packets_queue: Arc::new(Mutex::new(Vec::new())), + make_player: AtomicBool::new(false), + keep_alive_sender, + last_alive_received: AtomicCell::new(std::time::Instant::now()), } } - /// adds a Incoming packet to the queue - pub fn add_packet(&mut self, packet: RawPacket) { - self.client_packets_queue.push(packet); + /// Adds a Incoming packet to the queue + pub fn add_packet(&self, packet: RawPacket) { + let mut client_packets_queue = self.client_packets_queue.lock(); + client_packets_queue.push(packet); } - /// enables encryption - pub fn enable_encryption( - &mut self, - shared_secret: &[u8], // decrypted + /// Sets the Packet encryption + pub fn set_encryption( + &self, + shared_secret: Option<&[u8]>, // decrypted ) -> Result<(), EncryptionError> { - self.encryption = true; - let crypt_key: [u8; 16] = shared_secret - .try_into() - .map_err(|_| EncryptionError::SharedWrongLength)?; - self.dec.enable_encryption(&crypt_key); - self.enc.enable_encryption(&crypt_key); + if let Some(shared_secret) = shared_secret { + self.encryption + .store(true, std::sync::atomic::Ordering::Relaxed); + let crypt_key: [u8; 16] = shared_secret + .try_into() + .map_err(|_| EncryptionError::SharedWrongLength)?; + self.dec.lock().set_encryption(Some(&crypt_key)); + self.enc.lock().set_encryption(Some(&crypt_key)); + } else { + self.dec.lock().set_encryption(None); + self.enc.lock().set_encryption(None); + } Ok(()) } - // Compression threshold, Compression level - pub fn set_compression(&mut self, compression: Option<(u32, u32)>) { - self.dec.set_compression(compression.map(|v| v.0)); - self.enc.set_compression(compression); + /// Sets the Packet compression + pub fn set_compression(&self, compression: Option) { + self.dec.lock().set_compression(compression.is_some()); + self.enc.lock().set_compression(compression); } /// Send a Clientbound Packet to the Client - pub fn send_packet(&mut self, packet: &P) { + pub fn send_packet(&self, packet: &P) { // assert!(!self.closed); - - self.enc - .append_packet(packet) + let mut enc = self.enc.lock(); + enc.append_packet(packet) .unwrap_or_else(|e| self.kick(&e.to_string())); self.connection - .write_all(&self.enc.take()) + .lock() + .write_all(&enc.take()) .map_err(|_| PacketError::ConnectionWrite) .unwrap_or_else(|e| self.kick(&e.to_string())); } - pub fn try_send_packet(&mut self, packet: &P) -> Result<(), PacketError> { + pub fn try_send_packet(&self, packet: &P) -> Result<(), PacketError> { // assert!(!self.closed); - self.enc.append_packet(packet)?; + let mut enc = self.enc.lock(); + enc.append_packet(packet)?; self.connection - .write_all(&self.enc.take()) + .lock() + .write_all(&enc.take()) .map_err(|_| PacketError::ConnectionWrite)?; Ok(()) } - pub async fn process_packets(&mut self, server: &Arc) { - while let Some(mut packet) = self.client_packets_queue.pop() { - match self.handle_packet(server, &mut packet).await { - Ok(_) => {} - Err(e) => { - let text = format!("Error while reading incoming packet {}", e); - log::error!("{}", text); - self.kick(&text) - } - }; + /// Processes all packets send by the client + pub async fn process_packets(&self, server: &Arc) { + while let Some(mut packet) = self.client_packets_queue.lock().pop() { + let _ = self.handle_packet(server, &mut packet).await.map_err(|e| { + let text = format!("Error while reading incoming packet {}", e); + log::error!("{}", text); + self.kick(&text) + }); } } /// Handles an incoming decoded not Play state Packet pub async fn handle_packet( - &mut self, + &self, server: &Arc, packet: &mut RawPacket, ) -> Result<(), DeserializerError> { - // TODO: handle each packet's Error instead of calling .unwrap() - let bytebuf = &mut packet.bytebuf; - match self.connection_state { - pumpkin_protocol::ConnectionState::HandShake => match packet.id.0 { - SHandShake::PACKET_ID => { - self.handle_handshake(server, SHandShake::read(bytebuf)?); - Ok(()) - } - _ => { - log::error!( - "Failed to handle packet id {} while in Handshake state", - packet.id.0 - ); - Ok(()) - } - }, - pumpkin_protocol::ConnectionState::Status => match packet.id.0 { - SStatusRequest::PACKET_ID => { - self.handle_status_request(server, SStatusRequest::read(bytebuf)?); - Ok(()) - } - SStatusPingRequest::PACKET_ID => { - self.handle_ping_request(server, SStatusPingRequest::read(bytebuf)?); - Ok(()) - } - _ => { - log::error!( - "Failed to handle packet id {} while in Status state", - packet.id.0 - ); - Ok(()) - } - }, + match self.connection_state.load() { + pumpkin_protocol::ConnectionState::HandShake => { + self.handle_handshake_packet(server, packet) + } + pumpkin_protocol::ConnectionState::Status => self.handle_status_packet(server, packet), // TODO: Check config if transfer is enabled pumpkin_protocol::ConnectionState::Login - | pumpkin_protocol::ConnectionState::Transfer => match packet.id.0 { - SLoginStart::PACKET_ID => { - self.handle_login_start(server, SLoginStart::read(bytebuf)?); - Ok(()) - } - SEncryptionResponse::PACKET_ID => { - self.handle_encryption_response(server, SEncryptionResponse::read(bytebuf)?) - .await; - Ok(()) - } - SLoginPluginResponse::PACKET_ID => { - self.handle_plugin_response(server, SLoginPluginResponse::read(bytebuf)?); - Ok(()) - } - SLoginAcknowledged::PACKET_ID => { - self.handle_login_acknowledged(server, SLoginAcknowledged::read(bytebuf)?); - Ok(()) - } - _ => { - log::error!( - "Failed to handle packet id {} while in Login state", - packet.id.0 - ); - Ok(()) - } - }, - pumpkin_protocol::ConnectionState::Config => match packet.id.0 { - SClientInformationConfig::PACKET_ID => { - self.handle_client_information_config( - server, - SClientInformationConfig::read(bytebuf)?, - ); - Ok(()) - } - SPluginMessage::PACKET_ID => { - self.handle_plugin_message(server, SPluginMessage::read(bytebuf)?); - Ok(()) - } - SAcknowledgeFinishConfig::PACKET_ID => { - self.handle_config_acknowledged( - server, - SAcknowledgeFinishConfig::read(bytebuf)?, - ) - .await; - Ok(()) - } - SKnownPacks::PACKET_ID => { - self.handle_known_packs(server, SKnownPacks::read(bytebuf)?); - Ok(()) - } - _ => { - log::error!( - "Failed to handle packet id {} while in Config state", - packet.id.0 - ); - Ok(()) - } - }, + | pumpkin_protocol::ConnectionState::Transfer => { + self.handle_login_packet(server, packet).await + } + pumpkin_protocol::ConnectionState::Config => { + self.handle_config_packet(server, packet).await + } _ => { log::error!("Invalid Connection state {:?}", self.connection_state); Ok(()) @@ -268,15 +241,133 @@ impl Client { } } - // Reads the connection until our buffer of len 4096 is full, then decode - /// Close connection when an error occurs - pub async fn poll(&mut self, event: &Event) { + fn handle_handshake_packet( + &self, + server: &Arc, + packet: &mut RawPacket, + ) -> Result<(), DeserializerError> { + let bytebuf = &mut packet.bytebuf; + match packet.id.0 { + SHandShake::PACKET_ID => { + self.handle_handshake(server, SHandShake::read(bytebuf)?); + Ok(()) + } + _ => { + log::error!( + "Failed to handle packet id {} while in Handshake state", + packet.id.0 + ); + Ok(()) + } + } + } + + fn handle_status_packet( + &self, + server: &Arc, + packet: &mut RawPacket, + ) -> Result<(), DeserializerError> { + let bytebuf = &mut packet.bytebuf; + match packet.id.0 { + SStatusRequest::PACKET_ID => { + self.handle_status_request(server, SStatusRequest::read(bytebuf)?); + Ok(()) + } + SStatusPingRequest::PACKET_ID => { + self.handle_ping_request(server, SStatusPingRequest::read(bytebuf)?); + Ok(()) + } + _ => { + log::error!( + "Failed to handle packet id {} while in Status state", + packet.id.0 + ); + Ok(()) + } + } + } + + async fn handle_login_packet( + &self, + server: &Arc, + packet: &mut RawPacket, + ) -> Result<(), DeserializerError> { + let bytebuf = &mut packet.bytebuf; + match packet.id.0 { + SLoginStart::PACKET_ID => { + self.handle_login_start(server, SLoginStart::read(bytebuf)?); + Ok(()) + } + SEncryptionResponse::PACKET_ID => { + self.handle_encryption_response(server, SEncryptionResponse::read(bytebuf)?) + .await; + Ok(()) + } + SLoginPluginResponse::PACKET_ID => { + self.handle_plugin_response(server, SLoginPluginResponse::read(bytebuf)?); + Ok(()) + } + SLoginAcknowledged::PACKET_ID => { + self.handle_login_acknowledged(server, SLoginAcknowledged::read(bytebuf)?); + Ok(()) + } + _ => { + log::error!( + "Failed to handle packet id {} while in Login state", + packet.id.0 + ); + Ok(()) + } + } + } + + async fn handle_config_packet( + &self, + server: &Arc, + packet: &mut RawPacket, + ) -> Result<(), DeserializerError> { + let bytebuf = &mut packet.bytebuf; + match packet.id.0 { + SClientInformationConfig::PACKET_ID => { + self.handle_client_information_config( + server, + SClientInformationConfig::read(bytebuf)?, + ); + Ok(()) + } + SPluginMessage::PACKET_ID => { + self.handle_plugin_message(server, SPluginMessage::read(bytebuf)?); + Ok(()) + } + SAcknowledgeFinishConfig::PACKET_ID => { + self.handle_config_acknowledged(server, SAcknowledgeFinishConfig::read(bytebuf)?) + .await; + Ok(()) + } + SKnownPacks::PACKET_ID => { + self.handle_known_packs(server, SKnownPacks::read(bytebuf)?); + Ok(()) + } + _ => { + log::error!( + "Failed to handle packet id {} while in Config state", + packet.id.0 + ); + Ok(()) + } + } + } + + /// Reads the connection until our buffer of len 4096 is full, then decode + /// Close connection when an error occurs or when the Client closed the connection + pub async fn poll(&self, event: &Event) { if event.is_readable() { let mut received_data = vec![0; 4096]; let mut bytes_read = 0; - // We can (maybe) read from the connection. loop { - match self.connection.read(&mut received_data[bytes_read..]) { + let connection = self.connection.clone(); + let mut connection = connection.lock(); + match connection.read(&mut received_data[bytes_read..]) { Ok(0) => { // Reading 0 bytes means the other side has closed the // connection or is done writing, then so are we. @@ -297,9 +388,9 @@ impl Client { } if bytes_read != 0 { - self.dec.reserve(4096); - self.dec.queue_slice(&received_data[..bytes_read]); - match self.dec.decode() { + let mut dec = self.dec.lock(); + dec.queue_slice(&received_data[..bytes_read]); + match dec.decode() { Ok(packet) => { if let Some(packet) = packet { self.add_packet(packet); @@ -307,18 +398,18 @@ impl Client { } Err(err) => self.kick(&err.to_string()), } - self.dec.clear(); + dec.clear(); } } } /// Kicks the Client with a reason depending on the connection state - pub fn kick(&mut self, reason: &str) { + pub fn kick(&self, reason: &str) { dbg!(reason); - match self.connection_state { + match self.connection_state.load() { ConnectionState::Login => { self.try_send_packet(&CLoginDisconnect::new( - &serde_json::to_string_pretty(&reason).unwrap_or("".into()), + &serde_json::to_string_pretty(&reason).unwrap_or_else(|_| "".into()), )) .unwrap_or_else(|_| self.close()); } @@ -326,7 +417,7 @@ impl Client { self.try_send_packet(&CConfigDisconnect::new(reason)) .unwrap_or_else(|_| self.close()); } - // So we can also kick on errors, but generally should use Player::kick + // This way players get kicked when players using client functions (e.g. poll, send_packet) ConnectionState::Play => { self.try_send_packet(&CPlayDisconnect::new(&TextComponent::text(reason))) .unwrap_or_else(|_| self.close()); @@ -339,8 +430,9 @@ impl Client { } /// You should prefer to use `kick` when you can - pub fn close(&mut self) { - self.closed = true; + pub fn close(&self) { + self.closed + .store(true, std::sync::atomic::Ordering::Relaxed); } } diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index ac40d57b1..fd8a31a1a 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -9,7 +9,7 @@ use crate::{ use num_traits::FromPrimitive; use pumpkin_config::ADVANCED_CONFIG; use pumpkin_core::{ - math::{position::WorldPosition, wrap_degrees}, + math::{position::WorldPosition, vector3::Vector3, wrap_degrees}, text::TextComponent, GameMode, }; @@ -29,7 +29,7 @@ use pumpkin_protocol::{ SUseItemOn, Status, }, }; -use pumpkin_world::block::{BlockFace, BlockId}; +use pumpkin_world::block::{BlockFace, BlockState}; use pumpkin_world::global_registry; use super::PlayerConfig; @@ -42,16 +42,19 @@ fn modulus(a: f32, b: f32) -> f32 { /// NEVER TRUST THE CLIENT. HANDLE EVERY ERROR, UNWRAP/EXPECT ARE FORBIDDEN impl Player { pub fn handle_confirm_teleport( - &mut self, + &self, _server: &Arc, confirm_teleport: SConfirmTeleport, ) { - if let Some((id, position)) = self.awaiting_teleport.as_ref() { + let mut awaiting_teleport = self.awaiting_teleport.lock(); + if let Some((id, position)) = awaiting_teleport.as_ref() { if id == &confirm_teleport.teleport_id { // we should set the pos now to that we requested in the teleport packet, Is may fixed issues when the client sended position packets while being teleported - self.entity.set_pos(position.x, position.y, position.z); + self.living_entity + .entity + .set_pos(position.x, position.y, position.z); - self.awaiting_teleport = None; + *awaiting_teleport = None; } else { self.kick(TextComponent::text("Wrong teleport id")) } @@ -70,25 +73,27 @@ impl Player { pos.clamp(-2.0E7, 2.0E7) } - pub async fn handle_position(&mut self, _server: &Arc, position: SPlayerPosition) { + pub async fn handle_position(&self, _server: &Arc, position: SPlayerPosition) { if position.x.is_nan() || position.feet_y.is_nan() || position.z.is_nan() { self.kick(TextComponent::text("Invalid movement")); return; } - let entity = &mut self.entity; - self.last_position = entity.pos; + let entity = &self.living_entity.entity; entity.set_pos( Self::clamp_horizontal(position.x), Self::clamp_vertical(position.feet_y), Self::clamp_horizontal(position.z), ); - entity.on_ground = position.ground; - let on_ground = entity.on_ground; + let pos = entity.pos.load(); + self.last_position.store(pos); + let last_position = self.last_position.load(); + entity + .on_ground + .store(position.ground, std::sync::atomic::Ordering::Relaxed); let entity_id = entity.entity_id; - let (x, y, z) = entity.pos.into(); - let (lastx, lasty, lastz) = self.last_position.into(); - let world = self.entity.world.clone(); - let world = world.lock().await; + let Vector3 { x, y, z } = pos; + let (lastx, lasty, lastz) = (last_position.x, last_position.y, last_position.z); + let world = &entity.world; // let delta = Vector3::new(x - lastx, y - lasty, z - lastz); // let velocity = self.velocity; @@ -103,21 +108,21 @@ impl Player { // return; // } // send new position to all other players - world.broadcast_packet( + world.broadcast_packet_expect( &[self.client.token], &CUpdateEntityPos::new( entity_id.into(), - (x * 4096.0 - lastx * 4096.0) as i16, - (y * 4096.0 - lasty * 4096.0) as i16, - (z * 4096.0 - lastz * 4096.0) as i16, - on_ground, + x.mul_add(4096.0, -(lastx * 4096.0)) as i16, + y.mul_add(4096.0, -(lasty * 4096.0)) as i16, + z.mul_add(4096.0, -(lastz * 4096.0)) as i16, + position.ground, ), ); - player_chunker::update_position(&world, self).await; + player_chunker::update_position(entity, self).await; } pub async fn handle_position_rotation( - &mut self, + &self, _server: &Arc, position_rotation: SPlayerPositionRotation, ) { @@ -128,31 +133,36 @@ impl Player { self.kick(TextComponent::text("Invalid movement")); return; } - if !position_rotation.yaw.is_finite() || !position_rotation.pitch.is_finite() { + if position_rotation.yaw.is_infinite() || position_rotation.pitch.is_infinite() { self.kick(TextComponent::text("Invalid rotation")); return; } - let entity = &mut self.entity; + let entity = &self.living_entity.entity; - self.last_position = entity.pos; entity.set_pos( Self::clamp_horizontal(position_rotation.x), Self::clamp_vertical(position_rotation.feet_y), Self::clamp_horizontal(position_rotation.z), ); - entity.on_ground = position_rotation.ground; - entity.yaw = wrap_degrees(position_rotation.yaw) % 360.0; - entity.pitch = wrap_degrees(position_rotation.pitch).clamp(-90.0, 90.0) % 360.0; + let pos = entity.pos.load(); + self.last_position.store(pos); + let last_position = self.last_position.load(); + entity.on_ground.store( + position_rotation.ground, + std::sync::atomic::Ordering::Relaxed, + ); + entity.set_rotation( + wrap_degrees(position_rotation.yaw) % 360.0, + wrap_degrees(position_rotation.pitch).clamp(-90.0, 90.0) % 360.0, + ); - let on_ground = entity.on_ground; let entity_id = entity.entity_id; - let (x, y, z) = entity.pos.into(); - let (lastx, lasty, lastz) = self.last_position.into(); - let yaw = modulus(entity.yaw * 256.0 / 360.0, 256.0); - let pitch = modulus(entity.pitch * 256.0 / 360.0, 256.0); + let Vector3 { x, y, z } = pos; + let (lastx, lasty, lastz) = (last_position.x, last_position.y, last_position.z); + let yaw = modulus(entity.yaw.load() * 256.0 / 360.0, 256.0); + let pitch = modulus(entity.pitch.load() * 256.0 / 360.0, 256.0); // let head_yaw = (entity.head_yaw * 256.0 / 360.0).floor(); - let world = self.entity.world.clone(); - let world = world.lock().await; + let world = &entity.world; // let delta = Vector3::new(x - lastx, y - lasty, z - lastz); // let velocity = self.velocity; @@ -168,97 +178,111 @@ impl Player { // } // send new position to all other players - world.broadcast_packet( + world.broadcast_packet_expect( &[self.client.token], &CUpdateEntityPosRot::new( entity_id.into(), - (x * 4096.0 - lastx * 4096.0) as i16, - (y * 4096.0 - lasty * 4096.0) as i16, - (z * 4096.0 - lastz * 4096.0) as i16, + x.mul_add(4096.0, -(lastx * 4096.0)) as i16, + y.mul_add(4096.0, -(lasty * 4096.0)) as i16, + z.mul_add(4096.0, -(lastz * 4096.0)) as i16, yaw as u8, pitch as u8, - on_ground, + position_rotation.ground, ), ); - world.broadcast_packet( + world.broadcast_packet_expect( &[self.client.token], &CHeadRot::new(entity_id.into(), yaw as u8), ); - - player_chunker::update_position(&world, self).await; + player_chunker::update_position(entity, self).await; } - pub async fn handle_rotation(&mut self, _server: &Arc, rotation: SPlayerRotation) { + pub async fn handle_rotation(&self, _server: &Arc, rotation: SPlayerRotation) { if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() { self.kick(TextComponent::text("Invalid rotation")); return; } - let entity = &mut self.entity; - entity.on_ground = rotation.ground; - entity.yaw = wrap_degrees(rotation.yaw) % 360.0; - entity.pitch = wrap_degrees(rotation.pitch).clamp(-90.0, 90.0) % 360.0; + let entity = &self.living_entity.entity; + entity + .on_ground + .store(rotation.ground, std::sync::atomic::Ordering::Relaxed); + entity.set_rotation( + wrap_degrees(rotation.yaw) % 360.0, + wrap_degrees(rotation.pitch).clamp(-90.0, 90.0) % 360.0, + ); // send new position to all other players - let on_ground = entity.on_ground; let entity_id = entity.entity_id; - let yaw = modulus(entity.yaw * 256.0 / 360.0, 256.0); - let pitch = modulus(entity.pitch * 256.0 / 360.0, 256.0); + let yaw = modulus(entity.yaw.load() * 256.0 / 360.0, 256.0); + let pitch = modulus(entity.pitch.load() * 256.0 / 360.0, 256.0); // let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0); - let world = self.entity.world.lock().await; - let packet = CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, on_ground); - // self.client.send_packet(&packet); - world.broadcast_packet(&[self.client.token], &packet); + let world = &entity.world; + let packet = + CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, rotation.ground); + world.broadcast_packet_expect(&[self.client.token], &packet); let packet = CHeadRot::new(entity_id.into(), yaw as u8); - // self.client.send_packet(&packet); - world.broadcast_packet(&[self.client.token], &packet); + world.broadcast_packet_expect(&[self.client.token], &packet); } - pub fn handle_chat_command(&mut self, server: &Arc, command: SChatCommand) { + pub fn handle_chat_command(&self, server: &Arc, command: SChatCommand) { let dispatcher = server.command_dispatcher.clone(); dispatcher.handle_command(&mut CommandSender::Player(self), server, &command.command); + if ADVANCED_CONFIG.commands.log_console { + log::info!( + "Player ({}): executed command /{}", + self.gameprofile.name, + command.command + ); + } } - pub fn handle_player_ground(&mut self, _server: &Arc, ground: SSetPlayerGround) { - self.entity.on_ground = ground.on_ground; + pub fn handle_player_ground(&self, _server: &Arc, ground: SSetPlayerGround) { + self.living_entity + .entity + .on_ground + .store(ground.on_ground, std::sync::atomic::Ordering::Relaxed); } - pub async fn handle_player_command(&mut self, _server: &Arc, command: SPlayerCommand) { - if command.entity_id != self.entity.entity_id.into() { + pub async fn handle_player_command(&self, _server: &Arc, command: SPlayerCommand) { + if command.entity_id != self.entity_id().into() { return; } if let Some(action) = Action::from_i32(command.action.0) { + let entity = &self.living_entity.entity; match action { pumpkin_protocol::server::play::Action::StartSneaking => { - if !self.entity.sneaking { - self.entity.set_sneaking(&mut self.client, true).await + if !entity.sneaking.load(std::sync::atomic::Ordering::Relaxed) { + entity.set_sneaking(true).await } } pumpkin_protocol::server::play::Action::StopSneaking => { - if self.entity.sneaking { - self.entity.set_sneaking(&mut self.client, false).await + if entity.sneaking.load(std::sync::atomic::Ordering::Relaxed) { + entity.set_sneaking(false).await } } pumpkin_protocol::server::play::Action::LeaveBed => todo!(), pumpkin_protocol::server::play::Action::StartSprinting => { - if !self.entity.sprinting { - self.entity.set_sprinting(&mut self.client, true).await + if !entity.sprinting.load(std::sync::atomic::Ordering::Relaxed) { + entity.set_sprinting(true).await } } pumpkin_protocol::server::play::Action::StopSprinting => { - if self.entity.sprinting { - self.entity.set_sprinting(&mut self.client, false).await + if entity.sprinting.load(std::sync::atomic::Ordering::Relaxed) { + entity.set_sprinting(false).await } } pumpkin_protocol::server::play::Action::StartHorseJump => todo!(), pumpkin_protocol::server::play::Action::StopHorseJump => todo!(), pumpkin_protocol::server::play::Action::OpenVehicleInventory => todo!(), pumpkin_protocol::server::play::Action::StartFlyingElytra => { - let fall_flying = self.entity.check_fall_flying(); - if self.entity.fall_flying != fall_flying { - self.entity - .set_fall_flying(&mut self.client, fall_flying) - .await; + let fall_flying = entity.check_fall_flying(); + if entity + .fall_flying + .load(std::sync::atomic::Ordering::Relaxed) + != fall_flying + { + entity.set_fall_flying(fall_flying).await; } } // TODO } @@ -267,7 +291,7 @@ impl Player { } } - pub async fn handle_swing_arm(&mut self, _server: &Arc, swing_arm: SSwingArm) { + pub async fn handle_swing_arm(&self, _server: &Arc, swing_arm: SSwingArm) { match Hand::from_i32(swing_arm.hand.0) { Some(hand) => { let animation = match hand { @@ -275,8 +299,8 @@ impl Player { Hand::Off => Animation::SwingOffhand, }; let id = self.entity_id(); - let world = self.entity.world.lock().await; - world.broadcast_packet( + let world = &self.living_entity.entity.world; + world.broadcast_packet_expect( &[self.client.token], &CEntityAnimation::new(id.into(), animation as u8), ) @@ -287,7 +311,7 @@ impl Player { }; } - pub async fn handle_chat_message(&mut self, _server: &Arc, chat_message: SChatMessage) { + pub async fn handle_chat_message(&self, _server: &Arc, chat_message: SChatMessage) { dbg!("got message"); let message = chat_message.message; @@ -299,24 +323,22 @@ impl Player { // TODO: filter message & validation let gameprofile = &self.gameprofile; - let world = self.entity.world.lock().await; - world.broadcast_packet( - &[self.client.token], - &CPlayerChatMessage::new( - pumpkin_protocol::uuid::UUID(gameprofile.id), - 1.into(), - chat_message.signature.as_deref(), - &message, - chat_message.timestamp, - chat_message.salt, - &[], - Some(TextComponent::text(&message)), - FilterType::PassThrough, - 1.into(), - TextComponent::text(&gameprofile.name.clone()), - None, - ), - ) + let entity = &self.living_entity.entity; + let world = &entity.world; + world.broadcast_packet_all(&CPlayerChatMessage::new( + gameprofile.id, + 1.into(), + chat_message.signature.as_deref(), + &message, + chat_message.timestamp, + chat_message.salt, + &[], + Some(TextComponent::text(&message)), + FilterType::PassThrough, + 1.into(), + TextComponent::text(&gameprofile.name), + None, + )) /* server.broadcast_packet( self, @@ -330,7 +352,7 @@ impl Player { } pub fn handle_client_information_play( - &mut self, + &self, _server: &Arc, client_information: SClientInformationPlay, ) { @@ -338,7 +360,7 @@ impl Player { Hand::from_i32(client_information.main_hand.into()), ChatMode::from_i32(client_information.chat_mode.into()), ) { - self.config = PlayerConfig { + *self.config.lock() = PlayerConfig { locale: client_information.locale, view_distance: client_information.view_distance, chat_mode, @@ -353,10 +375,11 @@ impl Player { } } - pub async fn handle_interact(&mut self, _: &Arc, interact: SInteract) { + pub async fn handle_interact(&self, _: &Arc, interact: SInteract) { let sneaking = interact.sneaking; - if self.entity.sneaking != sneaking { - self.entity.set_sneaking(&mut self.client, sneaking).await; + let entity = &self.living_entity.entity; + if entity.sneaking.load(std::sync::atomic::Ordering::Relaxed) != sneaking { + entity.set_sneaking(sneaking).await; } match ActionType::from_i32(interact.typ.0) { Some(action) => match action { @@ -365,44 +388,44 @@ impl Player { // TODO: do validation and stuff let config = &ADVANCED_CONFIG.pvp; if config.enabled { - let world = self.entity.world.clone(); - let world = world.lock().await; - let attacked_player = world.get_by_entityid(self, entity_id.0 as EntityId); - if let Some(mut player) = attacked_player { - let token = player.client.token; - let velo = player.entity.velocity; - if config.protect_creative && player.gamemode == GameMode::Creative { + let world = &entity.world; + let attacked_player = world.get_player_by_entityid(entity_id.0 as EntityId); + if let Some(player) = attacked_player { + let victem_entity = &player.living_entity.entity; + if config.protect_creative + && player.gamemode.load() == GameMode::Creative + { return; } if config.knockback { - let yaw = self.entity.yaw; + let yaw = entity.yaw.load(); let strength = 1.0; - player.entity.knockback( + let victem_velocity = victem_entity.velocity.load(); + let saved_velo = victem_velocity; + victem_entity.knockback( strength * 0.5, (yaw * (PI / 180.0)).sin() as f64, -(yaw * (PI / 180.0)).cos() as f64, ); let packet = &CEntityVelocity::new( &entity_id, - velo.x as f32, - velo.y as f32, - velo.z as f32, + victem_velocity.x as f32, + victem_velocity.y as f32, + victem_velocity.z as f32, ); - self.entity.velocity = self.entity.velocity.multiply(0.6, 1.0, 0.6); + let velocity = entity.velocity.load(); + victem_entity + .velocity + .store(velocity.multiply(0.6, 1.0, 0.6)); - player.entity.velocity = velo; + victem_entity.velocity.store(saved_velo); player.client.send_packet(packet); } if config.hurt_animation { - // TODO - // thats how we prevent borrow errors :c - let packet = &CHurtAnimation::new(&entity_id, self.entity.yaw); - self.client.send_packet(packet); - player.client.send_packet(packet); - world.broadcast_packet( - &[self.client.token, token], - &CHurtAnimation::new(&entity_id, 10.0), - ) + world.broadcast_packet_all(&CHurtAnimation::new( + &entity_id, + entity.yaw.load(), + )) } if config.swing {} } else { @@ -420,11 +443,7 @@ impl Player { None => self.kick(TextComponent::text("Invalid action type")), } } - pub async fn handle_player_action( - &mut self, - _server: &Arc, - player_action: SPlayerAction, - ) { + pub async fn handle_player_action(&self, _server: &Arc, player_action: SPlayerAction) { match Status::from_i32(player_action.status.0) { Some(status) => match status { Status::StartedDigging => { @@ -434,20 +453,15 @@ impl Player { } // TODO: do validation // TODO: Config - if self.gamemode == GameMode::Creative { + if self.gamemode.load() == GameMode::Creative { let location = player_action.location; // Block break & block break sound // TODO: currently this is always dirt replace it - let world = self.entity.world.lock().await; - world.broadcast_packet( - &[self.client.token], - &CWorldEvent::new(2001, &location, 11, false), - ); + let entity = &self.living_entity.entity; + let world = &entity.world; + world.broadcast_packet_all(&CWorldEvent::new(2001, &location, 11, false)); // AIR - world.broadcast_packet( - &[self.client.token], - &CBlockUpdate::new(&location, 0.into()), - ); + world.broadcast_packet_all(&CBlockUpdate::new(&location, 0.into())); } } Status::CancelledDigging => { @@ -455,7 +469,8 @@ impl Player { // TODO: maybe log? return; } - self.current_block_destroy_stage = 0; + self.current_block_destroy_stage + .store(0, std::sync::atomic::Ordering::Relaxed); } Status::FinishedDigging => { // TODO: do validation @@ -466,16 +481,11 @@ impl Player { } // Block break & block break sound // TODO: currently this is always dirt replace it - let world = self.entity.world.lock().await; - world.broadcast_packet( - &[self.client.token], - &CWorldEvent::new(2001, &location, 11, false), - ); + let entity = &self.living_entity.entity; + let world = &entity.world; + world.broadcast_packet_all(&CWorldEvent::new(2001, &location, 11, false)); // AIR - world.broadcast_packet( - &[self.client.token], - &CBlockUpdate::new(&location, 0.into()), - ); + world.broadcast_packet_all(&CBlockUpdate::new(&location, 0.into())); // TODO: Send this every tick self.client .send_packet(&CAcknowledgeBlockChange::new(player_action.sequence)); @@ -497,12 +507,12 @@ impl Player { } } - pub fn handle_play_ping_request(&mut self, _server: &Arc, request: SPlayPingRequest) { + pub fn handle_play_ping_request(&self, _server: &Arc, request: SPlayPingRequest) { self.client .send_packet(&CPingResponse::new(request.payload)); } - pub async fn handle_use_item_on(&mut self, _server: &Arc, use_item_on: SUseItemOn) { + pub async fn handle_use_item_on(&self, _server: &Arc, use_item_on: SUseItemOn) { let location = use_item_on.location; if !self.can_interact_with_block_at(&location, 1.0) { @@ -511,25 +521,23 @@ impl Player { } if let Some(face) = BlockFace::from_i32(use_item_on.face.0) { - if let Some(item) = self.inventory.held_item() { + if let Some(item) = self.inventory.lock().held_item() { let minecraft_id = global_registry::find_minecraft_id( global_registry::ITEM_REGISTRY, item.item_id, ) .expect("All item ids are in the global registry"); - if let Ok(block_state_id) = BlockId::new(minecraft_id, None) { - let world = self.entity.world.lock().await; - world.broadcast_packet( - &[self.client.token], - &CBlockUpdate::new(&location, block_state_id.get_id_mojang_repr().into()), - ); - world.broadcast_packet( - &[self.client.token], - &CBlockUpdate::new( - &WorldPosition(location.0 + face.to_offset()), - block_state_id.get_id_mojang_repr().into(), - ), - ); + if let Ok(block_state_id) = BlockState::new(minecraft_id, None) { + let entity = &self.living_entity.entity; + let world = &entity.world; + world.broadcast_packet_all(&CBlockUpdate::new( + &location, + block_state_id.get_id_mojang_repr().into(), + )); + world.broadcast_packet_all(&CBlockUpdate::new( + &WorldPosition(location.0 + face.to_offset()), + block_state_id.get_id_mojang_repr().into(), + )); } } self.client @@ -539,46 +547,48 @@ impl Player { } } - pub fn handle_use_item(&mut self, _server: &Arc, _use_item: SUseItem) { + pub fn handle_use_item(&self, _server: &Arc, _use_item: SUseItem) { // TODO: handle packet correctly log::error!("An item was used(SUseItem), but the packet is not implemented yet"); } - pub fn handle_set_held_item(&mut self, _server: &Arc, held: SSetHeldItem) { + pub fn handle_set_held_item(&self, _server: &Arc, held: SSetHeldItem) { let slot = held.slot; if !(0..=8).contains(&slot) { self.kick(TextComponent::text("Invalid held slot")) } - self.inventory.set_selected(slot as usize); + self.inventory.lock().set_selected(slot as usize); } pub fn handle_set_creative_slot( - &mut self, + &self, _server: &Arc, packet: SSetCreativeSlot, ) -> Result<(), InventoryError> { - if self.gamemode != GameMode::Creative { + if self.gamemode.load() != GameMode::Creative { return Err(InventoryError::PermissionError); } self.inventory + .lock() .set_slot(packet.slot as usize, packet.clicked_item.to_item(), false) } // TODO: // This function will in the future be used to keep track of if the client is in a valid state. // But this is not possible yet - pub fn handle_close_container(&mut self, server: &Arc, packet: SCloseContainer) { + pub fn handle_close_container(&self, server: &Arc, packet: SCloseContainer) { // window_id 0 represents both 9x1 Generic AND inventory here - self.inventory.state_id = 0; - if let Some(id) = self.open_container { - let mut open_containers = server - .open_containers - .write() - .expect("open_containers got poisoned"); + self.inventory + .lock() + .state_id + .store(0, std::sync::atomic::Ordering::Relaxed); + let open_container = self.open_container.load(); + if let Some(id) = open_container { + let mut open_containers = server.open_containers.write(); if let Some(container) = open_containers.get_mut(&id) { container.remove_player(self.entity_id()) } - self.open_container = None; + self.open_container.store(None); } let Some(_window_type) = WindowType::from_u8(packet.window_id) else { self.kick(TextComponent::text("Invalid window ID")); diff --git a/pumpkin/src/commands/arg_player.rs b/pumpkin/src/commands/arg_player.rs index e3506b4cb..11e1fe3b6 100644 --- a/pumpkin/src/commands/arg_player.rs +++ b/pumpkin/src/commands/arg_player.rs @@ -1,8 +1,11 @@ +use std::sync::Arc; + use crate::commands::dispatcher::InvalidTreeError; use crate::commands::dispatcher::InvalidTreeError::InvalidConsumptionError; use crate::commands::tree::{ConsumedArgs, RawArgs}; use crate::commands::CommandSender; use crate::commands::CommandSender::Player; +use crate::server::Server; /// todo: implement (so far only own name + @s/@p is implemented) pub fn consume_arg_player(src: &CommandSender, args: &mut RawArgs) -> Option { @@ -29,9 +32,10 @@ pub fn consume_arg_player(src: &CommandSender, args: &mut RawArgs) -> Option( src: &'a mut CommandSender, + _server: &Arc, arg_name: &str, consumed_args: &ConsumedArgs, -) -> Result<&'a mut crate::entity::player::Player, InvalidTreeError> { +) -> Result<&'a crate::entity::player::Player, InvalidTreeError> { let s = consumed_args .get(arg_name) .ok_or(InvalidConsumptionError(None))? diff --git a/pumpkin/src/commands/cmd_echest.rs b/pumpkin/src/commands/cmd_echest.rs index c942d4c31..e793ccb8a 100644 --- a/pumpkin/src/commands/cmd_echest.rs +++ b/pumpkin/src/commands/cmd_echest.rs @@ -7,16 +7,13 @@ const NAMES: [&str; 2] = ["echest", "enderchest"]; const DESCRIPTION: &str = "Show your personal enderchest (this command is used for testing container behaviour)"; -pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { +pub fn init_command_tree<'a>() -> CommandTree<'a> { CommandTree::new(NAMES, DESCRIPTION).execute(&|sender, server, _| { if let Some(player) = sender.as_mut_player() { let entity_id = player.entity_id(); - player.open_container = Some(0); + player.open_container.store(Some(0)); { - let mut open_containers = server - .open_containers - .write() - .expect("open_containers got poisoned"); + let mut open_containers = server.open_containers.write(); match open_containers.get_mut(&0) { Some(ender_chest) => { ender_chest.add_player(entity_id); diff --git a/pumpkin/src/commands/cmd_gamemode.rs b/pumpkin/src/commands/cmd_gamemode.rs index d7b25ecf5..484d85c0e 100644 --- a/pumpkin/src/commands/cmd_gamemode.rs +++ b/pumpkin/src/commands/cmd_gamemode.rs @@ -56,7 +56,7 @@ pub fn parse_arg_gamemode(consumed_args: &ConsumedArgs) -> Result() -> CommandTree<'a> { +pub fn init_command_tree<'a>() -> CommandTree<'a> { CommandTree::new(NAMES, DESCRIPTION).with_child( require(&|sender| sender.permission_lvl() >= 2).with_child( argument(ARG_GAMEMODE, consume_arg_gamemode) @@ -65,15 +65,14 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { let gamemode = parse_arg_gamemode(args)?; return if let Player(target) = sender { - if target.gamemode == gamemode { + if target.gamemode.load() == gamemode { target.send_system_message(TextComponent::text(&format!( "You already in {:?} gamemode", gamemode ))); } else { // TODO - #[expect(clippy::let_underscore_future)] - let _ = target.set_gamemode(gamemode); + target.set_gamemode(gamemode); target.send_system_message(TextComponent::text(&format!( "Game mode was set to {:?}", gamemode @@ -86,19 +85,18 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { }), ) .with_child(argument(ARG_TARGET, consume_arg_player).execute( - &|sender, _, args| { + &|sender, server, args| { let gamemode = parse_arg_gamemode(args)?; - let target = parse_arg_player(sender, ARG_TARGET, args)?; + let target = parse_arg_player(sender, server, ARG_TARGET, args)?; - if target.gamemode == gamemode { + if target.gamemode.load() == gamemode { target.send_system_message(TextComponent::text(&format!( "You already in {:?} gamemode", gamemode ))); } else { // TODO - #[expect(clippy::let_underscore_future)] - let _ = target.set_gamemode(gamemode); + target.set_gamemode(gamemode); target.send_system_message(TextComponent::text(&format!( "Game mode was set to {:?}", gamemode diff --git a/pumpkin/src/commands/cmd_help.rs b/pumpkin/src/commands/cmd_help.rs index 59788b221..a955f4c8c 100644 --- a/pumpkin/src/commands/cmd_help.rs +++ b/pumpkin/src/commands/cmd_help.rs @@ -32,7 +32,7 @@ fn parse_arg_command<'a>( .map_err(|_| InvalidConsumptionError(Some(command_name.into()))) } -pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { +pub fn init_command_tree<'a>() -> CommandTree<'a> { CommandTree::new(NAMES, DESCRIPTION) .with_child( argument(ARG_COMMAND, consume_arg_command).execute(&|sender, server, args| { diff --git a/pumpkin/src/commands/cmd_kill.rs b/pumpkin/src/commands/cmd_kill.rs new file mode 100644 index 000000000..77c95f40e --- /dev/null +++ b/pumpkin/src/commands/cmd_kill.rs @@ -0,0 +1,30 @@ +use crate::commands::arg_player::{consume_arg_player, parse_arg_player}; +use crate::commands::tree::CommandTree; +use crate::commands::tree::RawArgs; +use crate::commands::tree_builder::argument; +use crate::commands::CommandSender; +use pumpkin_core::text::{color::NamedColor, TextComponent}; + +const NAMES: [&str; 1] = ["kill"]; +const DESCRIPTION: &str = "Kills a target player."; + +const ARG_TARGET: &str = "target"; + +pub fn consume_arg_target(_src: &CommandSender, args: &mut RawArgs) -> Option { + consume_arg_player(_src, args) +} + +pub fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(NAMES, DESCRIPTION).with_child( + argument(ARG_TARGET, consume_arg_target).execute(&|sender, server, args| { + let target = parse_arg_player(sender, server, ARG_TARGET, args)?; + target.living_entity.kill(); + + sender.send_message( + TextComponent::text("Player has been killed.").color_named(NamedColor::Blue), + ); + + Ok(()) + }), + ) +} diff --git a/pumpkin/src/commands/cmd_pumpkin.rs b/pumpkin/src/commands/cmd_pumpkin.rs index ad0b3561e..0d8d7565f 100644 --- a/pumpkin/src/commands/cmd_pumpkin.rs +++ b/pumpkin/src/commands/cmd_pumpkin.rs @@ -8,7 +8,7 @@ const NAMES: [&str; 1] = ["pumpkin"]; const DESCRIPTION: &str = "Display information about Pumpkin."; -pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { +pub fn init_command_tree<'a>() -> CommandTree<'a> { CommandTree::new(NAMES, DESCRIPTION).execute(&|sender, _, _| { let version = env!("CARGO_PKG_VERSION"); let description = env!("CARGO_PKG_DESCRIPTION"); diff --git a/pumpkin/src/commands/cmd_stop.rs b/pumpkin/src/commands/cmd_stop.rs index 5e12ccb48..2d90ae707 100644 --- a/pumpkin/src/commands/cmd_stop.rs +++ b/pumpkin/src/commands/cmd_stop.rs @@ -8,7 +8,7 @@ const NAMES: [&str; 1] = ["stop"]; const DESCRIPTION: &str = "Stop the server."; -pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> { +pub fn init_command_tree<'a>() -> CommandTree<'a> { CommandTree::new(NAMES, DESCRIPTION).with_child( require(&|sender| sender.permission_lvl() >= 4).execute(&|sender, _, _args| { sender diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index 3b3350b0d..322b96202 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -11,6 +11,7 @@ mod arg_player; mod cmd_echest; mod cmd_gamemode; mod cmd_help; +mod cmd_kill; mod cmd_pumpkin; mod cmd_stop; pub mod dispatcher; @@ -21,7 +22,7 @@ mod tree_format; pub enum CommandSender<'a> { Rcon(&'a mut Vec), Console, - Player(&'a mut Player), + Player(&'a Player), } impl<'a> CommandSender<'a> { @@ -34,7 +35,7 @@ impl<'a> CommandSender<'a> { } } - pub fn is_player(&self) -> bool { + pub const fn is_player(&self) -> bool { match self { CommandSender::Console => false, CommandSender::Player(_) => true, @@ -42,14 +43,14 @@ impl<'a> CommandSender<'a> { } } - pub fn is_console(&self) -> bool { + pub const fn is_console(&self) -> bool { match self { CommandSender::Console => true, CommandSender::Player(_) => false, CommandSender::Rcon(_) => true, } } - pub fn as_mut_player(&mut self) -> Option<&mut Player> { + pub fn as_mut_player(&mut self) -> Option<&Player> { match self { CommandSender::Player(player) => Some(player), CommandSender::Console => None, @@ -58,7 +59,7 @@ impl<'a> CommandSender<'a> { } /// todo: implement - pub fn permission_lvl(&self) -> i32 { + pub const fn permission_lvl(&self) -> i32 { match self { CommandSender::Rcon(_) => 4, CommandSender::Console => 4, @@ -75,6 +76,7 @@ pub fn default_dispatcher<'a>() -> CommandDispatcher<'a> { dispatcher.register(cmd_stop::init_command_tree()); dispatcher.register(cmd_help::init_command_tree()); dispatcher.register(cmd_echest::init_command_tree()); + dispatcher.register(cmd_kill::init_command_tree()); dispatcher } diff --git a/pumpkin/src/commands/tree.rs b/pumpkin/src/commands/tree.rs index 6f1d8517f..ebbe3d801 100644 --- a/pumpkin/src/commands/tree.rs +++ b/pumpkin/src/commands/tree.rs @@ -3,20 +3,20 @@ use crate::commands::CommandSender; use std::collections::{HashMap, VecDeque}; /// see [crate::commands::tree_builder::argument] -pub(crate) type RawArgs<'a> = Vec<&'a str>; +pub type RawArgs<'a> = Vec<&'a str>; /// see [crate::commands::tree_builder::argument] and [CommandTree::execute]/[crate::commands::tree_builder::NonLeafNodeBuilder::execute] -pub(crate) type ConsumedArgs<'a> = HashMap<&'a str, String>; +pub type ConsumedArgs<'a> = HashMap<&'a str, String>; /// see [crate::commands::tree_builder::argument] -pub(crate) type ArgumentConsumer<'a> = fn(&CommandSender, &mut RawArgs) -> Option; +pub type ArgumentConsumer<'a> = fn(&CommandSender, &mut RawArgs) -> Option; -pub(crate) struct Node<'a> { +pub struct Node<'a> { pub(crate) children: Vec, pub(crate) node_type: NodeType<'a>, } -pub(crate) enum NodeType<'a> { +pub enum NodeType<'a> { ExecuteLeaf { run: &'a RunFunctionType, }, @@ -32,12 +32,12 @@ pub(crate) enum NodeType<'a> { }, } -pub(crate) enum Command<'a> { +pub enum Command<'a> { Tree(CommandTree<'a>), Alias(&'a str), } -pub(crate) struct CommandTree<'a> { +pub struct CommandTree<'a> { pub(crate) nodes: Vec>, pub(crate) children: Vec, pub(crate) names: Vec<&'a str>, diff --git a/pumpkin/src/commands/tree_builder.rs b/pumpkin/src/commands/tree_builder.rs index 00a5449f1..ecb79880e 100644 --- a/pumpkin/src/commands/tree_builder.rs +++ b/pumpkin/src/commands/tree_builder.rs @@ -101,7 +101,7 @@ impl<'a> NodeBuilder<'a> for NonLeafNodeBuilder<'a> { impl<'a> NonLeafNodeBuilder<'a> { /// Add a child [Node] to this one. - pub fn with_child(mut self, child: NonLeafNodeBuilder<'a>) -> Self { + pub fn with_child(mut self, child: Self) -> Self { self.child_nodes.push(child); self } @@ -124,7 +124,7 @@ impl<'a> NonLeafNodeBuilder<'a> { /// Matches a sting literal. #[expect(dead_code)] // todo: remove (so far no commands requiring this are implemented) -pub fn literal(string: &str) -> NonLeafNodeBuilder { +pub const fn literal(string: &str) -> NonLeafNodeBuilder { NonLeafNodeBuilder { node_type: NodeType::Literal { string }, child_nodes: Vec::new(), diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs new file mode 100644 index 000000000..a6ae33ede --- /dev/null +++ b/pumpkin/src/entity/living.rs @@ -0,0 +1,46 @@ +use crossbeam::atomic::AtomicCell; +use pumpkin_protocol::client::play::{CEntityStatus, CSetEntityMetadata, Metadata}; + +use super::Entity; + +/// Represents a Living Entity (e.g. Player, Zombie, Enderman...) +pub struct LivingEntity { + pub entity: Entity, + /// The entity's current health level. + pub health: AtomicCell, +} + +impl LivingEntity { + pub const fn new(entity: Entity) -> Self { + Self { + entity, + health: AtomicCell::new(20.0), + } + } + + pub fn set_health(&self, health: f32) { + self.health.store(health); + // tell everyone entities health changed + self.entity + .world + .broadcast_packet_all(&CSetEntityMetadata::new( + self.entity.entity_id.into(), + Metadata::new(9, 3.into(), health), + )); + } + + /// Kills the Entity + /// + /// This is similar to `kill` but Spawn Particles, Animation and plays death sound + pub fn kill(&self) { + // Spawns death smoke particles + self.entity + .world + .broadcast_packet_all(&CEntityStatus::new(self.entity.entity_id, 60)); + // Plays the death sound and death animation + self.entity + .world + .broadcast_packet_all(&CEntityStatus::new(self.entity.entity_id, 3)); + self.entity.remove(); + } +} diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 986df49c0..9ffcbb94a 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -1,5 +1,8 @@ -use std::sync::Arc; +use std::sync::{atomic::AtomicBool, Arc}; +use crossbeam::atomic::AtomicCell; +use num_derive::{FromPrimitive, ToPrimitive}; +use num_traits::ToPrimitive; use pumpkin_core::math::{ get_section_cord, position::WorldPosition, vector2::Vector2, vector3::Vector3, }; @@ -9,111 +12,150 @@ use pumpkin_protocol::{ VarInt, }; -use crate::{client::Client, world::World}; +use crate::world::World; +pub mod living; pub mod player; +/// Represents a not living Entity (e.g. Item, Egg, Snowball...) pub struct Entity { + /// A unique identifier for the entity pub entity_id: EntityId, + /// The type of entity (e.g., player, zombie, item) pub entity_type: EntityType, - pub world: Arc>, + /// The world in which the entity exists. + pub world: Arc, + /// The entity's current health level. - pub pos: Vector3, - pub block_pos: WorldPosition, - pub chunk_pos: Vector2, + /// The entity's current position in the world + pub pos: AtomicCell>, + /// The entity's position rounded to the nearest block coordinates + pub block_pos: AtomicCell, + /// The chunk coordinates of the entity's current position + pub chunk_pos: AtomicCell>, - pub sneaking: bool, - pub sprinting: bool, - pub fall_flying: bool, - pub velocity: Vector3, + /// Indicates whether the entity is sneaking + pub sneaking: AtomicBool, + /// Indicates whether the entity is sprinting + pub sprinting: AtomicBool, + /// Indicates whether the entity is flying due to a fall + pub fall_flying: AtomicBool, - // Should be not trusted - pub on_ground: bool, + /// The entity's current velocity vector, aka Knockback + pub velocity: AtomicCell>, - pub yaw: f32, - pub head_yaw: f32, - pub pitch: f32, - // TODO: Change this in diffrent poses + /// Indicates whether the entity is on the ground (may not always be accurate). + pub on_ground: AtomicBool, + + /// The entity's yaw rotation (horizontal rotation) ← → + pub yaw: AtomicCell, + /// The entity's head yaw rotation (horizontal rotation of the head) + pub head_yaw: AtomicCell, + /// The entity's pitch rotation (vertical rotation) ↑ ↓ + pub pitch: AtomicCell, + /// The height of the entity's eyes from the ground. pub standing_eye_height: f32, - pub pose: EntityPose, + /// The entity's current pose (e.g., standing, sitting, swimming). + pub pose: AtomicCell, } -// TODO: Remove client: &mut Client, world: Arc> bs impl Entity { pub fn new( entity_id: EntityId, - world: Arc>, + world: Arc, entity_type: EntityType, standing_eye_height: f32, ) -> Self { Self { entity_id, entity_type, - on_ground: false, - pos: Vector3::new(0.0, 0.0, 0.0), - block_pos: WorldPosition(Vector3::new(0, 0, 0)), - chunk_pos: Vector2::new(0, 0), - sneaking: false, + on_ground: AtomicBool::new(false), + pos: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), + block_pos: AtomicCell::new(WorldPosition(Vector3::new(0, 0, 0))), + chunk_pos: AtomicCell::new(Vector2::new(0, 0)), + sneaking: AtomicBool::new(false), world, - sprinting: false, - fall_flying: false, - yaw: 0.0, - head_yaw: 0.0, - pitch: 0.0, - velocity: Vector3::new(0.0, 0.0, 0.0), + // TODO: Load this from previous instance + sprinting: AtomicBool::new(false), + fall_flying: AtomicBool::new(false), + yaw: AtomicCell::new(0.0), + head_yaw: AtomicCell::new(0.0), + pitch: AtomicCell::new(0.0), + velocity: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), standing_eye_height, - pose: EntityPose::Standing, + pose: AtomicCell::new(EntityPose::Standing), } } - pub fn set_pos(&mut self, x: f64, y: f64, z: f64) { - if self.pos.x != x || self.pos.y != y || self.pos.z != z { - self.pos = Vector3::new(x, y, z); + /// Updates the entity's position, block position, and chunk position. + /// + /// This function calculates the new position, block position, and chunk position based on the provided coordinates. If any of these values change, the corresponding fields are updated. + pub fn set_pos(&self, x: f64, y: f64, z: f64) { + let pos = self.pos.load(); + if pos.x != x || pos.y != y || pos.z != z { + self.pos.store(Vector3::new(x, y, z)); let i = x.floor() as i32; let j = y.floor() as i32; let k = z.floor() as i32; - let block_pos = self.block_pos.0; - if i != block_pos.x || j != block_pos.y || k != block_pos.z { - self.block_pos = WorldPosition(Vector3::new(i, j, k)); + let block_pos = self.block_pos.load(); + let block_pos_vec = block_pos.0; + if i != block_pos_vec.x || j != block_pos_vec.y || k != block_pos_vec.z { + self.block_pos.store(WorldPosition(Vector3::new(i, j, k))); - if get_section_cord(i) != self.chunk_pos.x - || get_section_cord(k) != self.chunk_pos.z - { - self.chunk_pos = - Vector2::new(get_section_cord(block_pos.x), get_section_cord(block_pos.z)); + let chunk_pos = self.chunk_pos.load(); + if get_section_cord(i) != chunk_pos.x || get_section_cord(k) != chunk_pos.z { + self.chunk_pos.store(Vector2::new( + get_section_cord(block_pos_vec.x), + get_section_cord(block_pos_vec.z), + )); } } } } - pub fn knockback(&mut self, strength: f64, x: f64, z: f64) { + /// Sets the Entity yaw & pitch Rotation + pub fn set_rotation(&self, yaw: f32, pitch: f32) { + // TODO + self.yaw.store(yaw); + self.pitch.store(pitch); + } + + /// Removes the Entity from their current World + pub fn remove(&self) { + self.world.remove_entity(self); + } + + /// Applies knockback to the entity, following vanilla Minecraft's mechanics. + /// + /// This function calculates the entity's new velocity based on the specified knockback strength and direction. + pub fn knockback(&self, strength: f64, x: f64, z: f64) { // This has some vanilla magic let mut x = x; let mut z = z; - while x * x + z * z < 1.0E-5 { + while x.mul_add(x, z * z) < 1.0E-5 { x = (rand::random::() - rand::random::()) * 0.01; z = (rand::random::() - rand::random::()) * 0.01; } let var8 = Vector3::new(x, 0.0, z).normalize() * strength; - let var7 = self.velocity; - self.velocity = Vector3::new( - var7.x / 2.0 - var8.x, - if self.on_ground { - (var7.y / 2.0 + strength).min(0.4) + let velocity = self.velocity.load(); + self.velocity.store(Vector3::new( + velocity.x / 2.0 - var8.x, + if self.on_ground.load(std::sync::atomic::Ordering::Relaxed) { + (velocity.y / 2.0 + strength).min(0.4) } else { - var7.y + velocity.y }, - var7.z / 2.0 - var8.z, - ); + velocity.z / 2.0 - var8.z, + )); } - pub async fn set_sneaking(&mut self, client: &mut Client, sneaking: bool) { - assert!(self.sneaking != sneaking); - self.sneaking = sneaking; - self.set_flag(client, Self::SNEAKING_FLAG_INDEX, sneaking) - .await; + pub async fn set_sneaking(&self, sneaking: bool) { + assert!(self.sneaking.load(std::sync::atomic::Ordering::Relaxed) != sneaking); + self.sneaking + .store(sneaking, std::sync::atomic::Ordering::Relaxed); + self.set_flag(Flag::Sneaking, sneaking).await; // if sneaking { // self.set_pose(EntityPose::Crouching).await; // } else { @@ -121,32 +163,26 @@ impl Entity { // } } - pub async fn set_sprinting(&mut self, client: &mut Client, sprinting: bool) { - assert!(self.sprinting != sprinting); - self.sprinting = sprinting; - self.set_flag(client, Self::SPRINTING_FLAG_INDEX, sprinting) - .await; + pub async fn set_sprinting(&self, sprinting: bool) { + assert!(self.sprinting.load(std::sync::atomic::Ordering::Relaxed) != sprinting); + self.sprinting + .store(sprinting, std::sync::atomic::Ordering::Relaxed); + self.set_flag(Flag::Sprinting, sprinting).await; } pub fn check_fall_flying(&self) -> bool { - !self.on_ground + !self.on_ground.load(std::sync::atomic::Ordering::Relaxed) } - pub async fn set_fall_flying(&mut self, client: &mut Client, fall_flying: bool) { - assert!(self.fall_flying != fall_flying); - self.fall_flying = fall_flying; - self.set_flag(client, Self::FALL_FLYING_FLAG_INDEX, fall_flying) - .await; + pub async fn set_fall_flying(&self, fall_flying: bool) { + assert!(self.fall_flying.load(std::sync::atomic::Ordering::Relaxed) != fall_flying); + self.fall_flying + .store(fall_flying, std::sync::atomic::Ordering::Relaxed); + self.set_flag(Flag::FallFlying, fall_flying).await; } - pub const ON_FIRE_FLAG_INDEX: u32 = 0; - pub const SNEAKING_FLAG_INDEX: u32 = 1; - pub const SPRINTING_FLAG_INDEX: u32 = 3; - pub const SWIMMING_FLAG_INDEX: u32 = 4; - pub const INVISIBLE_FLAG_INDEX: u32 = 5; - pub const GLOWING_FLAG_INDEX: u32 = 6; - pub const FALL_FLYING_FLAG_INDEX: u32 = 7; - async fn set_flag(&mut self, client: &mut Client, index: u32, value: bool) { + async fn set_flag(&self, flag: Flag, value: bool) { + let index = flag.to_u32().unwrap(); let mut b = 0i8; if value { b |= 1 << index; @@ -154,24 +190,41 @@ impl Entity { b &= !(1 << index); } let packet = CSetEntityMetadata::new(self.entity_id.into(), Metadata::new(0, 0.into(), b)); - client.send_packet(&packet); - self.world - .lock() - .await - .broadcast_packet(&[client.token], &packet); + self.world.broadcast_packet_all(&packet); } - pub async fn set_pose(&mut self, client: &mut Client, pose: EntityPose) { - self.pose = pose; - let pose = self.pose as i32; + pub async fn set_pose(&self, pose: EntityPose) { + self.pose.store(pose); + let pose = pose as i32; let packet = CSetEntityMetadata::::new( self.entity_id.into(), Metadata::new(6, 20.into(), (pose).into()), ); - client.send_packet(&packet); - self.world - .lock() - .await - .broadcast_packet(&[client.token], &packet) + self.world.broadcast_packet_all(&packet) } } + +#[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)] +/// Represents various entity flags that are sent in entity metadata. +/// +/// These flags are used by the client to modify the rendering of entities based on their current state. +/// +/// **Purpose:** +/// +/// This enum provides a more type-safe and readable way to represent entity flags compared to using raw integer values. +pub enum Flag { + /// Indicates if the entity is on fire. + OnFire, + /// Indicates if the entity is sneaking. + Sneaking, + /// Indicates if the entity is sprinting. + Sprinting, + /// Indicates if the entity is swimming. + Swimming, + /// Indicates if the entity is invisible. + Invisible, + /// Indicates if the entity is glowing. + Glowing, + /// Indicates if the entity is flying due to a fall. + FallFlying, +} diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 3b7ce24e2..b824275de 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -1,7 +1,12 @@ -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicI32, AtomicU8}, + Arc, +}; +use crossbeam::atomic::AtomicCell; use num_derive::FromPrimitive; use num_traits::ToPrimitive; +use parking_lot::Mutex; use pumpkin_core::{ math::{boundingbox::BoundingBox, position::WorldPosition, vector3::Vector3}, text::TextComponent, @@ -12,8 +17,8 @@ use pumpkin_inventory::player::PlayerInventory; use pumpkin_protocol::{ bytebuf::packet_id::Packet, client::play::{ - CGameEvent, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, CSyncPlayerPosition, - CSystemChatMessage, PlayerAction, + CGameEvent, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, CSetHealth, + CSyncPlayerPosition, CSystemChatMessage, GameEvent, PlayerAction, }, server::play::{ SChatCommand, SChatMessage, SClickContainer, SClientInformationPlay, SConfirmTeleport, @@ -21,10 +26,10 @@ use pumpkin_protocol::{ SPlayerPositionRotation, SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUseItem, SUseItemOn, }, - ConnectionState, RawPacket, ServerPacket, VarInt, + RawPacket, ServerPacket, VarInt, }; -use pumpkin_protocol::server::play::SCloseContainer; +use pumpkin_protocol::server::play::{SCloseContainer, SKeepAlive}; use pumpkin_world::item::ItemStack; use super::Entity; @@ -35,68 +40,65 @@ use crate::{ world::World, }; -pub struct PlayerAbilities { - pub invulnerable: bool, - pub flying: bool, - pub allow_flying: bool, - pub creative: bool, - pub fly_speed: f32, - pub walk_speed_fov: f32, -} - -impl Default for PlayerAbilities { - fn default() -> Self { - Self { - invulnerable: false, - flying: false, - allow_flying: false, - creative: false, - fly_speed: 0.5, - walk_speed_fov: 0.1, - } - } -} +use super::{living::LivingEntity, Entity}; +/// Represents a Minecraft player entity. +/// +/// A `Player` is a special type of entity that represents a human player connected to the server. pub struct Player { - pub entity: Entity, - + /// The underlying living entity object that represents the player. + pub living_entity: LivingEntity, + /// The player's game profile information, including their username and UUID. pub gameprofile: GameProfile, - pub client: Client, - pub config: PlayerConfig, - /// Current gamemode - pub gamemode: GameMode, - // TODO: prbly should put this into an Living Entitiy or something - pub health: f32, - pub food: i32, - pub food_saturation: f32, - pub inventory: PlayerInventory, - pub open_container: Option, - pub carried_item: Option, + /// The client connection associated with the player. + pub client: Arc, + /// The player's configuration settings. Changes when the Player changes their settings. + pub config: Mutex, + /// The player's current gamemode (e.g., Survival, Creative, Adventure). + pub gamemode: AtomicCell, + /// The player's hunger level. + pub food: AtomicI32, + /// The player's food saturation level. + pub food_saturation: AtomicCell, + /// The player's inventory, containing items and equipment. + pub inventory: Mutex, + /// The ID of the currently open container (if any). + pub open_container: AtomicCell>, + /// The item currently being held by the player. + pub carried_item: AtomicCell>, /// send `send_abilties_update` when changed + /// The player's abilities and special powers. + /// + /// This field represents the various abilities that the player possesses, such as flight, invulnerability, and other special effects. + /// + /// **Note:** When the `abilities` field is updated, the server should send a `send_abilities_update` packet to the client to notify them of the changes. pub abilities: PlayerAbilities, - pub last_position: Vector3, + /// The player's last known position. + /// + /// This field is used to calculate the player's movement delta for network synchronization and other purposes. + pub last_position: AtomicCell>, - // TODO: This is currently unused, We have to calculate the block breaking speed our own and then break the block our own if its done - pub current_block_destroy_stage: u8, + /// The current stage of the block the player is breaking. + pub current_block_destroy_stage: AtomicU8, + /// A counter for teleport IDs used to track pending teleports. + pub teleport_id_count: AtomicI32, + /// The pending teleport information, including the teleport ID and target location. + pub awaiting_teleport: Mutex)>>, - pub teleport_id_count: i32, - // Current awaiting teleport id and location, None if did not teleport - pub awaiting_teleport: Option<(VarInt, Vector3)>, - - pub watched_section: Vector3, + /// The coordinates of the chunk section the player is currently watching. + pub watched_section: AtomicCell>, } impl Player { pub fn new( - client: Client, - world: Arc>, + client: Arc, + world: Arc, entity_id: EntityId, gamemode: GameMode, ) -> Self { - let gameprofile = match client.gameprofile.clone() { - Some(profile) => profile, - None => { + let gameprofile = client.gameprofile.lock().clone().map_or_else( + || { log::error!("No gameprofile?. Impossible"); GameProfile { id: uuid::Uuid::new_v4(), @@ -104,41 +106,46 @@ impl Player { properties: vec![], profile_actions: None, } - } - }; - let config = client.config.clone().unwrap_or_default(); + }, + |profile| profile, + ); + let config = client.config.lock().clone().unwrap_or_default(); Self { - entity: Entity::new(entity_id, world, EntityType::Player, 1.62), - config, + living_entity: LivingEntity::new(Entity::new( + entity_id, + world, + EntityType::Player, + 1.62, + )), + config: Mutex::new(config), gameprofile, client, - awaiting_teleport: None, + awaiting_teleport: Mutex::new(None), // TODO: Load this from previous instance - health: 20.0, - food: 20, - food_saturation: 20.0, - current_block_destroy_stage: 0, - inventory: PlayerInventory::new(), - open_container: None, - carried_item: None, - teleport_id_count: 0, + food: AtomicI32::new(20), + food_saturation: AtomicCell::new(20.0), + current_block_destroy_stage: AtomicU8::new(0), + inventory: Mutex::new(PlayerInventory::new()), + open_container: AtomicCell::new(None), + carried_item: AtomicCell::new(None), + teleport_id_count: AtomicI32::new(0), abilities: PlayerAbilities::default(), - gamemode, - watched_section: Vector3::new(0, 0, 0), - last_position: Vector3::new(0.0, 0.0, 0.0), + gamemode: AtomicCell::new(gamemode), + watched_section: AtomicCell::new(Vector3::new(0, 0, 0)), + last_position: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), } } - // TODO: Put this into entity /// Removes the Player out of the current World - pub async fn remove(&mut self) { - self.entity.world.lock().await.remove_player(self); + pub async fn remove(&self) { + self.living_entity.entity.world.remove_player(self); } - pub fn entity_id(&self) -> EntityId { - self.entity.entity_id + pub const fn entity_id(&self) -> EntityId { + self.living_entity.entity.entity_id } + /// Updates the current abilities the Player has pub fn send_abilties_update(&mut self) { let mut b = 0i8; let abilities = &self.abilities; @@ -162,17 +169,21 @@ impl Player { )); } - pub fn teleport(&mut self, x: f64, y: f64, z: f64, yaw: f32, pitch: f32) { + pub fn teleport(&self, x: f64, y: f64, z: f64, yaw: f32, pitch: f32) { // this is the ultra special magic code used to create the teleport id - self.teleport_id_count += 1; - if self.teleport_id_count == i32::MAX { - self.teleport_id_count = 0; + // This returns the old value + let i = self + .teleport_id_count + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + if i + 2 == i32::MAX { + self.teleport_id_count + .store(0, std::sync::atomic::Ordering::Relaxed); } - let entity = &mut self.entity; + let teleport_id = i + 1; + let entity = &self.living_entity.entity; entity.set_pos(x, y, z); - entity.yaw = yaw; - entity.pitch = pitch; - self.awaiting_teleport = Some((self.teleport_id_count.into(), Vector3::new(x, y, z))); + entity.set_rotation(yaw, pitch); + *self.awaiting_teleport.lock() = Some((teleport_id.into(), Vector3::new(x, y, z))); self.client.send_packet(&CSyncPlayerPosition::new( x, y, @@ -180,12 +191,12 @@ impl Player { yaw, pitch, 0, - self.teleport_id_count.into(), + teleport_id.into(), )); } pub fn block_interaction_range(&self) -> f64 { - if self.gamemode == GameMode::Creative { + if self.gamemode.load() == GameMode::Creative { 5.0 } else { 4.5 @@ -195,17 +206,21 @@ impl Player { pub fn can_interact_with_block_at(&self, pos: &WorldPosition, additional_range: f64) -> bool { let d = self.block_interaction_range() + additional_range; let box_pos = BoundingBox::from_block(pos); + let entity_pos = self.living_entity.entity.pos.load(); + let standing_eye_height = self.living_entity.entity.standing_eye_height; box_pos.squared_magnitude(Vector3 { - x: self.entity.pos.x, - y: self.entity.pos.y + self.entity.standing_eye_height as f64, - z: self.entity.pos.z, + x: entity_pos.x, + y: entity_pos.y + standing_eye_height as f64, + z: entity_pos.z, }) < d * d } /// Kicks the Client with a reason depending on the connection state - pub fn kick(&mut self, reason: TextComponent) { - assert!(self.client.connection_state == ConnectionState::Play); - assert!(!self.client.closed); + pub fn kick(&self, reason: TextComponent) { + assert!(!self + .client + .closed + .load(std::sync::atomic::Ordering::Relaxed)); self.client .try_send_packet(&CPlayDisconnect::new(&reason)) @@ -218,53 +233,50 @@ impl Player { self.client.close() } - pub fn update_health(&mut self, health: f32, food: i32, food_saturation: f32) { - self.health = health; - self.food = food; - self.food_saturation = food_saturation; + pub fn set_health(&self, health: f32, food: i32, food_saturation: f32) { + self.living_entity.set_health(health); + self.food.store(food, std::sync::atomic::Ordering::Relaxed); + self.food_saturation.store(food_saturation); + self.client + .send_packet(&CSetHealth::new(health, food.into(), food_saturation)); } - pub async fn set_gamemode(&mut self, gamemode: GameMode) { + pub fn set_gamemode(&self, gamemode: GameMode) { // We could send the same gamemode without problems. But why waste bandwidth ? + let current_gamemode = self.gamemode.load(); assert!( - self.gamemode != gamemode, + current_gamemode != gamemode, "Setting the same gamemode as already is" ); - self.gamemode = gamemode; - // So a little story time. I actually made an abitlties_from_gamemode function. I looked at vanilla and they always send the abilties from the gamemode. But the funny thing actually is. That the client + self.gamemode.store(gamemode); + // So a little story time. I actually made an abilties_from_gamemode function. I looked at vanilla and they always send the abilties from the gamemode. But the funny thing actually is. That the client // does actually use the same method and set the abilties when receiving the CGameEvent gamemode packet. Just Mojang nonsense - - // TODO: fix this ugly mess :c, It gives me a liftime error when saving packet as a var - self.client.send_packet(&CPlayerInfoUpdate::new( - 0x04, - &[pumpkin_protocol::client::play::Player { - uuid: self.gameprofile.id, - actions: vec![PlayerAction::UpdateGameMode((self.gamemode as i32).into())], - }], - )); - self.entity.world.lock().await.broadcast_packet( - &[self.client.token], - &CPlayerInfoUpdate::new( + self.living_entity + .entity + .world + .broadcast_packet_all(&CPlayerInfoUpdate::new( 0x04, &[pumpkin_protocol::client::play::Player { uuid: self.gameprofile.id, - actions: vec![PlayerAction::UpdateGameMode((self.gamemode as i32).into())], + actions: vec![PlayerAction::UpdateGameMode((gamemode as i32).into())], }], - ), - ); - self.client - .send_packet(&CGameEvent::new(3, gamemode.to_f32().unwrap())); + )); + self.client.send_packet(&CGameEvent::new( + GameEvent::ChangeGameMode, + gamemode.to_f32().unwrap(), + )); } - pub fn send_system_message(&mut self, text: TextComponent) { + pub fn send_system_message(&self, text: TextComponent) { self.client .send_packet(&CSystemChatMessage::new(text, false)); } } impl Player { - pub async fn process_packets(&mut self, server: &Arc) { - while let Some(mut packet) = self.client.client_packets_queue.pop() { + pub async fn process_packets(&self, server: &Arc) { + let mut packets = self.client.client_packets_queue.lock(); + while let Some(mut packet) = packets.pop() { match self.handle_play_packet(server, &mut packet).await { Ok(_) => {} Err(e) => { @@ -285,7 +297,7 @@ impl Player { } pub async fn handle_play_packet( - &mut self, + &self, server: &Arc, packet: &mut RawPacket, ) -> Result<(), Box> { @@ -370,13 +382,20 @@ impl Player { } SClickContainer::PACKET_ID => { self.handle_click_container(server, SClickContainer::read(bytebuf)?) - .await?; Ok(()) } SCloseContainer::PACKET_ID => { self.handle_close_container(server, SCloseContainer::read(bytebuf)?); Ok(()) } + SKeepAlive::PACKET_ID => { + self.client + .keep_alive_sender + .send(SKeepAlive::read(bytebuf)?.keep_alive_id) + .await + .unwrap(); + Ok(()) + } _ => { log::error!("Failed to handle player packet id {:#04x}", packet.id.0); Ok(()) @@ -385,15 +404,53 @@ impl Player { } } +/// Represents a player's abilities and special powers. +/// +/// This struct contains information about the player's current abilities, such as flight, invulnerability, and creative mode. +pub struct PlayerAbilities { + /// Indicates whether the player is invulnerable to damage. + pub invulnerable: bool, + /// Indicates whether the player is currently flying. + pub flying: bool, + /// Indicates whether the player is allowed to fly (if enabled). + pub allow_flying: bool, + /// Indicates whether the player is in creative mode. + pub creative: bool, + /// The player's flying speed. + pub fly_speed: f32, + /// The field of view adjustment when the player is walking or sprinting. + pub walk_speed_fov: f32, +} + +impl Default for PlayerAbilities { + fn default() -> Self { + Self { + invulnerable: false, + flying: false, + allow_flying: false, + creative: false, + fly_speed: 0.5, + walk_speed_fov: 0.1, + } + } +} + +/// Represents the player's dominant hand. #[derive(FromPrimitive, Clone)] pub enum Hand { + /// The player's primary hand (usually the right hand). Main, + /// The player's off-hand (usually the left hand). Off, } +/// Represents the player's chat mode settings. #[derive(FromPrimitive, Clone)] pub enum ChatMode { + /// Chat is enabled for the player. Enabled, + /// The player should only see chat messages from commands CommandsOnly, + /// All messages should be hidden Hidden, } diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index c28cfe5e7..b0aa22468 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -1,16 +1,32 @@ +#![deny(clippy::all)] +// #![warn(clippy::pedantic)] +// #![warn(clippy::restriction)] +#![warn(clippy::nursery)] +#![warn(clippy::cargo)] +// expect +#![expect(clippy::cargo_common_metadata)] +#![expect(clippy::multiple_crate_versions)] +#![expect(clippy::while_float)] +#![expect(clippy::significant_drop_in_scrutinee)] +#![expect(clippy::significant_drop_tightening)] +#![expect(clippy::future_not_send)] +#![expect(clippy::single_call_fn)] #![expect(clippy::await_holding_lock)] #[cfg(target_os = "wasi")] compile_error!("Compiling for WASI targets is not supported!"); +use log::LevelFilter; use mio::net::TcpListener; use mio::{Events, Interest, Poll, Token}; -use std::collections::HashMap; -use std::io::{self}; - use client::{interrupted, Client}; +use pumpkin_protocol::client::play::CKeepAlive; +use pumpkin_protocol::ConnectionState; use server::Server; +use std::collections::HashMap; +use std::io::{self, Read}; +use std::time::Duration; // Setup some tokens to allow us to identify which event is for which socket. @@ -24,18 +40,47 @@ pub mod server; pub mod util; pub mod world; +fn init_logger() { + use pumpkin_config::ADVANCED_CONFIG; + if ADVANCED_CONFIG.logging.enabled { + let mut logger = simple_logger::SimpleLogger::new(); + + if !ADVANCED_CONFIG.logging.timestamp { + logger = logger.without_timestamps(); + } + + if ADVANCED_CONFIG.logging.env { + logger = logger.env(); + } + + logger = logger.with_level(convert_logger_filter(ADVANCED_CONFIG.logging.level)); + + logger = logger.with_colors(ADVANCED_CONFIG.logging.color); + logger = logger.with_threads(ADVANCED_CONFIG.logging.threads); + logger.init().unwrap() + } +} + +const fn convert_logger_filter(level: pumpkin_config::logging::LevelFilter) -> LevelFilter { + match level { + pumpkin_config::logging::LevelFilter::Off => LevelFilter::Off, + pumpkin_config::logging::LevelFilter::Error => LevelFilter::Error, + pumpkin_config::logging::LevelFilter::Warn => LevelFilter::Warn, + pumpkin_config::logging::LevelFilter::Info => LevelFilter::Info, + pumpkin_config::logging::LevelFilter::Debug => LevelFilter::Debug, + pumpkin_config::logging::LevelFilter::Trace => LevelFilter::Trace, + } +} + fn main() -> io::Result<()> { - use std::sync::{Arc, Mutex}; + use std::sync::Arc; use entity::player::Player; use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG}; use pumpkin_core::text::{color::NamedColor, TextComponent}; use rcon::RCONServer; - simple_logger::SimpleLogger::new() - .with_level(log::LevelFilter::Info) - .init() - .unwrap(); + init_logger(); let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -79,8 +124,8 @@ fn main() -> io::Result<()> { let use_console = ADVANCED_CONFIG.commands.use_console; let rcon = ADVANCED_CONFIG.rcon.clone(); - let mut clients: HashMap = HashMap::new(); - let mut players: HashMap>> = HashMap::new(); + let mut clients: HashMap> = HashMap::new(); + let mut players: HashMap> = HashMap::new(); let server = Arc::new(Server::new()); log::info!("Started Server took {}ms", time.elapsed().as_millis()); @@ -153,49 +198,90 @@ fn main() -> io::Result<()> { token, Interest::READABLE.add(Interest::WRITABLE), )?; - let client = Client::new(token, connection, addr); + let keep_alive = tokio::sync::mpsc::channel(1024); + let client = + Arc::new(Client::new(token, connection, addr, keep_alive.0.into())); + + { + let client = client.clone(); + let mut receiver = keep_alive.1; + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(1)); + loop { + interval.tick().await; + let now = std::time::Instant::now(); + if client.connection_state.load() == ConnectionState::Play { + if now.duration_since(client.last_alive_received.load()) + >= Duration::from_secs(15) + { + dbg!("no keep alive"); + client.kick("No keep alive received"); + break; + } + let random = rand::random::(); + client.send_packet(&CKeepAlive { + keep_alive_id: random, + }); + if let Some(id) = receiver.recv().await { + if id == random { + client.last_alive_received.store(now); + } + } + } else { + client.last_alive_received.store(now); + } + } + }); + } clients.insert(token, client); }, - + // Maybe received an event for a TCP connection. token => { // Poll Players if let Some(player) = players.get_mut(&token) { - let mut player = player.lock().unwrap(); player.client.poll(event).await; - if !player.client.closed { + let closed = player + .client + .closed + .load(std::sync::atomic::Ordering::Relaxed); + if !closed { player.process_packets(&server).await; } - if player.client.closed { - drop(player); + if closed { if let Some(player) = players.remove(&token) { - let mut player = player.lock().unwrap(); player.remove().await; - poll.registry().deregister(&mut player.client.connection)?; + let connection = &mut player.client.connection.lock(); + poll.registry().deregister(connection.by_ref())?; } } }; // Poll current Clients (non players) - // Maybe received an event for a TCP connection. let (done, make_player) = if let Some(client) = clients.get_mut(&token) { client.poll(event).await; - if !client.closed { + let closed = client.closed.load(std::sync::atomic::Ordering::Relaxed); + if !closed { client.process_packets(&server).await; } - (client.closed, client.make_player) + ( + closed, + client + .make_player + .load(std::sync::atomic::Ordering::Relaxed), + ) } else { // Sporadic events happen, we can safely ignore them. (false, false) }; if done || make_player { - if let Some(mut client) = clients.remove(&token) { + if let Some(client) = clients.remove(&token) { if done { - poll.registry().deregister(&mut client.connection)?; + let connection = &mut client.connection.lock(); + poll.registry().deregister(connection.by_ref())?; } else if make_player { let token = client.token; let (player, world) = server.add_player(token, client).await; players.insert(token, player.clone()); - let mut world = world.lock().await; world.spawn_player(&BASIC_CONFIG, player).await; } } diff --git a/pumpkin/src/proxy/velocity.rs b/pumpkin/src/proxy/velocity.rs index b23e17082..e97e84c97 100644 --- a/pumpkin/src/proxy/velocity.rs +++ b/pumpkin/src/proxy/velocity.rs @@ -15,7 +15,7 @@ type HmacSha256 = Hmac; const MAX_SUPPORTED_FORWARDING_VERSION: i32 = 4; const PLAYER_INFO_CHANNEL: &str = "velocity:player_info"; -pub fn velocity_login(client: &mut Client) { +pub fn velocity_login(client: &Client) { let velocity_message_id: i32 = 0; let mut buf = BytesMut::new(); @@ -36,7 +36,7 @@ pub fn check_integrity(data: (&[u8], &[u8]), secret: String) -> bool { } pub fn receive_plugin_response( - client: &mut Client, + client: &Client, config: VelocityConfig, response: SLoginPluginResponse, ) { @@ -62,7 +62,7 @@ pub fn receive_plugin_response( } // TODO: no unwrap let addr: SocketAddr = buf.get_string().unwrap().parse().unwrap(); - client.address = addr; + *client.address.lock() = addr; todo!() } else { client.kick("This server requires you to connect with Velocity.") diff --git a/pumpkin/src/rcon/mod.rs b/pumpkin/src/rcon/mod.rs index a2cf8dfc2..a508f941a 100644 --- a/pumpkin/src/rcon/mod.rs +++ b/pumpkin/src/rcon/mod.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, - io::{self, Read}, + io::{self, Read, Write}, + net::SocketAddr, sync::Arc, }; @@ -8,8 +9,8 @@ use mio::{ net::{TcpListener, TcpStream}, Events, Interest, Poll, Token, }; -use packet::{Packet, PacketError, PacketType}; -use pumpkin_config::RCONConfig; +use packet::{ClientboundPacket, Packet, PacketError, ServerboundPacket}; +use pumpkin_config::{RCONConfig, ADVANCED_CONFIG}; use thiserror::Error; use crate::server::Server; @@ -28,7 +29,7 @@ pub enum RCONError { const SERVER: Token = Token(0); -pub struct RCONServer {} +pub struct RCONServer; impl RCONServer { pub async fn new(config: &RCONConfig, server: Arc) -> Result { @@ -71,7 +72,11 @@ impl RCONServer { return Err(e); } }; - log::info!("Accepted connection from: {}", address); + if config.max_connections != 0 + && connections.len() >= config.max_connections as usize + { + break; + } let token = Self::next(&mut unique_token); poll.registry() @@ -81,7 +86,7 @@ impl RCONServer { Interest::READABLE.add(Interest::WRITABLE), ) .unwrap(); - connections.insert(token, RCONClient::new(connection)); + connections.insert(token, RCONClient::new(connection, address)); }, token => { @@ -92,6 +97,13 @@ impl RCONServer { }; if done { if let Some(mut client) = connections.remove(&token) { + let config = &ADVANCED_CONFIG.rcon; + if config.logging.log_quit { + log::info!( + "RCON ({}): Client closed connection", + client.address + ); + } poll.registry().deregister(&mut client.connection)?; } } @@ -110,15 +122,17 @@ impl RCONServer { pub struct RCONClient { connection: TcpStream, + address: SocketAddr, logged_in: bool, incoming: Vec, closed: bool, } impl RCONClient { - pub fn new(connection: TcpStream) -> Self { + pub const fn new(connection: TcpStream, address: SocketAddr) -> Self { Self { connection, + address, logged_in: false, incoming: Vec::new(), closed: false, @@ -140,13 +154,10 @@ impl RCONClient { } } // If we get a close here, we might have a reply, which we still want to write. - match self.poll(server, password).await { - Ok(()) => {} - Err(e) => { - log::error!("rcon error: {e}"); - self.closed = true; - } - } + let _ = self.poll(server, password).await.map_err(|e| { + log::error!("RCON error: {e}"); + self.closed = true; + }); } self.closed } @@ -158,28 +169,27 @@ impl RCONClient { None => return Ok(()), }; + let config = &ADVANCED_CONFIG.rcon; match packet.get_type() { - PacketType::Auth => { + ServerboundPacket::Auth => { let body = packet.get_body(); if !body.is_empty() && packet.get_body() == password { - self.send(&mut Packet::new( - packet.get_id(), - PacketType::AuthResponse, - "".into(), - )) - .await - .unwrap(); - log::info!("RCON Client logged in successfully"); + self.send(ClientboundPacket::AuthResponse, packet.get_id(), "".into()) + .await?; + if config.logging.log_logged_successfully { + log::info!("RCON ({}): Client logged in successfully", self.address); + } self.logged_in = true; } else { - log::warn!("RCON Client has tried wrong password"); - self.send(&mut Packet::new(-1, PacketType::AuthResponse, "".into())) - .await - .unwrap(); - return Err(PacketError::WrongPassword); + if config.logging.log_wrong_password { + log::info!("RCON ({}): Client has tried wrong password", self.address); + } + self.send(ClientboundPacket::AuthResponse, -1, "".into()) + .await?; + self.closed = true; } } - PacketType::ExecCommand => { + ServerboundPacket::ExecCommand => { if self.logged_in { let mut output = Vec::new(); let dispatcher = server.command_dispatcher.clone(); @@ -189,14 +199,14 @@ impl RCONClient { packet.get_body(), ); for line in output { - self.send(&mut Packet::new(packet.get_id(), PacketType::Output, line)) - .await - .unwrap(); + if config.logging.log_commands { + log::info!("RCON ({}): {}", self.address, line); + } + self.send(ClientboundPacket::Output, packet.get_id(), line) + .await?; } } } - PacketType::Output => todo!(), - PacketType::AuthResponse => unreachable!(), } } } @@ -211,8 +221,17 @@ impl RCONClient { Ok(false) } - async fn send(&mut self, packet: &mut Packet) -> io::Result<()> { - packet.send_packet(&mut self.connection).await + async fn send( + &mut self, + packet: ClientboundPacket, + id: i32, + body: String, + ) -> Result<(), PacketError> { + let buf = packet.write_buf(id, body); + self.connection + .write(&buf) + .map_err(PacketError::FailedSend)?; + Ok(()) } async fn receive_packet(&mut self) -> Result, PacketError> { diff --git a/pumpkin/src/rcon/packet.rs b/pumpkin/src/rcon/packet.rs index 7ed2a91a9..f4cdc7925 100644 --- a/pumpkin/src/rcon/packet.rs +++ b/pumpkin/src/rcon/packet.rs @@ -1,100 +1,107 @@ -use std::io::{self, BufRead, Cursor, Write}; +use std::{ + io::{BufRead, Cursor}, + string::FromUtf8Error, +}; use bytes::{BufMut, BytesMut}; -use mio::net::TcpStream; use thiserror::Error; use tokio::io::AsyncReadExt; -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PacketType { - Auth, - AuthResponse, - ExecCommand, - Output, +/// Client -> Server +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum ServerboundPacket { + /// Typically, the first packet sent by the client, which is used to authenticate the connection with the server. + Auth = 2, + /// This packet type represents a command issued to the server by a client. This can be a ConCommand such as kill or weather clear. + /// The response will vary depending on the command issued. + ExecCommand = 3, +} + +impl ServerboundPacket { + pub const fn from_i32(n: i32) -> Self { + match n { + 3 => Self::Auth, + 2 => Self::ExecCommand, + _ => Self::Auth, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +/// Server -> Client +pub enum ClientboundPacket { + /// This packet is a notification of the connection's current auth status. When the server receives an auth request, it will respond with an empty SERVERDATA_RESPONSE_VALUE, + /// followed immediately by a SERVERDATA_AUTH_RESPONSE indicating whether authentication succeeded or failed. Note that the status code is returned in the packet id field, so when pairing the response with the original auth request, you may need to look at the packet id of the preceeding SERVERDATA_RESPONSE_VALUE. + AuthResponse = 2, + /// A SERVERDATA_RESPONSE packet is the response to a SERVERDATA_EXECCOMMAND request. + Output = 0, +} + +impl ClientboundPacket { + pub fn write_buf(&self, id: i32, body: String) -> BytesMut { + // let len = outgoing.len() as u64; + let mut buf = BytesMut::new(); + // 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes. + buf.put_i32_le(10 + body.len() as i32); + buf.put_i32_le(id); + buf.put_i32_le(*self as i32); + let bytes = body.as_bytes(); + buf.put_slice(bytes); + buf.put_u8(0); + buf.put_u8(0); + buf + } } #[derive(Error, Debug)] pub enum PacketError { #[error("invalid length")] InvalidLength, - #[error("expected terminating NUL byte")] - NoNullTermination, - #[error("wrong password")] - WrongPassword, -} - -impl PacketType { - fn to_i32(self) -> i32 { - match self { - PacketType::Auth => 3, - PacketType::AuthResponse => 2, - PacketType::ExecCommand => 2, - PacketType::Output => 0, - } - } - - pub fn from_i32(n: i32) -> PacketType { - match n { - 3 => PacketType::Auth, - 2 => PacketType::ExecCommand, - _ => PacketType::Output, - } - } + #[error("failed to read packet")] + FailedRead(std::io::Error), + #[error("failed to send packet")] + FailedSend(std::io::Error), + #[error("invalid Packet String body")] + InvalidBody(FromUtf8Error), } #[derive(Debug)] +/// Serverbound Packet pub struct Packet { id: i32, - ptype: PacketType, + ptype: ServerboundPacket, body: String, } impl Packet { - pub fn new(id: i32, ptype: PacketType, body: String) -> Packet { - Packet { id, ptype, body } - } - - pub async fn send_packet(&mut self, connection: &mut TcpStream) -> io::Result<()> { - // let len = outgoing.len() as u64; - let mut buf = BytesMut::new(); - // 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes. - buf.put_i32_le(10 + self.get_body().len() as i32); - buf.put_i32_le(self.id); - buf.put_i32_le(self.get_type().to_i32()); - let bytes = self.get_body().as_bytes(); - buf.put_slice(bytes); - buf.put_u8(0); - buf.put_u8(0); - let _ = connection.write(&buf).unwrap(); - Ok(()) - } - - pub async fn deserialize(incoming: &mut Vec) -> Result, PacketError> { + pub async fn deserialize(incoming: &mut Vec) -> Result, PacketError> { if incoming.len() < 4 { return Ok(None); } let mut buf = Cursor::new(&incoming); - let len = buf.read_i32_le().await.unwrap() + 4; + let len = buf.read_i32_le().await.map_err(PacketError::FailedRead)? + 4; if !(0..=1460).contains(&len) { return Err(PacketError::InvalidLength); } - let id = buf.read_i32_le().await.unwrap(); - let ty = buf.read_i32_le().await.unwrap(); + let id = buf.read_i32_le().await.map_err(PacketError::FailedRead)?; + let ty = buf.read_i32_le().await.map_err(PacketError::FailedRead)?; let mut payload = vec![]; - let _ = buf.read_until(b'\0', &mut payload).unwrap(); + let _ = buf + .read_until(b'\0', &mut payload) + .map_err(PacketError::FailedRead)?; payload.pop(); - if buf.read_u8().await.unwrap() != 0 { - return Err(PacketError::NoNullTermination); - } + buf.read_u8().await.map_err(PacketError::FailedRead)?; if buf.position() != len as u64 { return Err(PacketError::InvalidLength); } incoming.drain(0..len as usize); - let packet = Packet { + let packet = Self { id, - ptype: PacketType::from_i32(ty), - body: String::from_utf8(payload).unwrap(), + ptype: ServerboundPacket::from_i32(ty), + body: String::from_utf8(payload).map_err(PacketError::InvalidBody)?, }; Ok(Some(packet)) @@ -103,11 +110,11 @@ impl Packet { &self.body } - pub fn get_type(&self) -> PacketType { + pub const fn get_type(&self) -> ServerboundPacket { self.ptype } - pub fn get_id(&self) -> i32 { + pub const fn get_id(&self) -> i32 { self.id } } diff --git a/pumpkin/src/server/connection_cache.rs b/pumpkin/src/server/connection_cache.rs new file mode 100644 index 000000000..c87091fc8 --- /dev/null +++ b/pumpkin/src/server/connection_cache.rs @@ -0,0 +1,102 @@ +use std::{fs::File, path::Path}; + +use base64::{engine::general_purpose, Engine as _}; +use pumpkin_config::{BasicConfiguration, BASIC_CONFIG}; +use pumpkin_protocol::{ + client::{config::CPluginMessage, status::CStatusResponse}, + Players, Sample, StatusResponse, VarInt, Version, CURRENT_MC_PROTOCOL, +}; + +use super::CURRENT_MC_VERSION; + +pub struct CachedStatus { + _status_response: StatusResponse, + // We cache the json response here so we don't parse it every time someone makes a Status request. + // Keep in mind that we must parse this again, when the StatusResponse changes which usually happen when a player joins or leaves + status_response_json: String, +} + +pub struct CachedBranding { + /// Cached Server brand buffer so we don't have to rebuild them every time a player joins + cached_server_brand: Vec, +} + +impl CachedBranding { + pub fn new() -> Self { + let cached_server_brand = Self::build_brand(); + Self { + cached_server_brand, + } + } + pub fn get_branding(&self) -> CPluginMessage { + CPluginMessage::new("minecraft:brand", &self.cached_server_brand) + } + fn build_brand() -> Vec { + let brand = "Pumpkin"; + let mut buf = vec![]; + let _ = VarInt(brand.len() as i32).encode(&mut buf); + buf.extend_from_slice(brand.as_bytes()); + buf + } +} + +impl CachedStatus { + pub fn new() -> Self { + let status_response = Self::build_response(&BASIC_CONFIG); + let status_response_json = serde_json::to_string(&status_response) + .expect("Failed to parse Status response into JSON"); + + Self { + _status_response: status_response, + status_response_json, + } + } + + pub fn get_status(&self) -> CStatusResponse<'_> { + CStatusResponse::new(&self.status_response_json) + } + + pub fn build_response(config: &BasicConfiguration) -> StatusResponse { + let icon_path = "/icon.png"; + let icon = if Path::new(icon_path).exists() { + Some(Self::load_icon(icon_path)) + } else { + None + }; + + StatusResponse { + version: Some(Version { + name: CURRENT_MC_VERSION.into(), + protocol: CURRENT_MC_PROTOCOL, + }), + players: Some(Players { + max: config.max_players, + online: 0, + sample: vec![Sample { + name: "".into(), + id: "".into(), + }], + }), + description: config.motd.clone(), + favicon: icon, + enforce_secure_chat: false, + } + } + + fn load_icon(path: &str) -> String { + let icon = png::Decoder::new(File::open(path).expect("Failed to load icon")); + let mut reader = icon.read_info().unwrap(); + let info = reader.info(); + assert!(info.width == 64, "Icon width must be 64"); + assert!(info.height == 64, "Icon height must be 64"); + // Allocate the output buffer. + let mut buf = vec![0; reader.output_buffer_size()]; + // Read the next frame. An APNG might contain multiple frames. + let info = reader.next_frame(&mut buf).unwrap(); + // Grab the bytes of the image. + let bytes = &buf[..info.buffer_size()]; + let mut result = "data:image/png;base64,".to_owned(); + general_purpose::STANDARD.encode_string(bytes, &mut result); + result + } +} diff --git a/pumpkin/src/server/key_store.rs b/pumpkin/src/server/key_store.rs new file mode 100644 index 000000000..12bfb829c --- /dev/null +++ b/pumpkin/src/server/key_store.rs @@ -0,0 +1,74 @@ +use num_bigint::BigInt; +use pumpkin_protocol::client::login::CEncryptionRequest; +use rsa::{traits::PublicKeyParts as _, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; +use sha1::Sha1; +use sha2::Digest; + +use crate::client::EncryptionError; + +pub struct KeyStore { + pub _public_key: RsaPublicKey, + pub private_key: RsaPrivateKey, + pub public_key_der: Box<[u8]>, +} + +impl KeyStore { + pub fn new() -> Self { + log::debug!("Creating encryption keys..."); + let (public_key, private_key) = Self::generate_keys(); + + let public_key_der = rsa_der::public_key_to_der( + &private_key.n().to_bytes_be(), + &private_key.e().to_bytes_be(), + ) + .into_boxed_slice(); + Self { + _public_key: public_key, + private_key, + public_key_der, + } + } + + fn generate_keys() -> (RsaPublicKey, RsaPrivateKey) { + let mut rng = rand::thread_rng(); + + let priv_key = RsaPrivateKey::new(&mut rng, 1024).expect("failed to generate a key"); + let pub_key = RsaPublicKey::from(&priv_key); + (pub_key, priv_key) + } + + pub fn encryption_request<'a>( + &'a self, + server_id: &'a str, + verification_token: &'a [u8; 4], + should_authenticate: bool, + ) -> CEncryptionRequest<'_> { + CEncryptionRequest::new( + server_id, + &self.public_key_der, + verification_token, + should_authenticate, + ) + } + + pub fn decrypt(&self, data: &[u8]) -> Result, EncryptionError> { + let decrypted = self + .private_key + .decrypt(Pkcs1v15Encrypt, data) + .map_err(|_| EncryptionError::FailedDecrypt)?; + Ok(decrypted) + } + + pub fn get_digest(&self, secret: &[u8]) -> String { + auth_digest( + &Sha1::new() + .chain_update(secret) + .chain_update(&self.public_key_der) + .finalize(), + ) + } +} + +pub fn auth_digest(bytes: &[u8]) -> String { + BigInt::from_signed_bytes_be(bytes).to_str_radix(16) +} diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index d9aaf61a3..4f9ada322 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -1,32 +1,28 @@ -use base64::{engine::general_purpose, Engine}; -use image::GenericImageView; +use connection_cache::{CachedBranding, CachedStatus}; +use key_store::KeyStore; use mio::Token; -use pumpkin_config::{BasicConfiguration, BASIC_CONFIG}; +use parking_lot::{Mutex, RwLock}; +use pumpkin_config::BASIC_CONFIG; use pumpkin_core::GameMode; use pumpkin_entity::EntityId; +use pumpkin_inventory::drag_handler::DragHandler; +use pumpkin_inventory::{Container, OpenContainer}; use pumpkin_plugin::PluginLoader; -use pumpkin_protocol::{ - client::config::CPluginMessage, ClientPacket, Players, Sample, StatusResponse, VarInt, Version, - CURRENT_MC_PROTOCOL, -}; +use pumpkin_protocol::client::login::CEncryptionRequest; +use pumpkin_protocol::client::status::CStatusResponse; +use pumpkin_protocol::{client::config::CPluginMessage, ClientPacket}; +use pumpkin_registry::Registry; use pumpkin_world::dimension::Dimension; use std::collections::HashMap; -use std::sync::RwLock; use std::{ - io::Cursor, - path::Path, sync::{ atomic::{AtomicI32, Ordering}, - Arc, Mutex, + Arc, }, time::Duration, }; -use pumpkin_inventory::drag_handler::DragHandler; -use pumpkin_inventory::{Container, OpenContainer}; -use pumpkin_registry::Registry; -use rsa::{traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey}; - +use crate::client::EncryptionError; use crate::{ client::Client, commands::{default_dispatcher, dispatcher::CommandDispatcher}, @@ -34,25 +30,18 @@ use crate::{ world::World, }; +mod connection_cache; +mod key_store; pub const CURRENT_MC_VERSION: &str = "1.21.1"; pub struct Server { - pub public_key: RsaPublicKey, - pub private_key: RsaPrivateKey, - pub public_key_der: Box<[u8]>, - + key_store: KeyStore, + server_listing: CachedStatus, + server_branding: CachedBranding, pub plugin_loader: PluginLoader, pub command_dispatcher: Arc>, - - pub worlds: Vec>>, - pub status_response: StatusResponse, - // We cache the json response here so we don't parse it every time someone makes a Status request. - // Keep in mind that we must parse this again, when the StatusResponse changes which usally happen when a player joins or leaves - pub status_response_json: String, - - /// Cache the Server brand buffer so we don't have to rebuild them every time a player joins - pub cached_server_brand: Vec, + pub worlds: Vec>, /// Cache the registry so we don't have to parse it every time a player joins pub cached_registry: Vec, @@ -68,20 +57,8 @@ pub struct Server { impl Server { #[allow(clippy::new_without_default)] pub fn new() -> Self { - let status_response = Self::build_response(&BASIC_CONFIG); - let status_response_json = serde_json::to_string(&status_response) - .expect("Failed to parse Status response into JSON"); - let cached_server_brand = Self::build_brand(); - // TODO: only create when needed - log::debug!("Creating encryption keys..."); - let (public_key, private_key) = Self::generate_keys(); - let public_key_der = rsa_der::public_key_to_der( - &private_key.n().to_bytes_be(), - &private_key.e().to_bytes_be(), - ) - .into_boxed_slice(); let auth_client = if BASIC_CONFIG.online_mode { Some( reqwest::Client::builder() @@ -109,40 +86,28 @@ impl Server { drag_handler: DragHandler::new(), // 0 is invalid entity_id: 2.into(), - worlds: vec![Arc::new(tokio::sync::Mutex::new(world))], - public_key, - cached_server_brand, - private_key, + worlds: vec![Arc::new(world)], command_dispatcher: Arc::new(command_dispatcher), - status_response, - status_response_json, - public_key_der, auth_client, + key_store: KeyStore::new(), + server_listing: CachedStatus::new(), + server_branding: CachedBranding::new(), } } - pub async fn add_player( - &self, - token: Token, - client: Client, - ) -> (Arc>, Arc>) { + pub async fn add_player(&self, token: Token, client: Arc) -> (Arc, Arc) { let entity_id = self.new_entity_id(); let gamemode = match BASIC_CONFIG.default_gamemode { GameMode::Undefined => GameMode::Survival, game_mode => game_mode, }; - // Basicly the default world + // Basically the default world // TODO: select default from config - let world = self.worlds[0].clone(); + let world = &self.worlds[0]; - let player = Arc::new(Mutex::new(Player::new( - client, - world.clone(), - entity_id, - gamemode, - ))); - world.lock().await.add_player(token, player.clone()); - (player, world) + let player = Arc::new(Player::new(client, world.clone(), entity_id, gamemode)); + world.add_player(token, player.clone()); + (player, world.clone()) } pub fn try_get_container( @@ -150,10 +115,7 @@ impl Server { player_id: EntityId, container_id: u64, ) -> Option>>> { - let open_containers = self - .open_containers - .read() - .expect("open_containers is poisoned"); + let open_containers = self.open_containers.read(); open_containers .get(&container_id)? .try_open(player_id) @@ -161,83 +123,53 @@ impl Server { } /// Sends a Packet to all Players in all worlds - pub fn broadcast_packet_all

(&self, except: &[Token], packet: &P) + pub fn broadcast_packet_all

(&self, packet: &P) where P: ClientPacket, { for world in &self.worlds { - world.blocking_lock().broadcast_packet(except, packet) + world.broadcast_packet_all(packet) } } - // move to world + /// Searches every world for a player by name + pub fn get_player_by_name(&self, name: &str) -> Option> { + for world in self.worlds.iter() { + if let Some(player) = world.get_player_by_name(name) { + return Some(player); + } + } + None + } + + /// Generates a new entity id + /// This should be global pub fn new_entity_id(&self) -> EntityId { self.entity_id.fetch_add(1, Ordering::SeqCst) } - pub fn build_brand() -> Vec { - let brand = "Pumpkin"; - let mut buf = vec![]; - let _ = VarInt(brand.len() as i32).encode(&mut buf); - buf.extend_from_slice(brand.as_bytes()); - buf + pub fn get_branding(&self) -> CPluginMessage<'_> { + self.server_branding.get_branding() } - pub fn send_brand(&self, client: &mut Client) { - // send server brand - client.send_packet(&CPluginMessage::new( - "minecraft:brand", - &self.cached_server_brand, - )); + pub fn get_status(&self) -> CStatusResponse<'_> { + self.server_listing.get_status() } - pub fn build_response(config: &BasicConfiguration) -> StatusResponse { - let icon_path = concat!(env!("CARGO_MANIFEST_DIR"), "/icon.png"); - let icon = if Path::new(icon_path).exists() { - Some(Self::load_icon(icon_path)) - } else { - None - }; - - StatusResponse { - version: Version { - name: CURRENT_MC_VERSION.into(), - protocol: CURRENT_MC_PROTOCOL, - }, - players: Players { - max: config.max_players, - online: 0, - sample: vec![Sample { - name: "".into(), - id: "".into(), - }], - }, - description: config.motd.clone(), - favicon: icon, - } + pub fn encryption_request<'a>( + &'a self, + verification_token: &'a [u8; 4], + should_authenticate: bool, + ) -> CEncryptionRequest<'_> { + self.key_store + .encryption_request("", verification_token, should_authenticate) } - pub fn load_icon(path: &str) -> String { - let icon = match image::open(path).map_err(|e| panic!("error loading icon: {}", e)) { - Ok(icon) => icon, - Err(_) => return "".into(), - }; - let dimension = icon.dimensions(); - assert!(dimension.0 == 64, "Icon width must be 64"); - assert!(dimension.1 == 64, "Icon height must be 64"); - let mut image = Vec::with_capacity(64 * 64 * 4); - icon.write_to(&mut Cursor::new(&mut image), image::ImageFormat::Png) - .unwrap(); - let mut result = "data:image/png;base64,".to_owned(); - general_purpose::STANDARD.encode_string(image, &mut result); - result + pub fn decrypt(&self, data: &[u8]) -> Result, EncryptionError> { + self.key_store.decrypt(data) } - pub fn generate_keys() -> (RsaPublicKey, RsaPrivateKey) { - let mut rng = rand::thread_rng(); - - let priv_key = RsaPrivateKey::new(&mut rng, 1024).expect("failed to generate a key"); - let pub_key = RsaPublicKey::from(&priv_key); - (pub_key, priv_key) + pub fn digest_secret(&self, secret: &[u8]) -> String { + self.key_store.get_digest(secret) } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 4c4f514c2..58ce26082 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1,68 +1,86 @@ -use std::{ - collections::HashMap, - ops::DerefMut, - sync::{Arc, Mutex, MutexGuard}, -}; +use std::{collections::HashMap, sync::Arc}; pub mod player_chunker; +use crate::{ + client::Client, + entity::{player::Player, Entity}, +}; use mio::Token; use num_traits::ToPrimitive; +use parking_lot::Mutex; use pumpkin_config::BasicConfiguration; use pumpkin_core::math::vector2::Vector2; use pumpkin_entity::{entity_type::EntityType, EntityId}; use pumpkin_protocol::{ client::play::{ CChunkData, CGameEvent, CLogin, CPlayerAbilities, CPlayerInfoUpdate, CRemoveEntities, - CRemovePlayerInfo, CSetEntityMetadata, CSpawnEntity, Metadata, PlayerAction, + CRemovePlayerInfo, CSetEntityMetadata, CSpawnEntity, GameEvent, Metadata, PlayerAction, }, - uuid::UUID, ClientPacket, VarInt, }; use pumpkin_world::level::Level; use tokio::sync::mpsc; -use crate::{client::Client, entity::player::Player}; - +/// Represents a Minecraft world, containing entities, players, and the underlying level data. +/// +/// Each dimension (Overworld, Nether, End) typically has its own `World`. +/// +/// **Key Responsibilities:** +/// +/// - Manages the `Level` instance for handling chunk-related operations. +/// - Stores and tracks active `Player` entities within the world. +/// - Provides a central hub for interacting with the world's entities and environment. pub struct World { + /// The underlying level, responsible for chunk management and terrain generation. pub level: Arc>, - pub current_players: HashMap>>, - // entities, players... + /// A map of active players within the world, keyed by their unique token. + pub current_players: Arc>>>, + // TODO: entities } impl World { pub fn load(level: Level) -> Self { Self { level: Arc::new(Mutex::new(level)), - current_players: HashMap::new(), + current_players: Arc::new(Mutex::new(HashMap::new())), } } - /// Sends a Packet to all Players, Expect some players. Because we can't lock them twice - pub fn broadcast_packet

(&self, except: &[Token], packet: &P) + /// Broadcasts a packet to all connected players within the world. + /// + /// Sends the specified packet to every player currently logged in to the server. + /// + /// **Note:** This function acquires a lock on the `current_players` map, ensuring thread safety. + pub fn broadcast_packet_all

(&self, packet: &P) where P: ClientPacket, { - for (_, player) in self - .current_players - .iter() - .filter(|c| !except.contains(c.0)) - { - let mut player = player.lock().unwrap(); + let current_players = self.current_players.lock(); + for player in current_players.values() { player.client.send_packet(packet); } } - pub async fn spawn_player( - &mut self, - base_config: &BasicConfiguration, - player: Arc>, - ) { - let mut player = player.lock().unwrap(); - let player = player.deref_mut(); + /// Broadcasts a packet to all connected players within the world, excluding the specified players. + /// + /// Sends the specified packet to every player currently logged in to the server, excluding the players listed in the `except` parameter. + /// + /// **Note:** This function acquires a lock on the `current_players` map, ensuring thread safety. + pub fn broadcast_packet_expect

(&self, except: &[Token], packet: &P) + where + P: ClientPacket, + { + let current_players = self.current_players.lock(); + for (_, player) in current_players.iter().filter(|c| !except.contains(c.0)) { + player.client.send_packet(packet); + } + } + + pub async fn spawn_player(&self, base_config: &BasicConfiguration, player: Arc) { // This code follows the vanilla packet order let entity_id = player.entity_id(); - let gamemode = player.gamemode; + let gamemode = player.gamemode.load(); log::debug!("spawning player, entity id {}", entity_id); // login packet for our new player @@ -104,69 +122,54 @@ impl World { let gameprofile = &player.gameprofile; // first send info update to our new player, So he can see his Skin // also send his info to everyone else - player.client.send_packet(&CPlayerInfoUpdate::new( + self.broadcast_packet_all(&CPlayerInfoUpdate::new( 0x01 | 0x08, &[pumpkin_protocol::client::play::Player { uuid: gameprofile.id, actions: vec![ PlayerAction::AddPlayer { - name: gameprofile.name.clone(), - properties: gameprofile.properties.clone(), + name: &gameprofile.name, + properties: &gameprofile.properties, }, PlayerAction::UpdateListed(true), ], }], )); - self.broadcast_packet( - &[player.client.token], - &CPlayerInfoUpdate::new( - 0x01 | 0x08, - &[pumpkin_protocol::client::play::Player { - uuid: gameprofile.id, - actions: vec![ - PlayerAction::AddPlayer { - name: gameprofile.name.clone(), - properties: gameprofile.properties.clone(), - }, - PlayerAction::UpdateListed(true), - ], - }], - ), - ); // here we send all the infos of already joined players let mut entries = Vec::new(); - for (_, playerr) in self - .current_players - .iter() - .filter(|(c, _)| **c != player.client.token) { - let playerr = playerr.as_ref().lock().unwrap(); - let gameprofile = &playerr.gameprofile; - entries.push(pumpkin_protocol::client::play::Player { - uuid: gameprofile.id, - actions: vec![ - PlayerAction::AddPlayer { - name: gameprofile.name.clone(), - properties: gameprofile.properties.clone(), - }, - PlayerAction::UpdateListed(true), - ], - }) + let current_players = self.current_players.lock(); + for (_, playerr) in current_players + .iter() + .filter(|(c, _)| **c != player.client.token) + { + let gameprofile = &playerr.gameprofile; + entries.push(pumpkin_protocol::client::play::Player { + uuid: gameprofile.id, + actions: vec![ + PlayerAction::AddPlayer { + name: &gameprofile.name, + properties: &gameprofile.properties, + }, + PlayerAction::UpdateListed(true), + ], + }) + } + player + .client + .send_packet(&CPlayerInfoUpdate::new(0x01 | 0x08, &entries)); } - player - .client - .send_packet(&CPlayerInfoUpdate::new(0x01 | 0x08, &entries)); let gameprofile = &player.gameprofile; // spawn player for every client - self.broadcast_packet( + self.broadcast_packet_expect( &[player.client.token], // TODO: add velo &CSpawnEntity::new( entity_id.into(), - UUID(gameprofile.id), + gameprofile.id, (EntityType::Player as i32).into(), x, y, @@ -182,20 +185,20 @@ impl World { ); // spawn players for our client let token = player.client.token; - for (_, existing_player) in self.current_players.iter().filter(|c| c.0 != &token) { - let existing_player = existing_player.as_ref().lock().unwrap(); - let entity = &existing_player.entity; + for (_, existing_player) in self.current_players.lock().iter().filter(|c| c.0 != &token) { + let entity = &existing_player.living_entity.entity; + let pos = entity.pos.load(); let gameprofile = &existing_player.gameprofile; player.client.send_packet(&CSpawnEntity::new( existing_player.entity_id().into(), - UUID(gameprofile.id), + gameprofile.id, (EntityType::Player as i32).into(), - entity.pos.x, - entity.pos.y, - entity.pos.z, - entity.yaw, - entity.pitch, - entity.head_yaw, + pos.x, + pos.y, + pos.z, + entity.yaw.load(), + entity.pitch.load(), + entity.head_yaw.load(), 0.into(), 0.0, 0.0, @@ -204,37 +207,31 @@ impl World { } // entity meta data // set skin parts - if let Some(config) = player.client.config.as_ref() { + if let Some(config) = player.client.config.lock().as_ref() { let packet = CSetEntityMetadata::new( entity_id.into(), Metadata::new(17, VarInt(0), config.skin_parts), ); - player.client.send_packet(&packet); - self.broadcast_packet(&[player.client.token], &packet) + self.broadcast_packet_all(&packet) } - // Start waiting for level chunks - player.client.send_packet(&CGameEvent::new(13, 0.0)); + // Start waiting for level chunks, Sets the "Loading Terrain" screen + player + .client + .send_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)); - // Spawn in inital chunks - player_chunker::player_join(self, player).await; + // Spawn in initial chunks + player_chunker::player_join(self, player.clone()).await; } - async fn spawn_world_chunks( - &self, - client: &mut Client, - chunks: Vec>, - distance: i32, - ) { + async fn spawn_world_chunks(&self, client: &Client, chunks: Vec>, distance: i32) { let inst = std::time::Instant::now(); let (sender, mut chunk_receiver) = mpsc::channel(distance as usize); let level = self.level.clone(); - let closed = client.closed; + let closed = client.closed.load(std::sync::atomic::Ordering::Relaxed); let chunks = Arc::new(chunks); - tokio::task::spawn_blocking(move || { - level.lock().unwrap().fetch_chunks(&chunks, sender, closed) - }); + tokio::task::spawn_blocking(move || level.lock().fetch_chunks(&chunks, sender, closed)); while let Some(chunk_data) = chunk_receiver.recv().await { // dbg!(chunk_pos); @@ -255,42 +252,51 @@ impl World { len / (1024 * 1024) ); } - if !client.closed { + if !client.closed.load(std::sync::atomic::Ordering::Relaxed) { client.send_packet(&CChunkData(&chunk_data)); } } dbg!("DONE CHUNKS", inst.elapsed()); } - /// TODO: This definitly should be in world - pub fn get_by_entityid(&self, from: &Player, id: EntityId) -> Option> { - for (_, player) in self - .current_players - .iter() - .filter(|c| c.0 != &from.client.token) - { - let player = player.lock().unwrap(); + /// Gets a Player by entity id + pub fn get_player_by_entityid(&self, id: EntityId) -> Option> { + for player in self.current_players.lock().values() { if player.entity_id() == id { - return Some(player); + return Some(player.clone()); } } None } - pub fn add_player(&mut self, token: Token, player: Arc>) { - self.current_players.insert(token, player); + /// Gets a Player by name + pub fn get_player_by_name(&self, name: &str) -> Option> { + for player in self.current_players.lock().values() { + if player.gameprofile.name == name { + return Some(player.clone()); + } + } + None } - pub fn remove_player(&mut self, player: &Player) { - self.current_players.remove(&player.client.token).unwrap(); - // despawn the player - // todo: put this into the entitiy struct - let id = player.entity_id(); + pub fn add_player(&self, token: Token, player: Arc) { + self.current_players.lock().insert(token, player); + } + + pub fn remove_player(&self, player: &Player) { + self.current_players + .lock() + .remove(&player.client.token) + .unwrap(); let uuid = player.gameprofile.id; - self.broadcast_packet( + self.broadcast_packet_expect( &[player.client.token], - &CRemovePlayerInfo::new(1.into(), &[UUID(uuid)]), + &CRemovePlayerInfo::new(1.into(), &[uuid]), ); - self.broadcast_packet(&[player.client.token], &CRemoveEntities::new(&[id.into()])) + self.remove_entity(&player.living_entity.entity); + } + + pub fn remove_entity(&self, entity: &Entity) { + self.broadcast_packet_all(&CRemoveEntities::new(&[entity.entity_id.into()])) } } diff --git a/pumpkin/src/world/player_chunker.rs b/pumpkin/src/world/player_chunker.rs index a060dfbd0..2c6f1c7d9 100644 --- a/pumpkin/src/world/player_chunker.rs +++ b/pumpkin/src/world/player_chunker.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use pumpkin_config::BASIC_CONFIG; use pumpkin_core::math::{ get_section_cord, position::WorldPosition, vector2::Vector2, vector3::Vector3, @@ -5,29 +7,31 @@ use pumpkin_core::math::{ use pumpkin_protocol::client::play::{CCenterChunk, CUnloadChunk}; use pumpkin_world::cylindrical_chunk_iterator::Cylindrical; -use crate::entity::player::Player; +use crate::entity::{player::Player, Entity}; use super::World; fn get_view_distance(player: &Player) -> i8 { player .config + .lock() .view_distance .clamp(2, BASIC_CONFIG.view_distance as i8) } -pub async fn player_join(world: &World, player: &mut Player) { - let new_watched = chunk_section_from_pos(&player.entity.block_pos); - player.watched_section = new_watched; - let chunk_pos = player.entity.chunk_pos; +pub async fn player_join(world: &World, player: Arc) { + let new_watched = chunk_section_from_pos(&player.living_entity.entity.block_pos.load()); + player.watched_section.store(new_watched); + let watched_section = new_watched; + let chunk_pos = player.living_entity.entity.chunk_pos.load(); player.client.send_packet(&CCenterChunk { chunk_x: chunk_pos.x.into(), chunk_z: chunk_pos.z.into(), }); - let view_distance = get_view_distance(player) as i32; + let view_distance = get_view_distance(&player) as i32; dbg!(view_distance); let old_cylindrical = Cylindrical::new( - Vector2::new(player.watched_section.x, player.watched_section.z), + Vector2::new(watched_section.x, watched_section.z), view_distance, ); let new_cylindrical = Cylindrical::new(Vector2::new(chunk_pos.x, chunk_pos.z), view_distance); @@ -47,16 +51,16 @@ pub async fn player_join(world: &World, player: &mut Player) { ); if !loading_chunks.is_empty() { world - .spawn_world_chunks(&mut player.client, loading_chunks, view_distance) + .spawn_world_chunks(&player.client, loading_chunks, view_distance) .await; } } -pub async fn update_position(world: &World, player: &mut Player) { - let current_watched = player.watched_section; - let new_watched = chunk_section_from_pos(&player.entity.block_pos); +pub async fn update_position(entity: &Entity, player: &Player) { + let current_watched = player.watched_section.load(); + let new_watched = chunk_section_from_pos(&entity.block_pos.load()); if current_watched != new_watched { - let chunk_pos = player.entity.chunk_pos; + let chunk_pos = entity.chunk_pos.load(); player.client.send_packet(&CCenterChunk { chunk_x: chunk_pos.x.into(), chunk_z: chunk_pos.z.into(), @@ -64,12 +68,12 @@ pub async fn update_position(world: &World, player: &mut Player) { let view_distance = get_view_distance(player) as i32; let old_cylindrical = Cylindrical::new( - Vector2::new(player.watched_section.x, player.watched_section.z), + Vector2::new(current_watched.x, current_watched.z), view_distance, ); let new_cylindrical = Cylindrical::new(Vector2::new(chunk_pos.x, chunk_pos.z), view_distance); - player.watched_section = new_watched; + player.watched_section.store(new_watched); let mut loading_chunks = Vec::new(); Cylindrical::for_each_changed_chunk( old_cylindrical, @@ -85,8 +89,9 @@ pub async fn update_position(world: &World, player: &mut Player) { false, ); if !loading_chunks.is_empty() { - world - .spawn_world_chunks(&mut player.client, loading_chunks, view_distance) + entity + .world + .spawn_world_chunks(&player.client, loading_chunks, view_distance) .await; } }